Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0b90d8a
feat(project): resolve deployed invoke resources
aidandaly24 Aug 25, 2026
f3ed995
refactor(invoke): share Runtime and Harness operations
aidandaly24 Aug 25, 2026
403728e
feat(runtime): add prompt input mode to invoke TUI
aidandaly24 Aug 25, 2026
6ff80ee
feat(project): add project-aware invoke command
aidandaly24 Aug 25, 2026
cbc986b
docs(project): document project-aware invoke
aidandaly24 Aug 25, 2026
46e16bf
feat(runtime): parse Strands prompt responses
aidandaly24 Aug 26, 2026
c90f57e
feat(runtime): render Strands responses in prompt mode
aidandaly24 Aug 26, 2026
9562eda
feat(project): render Strands runtime responses
aidandaly24 Aug 26, 2026
33c37e3
fix(runtime): handle Strands stream failures
aidandaly24 Aug 26, 2026
ce91be0
fix(project): preserve Runtime wire output modes
aidandaly24 Aug 26, 2026
8d81c24
fix(tui): hide unsupported project invoke route
aidandaly24 Aug 26, 2026
865ea25
docs(project): document Runtime output modes
aidandaly24 Aug 26, 2026
4b54aa9
fix(runtime): support caller-specific binary guidance
aidandaly24 Aug 27, 2026
a8d5596
refactor(runtime): target the Strands template event contract
aidandaly24 Aug 27, 2026
a325b23
refactor(project): remove invoke output file mode
aidandaly24 Aug 27, 2026
6091ba3
feat(project): add shared agent event parser
aidandaly24 Aug 27, 2026
6926857
refactor(dev): use shared agent event parser
aidandaly24 Aug 27, 2026
9fed35b
refactor(invoke): use shared agent event parser
aidandaly24 Aug 27, 2026
16c7c81
refactor(project): resolve resources from deployed stack state
aidandaly24 Aug 27, 2026
5ab6a1d
refactor(project): remove duplicate deployment reader
aidandaly24 Aug 27, 2026
5063cf9
test(project): seed deployed state through shared helper
aidandaly24 Aug 27, 2026
84cde7a
docs(templates): document project invoke contract
aidandaly24 Aug 27, 2026
cb5b431
fix(project): register invoke under project
aidandaly24 Aug 27, 2026
e9d1a2a
test(tui): remove obsolete root invoke assertion
aidandaly24 Aug 27, 2026
fff864d
docs(project): use project invoke command
aidandaly24 Aug 27, 2026
0428cc1
fix(project): separate runtime response summary
aidandaly24 Aug 27, 2026
bda4741
fix(tui): separate request and response blocks
aidandaly24 Aug 27, 2026
3e978e8
feat(project): add invoke resource picker
aidandaly24 Aug 27, 2026
21a8ce0
feat(project): open picker for bare invoke
aidandaly24 Aug 27, 2026
ab85490
docs(project): document bare invoke picker
aidandaly24 Aug 27, 2026
d8ba4c6
fix(project): clarify invoke picker name column
aidandaly24 Aug 27, 2026
6317ba9
refactor(invoke): support embedded invoke consoles
aidandaly24 Aug 27, 2026
323dba6
fix(project): keep invoke picker in one TUI session
aidandaly24 Aug 27, 2026
08732fe
feat(project): add compatible invoke shorthand
aidandaly24 Aug 27, 2026
a5c1aa8
docs(project): document invoke shorthand
aidandaly24 Aug 27, 2026
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Identity, and Gateway branches and leaves open their interactive flows.

```
agentcore # interactive TUI
├── invoke # project-aware invoke shorthand (requires a project)
├── harness # manage agentcore harnesses
│ ├── create # create a harness (auto-provisions a role if none given)
│ ├── get # fetch a harness by id
Expand Down Expand Up @@ -104,6 +105,15 @@ agentcore # interactive TUI
│ ├── get # get an evaluator by id (type-agnostic)
│ ├── list # list evaluators (server-side paginated)
│ └── delete # delete an evaluator by id
├── project # manage an AgentCore project
│ ├── create # create a project
│ ├── add # add project resources
│ ├── remove # remove project resources
│ ├── dev # run the project locally
│ ├── deploy # deploy the project
│ ├── invoke # invoke a project Runtime or Harness
│ ├── status # inspect deployed project resources
│ └── build # synthesize deployable artifacts
└── config # read/write global config values
```

