Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
399d557
feat(project): resolve deployed invoke resources
aidandaly24 Aug 25, 2026
732aba6
refactor(invoke): share Runtime and Harness operations
aidandaly24 Aug 25, 2026
13f70ab
feat(runtime): add prompt input mode to invoke TUI
aidandaly24 Aug 25, 2026
a8117b4
feat(project): add project-aware invoke command
aidandaly24 Aug 25, 2026
b162159
docs(project): document project-aware invoke
aidandaly24 Aug 25, 2026
f8d1be5
feat(runtime): parse Strands prompt responses
aidandaly24 Aug 26, 2026
029a3ff
feat(runtime): render Strands responses in prompt mode
aidandaly24 Aug 26, 2026
7b40916
feat(project): render Strands runtime responses
aidandaly24 Aug 26, 2026
ff043fc
fix(runtime): handle Strands stream failures
aidandaly24 Aug 26, 2026
d1c0a84
fix(project): preserve Runtime wire output modes
aidandaly24 Aug 26, 2026
9744136
fix(tui): hide unsupported project invoke route
aidandaly24 Aug 26, 2026
180d7e8
docs(project): document Runtime output modes
aidandaly24 Aug 26, 2026
9502ef4
fix(runtime): support caller-specific binary guidance
aidandaly24 Aug 27, 2026
5b08392
refactor(runtime): target the Strands template event contract
aidandaly24 Aug 27, 2026
2f4fd7d
refactor(project): remove invoke output file mode
aidandaly24 Aug 27, 2026
914e461
feat(project): add shared agent event parser
aidandaly24 Aug 27, 2026
b83d5fb
refactor(dev): use shared agent event parser
aidandaly24 Aug 27, 2026
0832776
refactor(invoke): use shared agent event parser
aidandaly24 Aug 27, 2026
9bdfd1d
refactor(project): resolve resources from deployed stack state
aidandaly24 Aug 27, 2026
4f2ff89
refactor(project): remove duplicate deployment reader
aidandaly24 Aug 27, 2026
13df912
test(project): seed deployed state through shared helper
aidandaly24 Aug 27, 2026
6bab26a
docs(templates): document project invoke contract
aidandaly24 Aug 27, 2026
dbfad63
fix(project): register invoke under project
aidandaly24 Aug 27, 2026
b586b26
test(tui): remove obsolete root invoke assertion
aidandaly24 Aug 27, 2026
d1453fe
docs(project): use project invoke command
aidandaly24 Aug 27, 2026
2b637c1
fix(project): separate runtime response summary
aidandaly24 Aug 27, 2026
fd2c08b
fix(tui): separate request and response blocks
aidandaly24 Aug 27, 2026
8b95ef4
feat(project): add invoke resource picker
aidandaly24 Aug 27, 2026
28b3c85
feat(project): open picker for bare invoke
aidandaly24 Aug 27, 2026
775cd3c
docs(project): document bare invoke picker
aidandaly24 Aug 27, 2026
ea53880
fix(project): clarify invoke picker name column
aidandaly24 Aug 27, 2026
748accd
refactor(invoke): support embedded invoke consoles
aidandaly24 Aug 27, 2026
cd2ff53
fix(project): keep invoke picker in one TUI session
aidandaly24 Aug 27, 2026
fc586b6
feat(project): add compatible invoke shorthand
aidandaly24 Aug 27, 2026
e017ca5
docs(project): document invoke shorthand
aidandaly24 Aug 27, 2026
a88ca85
feat(tui): auto-select sole picker result
aidandaly24 Aug 28, 2026
9c8a161
fix(project): discover runtime endpoints before invoke
aidandaly24 Aug 28, 2026
817db86
test(invoke): localize agent event coverage
aidandaly24 Aug 28, 2026
a4eb2c8
test(project): tighten invoke integration coverage
aidandaly24 Aug 28, 2026
e57ea9c
feat(errors): model Runtime agent response failures
aidandaly24 Aug 28, 2026
ac38790
fix(runtime): classify structured agent errors
aidandaly24 Aug 28, 2026
2e6be46
fix(project): surface Runtime agent errors
aidandaly24 Aug 28, 2026
aa4108a
refactor(dev): restore local agent event parsing
aidandaly24 Aug 28, 2026
33a500d
fix(project): reject unknown agent event envelopes
aidandaly24 Aug 28, 2026
bb1a439
fix(runtime): preserve SSE event semantics
aidandaly24 Aug 28, 2026
54fc894
refactor(project): align invoke launch context naming
aidandaly24 Aug 28, 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.
32 changes: 28 additions & 4 deletions src/components/PaginatedTablePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { Text, useInput } from "ink";
import { useEffect, useRef } from "react";
import { Layout } from "./Layout";
import { usePagedList } from "./usePagedList";
import { darkTheme } from "./ui/_core.js";
Expand Down Expand Up @@ -27,6 +28,7 @@ export interface PaginatedTablePickerProps<TItem, TRow extends Record<string, un
emptyMessage: string;
emptyPageMessage: string;
maxPageSize?: number;
autoSelectSingle?: boolean;
}

