@agent-compose/sdk 0.2.3 → 0.2.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.
Files changed (88) hide show
  1. package/README.md +145 -33
  2. package/dist/agent/agent-loop.d.ts +83 -5
  3. package/dist/agent/run-agent.d.ts +34 -9
  4. package/dist/client.d.ts +247 -99
  5. package/dist/index.d.ts +26 -11
  6. package/dist/index.js +1967 -745
  7. package/dist/processors/builtins.d.ts +35 -0
  8. package/dist/processors/index.d.ts +4 -0
  9. package/dist/processors/processor.d.ts +91 -0
  10. package/dist/processors/processor.test.d.ts +1 -0
  11. package/dist/processors/runner.d.ts +19 -0
  12. package/dist/request-context/index.d.ts +2 -0
  13. package/dist/request-context/request-context.d.ts +159 -0
  14. package/dist/request-context/request-context.test.d.ts +1 -0
  15. package/dist/runtimes/claude.d.ts +27 -50
  16. package/dist/runtimes/openai-desktop.js +1918 -741
  17. package/dist/runtimes/vercel.d.ts +34 -0
  18. package/dist/runtimes/vercel.js +474 -0
  19. package/dist/sandbox.d.ts +29 -25
  20. package/dist/step-invocation/__tests__/invoker.test.d.ts +1 -0
  21. package/dist/step-invocation/__tests__/protocol.test.d.ts +1 -0
  22. package/dist/step-invocation/__tests__/server.test.d.ts +1 -0
  23. package/dist/step-invocation/index.d.ts +25 -0
  24. package/dist/step-invocation/invoker.d.ts +65 -0
  25. package/dist/step-invocation/protocol.d.ts +44 -0
  26. package/dist/step-invocation/server.d.ts +63 -0
  27. package/dist/step-invocation/types.d.ts +72 -0
  28. package/dist/tools/coding.d.ts +49 -0
  29. package/dist/tools/coding.test.d.ts +1 -0
  30. package/dist/tools/index.d.ts +2 -0
  31. package/dist/types/events.d.ts +36 -0
  32. package/dist/types/execution-context.d.ts +22 -0
  33. package/dist/types/runtime.d.ts +32 -0
  34. package/dist/types/sandbox-environment.d.ts +5 -2
  35. package/dist/types/sandbox.d.ts +14 -12
  36. package/dist/types/workflow-metadata.d.ts +51 -0
  37. package/dist/types/workflow-plan.d.ts +19 -0
  38. package/dist/types/workflow.d.ts +57 -17
  39. package/dist/utils/bundler.d.ts +62 -3
  40. package/dist/workflow-steps/__tests__/observability.test.d.ts +1 -0
  41. package/dist/workflow-steps/index.d.ts +10 -0
  42. package/dist/workflow-steps/observability.d.ts +58 -0
  43. package/dist/workflow-steps/runner.d.ts +96 -0
  44. package/dist/workflow-steps/step.d.ts +25 -0
  45. package/dist/workflow-steps/types.d.ts +135 -0
  46. package/dist/workflow-steps/workflow-steps.test.d.ts +1 -0
  47. package/dist/workflow-steps/workflow.d.ts +50 -0
  48. package/dist/workflows/engine.d.ts +27 -13
  49. package/dist/workflows/invoke-child.d.ts +10 -0
  50. package/package.json +25 -15
  51. package/src/agent/agent-loop.ts +197 -26
  52. package/src/agent/run-agent.ts +40 -15
  53. package/src/client.ts +326 -76
  54. package/src/index.ts +124 -10
  55. package/src/processors/builtins.ts +72 -0
  56. package/src/processors/index.ts +15 -0
  57. package/src/processors/processor.ts +103 -0
  58. package/src/processors/runner.ts +42 -0
  59. package/src/request-context/index.ts +17 -0
  60. package/src/request-context/request-context.ts +302 -0
  61. package/src/runtimes/claude.ts +123 -254
  62. package/src/runtimes/vercel.ts +180 -0
  63. package/src/sandbox.ts +53 -21
  64. package/src/step-invocation/index.ts +33 -0
  65. package/src/step-invocation/invoker.ts +204 -0
  66. package/src/step-invocation/protocol.ts +57 -0
  67. package/src/step-invocation/server.ts +184 -0
  68. package/src/step-invocation/types.ts +70 -0
  69. package/src/tools/coding.ts +126 -0
  70. package/src/tools/index.ts +8 -0
  71. package/src/types/events.ts +40 -0
  72. package/src/types/execution-context.ts +30 -0
  73. package/src/types/runtime.ts +24 -0
  74. package/src/types/sandbox-environment.ts +7 -5
  75. package/src/types/sandbox.ts +16 -12
  76. package/src/types/workflow-metadata.ts +84 -0
  77. package/src/types/workflow-plan.ts +24 -0
  78. package/src/types/workflow.ts +139 -25
  79. package/src/utils/bundler.ts +206 -18
  80. package/src/utils/source-loader.ts +2 -2
  81. package/src/workflow-steps/index.ts +30 -0
  82. package/src/workflow-steps/observability.ts +103 -0
  83. package/src/workflow-steps/runner.ts +244 -0
  84. package/src/workflow-steps/step.ts +38 -0
  85. package/src/workflow-steps/types.ts +134 -0
  86. package/src/workflow-steps/workflow.ts +95 -0
  87. package/src/workflows/engine.ts +69 -40
  88. package/src/workflows/invoke-child.ts +29 -0