Expand All @@ -116,6 +126,42 @@ Global flags (declared at the root, available on every command):
| `--debug` | Debug logging. |
| `--endpoint-url` | Override the service endpoint URL (e.g. for testing against a stub). |

### Invoke a project resource

From anywhere inside an AgentCore project, invoke a deployed Runtime or Harness
by its logical project name:

```bash
# A project with exactly one Runtime or Harness needs no selector.
agentcore project invoke "Summarize this repository."

# Select explicitly when the project has multiple invokable resources.
agentcore project invoke --runtime checkout "Check order 123."
agentcore project invoke --harness support "Help with my account."

# Select another deployment target.
agentcore project invoke --target staging --runtime checkout "Run a smoke test."

# Preserve the Runtime wire response in a JSON envelope.
agentcore project invoke --runtime checkout "Check order 123." --json

# Omit content to open the selected resource's interactive console.
agentcore project invoke --runtime checkout

# Omit content and selectors to choose from the project's Runtimes and Harnesses.
agentcore project invoke

# The project-aware shorthand has the same behavior inside a project.
agentcore invoke
```

Project Runtime content is sent as `{"prompt": content}` with
`application/json`. `--target` defaults to `default` and supplies the AWS
account and region used for resource lookup and invocation. Default output
streams assistant text from supported Strands SSE responses and passes
unsupported SSE responses through unchanged. `--json` preserves the exact wire
response instead.

### Examples

```bash
Expand Down
6 changes: 6 additions & 0 deletions src/assets/templates/hello-world-python-container/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ Environment variables for local development go in `agentcore/.env.local`
```bash
agentcore project deploy
```

Invoke the deployed Runtime:

```bash
agentcore project invoke "Hello!"
```
8 changes: 5 additions & 3 deletions src/assets/templates/hello-world-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ curl -X POST http://localhost:8080/invocations \
-d '{"prompt": "Hello!"}'
```

<!-- TODO: replace the uv run + curl instructions with `agentcore dev` and
`agentcore invoke` once those commands are available. -->

## Build your agent

Start in `main.py`:
Expand Down Expand Up @@ -58,3 +55,8 @@ for multi-agent patterns, MCP tools, and model configuration.

Deploy from the project root with the AgentCore CLI; the CDK app under
`agentcore/cdk` provisions the Runtime that hosts this agent.

```bash
agentcore project deploy
agentcore project invoke "Hello!"
```
9 changes: 9 additions & 0 deletions src/assets/templates/strands-http-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,12 @@ Command Prompt, or `.\.venv\Scripts\activate.ps1` in Windows PowerShell.
# Deployment

After providing credentials, `agentcore project deploy` will deploy your project into Amazon Bedrock AgentCore.

Invoke the deployed Runtime from the project root:

```bash
agentcore project invoke "Hello!"
```

The CLI sends `{"prompt": content}` and streams assistant text from the Strands response. Use `--json` to preserve
the exact wire response.
5 changes: 5 additions & 0 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import { GatewayRuleScreen } from "../handlers/gateway/rule/screen.tsx";
import { GatewayRuleListScreen } from "../handlers/gateway/rule/list/screen.tsx";
import { GatewayRuleGetScreen } from "../handlers/gateway/rule/get/screen.tsx";
import { GatewayInvokeScreen } from "../handlers/gateway/invoke/screen.tsx";
import { ProjectInvokePickerScreen } from "../handlers/project/invoke/screen.tsx";
import { RootScreen, HelpScreen } from "../handlers/screen.tsx";
import type { Context } from "../router";

