@agent-compose/sdk 0.5.2 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -27,7 +27,7 @@ export type { Processor, ProcessorContext, ProcessorVerdict, ToolCall, } from ".
27
27
  export type { AgentMessage, AgentMessageInit, AgentMessageText, AgentMessageThinking, AgentMessageToolUse, AgentMessageToolResult, AgentMessageDone, AgentMessageError, AgentMessageUsage, AgentStatus, } from "./types/protocol.js";
28
28
  export type { SandboxProvider, DesktopSandboxProvider, } from "./types/sandbox.js";
29
29
  export { AgentComposeClient } from "./client.js";
30
- export type { RegisterResult, RegisterWorkflowInput, RuntimeSourceInput, InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, ListSnapshotsOptions, TemplateRow, ListTemplatesOptions, CreateFactoryInput, UpdateFactoryInput, SecretOptions, SetSecretResult, SecretListEntry, CreateApiKeyInput, StreamRunLogsOptions, EventSubjectType, EventRow, ReportEventInput, ListEventsOptions, ListEventsResult, RunLogLine, ListRunLogsOptions, RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry, SnapshotListResponse, ApiKey, ApiKeyCreated, UsageRollupRow, UsageResponse, CancelRunResponse, RequestAgentPauseOptions, RequestAgentPauseResponse, SendAgentMessageOptions, SendAgentMessageResponse, AnswerSteerOptions, } from "./client.js";
30
+ export type { RegisterResult, RegisterWorkflowInput, RuntimeSourceInput, InvokeWorkflowOptions, InvokeAndWaitOptions, InvokeResult, ListSnapshotsOptions, TemplateRow, ListTemplatesOptions, CreateFactoryInput, UpdateFactoryInput, SecretOptions, SetSecretResult, SecretListEntry, CreateApiKeyInput, StreamRunLogsOptions, EventSubjectType, EventRow, ReportEventInput, ListEventsOptions, ListEventsResult, RunLogLine, ListRunLogsOptions, RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry, SnapshotListResponse, ApiKey, ApiKeyCreated, UsageRollupRow, UsageResponse, CancelRunResponse, RequestAgentPauseOptions, RequestAgentPauseResponse, SendAgentMessageOptions, SendAgentMessageResponse, AnswerSteerOptions, ResumePauseOptions, ResumePauseResponse, ResumePauseSuccess, ResumePausePending, ResumePauseActor, } from "./client.js";
31
31
  export { parseSseStream } from "./sse.js";
32
32
  export { AgentComposeError } from "./errors.js";
33
33
  export { formatError } from "./utils/errors.js";
@@ -37,8 +37,15 @@ export { AgentStatusSchema } from "./utils/schemas.js";
37
37
  export { createClaudeRuntime, ClaudeRunner } from "./runtimes/claude.js";
38
38
  export type { ClaudeRuntimeConfig } from "./runtimes/claude.js";
39
39
  export { default as claudeRuntime } from "./runtimes/claude.js";
40
- export { createVercelRuntime, VercelRunner } from "./runtimes/vercel.js";
41
- export type { VercelRuntimeConfig } from "./runtimes/vercel.js";
40
+ export { createVercelRuntime, VercelRunner, listVercelRuntimeModels } from "./runtimes/vercel.js";
41
+ export type { VercelRuntimeConfig, VercelRuntimeModel } from "./runtimes/vercel.js";
42
+ export type { GatewayModelId } from "ai";
43
+ export { createCodexRuntime } from "./runtimes/codex.js";
44
+ export type { CodexRuntimeConfig } from "./runtimes/codex.js";
45
+ export { default as codexRuntime } from "./runtimes/codex.js";
46
+ export { createAmpRuntime } from "./runtimes/amp.js";
47
+ export type { AmpRuntimeConfig } from "./runtimes/amp.js";
48
+ export { default as ampRuntime } from "./runtimes/amp.js";
42
49
  export { bashTool, codingTools, editTool, readTool, writeTool } from "./tools/index.js";
43
50
  export type { CodingTool } from "./tools/index.js";
44
51
  export type { RunEvent } from "./types/events.js";
package/dist/index.js CHANGED
@@ -18,7 +18,7 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/runtimes/vercel.ts
21
- import { streamText, stepCountIs, tool } from "ai";
21
+ import { streamText, stepCountIs, tool, gateway } from "ai";
22
22
 
23
23
  // src/types/runtime.ts
24
24
  function defineRuntime(pkg) {
@@ -387,6 +387,8 @@ class VercelRunner {
387
387
  options;
388
388
  config;
389
389
  supportsToolCallProcessor = true;
390
+ kind;
391
+ model;
390
392
  tools;
391
393
  messages = [];
392
394
  constructor(sandbox, options, config) {
@@ -394,6 +396,8 @@ class VercelRunner {
394
396
  this.options = options;
395
397
  this.config = config;
396
398
  this.tools = config.tools ?? codingTools;
399
+ this.kind = config.kind;
400
+ this.model = config.modelId;
397
401
  }
398
402
  async gateToolCall(call, ctx) {
399
403
  const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
@@ -511,6 +515,10 @@ function createVercelRuntime(config) {
511
515
  create: (sandbox, opts) => new VercelRunner(sandbox, opts, config)
512
516
  });
513
517
  }
518
+ async function listVercelRuntimeModels() {
519
+ const { models } = await gateway.getAvailableModels();
520
+ return models.filter((m) => m.modelType == null || m.modelType === "language").map((m) => ({ id: m.id, name: m.name })).sort((a, b) => a.id.localeCompare(b.id));
521
+ }
514
522
  // src/types/workflow.ts
515
523
  import { z as z3 } from "zod";
516
524
 
@@ -2368,6 +2376,239 @@ function createClaudeRuntime(config = {}) {
2368
2376
  });
2369
2377
  }
2370
2378
  var claude_default = createClaudeRuntime();
