Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ed56385
feat(runtime): add interactive invoke console
aidandaly24 Jul 27, 2026
00c2e0c
refactor(runtime): clarify TUI target switching
aidandaly24 Jul 28, 2026
36c421a
fix(runtime): preserve TUI source boundary after rebase
aidandaly24 Jul 29, 2026
5c967e8
fix(runtime): submit TUI payloads with enter
aidandaly24 Jul 29, 2026
468b4ad
fix(runtime): stabilize invoke TUI after rebase
aidandaly24 Jul 30, 2026
49c87d5
fix(runtime): use shared TUI environment error
aidandaly24 Jul 30, 2026
52c9c35
refactor(runtime): clarify target option reset
aidandaly24 Jul 30, 2026
fd721e8
feat(runtime): redesign invoke request options
aidandaly24 Jul 30, 2026
9b2a515
test(runtime): streamline request options coverage
aidandaly24 Jul 31, 2026
24147a6
feat(runtime): float invoke request options
aidandaly24 Jul 31, 2026
189c88d
feat(runtime): enlarge request options modal
aidandaly24 Jul 31, 2026
00fef68
feat(runtime): add JSON payload templates
aidandaly24 Jul 31, 2026
df19c7b
fix(runtime): improve request options editing
aidandaly24 Jul 31, 2026
3bf185b
fix(runtime): center request options modal
aidandaly24 Jul 31, 2026
30afaed
fix(runtime): match request options background
aidandaly24 Jul 31, 2026
8bfbb7d
fix(runtime): preserve blank payload editor rows
aidandaly24 Jul 31, 2026
0bd5189
refactor(runtime): simplify invoke TUI to JSON console
aidandaly24 Jul 31, 2026
80bcdd5
feat(runtime): preserve invoke context in TUI
aidandaly24 Jul 31, 2026
826ff46
fix(runtime): clarify invoke session status
aidandaly24 Aug 3, 2026
cb25573
fix(runtime): use centralized environment error
aidandaly24 Aug 3, 2026
8bad46a
test(runtime): cover invoke console edge cases
aidandaly24 Aug 3, 2026
e44bf32
refactor(runtime): remove obsolete select customization
aidandaly24 Aug 3, 2026
d3d6305
refactor(runtime): remove request options residue
aidandaly24 Aug 3, 2026
620579f
fix(runtime): preserve invoke session boundaries
aidandaly24 Aug 3, 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
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ agentcore # interactive TUI
├── runtime # inspect deployed AgentCore Runtimes
│ ├── get # fetch a Runtime by id
│ ├── list # list Runtimes (server-side paginated)
│ ├── invoke # invoke a Runtime
│ ├── invoke # invoke a Runtime headlessly or in a persistent console
│ ├── version
│ │ ├── get # get a specific Runtime version
│ │ └── list # list a Runtime's versions
Expand Down Expand Up @@ -182,7 +182,7 @@ Source-aware values: any field flag documented as such accepts the value inline,

### Invoke a Runtime

Runtime invocation accepts inline, file, or stdin payload bytes:
Headless invocation accepts inline, file, or stdin payload bytes:

```bash
# Inline
Expand Down Expand Up @@ -254,14 +254,35 @@ agentcore runtime invoke --id <runtimeId> --payload '{"action":"status"}' --json
# {"statusCode":200,"contentType":"application/json","bodyEncoding":"utf8","body":"{\"ok\":true}","complete":true}
```

Without `--payload`, Runtime Invoke opens a persistent JSON console for repeated
requests. The console sends inline `application/json` payloads and renders each
response according to its returned content type. Bare invoke opens the Runtime
and endpoint pickers; `--id` skips the Runtime picker, and `--id` plus
`--qualifier` opens the console directly. `--session-id` resumes that Runtime
session in the console. `--user-id`, `--header`, and `--bearer-token` seed
request context that persists across sends and endpoint changes within that
Runtime. The console never displays their values, and switching Runtimes clears
them. Interactive bearer tokens may be inline or `file://` sources, but not
stdin.

| Shortcut | Action |
| ------------- | -------------------------------------------- |
| `Enter` | Send the JSON request |
| `Shift+Enter` | Insert a newline |
| `Ctrl+T` | Change Runtime or endpoint |
| `Ctrl+V` | Toggle raw and pretty completed JSON |
| `Esc` | Interrupt an active request or navigate back |
| `↑`/`↓` | Scroll response history |

Runtime Invoke accepts Runtime IDs from the current account only. It does not
accept ARNs, `--version`, `--interactive`, cross-account targets, or custom
request paths. All requests use the Runtime `/invocations` route, including MCP
Runtimes.

