Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,11 @@ agentcore # interactive TUI
│ │ ├── runtime # use the existing Runtime invoke experience
│ │ └── harness # use the existing Harness invoke experience
│ ├── status # inspect deployed project resources (TUI when run bare)
│ └── build # synthesize the project's CloudFormation templates
│ └── log
│ └── runtime # resolve a project Runtime and inspect its logs
│ ├── build # synthesize the project's CloudFormation templates
│ ├── log
│ │ └── runtime # resolve a project Runtime and inspect its logs
│ └── traces
│ └── runtime
└── config # read/write global config values
```

Expand Down Expand Up @@ -204,6 +206,21 @@ When the project declares exactly one Runtime, `--name` may be omitted. Use the
imperative `agentcore runtime logs --id <runtimeId>` command when addressing a
Runtime directly or working outside a project.

### Inspect project Runtime traces

Project tracing uses the same logical Runtime and deployment target resolution,
then lists or downloads traces from the resolved Runtime's deployment region:

```bash
agentcore project traces runtime list
agentcore project traces runtime list --name checkout --target production --since 30m
agentcore project traces runtime get <traceId> --name checkout --output trace.json
```

When the project declares exactly one Runtime, `--name` may be omitted. Use the
imperative `agentcore runtime traces` commands when addressing a Runtime by
physical ID or working outside a project.

### Examples

```bash
Expand Down Expand Up @@ -281,6 +298,10 @@ agentcore runtime logs --id <runtimeId> --since 2026-08-30T12:00:00Z --until now
agentcore runtime traces list --id <runtimeId> --since 30m
agentcore runtime traces get <traceId> --id <runtimeId> --output trace.json

# Resolve a project Runtime by logical name and deployment target
agentcore project traces runtime list --name checkout --target production --since 30m
agentcore project traces runtime get <traceId> --name checkout --output trace.json

# Inspect AgentCore Memories without project configuration or deployment
agentcore memory get --id <memoryId>
agentcore memory get --id <memoryId> --view without_decryption
Expand Down
2 changes: 2 additions & 0 deletions src/handlers/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { createAddProjectResourceHandler } from "./add";
import { createExportProjectResourceHandler } from "./export";
import { createProjectInvokeHandler } from "./invoke";
import { createProjectLogHandler } from "./log";
import { createProjectTracesHandler } from "./traces";

type ProjectHandlerConfig = {
core: Core;
Expand Down Expand Up @@ -101,6 +102,7 @@ export function createProjectHandler({ core, io }: ProjectHandlerConfig): Router
);
project.handler(createProjectInvokeHandler(core, io));
project.handler(createProjectLogHandler(core, io));
project.handler(createProjectTracesHandler(core, io));
// A bare `agentcore project status` in an interactive session opens the TUI
// linked-resources screen; any user-supplied flag, --json, or a non-TTY
// invocation keeps the headless JSON report (same dispatch shape as create).
Expand Down
11 changes: 11 additions & 0 deletions src/handlers/project/traces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { AppIO } from "../../../io";
import { withProject } from "../../../middleware";
import { Router } from "../../../router";
import type { Core } from "../../types";
import { createProjectRuntimeTracesHandler } from "./runtime";