2379
+ // src/runtimes/_cli-agent.ts
2380
+ function now3() {
2381
+ return new Date().toISOString();
2382
+ }
2383
+ function shellQuote(value) {
2384
+ return `'${value.replace(/'/g, `'\\''`)}'`;
2385
+ }
2386
+
2387
+ class CliAgentRunner {
2388
+ sandbox;
2389
+ options;
2390
+ spec;
2391
+ configModel;
2392
+ kind;
2393
+ constructor(sandbox, options, spec, configModel) {
2394
+ this.sandbox = sandbox;
2395
+ this.options = options;
2396
+ this.spec = spec;
2397
+ this.configModel = configModel;
2398
+ this.kind = spec.kind;
2399
+ }
2400
+ get model() {
2401
+ return this.configModel ?? this.options.model ?? this.spec.defaultModel;
2402
+ }
2403
+ async* sendMessage(opts) {
2404
+ const promptPath = `/tmp/ac-${this.spec.kind}-${this.options.agentId ?? "agent"}-${opts.iteration ?? 0}.in`;
2405
+ await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
2406
+ const cmd = this.spec.buildCommand({
2407
+ promptPath,
2408
+ sessionId: opts.sessionId,
2409
+ model: this.model,
2410
+ cwd: this.options.cwd
2411
+ });
2412
+ const lines = new AsyncQueue;
2413
+ let buf = "";
2414
+ const onStdout = (data) => {
2415
+ buf += data;
2416
+ let nl;
2417
+ while ((nl = buf.indexOf(`
2418
+ `)) >= 0) {
2419
+ const line = buf.slice(0, nl).trim();
2420
+ buf = buf.slice(nl + 1);
2421
+ if (line)
2422
+ lines.push(line);
2423
+ }
2424
+ };
2425
+ const runPromise = this.sandbox.commands.run(cmd, {
2426
+ ...this.options.cwd ? { cwd: this.options.cwd } : {},
2427
+ onStdout
2428
+ }).then((res) => {
2429
+ const tail = buf.trim();
2430
+ if (tail)
2431
+ lines.push(tail);
2432
+ lines.close();
2433
+ return res;
2434
+ }, (err) => {
2435
+ lines.close();
2436
+ throw err;
2437
+ });
2438
+ yield { type: "init", sessionId: opts.sessionId ?? "", timestamp: now3() };
2439
+ let sessionId = opts.sessionId;
2440
+ let sawError = false;
2441
+ try {
2442
+ for await (const line of lines) {
2443
+ let parsed;
2444
+ try {
2445
+ parsed = JSON.parse(line);
2446
+ } catch {
2447
+ continue;
2448
+ }
2449
+ const sid = this.spec.extractSessionId(parsed);
2450
+ if (sid)
2451
+ sessionId = sid;
2452
+ for (const msg of this.spec.mapEvent(parsed)) {
2453
+ if (msg.type === "error")
2454
+ sawError = true;
2455
+ yield msg;
2456
+ }
2457
+ }
2458
+ const res = await runPromise;
2459
+ if (res.exitCode !== 0 && !sawError) {
2460
+ const tail = (res.stderr ?? "").slice(-2000);
2461
+ yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp: now3() };
2462
+ return;
2463
+ }
2464
+ if (!sawError)
2465
+ yield { type: "done", sessionId: sessionId ?? "", timestamp: now3() };
2466
+ } catch (err) {
2467
+ yield { type: "error", text: formatError(err), timestamp: now3() };
2468
+ }
2469
+ }
2470
+ }
2471
+ function createCliAgentRuntime(spec, configModel) {
2472
+ return defineRuntime({
2473
+ create: (sandbox, opts) => new CliAgentRunner(sandbox, opts, spec, configModel)
2474
+ });
2475
+ }
2476
+
2477
+ // src/runtimes/codex.ts
2478
+ function now4() {
2479
+ return new Date().toISOString();
2480
+ }
2481
+ var codexSpec = {
2482
+ kind: "codex",
2483
+ authEnv: "CODEX_API_KEY",
2484
+ promptPayload: (prompt) => prompt,
2485
+ buildCommand: ({ promptPath, sessionId, model, cwd }) => {
2486
+ const flags = [
2487
+ "--json",
2488
+ "--skip-git-repo-check",
2489
+ "--dangerously-bypass-approvals-and-sandbox",
2490
+ ...model ? ["-m", shellQuote(model)] : [],
2491
+ ...cwd ? ["-C", shellQuote(cwd)] : []
2492
+ ].join(" ");
2493
+ const exec = sessionId ? `codex exec resume ${shellQuote(sessionId)} ${flags}` : `codex exec ${flags}`;
2494
+ return `${exec} - < ${shellQuote(promptPath)}`;
2495
+ },
2496
+ extractSessionId: (p) => p.type === "thread.started" && typeof p.thread_id === "string" ? p.thread_id : undefined,
2497
+ mapEvent: (p) => {
2498
+ const ts = now4();
2499
+ switch (p.type) {
2500
+ case "item.started":
2501
+ case "item.completed": {
2502
+ const item = p.item;
2503
+ if (!item)
2504
+ return [];
2505
+ const itype = String(item.type ?? "");
2506
+ if (itype === "agent_message") {
2507
+ return p.type === "item.completed" ? [{ type: "text", text: String(item.text ?? ""), timestamp: ts }] : [];
2508
+ }
2509
+ if (itype === "reasoning") {
2510
+ return p.type === "item.completed" ? [{ type: "thinking", text: String(item.text ?? ""), timestamp: ts }] : [];
2511
+ }
2512
+ if (itype === "command_execution") {
2513
+ const id = String(item.id ?? "");
2514
+ if (p.type === "item.started") {
2515
+ return [{ type: "tool_use", toolName: "shell", toolInput: { command: String(item.command ?? "") }, toolUseId: id, timestamp: ts }];
2516
+ }
2517
+ const failed = item.status === "failed" || typeof item.exit_code === "number" && item.exit_code !== 0;
2518
+ return [{ type: "tool_result", toolUseId: id, output: String(item.aggregated_output ?? item.output ?? ""), isError: failed, timestamp: ts }];
2519
+ }
2520
+ if (p.type === "item.completed") {
2521
+ return [{ type: "tool_use", toolName: itype || "item", toolInput: item, toolUseId: String(item.id ?? ""), timestamp: ts }];
2522
+ }
2523
+ return [];
2524
+ }
2525
+ case "turn.completed": {
2526
+ const u = p.usage;
2527
+ if (!u)
2528
+ return [];
2529
+ return [{
2530
+ type: "usage",
2531
+ inputTokens: u.input_tokens ?? 0,
2532
+ outputTokens: u.output_tokens ?? 0,
2533
+ cacheReadTokens: u.cached_input_tokens ?? 0,
2534
+ cacheCreationTokens: 0,
2535
+ durationMs: 0,
2536
+ numTurns: 1,
2537
+ timestamp: ts
2538
+ }];
2539
+ }
2540
+ case "turn.failed":
2541
+ case "error":
2542
+ return [{ type: "error", text: formatError(p.error ?? p.message ?? p), timestamp: ts }];
2543
+ default:
2544
+ return [];
2545
+ }
2546
+ }
2547
+ };
2548
+ function createCodexRuntime(config = {}) {
2549
+ return createCliAgentRuntime(codexSpec, config.model);
2550
+ }
2551
+ var codex_default = createCodexRuntime();
2552
+ // src/runtimes/amp.ts
2553
+ function now5() {
2554
+ return new Date().toISOString();
2555
+ }
2556
+ function blocks(msg) {
2557
+ const inner = msg.message?.content ?? msg.content ?? [];
2558
+ return inner;
2559
+ }
2560
+ var ampSpec = {
2561
+ kind: "amp",
2562
+ authEnv: "AMP_API_KEY",
2563
+ promptPayload: (prompt) => JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } }) + `
2564
+ `,
2565
+ buildCommand: ({ promptPath, sessionId }) => {
2566
+ const cont = sessionId ? `threads continue ${shellQuote(sessionId)} ` : "";
2567
+ return `amp ${cont}-x --stream-json --stream-json-input < ${shellQuote(promptPath)}`;
2568
+ },
2569
+ extractSessionId: (p) => typeof p.session_id === "string" ? p.session_id : undefined,
2570
+ mapEvent: (p) => {
2571
+ const ts = now5();
2572
+ if (p.type === "assistant") {
2573
+ return blocks(p).flatMap((b) => {
2574
+ if (b.type === "text")
2575
+ return [{ type: "text", text: String(b.text ?? ""), timestamp: ts }];
2576
+ if (b.type === "thinking")
2577
+ return [{ type: "thinking", text: String(b.thinking ?? ""), timestamp: ts }];
2578
+ if (b.type === "tool_use")
2579
+ return [{ type: "tool_use", toolName: String(b.name ?? ""), toolInput: b.input ?? {}, toolUseId: String(b.id ?? ""), timestamp: ts }];
2580
+ return [];
2581
+ });
2582
+ }
2583
+ if (p.type === "user") {
2584
+ return blocks(p).flatMap((b) => b.type === "tool_result" ? [{ type: "tool_result", toolUseId: String(b.tool_use_id ?? ""), output: typeof b.content === "string" ? b.content : JSON.stringify(b.content ?? ""), isError: Boolean(b.is_error), timestamp: ts }] : []);
2585
+ }
2586
+ if (p.type === "result") {
2587
+ const out = [];
2588
+ const u = p.usage;
2589
+ if (u)
2590
+ out.push({
2591
+ type: "usage",
2592
+ inputTokens: u.input_tokens ?? 0,
2593
+ outputTokens: u.output_tokens ?? 0,
2594
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
2595
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
2596
+ durationMs: Number(p.duration_ms ?? 0),
2597
+ numTurns: Number(p.num_turns ?? 0),
2598
+ timestamp: ts
2599
+ });
2600
+ if (p.is_error || p.subtype === "error") {
2601
+ out.push({ type: "error", text: formatError(p.result ?? p.error), timestamp: ts });
2602
+ }
2603
+ return out;
2604
+ }
2605
+ return [];
2606
+ }
2607
+ };
2608
+ function createAmpRuntime(config = {}) {
2609
+ return createCliAgentRuntime(ampSpec, config.model);
2610
+ }
2611
+ var amp_default = createAmpRuntime();
2371
2612
  // src/sandbox.ts
2372
2613
  import { promises as fs2 } from "node:fs";
2373
2614
  import { dirname as dirname2 } from "node:path";
@@ -3666,6 +3907,7 @@ export {
3666
3907
  parseAgentResponse,
3667
3908
  makeSandboxProvider,
3668
3909
  makeDesktopSandboxProvider,
3910
+ listVercelRuntimeModels,
3669
3911
  listOwnedSandboxes,
3670
3912
  killSandboxById,
3671
3913
  killAllSandboxes,
@@ -3683,8 +3925,11 @@ export {
3683
3925
  defineRuntime,
3684
3926
  createVercelRuntime,
3685
3927
  createSandbox,
3928
+ createCodexRuntime,
3686
3929
  createClaudeRuntime,
3930
+ createAmpRuntime,
3687
3931
  codingTools,
3932
+ codex_default as codexRuntime,
3688
3933
  claude_default as claudeRuntime,
3689
3934
  classifyError,
3690
3935
  bundleWorkflow,
@@ -3692,6 +3937,7 @@ export {
3692
3937
  buildInvokeChild,
3693
3938
  bashTool,
3694
3939
  assertDefaultExportIsDefineWorkflow,
3940
+ amp_default as ampRuntime,
3695
3941
  agentLoop,
3696
3942
  agent,
3697
3943
  WorkflowSourceValidationError,
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CLI-agent runtime base — drive an external agentic coding CLI inside the
3
+ * sandbox and map its JSON-Lines (JSONL) stream onto the `AgentMessage`
4
+ * contract.
5
+ *
6
+ * This is a different *mechanism* from the other runtimes: `claudeRuntime`
7
+ * drives the Anthropic Agent SDK and `vercelRuntime` drives the Vercel AI SDK,
8
+ * but a CLI-agent runtime spawns the provider's own CLI (`codex exec --json`,
9
+ * `amp -x --stream-json`) — the CLI brings its own agent loop + tools, and we
10
+ * only stream-parse the events it prints. Codex and Amp are the first two; this
11
+ * base is the shared machinery (spawn → line-buffer stdout → JSONL parse →
12
+ * AsyncQueue bridge → init/usage/done/error lifecycle), parameterised per-CLI
13
+ * by a `CliAgentSpec`.
14
+ *
15
+ * Requirements (per spec): the provider CLI is installed in the sandbox image,
16
+ * and the provider's API key is present in the sandbox environment (see each
17
+ * spec's `authEnv`). `commands.run` inherits the sandbox env, so secrets set via
18
+ * `agentc secrets set` are visible to the CLI.
19
+ *
20
+ * NOTE: the per-CLI command construction + resume flags + exact event shapes in
21
+ * the shipped specs are mapped from each tool's docs and have NOT been verified
22
+ * against a live CLI run — verify before relying on them in production.
23
+ */
24
+ import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvider } from "../index.js";
25
+ /** Single-quote a value for safe interpolation into a `sh -c` command line. */
26
+ export declare function shellQuote(value: string): string;
27
+ /** Per-CLI behaviour. The base owns the lifecycle (init/done/error) and the
28
+ * transport (spawn + JSONL parse); a spec owns the CLI-specific bits. */
29
+ export interface CliAgentSpec {
30
+ /** Runtime self-id surfaced on `agent.spawned` (dashboard runtime icon). */
31
+ kind: string;
32
+ /** Env var the CLI reads for auth — documentation only (must be set in the
33
+ * sandbox env via a workflow secret). */
34
+ authEnv: string;
35
+ /** Default model id when none is configured; omit to let the CLI choose. */
36
+ defaultModel?: string;
37
+ /** Serialise the user prompt into the bytes written to the prompt file —
38
+ * plain text for a CLI that reads the prompt from stdin (codex `-`), or a
39
+ * JSONL user message for a `--stream-json-input` CLI (amp). */
40
+ promptPayload(prompt: string): string;
41
+ /** Build the one-shot shell command for a turn. `promptPath` is a file in the
42
+ * sandbox holding `promptPayload(prompt)`; `sessionId` continues a thread. */
43
+ buildCommand(args: {
44
+ promptPath: string;
45
+ sessionId?: string;
46
+ model?: string;
47
+ cwd?: string;
48
+ }): string;
49
+ /** Map one parsed JSONL stdout event to `AgentMessage`s. The base emits
50
+ * `init`/`done`/`error` lifecycle itself, so a spec maps only content +
51
+ * usage (text / thinking / tool_use / tool_result / usage). */
52
+ mapEvent(parsed: Record<string, unknown>): AgentMessage[];
53
+ /** Pull a session/thread id out of a parsed event so the next turn can
54
+ * resume it (codex `thread.started.thread_id`, amp `session_id`). */
55
+ extractSessionId(parsed: Record<string, unknown>): string | undefined;
56
+ }
57
+ export declare class CliAgentRunner implements ModelExecutionContract {
58
+ private readonly sandbox;
59
+ private readonly options;
60
+ private readonly spec;
61
+ private readonly configModel?;
62
+ readonly kind: string;
63
+ constructor(sandbox: SandboxProvider, options: RuntimeOptions, spec: CliAgentSpec, configModel?: string | undefined);
64
+ get model(): string | undefined;
65
+ sendMessage(opts: {
66
+ prompt: string;
67
+ sessionId?: string;
68
+ iteration?: number;
69
+ signal?: AbortSignal;
70
+ }): AsyncGenerator<AgentMessage>;
71
+ }
72
+ export declare function createCliAgentRuntime(spec: CliAgentSpec, configModel?: string): import("../index.js").AgentRuntime<SandboxProvider>;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Amp CLI runtime — drives Sourcegraph's `amp -x --stream-json` agentic CLI
3
+ * inside the sandbox and maps its (Claude-Code-compatible) JSONL stream onto
4
+ * the AgentMessage contract. Built on the shared CLI-agent base; Amp brings its
5
+ * own loop + tools, so we only stream-parse what it prints.
6
+ *
7
+ * Auth: set `AMP_API_KEY` (`sgamp_…`) in the sandbox env via a workflow secret.
8
+ * Requires the `amp` CLI (`@ampcode/cli`) installed in the sandbox image. The
9
+ * model is chosen by the AMP_API_KEY account (e.g. a GPT-only token runs GPT);
10
+ * the runtime doesn't pin a model.
11
+ *
12
+ * ⚠️ NOT verified against a live `amp` run — the thread-continue syntax and the
13
+ * exact assistant/result shapes are mapped from the docs (ampcode.com). Verify
14
+ * before production use.
15
+ */
16
+ export interface AmpRuntimeConfig {
17
+ /** Amp uses its configured model; reserved for forward-compatibility. */
18
+ model?: string;
19
+ }
20
+ export declare function createAmpRuntime(config?: AmpRuntimeConfig): import("../index.js").AgentRuntime<import("../sandbox.js").SandboxProvider>;
21
+ declare const _default: import("../index.js").AgentRuntime<import("../sandbox.js").SandboxProvider>;
22
+ export default _default;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Codex CLI runtime — drives OpenAI's `codex exec --json` agentic CLI inside
3
+ * the sandbox and maps its JSONL event stream onto the AgentMessage contract.
4
+ * Built on the shared CLI-agent base; Codex brings its own loop + tools, so we
5
+ * only stream-parse what it prints.
6
+ *
7
+ * Auth: set `CODEX_API_KEY` (or `OPENAI_API_KEY`) in the sandbox env via a
8
+ * workflow secret. Requires the `codex` CLI installed in the sandbox image.
9
+ *
10
+ * ⚠️ NOT verified against a live `codex` run — the resume flag and the exact
11
+ * item shapes (command_execution / reasoning fields) are mapped from the docs
12
+ * (developers.openai.com/codex/noninteractive). Verify before production use.
13
+ */
14
+ export interface CodexRuntimeConfig {
15
+ /** Codex model id (`-m`). Omit to use the codex CLI's configured default. */
16
+ model?: string;
17
+ }
18
+ export declare function createCodexRuntime(config?: CodexRuntimeConfig): import("../index.js").AgentRuntime<import("../sandbox.js").SandboxProvider>;
19
+ declare const _default: import("../index.js").AgentRuntime<import("../sandbox.js").SandboxProvider>;
20
+ export default _default;
@@ -18,7 +18,7 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/runtimes/vercel.ts
21
- import { streamText, stepCountIs, tool } from "ai";
21
+ import { streamText, stepCountIs, tool, gateway } from "ai";
22
22
 
23
23
  // src/types/runtime.ts
24
24
  function defineRuntime(pkg) {
@@ -387,6 +387,8 @@ class VercelRunner {
387
387
  options;
388
388
  config;
389
389
  supportsToolCallProcessor = true;
390
+ kind;
391
+ model;
390
392
  tools;
391
393
  messages = [];
392
394
  constructor(sandbox, options, config) {
@@ -394,6 +396,8 @@ class VercelRunner {
394
396
  this.options = options;
395
397
  this.config = config;
396
398
  this.tools = config.tools ?? codingTools;
399
+ this.kind = config.kind;
400
+ this.model = config.modelId;
397
401
  }
398
402
  async gateToolCall(call, ctx) {
399
403
  const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
@@ -511,6 +515,10 @@ function createVercelRuntime(config) {
511
515
  create: (sandbox, opts) => new VercelRunner(sandbox, opts, config)
512
516
  });
513
517
  }
518
+ async function listVercelRuntimeModels() {
519
+ const { models } = await gateway.getAvailableModels();
520
+ return models.filter((m) => m.modelType == null || m.modelType === "language").map((m) => ({ id: m.id, name: m.name })).sort((a, b) => a.id.localeCompare(b.id));
521
+ }
514
522
  // src/types/workflow.ts
515
523
  import { z as z3 } from "zod";
516
524
 
@@ -2368,6 +2376,239 @@ function createClaudeRuntime(config = {}) {
2368
2376
  });
2369
2377
  }
2370
2378
  var claude_default = createClaudeRuntime();
2379
+ // src/runtimes/_cli-agent.ts
2380
+ function now3() {
2381
+ return new Date().toISOString();
2382
+ }
2383
+ function shellQuote(value) {
2384
+ return `'${value.replace(/'/g, `'\\''`)}'`;
2385
+ }
2386
+
2387
+ class CliAgentRunner {
2388
+ sandbox;
2389
+ options;
2390
+ spec;
2391
+ configModel;
2392
+ kind;
2393
+ constructor(sandbox, options, spec, configModel) {
2394
+ this.sandbox = sandbox;
2395
+ this.options = options;
2396
+ this.spec = spec;
2397
+ this.configModel = configModel;
2398
+ this.kind = spec.kind;
2399
+ }
2400
+ get model() {
2401
+ return this.configModel ?? this.options.model ?? this.spec.defaultModel;
2402
+ }
2403
+ async* sendMessage(opts) {
2404
+ const promptPath = `/tmp/ac-${this.spec.kind}-${this.options.agentId ?? "agent"}-${opts.iteration ?? 0}.in`;
2405
+ await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
2406
+ const cmd = this.spec.buildCommand({
2407
+ promptPath,
2408
+ sessionId: opts.sessionId,
2409
+ model: this.model,
2410
+ cwd: this.options.cwd
2411
+ });
2412
+ const lines = new AsyncQueue;
2413
+ let buf = "";
2414
+ const onStdout = (data) => {
2415
+ buf += data;
2416
+ let nl;
2417
+ while ((nl = buf.indexOf(`
2418
+ `)) >= 0) {
2419
+ const line = buf.slice(0, nl).trim();
2420
+ buf = buf.slice(nl + 1);
2421
+ if (line)
2422
+ lines.push(line);
2423
+ }
2424
+ };
2425
+ const runPromise = this.sandbox.commands.run(cmd, {
2426
+ ...this.options.cwd ? { cwd: this.options.cwd } : {},
2427
+ onStdout
2428
+ }).then((res) => {
2429
+ const tail = buf.trim();
2430
+ if (tail)
2431
+ lines.push(tail);
2432
+ lines.close();
2433
+ return res;
2434
+ }, (err) => {
2435
+ lines.close();
2436
+ throw err;
2437
+ });
2438
+ yield { type: "init", sessionId: opts.sessionId ?? "", timestamp: now3() };
2439
+ let sessionId = opts.sessionId;
2440
+ let sawError = false;
2441
+ try {
2442
+ for await (const line of lines) {
2443
+ let parsed;
2444
+ try {
2445
+ parsed = JSON.parse(line);
2446
+ } catch {
2447
+ continue;
2448
+ }
2449
+ const sid = this.spec.extractSessionId(parsed);
2450
+ if (sid)
2451
+ sessionId = sid;
2452
+ for (const msg of this.spec.mapEvent(parsed)) {
2453
+ if (msg.type === "error")
2454
+ sawError = true;
2455
+ yield msg;
2456
+ }
2457
+ }
2458
+ const res = await runPromise;
2459
+ if (res.exitCode !== 0 && !sawError) {
2460
+ const tail = (res.stderr ?? "").slice(-2000);
2461
+ yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp: now3() };
2462
+ return;
2463
+ }
2464
+ if (!sawError)
2465
+ yield { type: "done", sessionId: sessionId ?? "", timestamp: now3() };
2466
+ } catch (err) {
2467
+ yield { type: "error", text: formatError(err), timestamp: now3() };
2468
+ }
2469
+ }
2470
+ }
2471
+ function createCliAgentRuntime(spec, configModel) {
2472
+ return defineRuntime({
2473
+ create: (sandbox, opts) => new CliAgentRunner(sandbox, opts, spec, configModel)
2474
+ });
2475
+ }
2476
+
2477
+ // src/runtimes/codex.ts
2478
+ function now4() {
2479
+ return new Date().toISOString();
2480
+ }
2481
+ var codexSpec = {
2482
+ kind: "codex",
2483
+ authEnv: "CODEX_API_KEY",
2484
+ promptPayload: (prompt) => prompt,
2485
+ buildCommand: ({ promptPath, sessionId, model, cwd }) => {
2486
+ const flags = [
2487
+ "--json",
2488
+ "--skip-git-repo-check",
2489
+ "--dangerously-bypass-approvals-and-sandbox",
2490
+ ...model ? ["-m", shellQuote(model)] : [],
2491
+ ...cwd ? ["-C", shellQuote(cwd)] : []
2492
+ ].join(" ");
2493
+ const exec = sessionId ? `codex exec resume ${shellQuote(sessionId)} ${flags}` : `codex exec ${flags}`;
2494
+ return `${exec} - < ${shellQuote(promptPath)}`;
2495
+ },
2496
+ extractSessionId: (p) => p.type === "thread.started" && typeof p.thread_id === "string" ? p.thread_id : undefined,
2497
+ mapEvent: (p) => {
2498
+ const ts = now4();
2499
+ switch (p.type) {
2500
+ case "item.started":
2501
+ case "item.completed": {
2502
+ const item = p.item;
2503
+ if (!item)
2504
+ return [];
2505
+ const itype = String(item.type ?? "");
2506
+ if (itype === "agent_message") {
2507
+ return p.type === "item.completed" ? [{ type: "text", text: String(item.text ?? ""), timestamp: ts }] : [];
2508
+ }
2509
+ if (itype === "reasoning") {
2510
+ return p.type === "item.completed" ? [{ type: "thinking", text: String(item.text ?? ""), timestamp: ts }] : [];
2511
+ }
2512
+ if (itype === "command_execution") {
2513
+ const id = String(item.id ?? "");
2514
+ if (p.type === "item.started") {
2515
+ return [{ type: "tool_use", toolName: "shell", toolInput: { command: String(item.command ?? "") }, toolUseId: id, timestamp: ts }];
2516
+ }
2517
+ const failed = item.status === "failed" || typeof item.exit_code === "number" && item.exit_code !== 0;
2518
+ return [{ type: "tool_result", toolUseId: id, output: String(item.aggregated_output ?? item.output ?? ""), isError: failed, timestamp: ts }];
2519
+ }
2520
+ if (p.type === "item.completed") {
2521
+ return [{ type: "tool_use", toolName: itype || "item", toolInput: item, toolUseId: String(item.id ?? ""), timestamp: ts }];
2522
+ }
2523
+ return [];
2524
+ }
2525
+ case "turn.completed": {
2526
+ const u = p.usage;
2527
+ if (!u)
2528
+ return [];
2529
+ return [{
2530
+ type: "usage",
2531
+ inputTokens: u.input_tokens ?? 0,
2532
+ outputTokens: u.output_tokens ?? 0,
2533
+ cacheReadTokens: u.cached_input_tokens ?? 0,
2534
+ cacheCreationTokens: 0,
2535
+ durationMs: 0,
2536
+ numTurns: 1,
2537
+ timestamp: ts
2538
+ }];
2539
+ }
2540
+ case "turn.failed":
2541
+ case "error":
2542
+ return [{ type: "error", text: formatError(p.error ?? p.message ?? p), timestamp: ts }];
2543
+ default:
2544
+ return [];
2545
+ }
2546
+ }
2547
+ };
2548
+ function createCodexRuntime(config = {}) {
2549
+ return createCliAgentRuntime(codexSpec, config.model);
2550
+ }
2551
+ var codex_default = createCodexRuntime();
2552
+ // src/runtimes/amp.ts
2553
+ function now5() {
2554
+ return new Date().toISOString();
2555
+ }
2556
+ function blocks(msg) {
2557
+ const inner = msg.message?.content ?? msg.content ?? [];
2558
+ return inner;
2559
+ }
2560
+ var ampSpec = {
2561
+ kind: "amp",
2562
+ authEnv: "AMP_API_KEY",
2563
+ promptPayload: (prompt) => JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } }) + `
2564
+ `,
2565
+ buildCommand: ({ promptPath, sessionId }) => {
2566
+ const cont = sessionId ? `threads continue ${shellQuote(sessionId)} ` : "";
2567
+ return `amp ${cont}-x --stream-json --stream-json-input < ${shellQuote(promptPath)}`;
2568
+ },
2569
+ extractSessionId: (p) => typeof p.session_id === "string" ? p.session_id : undefined,
2570
+ mapEvent: (p) => {
2571
+ const ts = now5();
2572
+ if (p.type === "assistant") {
2573
+ return blocks(p).flatMap((b) => {
2574
+ if (b.type === "text")
2575
+ return [{ type: "text", text: String(b.text ?? ""), timestamp: ts }];
2576
+ if (b.type === "thinking")
2577
+ return [{ type: "thinking", text: String(b.thinking ?? ""), timestamp: ts }];
2578
+ if (b.type === "tool_use")
2579
+ return [{ type: "tool_use", toolName: String(b.name ?? ""), toolInput: b.input ?? {}, toolUseId: String(b.id ?? ""), timestamp: ts }];
2580
+ return [];
2581
+ });
2582
+ }
2583
+ if (p.type === "user") {
2584
+ return blocks(p).flatMap((b) => b.type === "tool_result" ? [{ type: "tool_result", toolUseId: String(b.tool_use_id ?? ""), output: typeof b.content === "string" ? b.content : JSON.stringify(b.content ?? ""), isError: Boolean(b.is_error), timestamp: ts }] : []);
2585
+ }
2586
+ if (p.type === "result") {
2587
+ const out = [];
2588
+ const u = p.usage;
2589
+ if (u)
2590
+ out.push({
2591
+ type: "usage",
2592
+ inputTokens: u.input_tokens ?? 0,
2593
+ outputTokens: u.output_tokens ?? 0,
2594
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
2595
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
2596
+ durationMs: Number(p.duration_ms ?? 0),
2597
+ numTurns: Number(p.num_turns ?? 0),
2598
+ timestamp: ts
2599
+ });
2600
+ if (p.is_error || p.subtype === "error") {
2601
+ out.push({ type: "error", text: formatError(p.result ?? p.error), timestamp: ts });
2602
+ }
2603
+ return out;
2604
+ }
2605
+ return [];
2606
+ }
2607
+ };
2608
+ function createAmpRuntime(config = {}) {
2609
+ return createCliAgentRuntime(ampSpec, config.model);
2610
+ }
2611
+ var amp_default = createAmpRuntime();
2371
2612
  // src/sandbox.ts