Bare Runtime and Memory branches and leaves require a TTY on stdin and stdout. Supplying
operation flags runs the command headlessly, and `--json` always suppresses TUI
rendering.
Bare Runtime and Memory branches and leaves require a TTY on stdin and stdout.
For Runtime Invoke, supplying a payload or headless-only request or output flags
runs headlessly; `--session-id` can instead seed the persistent console.
`--json` always suppresses TUI rendering.

```bash
agentcore runtime
Expand Down
13 changes: 13 additions & 0 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { RuntimeListVersionsScreen } from "../handlers/runtime/version/list/scre
import { MemoryScreen } from "../handlers/memory/screen.tsx";
import { MemoryGetJsonScreen, MemoryGetScreen } from "../handlers/memory/get/screen.tsx";
import { MemoryListScreen } from "../handlers/memory/list/screen.tsx";
import { RuntimeInvokeScreen } from "../handlers/runtime/invoke/screen.tsx";
import { RootScreen, HelpScreen } from "../handlers/screen.tsx";
import type { Context } from "../router";

Expand Down Expand Up @@ -275,6 +276,18 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/memory/get/:memoryId/json"
element={<MemoryGetJsonScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/invoke"
element={<RuntimeInvokeScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/invoke/:runtimeId"
element={<RuntimeInvokeScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/runtime/invoke/:runtimeId/:qualifier"
element={<RuntimeInvokeScreen ctx={ctx} core={core} />}
/>
<Route path="*" element={<HelpScreen ctx={ctx} core={core} />} />
</Routes>
</MemoryRouter>
Expand Down
4 changes: 3 additions & 1 deletion src/components/RuntimeEndpointPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface RuntimeEndpointPickerProps extends ScreenProps {
breadcrumb: string[];
description?: string;
onSelect: (qualifier: string) => void;
onEscape?: () => void;
}

export function RuntimeEndpointPicker({
Expand All @@ -51,10 +52,11 @@ export function RuntimeEndpointPicker({
breadcrumb,
description,
onSelect,
onEscape,
}: RuntimeEndpointPickerProps) {
const opts = coreOptsFromCtx(ctx);
const navigate = useNavigate();
const goBack = () => navigate(-1);
const goBack = onEscape ?? (() => navigate(-1));

return (
<PaginatedTablePicker
Expand Down
4 changes: 3 additions & 1 deletion src/components/RuntimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface RuntimePickerProps extends ScreenProps {
breadcrumb: string[];
description?: string;
onSelect: (runtimeId: string) => void;
onEscape?: () => void;
}

export function RuntimePicker({
Expand All @@ -60,10 +61,11 @@ export function RuntimePicker({
breadcrumb,
description,
onSelect,
onEscape,
}: RuntimePickerProps) {
const opts = coreOptsFromCtx(ctx);
const navigate = useNavigate();
const goBack = () => navigate("/" + breadcrumb.slice(0, -1).join("/"));
const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/")));

return (
<PaginatedTablePicker
Expand Down
116 changes: 116 additions & 0 deletions src/handlers/runtime/invoke/RuntimePayloadInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { useState } from "react";
import { Box, Text, useInput } from "ink";
import { darkTheme } from "../../../components/ui/_core.js";

const theme = darkTheme;
const PREVIEW_LINES = 4;
const PLACEHOLDER = "Enter JSON payload";

interface RuntimePayloadInputProps {
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
submitDisabled?: boolean;
}

function Cursor({ character }: { character: string }) {
return (
<Text color={theme.colors.focus} inverse>
{character}
</Text>
);
}

export function RuntimePayloadInput({
value,
onChange,
onSubmit,
submitDisabled = false,
}: RuntimePayloadInputProps) {
const [rawCursor, setRawCursor] = useState(value.length);
const cursor = Math.min(rawCursor, value.length);

useInput((input, key) => {
if (key.leftArrow) {
setRawCursor(Math.max(0, cursor - 1));
return;
}
if (key.rightArrow) {
setRawCursor(Math.min(value.length, cursor + 1));
return;
}
if (key.upArrow || key.downArrow) return;

if (key.backspace || key.delete) {
if (cursor === 0) return;
onChange(value.slice(0, cursor - 1) + value.slice(cursor));
setRawCursor(cursor - 1);
return;
}

if (key.return) {
if (key.shift || key.meta) {
onChange(value.slice(0, cursor) + "\n" + value.slice(cursor));
setRawCursor(cursor + 1);
} else if (!submitDisabled) {
onSubmit();
}
return;
}
if (key.ctrl || key.meta || key.escape || input === "") return;

const next = input.replace(/\r/g, "\n");
onChange(value.slice(0, cursor) + next + value.slice(cursor));
setRawCursor(cursor + next.length);
});

if (value === "") {
return (
<Box flexDirection="column">
<Text color={theme.colors.muted}>JSON payload</Text>
<Box>
<Cursor character={PLACEHOLDER[0]!} />
<Text color={theme.colors.muted}>{PLACEHOLDER.slice(1)}</Text>
</Box>
</Box>
);
}

const lines = value.split("\n");
const beforeCursor = value.slice(0, cursor);
const cursorLine = beforeCursor.split("\n").length - 1;
const lastNewline = beforeCursor.lastIndexOf("\n");
const cursorColumn = cursor - lastNewline - 1;
const start = Math.max(0, cursorLine - PREVIEW_LINES + 1);
const visible = lines.slice(start, start + PREVIEW_LINES);

return (
<Box flexDirection="column">
<Text color={theme.colors.muted}>JSON payload</Text>
{visible.map((line, index) => {
const lineIndex = start + index;
const prefix = index === 0 && start > 0 ? "… " : "";
if (lineIndex !== cursorLine) {
return (
<Box key={lineIndex}>
<Text color={theme.colors.border}>{prefix}</Text>
<Text>{line || " "}</Text>
</Box>
);
}

const before = line.slice(0, cursorColumn);
const at = line[cursorColumn] ?? " ";
const after = line.slice(cursorColumn + 1);
return (
<Box key={lineIndex}>
<Text color={theme.colors.border}>{prefix}</Text>
{before ? <Text>{before}</Text> : null}
<Cursor character={at} />
{after ? <Text>{after}</Text> : null}
</Box>
);
})}
</Box>
);
}
64 changes: 59 additions & 5 deletions src/handlers/runtime/invoke/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import z from "zod";
import { InputValidationError, RuntimeInvokeInterruptedError } from "../../../errors";
import { createHandler, flag } from "../../../router";
import {
InputValidationError,
InvalidEnvironmentError,
RuntimeInvokeInterruptedError,
} from "../../../errors";
import { createHandler, flag, PathKey } from "../../../router";
import type { AppIO } from "../../../io";
import type { Core } from "../../types";
import { coreOptsFromCtx } from "../../utils";
import { JsonKey } from "../../keys";
import { ExitCode } from "../../../runnable";
import { renderTuiAt } from "../../../tui";
import {
normalizeRuntimeInvokeRequest,
parseRuntimeInvokeHeaders,
resolveRuntimeInvokeSources,
resolveRuntimeInvokeTuiBearerToken,
runtimeIdSchema,
} from "./request";
import { writeRuntimeInvokeResponse } from "./response";
import { RuntimeInvokeLaunchContextKey } from "./launchContext";

export const createInvokeRuntimeHandler = (core: Core, io: AppIO) =>
createHandler({
Expand Down Expand Up @@ -55,9 +62,56 @@ export const createInvokeRuntimeHandler = (core: Core, io: AppIO) =>
});
}
if (flags.payload === undefined) {
throw new InputValidationError("required option '--payload <payload>' not specified", {
exitCode: ExitCode.USAGE,
});
const hasHeadlessOnlyFlag = Object.entries(flags).some(
([name, value]) =>
![
"id",
"qualifier",
"payload",
"session-id",
"user-id",
"header",
"bearer-token",
].includes(name) && value !== undefined,
);
if (ctx.require(JsonKey) || hasHeadlessOnlyFlag) {
throw new InputValidationError("required option '--payload <payload>' not specified", {
exitCode: ExitCode.USAGE,
});
}
let path = `${ctx.require(PathKey)}/${encodeURIComponent(flags.id)}`;
if (flags.qualifier !== undefined) {
path += `/${encodeURIComponent(flags.qualifier)}`;
}
const applicationHeaders = parseRuntimeInvokeHeaders(flags.header);
const bearerToken = await resolveRuntimeInvokeTuiBearerToken(
flags["bearer-token"],
io.stdin,
);
const launchContext = {
runtimeId: flags.id,
runtimeSessionId: flags["session-id"],
runtimeUserId: flags["user-id"],
applicationHeaders,
bearerToken,
};
try {
await renderTuiAt(
path,
ctx.withValue(RuntimeInvokeLaunchContextKey, launchContext),
core,
io,
);
} catch (error) {
if (error instanceof InvalidEnvironmentError) {
throw new InputValidationError(error.message, {
cause: error,
exitCode: ExitCode.USAGE,
});
}
throw error;
}
return;
}

const jsonOutput = ctx.require(JsonKey);
Expand Down
Loading
Loading