export function createProjectTracesHandler(core: Core, io: AppIO): Router {
return new Router("traces", "inspect traces for resources in the current project")
.use(withProject({ projectManager: core.projectManager }))
.handler(createProjectRuntimeTracesHandler(core, io));
}
199 changes: 199 additions & 0 deletions src/handlers/project/traces/runtime.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { afterEach, describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { GetTraceQuery, ListTracesQuery, LogSource } from "../../../core/observability/index";
import type { ProjectBackend, ResolveDeployedResourcesBackendInput } from "../../../core/project";
import { ProjectSpecSchema } from "../../../projectSchemas/project";
import {
createSilentLogger,
TestCoreClient,
TestGlobalConfigAccessor,
testIO,
} from "../../../testing";
import { createRootHandler } from "../../index";

const originalCwd = process.cwd();
const temporaryDirectories: string[] = [];
const DEFAULT_TARGET = {
name: "default",
account: "111122223333",
region: "eu-west-1",
} as const;
const PRODUCTION_TARGET = {
name: "production",
account: "111122223333",
region: "ap-southeast-2",
} as const;
const RUNTIMES = [
{
name: "checkout",
build: "CodeZip",
entrypoint: "main.py",
codeLocation: "app/checkout",
runtimeVersion: "PYTHON_3_14",
},
{
name: "inventory",
build: "CodeZip",
entrypoint: "main.py",
codeLocation: "app/inventory",
runtimeVersion: "PYTHON_3_14",
},
] as const;

afterEach(async () => {
process.chdir(originalCwd);
await Promise.all(
temporaryDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});

async function inProject(
runtimes: readonly unknown[],
targets = [DEFAULT_TARGET, PRODUCTION_TARGET],
) {
const root = await mkdtemp(join(tmpdir(), "agentcore-project-traces-"));
temporaryDirectories.push(root);
await mkdir(join(root, "agentcore"), { recursive: true });
const spec = ProjectSpecSchema.parse({
name: "orders",
version: 1,
runtimes,
});
await writeFile(join(root, "agentcore", "agentcore.json"), JSON.stringify(spec));
await writeFile(join(root, "agentcore", "aws-targets.json"), JSON.stringify(targets));
process.chdir(root);
return root;
}

function backend() {
const calls: ResolveDeployedResourcesBackendInput[] = [];
const value: ProjectBackend = {
async *build() {},
async *deploy() {
yield* [];
return { outputs: {} };
},
async resolveDeployedResources(project, input) {
calls.push(input);
return project.spec.runtimes.map(({ name }) => ({
resourceType: "runtime" as const,
name,
id: `${name}-AbCdEf1234`,
target: input.target,
}));
},
async resolveProjectResources() {
throw new Error("project traces resolve deployed resources, not project resources");
},
};
return { calls, value };
}

function command(projectBackend: ProjectBackend) {
const core = new TestCoreClient({ backends: { CDK: projectBackend } });
const io = testIO();
const root = createRootHandler(core, {
io: io.io,
logger: createSilentLogger(),
globalConfigAccessor: new TestGlobalConfigAccessor(),
});
return {
core,
io,
run: (args: string[]) =>
root.route([
"bun",
"agentcore",
"project",
"traces",
"runtime",
...args,
"--region",
"us-east-1",
]),
};
}

describe("project traces runtime", () => {
test("resolves the only logical Runtime and lists traces in the target region", async () => {
await inProject([RUNTIMES[0]]);
const resolved = backend();
const subject = command(resolved.value);
subject.core.observability.traceSummaries = [
{
traceId: "abc123",
timestamp: "1709391000000",
sessionId: "session-1",
},
];

await subject.run(["list", "--since", "1h"]);

expect(resolved.calls).toEqual([{ target: DEFAULT_TARGET }]);
const call = subject.core.observability.calls[0]!;
expect(call.method).toBe("listTraces");
expect(call.args[0] as LogSource).toEqual({
logGroupName: "/aws/bedrock-agentcore/runtimes/checkout-AbCdEf1234-DEFAULT",
});
expect(call.args[1] as ListTracesQuery).toMatchObject({ limit: 20 });
expect(call.args[2]).toEqual({ region: DEFAULT_TARGET.region, endpointUrl: undefined });
expect(subject.io.stdout()).toContain("abc123");
});

test("selects a named Runtime, deployment target, and endpoint qualifier", async () => {
await inProject(RUNTIMES);
const resolved = backend();
const subject = command(resolved.value);

await subject.run([
"list",
"--name",
"inventory",
"--target",
"production",
"--qualifier",
"BLUE",
"--since",
"1h",
"--limit",
"5",
]);

expect(resolved.calls).toEqual([{ target: PRODUCTION_TARGET }]);
const call = subject.core.observability.calls[0]!;
expect(call.args[0]).toEqual({
logGroupName: "/aws/bedrock-agentcore/runtimes/inventory-AbCdEf1234-BLUE",
});
expect(call.args[1] as ListTracesQuery).toMatchObject({ limit: 5 });
expect(call.args[2]).toEqual({ region: PRODUCTION_TARGET.region, endpointUrl: undefined });
});

test("downloads a trace from the resolved Runtime", async () => {
await inProject([RUNTIMES[0]]);
const resolved = backend();
const subject = command(resolved.value);
subject.core.observability.traceRecords = [
{ "@timestamp": "2026-09-10 12:00:00.000", "@message": { body: "hello" } },
];

await subject.run(["get", "abc123def456", "--output", "traces/trace.json"]);

const call = subject.core.observability.calls[0]!;
expect(call.method).toBe("getTrace");
expect(call.args[0]).toEqual({
logGroupName: "/aws/bedrock-agentcore/runtimes/checkout-AbCdEf1234-DEFAULT",
});
expect(call.args[1] as GetTraceQuery).toMatchObject({ traceId: "abc123def456" });
expect(call.args[2]).toEqual({ region: DEFAULT_TARGET.region, endpointUrl: undefined });

const output = join(process.cwd(), "traces", "trace.json");
expect(subject.io.stdout()).toBe(output);
expect(JSON.parse(await readFile(output, "utf8"))).toEqual(
subject.core.observability.traceRecords,
);
});
});
61 changes: 61 additions & 0 deletions src/handlers/project/traces/runtime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import z from "zod";
import { DEFAULT_ENDPOINT_QUALIFIER, runtimeLogGroup } from "../../../core/observability/index";
import type { AppIO } from "../../../io";
import { DEFAULT_TARGET_NAME } from "../../../projectSchemas/aws-targets";
import { flag, ProjectKey, Router, type Context } from "../../../router";
import { createGetTraceHandler, createListTracesHandler } from "../../observability/traces";
import { resolveTraceOutputPath } from "../../observability/traceOutputPath";
import type { ResourceFlagValues } from "../../observability/types";
import type { Core } from "../../types";
import { coreOptsFromCtx } from "../../utils";
import { selectProjectResource } from "../selection";

const projectRuntimeFlags = [
flag("name", "the logical project Runtime name", z.string().optional()),
flag("target", "project deployment target", z.string().min(1).default(DEFAULT_TARGET_NAME)),
flag("qualifier", "the Runtime endpoint qualifier", z.string().min(1).optional()),
] as const;

type ProjectRuntimeFlagValues = ResourceFlagValues<typeof projectRuntimeFlags>;

async function resolveProjectRuntime(core: Core, ctx: Context, flags: ProjectRuntimeFlagValues) {
const project = ctx.require(ProjectKey);
const name = selectProjectResource(project, "runtime", flags.name, "inspect traces for");
const deployed = await core.projectManager.resolveDeployedResource(project, {
target: flags.target,
resourceType: "runtime",
name,
});

return {
source: {
logGroupName: runtimeLogGroup(deployed.id, flags.qualifier ?? DEFAULT_ENDPOINT_QUALIFIER),
},
options: {
...coreOptsFromCtx(ctx),
region: deployed.target.region,
},
};
}

export function createProjectRuntimeTracesHandler(core: Core, io: AppIO): Router {
const list = createListTracesHandler(io, {
description: "list a Runtime's recent traces",
flags: projectRuntimeFlags,
read: async (ctx, flags, query, signal) => {
const { source, options } = await resolveProjectRuntime(core, ctx, flags);
return core.observability.listTraces(source, query, options, signal);
},
});
const get = createGetTraceHandler(io, {
description: "download a trace's log records to a JSON file",
flags: projectRuntimeFlags,
read: async (ctx, flags, query, signal) => {
const { source, options } = await resolveProjectRuntime(core, ctx, flags);
return core.observability.getTrace(source, query, options, signal);
},
resolveOutputPath: (_ctx, _flags, request) => resolveTraceOutputPath(request),
});

return new Router("runtime", "inspect a Runtime's traces").handler(list).handler(get);
}
Loading