2372
2613
  import { promises as fs2 } from "node:fs";
2373
2614
  import { dirname as dirname2 } from "node:path";
@@ -7,18 +7,38 @@ import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvi
7
7
  import { type CodingTool } from "../tools/index.js";
8
8
  import type { ProcessorContext, ToolCall } from "../processors/processor.js";
9
9
  export interface VercelRuntimeConfig {
10
- /** Vercel AI SDK language model (e.g. openai("gpt-5"), anthropic("claude-sonnet-4-5")). */
10
+ /** The model to drive — this is the only thing that varies per provider;
11
+ * there is no per-provider runtime. `LanguageModel` accepts every provider:
12
+ * - a gateway model-id string routed via the Vercel AI Gateway (set
13
+ * `AI_GATEWAY_API_KEY`; no provider package needed), e.g. "openai/gpt-5",
14
+ * "google/gemini-2.5-pro", "xai/grok-4", "deepseek/deepseek-chat",
15
+ * "mistral/mistral-large-latest", "anthropic/claude-sonnet-4-5";
16
+ * - or a `LanguageModel` object from a provider package (add the dep + set
17
+ * its API-key env), e.g. `openai("gpt-5")`, `google("gemini-2.5-pro")`. */
11
18
  model: LanguageModel;
12
19
  /** Optional system prompt prepended to every model call. */
13
20
  system?: string;
14
21
  /** Override/extend the default coding tools. Defaults: Read, Write, Edit, Bash. */
15
22
  tools?: readonly CodingTool[];
23
+ /** Short runtime self-id surfaced on `agent.spawned` so the dashboard can
24
+ * show a per-agent runtime icon (e.g. "openai", "gemini"). Provider presets
25
+ * set this; bare `createVercelRuntime` callers can leave it unset. */
26
+ kind?: string;
27
+ /** Display model id surfaced on `agent.spawned` (the Agent tab labels which
28
+ * model each agent ran). `model` above is the AI SDK LanguageModel object;
29
+ * this is its human-readable id string. */
30
+ modelId?: string;
16
31
  }