@@ -1,304 +1,173 @@
1
- /**
2
- * Claude CLI runtime — drives agents via the Claude CLI inside a sandbox.
3
- *
4
- * Works with any SandboxProvider (E2B, Vercel, etc.) — the provider is
5
- * selected via SANDBOX_PROVIDER env var, not the runtime definition.
6
- *
7
- * Usage:
8
- * import { createClaudeRuntime } from "@agent-compose/sdk";
9
- * export default createClaudeRuntime({ claudeMdContent: "..." }); // optional global instructions
10
- */
1
+ /** Claude Agent SDK runtime — replaces the old Claude CLI subprocess runtime. */
11
2
 
12
- import type { SandboxProvider, AgentMessage, ModelExecutionContract, RuntimeOptions } from "../index.js";
3
+ import { query, type HookCallback, type PreToolUseHookInput, type ThinkingConfig } from "@anthropic-ai/claude-agent-sdk";
4
+ import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvider, ToolCallGateResult } from "../index.js";
13
5
  import { defineRuntime } from "../types/runtime.js";
14
6
  import { DEFAULT_CLAUDE_MODEL } from "../agent/agent-loop.js";
7
+ import { runProcessorChain } from "../processors/runner.js";
8
+ import type { ProcessorContext, ToolCall } from "../processors/processor.js";
9
+ import { RequestContext } from "../request-context/request-context.js";
15
10
  import { formatError } from "../utils/errors.js";
16
11
 
17
- const NO_TIMEOUT = 0; // claude exits on its own via --max-turns
12
+ function now(): string { return new Date().toISOString(); }
18
13
 