Expand Down Expand Up @@ -140,6 +141,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route path="agentcore" element={<RootScreen ctx={ctx} core={core} />} />
<Route
path="agentcore/project/invoke"
element={<ProjectInvokePickerScreen ctx={ctx} core={core} />}
/>
<Route path="agentcore/harness" element={<HarnessScreen ctx={ctx} core={core} />} />
{/* Bare `get` (no id) has nothing to show — send the user to the list. */}
<Route
Expand Down
7 changes: 7 additions & 0 deletions src/components/RouterScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ describe("menu rendering", () => {
r.unmount();
});

test("does not offer the project-only invoke alias in the root menu", async () => {
const r = renderScreen("/agentcore");
await waitForText(r.lastFrame, "harness");
expect(r.lastFrame()).not.toMatch(/^[❯ ]*invoke\s/m);
r.unmount();
});

test("renders the harness subcommands when mounted at the harness path", async () => {
const r = renderScreen("/agentcore/harness");
await waitForText(r.lastFrame, "list");
Expand Down
24 changes: 12 additions & 12 deletions src/core/dev/inspector/invocations.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { afterEach, describe, expect, test } from "bun:test";
import { type HttpRequestHandler, startHttpServer } from "../../../io/httpServer";
import { parseAgentEvent } from "./invocations";
import { ServerFarm, fakeSupervisor, post, runningAgent } from "./testkit";
import type { InspectorDeps } from "./types";

Expand Down Expand Up @@ -63,17 +62,6 @@ describe("upstream connection failures", () => {
});
});

describe("parseAgentEvent drops non-renderable frames", () => {
test.each([
{ name: "a JSON primitive that is not text", data: JSON.stringify(42) },
{ name: "a blank error field", data: JSON.stringify({ error: "" }) },
{ name: "a blank text field", data: JSON.stringify({ text: "" }) },
{ name: "an empty non-JSON token", data: "" },
])("returns null for $name", ({ data }) => {
expect(parseAgentEvent(data)).toBeNull();
});
});