17
32
  export declare class VercelRunner implements ModelExecutionContract {
18
33
  private readonly sandbox;
19
34
  private readonly options;
20
35
  private readonly config;
21
36
  supportsToolCallProcessor: boolean;
37
+ /** Surfaced on `agent.spawned` for the dashboard's per-agent runtime icon +
38
+ * model label. Set from the (provider preset's) config; undefined for a
39
+ * bare `createVercelRuntime` that didn't label itself. */
40
+ readonly kind?: string;
41
+ readonly model?: string;
22
42
  private readonly tools;
23
43
  private readonly messages;
24
44
  constructor(sandbox: SandboxProvider, options: RuntimeOptions, config: VercelRuntimeConfig);
@@ -44,3 +64,23 @@ export declare class VercelRunner implements ModelExecutionContract {
44
64
  }): AsyncGenerator<AgentMessage>;
45
65
  }
46
66
  export declare function createVercelRuntime(config: VercelRuntimeConfig): import("../index.js").AgentRuntime<SandboxProvider>;
67
+ /** One model offered by the Vercel AI Gateway. Pass `id` straight to
68
+ * `createVercelRuntime({ model: id })`. */
69
+ export interface VercelRuntimeModel {
70
+ /** Gateway model id, e.g. "openai/gpt-5". Usable directly as the runtime model. */
71
+ id: string;
72
+ /** Human-readable display name. */
73
+ name: string;
74
+ }
75
+ /**
76
+ * List the models the Vercel runtime accepts as a gateway model-id string — the
77
+ * LIVE Vercel AI Gateway catalog, so it never goes stale. This is the canonical
78
+ * answer to "what models can I pass to `createVercelRuntime`?" for the string
79
+ * form (`createVercelRuntime({ model: "openai/gpt-5" })`).
80
+ *
81
+ * Requires `AI_GATEWAY_API_KEY`. The other form — a `LanguageModel` object from
82
+ * an `@ai-sdk/<provider>` package — supports whatever that provider package
83
+ * does (see its docs); there's no single cross-form list because the runtime is
84
+ * model-agnostic. Browse the catalog in a UI at https://vercel.com/ai-gateway/models.
85
+ */
86
+ export declare function listVercelRuntimeModels(): Promise<VercelRuntimeModel[]>;
@@ -18,7 +18,7 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/runtimes/vercel.ts
21
- import { streamText, stepCountIs, tool } from "ai";
21
+ import { streamText, stepCountIs, tool, gateway } from "ai";
22
22
 
