@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 +10 -3
- package/dist/index.js +247 -1
- package/dist/runtimes/_cli-agent.d.ts +72 -0
- package/dist/runtimes/amp.d.ts +22 -0
- package/dist/runtimes/codex.d.ts +20 -0
- package/dist/runtimes/openai-desktop.js +242 -1
- package/dist/runtimes/vercel.d.ts +41 -1
- package/dist/runtimes/vercel.js +10 -1
- package/package.json +1 -1
- package/src/index.ts +20 -2
- package/src/runtimes/_cli-agent.ts +161 -0
- package/src/runtimes/amp.ts +94 -0
- package/src/runtimes/codex.ts +109 -0
- package/src/runtimes/vercel.ts +52 -2
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
|
-
/**
|
|
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[]>;
|
package/dist/runtimes/vercel.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,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
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();
|
package/src/runtimes/vercel.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
+
}
|