describe("HTTP agent SSE normalization", () => {
test.each([
{ name: "a bedrock text event", frame: JSON.stringify({ text: "hello" }), expected: "hello" },
Expand All @@ -97,6 +85,18 @@ describe("HTTP agent SSE normalization", () => {
expect(await response.text()).toBe(`data: ${JSON.stringify({ error: "boom" })}\n\n`);
});

test.each([
{
name: "a recognized control event",
frame: JSON.stringify({ event: { messageStart: { role: "assistant" } } }),
},
{ name: "an unsupported event", frame: JSON.stringify({ progress: 1 }) },
])("does not emit $name", async ({ frame }) => {
const { url } = await inspectorFor(sseAgent([frame]));
const response = await post(url, "/invocations", { agentName: "orders", prompt: "hi" });
expect(await response.text()).toBe("");
});

test("passes a non-SSE response body through untouched", async () => {
const { url } = await inspectorFor(() => ({
status: 200,
Expand Down
27 changes: 4 additions & 23 deletions src/core/dev/inspector/invocations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
sseEvent,
} from "./respond";
import type { InspectorDeps } from "./types";
import { parseAgentEvent } from "../../project/agentEventParser";

export async function handleInvocations(
deps: InspectorDeps,
Expand Down Expand Up @@ -89,32 +90,12 @@ async function* transformAgentSse(
stream: AsyncIterable<Uint8Array>,
): AsyncGenerator<Uint8Array, void> {
for await (const data of sseData(stream)) {
const payload = parseAgentEvent(data);
if (payload !== null) yield sseEvent(payload);
const event = parseAgentEvent(data);
if (event.kind === "text") yield sseEvent(event.text);
if (event.kind === "error") yield sseEvent({ error: event.message });
}
}

// Handles bedrock {text}, {error}, ConverseStream contentBlockDelta, bare JSON string, and non-JSON tokens.
export function parseAgentEvent(data: string): string | { error: string } | null {
try {
const parsed: unknown = JSON.parse(data);
if (typeof parsed === "string") return parsed || null;
if (parsed && typeof parsed === "object") {
if ("error" in parsed) {
const error = String((parsed as { error: unknown }).error);
return error ? { error } : null;
}
if ("text" in parsed) return String((parsed as { text: unknown }).text) || null;
const event = (parsed as { event?: { contentBlockDelta?: { delta?: { text?: string } } } })
.event;
return event?.contentBlockDelta?.delta?.text || null;
}
} catch {
return data || null;
}
return null;
}

// A2A agents speak JSON-RPC at their root path, so {prompt} becomes a message/stream call reduced to text frames.
async function invokeA2aAgent(
port: number,
Expand Down
69 changes: 69 additions & 0 deletions src/core/project/agentEventParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, test } from "bun:test";
import { parseAgentEvent, type AgentEvent } from "./agentEventParser";

describe("parseAgentEvent", () => {
test.each([
{
name: "JSON string",
data: JSON.stringify("string token"),
expected: { kind: "text", text: "string token" },
},
{
name: "text object",
data: JSON.stringify({ text: "text token" }),
expected: { kind: "text", text: "text token" },
},
{
name: "Converse text delta",
data: JSON.stringify({
event: { contentBlockDelta: { delta: { text: "delta token" } } },
}),
expected: { kind: "text", text: "delta token" },
},
{
name: "non-JSON token",
data: "raw token",
expected: { kind: "text", text: "raw token" },
},
] satisfies { name: string; data: string; expected: AgentEvent }[])(
"extracts a $name",
({ data, expected }) => {
expect(parseAgentEvent(data)).toEqual(expected);
},
);

test("extracts an error object", () => {
expect(parseAgentEvent(JSON.stringify({ error: "model denied" }))).toEqual({
kind: "error",
message: "model denied",
});
});

test.each([
{
name: "message start",
data: JSON.stringify({ event: { messageStart: { role: "assistant" } } }),
},
{
name: "message stop",
data: JSON.stringify({ event: { messageStop: { stopReason: "end_turn" } } }),
},
{ name: "empty JSON string", data: JSON.stringify("") },
{ name: "blank text", data: JSON.stringify({ text: "" }) },
{ name: "blank error", data: JSON.stringify({ error: "" }) },
{ name: "empty non-JSON token", data: "" },
])("identifies $name as control", ({ data }) => {
expect(parseAgentEvent(data)).toEqual({ kind: "control" });
});

test.each([
{ name: "unknown object", data: JSON.stringify({ progress: 1 }) },
{ name: "JSON number", data: JSON.stringify(42) },
{ name: "JSON boolean", data: JSON.stringify(true) },
{ name: "JSON null", data: JSON.stringify(null) },
{ name: "JSON array", data: JSON.stringify(["token"]) },
{ name: "malformed event", data: JSON.stringify({ event: "not-an-object" }) },
])("identifies an unsupported $name", ({ data }) => {
expect(parseAgentEvent(data)).toEqual({ kind: "unsupported" });
});
});
46 changes: 46 additions & 0 deletions src/core/project/agentEventParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export type AgentEvent =
| { kind: "text"; text: string }
| { kind: "error"; message: string }
| { kind: "control" }
| { kind: "unsupported" };

function asRecord(value: unknown): Record<string, unknown> | undefined {
return value !== null && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
}

function textEvent(value: unknown): AgentEvent {
const text = String(value);
return text ? { kind: "text", text } : { kind: "control" };
}

export function parseAgentEvent(data: string): AgentEvent {
let parsed: unknown;
try {
parsed = JSON.parse(data);
} catch {
return data ? { kind: "text", text: data } : { kind: "control" };
}

if (typeof parsed === "string") return textEvent(parsed);

const root = asRecord(parsed);
if (!root) return { kind: "unsupported" };

if ("error" in root) {
const message = String(root.error);
return message ? { kind: "error", message } : { kind: "control" };
}
if ("text" in root) return textEvent(root.text);

if ("event" in root) {
const event = asRecord(root.event);
if (!event) return { kind: "unsupported" };
const contentBlockDelta = asRecord(event.contentBlockDelta);
const delta = asRecord(contentBlockDelta?.delta);
return typeof delta?.text === "string" ? textEvent(delta.text) : { kind: "control" };
}

return { kind: "unsupported" };
}
Loading
Loading