23
23
  // src/types/runtime.ts
24
24
  function defineRuntime(pkg) {
@@ -387,6 +387,8 @@ class VercelRunner {
387
387
  options;
388
388
  config;
389
389
  supportsToolCallProcessor = true;
390
+ kind;
391
+ model;
390
392
  tools;
391
393
  messages = [];
392
394
  constructor(sandbox, options, config) {
@@ -394,6 +396,8 @@ class VercelRunner {
394
396
  this.options = options;
395
397
  this.config = config;
396
398
  this.tools = config.tools ?? codingTools;
399
+ this.kind = config.kind;
400
+ this.model = config.modelId;
397
401
  }
398
402
  async gateToolCall(call, ctx) {
399
403
  const verdict = await runProcessorChain(this.options.processors ?? [], (p) => p.processToolCall, call, ctx);
@@ -511,7 +515,12 @@ function createVercelRuntime(config) {
511
515
  create: (sandbox, opts) => new VercelRunner(sandbox, opts, config)
512
516
  });
513
517
  }
518
+ async function listVercelRuntimeModels() {
519
+ const { models } = await gateway.getAvailableModels();
520
+ return models.filter((m) => m.modelType == null || m.modelType === "language").map((m) => ({ id: m.id, name: m.name })).sort((a, b) => a.id.localeCompare(b.id));
521
+ }
514
522
  export {
523
+ listVercelRuntimeModels,
515
524
  createVercelRuntime,
516
525
  VercelRunner
517
526
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/sdk",
3
- "version": "0.5.2",
3
+ "version": "0.5.5",
4
4
  "description": "Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -120,6 +120,7 @@ export type {
120
120
  RequestAgentPauseOptions, RequestAgentPauseResponse,
121
121
  SendAgentMessageOptions, SendAgentMessageResponse,
122
122
  AnswerSteerOptions,
123
+ ResumePauseOptions, ResumePauseResponse, ResumePauseSuccess, ResumePausePending, ResumePauseActor,
123
124
  } from "./client.js";
124
125
 
125
126
  // SSE parser — exposed so tests and downstream callers can reuse it.
@@ -147,8 +148,25 @@ export { AgentStatusSchema } from "./utils/schemas.js";
147
148
  export { createClaudeRuntime, ClaudeRunner } from "./runtimes/claude.js";
148
149
  export type { ClaudeRuntimeConfig } from "./runtimes/claude.js";
149
150
  export { default as claudeRuntime } from "./runtimes/claude.js";
150
- export { createVercelRuntime, VercelRunner } from "./runtimes/vercel.js";
151
- export type { VercelRuntimeConfig } from "./runtimes/vercel.js";
151
+ export { createVercelRuntime, VercelRunner, listVercelRuntimeModels } from "./runtimes/vercel.js";
152
+ export type { VercelRuntimeConfig, VercelRuntimeModel } from "./runtimes/vercel.js";
153
+ // There is no per-provider runtime — the model is a parameter, not a runtime.
154
+ // `createVercelRuntime({ model })` drives any provider: pass a gateway model-id
155
+ // string ("openai/gpt-5", "google/gemini-2.5-pro", "anthropic/claude-…") or a
156
+ // LanguageModel object from any @ai-sdk/<provider> package. To DISCOVER which
157
+ // gateway models are available, call `listVercelRuntimeModels()` (live catalog)
158
+ // or browse https://vercel.com/ai-gateway/models. `GatewayModelId` is the
159
+ // string-form model-id type. See docs/custom-runtimes.md.
160
+ export type { GatewayModelId } from "ai";
161
+ // CLI-agent runtimes — drive an external agentic CLI (codex / amp) inside the
162
+ // sandbox and stream-parse its JSONL. No heavy npm deps (the CLI lives in the
163
+ // sandbox image), so these are root-exported like claudeRuntime.
164
+ export { createCodexRuntime } from "./runtimes/codex.js";
165
+ export type { CodexRuntimeConfig } from "./runtimes/codex.js";
166
+ export { default as codexRuntime } from "./runtimes/codex.js";
167
+ export { createAmpRuntime } from "./runtimes/amp.js";
168
+ export type { AmpRuntimeConfig } from "./runtimes/amp.js";
169
+ export { default as ampRuntime } from "./runtimes/amp.js";
152
170
 
153
171
  // Built-in coding tools for Vercel AI SDK runtime.
154
172
  export { bashTool, codingTools, editTool, readTool, writeTool } from "./tools/index.js";
@@ -0,0 +1,161 @@
1
+ /**
2
+ * CLI-agent runtime base — drive an external agentic coding CLI inside the
3
+ * sandbox and map its JSON-Lines (JSONL) stream onto the `AgentMessage`
4
+ * contract.
5
+ *
6
+ * This is a different *mechanism* from the other runtimes: `claudeRuntime`
7
+ * drives the Anthropic Agent SDK and `vercelRuntime` drives the Vercel AI SDK,
8
+ * but a CLI-agent runtime spawns the provider's own CLI (`codex exec --json`,
9
+ * `amp -x --stream-json`) — the CLI brings its own agent loop + tools, and we
10
+ * only stream-parse the events it prints. Codex and Amp are the first two; this
11
+ * base is the shared machinery (spawn → line-buffer stdout → JSONL parse →
12
+ * AsyncQueue bridge → init/usage/done/error lifecycle), parameterised per-CLI
13
+ * by a `CliAgentSpec`.
14
+ *
15
+ * Requirements (per spec): the provider CLI is installed in the sandbox image,
16
+ * and the provider's API key is present in the sandbox environment (see each
17
+ * spec's `authEnv`). `commands.run` inherits the sandbox env, so secrets set via
18
+ * `agentc secrets set` are visible to the CLI.
19
+ *
20
+ * NOTE: the per-CLI command construction + resume flags + exact event shapes in
21
+ * the shipped specs are mapped from each tool's docs and have NOT been verified
22
+ * against a live CLI run — verify before relying on them in production.
23
+ */
24
+
25
+ import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvider } from "../index.js";
26
+ import { defineRuntime } from "../types/runtime.js";
27
+ import { AsyncQueue } from "../agent/async-queue.js";
28
+ import { formatError } from "../utils/errors.js";
29
+
30
+ function now(): string { return new Date().toISOString(); }
31
+
32
+ /** Single-quote a value for safe interpolation into a `sh -c` command line. */
33
+ export function shellQuote(value: string): string {
34
+ return `'${value.replace(/'/g, `'\\''`)}'`;
35
+ }
36
+
37
+ /** Per-CLI behaviour. The base owns the lifecycle (init/done/error) and the
38
+ * transport (spawn + JSONL parse); a spec owns the CLI-specific bits. */
39
+ export interface CliAgentSpec {
40
+ /** Runtime self-id surfaced on `agent.spawned` (dashboard runtime icon). */
41
+ kind: string;
42
+ /** Env var the CLI reads for auth — documentation only (must be set in the
43
+ * sandbox env via a workflow secret). */
44
+ authEnv: string;
45
+ /** Default model id when none is configured; omit to let the CLI choose. */
46
+ defaultModel?: string;
47
+ /** Serialise the user prompt into the bytes written to the prompt file —
48
+ * plain text for a CLI that reads the prompt from stdin (codex `-`), or a
49
+ * JSONL user message for a `--stream-json-input` CLI (amp). */
50
+ promptPayload(prompt: string): string;
51
+ /** Build the one-shot shell command for a turn. `promptPath` is a file in the
52
+ * sandbox holding `promptPayload(prompt)`; `sessionId` continues a thread. */
53
+ buildCommand(args: { promptPath: string; sessionId?: string; model?: string; cwd?: string }): string;
54
+ /** Map one parsed JSONL stdout event to `AgentMessage`s. The base emits
55
+ * `init`/`done`/`error` lifecycle itself, so a spec maps only content +
56
+ * usage (text / thinking / tool_use / tool_result / usage). */
57
+ mapEvent(parsed: Record<string, unknown>): AgentMessage[];
58
+ /** Pull a session/thread id out of a parsed event so the next turn can
59
+ * resume it (codex `thread.started.thread_id`, amp `session_id`). */
60
+ extractSessionId(parsed: Record<string, unknown>): string | undefined;
61
+ }
62
+
63
+ export class CliAgentRunner implements ModelExecutionContract {
64
+ readonly kind: string;
65
+
66
+ constructor(
67
+ private readonly sandbox: SandboxProvider,
68
+ private readonly options: RuntimeOptions,
69
+ private readonly spec: CliAgentSpec,
70
+ private readonly configModel?: string,
71
+ ) {
72
+ this.kind = spec.kind;
73
+ }
74
+
75
+ get model(): string | undefined {
76
+ return this.configModel ?? this.options.model ?? this.spec.defaultModel;
77
+ }
78
+
79
+ // No captureCheckpoint/restoreCheckpoint: the CLI persists its thread/rollout
80
+ // on the sandbox filesystem (which round-trips through the pause snapshot),
81
+ // and the loop already carries the session id we emit on init/done and pass
82
+ // back as `sessionId` for resume — same model as claudeRuntime.
83
+
84
+ async *sendMessage(opts: {
85
+ prompt: string;
86
+ sessionId?: string;
87
+ iteration?: number;
88
+ signal?: AbortSignal;
89
+ }): AsyncGenerator<AgentMessage> {
90
+ const promptPath = `/tmp/ac-${this.spec.kind}-${this.options.agentId ?? "agent"}-${opts.iteration ?? 0}.in`;
91
+ await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
92
+ const cmd = this.spec.buildCommand({
93
+ promptPath,
94
+ sessionId: opts.sessionId,
95
+ model: this.model,
96
+ cwd: this.options.cwd,
97
+ });
98
+
99
+ // Bridge the streaming stdout callback into an async-iterable of complete
100
+ // JSONL lines. `onStdout` chunks aren't line-aligned, so buffer + split.
101
+ const lines = new AsyncQueue<string>();
102
+ let buf = "";
103
+ const onStdout = (data: string) => {
104
+ buf += data;
105
+ let nl: number;
106
+ while ((nl = buf.indexOf("\n")) >= 0) {
107
+ const line = buf.slice(0, nl).trim();
108
+ buf = buf.slice(nl + 1);
109
+ if (line) lines.push(line);
110
+ }
111
+ };
112
+
113
+ // `commands.run` resolves when the process exits. Kick it off (don't await
114
+ // yet); flush the trailing buffer + close the queue on completion so the
115
+ // for-await below drains and we can read the exit code.
116
+ const runPromise = this.sandbox.commands.run(cmd, {
117
+ ...(this.options.cwd ? { cwd: this.options.cwd } : {}),
118
+ onStdout,
119
+ }).then(
120
+ (res) => { const tail = buf.trim(); if (tail) lines.push(tail); lines.close(); return res; },
121
+ (err) => { lines.close(); throw err; },
122
+ );
123
+
124
+ yield { type: "init", sessionId: opts.sessionId ?? "", timestamp: now() };
125
+
126
+ let sessionId = opts.sessionId;
127
+ let sawError = false;
128
+ try {
129
+ for await (const line of lines) {
130
+ let parsed: Record<string, unknown>;
131
+ try {
132
+ parsed = JSON.parse(line) as Record<string, unknown>;
133
+ } catch {
134
+ continue; // skip any non-JSON noise that lands on stdout
135
+ }
136
+ const sid = this.spec.extractSessionId(parsed);
137
+ if (sid) sessionId = sid;
138
+ for (const msg of this.spec.mapEvent(parsed)) {
139
+ if (msg.type === "error") sawError = true;
140
+ yield msg;
141
+ }
142
+ }
143
+
144
+ const res = await runPromise;
145
+ if (res.exitCode !== 0 && !sawError) {
146
+ const tail = (res.stderr ?? "").slice(-2000);
147
+ yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp: now() };
148
+ return;
149
+ }
150
+ if (!sawError) yield { type: "done", sessionId: sessionId ?? "", timestamp: now() };
151
+ } catch (err) {
152
+ yield { type: "error", text: formatError(err), timestamp: now() };
153
+ }
154
+ }
155
+ }
156
+
157
+ export function createCliAgentRuntime(spec: CliAgentSpec, configModel?: string) {
158
+ return defineRuntime({
159
+ create: (sandbox, opts) => new CliAgentRunner(sandbox, opts, spec, configModel),
160
+ });
161
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Amp CLI runtime — drives Sourcegraph's `amp -x --stream-json` agentic CLI
3
+ * inside the sandbox and maps its (Claude-Code-compatible) JSONL stream onto
4
+ * the AgentMessage contract. Built on the shared CLI-agent base; Amp brings its
5
+ * own loop + tools, so we only stream-parse what it prints.
6
+ *
7
+ * Auth: set `AMP_API_KEY` (`sgamp_…`) in the sandbox env via a workflow secret.
8
+ * Requires the `amp` CLI (`@ampcode/cli`) installed in the sandbox image. The
9
+ * model is chosen by the AMP_API_KEY account (e.g. a GPT-only token runs GPT);
10
+ * the runtime doesn't pin a model.
11
+ *
12
+ * ⚠️ NOT verified against a live `amp` run — the thread-continue syntax and the
13
+ * exact assistant/result shapes are mapped from the docs (ampcode.com). Verify
14
+ * before production use.
15
+ */
16
+
17
+ import type { AgentMessage } from "../index.js";
18
+ import { createCliAgentRuntime, shellQuote, type CliAgentSpec } from "./_cli-agent.js";
19
+ import { formatError } from "../utils/errors.js";
20
+
21
+ function now(): string { return new Date().toISOString(); }
22
+
23
+ /** Block array off either `{ message: { content } }` (Claude shape) or a
24
+ * top-level `{ content }`, whichever the stream uses. */
25
+ function blocks(msg: Record<string, unknown>): Array<Record<string, unknown>> {
26
+ const inner = (msg.message as { content?: unknown[] } | undefined)?.content
27
+ ?? (msg.content as unknown[] | undefined)
28
+ ?? [];
29
+ return inner as Array<Record<string, unknown>>;
30
+ }
31
+
32
+ const ampSpec: CliAgentSpec = {
33
+ kind: "amp",
34
+ authEnv: "AMP_API_KEY",
35
+ // `--stream-json-input` reads JSON Lines user messages from stdin; write one.
36
+ // amp's --stream-json-input wants Claude-shaped content blocks, not a bare
37
+ // string (it rejects a string `content` with "expected array, received string").
38
+ promptPayload: (prompt) =>
39
+ JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } }) + "\n",
40
+ buildCommand: ({ promptPath, sessionId }) => {
41
+ // Continue the prior thread by id when we have one; else start fresh.
42
+ const cont = sessionId ? `threads continue ${shellQuote(sessionId)} ` : "";
43
+ return `amp ${cont}-x --stream-json --stream-json-input < ${shellQuote(promptPath)}`;
44
+ },
45
+ // Amp stamps `session_id` (a "T-…" thread id) on every message.
46
+ extractSessionId: (p) => (typeof p.session_id === "string" ? p.session_id : undefined),
47
+ mapEvent: (p): AgentMessage[] => {
48
+ const ts = now();
49
+ if (p.type === "assistant") {
50
+ return blocks(p).flatMap((b): AgentMessage[] => {
51
+ if (b.type === "text") return [{ type: "text", text: String(b.text ?? ""), timestamp: ts }];
52
+ if (b.type === "thinking") return [{ type: "thinking", text: String(b.thinking ?? ""), timestamp: ts }];
53
+ if (b.type === "tool_use") return [{ type: "tool_use", toolName: String(b.name ?? ""), toolInput: (b.input ?? {}) as Record<string, unknown>, toolUseId: String(b.id ?? ""), timestamp: ts }];
54
+ return [];
55
+ });
56
+ }
57
+ if (p.type === "user") {
58
+ return blocks(p).flatMap((b): AgentMessage[] =>
59
+ b.type === "tool_result"
60
+ ? [{ type: "tool_result", toolUseId: String(b.tool_use_id ?? ""), output: typeof b.content === "string" ? b.content : JSON.stringify(b.content ?? ""), isError: Boolean(b.is_error), timestamp: ts }]
61
+ : []);
62
+ }
63
+ if (p.type === "result") {
64
+ const out: AgentMessage[] = [];
65
+ const u = p.usage as Record<string, number> | undefined;
66
+ if (u) out.push({
67
+ type: "usage",
68
+ inputTokens: u.input_tokens ?? 0,
69
+ outputTokens: u.output_tokens ?? 0,
70
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
71
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
72
+ durationMs: Number(p.duration_ms ?? 0),
73
+ numTurns: Number(p.num_turns ?? 0),
74
+ timestamp: ts,
75
+ });
76
+ if (p.is_error || p.subtype === "error") {
77
+ out.push({ type: "error", text: formatError(p.result ?? p.error), timestamp: ts });
78
+ }
79
+ return out;
80
+ }
81
+ return []; // "system" → session id captured by extractSessionId; init/done owned by the base
82
+ },
83
+ };
84
+
85
+ export interface AmpRuntimeConfig {
86
+ /** Amp uses its configured model; reserved for forward-compatibility. */
87
+ model?: string;
88
+ }
89
+
90
+ export function createAmpRuntime(config: AmpRuntimeConfig = {}) {
91
+ return createCliAgentRuntime(ampSpec, config.model);
92
+ }
93
+
94
+ export default createAmpRuntime();
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Codex CLI runtime — drives OpenAI's `codex exec --json` agentic CLI inside
3
+ * the sandbox and maps its JSONL event stream onto the AgentMessage contract.
4
+ * Built on the shared CLI-agent base; Codex brings its own loop + tools, so we
5
+ * only stream-parse what it prints.
6
+ *
7
+ * Auth: set `CODEX_API_KEY` (or `OPENAI_API_KEY`) in the sandbox env via a
8
+ * workflow secret. Requires the `codex` CLI installed in the sandbox image.
9
+ *
10
+ * ⚠️ NOT verified against a live `codex` run — the resume flag and the exact
11
+ * item shapes (command_execution / reasoning fields) are mapped from the docs
12
+ * (developers.openai.com/codex/noninteractive). Verify before production use.
13
+ */
14
+
15
+ import type { AgentMessage } from "../index.js";
16
+ import { createCliAgentRuntime, shellQuote, type CliAgentSpec } from "./_cli-agent.js";
17
+ import { formatError } from "../utils/errors.js";
18
+
19
+ function now(): string { return new Date().toISOString(); }
20
+
21
+ const codexSpec: CliAgentSpec = {
22
+ kind: "codex",
23
+ authEnv: "CODEX_API_KEY",
24
+ // Codex reads the prompt from stdin when invoked as `codex exec ... -`.
25
+ promptPayload: (prompt) => prompt,
26
+ buildCommand: ({ promptPath, sessionId, model, cwd }) => {
27
+ const flags = [
28
+ "--json",
29
+ "--skip-git-repo-check",
30
+ // agent-compose already runs us inside an isolated sandbox VM, so codex
31
+ // must not try to nest its own seccomp/landlock sandbox or block on
32
+ // approvals (non-interactive). codex docs: this flag is "intended solely
33
+ // for running in environments that are externally sandboxed".
34
+ "--dangerously-bypass-approvals-and-sandbox",
35
+ ...(model ? ["-m", shellQuote(model)] : []),
36
+ ...(cwd ? ["-C", shellQuote(cwd)] : []),
37
+ ].join(" ");
38
+ // Fresh turn: `codex exec <flags> - < prompt`. Continue a thread:
39
+ // `codex exec resume <id> <flags> - < prompt`. (`-` = read prompt from stdin.)
40
+ const exec = sessionId
41
+ ? `codex exec resume ${shellQuote(sessionId)} ${flags}`
42
+ : `codex exec ${flags}`;
43
+ return `${exec} - < ${shellQuote(promptPath)}`;
44
+ },
45
+ extractSessionId: (p) =>
46
+ p.type === "thread.started" && typeof p.thread_id === "string" ? p.thread_id : undefined,
47
+ mapEvent: (p): AgentMessage[] => {
48
+ const ts = now();
49
+ switch (p.type) {
50
+ case "item.started":
51
+ case "item.completed": {
52
+ const item = p.item as Record<string, unknown> | undefined;
53
+ if (!item) return [];
54
+ const itype = String(item.type ?? "");
55
+ // Text + reasoning land on completion (started carries no final text).
56
+ if (itype === "agent_message") {
57
+ return p.type === "item.completed" ? [{ type: "text", text: String(item.text ?? ""), timestamp: ts }] : [];
58
+ }
59
+ if (itype === "reasoning") {
60
+ return p.type === "item.completed" ? [{ type: "thinking", text: String(item.text ?? ""), timestamp: ts }] : [];
61
+ }
62
+ // Command execution: started → tool_use, completed → tool_result.
63
+ if (itype === "command_execution") {
64
+ const id = String(item.id ?? "");
65
+ if (p.type === "item.started") {
66
+ return [{ type: "tool_use", toolName: "shell", toolInput: { command: String(item.command ?? "") }, toolUseId: id, timestamp: ts }];
67
+ }
68
+ const failed = item.status === "failed" || (typeof item.exit_code === "number" && item.exit_code !== 0);
69
+ return [{ type: "tool_result", toolUseId: id, output: String(item.aggregated_output ?? item.output ?? ""), isError: failed, timestamp: ts }];
70
+ }
71
+ // file_change / mcp_tool_call / web_search / todo: surface once, on completion.
72
+ if (p.type === "item.completed") {
73
+ return [{ type: "tool_use", toolName: itype || "item", toolInput: item, toolUseId: String(item.id ?? ""), timestamp: ts }];
74
+ }
75
+ return [];
76
+ }
77
+ case "turn.completed": {
78
+ const u = p.usage as Record<string, number> | undefined;
79
+ if (!u) return [];
80
+ return [{
81
+ type: "usage",
82
+ inputTokens: u.input_tokens ?? 0,
83
+ outputTokens: u.output_tokens ?? 0,
84
+ cacheReadTokens: u.cached_input_tokens ?? 0,
85
+ cacheCreationTokens: 0,
86
+ durationMs: 0,
87
+ numTurns: 1,
88
+ timestamp: ts,
89
+ }];
90
+ }
91
+ case "turn.failed":
92
+ case "error":
93
+ return [{ type: "error", text: formatError(p.error ?? p.message ?? p), timestamp: ts }];
94
+ default:
95
+ return [];
96
+ }
97
+ },
98
+ };
99
+
100
+ export interface CodexRuntimeConfig {
101
+ /** Codex model id (`-m`). Omit to use the codex CLI's configured default. */
102
+ model?: string;
103
+ }
104
+
105
+ export function createCodexRuntime(config: CodexRuntimeConfig = {}) {
106
+ return createCliAgentRuntime(codexSpec, config.model);
107
+ }
108
+
109
+ export default createCodexRuntime();
@@ -3,7 +3,7 @@
3
3
  * owned coding tools over SandboxProvider.
4
4
  */
5
5
 
6
- import { streamText, stepCountIs, tool, type LanguageModel } from "ai";
6
+ import { streamText, stepCountIs, tool, gateway, type LanguageModel } from "ai";
7
7
  import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvider, ToolCallGateResult } from "../index.js";
8
8
  import { defineRuntime } from "../types/runtime.js";
9
9
  import { codingTools, type CodingTool } from "../tools/index.js";
@@ -15,12 +15,27 @@ import { formatError } from "../utils/errors.js";
15
15
  type AiToolSet = Record<string, ReturnType<typeof tool<Record<string, unknown>, string>>>;
16
16
 
17
17
  export interface VercelRuntimeConfig {
18
- /** Vercel AI SDK language model (e.g. openai("gpt-5"), anthropic("claude-sonnet-4-5")). */
18
+ /** The model to drive — this is the only thing that varies per provider;
19
+ * there is no per-provider runtime. `LanguageModel` accepts every provider:
20
+ * - a gateway model-id string routed via the Vercel AI Gateway (set
21
+ * `AI_GATEWAY_API_KEY`; no provider package needed), e.g. "openai/gpt-5",
22
+ * "google/gemini-2.5-pro", "xai/grok-4", "deepseek/deepseek-chat",
23
+ * "mistral/mistral-large-latest", "anthropic/claude-sonnet-4-5";
24
+ * - or a `LanguageModel` object from a provider package (add the dep + set
25
+ * its API-key env), e.g. `openai("gpt-5")`, `google("gemini-2.5-pro")`. */
19
26
  model: LanguageModel;
20
27
  /** Optional system prompt prepended to every model call. */
21
28
  system?: string;
22
29
  /** Override/extend the default coding tools. Defaults: Read, Write, Edit, Bash. */
23
30
  tools?: readonly CodingTool[];
31
+ /** Short runtime self-id surfaced on `agent.spawned` so the dashboard can
32
+ * show a per-agent runtime icon (e.g. "openai", "gemini"). Provider presets
33
+ * set this; bare `createVercelRuntime` callers can leave it unset. */
34
+ kind?: string;
35
+ /** Display model id surfaced on `agent.spawned` (the Agent tab labels which
36
+ * model each agent ran). `model` above is the AI SDK LanguageModel object;
37
+ * this is its human-readable id string. */
38
+ modelId?: string;
24
39
  }
25
40
 
26
41
  function now(): string { return new Date().toISOString(); }
@@ -71,6 +86,11 @@ function toAgentMessages(part: Record<string, unknown>): AgentMessage[] {
71
86
 
72
87
  export class VercelRunner implements ModelExecutionContract {
73
88
  supportsToolCallProcessor = true;
89
+ /** Surfaced on `agent.spawned` for the dashboard's per-agent runtime icon +
90
+ * model label. Set from the (provider preset's) config; undefined for a
91
+ * bare `createVercelRuntime` that didn't label itself. */
92
+ readonly kind?: string;
93
+ readonly model?: string;
74
94
  private readonly tools: readonly CodingTool[];
75
95
  private readonly messages: unknown[] = [];
76
96
 
@@ -80,6 +100,8 @@ export class VercelRunner implements ModelExecutionContract {
80
100
  private readonly config: VercelRuntimeConfig,
81
101
  ) {
82
102
  this.tools = config.tools ?? codingTools;
103
+ this.kind = config.kind;
104
+ this.model = config.modelId;
83
105
  }
84
106
 
85
107
  async gateToolCall(call: ToolCall, ctx: ProcessorContext): Promise<ToolCallGateResult> {
@@ -204,3 +226,31 @@ export function createVercelRuntime(config: VercelRuntimeConfig) {
204
226
  create: (sandbox, opts) => new VercelRunner(sandbox, opts, config),
205
227
  });
206
228
  }
229
+
230
+ /** One model offered by the Vercel AI Gateway. Pass `id` straight to
231
+ * `createVercelRuntime({ model: id })`. */
232
+ export interface VercelRuntimeModel {
233
+ /** Gateway model id, e.g. "openai/gpt-5". Usable directly as the runtime model. */
234
+ id: string;
235
+ /** Human-readable display name. */
236
+ name: string;
237
+ }
238
+
239
+ /**
240
+ * List the models the Vercel runtime accepts as a gateway model-id string — the
241
+ * LIVE Vercel AI Gateway catalog, so it never goes stale. This is the canonical
242
+ * answer to "what models can I pass to `createVercelRuntime`?" for the string
243
+ * form (`createVercelRuntime({ model: "openai/gpt-5" })`).
244
+ *
245
+ * Requires `AI_GATEWAY_API_KEY`. The other form — a `LanguageModel` object from
246
+ * an `@ai-sdk/<provider>` package — supports whatever that provider package
247
+ * does (see its docs); there's no single cross-form list because the runtime is
248
+ * model-agnostic. Browse the catalog in a UI at https://vercel.com/ai-gateway/models.
249
+ */
250
+ export async function listVercelRuntimeModels(): Promise<VercelRuntimeModel[]> {
251
+ const { models } = await gateway.getAvailableModels();
252
+ return models
253
+ .filter((m) => m.modelType == null || m.modelType === "language")
254
+ .map((m) => ({ id: m.id, name: m.name }))
255
+ .sort((a, b) => a.id.localeCompare(b.id));
256
+ }