export function PaginatedTablePicker<TItem, TRow extends Record<string, unknown>>({
Expand All @@ -45,6 +47,7 @@ export function PaginatedTablePicker<TItem, TRow extends Record<string, unknown>
emptyMessage,
emptyPageMessage,
maxPageSize,
autoSelectSingle = false,
}: PaginatedTablePickerProps<TItem, TRow>) {
const paging = usePagedList(maxPageSize);
const list = useQuery({
Expand All @@ -57,6 +60,27 @@ export function PaginatedTablePicker<TItem, TRow extends Record<string, unknown>
const pageTransition = list.isFetching && !list.isPending;
const mappedRows = (list.data?.items ?? []).map(toRow);
const rows = sortRows ? sortRows(mappedRows) : mappedRows;
const autoSelectIdentity = JSON.stringify(queryKey);
const lastAutoSelection = useRef<string | undefined>(undefined);
const autoSelectValue =
autoSelectSingle &&
!list.isPending &&
!list.isError &&
!list.isFetching &&
paging.pageIndex === 0 &&
nextToken === undefined &&
rows.length === 1
? getValue(rows[0]!)
: undefined;
const autoSelecting =
autoSelectValue !== undefined &&
lastAutoSelection.current !== `${autoSelectIdentity}:${autoSelectValue}`;

useEffect(() => {
if (!autoSelecting || autoSelectValue === undefined) return;
lastAutoSelection.current = `${autoSelectIdentity}:${autoSelectValue}`;
onSelect(autoSelectValue);
}, [autoSelectIdentity, autoSelectValue, autoSelecting, onSelect]);

useInput(
(input, key) => {
Expand All @@ -70,15 +94,15 @@ export function PaginatedTablePicker<TItem, TRow extends Record<string, unknown>
}
if (input === "r" && list.isError) void list.refetch();
},
{ isActive: list.isPending || list.isError || pageTransition },
{ isActive: list.isPending || list.isError || pageTransition || autoSelecting },
);

return (
<Layout
breadcrumb={breadcrumb}
description={description}
keyHints={[
...(!list.isPending && !list.isError && !pageTransition
...(!list.isPending && !list.isError && !pageTransition && !autoSelecting
? [
{ key: "↑↓/jk", label: "navigate" },
...(paginated ? [{ key: "←→/hl", label: "page" }] : []),
Expand All @@ -92,8 +116,8 @@ export function PaginatedTablePicker<TItem, TRow extends Record<string, unknown>
{ key: "ctl+c", label: "quit" },
]}
>
{list.isPending ? (
<Spinner label={loadingMessage} />
{list.isPending || autoSelecting ? (
<Spinner label={list.isPending ? loadingMessage : "Opening…"} />
) : list.isError ? (
<Text color="red">{errorMessage(list.error as Error)}</Text>
) : (
Expand Down
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
3 changes: 3 additions & 0 deletions src/components/RuntimeEndpointPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface RuntimeEndpointPickerProps extends ScreenProps {
description?: string;
onSelect: (qualifier: string) => void;
onEscape?: () => void;
autoSelectSingle?: boolean;
}

export function RuntimeEndpointPicker({
Expand All @@ -53,6 +54,7 @@ export function RuntimeEndpointPicker({
description,
onSelect,
onEscape,
autoSelectSingle,
}: RuntimeEndpointPickerProps) {
const opts = coreOptsFromCtx(ctx);
const navigate = useNavigate();
Expand All @@ -79,6 +81,7 @@ export function RuntimeEndpointPicker({
errorMessage={(error) => `Error loading endpoints for Runtime ${runtimeId}: ${error.message}`}
emptyMessage="This Runtime has no endpoints."
emptyPageMessage={`No endpoints on this page for Runtime ${runtimeId}.`}
autoSelectSingle={autoSelectSingle}
/>
);
}
93 changes: 93 additions & 0 deletions src/core/project/agentEventParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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: "content block start",
data: JSON.stringify({
event: { contentBlockStart: { start: { toolUse: { name: "weather" } } } },
}),
},
{
name: "non-text content block delta",
data: JSON.stringify({
event: { contentBlockDelta: { delta: { toolUse: { input: "{}" } } } },
}),
},
{
name: "content block stop",
data: JSON.stringify({ event: { contentBlockStop: { contentBlockIndex: 0 } } }),
},
{
name: "metadata",
data: JSON.stringify({ event: { metadata: { usage: { totalTokens: 1 } } } }),
},
{ 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" }) },
{
name: "unknown event envelope",
data: JSON.stringify({ event: { customDelta: { text: "must survive" } } }),
},
])("identifies an unsupported $name", ({ data }) => {
expect(parseAgentEvent(data)).toEqual({ kind: "unsupported" });
});
});
Loading
Loading