19
-
20
- function translateEvent(event: Record<string, unknown>): AgentMessage[] {
14
+ function translateMessage(message: Record<string, unknown>): AgentMessage[] {
15
+ const ts = now();
21
16
  const msgs: AgentMessage[] = [];
22
- const ts = new Date().toISOString();
23
17
 
24
- if (event.type === "system" && event.subtype === "init") {
25
- msgs.push({ type: "init", sessionId: String(event.session_id ?? ""), timestamp: ts });
18
+ if (message.type === "system" && message.subtype === "init") {
19
+ msgs.push({ type: "init", sessionId: String(message.session_id ?? ""), timestamp: ts });
26
20
  return msgs;
27
21
  }
28
22
 
29
- if (event.type === "assistant") {
30
- const message = event.message as { content?: unknown[] } | undefined;
31
- for (const block of message?.content ?? []) {
23
+ if (message.type === "assistant") {
24
+ const raw = message.message as { content?: unknown[] } | undefined;
25
+ for (const block of raw?.content ?? []) {
32
26
  const b = block as Record<string, unknown>;
33
- if (b.type === "text") msgs.push({ type: "text", text: String(b.text ?? ""), timestamp: ts });
27
+ if (b.type === "text") msgs.push({ type: "text", text: String(b.text ?? ""), timestamp: ts });
34
28
  if (b.type === "thinking") msgs.push({ type: "thinking", text: String(b.thinking ?? ""), timestamp: ts });
35
29
  if (b.type === "tool_use") msgs.push({ type: "tool_use", toolName: String(b.name ?? ""), toolInput: (b.input ?? {}) as Record<string, unknown>, toolUseId: String(b.id ?? ""), timestamp: ts });
36
30
  }
37
31
  return msgs;
38
32
  }
39
33
 
40
- if (event.type === "user") {
41
- const message = event.message as { content?: unknown[] } | undefined;
42
- for (const block of message?.content ?? []) {
34
+ if (message.type === "user") {
35
+ const raw = message.message as { content?: unknown[] } | undefined;
36
+ for (const block of raw?.content ?? []) {
43
37
  const b = block as Record<string, unknown>;
44
- if (b.type === "tool_result") {
45
- const raw = b.content;
46
- msgs.push({ type: "tool_result", toolUseId: String(b.tool_use_id ?? ""), output: typeof raw === "string" ? raw : JSON.stringify(raw ?? ""), isError: Boolean(b.is_error), timestamp: ts });
47
- }
38
+ if (b.type === "tool_result") msgs.push({ 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 });
48
39
  }
49
40
  return msgs;
50
41
  }
51
42
 
52
- if (event.type === "result") {
53
- const usage = event.usage as Record<string, number> | undefined;
54
- if (usage) {
55
- msgs.push({ type: "usage", inputTokens: usage.input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0, cacheReadTokens: usage.cache_read_input_tokens ?? 0, cacheCreationTokens: usage.cache_creation_input_tokens ?? 0, durationMs: Number(event.duration_ms ?? 0), numTurns: Number(event.num_turns ?? 0), timestamp: ts });
43
+ if (message.type === "result") {
44
+ if (typeof message.result === "string" && message.result.trim()) {
45
+ msgs.push({ type: "text", text: message.result, timestamp: ts });
56
46
  }
57
- if (event.is_error || event.subtype === "error_during_execution") {
58
- const raw = event.error ?? event.result ?? event.message;
59
- msgs.push({ type: "error", text: raw == null ? "Unknown error" : typeof raw === "object" ? JSON.stringify(raw) : String(raw), timestamp: ts });
47
+ const usage = message.usage as Record<string, number> | undefined;
48
+ if (usage) msgs.push({
49
+ type: "usage",
50
+ inputTokens: usage.input_tokens ?? 0,
51
+ outputTokens: usage.output_tokens ?? 0,
52
+ cacheReadTokens: usage.cache_read_input_tokens ?? 0,
53
+ cacheCreationTokens: usage.cache_creation_input_tokens ?? 0,
54
+ durationMs: Number(message.duration_ms ?? 0),
55
+ numTurns: Number(message.num_turns ?? 0),
56
+ timestamp: ts,
57
+ });
58
+ if (message.is_error || message.subtype === "error_during_execution") {
59
+ msgs.push({ type: "error", text: formatError(message.error ?? message.result ?? message.message), timestamp: ts });
60
60
  } else {
61
- msgs.push({ type: "done", sessionId: String(event.session_id ?? ""), timestamp: ts });
61
+ msgs.push({ type: "done", sessionId: String(message.session_id ?? ""), timestamp: ts });
62
62
  }
63
- return msgs;
64
63
  }
65
64
 
66
65
  return msgs;
67
66
  }
68
67
 
68
+ export interface ClaudeRuntimeConfig {
69
+ /** Global instructions appended to the Claude Code system prompt. */
70
+ claudeMdContent?: string;
71
+ /** Env overrides for Agent SDK provider routing. */
72
+ env?: Record<string, string>;
73
+ /** Model to use. Defaults to DEFAULT_CLAUDE_MODEL. */
74
+ model?: string;
75
+ /** MCP servers to configure for the Agent SDK. */
76
+ mcpServers?: Record<string, { command: string; args?: string[]; env?: Record<string, string> }>;
77
+ /** Claude Code executable. Defaults to `claude` from PATH inside the sandbox. */
78
+ pathToClaudeCodeExecutable?: string;
79
+ /** Controls Claude's extended thinking behavior when supported by the model. */
80
+ thinking?: ThinkingConfig;
81
+ /** Reasoning effort hint for models that support adaptive thinking. */
82
+ effort?: "low" | "medium" | "high" | "xhigh" | "max";
83
+ }
84
+
69
85
  export class ClaudeRunner implements ModelExecutionContract {
70
- private claudeMdWritten = false;
71
- private homeDir: string | null = null;
72
- private label: string;
86
+ supportsToolCallProcessor = true;
73
87
 
74
88
  constructor(
75
- private sandbox: SandboxProvider,
76
- private options: RuntimeOptions = {},
77
- private claudeMdContent: string = "",
78
- private configEnv: Record<string, string> = {},
79
- private configModel?: string,
80
- private configMcpServers?: Record<string, { command: string; args?: string[]; env?: Record<string, string> }>,
81
- ) {
82
- this.label = options.label ?? "[Agent]";
83
- }
84
-
85
- private async getHomeDir(): Promise<string> {
86
- if (!this.homeDir) {
87
- const { stdout } = await this.sandbox.commands.run("echo $HOME", { timeoutMs: 30_000 });
88
- this.homeDir = stdout.trim() || "/home/user";
89
- }
90
- return this.homeDir;
89
+ // Claude Agent SDK executes in this process. In dispatched workflows this
90
+ // process is already the runner sandbox, so there is no remote provider hop.
91
+ _sandbox: SandboxProvider,
92
+ private readonly options: RuntimeOptions = {},
93
+ private readonly config: ClaudeRuntimeConfig = {},
94
+ ) {}
95
+
96
+ async gateToolCall(call: ToolCall, ctx: ProcessorContext): Promise<ToolCallGateResult> {
97
+ const verdict = await runProcessorChain(this.options.processors ?? [], p => p.processToolCall, call, ctx);
98
+ if (verdict.kind === "continue") return { kind: "allow", call: verdict.value };
99
+ return verdict;
91
100
  }
92
101
 
93
- private async deployGlobalConfig(): Promise<void> {
94
- if (this.claudeMdWritten) return;
95
- const home = await this.getHomeDir();
96
-
97
- await this.sandbox.commands.run(`mkdir -p "${home}/.claude" && find "${home}/.claude" -name "*.json" -not -name "CLAUDE.md" -delete 2>/dev/null || true`, { timeoutMs: 5000 });
98
- await this.sandbox.files.write(`${home}/.claude/CLAUDE.md`, this.claudeMdContent);
99
- // Write env overrides to settings.json so the CLI picks them up at startup.
100
- // This is how the CLI reads ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, etc.
101
- if (Object.keys(this.configEnv).length) {
102
- const settings = JSON.stringify({ env: this.configEnv });
103
- const b64 = Buffer.from(settings).toString("base64");
104
- await this.sandbox.commands.run(`echo "${b64}" | base64 -d > "${home}/.claude/settings.json"`, { timeoutMs: 5_000 });
105
- }
106
- // MCP servers: config-level (from createClaudeRuntime) as base, opts-level (from agent definition) overrides.
107
- const mcpServers = this.configMcpServers;
108
- if (mcpServers && Object.keys(mcpServers).length) {
109
- // MCP servers live in ~/.claude.json per Claude Code docs (not ~/.claude/settings.json).
110
- // Use a shell command rather than files.write — Vercel's writeFiles API defaults paths to
111
- // /vercel/sandbox, so absolute paths in the home directory may not resolve correctly.
112
- const b64 = Buffer.from(JSON.stringify({ mcpServers })).toString("base64");
113
- await this.sandbox.commands.run(`echo "${b64}" | base64 -d > "${home}/.claude.json"`, { timeoutMs: 5_000 });
114
- process.stderr.write(`${this.label}[mcp] wrote ${Object.keys(mcpServers).join(",")} to ~/.claude.json\n`);
115
- }
116
- // Configure git in one shot: disable credential prompts, clear credential helper,
117
- // and set a placeholder Authorization header for github.com HTTPS requests.
118
- // The sandbox provider's network policy firewall replaces this header with the
119
- // real token before forwarding — the token is never readable inside the VM.
120
- // (api.github.com is the REST API, not git; git only uses github.com URLs.)
121
- const placeholderB64 = Buffer.from("x-access-token:placeholder").toString("base64");
122
- await this.sandbox.commands.run(
123
- [
124
- `git config --global core.askPass ""`,
125
- `git config --global credential.interactive false`,
126
- `git config --global credential.helper ""`,
127
- `git config --global "http.https://github.com/.extraHeader" "Authorization: Basic ${placeholderB64}"`,
128
- ].join(" && "),
129
- { timeoutMs: 5_000 },
130
- ).catch((e: unknown) => console.warn(`${this.label}[init] git config failed (non-fatal): ${formatError(e)}`));
131
-
132
- try {
133
- await this.sandbox.commands.run("rtk init --global --hook-only --auto-patch", { timeoutMs: 10_000 });
134
- } catch {
135
- console.warn("[Transport] rtk init failed (non-fatal) — token compression disabled");
136
- }
137
- this.claudeMdWritten = true;
138
- }
139
-
140
- /** Stream one CLI invocation, yielding AgentMessages. Resolves when the process exits. */
141
- private async *_runCli(cmd: string, label: string, signal?: AbortSignal): AsyncGenerator<AgentMessage> {
142
- type QueueItem = AgentMessage | { sentinel: "error"; text: string } | null;
143
- const queue: QueueItem[] = [];
144
- let notify: (() => void) | null = null;
145
-
146
- function enqueue(items: AgentMessage[]): void { queue.push(...items); notify?.(); notify = null; }
147
-
148
- let lineBuffer = "";
149
- let stderrBuffer = "";
150
-
151
- const maxTurns = this.options.maxTurns ?? 40;
152
-
153
- function handleStdout(data: string): void {
154
- lineBuffer += data;
155
- const lines = lineBuffer.split("\n");
156
- lineBuffer = lines.pop() ?? "";
157
- for (const line of lines) {
158
- const trimmed = line.trim();
159
- if (!trimmed) continue;
160
- try {
161
- const event = JSON.parse(trimmed) as Record<string, unknown>;
162
- if (event.type === "assistant") {
163
- const content = (event.message as { content?: unknown[] })?.content ?? [];
164
- for (const block of content) {
165
- const b = block as Record<string, unknown>;
166
- if (b.type === "text") process.stdout.write(".");
167
- if (b.type === "tool_use") process.stderr.write(`\n${label} ${b.name}(${JSON.stringify(b.input).slice(0, 80)})`);
168
- }
169
- } else if (event.type === "result") {
170
- process.stderr.write(`\n${label}[result:${event.num_turns}/${maxTurns}/${event.subtype}]\n`);
171
- } else if (event.type === "system") {
172
- const ev = event as Record<string,unknown>;
173
- const mcpSrvs = (ev.mcp_servers as {name:string;status:string}[] | undefined) ?? [];
174
- const mcpStr = mcpSrvs.length ? ` mcp=[${mcpSrvs.map(s=>`${s.name}:${s.status}`).join(",")}]` : " mcp=[]";
175
- process.stderr.write(`\n${label}[init:${ev.session_id}]${mcpStr}\n`);
176
- }
177
- enqueue(translateEvent(event));
178
- } catch { /* non-JSON line */ }
179
- }
180
- }
181
-
182
- process.stderr.write(`${label}[cwd:${this.options.cwd ?? "(none)"}]\n`);
183
- const runPromise = this.sandbox.commands.run(cmd, {
184
- cwd: this.options.cwd,
185
- timeoutMs: NO_TIMEOUT,
186
- onStdout: handleStdout,
187
- onStderr: (data: string) => { stderrBuffer += data; process.stderr.write(`${label}[stderr] ${data.trimEnd()}\n`); },
102
+ async *sendMessage(opts: { prompt: string; sessionId?: string; iteration?: number; signal?: AbortSignal }): AsyncGenerator<AgentMessage> {
103
+ const requestContext = this.options.requestContext ?? RequestContext.fromReserved({
104
+ teamId: "", runId: "", workflowId: "",
105
+ factoryId: null, apiKeyScopes: [], parentRunId: null,
188
106
  });
189
107
 
190
- runPromise
191
- .catch((err: unknown) => {
192
- const e = err as Record<string, unknown>;
193
- if (e && typeof e === "object" && "exitCode" in e) process.stderr.write(`${label}[transport] command failed — exitCode=${e.exitCode} error=${JSON.stringify(e.error)} stdout_tail=${String(e.stdout ?? "").slice(-200)} stderr_tail=${String(e.stderr ?? "").slice(-200)}\n`);
194
- queue.push({ sentinel: "error", text: stderrBuffer.trim() ? `Agent process failed: ${formatError(err)}\n\n${stderrBuffer.trim()}` : `Agent process failed: ${formatError(err)}` });
195
- })
196
- .finally(() => {
197
- if (lineBuffer.trim()) { try { enqueue(translateEvent(JSON.parse(lineBuffer.trim()) as Record<string, unknown>)); } catch { /* ignore */ } }
198
- queue.push(null);
199
- notify?.(); notify = null;
108
+ const preToolHook: HookCallback = async (input, toolUseId, { signal }) => {
109
+ const pre = input as PreToolUseHookInput;
110
+ const call: ToolCall = {
111
+ toolName: pre.tool_name,
112
+ toolInput: (pre.tool_input ?? {}) as Record<string, unknown>,
113
+ toolUseId: toolUseId ?? `${pre.tool_name}-${Date.now()}`,
114
+ };
115
+ const gate = await this.gateToolCall(call, {
116
+ requestContext,
117
+ abortSignal: signal ?? opts.signal ?? new AbortController().signal,
118
+ retryCount: 0,
119
+ agentId: this.options.agentId ?? "agent",
120
+ iteration: opts.iteration ?? 1,
200
121
  });
122
+ if (gate.kind === "allow") return { hookSpecificOutput: { hookEventName: pre.hook_event_name, permissionDecision: "allow", updatedInput: gate.call.toolInput } };
123
+ if (gate.kind === "deny") return { hookSpecificOutput: { hookEventName: pre.hook_event_name, permissionDecision: "deny", permissionDecisionReason: gate.reason } };
124
+ return { continue: false, systemMessage: gate.reason };
125
+ };
201
126
 
202
- while (true) {
203
- while (queue.length > 0) {
204
- const item = queue.shift()!;
205
- if (item === null) return;
206
- if ("sentinel" in item) { yield { type: "error", text: item.text, timestamp: new Date().toISOString() }; return; }
207
- if (signal?.aborted) return;
208
- const msg = item as AgentMessage;
209
- yield (msg.type === "error" && stderrBuffer.trim() && !msg.text.includes(stderrBuffer.trim()))
210
- ? { ...msg, text: `${msg.text}\n\n${stderrBuffer.trim()}` }
211
- : msg;
212
- }
213
- await new Promise<void>(r => { notify = r; });
214
- }
215
- }
216
-
217
- async *sendMessage(opts: { prompt: string; sessionId?: string; signal?: AbortSignal }): AsyncGenerator<AgentMessage> {
218
- await this.deployGlobalConfig();
219
-
220
- if (opts.sessionId) {
221
- await this.sandbox.commands.run(`pkill -x claude 2>/dev/null || true`, { timeoutMs: 10_000 });
222
- }
223
-
224
- const maxTurns = this.options.maxTurns ?? 40;
225
- const model = this.configModel ?? this.options.model ?? DEFAULT_CLAUDE_MODEL;
226
- const label = this.label;
227
-
228
- const promptFile = `/tmp/agent-prompt-${Date.now()}.txt`;
229
- await this.sandbox.files.write(promptFile, opts.prompt);
230
-
231
- // Don't restrict tools when MCP servers are configured — --allowedTools also
232
- // blocks MCP tools, which would prevent the agent from using them.
233
- const hasMcp = this.configMcpServers && Object.keys(this.configMcpServers).length > 0;
234
- const allowedToolsFlag = (!hasMcp && this.options.allowedTools?.length)
235
- ? ["--allowedTools", this.options.allowedTools.join(",")]
236
- : [];
237
-
238
- const buildCmd = (sessionId?: string) => [
239
- "claude", "--dangerously-skip-permissions", "--verbose",
240
- "--output-format", "stream-json",
241
- "--max-turns", String(maxTurns),
242
- "--model", model,
243
- ...allowedToolsFlag,
244
- ...(sessionId ? ["--resume", sessionId] : []),
245
- "-p", `"$(cat ${promptFile})"`,
246
- ].join(" ");
247
-
248
- const MAX_529_RETRIES = 5;
249
- const BACKOFF_MS = [60_000, 120_000, 240_000, 480_000, 600_000];
250
- let resumeSessionId = opts.sessionId;
251
-
252
- for (let attempt = 0; attempt <= MAX_529_RETRIES; attempt++) {
253
- let got529 = false;
254
- let newSession = "";
255
-
256
- for await (const msg of this._runCli(buildCmd(resumeSessionId), label, opts.signal)) {
257
- if (msg.type === "init") newSession = msg.sessionId;
258
- // Intercept 529 — retry instead of propagating
259
- if (msg.type === "error" && msg.text.includes("529")) { got529 = true; break; }
260
- yield msg;
261
- if (msg.type === "done" || msg.type === "error") return;
262
- }
263
-
264
- if (!got529) return;
265
- if (attempt === MAX_529_RETRIES) {
266
- yield { type: "error", text: "Agent process failed: Repeated 529 Overloaded errors — all retries exhausted", timestamp: new Date().toISOString() };
267
- return;
127
+ try {
128
+ let emittedAssistantText = false;
129
+ for await (const message of query({
130
+ prompt: opts.prompt,
131
+ options: {
132
+ tools: this.options.allowedTools,
133
+ allowedTools: this.options.allowedTools,
134
+ maxTurns: this.options.maxTurns,
135
+ model: this.config.model ?? this.options.model ?? DEFAULT_CLAUDE_MODEL,
136
+ outputFormat: this.options.outputFormat,
137
+ thinking: this.config.thinking,
138
+ effort: this.config.effort,
139
+ cwd: this.options.cwd,
140
+ env: { ...process.env, ...(this.config.env ?? {}) },
141
+ pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? "claude",
142
+ ...(this.config.claudeMdContent ? { systemPrompt: { type: "preset" as const, preset: "claude_code" as const, append: this.config.claudeMdContent } } : {}),
143
+ resume: opts.sessionId,
144
+ mcpServers: this.config.mcpServers,
145
+ permissionMode: "acceptEdits",
146
+ hooks: { PreToolUse: [{ hooks: [preToolHook] }] },
147
+ },
148
+ })) {
149
+ const raw = message as unknown as Record<string, unknown>;
150
+ const translated = translateMessage(raw);
151
+ for (const msg of translated) {
152
+ // Claude Agent SDK can surface the same final assistant content both
153
+ // as an assistant text block and again as result.result. Preserve
154
+ // result.result for structured-output-only responses, but don't
155
+ // double-persist identical visible text when assistant text already
156
+ // arrived in the stream.
157
+ if (raw.type === "result" && msg.type === "text" && emittedAssistantText) continue;
158
+ if (msg.type === "text" && raw.type === "assistant") emittedAssistantText = true;
159
+ yield msg;
160
+ }
268
161
  }
269
-
270
- if (newSession) resumeSessionId = newSession;
271
- const wait = BACKOFF_MS[attempt];
272
- process.stderr.write(`\n${label}[529] Overloaded — resuming in ${wait / 1000}s (attempt ${attempt + 1}/${MAX_529_RETRIES})${resumeSessionId ? " with session resume" : ""}\n`);
273
- await new Promise(r => setTimeout(r, wait));
162
+ } catch (err) {
163
+ yield { type: "error", text: formatError(err), timestamp: now() };
274
164
  }
275
165
  }
276
166
  }
277
167
 
278
- export interface ClaudeRuntimeConfig {
279
- /** Global instructions written to ~/.claude/CLAUDE.md in every sandbox. */
280
- claudeMdContent?: string;
281
- /**
282
- * Environment variables set before each CLI invocation.
283
- * Use to configure API provider routing (e.g. OpenRouter):
284
- * env: { ANTHROPIC_BASE_URL: "https://openrouter.ai/api", ANTHROPIC_AUTH_TOKEN: "$OPENROUTER_API_KEY", ANTHROPIC_API_KEY: "" }
285
- */
286
- env?: Record<string, string>;
287
- /** Model to use (e.g. "claude-opus-4-6", "anthropic/claude-sonnet-4-6"). */
288
- model?: string;
289
- /** MCP servers to configure in the sandbox. Written to ~/.claude.json before the first iteration. */
290
- mcpServers?: Record<string, { command: string; args?: string[]; env?: Record<string, string> }>;
291
- }
292
-
293
- /**
294
- * Create a Claude CLI runtime.
295
- *
296
- * Encapsulates everything about how the Claude CLI runs in a sandbox:
297
- * API provider routing, MCP server configuration, and global instructions.
298
- */
299
168
  export function createClaudeRuntime(config: ClaudeRuntimeConfig = {}) {
300
169
  return defineRuntime({
301
- create: (sandbox, opts) => new ClaudeRunner(sandbox, opts, config.claudeMdContent ?? "", config.env ?? {}, config.model, config.mcpServers),
170
+ create: (sandbox, opts) => new ClaudeRunner(sandbox, opts, config),
302
171
  });
303
172
  }
304
173
 
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Vercel AI SDK runtime — model-agnostic agent execution with agent-compose
3
+ * owned coding tools over SandboxProvider.
4
+ */
5
+
6
+ import { streamText, stepCountIs, tool, type LanguageModel } from "ai";
7
+ import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvider, ToolCallGateResult } from "../index.js";
8
+ import { defineRuntime } from "../types/runtime.js";
9
+ import { codingTools, type CodingTool } from "../tools/index.js";
10
+ import { runProcessorChain } from "../processors/runner.js";
11
+ import type { ProcessorContext, ToolCall } from "../processors/processor.js";
12
+ import { RequestContext } from "../request-context/request-context.js";
13
+ import { formatError } from "../utils/errors.js";
14
+
15
+ type AiToolSet = Record<string, ReturnType<typeof tool<Record<string, unknown>, string>>>;
16
+
17
+ export interface VercelRuntimeConfig {
18
+ /** Vercel AI SDK language model (e.g. openai("gpt-5"), anthropic("claude-sonnet-4-5")). */
19
+ model: LanguageModel;
20
+ /** Optional system prompt prepended to every model call. */
21
+ system?: string;
22
+ /** Override/extend the default coding tools. Defaults: Read, Write, Edit, Bash. */
23
+ tools?: readonly CodingTool[];
24
+ }
25
+
26
+ function now(): string { return new Date().toISOString(); }
27
+
28
+ function serialiseOutput(value: unknown): string {
29
+ return typeof value === "string" ? value : JSON.stringify(value ?? "");
30
+ }
31
+
32
+ function toAgentMessages(part: Record<string, unknown>): AgentMessage[] {
33
+ const ts = now();
34
+ switch (part.type) {
35
+ case "text-delta":
36
+ case "text":
37
+ return [{ type: "text", text: String(part.text ?? ""), timestamp: ts }];
38
+ case "reasoning-delta":
39
+ case "reasoning":
40
+ return [{ type: "thinking", text: String(part.text ?? ""), timestamp: ts }];
41
+ case "tool-call":
42
+ return [{
43
+ type: "tool_use",
44
+ toolName: String(part.toolName ?? ""),
45
+ toolInput: (part.input ?? {}) as Record<string, unknown>,
46
+ toolUseId: String(part.toolCallId ?? ""),
47
+ timestamp: ts,
48
+ }];
49
+ case "tool-result":
50
+ return [{
51
+ type: "tool_result",
52
+ toolUseId: String(part.toolCallId ?? ""),
53
+ output: serialiseOutput(part.output),
54
+ isError: false,
55
+ timestamp: ts,
56
+ }];
57
+ case "tool-error":
58
+ return [{
59
+ type: "tool_result",
60
+ toolUseId: String(part.toolCallId ?? ""),
61
+ output: formatError(part.error),
62
+ isError: true,
63
+ timestamp: ts,
64
+ }];
65
+ case "error":
66
+ return [{ type: "error", text: formatError(part.error), timestamp: ts }];
67
+ default:
68
+ return [];
69
+ }
70
+ }
71
+
72
+ export class VercelRunner implements ModelExecutionContract {
73
+ supportsToolCallProcessor = true;
74
+ private readonly tools: readonly CodingTool[];
75
+ private readonly messages: unknown[] = [];
76
+
77
+ constructor(
78
+ private readonly sandbox: SandboxProvider,
79
+ private readonly options: RuntimeOptions,
80
+ private readonly config: VercelRuntimeConfig,
81
+ ) {
82
+ this.tools = config.tools ?? codingTools;
83
+ }
84
+
85
+ async gateToolCall(call: ToolCall, ctx: ProcessorContext): Promise<ToolCallGateResult> {
86
+ const verdict = await runProcessorChain(this.options.processors ?? [], p => p.processToolCall, call, ctx);
87
+ if (verdict.kind === "continue") return { kind: "allow", call: verdict.value };
88
+ return verdict;
89
+ }
90
+
91
+ private buildTools(iteration: number, signal?: AbortSignal): AiToolSet {
92
+ const set: AiToolSet = {};
93
+ for (const t of this.tools) {
94
+ if (this.options.allowedTools && !this.options.allowedTools.includes(t.name)) continue;
95
+ set[t.name] = tool({
96
+ description: t.description,
97
+ inputSchema: t.inputSchema,
98
+ execute: async (input: Record<string, unknown>, execOpts: { toolCallId?: string; abortSignal?: AbortSignal }) => {
99
+ const call: ToolCall = {
100
+ toolName: t.name,
101
+ toolInput: input,
102
+ toolUseId: execOpts.toolCallId ?? `${t.name}-${Date.now()}`,
103
+ };
104
+ const requestContext = this.options.requestContext ?? RequestContext.fromReserved({
105
+ teamId: "", runId: "", workflowId: "",
106
+ factoryId: null, apiKeyScopes: [], parentRunId: null,
107
+ });
108
+ const gate = await this.gateToolCall(call, {
109
+ requestContext,
110
+ abortSignal: execOpts.abortSignal ?? signal ?? new AbortController().signal,
111
+ retryCount: 0,
112
+ agentId: this.options.agentId ?? "agent",
113
+ iteration,
114
+ });
115
+ if (gate.kind === "deny") return gate.reason;
116
+ if (gate.kind === "abort") throw new Error(gate.reason);
117
+ return t.execute(gate.call.toolInput, {
118
+ sandbox: this.sandbox,
119
+ cwd: this.options.cwd,
120
+ abortSignal: execOpts.abortSignal ?? signal,
121
+ });
122
+ },
123
+ });
124
+ }
125
+ return set;
126
+ }
127
+
128
+ async *sendMessage(opts: { prompt: string; sessionId?: string; iteration?: number; signal?: AbortSignal }): AsyncGenerator<AgentMessage> {
129
+ const sessionId = opts.sessionId ?? `vercel-${Date.now()}`;
130
+ yield { type: "init", sessionId, timestamp: now() };
131
+
132
+ const inputMessages = [...this.messages, { role: "user", content: opts.prompt }];
133
+ const result = streamText({
134
+ model: this.config.model,
135
+ ...(this.config.system ? { system: this.config.system } : {}),
136
+ messages: inputMessages as never,
137
+ tools: this.buildTools(opts.iteration ?? 1, opts.signal),
138
+ stopWhen: stepCountIs(this.options.maxTurns ?? 40),
139
+ abortSignal: opts.signal,
140
+ });
141
+
142
+ try {
143
+ for await (const part of result.fullStream as AsyncIterable<Record<string, unknown>>) {
144
+ for (const msg of toAgentMessages(part)) yield msg;
145
+ }
146
+
147
+ const response = await result.response;
148
+ const responseMessages = (response as unknown as { messages?: unknown[] }).messages;
149
+ if (responseMessages) {
150
+ this.messages.length = 0;
151
+ this.messages.push(...responseMessages);
152
+ }
153
+ let usage: Awaited<typeof result.usage> | undefined;
154
+ try { usage = await result.usage; } catch { usage = undefined; }
155
+ if (usage) {
156
+ yield {
157
+ type: "usage",
158
+ inputTokens: usage.inputTokens ?? 0,
159
+ outputTokens: usage.outputTokens ?? 0,
160
+ cacheReadTokens: usage.inputTokenDetails?.cacheReadTokens ?? 0,
161
+ cacheCreationTokens: usage.inputTokenDetails?.cacheWriteTokens ?? 0,
162
+ durationMs: 0,
163
+ numTurns: opts.iteration ?? 1,
164
+ timestamp: now(),
165
+ };
166
+ }
167
+ } catch (err) {
168
+ yield { type: "error", text: formatError(err), timestamp: now() };
169
+ return;
170
+ }
171
+
172
+ yield { type: "done", sessionId, timestamp: now() };
173
+ }
174
+ }
175
+
176
+ export function createVercelRuntime(config: VercelRuntimeConfig) {
177
+ return defineRuntime({
178
+ create: (sandbox, opts) => new VercelRunner(sandbox, opts, config),
179
+ });
180
+ }