Skip to content

Commit 186f66b

Browse files
authored
feat(runtime): add Runtime invoke command (#1820)
* feat(runtime): add invoke transport * feat(runtime): add headless invoke command * docs(runtime): document invoke workflows * fix(runtime): distinguish streaming invoke responses * refactor(errors): adopt shared CLI error types * refactor(runtime): dispatch invoke output by mode * refactor(runtime): split invoke transports * refactor(io): require stdin for source resolution * fix(runtime): align validation with rebased errors * fix(runtime): address invoke review feedback
1 parent 9505185 commit 186f66b

23 files changed

Lines changed: 2898 additions & 156 deletions

README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ agentcore # interactive TUI
5858
├── runtime # inspect deployed AgentCore Runtimes
5959
│ ├── get # fetch a Runtime by id
6060
│ ├── list # list Runtimes (server-side paginated)
61+
│ ├── invoke # invoke a Runtime
6162
│ ├── version
6263
│ │ ├── get # get a specific Runtime version
6364
│ │ └── list # list a Runtime's versions
@@ -154,6 +155,85 @@ Source-aware values: any field flag documented as such accepts the value inline,
154155
`file://` convention). A command reads stdin from at most one flag. For example,
155156
`--instructions file://order-quality.txt` or `--instructions -`.
156157

158+
### Invoke a Runtime
159+
160+
Runtime invocation accepts inline, file, or stdin payload bytes:
161+
162+
```bash
163+
# Inline
164+
agentcore runtime invoke \
165+
--id <runtimeId> \
166+
--payload '{"action":"status"}' \
167+
--content-type application/json \
168+
--accept text/event-stream
169+
170+
# File
171+
agentcore runtime invoke --id <runtimeId> --payload file://request.json
172+
173+
# stdin
174+
cat request.json | agentcore runtime invoke --id <runtimeId> --payload -
175+
```
176+
177+
CUSTOM_JWT Runtimes require `--bearer-token`. The token accepts the same inline,
178+
`file://`, or stdin sources as the payload; payload and token cannot both read
179+
stdin.
180+
181+
```bash
182+
agentcore runtime invoke \
183+
--id <runtimeId> \
184+
--payload file://request.json \
185+
--bearer-token file://$HOME/.config/agentcore/runtime-token
186+
```
187+
188+
For MCP Runtimes, initialize first, then pass the returned Runtime and MCP
189+
session IDs to later methods. MCP requests accept both JSON and SSE responses.
190+
191+
```bash
192+
agentcore runtime invoke \
193+
--id <runtimeId> \
194+
--payload '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"agentcore-cli","version":"1"}}}' \
195+
--accept 'application/json, text/event-stream' \
196+
--mcp-protocol-version 2025-03-26 \
197+
--mcp-method initialize
198+
199+
agentcore runtime invoke \
200+
--id <runtimeId> \
201+
--payload '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
202+
--accept 'application/json, text/event-stream' \
203+
--session-id <returnedRuntimeSessionId> \
204+
--mcp-session-id <returnedMcpSessionId> \
205+
--mcp-protocol-version 2025-03-26 \
206+
--mcp-method tools/list
207+
```
208+
209+
Raw stdout always streams exact response bytes as they arrive, regardless of
210+
content type. `--output-file` streams the same bytes directly to disk. Binary or
211+
unknown responses require `--output-file` or `--json` when stdout is a terminal.
212+
Response metadata is written to stderr.
213+
214+
`--json` buffers the complete response, including streaming representations, and
215+
emits one metadata envelope without interpreting the customer body. If a raw or
216+
file response fails, bytes already written remain available and the stderr
217+
summary reports `complete=false`. A failed JSON response emits no partial
218+
envelope.
219+
220+
```bash
221+
agentcore runtime invoke \
222+
--id <runtimeId> \
223+
--payload file://request.bin \
224+
--content-type application/octet-stream \
225+
--accept application/octet-stream \
226+
--output-file response.bin
227+
228+
agentcore runtime invoke --id <runtimeId> --payload '{"action":"status"}' --json
229+
# {"statusCode":200,"contentType":"application/json","bodyEncoding":"utf8","body":"{\"ok\":true}","complete":true}
230+
```
231+
232+
Runtime Invoke accepts Runtime IDs from the current account only. It does not
233+
accept ARNs, `--version`, `--interactive`, cross-account targets, or custom
234+
request paths. All requests use the Runtime `/invocations` route, including MCP
235+
Runtimes.
236+
157237
Bare Runtime branches and leaves require a TTY on stdin and stdout. Supplying
158238
operation flags runs the command headlessly, and `--json` always suppresses TUI
159239
rendering.

bun.lock

Lines changed: 125 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"typescript": "^5"
5050
},
5151
"dependencies": {
52-
"@aws-sdk/client-bedrock-agentcore": "^3.1079.0",
52+
"@aws-sdk/client-bedrock-agentcore": "^3.1092.0",
5353
"@aws-sdk/client-bedrock-agentcore-control": "^3.1079.0",
5454
"@aws-sdk/client-iam": "^3.1080.0",
5555
"@inkui-cli/data-table": "^0.2.0",

src/core/abortable.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Relays `source` while making an in-flight read reject as soon as `signal` aborts.
3+
*
4+
* Aborts use `signal.reason` or an `AbortError` fallback. The rejection remains
5+
* observed when no read is pending, and source cleanup is fire-and-forget because
6+
* a stalled stream may also stall `iterator.return()`.
7+
*/
8+
export async function* abortable<T>(
9+
source: AsyncIterable<T>,
10+
signal: AbortSignal,
11+
): AsyncGenerator<T> {
12+
let abort = () => {};
13+
const aborted = new Promise<never>((_, reject) => {
14+
abort = () =>
15+
reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError"));
16+
if (signal.aborted) abort();
17+
else signal.addEventListener("abort", abort, { once: true });
18+
});
19+
aborted.catch(() => {});
20+
21+
const iterator = source[Symbol.asyncIterator]();
22+
try {
23+
for (;;) {
24+
const result = await Promise.race([iterator.next(), aborted]);
25+
if (result.done) return;
26+
yield result.value;
27+
}
28+
} finally {
29+
signal.removeEventListener("abort", abort);
30+
void iterator.return?.()?.catch(() => {});
31+
}
32+
}

0 commit comments

Comments
 (0)