@agent-compose/sdk 0.6.0 → 0.8.0
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/README.md +66 -39
- package/dist/agent/__tests__/runtime-json-schema.test.d.ts +10 -0
- package/dist/agent/agent-context.d.ts +21 -1
- package/dist/agent/agent-loop.d.ts +24 -1
- package/dist/client.d.ts +338 -534
- package/dist/directives.d.ts +112 -0
- package/dist/display.d.ts +242 -0
- package/dist/errors.d.ts +24 -1
- package/dist/index.d.ts +34 -13
- package/dist/index.js +2984 -861
- package/dist/pause/wrappers.d.ts +31 -9
- package/dist/processors/ask-human.d.ts +30 -0
- package/dist/processors/ask-human.test.d.ts +1 -0
- package/dist/processors/index.d.ts +1 -0
- package/dist/runtimes/_acp-client.d.ts +46 -1
- package/dist/runtimes/_cli-agent.d.ts +58 -4
- package/dist/runtimes/_jsonl-guard.d.ts +103 -0
- package/dist/runtimes/amp.d.ts +2 -2
- package/dist/runtimes/claude-code.d.ts +59 -0
- package/dist/runtimes/claude-code.test.d.ts +14 -0
- package/dist/runtimes/claude.d.ts +16 -0
- package/dist/runtimes/claude.test.d.ts +8 -0
- package/dist/runtimes/codex.d.ts +9 -3
- package/dist/runtimes/cursor.d.ts +9 -0
- package/dist/runtimes/droid.d.ts +9 -0
- package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
- package/dist/runtimes/openai-desktop.js +2922 -861
- package/dist/runtimes/opencode.d.ts +25 -0
- package/dist/runtimes/vercel.js +22 -1
- package/dist/sandbox/devbox.d.ts +42 -0
- package/dist/sandbox/exec-stream.d.ts +14 -0
- package/dist/sandbox/network-policy.d.ts +100 -0
- package/dist/sandbox/provider-def.d.ts +79 -0
- package/dist/sandbox/providers/desktop.d.ts +10 -0
- package/dist/sandbox/providers/e2b.d.ts +17 -0
- package/dist/sandbox/providers/local.d.ts +11 -0
- package/dist/sandbox/providers/vercel.d.ts +18 -0
- package/dist/sandbox/registry.d.ts +45 -0
- package/dist/sandbox/sizes.d.ts +68 -0
- package/dist/sandbox.d.ts +24 -299
- package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
- package/dist/step-invocation/invoker.d.ts +24 -1
- package/dist/step-invocation/protocol.d.ts +13 -0
- package/dist/types/api-compliance.d.ts +71 -0
- package/dist/types/api-conversations.d.ts +492 -0
- package/dist/types/api-factory.d.ts +309 -0
- package/dist/types/api-projects.d.ts +131 -0
- package/dist/types/api-runs.d.ts +377 -0
- package/dist/types/api-scopes.d.ts +102 -0
- package/dist/types/conversation-stream.d.ts +191 -0
- package/dist/types/execution-context.d.ts +12 -2
- package/dist/types/protocol.d.ts +30 -1
- package/dist/types/sandbox-environment.d.ts +8 -5
- package/dist/types/sandbox.d.ts +79 -0
- package/dist/types/workflow-metadata.d.ts +33 -8
- package/dist/types/workflow-plan.d.ts +10 -0
- package/dist/types/workflow.d.ts +18 -193
- package/dist/utils/bundler.d.ts +12 -1
- package/dist/utils/errors.d.ts +9 -1
- package/dist/workflow-steps/index.d.ts +1 -1
- package/dist/workflow-steps/observability.d.ts +8 -1
- package/dist/workflow-steps/runner.d.ts +3 -3
- package/dist/workflow-steps/step.d.ts +15 -1
- package/dist/workflow-steps/types.d.ts +19 -5
- package/dist/workflow-steps/workflow.d.ts +22 -1
- package/dist/workflows/engine.d.ts +3 -2
- package/dist/workflows/invoke-child.d.ts +2 -2
- package/package.json +1 -1
- package/src/agent/agent-context.ts +206 -16
- package/src/agent/agent-loop.ts +40 -4
- package/src/agent/run-agent.ts +9 -1
- package/src/client.ts +909 -621
- package/src/directives.ts +184 -0
- package/src/display.ts +788 -0
- package/src/errors.ts +39 -0
- package/src/index.ts +117 -10
- package/src/pause/wrappers.ts +44 -9
- package/src/processors/ask-human.ts +136 -0
- package/src/processors/index.ts +5 -0
- package/src/runtimes/_acp-client.ts +72 -3
- package/src/runtimes/_cli-agent.ts +171 -38
- package/src/runtimes/_jsonl-guard.ts +219 -0
- package/src/runtimes/claude-code.ts +246 -0
- package/src/runtimes/claude.ts +32 -2
- package/src/runtimes/codex.ts +55 -3
- package/src/runtimes/cursor.ts +59 -0
- package/src/runtimes/droid.ts +63 -0
- package/src/runtimes/openai-desktop.ts +59 -14
- package/src/runtimes/opencode.ts +61 -0
- package/src/sandbox/devbox.ts +48 -0
- package/src/sandbox/exec-stream.ts +48 -0
- package/src/sandbox/network-policy.ts +181 -0
- package/src/sandbox/provider-def.ts +94 -0
- package/src/sandbox/providers/desktop.ts +57 -0
- package/src/sandbox/providers/e2b.ts +354 -0
- package/src/sandbox/providers/local.ts +106 -0
- package/src/sandbox/providers/vercel.ts +331 -0
- package/src/sandbox/registry.ts +198 -0
- package/src/sandbox/sizes.ts +95 -0
- package/src/sandbox.ts +59 -1263
- package/src/step-invocation/invoker.ts +319 -34
- package/src/step-invocation/protocol.ts +19 -0
- package/src/types/api-compliance.ts +79 -0
- package/src/types/api-conversations.ts +522 -0
- package/src/types/api-factory.ts +336 -0
- package/src/types/api-projects.ts +140 -0
- package/src/types/api-runs.ts +412 -0
- package/src/types/api-scopes.ts +102 -0
- package/src/types/conversation-stream.ts +231 -0
- package/src/types/execution-context.ts +10 -2
- package/src/types/protocol.ts +33 -0
- package/src/types/sandbox-environment.ts +28 -9
- package/src/types/sandbox.ts +78 -0
- package/src/types/workflow-metadata.ts +35 -8
- package/src/types/workflow-plan.ts +11 -0
- package/src/types/workflow.ts +25 -280
- package/src/utils/bundler.ts +32 -5
- package/src/utils/errors.ts +34 -2
- package/src/workflow-steps/index.ts +1 -0
- package/src/workflow-steps/observability.ts +19 -8
- package/src/workflow-steps/runner.ts +4 -4
- package/src/workflow-steps/step.ts +49 -1
- package/src/workflow-steps/types.ts +20 -5
- package/src/workflow-steps/workflow.ts +22 -1
- package/src/workflows/engine.ts +3 -2
- package/src/workflows/invoke-child.ts +2 -2
|
@@ -717,7 +717,28 @@ async function corePause(req, coord, kind = "custom") {
|
|
|
717
717
|
|
|
718
718
|
// src/utils/errors.ts
|
|
719
719
|
function formatError(err) {
|
|
720
|
-
|
|
720
|
+
if (!(err instanceof Error)) {
|
|
721
|
+
if (typeof err === "object" && err !== null) {
|
|
722
|
+
const msg = err.message;
|
|
723
|
+
if (typeof msg === "string" && msg.length > 0)
|
|
724
|
+
return msg;
|
|
725
|
+
try {
|
|
726
|
+
return JSON.stringify(err) ?? String(err);
|
|
727
|
+
} catch {
|
|
728
|
+
return String(err);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return String(err);
|
|
732
|
+
}
|
|
733
|
+
const parts = [err.message];
|
|
734
|
+
const seen = new Set([err]);
|
|
735
|
+
let cause = err.cause;
|
|
736
|
+
while (cause != null && !seen.has(cause)) {
|
|
737
|
+
seen.add(cause);
|
|
738
|
+
parts.push(cause instanceof Error ? cause.message : String(cause));
|
|
739
|
+
cause = cause instanceof Error ? cause.cause : undefined;
|
|
740
|
+
}
|
|
741
|
+
return parts.join(": ");
|
|
721
742
|
}
|
|
722
743
|
|
|
723
744
|
// src/runtimes/vercel.ts
|
|
@@ -906,9 +927,6 @@ async function listVercelRuntimeModels() {
|
|
|
906
927
|
const { models } = await gateway.getAvailableModels();
|
|
907
928
|
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));
|
|
908
929
|
}
|
|
909
|
-
// src/types/workflow.ts
|
|
910
|
-
import { z as z4 } from "zod";
|
|
911
|
-
|
|
912
930
|
// src/workflow-steps/types.ts
|
|
913
931
|
var WORKFLOW_BRAND = Symbol.for("@agent-compose/sdk.Workflow");
|
|
914
932
|
|
|
@@ -929,6 +947,8 @@ function extractMetadata(source) {
|
|
|
929
947
|
out.snapshots = Object.freeze({ ...source.snapshots });
|
|
930
948
|
if (source.resources !== undefined)
|
|
931
949
|
out.resources = Object.freeze({ ...source.resources });
|
|
950
|
+
if (source.mergePolicy !== undefined)
|
|
951
|
+
out.mergePolicy = source.mergePolicy;
|
|
932
952
|
if (source.processors !== undefined)
|
|
933
953
|
out.processors = Object.freeze([...source.processors]);
|
|
934
954
|
if (source.connectors !== undefined)
|
|
@@ -989,61 +1009,39 @@ function isWorkflow(value) {
|
|
|
989
1009
|
}
|
|
990
1010
|
|
|
991
1011
|
// src/types/workflow.ts
|
|
992
|
-
function compileRunForm(def, metadata) {
|
|
993
|
-
const unknownSchema = z4.unknown();
|
|
994
|
-
const inputSchema = def.input ?? unknownSchema;
|
|
995
|
-
const outputSchema = def.output ?? unknownSchema;
|
|
996
|
-
const step = {
|
|
997
|
-
name: "run",
|
|
998
|
-
input: inputSchema,
|
|
999
|
-
output: outputSchema,
|
|
1000
|
-
run: async (stepCtx) => {
|
|
1001
|
-
const workflowCtx = {
|
|
1002
|
-
input: stepCtx.input,
|
|
1003
|
-
run: stepCtx.run,
|
|
1004
|
-
requestContext: stepCtx.requestContext,
|
|
1005
|
-
invokeChild: stepCtx.invokeChild,
|
|
1006
|
-
setMetadata: stepCtx.setMetadata,
|
|
1007
|
-
step: stepCtx.step,
|
|
1008
|
-
agentEvents: stepCtx.agentEvents,
|
|
1009
|
-
pause: stepCtx.pause,
|
|
1010
|
-
sleep: stepCtx.sleep,
|
|
1011
|
-
waitForEvent: stepCtx.waitForEvent,
|
|
1012
|
-
processors: metadata.processors ?? []
|
|
1013
|
-
};
|
|
1014
|
-
const sandbox = stepCtx.sandbox;
|
|
1015
|
-
if (!sandbox)
|
|
1016
|
-
throw new Error("legacy run-form workflow requires a sandbox in StepContext");
|
|
1017
|
-
return def.run(workflowCtx, sandbox);
|
|
1018
|
-
}
|
|
1019
|
-
};
|
|
1020
|
-
const workflow = {
|
|
1021
|
-
id: "@run-form",
|
|
1022
|
-
input: inputSchema,
|
|
1023
|
-
output: outputSchema,
|
|
1024
|
-
steps: Object.freeze([step]),
|
|
1025
|
-
metadata
|
|
1026
|
-
};
|
|
1027
|
-
Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
|
|
1028
|
-
return Object.freeze(workflow);
|
|
1029
|
-
}
|
|
1030
1012
|
function defineWorkflow(def) {
|
|
1031
|
-
if ("run" in def)
|
|
1032
|
-
|
|
1013
|
+
if ("run" in def) {
|
|
1014
|
+
throw new Error("defineWorkflow: the legacy run-form (`defineWorkflow({ run(ctx, sandbox) { … } })`) has been removed. " + "Author workflows in step-form — `defineWorkflow({ id, input, output }).step(defineStep(...)).build()` — " + "or run /ac:generate-workflow to scaffold the current shape.");
|
|
1015
|
+
}
|
|
1033
1016
|
return createStepWorkflow(def);
|
|
1034
1017
|
}
|
|
1035
1018
|
// src/types/sandbox-environment.ts
|
|
1019
|
+
import { z as z4 } from "zod";
|
|
1036
1020
|
function defineSandboxEnvironment(env) {
|
|
1037
1021
|
if (typeof env.setup !== "function") {
|
|
1038
1022
|
throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
|
|
1039
1023
|
}
|
|
1040
|
-
|
|
1024
|
+
const input = z4.unknown();
|
|
1025
|
+
const output = z4.unknown();
|
|
1026
|
+
return createStepWorkflow({
|
|
1027
|
+
id: env.name,
|
|
1041
1028
|
...env.description !== undefined ? { description: env.description } : {},
|
|
1042
1029
|
...env.resources !== undefined ? { resources: env.resources } : {},
|
|
1030
|
+
input,
|
|
1031
|
+
output,
|
|
1043
1032
|
snapshots: env.snapshots ?? { saveLatest: true },
|
|
1044
|
-
environmentBuild: true
|
|
1045
|
-
|
|
1046
|
-
|
|
1033
|
+
environmentBuild: true
|
|
1034
|
+
}).step({
|
|
1035
|
+
name: "setup",
|
|
1036
|
+
input,
|
|
1037
|
+
output,
|
|
1038
|
+
run: async (ctx) => {
|
|
1039
|
+
const sandbox = ctx.sandbox;
|
|
1040
|
+
if (!sandbox)
|
|
1041
|
+
throw new Error(`defineSandboxEnvironment(${env.name}): setup requires a sandbox in StepContext`);
|
|
1042
|
+
await env.setup(sandbox);
|
|
1043
|
+
}
|
|
1044
|
+
}).build();
|
|
1047
1045
|
}
|
|
1048
1046
|
// src/processors/processor.ts
|
|
1049
1047
|
var Verdict = {
|
|
@@ -1195,18 +1193,106 @@ function createGatePauseProcessor(opts) {
|
|
|
1195
1193
|
}
|
|
1196
1194
|
};
|
|
1197
1195
|
}
|
|
1196
|
+
// src/processors/ask-human.ts
|
|
1197
|
+
var ASK_USER_QUESTION_TOOL = "AskUserQuestion";
|
|
1198
|
+
function parseAsk(input) {
|
|
1199
|
+
const questions = Array.isArray(input.questions) ? input.questions : [];
|
|
1200
|
+
const first = questions[0] ?? {};
|
|
1201
|
+
const head = typeof first.question === "string" && first.question.trim() ? first.question.trim() : "The agent needs your input to continue.";
|
|
1202
|
+
const extra = questions.length > 1 ? ` (+${questions.length - 1} more question${questions.length > 2 ? "s" : ""})` : "";
|
|
1203
|
+
const options = Array.isArray(first.options) ? first.options.map((o) => String(o?.label ?? "").trim()).filter(Boolean).map((label) => ({ id: label, label })) : [];
|
|
1204
|
+
return { reason: head + extra, options };
|
|
1205
|
+
}
|
|
1206
|
+
function answerText2(decision) {
|
|
1207
|
+
const raw = decision !== null && typeof decision === "object" && "decision" in decision ? decision.decision : decision;
|
|
1208
|
+
return typeof raw === "string" ? raw.trim() : raw == null ? "" : JSON.stringify(raw);
|
|
1209
|
+
}
|
|
1210
|
+
function parseAgentcPause(command) {
|
|
1211
|
+
if (!/(^|\s|&&|;|\|)\s*agentc\s+pause(\s|$)/.test(command))
|
|
1212
|
+
return null;
|
|
1213
|
+
const r = command.match(/--reason(?:=|\s+)(?:"([^"]*)"|'([^']*)'|(\S+))/);
|
|
1214
|
+
const reason = (r?.[1] ?? r?.[2] ?? r?.[3] ?? "The agent needs your input to continue.").trim();
|
|
1215
|
+
const options = [...command.matchAll(/--option(?:=|\s+)(?:"([^"]*)"|'([^']*)'|(\S+))/g)].map((m) => (m[1] ?? m[2] ?? m[3] ?? "").trim()).filter(Boolean).map((label) => ({ id: label, label }));
|
|
1216
|
+
return { reason, options };
|
|
1217
|
+
}
|
|
1218
|
+
function extractAsk(call) {
|
|
1219
|
+
if (call.toolName === ASK_USER_QUESTION_TOOL)
|
|
1220
|
+
return parseAsk(call.toolInput);
|
|
1221
|
+
if (call.toolName === "Bash") {
|
|
1222
|
+
const cmd = call.toolInput?.command;
|
|
1223
|
+
return typeof cmd === "string" ? parseAgentcPause(cmd) : null;
|
|
1224
|
+
}
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
function createAskHumanProcessor(opts = {}) {
|
|
1228
|
+
return {
|
|
1229
|
+
name: "ask-human",
|
|
1230
|
+
async processToolCall(call, ctx) {
|
|
1231
|
+
const ask = extractAsk(call);
|
|
1232
|
+
if (!ask)
|
|
1233
|
+
return Verdict.continue(call);
|
|
1234
|
+
const conn = opts.connection ?? {
|
|
1235
|
+
baseUrl: process.env.AGENT_COMPOSE_URL ?? "",
|
|
1236
|
+
token: process.env.AGENT_COMPOSE_RUN_TOKEN ?? "",
|
|
1237
|
+
runId: process.env.RUN_ID ?? ""
|
|
1238
|
+
};
|
|
1239
|
+
if (!conn.baseUrl || !conn.token || !conn.runId)
|
|
1240
|
+
return Verdict.continue(call);
|
|
1241
|
+
let decision;
|
|
1242
|
+
try {
|
|
1243
|
+
decision = await requestPauseAndAwait({
|
|
1244
|
+
baseUrl: conn.baseUrl,
|
|
1245
|
+
token: conn.token,
|
|
1246
|
+
runId: conn.runId,
|
|
1247
|
+
reason: ask.reason,
|
|
1248
|
+
...ask.options.length ? { options: ask.options } : {},
|
|
1249
|
+
action: { tool: call.toolName, input: call.toolInput },
|
|
1250
|
+
signal: ctx.abortSignal
|
|
1251
|
+
});
|
|
1252
|
+
} catch (err) {
|
|
1253
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1254
|
+
return Verdict.abort(`Could not ask the human — the run could not be paused (${msg}). Stopping rather than guessing an answer.`);
|
|
1255
|
+
}
|
|
1256
|
+
if (decision.status === "resolved") {
|
|
1257
|
+
const answer = answerText2(decision.decision);
|
|
1258
|
+
return Verdict.deny(`The human answered: ${answer || "(no text returned)"}. Continue using this answer.`);
|
|
1259
|
+
}
|
|
1260
|
+
return Verdict.deny(`No answer came back (${decision.status}). Do NOT assume an answer — ask again, or stop and report exactly what you need from a human.`);
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1198
1264
|
// src/client.ts
|
|
1199
1265
|
import { ofetch } from "ofetch";
|
|
1200
1266
|
|
|
1201
1267
|
// src/errors.ts
|
|
1202
1268
|
class AgentComposeError extends Error {
|
|
1203
1269
|
status;
|
|
1204
|
-
|
|
1270
|
+
retryAfterMs;
|
|
1271
|
+
code;
|
|
1272
|
+
capability;
|
|
1273
|
+
constructor(status, message, opts) {
|
|
1205
1274
|
super(message);
|
|
1206
1275
|
this.status = status;
|
|
1207
1276
|
this.name = "AgentComposeError";
|
|
1277
|
+
if (opts?.retryAfterMs !== undefined)
|
|
1278
|
+
this.retryAfterMs = opts.retryAfterMs;
|
|
1279
|
+
if (opts?.code !== undefined)
|
|
1280
|
+
this.code = opts.code;
|
|
1281
|
+
if (opts?.capability !== undefined)
|
|
1282
|
+
this.capability = opts.capability;
|
|
1208
1283
|
}
|
|
1209
1284
|
}
|
|
1285
|
+
function parseRetryAfterMs(header) {
|
|
1286
|
+
if (!header)
|
|
1287
|
+
return;
|
|
1288
|
+
const seconds = Number(header);
|
|
1289
|
+
if (Number.isFinite(seconds))
|
|
1290
|
+
return Math.max(0, seconds * 1000);
|
|
1291
|
+
const date = Date.parse(header);
|
|
1292
|
+
if (!Number.isNaN(date))
|
|
1293
|
+
return Math.max(0, date - Date.now());
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1210
1296
|
|
|
1211
1297
|
// src/sse.ts
|
|
1212
1298
|
async function* parseSseStream(body) {
|
|
@@ -1258,6 +1344,19 @@ async function* parseSseStream(body) {
|
|
|
1258
1344
|
}
|
|
1259
1345
|
}
|
|
1260
1346
|
|
|
1347
|
+
// src/types/conversation-stream.ts
|
|
1348
|
+
function normalizeConversationStreamEvent(eventName, data, sseId) {
|
|
1349
|
+
const name = eventName || (typeof data.event === "string" ? data.event : "message");
|
|
1350
|
+
const dataId = typeof data.id === "number" && Number.isFinite(data.id) ? data.id : undefined;
|
|
1351
|
+
const durableId = sseId !== undefined && Number.isFinite(sseId) && sseId > 0 ? sseId : dataId !== undefined && dataId > 0 ? dataId : undefined;
|
|
1352
|
+
const ev = { ...data, event: name };
|
|
1353
|
+
if (durableId !== undefined)
|
|
1354
|
+
ev.id = durableId;
|
|
1355
|
+
else
|
|
1356
|
+
delete ev.id;
|
|
1357
|
+
return ev;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1261
1360
|
// src/client.ts
|
|
1262
1361
|
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1263
1362
|
var DEFAULT_FACTORY = "default";
|
|
@@ -1294,10 +1393,39 @@ class AgentComposeClient {
|
|
|
1294
1393
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
1295
1394
|
async onResponseError({ response }) {
|
|
1296
1395
|
const body = response._data;
|
|
1297
|
-
throw new AgentComposeError(response.status, body?.error ?? response.statusText
|
|
1396
|
+
throw new AgentComposeError(response.status, body?.error ?? response.statusText, {
|
|
1397
|
+
retryAfterMs: parseRetryAfterMs(response.headers.get("retry-after")),
|
|
1398
|
+
...body?.code ? { code: body.code } : {},
|
|
1399
|
+
...body?.capability ? { capability: body.capability } : {}
|
|
1400
|
+
});
|
|
1298
1401
|
}
|
|
1299
1402
|
});
|
|
1300
1403
|
}
|
|
1404
|
+
async* openSseStream(path, opts) {
|
|
1405
|
+
const headers = {
|
|
1406
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
1407
|
+
Accept: "text/event-stream"
|
|
1408
|
+
};
|
|
1409
|
+
if (opts?.lastEventId && opts.lastEventId > 0) {
|
|
1410
|
+
headers["Last-Event-ID"] = String(opts.lastEventId);
|
|
1411
|
+
}
|
|
1412
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
1413
|
+
headers,
|
|
1414
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
1415
|
+
});
|
|
1416
|
+
if (!res.ok || !res.body) {
|
|
1417
|
+
let message = res.statusText;
|
|
1418
|
+
try {
|
|
1419
|
+
const body = await res.json();
|
|
1420
|
+
if (body.error)
|
|
1421
|
+
message = body.error;
|
|
1422
|
+
} catch {}
|
|
1423
|
+
throw new AgentComposeError(res.status, message, {
|
|
1424
|
+
retryAfterMs: parseRetryAfterMs(res.headers.get("retry-after"))
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
yield* parseSseStream(res.body);
|
|
1428
|
+
}
|
|
1301
1429
|
register(payload) {
|
|
1302
1430
|
const { factorySlug = DEFAULT_FACTORY, ...body } = payload;
|
|
1303
1431
|
return this.fetch(templatePath(factorySlug), { method: "POST", body });
|
|
@@ -1417,6 +1545,22 @@ class AgentComposeClient {
|
|
|
1417
1545
|
getRun(runId) {
|
|
1418
1546
|
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}`);
|
|
1419
1547
|
}
|
|
1548
|
+
async listRuns(opts = {}) {
|
|
1549
|
+
const q2 = new URLSearchParams;
|
|
1550
|
+
if (opts.workflow)
|
|
1551
|
+
q2.set("workflow", opts.workflow);
|
|
1552
|
+
if (opts.search)
|
|
1553
|
+
q2.set("search", opts.search);
|
|
1554
|
+
if (opts.outcome)
|
|
1555
|
+
q2.set("outcome", opts.outcome);
|
|
1556
|
+
if (opts.sort)
|
|
1557
|
+
q2.set("sort", opts.sort);
|
|
1558
|
+
if (opts.limit !== undefined)
|
|
1559
|
+
q2.set("limit", String(opts.limit));
|
|
1560
|
+
const slug = encodeURIComponent(opts.factorySlug ?? "default");
|
|
1561
|
+
const body = await this.fetch(`/api/v1/factories/${slug}/runs${q2.toString() ? `?${q2}` : ""}`);
|
|
1562
|
+
return body.workflows;
|
|
1563
|
+
}
|
|
1420
1564
|
async getRunTimeline(runId, opts) {
|
|
1421
1565
|
const q2 = new URLSearchParams;
|
|
1422
1566
|
if (opts?.limit !== undefined)
|
|
@@ -1426,6 +1570,9 @@ class AgentComposeClient {
|
|
|
1426
1570
|
const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/timeline${q2.toString() ? `?${q2}` : ""}`);
|
|
1427
1571
|
return body.events;
|
|
1428
1572
|
}
|
|
1573
|
+
async getRunFunding(runId) {
|
|
1574
|
+
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/funding`);
|
|
1575
|
+
}
|
|
1429
1576
|
async reportEvent(runId, input) {
|
|
1430
1577
|
const body = {
|
|
1431
1578
|
...input,
|
|
@@ -1469,12 +1616,151 @@ class AgentComposeClient {
|
|
|
1469
1616
|
created: wire.created
|
|
1470
1617
|
};
|
|
1471
1618
|
}
|
|
1472
|
-
async
|
|
1619
|
+
async getFactoryFileBytes(path, opts) {
|
|
1473
1620
|
const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
|
|
1474
1621
|
const q2 = new URLSearchParams({ path });
|
|
1475
1622
|
if (opts?.revision !== undefined)
|
|
1476
1623
|
q2.set("revision", String(opts.revision));
|
|
1477
|
-
|
|
1624
|
+
const buf = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q2}`, { responseType: "arrayBuffer" });
|
|
1625
|
+
return new Uint8Array(buf);
|
|
1626
|
+
}
|
|
1627
|
+
async getFactoryFile(path, opts) {
|
|
1628
|
+
return new TextDecoder().decode(await this.getFactoryFileBytes(path, opts));
|
|
1629
|
+
}
|
|
1630
|
+
listConversations(opts) {
|
|
1631
|
+
const q2 = new URLSearchParams;
|
|
1632
|
+
if (opts?.cursor)
|
|
1633
|
+
q2.set("cursor", opts.cursor);
|
|
1634
|
+
return this.fetch(`/api/v1/conversations${q2.toString() ? `?${q2}` : ""}`);
|
|
1635
|
+
}
|
|
1636
|
+
listSessions(factoryId, opts) {
|
|
1637
|
+
const q2 = new URLSearchParams({ factoryId });
|
|
1638
|
+
if (opts?.cursor)
|
|
1639
|
+
q2.set("cursor", opts.cursor);
|
|
1640
|
+
return this.fetch(`/api/v1/sessions?${q2}`);
|
|
1641
|
+
}
|
|
1642
|
+
getConversation(id) {
|
|
1643
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(id)}`);
|
|
1644
|
+
}
|
|
1645
|
+
createCloudSession(input) {
|
|
1646
|
+
return this.fetch(`/api/v1/sessions/cloud`, {
|
|
1647
|
+
method: "POST",
|
|
1648
|
+
body: {
|
|
1649
|
+
...input.type ? { type: input.type } : {},
|
|
1650
|
+
...input.runtime ? { runtime: input.runtime } : {},
|
|
1651
|
+
factoryId: input.factoryId,
|
|
1652
|
+
...input.presetId ? { presetId: input.presetId } : {},
|
|
1653
|
+
...input.preStart ? { preStart: input.preStart } : {},
|
|
1654
|
+
...input.title ? { title: input.title } : {},
|
|
1655
|
+
...input.repoUrl ? { repoUrl: input.repoUrl } : {},
|
|
1656
|
+
...input.repoBranch ? { repoBranch: input.repoBranch } : {},
|
|
1657
|
+
...input.channelId ? { channelId: input.channelId } : {},
|
|
1658
|
+
...input.templateId ? { templateId: input.templateId } : {},
|
|
1659
|
+
...input.sandboxSize ? { sandboxSize: input.sandboxSize } : {},
|
|
1660
|
+
...input.networkPolicy !== undefined ? { networkPolicy: input.networkPolicy } : {},
|
|
1661
|
+
...input.connectorProfileId !== undefined ? { connectorProfileId: input.connectorProfileId } : {}
|
|
1662
|
+
}
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
getConversationThread(conversationId, rootId) {
|
|
1666
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/threads/${encodeURIComponent(rootId)}`);
|
|
1667
|
+
}
|
|
1668
|
+
async listChannelSessions(channelId) {
|
|
1669
|
+
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(channelId)}/sessions`);
|
|
1670
|
+
return body.sessions;
|
|
1671
|
+
}
|
|
1672
|
+
async attachChannelSession(channelId, sessionConversationId) {
|
|
1673
|
+
await this.fetch(`/api/v1/conversations/${encodeURIComponent(channelId)}/sessions`, { method: "POST", body: { sessionConversationId } });
|
|
1674
|
+
}
|
|
1675
|
+
async detachChannelSession(channelId, sessionConversationId) {
|
|
1676
|
+
await this.fetch(`/api/v1/conversations/${encodeURIComponent(channelId)}/sessions/${encodeURIComponent(sessionConversationId)}`, { method: "DELETE" });
|
|
1677
|
+
}
|
|
1678
|
+
postSessionChannelMessage(channelId, input) {
|
|
1679
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(channelId)}/session-messages`, { method: "POST", body: { text: input.text, ...input.threadRootId ? { threadRootId: input.threadRootId } : {} } });
|
|
1680
|
+
}
|
|
1681
|
+
openPreview(conversationId, input) {
|
|
1682
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/preview`, {
|
|
1683
|
+
method: "POST",
|
|
1684
|
+
body: {
|
|
1685
|
+
port: input.port,
|
|
1686
|
+
...input.name ? { name: input.name } : {},
|
|
1687
|
+
...input.path ? { path: input.path } : {}
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
async listPreviews(conversationId) {
|
|
1692
|
+
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/previews`);
|
|
1693
|
+
return body.previews;
|
|
1694
|
+
}
|
|
1695
|
+
async closePreview(conversationId, port) {
|
|
1696
|
+
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/preview/${port}`, { method: "DELETE" });
|
|
1697
|
+
return body.closed;
|
|
1698
|
+
}
|
|
1699
|
+
forkSession(conversationId, input) {
|
|
1700
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/fork`, { method: "POST", body: input?.title ? { title: input.title } : {} });
|
|
1701
|
+
}
|
|
1702
|
+
getSessionChanges(conversationId) {
|
|
1703
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes`);
|
|
1704
|
+
}
|
|
1705
|
+
mergeSessionChanges(conversationId, opts = {}) {
|
|
1706
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/merge`, { method: "POST", body: opts.branch ? { branch: opts.branch } : {} });
|
|
1707
|
+
}
|
|
1708
|
+
discardSessionChanges(conversationId, opts = {}) {
|
|
1709
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/changes/discard`, { method: "POST", body: opts.branch ? { branch: opts.branch } : {} });
|
|
1710
|
+
}
|
|
1711
|
+
async getConversationMembers(conversationId) {
|
|
1712
|
+
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/members`);
|
|
1713
|
+
return body.members;
|
|
1714
|
+
}
|
|
1715
|
+
async addConversationMembers(conversationId, members) {
|
|
1716
|
+
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/members`, { method: "POST", body: { members } });
|
|
1717
|
+
return body.members;
|
|
1718
|
+
}
|
|
1719
|
+
async removeConversationMember(conversationId, userId) {
|
|
1720
|
+
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/members/${encodeURIComponent(userId)}`, { method: "DELETE" });
|
|
1721
|
+
return body.members;
|
|
1722
|
+
}
|
|
1723
|
+
async setConversationMemberRole(conversationId, userId, role) {
|
|
1724
|
+
const body = await this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/members/${encodeURIComponent(userId)}`, { method: "PATCH", body: { role } });
|
|
1725
|
+
return body.members;
|
|
1726
|
+
}
|
|
1727
|
+
sendConversationMessage(conversationId, input) {
|
|
1728
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/messages`, {
|
|
1729
|
+
method: "POST",
|
|
1730
|
+
body: {
|
|
1731
|
+
text: input.text,
|
|
1732
|
+
...input.threadRootId ? { threadRootId: input.threadRootId } : {},
|
|
1733
|
+
...input.pageContext ? { pageContext: input.pageContext } : {}
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
prewarmConversation(conversationId) {
|
|
1738
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/prewarm`, { method: "POST", body: {} });
|
|
1739
|
+
}
|
|
1740
|
+
cancelConversationTurn(conversationId) {
|
|
1741
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/cancel-turn`, { method: "POST", body: {} });
|
|
1742
|
+
}
|
|
1743
|
+
heartbeatConversationPresence(conversationId, input) {
|
|
1744
|
+
return this.fetch(`/api/v1/conversations/${encodeURIComponent(conversationId)}/presence`, { method: "POST", body: input });
|
|
1745
|
+
}
|
|
1746
|
+
async* streamConversation(conversationId, opts) {
|
|
1747
|
+
const path = `/api/v1/conversations/${encodeURIComponent(conversationId)}/stream`;
|
|
1748
|
+
for await (const ev of this.openSseStream(path, opts)) {
|
|
1749
|
+
yield normalizeConversationStreamEvent(ev.event, ev.data, ev.id > 0 ? ev.id : undefined);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
async listAgents() {
|
|
1753
|
+
const body = await this.fetch("/api/v1/agents");
|
|
1754
|
+
return body.agents;
|
|
1755
|
+
}
|
|
1756
|
+
searchFactoryFiles(query, opts) {
|
|
1757
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1758
|
+
const q2 = new URLSearchParams({ q: query });
|
|
1759
|
+
if (opts?.limit != null)
|
|
1760
|
+
q2.set("limit", String(opts.limit));
|
|
1761
|
+
if (opts?.cursor)
|
|
1762
|
+
q2.set("cursor", opts.cursor);
|
|
1763
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/search?${q2}`);
|
|
1478
1764
|
}
|
|
1479
1765
|
async listMembers() {
|
|
1480
1766
|
const body = await this.fetch("/api/v1/team/members");
|
|
@@ -1520,6 +1806,88 @@ class AgentComposeClient {
|
|
|
1520
1806
|
const body = await this.fetch(path);
|
|
1521
1807
|
return body.templates;
|
|
1522
1808
|
}
|
|
1809
|
+
getTemplate(name, opts) {
|
|
1810
|
+
return this.fetch(templatePath(opts?.factorySlug ?? DEFAULT_FACTORY, name));
|
|
1811
|
+
}
|
|
1812
|
+
setTemplateScope(name, input, opts) {
|
|
1813
|
+
return this.fetch(templatePath(opts?.factorySlug ?? DEFAULT_FACTORY, name, "scope"), {
|
|
1814
|
+
method: "PUT",
|
|
1815
|
+
body: {
|
|
1816
|
+
grants: input.grants,
|
|
1817
|
+
...input.conversationId !== undefined ? { conversationId: input.conversationId } : {}
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
setFileScope(path, input, opts) {
|
|
1822
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1823
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/scope`, { method: "PUT", body: { path, grants: input.grants } });
|
|
1824
|
+
}
|
|
1825
|
+
async shareFileWithSession(factorySlug, input) {
|
|
1826
|
+
await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/session-grants`, { method: "POST", body: { path: input.path, conversationId: input.conversationId } });
|
|
1827
|
+
}
|
|
1828
|
+
async unshareFileWithSession(factorySlug, input) {
|
|
1829
|
+
await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/session-grants`, { method: "DELETE", body: { path: input.path, conversationId: input.conversationId } });
|
|
1830
|
+
}
|
|
1831
|
+
listProjects(opts) {
|
|
1832
|
+
const q2 = new URLSearchParams;
|
|
1833
|
+
if (opts?.cursor)
|
|
1834
|
+
q2.set("cursor", opts.cursor);
|
|
1835
|
+
return this.fetch(`/api/v1/projects${q2.toString() ? `?${q2}` : ""}`);
|
|
1836
|
+
}
|
|
1837
|
+
async createProject(name) {
|
|
1838
|
+
const body = await this.fetch("/api/v1/projects", {
|
|
1839
|
+
method: "POST",
|
|
1840
|
+
body: { name }
|
|
1841
|
+
});
|
|
1842
|
+
return body.project;
|
|
1843
|
+
}
|
|
1844
|
+
async getProject(id) {
|
|
1845
|
+
const body = await this.fetch(`/api/v1/projects/${encodeURIComponent(id)}`);
|
|
1846
|
+
return body.project;
|
|
1847
|
+
}
|
|
1848
|
+
async renameProject(id, name) {
|
|
1849
|
+
const body = await this.fetch(`/api/v1/projects/${encodeURIComponent(id)}`, { method: "PATCH", body: { name } });
|
|
1850
|
+
return body.project;
|
|
1851
|
+
}
|
|
1852
|
+
async deleteProject(id) {
|
|
1853
|
+
await this.fetch(`/api/v1/projects/${encodeURIComponent(id)}`, {
|
|
1854
|
+
method: "DELETE"
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
async getProjectMembers(id) {
|
|
1858
|
+
const body = await this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/members`);
|
|
1859
|
+
return body.members;
|
|
1860
|
+
}
|
|
1861
|
+
async addProjectMembers(id, members) {
|
|
1862
|
+
const body = await this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/members`, { method: "POST", body: { members } });
|
|
1863
|
+
return body.members;
|
|
1864
|
+
}
|
|
1865
|
+
async removeProjectMember(id, userId) {
|
|
1866
|
+
const body = await this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/members/${encodeURIComponent(userId)}`, { method: "DELETE" });
|
|
1867
|
+
return body.members;
|
|
1868
|
+
}
|
|
1869
|
+
async setProjectMemberRole(id, userId, role) {
|
|
1870
|
+
const body = await this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/members/${encodeURIComponent(userId)}`, { method: "PATCH", body: { role } });
|
|
1871
|
+
return body.members;
|
|
1872
|
+
}
|
|
1873
|
+
listProjectObjects(id, opts) {
|
|
1874
|
+
const q2 = new URLSearchParams;
|
|
1875
|
+
if (opts?.cursor)
|
|
1876
|
+
q2.set("cursor", opts.cursor);
|
|
1877
|
+
return this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/objects${q2.toString() ? `?${q2}` : ""}`);
|
|
1878
|
+
}
|
|
1879
|
+
previewProjectObject(id, input) {
|
|
1880
|
+
return this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/objects/preview`, { method: "POST", body: { type: "session", conversationId: input.conversationId } });
|
|
1881
|
+
}
|
|
1882
|
+
addProjectObject(id, input) {
|
|
1883
|
+
return this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/objects`, { method: "POST", body: input });
|
|
1884
|
+
}
|
|
1885
|
+
refreshProjectObject(id, objectId) {
|
|
1886
|
+
return this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/objects/${encodeURIComponent(objectId)}/refresh`, { method: "POST", body: {} });
|
|
1887
|
+
}
|
|
1888
|
+
async removeProjectObject(id, objectId) {
|
|
1889
|
+
await this.fetch(`/api/v1/projects/${encodeURIComponent(id)}/objects/${encodeURIComponent(objectId)}`, { method: "DELETE" });
|
|
1890
|
+
}
|
|
1523
1891
|
async listFactories() {
|
|
1524
1892
|
const body = await this.fetch("/api/v1/factories");
|
|
1525
1893
|
return body.factories;
|
|
@@ -1557,15 +1925,32 @@ class AgentComposeClient {
|
|
|
1557
1925
|
body: { key, value }
|
|
1558
1926
|
});
|
|
1559
1927
|
}
|
|
1560
|
-
async
|
|
1561
|
-
const
|
|
1562
|
-
const body = await this.fetch(templatePath(factorySlug, workflowName, "secrets"));
|
|
1928
|
+
async listSecretEntries(path) {
|
|
1929
|
+
const body = await this.fetch(path);
|
|
1563
1930
|
return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
|
|
1564
1931
|
}
|
|
1932
|
+
listSecrets(workflowName, opts) {
|
|
1933
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1934
|
+
return this.listSecretEntries(templatePath(factorySlug, workflowName, "secrets"));
|
|
1935
|
+
}
|
|
1565
1936
|
deleteSecret(workflowName, key, opts) {
|
|
1566
1937
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1567
1938
|
return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
|
|
1568
1939
|
}
|
|
1940
|
+
async listRepoLinks(opts) {
|
|
1941
|
+
const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1942
|
+
const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}/repo-links`);
|
|
1943
|
+
return body.links;
|
|
1944
|
+
}
|
|
1945
|
+
async createRepoLink(input, opts) {
|
|
1946
|
+
const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1947
|
+
const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}/repo-links`, { method: "POST", body: input });
|
|
1948
|
+
return body.link;
|
|
1949
|
+
}
|
|
1950
|
+
deleteRepoLink(linkId, opts) {
|
|
1951
|
+
const slug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1952
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(slug)}/repo-links/${encodeURIComponent(linkId)}`, { method: "DELETE" });
|
|
1953
|
+
}
|
|
1569
1954
|
setFactorySecret(key, value, opts) {
|
|
1570
1955
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1571
1956
|
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`, {
|
|
@@ -1573,10 +1958,9 @@ class AgentComposeClient {
|
|
|
1573
1958
|
body: { key, value }
|
|
1574
1959
|
});
|
|
1575
1960
|
}
|
|
1576
|
-
|
|
1961
|
+
listFactorySecrets(opts) {
|
|
1577
1962
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1578
|
-
|
|
1579
|
-
return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
|
|
1963
|
+
return this.listSecretEntries(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`);
|
|
1580
1964
|
}
|
|
1581
1965
|
deleteFactorySecret(key, opts) {
|
|
1582
1966
|
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
@@ -1596,94 +1980,555 @@ class AgentComposeClient {
|
|
|
1596
1980
|
cancelRun(runId) {
|
|
1597
1981
|
return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
|
|
1598
1982
|
}
|
|
1599
|
-
async
|
|
1600
|
-
const
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1983
|
+
async requestComplianceSession(input) {
|
|
1984
|
+
const body = await this.fetch("/api/v1/compliance/sessions", {
|
|
1985
|
+
method: "POST",
|
|
1986
|
+
body: {
|
|
1987
|
+
scopeKind: input.scopeKind,
|
|
1988
|
+
reason: input.reason,
|
|
1989
|
+
ttlSeconds: input.ttlSeconds,
|
|
1990
|
+
...input.scopeRef != null ? { scopeRef: input.scopeRef } : {}
|
|
1991
|
+
}
|
|
1607
1992
|
});
|
|
1608
|
-
|
|
1609
|
-
let message = res.statusText;
|
|
1610
|
-
try {
|
|
1611
|
-
const body = await res.json();
|
|
1612
|
-
if (body.error)
|
|
1613
|
-
message = body.error;
|
|
1614
|
-
} catch {}
|
|
1615
|
-
throw new AgentComposeError(res.status, message);
|
|
1616
|
-
}
|
|
1617
|
-
for await (const ev of parseSseStream(res.body)) {
|
|
1618
|
-
yield ev.data;
|
|
1619
|
-
}
|
|
1993
|
+
return body.session;
|
|
1620
1994
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
import { parse as babelParse } from "@babel/parser";
|
|
1625
|
-
|
|
1626
|
-
// src/utils/source-loader.ts
|
|
1627
|
-
import { dirname } from "path";
|
|
1628
|
-
import { writeFile, rm, mkdir } from "node:fs/promises";
|
|
1629
|
-
var TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
|
|
1630
|
-
var LATEST_VERSION = "$LATEST";
|
|
1631
|
-
async function importSourceModule(source, tmpPath) {
|
|
1632
|
-
await mkdir(dirname(tmpPath), { recursive: true });
|
|
1633
|
-
await writeFile(tmpPath, source, "utf8");
|
|
1634
|
-
const mod = await import(tmpPath);
|
|
1635
|
-
const cleanup = () => rm(tmpPath, { force: true });
|
|
1636
|
-
return { mod, cleanup };
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// src/types/workflow-plan.ts
|
|
1640
|
-
function workflowPlan(steps) {
|
|
1641
|
-
return { steps };
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
// src/utils/bundler.ts
|
|
1645
|
-
var BUNDLER_VERSION = 2;
|
|
1646
|
-
|
|
1647
|
-
class WorkflowSourceValidationError extends Error {
|
|
1648
|
-
constructor(message) {
|
|
1649
|
-
super(message);
|
|
1650
|
-
this.name = "WorkflowSourceValidationError";
|
|
1995
|
+
async approveComplianceSession(sessionId) {
|
|
1996
|
+
const body = await this.fetch(`/api/v1/compliance/sessions/${encodeURIComponent(sessionId)}/approve`, { method: "POST" });
|
|
1997
|
+
return body.session;
|
|
1651
1998
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
if (!bun.Bun?.build)
|
|
1656
|
-
throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
|
|
1657
|
-
const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "node" });
|
|
1658
|
-
if (!result.success) {
|
|
1659
|
-
throw new Error(`Failed to bundle ${label}:
|
|
1660
|
-
${result.logs.map((l) => l.message).join(`
|
|
1661
|
-
`)}`);
|
|
1999
|
+
async revokeComplianceSession(sessionId) {
|
|
2000
|
+
const body = await this.fetch(`/api/v1/compliance/sessions/${encodeURIComponent(sessionId)}/revoke`, { method: "POST" });
|
|
2001
|
+
return body.session;
|
|
1662
2002
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
}
|
|
1672
|
-
await loaded.cleanup();
|
|
1673
|
-
}
|
|
1674
|
-
} catch (err) {
|
|
1675
|
-
throw new WorkflowSourceValidationError(`Bundled ${label} could not be evaluated: ${err instanceof Error ? err.message : String(err)}`);
|
|
2003
|
+
listComplianceSessions(opts) {
|
|
2004
|
+
const q2 = new URLSearchParams;
|
|
2005
|
+
if (opts?.status)
|
|
2006
|
+
q2.set("status", opts.status);
|
|
2007
|
+
if (opts?.limit != null)
|
|
2008
|
+
q2.set("limit", String(opts.limit));
|
|
2009
|
+
if (opts?.cursor)
|
|
2010
|
+
q2.set("cursor", opts.cursor);
|
|
2011
|
+
return this.fetch(`/api/v1/compliance/sessions${q2.toString() ? `?${q2}` : ""}`);
|
|
1676
2012
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2013
|
+
listComplianceAccesses(sessionId, opts) {
|
|
2014
|
+
const q2 = new URLSearchParams;
|
|
2015
|
+
if (opts?.limit != null)
|
|
2016
|
+
q2.set("limit", String(opts.limit));
|
|
2017
|
+
if (opts?.cursor)
|
|
2018
|
+
q2.set("cursor", opts.cursor);
|
|
2019
|
+
return this.fetch(`/api/v1/compliance/sessions/${encodeURIComponent(sessionId)}/accesses${q2.toString() ? `?${q2}` : ""}`);
|
|
1682
2020
|
}
|
|
1683
|
-
|
|
1684
|
-
|
|
2021
|
+
async* streamRunLogs(runId, opts) {
|
|
2022
|
+
const path = `/api/v1/workflows/${encodeURIComponent(runId)}/stream`;
|
|
2023
|
+
for await (const ev of this.openSseStream(path, opts)) {
|
|
2024
|
+
yield ev.data;
|
|
2025
|
+
}
|
|
1685
2026
|
}
|
|
1686
|
-
|
|
2027
|
+
}
|
|
2028
|
+
// src/directives.ts
|
|
2029
|
+
var DIRECTIVE_MARKER_PREFIX = "[[ac:directive:v1 ";
|
|
2030
|
+
var DIRECTIVE_MARKER_SUFFIX = "]]";
|
|
2031
|
+
var MAX_DIRECTIVES_PER_OUTPUT = 4;
|
|
2032
|
+
var RUN_ID_RE = /^(latest|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
2033
|
+
var REVISION_SELECTOR_RE = /^(\d{1,12}|head|prev|head~[1-9]\d{0,2})$/;
|
|
2034
|
+
var DIRECTIVE_PATH_MAX = 512;
|
|
2035
|
+
var ASK_DIRECTIVE_PROMPT_MAX = 500;
|
|
2036
|
+
var ASK_DIRECTIVE_MAX_OPTIONS = 6;
|
|
2037
|
+
var ASK_DIRECTIVE_OPTION_ID_MAX = 50;
|
|
2038
|
+
var ASK_DIRECTIVE_OPTION_LABEL_MAX = 60;
|
|
2039
|
+
var ASK_APPROVER_MIN = 3;
|
|
2040
|
+
var ASK_APPROVER_MAX = 320;
|
|
2041
|
+
function validDirectivePath(v) {
|
|
2042
|
+
return typeof v === "string" && v.length >= 1 && v.length <= DIRECTIVE_PATH_MAX ? v : null;
|
|
2043
|
+
}
|
|
2044
|
+
function renderDirectiveMarker(directive) {
|
|
2045
|
+
return `${DIRECTIVE_MARKER_PREFIX}${JSON.stringify(directive)}${DIRECTIVE_MARKER_SUFFIX}`;
|
|
2046
|
+
}
|
|
2047
|
+
function validDirective(value) {
|
|
2048
|
+
if (typeof value !== "object" || value === null)
|
|
2049
|
+
return null;
|
|
2050
|
+
const d = value;
|
|
2051
|
+
if (d.kind === "navigate") {
|
|
2052
|
+
const url = d.url;
|
|
2053
|
+
if (typeof url !== "string" || url.length === 0 || url.length > 400)
|
|
2054
|
+
return null;
|
|
2055
|
+
if (!url.startsWith("/") || url.startsWith("//"))
|
|
2056
|
+
return null;
|
|
2057
|
+
return { kind: "navigate", url };
|
|
2058
|
+
}
|
|
2059
|
+
if (d.kind === "run" || d.kind === "run_changes") {
|
|
2060
|
+
const runId = d.runId;
|
|
2061
|
+
if (typeof runId !== "string" || !RUN_ID_RE.test(runId))
|
|
2062
|
+
return null;
|
|
2063
|
+
const workflow = d.workflow;
|
|
2064
|
+
if (workflow !== undefined && (typeof workflow !== "string" || workflow.length === 0 || workflow.length > 255)) {
|
|
2065
|
+
return null;
|
|
2066
|
+
}
|
|
2067
|
+
return { kind: d.kind, runId, ...workflow !== undefined ? { workflow } : {} };
|
|
2068
|
+
}
|
|
2069
|
+
if (d.kind === "preview" || d.kind === "table_file") {
|
|
2070
|
+
const path = validDirectivePath(d.path);
|
|
2071
|
+
if (!path)
|
|
2072
|
+
return null;
|
|
2073
|
+
if (d.kind === "table_file" && !/\.(csv|json)$/i.test(path))
|
|
2074
|
+
return null;
|
|
2075
|
+
return { kind: d.kind, path };
|
|
2076
|
+
}
|
|
2077
|
+
if (d.kind === "file_diff") {
|
|
2078
|
+
const path = validDirectivePath(d.path);
|
|
2079
|
+
if (!path)
|
|
2080
|
+
return null;
|
|
2081
|
+
const { from, to } = d;
|
|
2082
|
+
if (typeof from !== "string" || !REVISION_SELECTOR_RE.test(from))
|
|
2083
|
+
return null;
|
|
2084
|
+
if (typeof to !== "string" || !REVISION_SELECTOR_RE.test(to))
|
|
2085
|
+
return null;
|
|
2086
|
+
return { kind: "file_diff", path, from, to };
|
|
2087
|
+
}
|
|
2088
|
+
if (d.kind === "ask") {
|
|
2089
|
+
const { prompt, approver } = d;
|
|
2090
|
+
if (typeof prompt !== "string" || prompt.length < 1 || prompt.length > ASK_DIRECTIVE_PROMPT_MAX)
|
|
2091
|
+
return null;
|
|
2092
|
+
if (typeof approver !== "string" || approver.length < ASK_APPROVER_MIN || approver.length > ASK_APPROVER_MAX) {
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
let options;
|
|
2096
|
+
if (d.options !== undefined) {
|
|
2097
|
+
if (!Array.isArray(d.options) || d.options.length === 0 || d.options.length > ASK_DIRECTIVE_MAX_OPTIONS) {
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
2100
|
+
options = [];
|
|
2101
|
+
for (const raw of d.options) {
|
|
2102
|
+
if (typeof raw !== "object" || raw === null)
|
|
2103
|
+
return null;
|
|
2104
|
+
const { id, label } = raw;
|
|
2105
|
+
if (typeof id !== "string" || id.length < 1 || id.length > ASK_DIRECTIVE_OPTION_ID_MAX)
|
|
2106
|
+
return null;
|
|
2107
|
+
if (typeof label !== "string" || label.length < 1 || label.length > ASK_DIRECTIVE_OPTION_LABEL_MAX) {
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
options.push({ id, label });
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return { kind: "ask", prompt, approver, ...options !== undefined ? { options } : {} };
|
|
2114
|
+
}
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
function parseDirectiveMarkers(text) {
|
|
2118
|
+
const out = [];
|
|
2119
|
+
let from = 0;
|
|
2120
|
+
while (out.length < MAX_DIRECTIVES_PER_OUTPUT) {
|
|
2121
|
+
const start = text.indexOf(DIRECTIVE_MARKER_PREFIX, from);
|
|
2122
|
+
if (start < 0)
|
|
2123
|
+
break;
|
|
2124
|
+
const bodyStart = start + DIRECTIVE_MARKER_PREFIX.length;
|
|
2125
|
+
const end = text.indexOf(DIRECTIVE_MARKER_SUFFIX, bodyStart);
|
|
2126
|
+
if (end < 0)
|
|
2127
|
+
break;
|
|
2128
|
+
from = end + DIRECTIVE_MARKER_SUFFIX.length;
|
|
2129
|
+
const body = text.slice(bodyStart, end);
|
|
2130
|
+
if (body.length > 1024)
|
|
2131
|
+
continue;
|
|
2132
|
+
try {
|
|
2133
|
+
const directive = validDirective(JSON.parse(body));
|
|
2134
|
+
if (directive)
|
|
2135
|
+
out.push(directive);
|
|
2136
|
+
} catch {}
|
|
2137
|
+
}
|
|
2138
|
+
return out;
|
|
2139
|
+
}
|
|
2140
|
+
// src/runtimes/_acp-client.ts
|
|
2141
|
+
import {
|
|
2142
|
+
ClientSideConnection,
|
|
2143
|
+
RequestError
|
|
2144
|
+
} from "@agentclientprotocol/sdk";
|
|
2145
|
+
|
|
2146
|
+
// src/agent/async-queue.ts
|
|
2147
|
+
class AsyncQueue {
|
|
2148
|
+
buffer = [];
|
|
2149
|
+
resolvers = [];
|
|
2150
|
+
done = false;
|
|
2151
|
+
push(value) {
|
|
2152
|
+
if (this.done)
|
|
2153
|
+
return;
|
|
2154
|
+
const resolve = this.resolvers.shift();
|
|
2155
|
+
if (resolve) {
|
|
2156
|
+
resolve({ value, done: false });
|
|
2157
|
+
} else {
|
|
2158
|
+
this.buffer.push(value);
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
close() {
|
|
2162
|
+
if (this.done)
|
|
2163
|
+
return;
|
|
2164
|
+
this.done = true;
|
|
2165
|
+
while (this.resolvers.length > 0) {
|
|
2166
|
+
const resolve = this.resolvers.shift();
|
|
2167
|
+
resolve({ value: undefined, done: true });
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
[Symbol.asyncIterator]() {
|
|
2171
|
+
return {
|
|
2172
|
+
next: () => {
|
|
2173
|
+
const buffered = this.buffer.shift();
|
|
2174
|
+
if (buffered !== undefined)
|
|
2175
|
+
return Promise.resolve({ value: buffered, done: false });
|
|
2176
|
+
if (this.done)
|
|
2177
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
2178
|
+
return new Promise((resolve) => {
|
|
2179
|
+
this.resolvers.push(resolve);
|
|
2180
|
+
});
|
|
2181
|
+
},
|
|
2182
|
+
return: () => {
|
|
2183
|
+
this.close();
|
|
2184
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// src/runtimes/_acp-client.ts
|
|
2191
|
+
function now2() {
|
|
2192
|
+
return new Date().toISOString();
|
|
2193
|
+
}
|
|
2194
|
+
var ACP_PROTOCOL_VERSION = 1;
|
|
2195
|
+
var OPTION_KIND_FOR_VERDICT = {
|
|
2196
|
+
allow: ["allow_once", "allow_always"],
|
|
2197
|
+
deny: ["reject_once", "reject_always"]
|
|
2198
|
+
};
|
|
2199
|
+
function contentBlockText(block) {
|
|
2200
|
+
if (!block)
|
|
2201
|
+
return "";
|
|
2202
|
+
return block.type === "text" ? block.text : "";
|
|
2203
|
+
}
|
|
2204
|
+
function renderToolContent(content) {
|
|
2205
|
+
const parts = [];
|
|
2206
|
+
const diffs = [];
|
|
2207
|
+
for (const block of content ?? []) {
|
|
2208
|
+
if (block.type === "content") {
|
|
2209
|
+
parts.push(contentBlockText(block.content));
|
|
2210
|
+
} else if (block.type === "diff") {
|
|
2211
|
+
diffs.push({ path: block.path, oldText: block.oldText ?? null, newText: block.newText });
|
|
2212
|
+
const header = block.oldText == null ? `--- (new file)
|
|
2213
|
+
+++ ${block.path}` : `--- ${block.path}
|
|
2214
|
+
+++ ${block.path}`;
|
|
2215
|
+
parts.push(`${header}
|
|
2216
|
+
${block.newText}`);
|
|
2217
|
+
} else if (block.type === "terminal") {
|
|
2218
|
+
parts.push(`[terminal ${block.terminalId}]`);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return { output: parts.filter((p) => p !== "").join(`
|
|
2222
|
+
`), diffs };
|
|
2223
|
+
}
|
|
2224
|
+
function mapLocations(locations) {
|
|
2225
|
+
if (!locations || locations.length === 0)
|
|
2226
|
+
return;
|
|
2227
|
+
return locations.map((l) => l.line != null ? { path: l.path, line: l.line } : { path: l.path });
|
|
2228
|
+
}
|
|
2229
|
+
function terminalToolResult(args) {
|
|
2230
|
+
const { output, diffs } = renderToolContent(args.content);
|
|
2231
|
+
const locations = mapLocations(args.locations);
|
|
2232
|
+
return {
|
|
2233
|
+
type: "tool_result",
|
|
2234
|
+
toolUseId: args.toolCallId,
|
|
2235
|
+
output,
|
|
2236
|
+
isError: args.status === "failed",
|
|
2237
|
+
timestamp: args.timestamp,
|
|
2238
|
+
...diffs.length > 0 ? { diffs } : {},
|
|
2239
|
+
...locations ? { locations } : {}
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function normalizeSessionUpdate(update) {
|
|
2243
|
+
const ts = now2();
|
|
2244
|
+
switch (update.sessionUpdate) {
|
|
2245
|
+
case "agent_message_chunk":
|
|
2246
|
+
return [{ type: "text", text: contentBlockText(update.content), timestamp: ts }];
|
|
2247
|
+
case "agent_thought_chunk":
|
|
2248
|
+
return [{ type: "thinking", text: contentBlockText(update.content), timestamp: ts }];
|
|
2249
|
+
case "tool_call": {
|
|
2250
|
+
const messages = [{
|
|
2251
|
+
type: "tool_use",
|
|
2252
|
+
toolName: update.title || update.kind || "tool",
|
|
2253
|
+
toolInput: update.rawInput ?? {},
|
|
2254
|
+
toolUseId: update.toolCallId,
|
|
2255
|
+
timestamp: ts
|
|
2256
|
+
}];
|
|
2257
|
+
if (update.status === "completed" || update.status === "failed") {
|
|
2258
|
+
messages.push(terminalToolResult({
|
|
2259
|
+
toolCallId: update.toolCallId,
|
|
2260
|
+
status: update.status,
|
|
2261
|
+
content: update.content,
|
|
2262
|
+
locations: update.locations,
|
|
2263
|
+
timestamp: ts
|
|
2264
|
+
}));
|
|
2265
|
+
}
|
|
2266
|
+
return messages;
|
|
2267
|
+
}
|
|
2268
|
+
case "tool_call_update": {
|
|
2269
|
+
if (update.status !== "completed" && update.status !== "failed")
|
|
2270
|
+
return [];
|
|
2271
|
+
return [terminalToolResult({
|
|
2272
|
+
toolCallId: update.toolCallId,
|
|
2273
|
+
status: update.status,
|
|
2274
|
+
content: update.content,
|
|
2275
|
+
locations: update.locations,
|
|
2276
|
+
timestamp: ts
|
|
2277
|
+
})];
|
|
2278
|
+
}
|
|
2279
|
+
case "plan":
|
|
2280
|
+
return [{
|
|
2281
|
+
type: "plan",
|
|
2282
|
+
entries: update.entries.map((e) => ({ content: e.content, priority: e.priority, status: e.status })),
|
|
2283
|
+
timestamp: ts
|
|
2284
|
+
}];
|
|
2285
|
+
case "user_message_chunk":
|
|
2286
|
+
case "available_commands_update":
|
|
2287
|
+
case "current_mode_update":
|
|
2288
|
+
case "usage_update":
|
|
2289
|
+
case "plan_update":
|
|
2290
|
+
case "plan_removed":
|
|
2291
|
+
case "config_option_update":
|
|
2292
|
+
case "session_info_update":
|
|
2293
|
+
return [];
|
|
2294
|
+
default:
|
|
2295
|
+
return [];
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
class AcpClientPeer {
|
|
2300
|
+
deps;
|
|
2301
|
+
conn;
|
|
2302
|
+
updates = new AsyncQueue;
|
|
2303
|
+
sessionId;
|
|
2304
|
+
pendingPause = null;
|
|
2305
|
+
cancelled = false;
|
|
2306
|
+
abortPrompt = null;
|
|
2307
|
+
constructor(deps) {
|
|
2308
|
+
this.deps = deps;
|
|
2309
|
+
const handler = {
|
|
2310
|
+
sessionUpdate: (params) => {
|
|
2311
|
+
if (params.update.sessionUpdate === "available_commands_update") {
|
|
2312
|
+
this.deps.onAvailableCommands?.(params.update.availableCommands.map((c) => ({
|
|
2313
|
+
name: c.name,
|
|
2314
|
+
description: c.description
|
|
2315
|
+
})));
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
for (const msg of normalizeSessionUpdate(params.update))
|
|
2319
|
+
this.updates.push(msg);
|
|
2320
|
+
},
|
|
2321
|
+
requestPermission: (params) => this.onRequestPermission(params)
|
|
2322
|
+
};
|
|
2323
|
+
this.conn = new ClientSideConnection((_agent) => handler, this.deps.stream);
|
|
2324
|
+
if (this.deps.signal) {
|
|
2325
|
+
if (this.deps.signal.aborted)
|
|
2326
|
+
this.cancel();
|
|
2327
|
+
else
|
|
2328
|
+
this.deps.signal.addEventListener("abort", () => this.cancel(), { once: true });
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
async initialize() {
|
|
2332
|
+
const res = await this.conn.initialize({
|
|
2333
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2334
|
+
clientCapabilities: {
|
|
2335
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
2336
|
+
terminal: false
|
|
2337
|
+
}
|
|
2338
|
+
});
|
|
2339
|
+
return {
|
|
2340
|
+
protocolVersion: res.protocolVersion,
|
|
2341
|
+
caps: {
|
|
2342
|
+
loadSession: res.agentCapabilities?.loadSession ?? false,
|
|
2343
|
+
resume: res.agentCapabilities?.sessionCapabilities?.resume != null
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
async startSession(cwd) {
|
|
2348
|
+
const res = await this.conn.newSession({ cwd, mcpServers: [] });
|
|
2349
|
+
this.sessionId = res.sessionId;
|
|
2350
|
+
return res.sessionId;
|
|
2351
|
+
}
|
|
2352
|
+
async resumeSession(cwd, acpSessionId) {
|
|
2353
|
+
await this.conn.resumeSession({ sessionId: acpSessionId, cwd, mcpServers: [] });
|
|
2354
|
+
this.sessionId = acpSessionId;
|
|
2355
|
+
}
|
|
2356
|
+
async* prompt(text) {
|
|
2357
|
+
if (!this.sessionId)
|
|
2358
|
+
throw new Error("AcpClientPeer.prompt called before startSession");
|
|
2359
|
+
const updates = new AsyncQueue;
|
|
2360
|
+
this.updates = updates;
|
|
2361
|
+
const aborted = new Promise((resolve) => {
|
|
2362
|
+
this.abortPrompt = () => {
|
|
2363
|
+
resolve();
|
|
2364
|
+
updates.close();
|
|
2365
|
+
};
|
|
2366
|
+
});
|
|
2367
|
+
if (this.cancelled)
|
|
2368
|
+
this.abortPrompt();
|
|
2369
|
+
const promptPromise = this.conn.prompt({ sessionId: this.sessionId, prompt: [{ type: "text", text }] }).then((res) => {
|
|
2370
|
+
updates.close();
|
|
2371
|
+
return { ok: true, res };
|
|
2372
|
+
}, (err) => {
|
|
2373
|
+
updates.close();
|
|
2374
|
+
return { ok: false, err };
|
|
2375
|
+
});
|
|
2376
|
+
for await (const msg of updates) {
|
|
2377
|
+
yield msg;
|
|
2378
|
+
}
|
|
2379
|
+
if (this.pendingPause !== null) {
|
|
2380
|
+
const pause = this.pendingPause;
|
|
2381
|
+
this.pendingPause = null;
|
|
2382
|
+
this.abortPrompt = null;
|
|
2383
|
+
throw pause;
|
|
2384
|
+
}
|
|
2385
|
+
const settled = await Promise.race([
|
|
2386
|
+
promptPromise.then((o) => ({ kind: "prompt", o })),
|
|
2387
|
+
aborted.then(() => ({ kind: "aborted" }))
|
|
2388
|
+
]);
|
|
2389
|
+
this.abortPrompt = null;
|
|
2390
|
+
if (settled.kind === "aborted")
|
|
2391
|
+
return;
|
|
2392
|
+
const outcome = settled.o;
|
|
2393
|
+
if (!outcome.ok) {
|
|
2394
|
+
yield { type: "error", text: formatError(outcome.err), timestamp: now2() };
|
|
2395
|
+
return;
|
|
2396
|
+
}
|
|
2397
|
+
const stopReason = outcome.res.stopReason;
|
|
2398
|
+
if (stopReason === "refusal") {
|
|
2399
|
+
yield { type: "error", text: "agent refused the request", timestamp: now2() };
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
const usage = outcome.res.usage;
|
|
2403
|
+
if (usage) {
|
|
2404
|
+
yield {
|
|
2405
|
+
type: "usage",
|
|
2406
|
+
inputTokens: usage.inputTokens,
|
|
2407
|
+
outputTokens: usage.outputTokens,
|
|
2408
|
+
cacheReadTokens: usage.cachedReadTokens ?? 0,
|
|
2409
|
+
cacheCreationTokens: usage.cachedWriteTokens ?? 0,
|
|
2410
|
+
durationMs: 0,
|
|
2411
|
+
numTurns: 1,
|
|
2412
|
+
timestamp: now2()
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
get currentSessionId() {
|
|
2417
|
+
return this.sessionId;
|
|
2418
|
+
}
|
|
2419
|
+
cancel() {
|
|
2420
|
+
if (this.cancelled) {
|
|
2421
|
+
this.abortPrompt?.();
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
this.cancelled = true;
|
|
2425
|
+
if (this.sessionId) {
|
|
2426
|
+
this.conn.cancel({ sessionId: this.sessionId }).catch(() => {});
|
|
2427
|
+
}
|
|
2428
|
+
this.abortPrompt?.();
|
|
2429
|
+
}
|
|
2430
|
+
close() {
|
|
2431
|
+
this.abortPrompt?.();
|
|
2432
|
+
this.updates.close();
|
|
2433
|
+
}
|
|
2434
|
+
async onRequestPermission(params) {
|
|
2435
|
+
if (this.cancelled || this.deps.signal?.aborted) {
|
|
2436
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2437
|
+
}
|
|
2438
|
+
const tc = params.toolCall;
|
|
2439
|
+
const call = {
|
|
2440
|
+
toolName: tc.title || tc.kind || "tool",
|
|
2441
|
+
toolInput: tc.rawInput ?? {},
|
|
2442
|
+
toolUseId: tc.toolCallId
|
|
2443
|
+
};
|
|
2444
|
+
let verdict;
|
|
2445
|
+
try {
|
|
2446
|
+
verdict = await this.deps.gateToolCall(call, this.deps.procCtx());
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
if (isPauseSignal(err)) {
|
|
2449
|
+
this.pendingPause = err;
|
|
2450
|
+
this.cancel();
|
|
2451
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2452
|
+
}
|
|
2453
|
+
throw err;
|
|
2454
|
+
}
|
|
2455
|
+
if (verdict.kind === "abort") {
|
|
2456
|
+
this.cancel();
|
|
2457
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2458
|
+
}
|
|
2459
|
+
const wantedKinds = verdict.kind === "allow" ? OPTION_KIND_FOR_VERDICT.allow : OPTION_KIND_FOR_VERDICT.deny;
|
|
2460
|
+
const option = params.options.find((o) => wantedKinds.includes(o.kind)) ?? params.options[0];
|
|
2461
|
+
if (!option) {
|
|
2462
|
+
return { outcome: { outcome: "cancelled" } };
|
|
2463
|
+
}
|
|
2464
|
+
return { outcome: { outcome: "selected", optionId: option.optionId } };
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
// src/utils/bundler.ts
|
|
2468
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2469
|
+
import { parse as babelParse } from "@babel/parser";
|
|
2470
|
+
|
|
2471
|
+
// src/utils/source-loader.ts
|
|
2472
|
+
import { dirname } from "path";
|
|
2473
|
+
import { writeFile, rm, mkdir } from "node:fs/promises";
|
|
2474
|
+
var TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
|
|
2475
|
+
var LATEST_VERSION = "$LATEST";
|
|
2476
|
+
async function importSourceModule(source, tmpPath) {
|
|
2477
|
+
await mkdir(dirname(tmpPath), { recursive: true });
|
|
2478
|
+
await writeFile(tmpPath, source, "utf8");
|
|
2479
|
+
const mod = await import(tmpPath);
|
|
2480
|
+
const cleanup = () => rm(tmpPath, { force: true });
|
|
2481
|
+
return { mod, cleanup };
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// src/types/workflow-plan.ts
|
|
2485
|
+
function workflowPlan(steps) {
|
|
2486
|
+
return { steps };
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// src/utils/bundler.ts
|
|
2490
|
+
var BUNDLER_VERSION = 2;
|
|
2491
|
+
|
|
2492
|
+
class WorkflowSourceValidationError extends Error {
|
|
2493
|
+
constructor(message) {
|
|
2494
|
+
super(message);
|
|
2495
|
+
this.name = "WorkflowSourceValidationError";
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
async function bundle(path, label) {
|
|
2499
|
+
const bun = globalThis;
|
|
2500
|
+
if (!bun.Bun?.build)
|
|
2501
|
+
throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
|
|
2502
|
+
const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "node" });
|
|
2503
|
+
if (!result.success) {
|
|
2504
|
+
throw new Error(`Failed to bundle ${label}:
|
|
2505
|
+
${result.logs.map((l) => l.message).join(`
|
|
2506
|
+
`)}`);
|
|
2507
|
+
}
|
|
2508
|
+
return result.outputs[0].text();
|
|
2509
|
+
}
|
|
2510
|
+
async function extractFromBundle(source, label, pick) {
|
|
2511
|
+
const tmpPath = `/tmp/agentc-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`;
|
|
2512
|
+
try {
|
|
2513
|
+
const loaded = await importSourceModule(source, tmpPath);
|
|
2514
|
+
try {
|
|
2515
|
+
return pick(loaded.mod.default);
|
|
2516
|
+
} finally {
|
|
2517
|
+
await loaded.cleanup();
|
|
2518
|
+
}
|
|
2519
|
+
} catch (err) {
|
|
2520
|
+
throw new WorkflowSourceValidationError(`Bundled ${label} could not be evaluated: ${err instanceof Error ? err.message : String(err)}`);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
var DEFINE_WORKFLOW_CALLEE = /^(?:defineWorkflow|defineSandboxEnvironment)\d*$/;
|
|
2524
|
+
function isDefineWorkflowCall(node) {
|
|
2525
|
+
if (node.callee.type === "Identifier") {
|
|
2526
|
+
return DEFINE_WORKFLOW_CALLEE.test(node.callee.name);
|
|
2527
|
+
}
|
|
2528
|
+
if (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") {
|
|
2529
|
+
return isDefineWorkflowCall(node.callee.object);
|
|
2530
|
+
}
|
|
2531
|
+
return false;
|
|
1687
2532
|
}
|
|
1688
2533
|
function assertDefaultExportIsDefineWorkflow(source, label) {
|
|
1689
2534
|
let ast;
|
|
@@ -1731,6 +2576,19 @@ function findDefaultExportExpression(body) {
|
|
|
1731
2576
|
function sha256(text) {
|
|
1732
2577
|
return createHash2("sha256").update(text, "utf8").digest("hex");
|
|
1733
2578
|
}
|
|
2579
|
+
function extractWorkflowPlan(workflow) {
|
|
2580
|
+
return workflowPlan(workflow.steps.map((step, index) => ({
|
|
2581
|
+
index,
|
|
2582
|
+
name: step.name,
|
|
2583
|
+
...step.summary !== undefined ? { summary: step.summary } : {},
|
|
2584
|
+
...step.deliverables?.length ? {
|
|
2585
|
+
deliverables: step.deliverables.map((d) => ({
|
|
2586
|
+
path: d.path,
|
|
2587
|
+
...d.description !== undefined ? { description: d.description } : {}
|
|
2588
|
+
}))
|
|
2589
|
+
} : {}
|
|
2590
|
+
})));
|
|
2591
|
+
}
|
|
1734
2592
|
async function bundleWorkflow(workflowPath, overrides) {
|
|
1735
2593
|
const label = `"${workflowPath}"`;
|
|
1736
2594
|
const source = await bundle(workflowPath, `workflow ${label}`);
|
|
@@ -1740,7 +2598,7 @@ async function bundleWorkflow(workflowPath, overrides) {
|
|
|
1740
2598
|
throw new WorkflowSourceValidationError(`Bundled workflow ${label} default export is not a Workflow. ` + `Wrap your workflow with \`defineWorkflow\` from "@agent-compose/sdk" — ` + `raw \`export default async function\` is no longer supported.`);
|
|
1741
2599
|
}
|
|
1742
2600
|
const { metadata } = workflow;
|
|
1743
|
-
const plan =
|
|
2601
|
+
const plan = extractWorkflowPlan(workflow);
|
|
1744
2602
|
const manifest = {
|
|
1745
2603
|
definitionBrand: true,
|
|
1746
2604
|
sourceHash: sha256(source),
|
|
@@ -1759,6 +2617,7 @@ async function bundleWorkflow(workflowPath, overrides) {
|
|
|
1759
2617
|
...outputSchema !== undefined ? { outputSchema } : {},
|
|
1760
2618
|
...metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {},
|
|
1761
2619
|
...metadata.resources !== undefined ? { resources: metadata.resources } : {},
|
|
2620
|
+
...metadata.mergePolicy !== undefined ? { mergePolicy: metadata.mergePolicy } : {},
|
|
1762
2621
|
...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
|
|
1763
2622
|
...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
|
|
1764
2623
|
...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {},
|
|
@@ -2029,6 +2888,10 @@ function parseAgentStatus(text) {
|
|
|
2029
2888
|
}
|
|
2030
2889
|
}
|
|
2031
2890
|
var DEFAULT_ALLOWED_TOOLS = ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch"];
|
|
2891
|
+
function runtimeJsonSchema(schema) {
|
|
2892
|
+
const { $schema: _$schema, ...rest } = z10.toJSONSchema(schema);
|
|
2893
|
+
return rest;
|
|
2894
|
+
}
|
|
2032
2895
|
function truncate(value) {
|
|
2033
2896
|
return value.length > MESSAGE_PREVIEW_CHARS ? `${value.slice(0, MESSAGE_PREVIEW_CHARS)}…` : value;
|
|
2034
2897
|
}
|
|
@@ -2075,7 +2938,7 @@ async function agentLoop(opts) {
|
|
|
2075
2938
|
const logLabel = opts.label ?? "[Agent Loop]";
|
|
2076
2939
|
const startedAt = Date.now();
|
|
2077
2940
|
const turnsPerIteration = opts.turnsPerIteration;
|
|
2078
|
-
const maxIterations = opts.maxIterations ?? (turnsPerIteration === undefined ?
|
|
2941
|
+
const maxIterations = opts.maxIterations ?? (turnsPerIteration === undefined ? 3 : 8);
|
|
2079
2942
|
let schemaRetriesLeft = 10;
|
|
2080
2943
|
const processors = opts.processors ?? [];
|
|
2081
2944
|
const requestContext = opts.requestContext ?? RequestContext.fromReserved({
|
|
@@ -2106,7 +2969,7 @@ async function agentLoop(opts) {
|
|
|
2106
2969
|
requestContext,
|
|
2107
2970
|
agentId,
|
|
2108
2971
|
...opts.pause ? { pause: opts.pause } : {},
|
|
2109
|
-
...opts.responseSchema ? { outputFormat: { type: "json_schema", schema:
|
|
2972
|
+
...opts.responseSchema ? { outputFormat: { type: "json_schema", schema: runtimeJsonSchema(opts.responseSchema) } } : {}
|
|
2110
2973
|
});
|
|
2111
2974
|
if (processors.some((p) => p.processToolCall) && !client.supportsToolCallProcessor) {
|
|
2112
2975
|
process.stderr.write(`${logLabel} processToolCall registered but runtime does not support pre-tool-use gating — hook is dormant
|
|
@@ -2240,6 +3103,8 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2240
3103
|
signal: loopAbort.signal,
|
|
2241
3104
|
...opts.inbox ? { inboxStream: opts.inbox } : {}
|
|
2242
3105
|
})) {
|
|
3106
|
+
if (rawMsg.type === "text_delta" || rawMsg.type === "usage_delta")
|
|
3107
|
+
continue;
|
|
2243
3108
|
const outputVerdict = await runProcessorChain(processors, (p) => p.processOutput, rawMsg, procCtx);
|
|
2244
3109
|
if (outputVerdict.kind === "abort") {
|
|
2245
3110
|
loopAbort.abort();
|
|
@@ -2251,6 +3116,8 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2251
3116
|
continue;
|
|
2252
3117
|
}
|
|
2253
3118
|
const msg = outputVerdict.value;
|
|
3119
|
+
if (msg.type === "text_delta" || msg.type === "usage_delta")
|
|
3120
|
+
continue;
|
|
2254
3121
|
opts.onAgentEvent?.(iteration, msg);
|
|
2255
3122
|
const summary = summarizeAgentMessage(msg);
|
|
2256
3123
|
opts.onAgentLifecycleEvent?.({
|
|
@@ -2396,56 +3263,12 @@ function parseRawJsonResponse(text) {
|
|
|
2396
3263
|
}
|
|
2397
3264
|
}
|
|
2398
3265
|
|
|
2399
|
-
// src/agent/async-queue.ts
|
|
2400
|
-
class AsyncQueue {
|
|
2401
|
-
buffer = [];
|
|
2402
|
-
resolvers = [];
|
|
2403
|
-
done = false;
|
|
2404
|
-
push(value) {
|
|
2405
|
-
if (this.done)
|
|
2406
|
-
return;
|
|
2407
|
-
const resolve = this.resolvers.shift();
|
|
2408
|
-
if (resolve) {
|
|
2409
|
-
resolve({ value, done: false });
|
|
2410
|
-
} else {
|
|
2411
|
-
this.buffer.push(value);
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
close() {
|
|
2415
|
-
if (this.done)
|
|
2416
|
-
return;
|
|
2417
|
-
this.done = true;
|
|
2418
|
-
while (this.resolvers.length > 0) {
|
|
2419
|
-
const resolve = this.resolvers.shift();
|
|
2420
|
-
resolve({ value: undefined, done: true });
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
[Symbol.asyncIterator]() {
|
|
2424
|
-
return {
|
|
2425
|
-
next: () => {
|
|
2426
|
-
const buffered = this.buffer.shift();
|
|
2427
|
-
if (buffered !== undefined)
|
|
2428
|
-
return Promise.resolve({ value: buffered, done: false });
|
|
2429
|
-
if (this.done)
|
|
2430
|
-
return Promise.resolve({ value: undefined, done: true });
|
|
2431
|
-
return new Promise((resolve) => {
|
|
2432
|
-
this.resolvers.push(resolve);
|
|
2433
|
-
});
|
|
2434
|
-
},
|
|
2435
|
-
return: () => {
|
|
2436
|
-
this.close();
|
|
2437
|
-
return Promise.resolve({ value: undefined, done: true });
|
|
2438
|
-
}
|
|
2439
|
-
};
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
3266
|
// src/runtimes/claude.ts
|
|
2444
|
-
function
|
|
3267
|
+
function now3() {
|
|
2445
3268
|
return new Date().toISOString();
|
|
2446
3269
|
}
|
|
2447
3270
|
function translateMessage(message) {
|
|
2448
|
-
const ts =
|
|
3271
|
+
const ts = now3();
|
|
2449
3272
|
const msgs = [];
|
|
2450
3273
|
if (message.type === "system" && message.subtype === "init") {
|
|
2451
3274
|
msgs.push({ type: "init", sessionId: String(message.session_id ?? ""), timestamp: ts });
|
|
@@ -2519,6 +3342,22 @@ function resolveThinking(model, thinking) {
|
|
|
2519
3342
|
return;
|
|
2520
3343
|
return thinking;
|
|
2521
3344
|
}
|
|
3345
|
+
var STDERR_TAIL_CAP = 2000;
|
|
3346
|
+
|
|
3347
|
+
class StderrTail {
|
|
3348
|
+
buf = "";
|
|
3349
|
+
append(data) {
|
|
3350
|
+
this.buf = (this.buf + data).slice(-STDERR_TAIL_CAP);
|
|
3351
|
+
}
|
|
3352
|
+
get text() {
|
|
3353
|
+
return this.buf.trim();
|
|
3354
|
+
}
|
|
3355
|
+
decorate(message) {
|
|
3356
|
+
return this.text ? `${message}
|
|
3357
|
+
stderr tail:
|
|
3358
|
+
${this.text}` : message;
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
2522
3361
|
var DEFAULT_CLAUDE_PATH = "/usr/local/bin/claude";
|
|
2523
3362
|
|
|
2524
3363
|
class ClaudeRunner {
|
|
@@ -2605,6 +3444,7 @@ class ClaudeRunner {
|
|
|
2605
3444
|
const systemPromptAppend = [this.config.claudeMdContent, this.options.agentManual].filter((s) => Boolean(s && s.trim())).join(`
|
|
2606
3445
|
|
|
2607
3446
|
`);
|
|
3447
|
+
const stderrTail = new StderrTail;
|
|
2608
3448
|
try {
|
|
2609
3449
|
let emittedAssistantText = false;
|
|
2610
3450
|
for await (const message of query({
|
|
@@ -2619,6 +3459,7 @@ class ClaudeRunner {
|
|
|
2619
3459
|
effort: this.config.effort,
|
|
2620
3460
|
cwd: this.options.cwd,
|
|
2621
3461
|
abortController: queryAbort,
|
|
3462
|
+
stderr: (data) => stderrTail.append(data),
|
|
2622
3463
|
env: { ...process.env, ...this.config.env ?? {} },
|
|
2623
3464
|
pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
|
|
2624
3465
|
...systemPromptAppend ? { systemPrompt: { type: "preset", preset: "claude_code", append: systemPromptAppend } } : {},
|
|
@@ -2636,7 +3477,7 @@ class ClaudeRunner {
|
|
|
2636
3477
|
continue;
|
|
2637
3478
|
if (msg.type === "text" && raw.type === "assistant")
|
|
2638
3479
|
emittedAssistantText = true;
|
|
2639
|
-
yield msg;
|
|
3480
|
+
yield msg.type === "error" ? { ...msg, text: stderrTail.decorate(msg.text) } : msg;
|
|
2640
3481
|
}
|
|
2641
3482
|
if (raw.type === "result" && inboxQueue)
|
|
2642
3483
|
inboxQueue.close();
|
|
@@ -2644,9 +3485,9 @@ class ClaudeRunner {
|
|
|
2644
3485
|
} catch (err) {
|
|
2645
3486
|
const text = formatError(err);
|
|
2646
3487
|
if (/maximum number of turns/i.test(text)) {
|
|
2647
|
-
yield { type: "done", sessionId: opts.sessionId ?? "", timestamp:
|
|
3488
|
+
yield { type: "done", sessionId: opts.sessionId ?? "", timestamp: now3() };
|
|
2648
3489
|
} else {
|
|
2649
|
-
yield { type: "error", text, timestamp:
|
|
3490
|
+
yield { type: "error", text: stderrTail.decorate(text), timestamp: now3() };
|
|
2650
3491
|
}
|
|
2651
3492
|
} finally {
|
|
2652
3493
|
inboxQueue?.close();
|
|
@@ -2663,269 +3504,112 @@ var claude_default = createClaudeRuntime();
|
|
|
2663
3504
|
// src/runtimes/_cli-agent.ts
|
|
2664
3505
|
import { ndJsonStream } from "@agentclientprotocol/sdk";
|
|
2665
3506
|
|
|
2666
|
-
// src/runtimes/
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
};
|
|
2679
|
-
function contentBlockText(block) {
|
|
2680
|
-
if (!block)
|
|
2681
|
-
return "";
|
|
2682
|
-
return block.type === "text" ? block.text : "";
|
|
2683
|
-
}
|
|
2684
|
-
function renderToolContent(content) {
|
|
2685
|
-
const parts = [];
|
|
2686
|
-
const diffs = [];
|
|
2687
|
-
for (const block of content ?? []) {
|
|
2688
|
-
if (block.type === "content") {
|
|
2689
|
-
parts.push(contentBlockText(block.content));
|
|
2690
|
-
} else if (block.type === "diff") {
|
|
2691
|
-
diffs.push({ path: block.path, oldText: block.oldText ?? null, newText: block.newText });
|
|
2692
|
-
const header = block.oldText == null ? `--- (new file)
|
|
2693
|
-
+++ ${block.path}` : `--- ${block.path}
|
|
2694
|
-
+++ ${block.path}`;
|
|
2695
|
-
parts.push(`${header}
|
|
2696
|
-
${block.newText}`);
|
|
2697
|
-
} else if (block.type === "terminal") {
|
|
2698
|
-
parts.push(`[terminal ${block.terminalId}]`);
|
|
2699
|
-
}
|
|
3507
|
+
// src/runtimes/_jsonl-guard.ts
|
|
3508
|
+
var JSONL_GUARD_LINE_CAP_BYTES = Number(process.env.AC_JSONL_LINE_CAP_BYTES) || 131072;
|
|
3509
|
+
var JSONL_GUARD_KEEP_BYTES = Number(process.env.AC_JSONL_KEEP_BYTES) || 32768;
|
|
3510
|
+
var JSONL_GUARD_NO_SENTINEL_EXIT = 121;
|
|
3511
|
+
function capJsonlLine(line, maxBytes, keepBytes) {
|
|
3512
|
+
if (Buffer.byteLength(line, "utf8") <= maxBytes)
|
|
3513
|
+
return line;
|
|
3514
|
+
let parsed;
|
|
3515
|
+
try {
|
|
3516
|
+
parsed = JSON.parse(line);
|
|
3517
|
+
} catch {
|
|
3518
|
+
parsed = undefined;
|
|
2700
3519
|
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
return {
|
|
2713
|
-
type: "tool_result",
|
|
2714
|
-
toolUseId: args.toolCallId,
|
|
2715
|
-
output,
|
|
2716
|
-
isError: args.status === "failed",
|
|
2717
|
-
timestamp: args.timestamp,
|
|
2718
|
-
...diffs.length > 0 ? { diffs } : {},
|
|
2719
|
-
...locations ? { locations } : {}
|
|
2720
|
-
};
|
|
2721
|
-
}
|
|
2722
|
-
function normalizeSessionUpdate(update) {
|
|
2723
|
-
const ts = now3();
|
|
2724
|
-
switch (update.sessionUpdate) {
|
|
2725
|
-
case "agent_message_chunk":
|
|
2726
|
-
return [{ type: "text", text: contentBlockText(update.content), timestamp: ts }];
|
|
2727
|
-
case "agent_thought_chunk":
|
|
2728
|
-
return [{ type: "thinking", text: contentBlockText(update.content), timestamp: ts }];
|
|
2729
|
-
case "tool_call": {
|
|
2730
|
-
const messages = [{
|
|
2731
|
-
type: "tool_use",
|
|
2732
|
-
toolName: update.title || update.kind || "tool",
|
|
2733
|
-
toolInput: update.rawInput ?? {},
|
|
2734
|
-
toolUseId: update.toolCallId,
|
|
2735
|
-
timestamp: ts
|
|
2736
|
-
}];
|
|
2737
|
-
if (update.status === "completed" || update.status === "failed") {
|
|
2738
|
-
messages.push(terminalToolResult({
|
|
2739
|
-
toolCallId: update.toolCallId,
|
|
2740
|
-
status: update.status,
|
|
2741
|
-
content: update.content,
|
|
2742
|
-
locations: update.locations,
|
|
2743
|
-
timestamp: ts
|
|
2744
|
-
}));
|
|
3520
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
3521
|
+
const truncate2 = (value, budget) => {
|
|
3522
|
+
const bytes = Buffer.byteLength(value, "utf8");
|
|
3523
|
+
if (bytes <= budget)
|
|
3524
|
+
return value;
|
|
3525
|
+
let head = Buffer.from(value, "utf8").subarray(0, budget).toString("utf8");
|
|
3526
|
+
while (head.endsWith("�"))
|
|
3527
|
+
head = head.slice(0, -1);
|
|
3528
|
+
if (budget < 2048) {
|
|
3529
|
+
return head + `
|
|
3530
|
+
[agent-compose: PAYLOAD ELIDED IN TRANSIT - kept first ` + budget + " of " + bytes + " bytes; full content remains in the sandbox]";
|
|
2745
3531
|
}
|
|
2746
|
-
return
|
|
2747
|
-
}
|
|
2748
|
-
case "tool_call_update": {
|
|
2749
|
-
if (update.status !== "completed" && update.status !== "failed")
|
|
2750
|
-
return [];
|
|
2751
|
-
return [terminalToolResult({
|
|
2752
|
-
toolCallId: update.toolCallId,
|
|
2753
|
-
status: update.status,
|
|
2754
|
-
content: update.content,
|
|
2755
|
-
locations: update.locations,
|
|
2756
|
-
timestamp: ts
|
|
2757
|
-
})];
|
|
2758
|
-
}
|
|
2759
|
-
case "plan":
|
|
2760
|
-
return [{
|
|
2761
|
-
type: "plan",
|
|
2762
|
-
entries: update.entries.map((e) => ({ content: e.content, priority: e.priority, status: e.status })),
|
|
2763
|
-
timestamp: ts
|
|
2764
|
-
}];
|
|
2765
|
-
case "user_message_chunk":
|
|
2766
|
-
case "available_commands_update":
|
|
2767
|
-
case "current_mode_update":
|
|
2768
|
-
case "usage_update":
|
|
2769
|
-
case "plan_update":
|
|
2770
|
-
case "plan_removed":
|
|
2771
|
-
case "config_option_update":
|
|
2772
|
-
case "session_info_update":
|
|
2773
|
-
return [];
|
|
2774
|
-
default:
|
|
2775
|
-
return [];
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
3532
|
+
return head + `
|
|
2778
3533
|
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
async initialize() {
|
|
2805
|
-
const res = await this.conn.initialize({
|
|
2806
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2807
|
-
clientCapabilities: {
|
|
2808
|
-
fs: { readTextFile: false, writeTextFile: false },
|
|
2809
|
-
terminal: false
|
|
2810
|
-
}
|
|
2811
|
-
});
|
|
2812
|
-
return { protocolVersion: res.protocolVersion };
|
|
2813
|
-
}
|
|
2814
|
-
async startSession(cwd) {
|
|
2815
|
-
const res = await this.conn.newSession({ cwd, mcpServers: [] });
|
|
2816
|
-
this.sessionId = res.sessionId;
|
|
2817
|
-
return res.sessionId;
|
|
2818
|
-
}
|
|
2819
|
-
async* prompt(text) {
|
|
2820
|
-
if (!this.sessionId)
|
|
2821
|
-
throw new Error("AcpClientPeer.prompt called before startSession");
|
|
2822
|
-
const updates = new AsyncQueue;
|
|
2823
|
-
this.updates = updates;
|
|
2824
|
-
const aborted = new Promise((resolve) => {
|
|
2825
|
-
this.abortPrompt = () => {
|
|
2826
|
-
resolve();
|
|
2827
|
-
updates.close();
|
|
2828
|
-
};
|
|
2829
|
-
});
|
|
2830
|
-
if (this.cancelled)
|
|
2831
|
-
this.abortPrompt();
|
|
2832
|
-
const promptPromise = this.conn.prompt({ sessionId: this.sessionId, prompt: [{ type: "text", text }] }).then((res) => {
|
|
2833
|
-
updates.close();
|
|
2834
|
-
return { ok: true, res };
|
|
2835
|
-
}, (err) => {
|
|
2836
|
-
updates.close();
|
|
2837
|
-
return { ok: false, err };
|
|
2838
|
-
});
|
|
2839
|
-
for await (const msg of updates) {
|
|
2840
|
-
yield msg;
|
|
2841
|
-
}
|
|
2842
|
-
if (this.pendingPause !== null) {
|
|
2843
|
-
const pause = this.pendingPause;
|
|
2844
|
-
this.pendingPause = null;
|
|
2845
|
-
this.abortPrompt = null;
|
|
2846
|
-
throw pause;
|
|
2847
|
-
}
|
|
2848
|
-
const settled = await Promise.race([
|
|
2849
|
-
promptPromise.then((o) => ({ kind: "prompt", o })),
|
|
2850
|
-
aborted.then(() => ({ kind: "aborted" }))
|
|
2851
|
-
]);
|
|
2852
|
-
this.abortPrompt = null;
|
|
2853
|
-
if (settled.kind === "aborted")
|
|
2854
|
-
return;
|
|
2855
|
-
const outcome = settled.o;
|
|
2856
|
-
if (!outcome.ok) {
|
|
2857
|
-
yield { type: "error", text: formatError(outcome.err), timestamp: now3() };
|
|
2858
|
-
return;
|
|
2859
|
-
}
|
|
2860
|
-
const stopReason = outcome.res.stopReason;
|
|
2861
|
-
if (stopReason === "refusal") {
|
|
2862
|
-
yield { type: "error", text: "agent refused the request", timestamp: now3() };
|
|
2863
|
-
return;
|
|
2864
|
-
}
|
|
2865
|
-
const usage = outcome.res.usage;
|
|
2866
|
-
if (usage) {
|
|
2867
|
-
yield {
|
|
2868
|
-
type: "usage",
|
|
2869
|
-
inputTokens: usage.inputTokens,
|
|
2870
|
-
outputTokens: usage.outputTokens,
|
|
2871
|
-
cacheReadTokens: usage.cachedReadTokens ?? 0,
|
|
2872
|
-
cacheCreationTokens: usage.cachedWriteTokens ?? 0,
|
|
2873
|
-
durationMs: 0,
|
|
2874
|
-
numTurns: 1,
|
|
2875
|
-
timestamp: now3()
|
|
2876
|
-
};
|
|
2877
|
-
}
|
|
2878
|
-
}
|
|
2879
|
-
get currentSessionId() {
|
|
2880
|
-
return this.sessionId;
|
|
2881
|
-
}
|
|
2882
|
-
cancel() {
|
|
2883
|
-
if (this.cancelled) {
|
|
2884
|
-
this.abortPrompt?.();
|
|
2885
|
-
return;
|
|
2886
|
-
}
|
|
2887
|
-
this.cancelled = true;
|
|
2888
|
-
if (this.sessionId) {
|
|
2889
|
-
this.conn.cancel({ sessionId: this.sessionId }).catch(() => {});
|
|
2890
|
-
}
|
|
2891
|
-
this.abortPrompt?.();
|
|
2892
|
-
}
|
|
2893
|
-
close() {
|
|
2894
|
-
this.abortPrompt?.();
|
|
2895
|
-
this.updates.close();
|
|
2896
|
-
}
|
|
2897
|
-
async onRequestPermission(params) {
|
|
2898
|
-
if (this.cancelled || this.deps.signal?.aborted) {
|
|
2899
|
-
return { outcome: { outcome: "cancelled" } };
|
|
2900
|
-
}
|
|
2901
|
-
const tc = params.toolCall;
|
|
2902
|
-
const call = {
|
|
2903
|
-
toolName: tc.title || tc.kind || "tool",
|
|
2904
|
-
toolInput: tc.rawInput ?? {},
|
|
2905
|
-
toolUseId: tc.toolCallId
|
|
2906
|
-
};
|
|
2907
|
-
let verdict;
|
|
2908
|
-
try {
|
|
2909
|
-
verdict = await this.deps.gateToolCall(call, this.deps.procCtx());
|
|
2910
|
-
} catch (err) {
|
|
2911
|
-
if (isPauseSignal(err)) {
|
|
2912
|
-
this.pendingPause = err;
|
|
2913
|
-
this.cancel();
|
|
2914
|
-
return { outcome: { outcome: "cancelled" } };
|
|
3534
|
+
[agent-compose: PAYLOAD ELIDED IN TRANSIT - this content was ` + bytes + " bytes; only the first " + budget + " bytes are shown. It was cut to protect the" + " session's transport, NOT because the source is short: the full content still exists" + " inside this sandbox (the file on disk / the command's output). Do not guess the" + " elided part - re-read the source file, or re-run the command with narrower output" + " (head, tail, grep, offset+limit), to see it.]";
|
|
3535
|
+
};
|
|
3536
|
+
const walk = (node, budget) => {
|
|
3537
|
+
if (typeof node === "string")
|
|
3538
|
+
return truncate2(node, budget);
|
|
3539
|
+
if (Array.isArray(node))
|
|
3540
|
+
return node.map((item) => walk(item, budget));
|
|
3541
|
+
if (node !== null && typeof node === "object") {
|
|
3542
|
+
const out = {};
|
|
3543
|
+
for (const key of Object.keys(node)) {
|
|
3544
|
+
out[key] = walk(node[key], budget);
|
|
3545
|
+
}
|
|
3546
|
+
return out;
|
|
3547
|
+
}
|
|
3548
|
+
return node;
|
|
3549
|
+
};
|
|
3550
|
+
for (const shrink of [1, 8, 64]) {
|
|
3551
|
+
const budget = Math.max(64, Math.floor(keepBytes / shrink));
|
|
3552
|
+
try {
|
|
3553
|
+
const capped = JSON.stringify(walk(parsed, budget));
|
|
3554
|
+
if (typeof capped === "string" && Buffer.byteLength(capped, "utf8") <= maxBytes) {
|
|
3555
|
+
return capped;
|
|
3556
|
+
}
|
|
3557
|
+
} catch {
|
|
3558
|
+
break;
|
|
2915
3559
|
}
|
|
2916
|
-
throw err;
|
|
2917
|
-
}
|
|
2918
|
-
if (verdict.kind === "abort") {
|
|
2919
|
-
this.cancel();
|
|
2920
|
-
return { outcome: { outcome: "cancelled" } };
|
|
2921
|
-
}
|
|
2922
|
-
const wantedKinds = verdict.kind === "allow" ? OPTION_KIND_FOR_VERDICT.allow : OPTION_KIND_FOR_VERDICT.deny;
|
|
2923
|
-
const option = params.options.find((o) => wantedKinds.includes(o.kind)) ?? params.options[0];
|
|
2924
|
-
if (!option) {
|
|
2925
|
-
return { outcome: { outcome: "cancelled" } };
|
|
2926
3560
|
}
|
|
2927
|
-
return { outcome: { outcome: "selected", optionId: option.optionId } };
|
|
2928
3561
|
}
|
|
3562
|
+
let raw = Buffer.from(line, "utf8").subarray(0, maxBytes).toString("utf8");
|
|
3563
|
+
while (raw.endsWith("�"))
|
|
3564
|
+
raw = raw.slice(0, -1);
|
|
3565
|
+
return raw;
|
|
3566
|
+
}
|
|
3567
|
+
function jsonlGuardScript() {
|
|
3568
|
+
return [
|
|
3569
|
+
"'use strict';",
|
|
3570
|
+
`const capJsonlLine = ${capJsonlLine.toString()};`,
|
|
3571
|
+
`const NO_SENTINEL_EXIT = ${JSONL_GUARD_NO_SENTINEL_EXIT};`,
|
|
3572
|
+
"const sentinel = process.argv[2];",
|
|
3573
|
+
"const maxBytes = Number(process.argv[3]);",
|
|
3574
|
+
"const keepBytes = Number(process.argv[4]);",
|
|
3575
|
+
"let sawSentinel = false;",
|
|
3576
|
+
"let buf = '';",
|
|
3577
|
+
"// Downstream (the command-stream consumer) going away must never crash",
|
|
3578
|
+
"// the pipe with EPIPE noise — the producer is being killed anyway.",
|
|
3579
|
+
"process.stdout.on('error', () => process.exit(0));",
|
|
3580
|
+
"function handle(line) {",
|
|
3581
|
+
" const t = line.trim();",
|
|
3582
|
+
" if (sentinel && t.startsWith(sentinel)) {",
|
|
3583
|
+
" const code = Number(t.slice(sentinel.length).trim());",
|
|
3584
|
+
" process.exitCode = Number.isFinite(code) ? code : 0;",
|
|
3585
|
+
" sawSentinel = true;",
|
|
3586
|
+
" return;",
|
|
3587
|
+
" }",
|
|
3588
|
+
" process.stdout.write(capJsonlLine(line, maxBytes, keepBytes) + '\\n');",
|
|
3589
|
+
"}",
|
|
3590
|
+
"process.stdin.setEncoding('utf8');",
|
|
3591
|
+
"process.stdin.on('data', (d) => {",
|
|
3592
|
+
" buf += d;",
|
|
3593
|
+
" let nl;",
|
|
3594
|
+
" while ((nl = buf.indexOf('\\n')) >= 0) {",
|
|
3595
|
+
" const line = buf.slice(0, nl);",
|
|
3596
|
+
" buf = buf.slice(nl + 1);",
|
|
3597
|
+
" if (line.length > 0) handle(line);",
|
|
3598
|
+
" }",
|
|
3599
|
+
"});",
|
|
3600
|
+
"process.stdin.on('end', () => {",
|
|
3601
|
+
" if (buf.trim().length > 0) handle(buf);",
|
|
3602
|
+
" if (!sawSentinel) process.exitCode = NO_SENTINEL_EXIT;",
|
|
3603
|
+
"});"
|
|
3604
|
+
].join(`
|
|
3605
|
+
`);
|
|
3606
|
+
}
|
|
3607
|
+
function quote(value) {
|
|
3608
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
3609
|
+
}
|
|
3610
|
+
function wrapJsonlCommand(args) {
|
|
3611
|
+
const { cmd, guardPath, sentinel, maxBytes, keepBytes } = args;
|
|
3612
|
+
return `if command -v node >/dev/null 2>&1; then ` + `{ ( ${cmd} ); printf '\\n%s %s\\n' ${quote(sentinel)} "$?"; } ` + `| node ${quote(guardPath)} ${quote(sentinel)} ${maxBytes} ${keepBytes}; ` + `else ${cmd}; fi`;
|
|
2929
3613
|
}
|
|
2930
3614
|
|
|
2931
3615
|
// src/runtimes/_cli-agent.ts
|
|
@@ -2935,8 +3619,14 @@ function now4() {
|
|
|
2935
3619
|
function shellQuote(value) {
|
|
2936
3620
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2937
3621
|
}
|
|
3622
|
+
var promptFileCounter = 0;
|
|
3623
|
+
function uniquePromptPath(kind, agentId, iteration) {
|
|
3624
|
+
const unique = `${Date.now().toString(36)}-${(promptFileCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
3625
|
+
return `/tmp/ac-${kind}-${agentId}-${iteration}-${unique}.in`;
|
|
3626
|
+
}
|
|
2938
3627
|
var ACP_FALLBACK = Symbol("acp-fallback");
|
|
2939
3628
|
var ACP_HANDSHAKE_TIMEOUT_MS = Number(process.env.AC_ACP_HANDSHAKE_TIMEOUT_MS) || 1e4;
|
|
3629
|
+
var ACP_TURN_IDLE_TIMEOUT_MS = Number(process.env.AC_ACP_TURN_IDLE_TIMEOUT_MS) || 120000;
|
|
2940
3630
|
var ACP_TRANSPORT_READY = true;
|
|
2941
3631
|
async function withHandshakeTimeout(p, ms) {
|
|
2942
3632
|
let timer;
|
|
@@ -2956,6 +3646,7 @@ class CliAgentRunner {
|
|
|
2956
3646
|
options;
|
|
2957
3647
|
spec;
|
|
2958
3648
|
configModel;
|
|
3649
|
+
configEffort;
|
|
2959
3650
|
kind;
|
|
2960
3651
|
get acpCapable() {
|
|
2961
3652
|
return this.spec.acp !== undefined && this.acpTransportReady && typeof this.sandbox.commands.spawnDuplex === "function";
|
|
@@ -2967,11 +3658,12 @@ class CliAgentRunner {
|
|
|
2967
3658
|
acpSession;
|
|
2968
3659
|
currentAcpProcCtx;
|
|
2969
3660
|
acpTransportReady = ACP_TRANSPORT_READY;
|
|
2970
|
-
constructor(sandbox, options, spec, configModel) {
|
|
3661
|
+
constructor(sandbox, options, spec, configModel, configEffort) {
|
|
2971
3662
|
this.sandbox = sandbox;
|
|
2972
3663
|
this.options = options;
|
|
2973
3664
|
this.spec = spec;
|
|
2974
3665
|
this.configModel = configModel;
|
|
3666
|
+
this.configEffort = configEffort;
|
|
2975
3667
|
this.kind = spec.kind;
|
|
2976
3668
|
}
|
|
2977
3669
|
enableAcpForTesting() {
|
|
@@ -3104,7 +3796,7 @@ class CliAgentRunner {
|
|
|
3104
3796
|
watchdog = setTimeout(() => {
|
|
3105
3797
|
stalled = true;
|
|
3106
3798
|
peer.cancel();
|
|
3107
|
-
},
|
|
3799
|
+
}, ACP_TURN_IDLE_TIMEOUT_MS);
|
|
3108
3800
|
};
|
|
3109
3801
|
try {
|
|
3110
3802
|
armWatchdog();
|
|
@@ -3121,7 +3813,7 @@ class CliAgentRunner {
|
|
|
3121
3813
|
if (stalled && !sawError) {
|
|
3122
3814
|
await this.teardownAcp(proc, peer);
|
|
3123
3815
|
this.acpSession = undefined;
|
|
3124
|
-
yield { type: "error", text: `${this.spec.kind} prompt turn stalled (no activity for ${
|
|
3816
|
+
yield { type: "error", text: `${this.spec.kind} prompt turn stalled (no activity for ${ACP_TURN_IDLE_TIMEOUT_MS}ms)`, timestamp: now4() };
|
|
3125
3817
|
return;
|
|
3126
3818
|
}
|
|
3127
3819
|
if (!sawError)
|
|
@@ -3151,14 +3843,27 @@ class CliAgentRunner {
|
|
|
3151
3843
|
let sessionId = opts.sessionId;
|
|
3152
3844
|
let sawError = false;
|
|
3153
3845
|
try {
|
|
3846
|
+
if (opts.signal?.aborted)
|
|
3847
|
+
return;
|
|
3154
3848
|
await this.ensureInstalled();
|
|
3155
|
-
const promptPath =
|
|
3849
|
+
const promptPath = uniquePromptPath(this.spec.kind, this.options.agentId ?? "agent", opts.iteration ?? 0);
|
|
3156
3850
|
await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
|
|
3157
3851
|
const cmd = this.spec.buildCommand({
|
|
3158
3852
|
promptPath,
|
|
3159
3853
|
sessionId: opts.sessionId,
|
|
3160
3854
|
model: this.model,
|
|
3161
|
-
cwd: this.options.cwd
|
|
3855
|
+
cwd: this.options.cwd,
|
|
3856
|
+
effort: this.configEffort
|
|
3857
|
+
});
|
|
3858
|
+
const guardPath = `${promptPath}.guard.js`;
|
|
3859
|
+
const sentinel = `__AC_JSONL_EXIT_${Math.random().toString(36).slice(2, 10)}__`;
|
|
3860
|
+
await this.sandbox.files.write(guardPath, jsonlGuardScript());
|
|
3861
|
+
const guardedCmd = wrapJsonlCommand({
|
|
3862
|
+
cmd,
|
|
3863
|
+
guardPath,
|
|
3864
|
+
sentinel,
|
|
3865
|
+
maxBytes: JSONL_GUARD_LINE_CAP_BYTES,
|
|
3866
|
+
keepBytes: JSONL_GUARD_KEEP_BYTES
|
|
3162
3867
|
});
|
|
3163
3868
|
const lines = new AsyncQueue;
|
|
3164
3869
|
let buf = "";
|
|
@@ -3173,51 +3878,83 @@ class CliAgentRunner {
|
|
|
3173
3878
|
lines.push(line);
|
|
3174
3879
|
}
|
|
3175
3880
|
};
|
|
3176
|
-
const
|
|
3881
|
+
const runOpts = {
|
|
3177
3882
|
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
3178
|
-
onStdout
|
|
3179
|
-
|
|
3883
|
+
onStdout,
|
|
3884
|
+
timeoutMs: 0
|
|
3885
|
+
};
|
|
3886
|
+
let exited = false;
|
|
3887
|
+
const settle = (res) => {
|
|
3888
|
+
exited = true;
|
|
3180
3889
|
const tail = buf.trim();
|
|
3181
3890
|
if (tail)
|
|
3182
3891
|
lines.push(tail);
|
|
3183
3892
|
lines.close();
|
|
3184
|
-
return
|
|
3185
|
-
}
|
|
3893
|
+
return res;
|
|
3894
|
+
};
|
|
3895
|
+
const fail = (err) => {
|
|
3896
|
+
exited = true;
|
|
3186
3897
|
lines.close();
|
|
3187
3898
|
throw err;
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3899
|
+
};
|
|
3900
|
+
let kill;
|
|
3901
|
+
let runPromise;
|
|
3902
|
+
if (this.sandbox.commands.runBackground) {
|
|
3903
|
+
const handle = await this.sandbox.commands.runBackground(guardedCmd, runOpts);
|
|
3904
|
+
kill = () => handle.kill();
|
|
3905
|
+
runPromise = handle.wait().then(settle, fail);
|
|
3906
|
+
} else {
|
|
3907
|
+
runPromise = this.sandbox.commands.run(guardedCmd, runOpts).then(settle, fail);
|
|
3908
|
+
}
|
|
3909
|
+
runPromise.catch(() => {});
|
|
3910
|
+
const reap = () => {
|
|
3911
|
+
if (!exited)
|
|
3912
|
+
kill?.().catch(() => {});
|
|
3913
|
+
};
|
|
3914
|
+
if (opts.signal) {
|
|
3915
|
+
if (opts.signal.aborted)
|
|
3916
|
+
reap();
|
|
3917
|
+
else
|
|
3918
|
+
opts.signal.addEventListener("abort", reap, { once: true });
|
|
3919
|
+
}
|
|
3920
|
+
try {
|
|
3921
|
+
for await (const line of lines) {
|
|
3922
|
+
let parsed;
|
|
3923
|
+
try {
|
|
3924
|
+
parsed = JSON.parse(line);
|
|
3925
|
+
} catch {
|
|
3926
|
+
continue;
|
|
3927
|
+
}
|
|
3928
|
+
const sid = this.spec.extractSessionId(parsed);
|
|
3929
|
+
if (sid)
|
|
3930
|
+
sessionId = sid;
|
|
3931
|
+
for (const msg of this.spec.mapEvent(parsed)) {
|
|
3932
|
+
if (msg.type === "error")
|
|
3933
|
+
sawError = true;
|
|
3934
|
+
yield msg;
|
|
3935
|
+
}
|
|
3195
3936
|
}
|
|
3196
|
-
const
|
|
3197
|
-
if (
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
sawError = true;
|
|
3202
|
-
yield msg;
|
|
3937
|
+
const res = await runPromise;
|
|
3938
|
+
if (res.exitCode !== 0 && !sawError) {
|
|
3939
|
+
const tail = (res.stderr ?? "").slice(-2000);
|
|
3940
|
+
yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp: now4() };
|
|
3941
|
+
return;
|
|
3203
3942
|
}
|
|
3943
|
+
if (!sawError)
|
|
3944
|
+
yield { type: "done", sessionId: sessionId ?? "", timestamp: now4() };
|
|
3945
|
+
} finally {
|
|
3946
|
+
opts.signal?.removeEventListener("abort", reap);
|
|
3947
|
+
reap();
|
|
3948
|
+
this.sandbox.commands.run(`rm -f ${shellQuote(promptPath)} ${shellQuote(guardPath)}`, { timeoutMs: 1e4 }).catch(() => {});
|
|
3204
3949
|
}
|
|
3205
|
-
const res = await runPromise;
|
|
3206
|
-
if (res.exitCode !== 0 && !sawError) {
|
|
3207
|
-
const tail = (res.stderr ?? "").slice(-2000);
|
|
3208
|
-
yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp: now4() };
|
|
3209
|
-
return;
|
|
3210
|
-
}
|
|
3211
|
-
if (!sawError)
|
|
3212
|
-
yield { type: "done", sessionId: sessionId ?? "", timestamp: now4() };
|
|
3213
3950
|
} catch (err) {
|
|
3214
3951
|
yield { type: "error", text: formatError(err), timestamp: now4() };
|
|
3215
3952
|
}
|
|
3216
3953
|
}
|
|
3217
3954
|
}
|
|
3218
|
-
function createCliAgentRuntime(spec, configModel) {
|
|
3955
|
+
function createCliAgentRuntime(spec, configModel, configEffort) {
|
|
3219
3956
|
return defineRuntime({
|
|
3220
|
-
create: (sandbox, opts) => new CliAgentRunner(sandbox, opts, spec, configModel)
|
|
3957
|
+
create: (sandbox, opts) => new CliAgentRunner(sandbox, opts, spec, configModel, configEffort)
|
|
3221
3958
|
});
|
|
3222
3959
|
}
|
|
3223
3960
|
|
|
@@ -3226,6 +3963,12 @@ function now5() {
|
|
|
3226
3963
|
return new Date().toISOString();
|
|
3227
3964
|
}
|
|
3228
3965
|
var CODEX_ACP_ADAPTER = "@agentclientprotocol/codex-acp@0.1.0";
|
|
3966
|
+
var CODEX_ADVISORY_PATTERNS = [
|
|
3967
|
+
/Model metadata for .+ not found\. Defaulting to fallback metadata/
|
|
3968
|
+
];
|
|
3969
|
+
function isCodexAdvisoryNoise(text) {
|
|
3970
|
+
return CODEX_ADVISORY_PATTERNS.some((re) => re.test(text));
|
|
3971
|
+
}
|
|
3229
3972
|
var codexSpec = {
|
|
3230
3973
|
kind: "codex",
|
|
3231
3974
|
authEnv: "CODEX_API_KEY",
|
|
@@ -3237,12 +3980,13 @@ var codexSpec = {
|
|
|
3237
3980
|
},
|
|
3238
3981
|
install: 'sudo npm install -g @openai/codex && (command -v codex >/dev/null 2>&1 || sudo ln -sf "$(npm prefix -g)/bin/codex" /usr/local/bin/codex)',
|
|
3239
3982
|
promptPayload: (prompt) => prompt,
|
|
3240
|
-
buildCommand: ({ promptPath, sessionId, model, cwd }) => {
|
|
3983
|
+
buildCommand: ({ promptPath, sessionId, model, cwd, effort }) => {
|
|
3241
3984
|
const flags = [
|
|
3242
3985
|
"--json",
|
|
3243
3986
|
"--skip-git-repo-check",
|
|
3244
3987
|
"--dangerously-bypass-approvals-and-sandbox",
|
|
3245
3988
|
...model ? ["-m", shellQuote(model)] : [],
|
|
3989
|
+
...effort ? ["-c", `model_reasoning_effort=${effort}`] : [],
|
|
3246
3990
|
...cwd ? ["-C", shellQuote(cwd)] : []
|
|
3247
3991
|
].join(" ");
|
|
3248
3992
|
const exec = sessionId ? `codex exec resume ${shellQuote(sessionId)} ${flags}` : `codex exec ${flags}`;
|
|
@@ -3272,6 +4016,16 @@ var codexSpec = {
|
|
|
3272
4016
|
const failed = item.status === "failed" || typeof item.exit_code === "number" && item.exit_code !== 0;
|
|
3273
4017
|
return [{ type: "tool_result", toolUseId: id, output: String(item.aggregated_output ?? item.output ?? ""), isError: failed, timestamp: ts }];
|
|
3274
4018
|
}
|
|
4019
|
+
if (itype === "error") {
|
|
4020
|
+
if (p.type !== "item.completed")
|
|
4021
|
+
return [];
|
|
4022
|
+
const text = formatError(item.message ?? item);
|
|
4023
|
+
if (isCodexAdvisoryNoise(text)) {
|
|
4024
|
+
console.warn(`[codex] advisory (kept out of transcript): ${text}`);
|
|
4025
|
+
return [];
|
|
4026
|
+
}
|
|
4027
|
+
return [{ type: "error", text, timestamp: ts }];
|
|
4028
|
+
}
|
|
3275
4029
|
if (p.type === "item.completed") {
|
|
3276
4030
|
return [{ type: "tool_use", toolName: itype || "item", toolInput: item, toolUseId: String(item.id ?? ""), timestamp: ts }];
|
|
3277
4031
|
}
|
|
@@ -3301,7 +4055,7 @@ var codexSpec = {
|
|
|
3301
4055
|
}
|
|
3302
4056
|
};
|
|
3303
4057
|
function createCodexRuntime(config = {}) {
|
|
3304
|
-
return createCliAgentRuntime(codexSpec, config.model);
|
|
4058
|
+
return createCliAgentRuntime(codexSpec, config.model, config.effort);
|
|
3305
4059
|
}
|
|
3306
4060
|
var codex_default = createCodexRuntime();
|
|
3307
4061
|
// src/runtimes/amp.ts
|
|
@@ -3366,59 +4120,353 @@ function createAmpRuntime(config = {}) {
|
|
|
3366
4120
|
return createCliAgentRuntime(ampSpec, config.model);
|
|
3367
4121
|
}
|
|
3368
4122
|
var amp_default = createAmpRuntime();
|
|
3369
|
-
// src/
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
4123
|
+
// src/runtimes/opencode.ts
|
|
4124
|
+
function now7() {
|
|
4125
|
+
return new Date().toISOString();
|
|
4126
|
+
}
|
|
4127
|
+
var opencodeSpec = {
|
|
4128
|
+
kind: "opencode",
|
|
4129
|
+
authEnv: "OPENROUTER_API_KEY",
|
|
4130
|
+
bin: "opencode",
|
|
4131
|
+
defaultModel: "openrouter/z-ai/glm-5.2",
|
|
4132
|
+
acp: { command: "opencode", args: ["acp"] },
|
|
4133
|
+
install: 'sudo npm install -g opencode-ai && (command -v opencode >/dev/null 2>&1 || sudo ln -sf "$(npm prefix -g)/bin/opencode" /usr/local/bin/opencode)',
|
|
4134
|
+
promptPayload: (prompt) => prompt,
|
|
4135
|
+
buildCommand: ({ promptPath, model, cwd }) => `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}opencode run ${model ? `--model ${shellQuote(model)} ` : ""}"$(cat ${shellQuote(promptPath)})"`,
|
|
4136
|
+
extractSessionId: () => {
|
|
4137
|
+
return;
|
|
4138
|
+
},
|
|
4139
|
+
mapEvent: (p) => {
|
|
4140
|
+
const ts = now7();
|
|
4141
|
+
const text = typeof p.text === "string" ? p.text : typeof p.content === "string" ? p.content : "";
|
|
4142
|
+
return text ? [{ type: "text", text, timestamp: ts }] : [];
|
|
4143
|
+
}
|
|
4144
|
+
};
|
|
4145
|
+
function createOpencodeRuntime(config = {}) {
|
|
4146
|
+
return createCliAgentRuntime(opencodeSpec, config.model ?? opencodeSpec.defaultModel);
|
|
4147
|
+
}
|
|
4148
|
+
var opencode_default = createOpencodeRuntime();
|
|
4149
|
+
// src/runtimes/cursor.ts
|
|
4150
|
+
function now8() {
|
|
4151
|
+
return new Date().toISOString();
|
|
4152
|
+
}
|
|
4153
|
+
var cursorSpec = {
|
|
4154
|
+
kind: "cursor",
|
|
4155
|
+
authEnv: "CURSOR_API_KEY",
|
|
4156
|
+
bin: "cursor-agent",
|
|
4157
|
+
defaultModel: "auto",
|
|
4158
|
+
acp: { command: "cursor-agent", args: ["acp"] },
|
|
4159
|
+
install: 'curl https://cursor.com/install -fsS | bash && (command -v cursor-agent >/dev/null 2>&1 || sudo ln -sf "$HOME/.local/bin/cursor-agent" /usr/local/bin/cursor-agent)',
|
|
4160
|
+
promptPayload: (prompt) => prompt,
|
|
4161
|
+
buildCommand: ({ promptPath, model, cwd }) => `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}cursor-agent -p --force ${model ? `--model ${shellQuote(model)} ` : ""}--output-format text "$(cat ${shellQuote(promptPath)})"`,
|
|
4162
|
+
extractSessionId: () => {
|
|
4163
|
+
return;
|
|
4164
|
+
},
|
|
4165
|
+
mapEvent: (p) => {
|
|
4166
|
+
const ts = now8();
|
|
4167
|
+
const text = typeof p.text === "string" ? p.text : typeof p.content === "string" ? p.content : "";
|
|
4168
|
+
return text ? [{ type: "text", text, timestamp: ts }] : [];
|
|
4169
|
+
}
|
|
4170
|
+
};
|
|
4171
|
+
function createCursorRuntime(config = {}) {
|
|
4172
|
+
return createCliAgentRuntime(cursorSpec, config.model ?? cursorSpec.defaultModel);
|
|
4173
|
+
}
|
|
4174
|
+
var cursor_default = createCursorRuntime();
|
|
4175
|
+
// src/runtimes/droid.ts
|
|
4176
|
+
function now9() {
|
|
4177
|
+
return new Date().toISOString();
|
|
4178
|
+
}
|
|
4179
|
+
var droidSpec = {
|
|
4180
|
+
kind: "droid",
|
|
4181
|
+
authEnv: "OPENROUTER_API_KEY",
|
|
4182
|
+
bin: "droid",
|
|
4183
|
+
defaultModel: "custom:GLM-5.2-OR-0",
|
|
4184
|
+
install: 'curl -fsSL https://app.factory.ai/cli | sh && (command -v droid >/dev/null 2>&1 || sudo ln -sf "$HOME/.local/bin/droid" /usr/local/bin/droid)',
|
|
4185
|
+
promptPayload: (prompt) => prompt,
|
|
4186
|
+
buildCommand: ({ promptPath, model, cwd }) => `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}droid exec --auto medium ${model ? `--model ${shellQuote(model)} ` : ""}--output-format json -f ${shellQuote(promptPath)}`,
|
|
4187
|
+
extractSessionId: (p) => typeof p.session_id === "string" ? p.session_id : undefined,
|
|
4188
|
+
mapEvent: (p) => {
|
|
4189
|
+
const ts = now9();
|
|
4190
|
+
const text = p.type === "result" && typeof p.result === "string" ? p.result : typeof p.text === "string" ? p.text : typeof p.content === "string" ? p.content : "";
|
|
4191
|
+
return text ? [{ type: "text", text, timestamp: ts }] : [];
|
|
4192
|
+
}
|
|
4193
|
+
};
|
|
4194
|
+
function createDroidRuntime(config = {}) {
|
|
4195
|
+
return createCliAgentRuntime(droidSpec, config.model ?? droidSpec.defaultModel);
|
|
4196
|
+
}
|
|
4197
|
+
var droid_default = createDroidRuntime();
|
|
4198
|
+
// src/runtimes/claude-code.ts
|
|
4199
|
+
function now10() {
|
|
4200
|
+
return new Date().toISOString();
|
|
4201
|
+
}
|
|
4202
|
+
var CLAUDE_CODE_ACP_ADAPTER = "@zed-industries/claude-code-acp@0.16.2";
|
|
4203
|
+
function toolResultText(content) {
|
|
4204
|
+
if (typeof content === "string")
|
|
4205
|
+
return content;
|
|
4206
|
+
if (Array.isArray(content)) {
|
|
4207
|
+
return content.map((b) => typeof b?.text === "string" ? b.text : "").join("");
|
|
4208
|
+
}
|
|
4209
|
+
return content == null ? "" : JSON.stringify(content) ?? "";
|
|
4210
|
+
}
|
|
4211
|
+
var CLAUDE_CODE_THINKING_TOKENS = { low: 2048, medium: 8192, high: 16384 };
|
|
4212
|
+
var claudeCodeSpec = {
|
|
4213
|
+
kind: "claude-code",
|
|
4214
|
+
authEnv: "ANTHROPIC_API_KEY",
|
|
4215
|
+
bin: "claude",
|
|
4216
|
+
acp: { command: "npx", args: ["--yes", CLAUDE_CODE_ACP_ADAPTER] },
|
|
4217
|
+
install: "curl -fsSL https://claude.ai/install.sh | bash && " + 'REAL=$(readlink -f "$HOME/.local/bin/claude") && test -f "$REAL" && ' + 'sudo cp "$REAL" /usr/local/bin/claude && sudo chmod 0755 /usr/local/bin/claude',
|
|
4218
|
+
promptPayload: (prompt) => prompt,
|
|
4219
|
+
buildCommand: ({ promptPath, sessionId, model, cwd, effort }) => {
|
|
4220
|
+
const flags = [
|
|
4221
|
+
"-p",
|
|
4222
|
+
"--output-format stream-json",
|
|
4223
|
+
"--verbose",
|
|
4224
|
+
"--include-partial-messages",
|
|
4225
|
+
"--dangerously-skip-permissions",
|
|
4226
|
+
...model ? [`--model ${shellQuote(model)}`] : [],
|
|
4227
|
+
...sessionId ? [`--resume ${shellQuote(sessionId)}`] : []
|
|
4228
|
+
].join(" ");
|
|
4229
|
+
const thinking = effort ? `MAX_THINKING_TOKENS=${CLAUDE_CODE_THINKING_TOKENS[effort]} ` : "";
|
|
4230
|
+
return `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}${thinking}IS_SANDBOX=1 claude ${flags} < ${shellQuote(promptPath)}`;
|
|
4231
|
+
},
|
|
4232
|
+
extractSessionId: (p) => typeof p.session_id === "string" ? p.session_id : undefined,
|
|
4233
|
+
mapEvent: (p) => {
|
|
4234
|
+
const ts = now10();
|
|
4235
|
+
switch (p.type) {
|
|
4236
|
+
case "assistant": {
|
|
4237
|
+
const message = p.message;
|
|
4238
|
+
const blocks2 = Array.isArray(message?.content) ? message.content : [];
|
|
4239
|
+
return blocks2.flatMap((b) => {
|
|
4240
|
+
if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
|
|
4241
|
+
return [{ type: "text", text: b.text, timestamp: ts }];
|
|
4242
|
+
}
|
|
4243
|
+
if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.length > 0) {
|
|
4244
|
+
return [{ type: "thinking", text: b.thinking, timestamp: ts }];
|
|
4245
|
+
}
|
|
4246
|
+
if (b.type === "tool_use") {
|
|
4247
|
+
return [{
|
|
4248
|
+
type: "tool_use",
|
|
4249
|
+
toolName: String(b.name ?? "tool"),
|
|
4250
|
+
toolInput: b.input ?? {},
|
|
4251
|
+
toolUseId: String(b.id ?? ""),
|
|
4252
|
+
timestamp: ts
|
|
4253
|
+
}];
|
|
4254
|
+
}
|
|
4255
|
+
return [];
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
case "user": {
|
|
4259
|
+
const message = p.message;
|
|
4260
|
+
const blocks2 = Array.isArray(message?.content) ? message.content : [];
|
|
4261
|
+
return blocks2.flatMap((b) => b.type === "tool_result" ? [{
|
|
4262
|
+
type: "tool_result",
|
|
4263
|
+
toolUseId: String(b.tool_use_id ?? ""),
|
|
4264
|
+
output: toolResultText(b.content),
|
|
4265
|
+
isError: b.is_error === true,
|
|
4266
|
+
timestamp: ts
|
|
4267
|
+
}] : []);
|
|
4268
|
+
}
|
|
4269
|
+
case "stream_event": {
|
|
4270
|
+
const ev = p.event;
|
|
4271
|
+
if (ev?.type === "content_block_delta" && ev.delta?.type === "text_delta" && typeof ev.delta.text === "string" && ev.delta.text.length > 0) {
|
|
4272
|
+
return [{ type: "text_delta", text: ev.delta.text, timestamp: ts }];
|
|
4273
|
+
}
|
|
4274
|
+
if (ev?.type === "message_start" && ev.message?.usage) {
|
|
4275
|
+
const u = ev.message.usage;
|
|
4276
|
+
return [{
|
|
4277
|
+
type: "usage_delta",
|
|
4278
|
+
boundary: "call_start",
|
|
4279
|
+
inputTokens: u.input_tokens ?? 0,
|
|
4280
|
+
outputTokens: u.output_tokens ?? 0,
|
|
4281
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
4282
|
+
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
4283
|
+
timestamp: ts
|
|
4284
|
+
}];
|
|
4285
|
+
}
|
|
4286
|
+
if (ev?.type === "message_delta" && ev.usage && typeof ev.usage.output_tokens === "number") {
|
|
4287
|
+
return [{
|
|
4288
|
+
type: "usage_delta",
|
|
4289
|
+
boundary: "call_delta",
|
|
4290
|
+
inputTokens: 0,
|
|
4291
|
+
outputTokens: ev.usage.output_tokens,
|
|
4292
|
+
cacheReadTokens: 0,
|
|
4293
|
+
cacheCreationTokens: 0,
|
|
4294
|
+
timestamp: ts
|
|
4295
|
+
}];
|
|
4296
|
+
}
|
|
4297
|
+
return [];
|
|
4298
|
+
}
|
|
4299
|
+
case "result": {
|
|
4300
|
+
if (p.is_error === true) {
|
|
4301
|
+
return [{ type: "error", text: formatError(p.result ?? p.subtype ?? p), timestamp: ts }];
|
|
4302
|
+
}
|
|
4303
|
+
const u = p.usage;
|
|
4304
|
+
if (!u)
|
|
4305
|
+
return [];
|
|
4306
|
+
return [{
|
|
4307
|
+
type: "usage",
|
|
4308
|
+
inputTokens: u.input_tokens ?? 0,
|
|
4309
|
+
outputTokens: u.output_tokens ?? 0,
|
|
4310
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
4311
|
+
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
4312
|
+
durationMs: typeof p.duration_ms === "number" ? p.duration_ms : 0,
|
|
4313
|
+
numTurns: typeof p.num_turns === "number" ? p.num_turns : 1,
|
|
4314
|
+
timestamp: ts
|
|
4315
|
+
}];
|
|
4316
|
+
}
|
|
4317
|
+
default:
|
|
4318
|
+
return [];
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
};
|
|
4322
|
+
function createClaudeCodeRuntime(config = {}) {
|
|
4323
|
+
return createCliAgentRuntime(claudeCodeSpec, config.model, config.effort);
|
|
4324
|
+
}
|
|
4325
|
+
var claude_code_default = createClaudeCodeRuntime();
|
|
4326
|
+
// src/sandbox/network-policy.ts
|
|
4327
|
+
var DOT_SEGMENT_PATH_RE2 = ".*(?:^|/|%2[fF])(?:\\.|%2[eE]){2}(?:/|%2[fF]|$).*";
|
|
4328
|
+
var DOT_SEGMENT_RE = /(?:^|\/|%2f)(?:\.|%2e){2}(?:\/|%2f|$)/i;
|
|
4329
|
+
function assertSafePathPrefix(domain, prefix) {
|
|
4330
|
+
if (!prefix.startsWith("/") || DOT_SEGMENT_RE.test(prefix)) {
|
|
4331
|
+
throw new Error(`Invalid Tier-2 pathPrefix ${JSON.stringify(prefix)} for "${domain}": prefixes must start with "/" and contain no dot-segments`);
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
function toVercelNetworkPolicy(policy) {
|
|
4335
|
+
if (typeof policy === "string" || !policy.allow || Array.isArray(policy.allow)) {
|
|
4336
|
+
return policy;
|
|
4337
|
+
}
|
|
4338
|
+
const allow = {};
|
|
4339
|
+
for (const [domain, rules] of Object.entries(policy.allow)) {
|
|
4340
|
+
const translated = rules.flatMap((rule) => {
|
|
4341
|
+
const { requestRules, ...rest } = rule;
|
|
4342
|
+
const methods = requestRules?.methods ?? [];
|
|
4343
|
+
const prefixes = requestRules?.pathPrefixes ?? [];
|
|
4344
|
+
if (methods.length === 0 && prefixes.length === 0)
|
|
4345
|
+
return [rest];
|
|
4346
|
+
const methodMatch = methods.length > 0 ? { method: methods } : {};
|
|
4347
|
+
if (prefixes.length === 0)
|
|
4348
|
+
return [{ ...rest, match: methodMatch }];
|
|
4349
|
+
for (const prefix of prefixes)
|
|
4350
|
+
assertSafePathPrefix(domain, prefix);
|
|
4351
|
+
return prefixes.map((prefix) => ({ ...rest, match: { ...methodMatch, path: { startsWith: prefix } } }));
|
|
4352
|
+
});
|
|
4353
|
+
const hasPathGate = rules.some((rule) => (rule.requestRules?.pathPrefixes?.length ?? 0) > 0);
|
|
4354
|
+
allow[domain] = hasPathGate ? [{ match: { path: { regex: DOT_SEGMENT_PATH_RE2 } } }, ...translated] : translated;
|
|
4355
|
+
}
|
|
4356
|
+
return { ...policy, allow };
|
|
4357
|
+
}
|
|
4358
|
+
function toE2bNetwork(policy) {
|
|
4359
|
+
if (policy === "allow-all")
|
|
4360
|
+
return {};
|
|
4361
|
+
if (policy === "deny-all")
|
|
4362
|
+
return { denyOut: ({ allTraffic }) => [allTraffic] };
|
|
4363
|
+
const allowHosts = Array.isArray(policy.allow) ? [...policy.allow] : Object.keys(policy.allow ?? {});
|
|
4364
|
+
const subnetAllow = policy.subnets?.allow ?? [];
|
|
4365
|
+
const allowOut = [...new Set([...allowHosts, ...subnetAllow])];
|
|
4366
|
+
const network = {
|
|
4367
|
+
denyOut: ({ allTraffic }) => [allTraffic]
|
|
4368
|
+
};
|
|
4369
|
+
if (allowOut.length > 0)
|
|
4370
|
+
network.allowOut = allowOut;
|
|
4371
|
+
if (!Array.isArray(policy.allow) && policy.allow) {
|
|
4372
|
+
const rules = {};
|
|
4373
|
+
for (const [host, hostRules] of Object.entries(policy.allow)) {
|
|
4374
|
+
const hostTransforms = hostRules.flatMap((rule) => (rule.transform ?? []).filter((t) => t.headers && Object.keys(t.headers).length > 0).map((t) => ({ transform: { headers: t.headers } })));
|
|
4375
|
+
if (hostTransforms.length > 0)
|
|
4376
|
+
rules[host] = hostTransforms;
|
|
4377
|
+
}
|
|
4378
|
+
if (Object.keys(rules).length > 0)
|
|
4379
|
+
network.rules = rules;
|
|
4380
|
+
}
|
|
4381
|
+
return network;
|
|
4382
|
+
}
|
|
3380
4383
|
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
4384
|
+
// src/sandbox/sizes.ts
|
|
4385
|
+
var SANDBOX_VCPUS = {
|
|
4386
|
+
"2vcpu-4gb": 2,
|
|
4387
|
+
"4vcpu-8gb": 4,
|
|
4388
|
+
"8vcpu-16gb": 8,
|
|
4389
|
+
"32vcpu-64gb": 32
|
|
4390
|
+
};
|
|
4391
|
+
var DEFAULT_SANDBOX_SIZE = "2vcpu-4gb";
|
|
4392
|
+
var E2B_TEMPLATE_SIZES = [
|
|
4393
|
+
"2vcpu-4gb",
|
|
4394
|
+
"4vcpu-8gb"
|
|
4395
|
+
];
|
|
4396
|
+
function isE2bSupportedSize(size) {
|
|
4397
|
+
return E2B_TEMPLATE_SIZES.includes(size);
|
|
4398
|
+
}
|
|
4399
|
+
function e2bMachineSpec(size) {
|
|
4400
|
+
const cpuCount = SANDBOX_VCPUS[size];
|
|
4401
|
+
return { cpuCount, memoryMB: cpuCount * 2048 };
|
|
4402
|
+
}
|
|
4403
|
+
function e2bBaseTemplate(size) {
|
|
4404
|
+
return `agent-compose-base-${size}`;
|
|
4405
|
+
}
|
|
4406
|
+
function e2bAgentEnvTemplate(size) {
|
|
4407
|
+
return `agent-env-${size}`;
|
|
4408
|
+
}
|
|
4409
|
+
function isPlatformE2bTemplateAlias(id) {
|
|
4410
|
+
return E2B_TEMPLATE_SIZES.some((size) => id === e2bBaseTemplate(size) || id === e2bAgentEnvTemplate(size));
|
|
4411
|
+
}
|
|
4412
|
+
// src/sandbox/devbox.ts
|
|
4413
|
+
var E2B_DEVBOX_TEMPLATE = "agent-compose-devbox";
|
|
4414
|
+
var E2B_DEVBOX_SPEC = {
|
|
4415
|
+
cpuCount: 8,
|
|
4416
|
+
memoryMB: 8192
|
|
4417
|
+
};
|
|
4418
|
+
var E2B_DEVBOX_RECIPE_VERSION = "1";
|
|
4419
|
+
function e2bDevboxTemplateRef() {
|
|
4420
|
+
return `${E2B_DEVBOX_TEMPLATE}@${E2B_DEVBOX_RECIPE_VERSION}`;
|
|
4421
|
+
}
|
|
4422
|
+
// src/sandbox/provider-def.ts
|
|
4423
|
+
var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
|
|
4424
|
+
// src/sandbox/exec-stream.ts
|
|
4425
|
+
async function parseSseExecStream(body, opts) {
|
|
4426
|
+
let stdout = "", stderr = "", exitCode = 0, exited = false;
|
|
4427
|
+
const reader = body.getReader(), decoder = new TextDecoder;
|
|
4428
|
+
let buf = "";
|
|
4429
|
+
while (true) {
|
|
4430
|
+
const { done, value } = await reader.read();
|
|
4431
|
+
if (done)
|
|
4432
|
+
break;
|
|
4433
|
+
buf += decoder.decode(value, { stream: true });
|
|
4434
|
+
const lines = buf.replace(/\r\n/g, `
|
|
4435
|
+
`).split(`
|
|
4436
|
+
`);
|
|
4437
|
+
buf = lines.pop() ?? "";
|
|
4438
|
+
for (const line of lines) {
|
|
4439
|
+
if (!line.startsWith("data: "))
|
|
4440
|
+
continue;
|
|
4441
|
+
let event;
|
|
4442
|
+
try {
|
|
4443
|
+
event = JSON.parse(line.slice(6));
|
|
4444
|
+
} catch (err) {
|
|
4445
|
+
throw new Error(`Malformed sandbox exec event: ${err instanceof Error ? err.message : String(err)}`);
|
|
4446
|
+
}
|
|
4447
|
+
if (event.type === "stdout") {
|
|
4448
|
+
stdout += event.data ?? "";
|
|
4449
|
+
opts?.onStdout?.(event.data ?? "");
|
|
4450
|
+
} else if (event.type === "stderr") {
|
|
4451
|
+
stderr += event.data ?? "";
|
|
4452
|
+
opts?.onStderr?.(event.data ?? "");
|
|
4453
|
+
} else if (event.type === "exit") {
|
|
4454
|
+
exitCode = event.exitCode ?? 0;
|
|
4455
|
+
exited = true;
|
|
4456
|
+
} else if (event.type === "error") {
|
|
4457
|
+
throw new Error(`Sandbox exec error: ${event.data}`);
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
if (exited)
|
|
4461
|
+
break;
|
|
3389
4462
|
}
|
|
4463
|
+
if (!exited)
|
|
4464
|
+
throw new Error("Sandbox exec stream ended without an exit event");
|
|
4465
|
+
return { exitCode, stdout, stderr };
|
|
3390
4466
|
}
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
|
|
3395
|
-
var SANDBOX_VCPUS = {
|
|
3396
|
-
"2vcpu-4gb": 2,
|
|
3397
|
-
"4vcpu-8gb": 4,
|
|
3398
|
-
"8vcpu-16gb": 8,
|
|
3399
|
-
"32vcpu-64gb": 32
|
|
3400
|
-
};
|
|
3401
|
-
var DEFAULT_SANDBOX_SIZE = "2vcpu-4gb";
|
|
3402
|
-
var E2B_TEMPLATE_SIZES = [
|
|
3403
|
-
"2vcpu-4gb",
|
|
3404
|
-
"4vcpu-8gb"
|
|
3405
|
-
];
|
|
3406
|
-
function isE2bSupportedSize(size) {
|
|
3407
|
-
return E2B_TEMPLATE_SIZES.includes(size);
|
|
3408
|
-
}
|
|
3409
|
-
function e2bMachineSpec(size) {
|
|
3410
|
-
const cpuCount = SANDBOX_VCPUS[size];
|
|
3411
|
-
return { cpuCount, memoryMB: cpuCount * 2048 };
|
|
3412
|
-
}
|
|
3413
|
-
function e2bBaseTemplate(size) {
|
|
3414
|
-
return `agent-compose-base-${size}`;
|
|
3415
|
-
}
|
|
3416
|
-
function e2bAgentEnvTemplate(size) {
|
|
3417
|
-
return `agent-env-${size}`;
|
|
3418
|
-
}
|
|
3419
|
-
function isPlatformE2bTemplateAlias(id) {
|
|
3420
|
-
return E2B_TEMPLATE_SIZES.some((size) => id === e2bBaseTemplate(size) || id === e2bAgentEnvTemplate(size));
|
|
3421
|
-
}
|
|
4467
|
+
// src/sandbox/providers/e2b.ts
|
|
4468
|
+
import { Sandbox } from "e2b";
|
|
4469
|
+
import pRetry from "p-retry";
|
|
3422
4470
|
function makeSandboxProvider(sb) {
|
|
3423
4471
|
const commands = sb.commands;
|
|
3424
4472
|
return {
|
|
@@ -3458,31 +4506,22 @@ function makeSandboxProvider(sb) {
|
|
|
3458
4506
|
files: {
|
|
3459
4507
|
async write(path, content) {
|
|
3460
4508
|
await sb.files.write(path, content);
|
|
4509
|
+
},
|
|
4510
|
+
async read(path) {
|
|
4511
|
+
return await sb.files.read(path);
|
|
3461
4512
|
}
|
|
3462
4513
|
},
|
|
3463
4514
|
async kill() {
|
|
3464
4515
|
await sb.kill();
|
|
4516
|
+
},
|
|
4517
|
+
getHost(port) {
|
|
4518
|
+
return sb.getHost(port);
|
|
3465
4519
|
}
|
|
3466
4520
|
};
|
|
3467
4521
|
}
|
|
3468
4522
|
function e2bMaxSandboxMs() {
|
|
3469
4523
|
return Number(process.env.E2B_MAX_SANDBOX_MS) || 60 * 60 * 1000;
|
|
3470
4524
|
}
|
|
3471
|
-
function isTransientSandboxError(error) {
|
|
3472
|
-
if (error instanceof SandboxNotFoundError || error instanceof RateLimitError)
|
|
3473
|
-
return true;
|
|
3474
|
-
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
3475
|
-
return /paus|stopp|resum|transition|status(?:\s+code)?[:\s]+5\d\d\b|\b5\d\d\s+(?:internal|bad gateway|service|gateway)|\b429\b|ECONNRESET|ECONNREFUSED|ETIMEDOUT|socket hang up|fetch failed/i.test(message);
|
|
3476
|
-
}
|
|
3477
|
-
function withSandboxRetry(fn, shouldRetry = isTransientSandboxError) {
|
|
3478
|
-
return pRetry(fn, { retries: 4, minTimeout: 400, factor: 2, shouldRetry });
|
|
3479
|
-
}
|
|
3480
|
-
function withSnapshotRetry(p) {
|
|
3481
|
-
if (!p.snapshot)
|
|
3482
|
-
return p;
|
|
3483
|
-
const snapshot = p.snapshot.bind(p);
|
|
3484
|
-
return { ...p, snapshot: () => withSandboxRetry(snapshot) };
|
|
3485
|
-
}
|
|
3486
4525
|
function wrapE2bBackgroundProcess(handle) {
|
|
3487
4526
|
return {
|
|
3488
4527
|
pid: handle.pid,
|
|
@@ -3507,6 +4546,22 @@ function wrapE2bBackgroundProcess(handle) {
|
|
|
3507
4546
|
}
|
|
3508
4547
|
};
|
|
3509
4548
|
}
|
|
4549
|
+
function wrapE2bPtyHandle(sb, handle, onExit) {
|
|
4550
|
+
handle.wait().catch(() => {
|
|
4551
|
+
return;
|
|
4552
|
+
}).finally(() => onExit?.());
|
|
4553
|
+
return {
|
|
4554
|
+
pid: handle.pid,
|
|
4555
|
+
sendInput: (data) => sb.pty.sendInput(handle.pid, data),
|
|
4556
|
+
resize: (size) => sb.pty.resize(handle.pid, size),
|
|
4557
|
+
kill: async () => {
|
|
4558
|
+
await sb.pty.kill(handle.pid);
|
|
4559
|
+
},
|
|
4560
|
+
disconnect: async () => {
|
|
4561
|
+
await handle.disconnect();
|
|
4562
|
+
}
|
|
4563
|
+
};
|
|
4564
|
+
}
|
|
3510
4565
|
function makeE2bSandboxProvider(sb) {
|
|
3511
4566
|
const base = makeSandboxProvider(sb);
|
|
3512
4567
|
return {
|
|
@@ -3533,6 +4588,24 @@ function makeE2bSandboxProvider(sb) {
|
|
|
3533
4588
|
return wrapE2bBackgroundProcess(handle);
|
|
3534
4589
|
}
|
|
3535
4590
|
},
|
|
4591
|
+
async createPty(opts) {
|
|
4592
|
+
const handle = await sb.pty.create({
|
|
4593
|
+
cols: opts.cols,
|
|
4594
|
+
rows: opts.rows,
|
|
4595
|
+
...opts.cwd ? { cwd: opts.cwd } : {},
|
|
4596
|
+
envs: { TERM: "xterm-256color", ...opts.envs ?? {} },
|
|
4597
|
+
onData: opts.onData,
|
|
4598
|
+
timeoutMs: 0
|
|
4599
|
+
});
|
|
4600
|
+
return wrapE2bPtyHandle(sb, handle, opts.onExit);
|
|
4601
|
+
},
|
|
4602
|
+
async connectPty(pid, opts) {
|
|
4603
|
+
const handle = await sb.pty.connect(pid, {
|
|
4604
|
+
onData: opts.onData,
|
|
4605
|
+
timeoutMs: 0
|
|
4606
|
+
});
|
|
4607
|
+
return wrapE2bPtyHandle(sb, handle, opts.onExit);
|
|
4608
|
+
},
|
|
3536
4609
|
async snapshot() {
|
|
3537
4610
|
const { snapshotId } = await sb.createSnapshot();
|
|
3538
4611
|
return { snapshotId };
|
|
@@ -3541,11 +4614,64 @@ function makeE2bSandboxProvider(sb) {
|
|
|
3541
4614
|
await sb.pause();
|
|
3542
4615
|
return { resumeHandle: sb.sandboxId };
|
|
3543
4616
|
},
|
|
4617
|
+
async extendLifetime(ms) {
|
|
4618
|
+
await sb.setTimeout(ms);
|
|
4619
|
+
},
|
|
3544
4620
|
async updateNetworkPolicy(policy) {
|
|
3545
4621
|
await sb.updateNetwork(toE2bNetwork(policy));
|
|
3546
4622
|
}
|
|
3547
4623
|
};
|
|
3548
4624
|
}
|
|
4625
|
+
async function listOwned() {
|
|
4626
|
+
const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG }, state: ["running"] } });
|
|
4627
|
+
const out = [];
|
|
4628
|
+
while (paginator.hasNext) {
|
|
4629
|
+
const items = await pRetry(() => paginator.nextItems(), { retries: 3, minTimeout: 500, factor: 2 });
|
|
4630
|
+
for (const sb of items) {
|
|
4631
|
+
out.push({
|
|
4632
|
+
sandboxId: sb.sandboxId,
|
|
4633
|
+
createdAt: new Date(sb.startedAt),
|
|
4634
|
+
metadata: sb.metadata ?? {}
|
|
4635
|
+
});
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
return out;
|
|
4639
|
+
}
|
|
4640
|
+
var e2bProviderDef = {
|
|
4641
|
+
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
4642
|
+
create: async ({ template, timeoutMs, networkPolicy, size, ...rest }) => {
|
|
4643
|
+
const maxMs = e2bMaxSandboxMs();
|
|
4644
|
+
if (timeoutMs > maxMs) {
|
|
4645
|
+
console.warn(`[sandbox] e2b create timeout clamped: requested ${timeoutMs}ms exceeds the plan cap ${maxMs}ms — ` + `the sandbox dies at the cap, not the requested deadline. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`);
|
|
4646
|
+
}
|
|
4647
|
+
const sandboxOpts = {
|
|
4648
|
+
...rest,
|
|
4649
|
+
timeoutMs: Math.min(timeoutMs, maxMs),
|
|
4650
|
+
...networkPolicy ? { network: toE2bNetwork(networkPolicy) } : {}
|
|
4651
|
+
};
|
|
4652
|
+
const resolvedSize = size ?? DEFAULT_SANDBOX_SIZE;
|
|
4653
|
+
const tmpl = template ?? (isE2bSupportedSize(resolvedSize) ? e2bBaseTemplate(resolvedSize) : undefined) ?? process.env.E2B_DEFAULT_TEMPLATE;
|
|
4654
|
+
return makeE2bSandboxProvider(await (tmpl ? Sandbox.create(tmpl, sandboxOpts) : Sandbox.create(sandboxOpts)));
|
|
4655
|
+
},
|
|
4656
|
+
reconnect: async (sandboxId) => makeE2bSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 60 * 60 * 1000 })),
|
|
4657
|
+
killAll: async () => {
|
|
4658
|
+
const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG } } });
|
|
4659
|
+
const sandboxes = [];
|
|
4660
|
+
while (paginator.hasNext)
|
|
4661
|
+
sandboxes.push(...await paginator.nextItems());
|
|
4662
|
+
if (sandboxes.length === 0)
|
|
4663
|
+
return;
|
|
4664
|
+
await Promise.all(sandboxes.map((s) => Sandbox.kill(s.sandboxId)));
|
|
4665
|
+
console.info(`[sandbox] killed ${sandboxes.length} stale E2B sandboxes (tag: ${AGENT_COMPOSE_TAG})`);
|
|
4666
|
+
},
|
|
4667
|
+
getActiveCount: async () => (await listOwned()).length,
|
|
4668
|
+
listOwned,
|
|
4669
|
+
deleteSnapshot: async (snapshotId, env) => {
|
|
4670
|
+
await Sandbox.deleteSnapshot(snapshotId, { apiKey: env.E2B_API_KEY });
|
|
4671
|
+
}
|
|
4672
|
+
};
|
|
4673
|
+
// src/sandbox/providers/desktop.ts
|
|
4674
|
+
import { Sandbox as Desktop } from "@e2b/desktop";
|
|
3549
4675
|
function makeDesktopSandboxProvider(sb) {
|
|
3550
4676
|
return {
|
|
3551
4677
|
...makeSandboxProvider(sb),
|
|
@@ -3561,104 +4687,39 @@ function makeDesktopSandboxProvider(sb) {
|
|
|
3561
4687
|
drag: (from, to) => sb.drag(from, to)
|
|
3562
4688
|
};
|
|
3563
4689
|
}
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
break;
|
|
3572
|
-
buf += decoder.decode(value, { stream: true });
|
|
3573
|
-
const lines = buf.replace(/\r\n/g, `
|
|
3574
|
-
`).split(`
|
|
3575
|
-
`);
|
|
3576
|
-
buf = lines.pop() ?? "";
|
|
3577
|
-
for (const line of lines) {
|
|
3578
|
-
if (!line.startsWith("data: "))
|
|
3579
|
-
continue;
|
|
3580
|
-
let event;
|
|
3581
|
-
try {
|
|
3582
|
-
event = JSON.parse(line.slice(6));
|
|
3583
|
-
} catch (err) {
|
|
3584
|
-
throw new Error(`Malformed sandbox exec event: ${err instanceof Error ? err.message : String(err)}`);
|
|
3585
|
-
}
|
|
3586
|
-
if (event.type === "stdout") {
|
|
3587
|
-
stdout += event.data ?? "";
|
|
3588
|
-
opts?.onStdout?.(event.data ?? "");
|
|
3589
|
-
} else if (event.type === "stderr") {
|
|
3590
|
-
stderr += event.data ?? "";
|
|
3591
|
-
opts?.onStderr?.(event.data ?? "");
|
|
3592
|
-
} else if (event.type === "exit") {
|
|
3593
|
-
exitCode = event.exitCode ?? 0;
|
|
3594
|
-
exited = true;
|
|
3595
|
-
} else if (event.type === "error") {
|
|
3596
|
-
throw new Error(`Sandbox exec error: ${event.data}`);
|
|
3597
|
-
}
|
|
4690
|
+
var e2bDesktopProviderDef = {
|
|
4691
|
+
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
4692
|
+
create: async ({ template, timeoutMs, networkPolicy, size: _size, ...rest }) => {
|
|
4693
|
+
if (!template)
|
|
4694
|
+
throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
|
|
4695
|
+
if (networkPolicy && networkPolicy !== "allow-all") {
|
|
4696
|
+
throw new Error("E2B Desktop provider cannot enforce an egress network policy — `@e2b/desktop` bundles an e2b version that predates the native firewall. " + "e2b-desktop is registry-only and must not run with a confined policy.");
|
|
3598
4697
|
}
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
throw new Error("Sandbox exec stream ended without an exit event");
|
|
3604
|
-
return { exitCode, stdout, stderr };
|
|
3605
|
-
}
|
|
3606
|
-
var DOT_SEGMENT_PATH_RE2 = ".*(?:^|/|%2[fF])(?:\\.|%2[eE]){2}(?:/|%2[fF]|$).*";
|
|
3607
|
-
var DOT_SEGMENT_RE = /(?:^|\/|%2f)(?:\.|%2e){2}(?:\/|%2f|$)/i;
|
|
3608
|
-
function assertSafePathPrefix(domain, prefix) {
|
|
3609
|
-
if (!prefix.startsWith("/") || DOT_SEGMENT_RE.test(prefix)) {
|
|
3610
|
-
throw new Error(`Invalid Tier-2 pathPrefix ${JSON.stringify(prefix)} for "${domain}": prefixes must start with "/" and contain no dot-segments`);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
function toVercelNetworkPolicy(policy) {
|
|
3614
|
-
if (typeof policy === "string" || !policy.allow || Array.isArray(policy.allow)) {
|
|
3615
|
-
return policy;
|
|
3616
|
-
}
|
|
3617
|
-
const allow = {};
|
|
3618
|
-
for (const [domain, rules] of Object.entries(policy.allow)) {
|
|
3619
|
-
const translated = rules.flatMap((rule) => {
|
|
3620
|
-
const { requestRules, ...rest } = rule;
|
|
3621
|
-
const methods = requestRules?.methods ?? [];
|
|
3622
|
-
const prefixes = requestRules?.pathPrefixes ?? [];
|
|
3623
|
-
if (methods.length === 0 && prefixes.length === 0)
|
|
3624
|
-
return [rest];
|
|
3625
|
-
const methodMatch = methods.length > 0 ? { method: methods } : {};
|
|
3626
|
-
if (prefixes.length === 0)
|
|
3627
|
-
return [{ ...rest, match: methodMatch }];
|
|
3628
|
-
for (const prefix of prefixes)
|
|
3629
|
-
assertSafePathPrefix(domain, prefix);
|
|
3630
|
-
return prefixes.map((prefix) => ({ ...rest, match: { ...methodMatch, path: { startsWith: prefix } } }));
|
|
3631
|
-
});
|
|
3632
|
-
const hasPathGate = rules.some((rule) => (rule.requestRules?.pathPrefixes?.length ?? 0) > 0);
|
|
3633
|
-
allow[domain] = hasPathGate ? [{ match: { path: { regex: DOT_SEGMENT_PATH_RE2 } } }, ...translated] : translated;
|
|
4698
|
+
return makeDesktopSandboxProvider(await Desktop.create(template, {
|
|
4699
|
+
...rest,
|
|
4700
|
+
timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs())
|
|
4701
|
+
}));
|
|
3634
4702
|
}
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
if (!Array.isArray(policy.allow) && policy.allow) {
|
|
3651
|
-
const rules = {};
|
|
3652
|
-
for (const [host, hostRules] of Object.entries(policy.allow)) {
|
|
3653
|
-
const hostTransforms = hostRules.flatMap((rule) => (rule.transform ?? []).filter((t) => t.headers && Object.keys(t.headers).length > 0).map((t) => ({ transform: { headers: t.headers } })));
|
|
3654
|
-
if (hostTransforms.length > 0)
|
|
3655
|
-
rules[host] = hostTransforms;
|
|
3656
|
-
}
|
|
3657
|
-
if (Object.keys(rules).length > 0)
|
|
3658
|
-
network.rules = rules;
|
|
4703
|
+
};
|
|
4704
|
+
// src/sandbox/providers/vercel.ts
|
|
4705
|
+
import pRetry2 from "p-retry";
|
|
4706
|
+
|
|
4707
|
+
// src/sandbox-errors.ts
|
|
4708
|
+
var SANDBOX_UNAVAILABLE_PREFIX = "[sandbox-unavailable";
|
|
4709
|
+
|
|
4710
|
+
class SandboxUnavailableError extends Error {
|
|
4711
|
+
retryable;
|
|
4712
|
+
sandboxId;
|
|
4713
|
+
constructor(detail, opts) {
|
|
4714
|
+
super(`${SANDBOX_UNAVAILABLE_PREFIX}:${opts.retryable ? "retryable" : "terminal"}] ${detail}`);
|
|
4715
|
+
this.name = "SandboxUnavailableError";
|
|
4716
|
+
this.retryable = opts.retryable;
|
|
4717
|
+
this.sandboxId = opts.sandboxId;
|
|
3659
4718
|
}
|
|
3660
|
-
return network;
|
|
3661
4719
|
}
|
|
4720
|
+
|
|
4721
|
+
// src/sandbox/providers/vercel.ts
|
|
4722
|
+
var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
|
|
3662
4723
|
var VERCEL_MAX_TAGS = 5;
|
|
3663
4724
|
function buildVercelTags(metadata) {
|
|
3664
4725
|
const tags = { ...metadata, executor: AGENT_COMPOSE_TAG };
|
|
@@ -3681,23 +4742,33 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
3681
4742
|
let stderr = "";
|
|
3682
4743
|
const handle = await sb.runCommand({ cmd: "sh", args: ["-c", cmd], cwd: opts?.cwd, env: mergeEnvs(opts?.envs), detached: true, signal, ...opts?.sudo ? { sudo: true } : {} }).catch((err) => asUnavailable(err, true));
|
|
3683
4744
|
const collect = async () => {
|
|
3684
|
-
await
|
|
4745
|
+
const finished = await pRetry2(async (attempt) => {
|
|
3685
4746
|
const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
4747
|
+
let seenOut = 0;
|
|
4748
|
+
let seenErr = 0;
|
|
4749
|
+
const fresh = (data, seen, delivered) => {
|
|
4750
|
+
const skip = Math.min(Math.max(delivered - seen, 0), data.length);
|
|
4751
|
+
return skip > 0 ? data.slice(skip) : data;
|
|
4752
|
+
};
|
|
3690
4753
|
for await (const log of h.logs()) {
|
|
3691
4754
|
if (log.stream === "stdout") {
|
|
3692
|
-
|
|
3693
|
-
|
|
4755
|
+
const add = fresh(log.data, seenOut, stdout.length);
|
|
4756
|
+
seenOut += log.data.length;
|
|
4757
|
+
if (add) {
|
|
4758
|
+
stdout += add;
|
|
4759
|
+
opts?.onStdout?.(add);
|
|
4760
|
+
}
|
|
3694
4761
|
} else {
|
|
3695
|
-
|
|
3696
|
-
|
|
4762
|
+
const add = fresh(log.data, seenErr, stderr.length);
|
|
4763
|
+
seenErr += log.data.length;
|
|
4764
|
+
if (add) {
|
|
4765
|
+
stderr += add;
|
|
4766
|
+
opts?.onStderr?.(add);
|
|
4767
|
+
}
|
|
3697
4768
|
}
|
|
3698
4769
|
}
|
|
4770
|
+
return await h.wait();
|
|
3699
4771
|
}, { retries: 3, minTimeout: 1000, factor: 2 });
|
|
3700
|
-
const finished = await handle.wait();
|
|
3701
4772
|
return { exitCode: finished.exitCode, stdout, stderr };
|
|
3702
4773
|
};
|
|
3703
4774
|
let timer;
|
|
@@ -3721,6 +4792,12 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
3721
4792
|
files: {
|
|
3722
4793
|
async write(path, content) {
|
|
3723
4794
|
await sb.writeFiles([{ path, content }]).catch((err) => asUnavailable(err, true));
|
|
4795
|
+
},
|
|
4796
|
+
async read(path) {
|
|
4797
|
+
const buf = await sb.readFileToBuffer({ path });
|
|
4798
|
+
if (buf === null)
|
|
4799
|
+
throw new Error(`file not found: ${path}`);
|
|
4800
|
+
return buf.toString("utf8");
|
|
3724
4801
|
}
|
|
3725
4802
|
},
|
|
3726
4803
|
async kill() {
|
|
@@ -3740,6 +4817,84 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
|
|
|
3740
4817
|
}
|
|
3741
4818
|
};
|
|
3742
4819
|
}
|
|
4820
|
+
async function listOwned2(env) {
|
|
4821
|
+
const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
|
|
4822
|
+
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
4823
|
+
const since = Date.now() - VERCEL_VM_LIFETIME_WINDOW_MS;
|
|
4824
|
+
const out = [];
|
|
4825
|
+
let cursor;
|
|
4826
|
+
for (let page = 0;page < 200; page++) {
|
|
4827
|
+
const result = await pRetry2(() => VercelSandbox.list({ ...creds, limit: 50, sortBy: "createdAt", sortOrder: "desc", tags: { executor: AGENT_COMPOSE_TAG }, ...cursor !== undefined ? { cursor } : {} }), { retries: 3, minTimeout: 500, factor: 2 });
|
|
4828
|
+
for (const sb of result.sandboxes) {
|
|
4829
|
+
if (sb.createdAt < since)
|
|
4830
|
+
return out;
|
|
4831
|
+
if (sb.status === "running") {
|
|
4832
|
+
out.push({ sandboxId: sb.name, createdAt: new Date(sb.createdAt), metadata: sb.tags ?? {} });
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
if (result.pagination.next === null)
|
|
4836
|
+
return out;
|
|
4837
|
+
cursor = result.pagination.next;
|
|
4838
|
+
}
|
|
4839
|
+
throw new Error("Vercel listSandboxes exceeded 200 pages (10k sandboxes) within the lifetime window — refuse to silently truncate");
|
|
4840
|
+
}
|
|
4841
|
+
var vercelProviderDef = {
|
|
4842
|
+
requiredEnv: {
|
|
4843
|
+
VERCEL_ACCESS_TOKEN: "Vercel access token — vercel.com/account/tokens",
|
|
4844
|
+
VERCEL_TEAM_ID: "Vercel team ID — vercel.com/account/settings",
|
|
4845
|
+
VERCEL_PROJECT_ID: "Vercel project ID — vercel.com/[team]/[project]/settings"
|
|
4846
|
+
},
|
|
4847
|
+
create: async (opts, env) => {
|
|
4848
|
+
const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
|
|
4849
|
+
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
4850
|
+
const np = opts.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(opts.networkPolicy) } : {};
|
|
4851
|
+
const tags = buildVercelTags(opts.metadata);
|
|
4852
|
+
const tmpl = opts.template ?? process.env.VERCEL_DEFAULT_SNAPSHOT;
|
|
4853
|
+
const resources = { vcpus: SANDBOX_VCPUS[opts.size ?? DEFAULT_SANDBOX_SIZE] };
|
|
4854
|
+
const sb = await VercelSandbox.create(tmpl ? { source: { type: "snapshot", snapshotId: tmpl }, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds } : { runtime: "node24", timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds });
|
|
4855
|
+
return makeVercelSandboxProvider(sb, opts.envs);
|
|
4856
|
+
},
|
|
4857
|
+
reconnect: async (sandboxId) => {
|
|
4858
|
+
const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
|
|
4859
|
+
const creds = {
|
|
4860
|
+
token: process.env.VERCEL_ACCESS_TOKEN ?? "",
|
|
4861
|
+
teamId: process.env.VERCEL_TEAM_ID ?? "",
|
|
4862
|
+
projectId: process.env.VERCEL_PROJECT_ID ?? ""
|
|
4863
|
+
};
|
|
4864
|
+
const sb = await VercelSandbox.get({ name: sandboxId, ...creds }).catch((err) => {
|
|
4865
|
+
throw new SandboxUnavailableError(err instanceof Error ? err.message : String(err), { retryable: true, sandboxId });
|
|
4866
|
+
});
|
|
4867
|
+
return makeVercelSandboxProvider(sb);
|
|
4868
|
+
},
|
|
4869
|
+
getActiveCount: async (env) => (await listOwned2(env)).length,
|
|
4870
|
+
listOwned: listOwned2,
|
|
4871
|
+
deleteSnapshot: async (snapshotId, env) => {
|
|
4872
|
+
const { Snapshot } = await import("@vercel/sandbox");
|
|
4873
|
+
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
4874
|
+
const snap = await Snapshot.get({ snapshotId, ...creds });
|
|
4875
|
+
await snap.delete();
|
|
4876
|
+
},
|
|
4877
|
+
snapshotExists: async (snapshotId, env) => {
|
|
4878
|
+
const { Snapshot } = await import("@vercel/sandbox");
|
|
4879
|
+
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
4880
|
+
try {
|
|
4881
|
+
await Snapshot.get({ snapshotId, ...creds });
|
|
4882
|
+
return true;
|
|
4883
|
+
} catch (err) {
|
|
4884
|
+
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
4885
|
+
const status = err?.status ?? err?.statusCode;
|
|
4886
|
+
if (status === 404 || /\b404\b|not[\s_-]?found|no such snapshot/i.test(message))
|
|
4887
|
+
return false;
|
|
4888
|
+
throw err;
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
};
|
|
4892
|
+
|
|
4893
|
+
// src/sandbox/providers/local.ts
|
|
4894
|
+
import { promises as fs2 } from "node:fs";
|
|
4895
|
+
import { dirname as dirname2 } from "node:path";
|
|
4896
|
+
import { spawn } from "node:child_process";
|
|
4897
|
+
import { Readable, Writable } from "node:stream";
|
|
3743
4898
|
function makeLocalSandboxProvider() {
|
|
3744
4899
|
return {
|
|
3745
4900
|
sandboxId: "local",
|
|
@@ -3805,145 +4960,37 @@ function makeLocalSandboxProvider() {
|
|
|
3805
4960
|
async write(path, content) {
|
|
3806
4961
|
await fs2.mkdir(dirname2(path), { recursive: true });
|
|
3807
4962
|
await fs2.writeFile(path, content);
|
|
4963
|
+
},
|
|
4964
|
+
async read(path) {
|
|
4965
|
+
return await fs2.readFile(path, "utf8");
|
|
3808
4966
|
}
|
|
3809
4967
|
},
|
|
3810
4968
|
async kill() {}
|
|
3811
4969
|
};
|
|
3812
4970
|
}
|
|
4971
|
+
// src/sandbox/registry.ts
|
|
4972
|
+
import { SandboxNotFoundError, RateLimitError } from "e2b";
|
|
4973
|
+
import pRetry3 from "p-retry";
|
|
3813
4974
|
var SANDBOX_PROVIDERS = {
|
|
3814
|
-
vercel:
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
VERCEL_TEAM_ID: "Vercel team ID — vercel.com/account/settings",
|
|
3818
|
-
VERCEL_PROJECT_ID: "Vercel project ID — vercel.com/[team]/[project]/settings"
|
|
3819
|
-
},
|
|
3820
|
-
create: async (opts, env) => {
|
|
3821
|
-
const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
|
|
3822
|
-
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
3823
|
-
const np = opts.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(opts.networkPolicy) } : {};
|
|
3824
|
-
const tags = buildVercelTags(opts.metadata);
|
|
3825
|
-
const tmpl = opts.template ?? process.env.VERCEL_DEFAULT_SNAPSHOT;
|
|
3826
|
-
const resources = { vcpus: SANDBOX_VCPUS[opts.size ?? DEFAULT_SANDBOX_SIZE] };
|
|
3827
|
-
const sb = await VercelSandbox.create(tmpl ? { source: { type: "snapshot", snapshotId: tmpl }, timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds } : { runtime: "node24", timeout: opts.timeoutMs, env: opts.envs, tags, persistent: false, resources, ...np, ...creds });
|
|
3828
|
-
return makeVercelSandboxProvider(sb, opts.envs);
|
|
3829
|
-
},
|
|
3830
|
-
reconnect: async (sandboxId) => {
|
|
3831
|
-
const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
|
|
3832
|
-
const creds = {
|
|
3833
|
-
token: process.env.VERCEL_ACCESS_TOKEN ?? "",
|
|
3834
|
-
teamId: process.env.VERCEL_TEAM_ID ?? "",
|
|
3835
|
-
projectId: process.env.VERCEL_PROJECT_ID ?? ""
|
|
3836
|
-
};
|
|
3837
|
-
const sb = await VercelSandbox.get({ name: sandboxId, ...creds }).catch((err) => {
|
|
3838
|
-
throw new SandboxUnavailableError(err instanceof Error ? err.message : String(err), { retryable: true, sandboxId });
|
|
3839
|
-
});
|
|
3840
|
-
return makeVercelSandboxProvider(sb);
|
|
3841
|
-
},
|
|
3842
|
-
getActiveCount: async (env) => (await SANDBOX_PROVIDERS.vercel.listOwned(env)).length,
|
|
3843
|
-
listOwned: async (env) => {
|
|
3844
|
-
const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
|
|
3845
|
-
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
3846
|
-
const since = Date.now() - VERCEL_VM_LIFETIME_WINDOW_MS;
|
|
3847
|
-
const out = [];
|
|
3848
|
-
let cursor;
|
|
3849
|
-
for (let page = 0;page < 200; page++) {
|
|
3850
|
-
const result = await pRetry(() => VercelSandbox.list({ ...creds, limit: 50, sortBy: "createdAt", sortOrder: "desc", tags: { executor: AGENT_COMPOSE_TAG }, ...cursor !== undefined ? { cursor } : {} }), { retries: 3, minTimeout: 500, factor: 2 });
|
|
3851
|
-
for (const sb of result.sandboxes) {
|
|
3852
|
-
if (sb.createdAt < since)
|
|
3853
|
-
return out;
|
|
3854
|
-
if (sb.status === "running") {
|
|
3855
|
-
out.push({ sandboxId: sb.name, createdAt: new Date(sb.createdAt), metadata: sb.tags ?? {} });
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
if (result.pagination.next === null)
|
|
3859
|
-
return out;
|
|
3860
|
-
cursor = result.pagination.next;
|
|
3861
|
-
}
|
|
3862
|
-
throw new Error("Vercel listSandboxes exceeded 200 pages (10k sandboxes) within the lifetime window — refuse to silently truncate");
|
|
3863
|
-
},
|
|
3864
|
-
deleteSnapshot: async (snapshotId, env) => {
|
|
3865
|
-
const { Snapshot } = await import("@vercel/sandbox");
|
|
3866
|
-
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
3867
|
-
const snap = await Snapshot.get({ snapshotId, ...creds });
|
|
3868
|
-
await snap.delete();
|
|
3869
|
-
},
|
|
3870
|
-
snapshotExists: async (snapshotId, env) => {
|
|
3871
|
-
const { Snapshot } = await import("@vercel/sandbox");
|
|
3872
|
-
const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
|
|
3873
|
-
try {
|
|
3874
|
-
await Snapshot.get({ snapshotId, ...creds });
|
|
3875
|
-
return true;
|
|
3876
|
-
} catch (err) {
|
|
3877
|
-
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
3878
|
-
const status = err?.status ?? err?.statusCode;
|
|
3879
|
-
if (status === 404 || /\b404\b|not[\s_-]?found|no such snapshot/i.test(message))
|
|
3880
|
-
return false;
|
|
3881
|
-
throw err;
|
|
3882
|
-
}
|
|
3883
|
-
}
|
|
3884
|
-
},
|
|
3885
|
-
e2b: {
|
|
3886
|
-
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
3887
|
-
create: async ({ template, timeoutMs, networkPolicy, size, ...rest }) => {
|
|
3888
|
-
const maxMs = e2bMaxSandboxMs();
|
|
3889
|
-
if (timeoutMs > maxMs) {
|
|
3890
|
-
console.warn(`[sandbox] e2b create timeout clamped: requested ${timeoutMs}ms exceeds the plan cap ${maxMs}ms — ` + `the sandbox dies at the cap, not the requested deadline. Raise E2B_MAX_SANDBOX_MS on plans that allow more.`);
|
|
3891
|
-
}
|
|
3892
|
-
const sandboxOpts = {
|
|
3893
|
-
...rest,
|
|
3894
|
-
timeoutMs: Math.min(timeoutMs, maxMs),
|
|
3895
|
-
...networkPolicy ? { network: toE2bNetwork(networkPolicy) } : {}
|
|
3896
|
-
};
|
|
3897
|
-
const resolvedSize = size ?? DEFAULT_SANDBOX_SIZE;
|
|
3898
|
-
const tmpl = template ?? (isE2bSupportedSize(resolvedSize) ? e2bBaseTemplate(resolvedSize) : undefined) ?? process.env.E2B_DEFAULT_TEMPLATE;
|
|
3899
|
-
return makeE2bSandboxProvider(await (tmpl ? Sandbox.create(tmpl, sandboxOpts) : Sandbox.create(sandboxOpts)));
|
|
3900
|
-
},
|
|
3901
|
-
reconnect: async (sandboxId) => makeE2bSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
|
|
3902
|
-
killAll: async () => {
|
|
3903
|
-
const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG } } });
|
|
3904
|
-
const sandboxes = [];
|
|
3905
|
-
while (paginator.hasNext)
|
|
3906
|
-
sandboxes.push(...await paginator.nextItems());
|
|
3907
|
-
if (sandboxes.length === 0)
|
|
3908
|
-
return;
|
|
3909
|
-
await Promise.all(sandboxes.map((s) => Sandbox.kill(s.sandboxId)));
|
|
3910
|
-
console.info(`[sandbox] killed ${sandboxes.length} stale E2B sandboxes (tag: ${AGENT_COMPOSE_TAG})`);
|
|
3911
|
-
},
|
|
3912
|
-
getActiveCount: async () => (await SANDBOX_PROVIDERS.e2b.listOwned({})).length,
|
|
3913
|
-
listOwned: async () => {
|
|
3914
|
-
const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG }, state: ["running"] } });
|
|
3915
|
-
const out = [];
|
|
3916
|
-
while (paginator.hasNext) {
|
|
3917
|
-
const items = await pRetry(() => paginator.nextItems(), { retries: 3, minTimeout: 500, factor: 2 });
|
|
3918
|
-
for (const sb of items) {
|
|
3919
|
-
out.push({
|
|
3920
|
-
sandboxId: sb.sandboxId,
|
|
3921
|
-
createdAt: new Date(sb.startedAt),
|
|
3922
|
-
metadata: sb.metadata ?? {}
|
|
3923
|
-
});
|
|
3924
|
-
}
|
|
3925
|
-
}
|
|
3926
|
-
return out;
|
|
3927
|
-
},
|
|
3928
|
-
deleteSnapshot: async (snapshotId, env) => {
|
|
3929
|
-
await Sandbox.deleteSnapshot(snapshotId, { apiKey: env.E2B_API_KEY });
|
|
3930
|
-
}
|
|
3931
|
-
},
|
|
3932
|
-
"e2b-desktop": {
|
|
3933
|
-
requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
|
|
3934
|
-
create: async ({ template, timeoutMs, networkPolicy, size: _size, ...rest }) => {
|
|
3935
|
-
if (!template)
|
|
3936
|
-
throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
|
|
3937
|
-
if (networkPolicy && networkPolicy !== "allow-all") {
|
|
3938
|
-
throw new Error("E2B Desktop provider cannot enforce an egress network policy — `@e2b/desktop` bundles an e2b version that predates the native firewall. " + "e2b-desktop is registry-only and must not run with a confined policy.");
|
|
3939
|
-
}
|
|
3940
|
-
return makeDesktopSandboxProvider(await Desktop.create(template, {
|
|
3941
|
-
...rest,
|
|
3942
|
-
timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs())
|
|
3943
|
-
}));
|
|
3944
|
-
}
|
|
3945
|
-
}
|
|
4975
|
+
vercel: vercelProviderDef,
|
|
4976
|
+
e2b: e2bProviderDef,
|
|
4977
|
+
"e2b-desktop": e2bDesktopProviderDef
|
|
3946
4978
|
};
|
|
4979
|
+
function isTransientSandboxError(error) {
|
|
4980
|
+
if (error instanceof SandboxNotFoundError || error instanceof RateLimitError)
|
|
4981
|
+
return true;
|
|
4982
|
+
const message = error instanceof Error ? error.message : String(error ?? "");
|
|
4983
|
+
return /paus|stopp|resum|transition|status(?:\s+code)?[:\s]+5\d\d\b|\b5\d\d\s+(?:internal|bad gateway|service|gateway)|\b429\b|ECONNRESET|ECONNREFUSED|ETIMEDOUT|socket hang up|fetch failed/i.test(message);
|
|
4984
|
+
}
|
|
4985
|
+
function withSandboxRetry(fn, shouldRetry = isTransientSandboxError) {
|
|
4986
|
+
return pRetry3(fn, { retries: 4, minTimeout: 400, factor: 2, shouldRetry });
|
|
4987
|
+
}
|
|
4988
|
+
function withSnapshotRetry(p) {
|
|
4989
|
+
if (!p.snapshot)
|
|
4990
|
+
return p;
|
|
4991
|
+
const snapshot = p.snapshot.bind(p);
|
|
4992
|
+
return { ...p, snapshot: () => withSandboxRetry(snapshot) };
|
|
4993
|
+
}
|
|
3947
4994
|
async function createSandbox(provider, opts) {
|
|
3948
4995
|
const def = SANDBOX_PROVIDERS[provider];
|
|
3949
4996
|
if (!def)
|
|
@@ -4114,7 +5161,7 @@ class StepObservabilityCollector {
|
|
|
4114
5161
|
await this.drainInFlight();
|
|
4115
5162
|
const hasMetadata = Object.keys(this.metadata).length > 0;
|
|
4116
5163
|
const hasSubSteps = this.subSteps.length > 0;
|
|
4117
|
-
const remainingEvents = this.events.
|
|
5164
|
+
const remainingEvents = this.events.map((event, seq) => ({ ...event, seq })).filter(({ seq }) => !this.ackedSeqs.has(seq));
|
|
4118
5165
|
const hasEvents = remainingEvents.length > 0;
|
|
4119
5166
|
if (!hasMetadata && !hasEvents && !hasSubSteps)
|
|
4120
5167
|
return;
|
|
@@ -4147,6 +5194,8 @@ function scopedMemoize(scope) {
|
|
|
4147
5194
|
}
|
|
4148
5195
|
|
|
4149
5196
|
// src/pause/wrappers.ts
|
|
5197
|
+
import { z as z11 } from "zod";
|
|
5198
|
+
var DecisionResultSchema = z11.object({ decision: z11.string() });
|
|
4150
5199
|
function buildPauseWrappers(pause) {
|
|
4151
5200
|
return {
|
|
4152
5201
|
sleep(durationMs) {
|
|
@@ -4164,6 +5213,20 @@ function buildPauseWrappers(pause) {
|
|
|
4164
5213
|
...req.schema !== undefined ? { schema: req.schema } : {},
|
|
4165
5214
|
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}
|
|
4166
5215
|
}, "event");
|
|
5216
|
+
},
|
|
5217
|
+
requestDecision(req) {
|
|
5218
|
+
return pause({
|
|
5219
|
+
reason: req.prompt,
|
|
5220
|
+
payload: {
|
|
5221
|
+
prompt: req.prompt,
|
|
5222
|
+
...req.options !== undefined ? { options: req.options } : {},
|
|
5223
|
+
...req.approvers !== undefined ? { approvers: req.approvers } : {},
|
|
5224
|
+
...req.draft !== undefined ? { draft: req.draft } : {}
|
|
5225
|
+
},
|
|
5226
|
+
schema: DecisionResultSchema,
|
|
5227
|
+
...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {},
|
|
5228
|
+
...req.correlationKey !== undefined ? { correlationKey: req.correlationKey } : {}
|
|
5229
|
+
}, "decision");
|
|
4167
5230
|
}
|
|
4168
5231
|
};
|
|
4169
5232
|
}
|
|
@@ -4466,14 +5529,35 @@ function buildInvokeChild(runId, opts = {}) {
|
|
|
4466
5529
|
});
|
|
4467
5530
|
}
|
|
4468
5531
|
// src/workflow-steps/step.ts
|
|
5532
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
4469
5533
|
function defineStep(opts) {
|
|
4470
5534
|
if (!opts.name)
|
|
4471
5535
|
throw new Error("defineStep: 'name' is required");
|
|
5536
|
+
if (opts.summary !== undefined) {
|
|
5537
|
+
if (opts.summary.length === 0 || opts.summary.length > 200 || CONTROL_CHARS.test(opts.summary)) {
|
|
5538
|
+
throw new Error(`defineStep("${opts.name}"): 'summary' must be one plain sentence, 1-200 characters, no control characters`);
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
if (opts.deliverables !== undefined) {
|
|
5542
|
+
if (opts.deliverables.length > 8) {
|
|
5543
|
+
throw new Error(`defineStep("${opts.name}"): at most 8 deliverables`);
|
|
5544
|
+
}
|
|
5545
|
+
for (const d of opts.deliverables) {
|
|
5546
|
+
if (!d.path || d.path.length > 200 || CONTROL_CHARS.test(d.path)) {
|
|
5547
|
+
throw new Error(`defineStep("${opts.name}"): each deliverable needs a 'path' of 1-200 characters, no control characters`);
|
|
5548
|
+
}
|
|
5549
|
+
if (d.description !== undefined && (d.description.length === 0 || d.description.length > 200 || CONTROL_CHARS.test(d.description))) {
|
|
5550
|
+
throw new Error(`defineStep("${opts.name}"): deliverable descriptions must be 1-200 characters, no control characters`);
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
4472
5554
|
return {
|
|
4473
5555
|
name: opts.name,
|
|
4474
5556
|
input: opts.input,
|
|
4475
5557
|
output: opts.output,
|
|
4476
|
-
run: opts.run
|
|
5558
|
+
run: opts.run,
|
|
5559
|
+
...opts.summary !== undefined ? { summary: opts.summary } : {},
|
|
5560
|
+
...opts.deliverables !== undefined ? { deliverables: Object.freeze(opts.deliverables.map((d) => ({ ...d }))) } : {}
|
|
4477
5561
|
};
|
|
4478
5562
|
}
|
|
4479
5563
|
// src/step-invocation/protocol.ts
|
|
@@ -4505,34 +5589,40 @@ function requestContextPath(stepIndex) {
|
|
|
4505
5589
|
function stepResultFilePath(token) {
|
|
4506
5590
|
return `/tmp/wf/step-result-${token}.json`;
|
|
4507
5591
|
}
|
|
5592
|
+
function stepPidFilePath(token) {
|
|
5593
|
+
return `/tmp/wf/step-pid-${token}.pid`;
|
|
5594
|
+
}
|
|
5595
|
+
function stepLogFilePath(token) {
|
|
5596
|
+
return `/tmp/wf/step-log-${token}.log`;
|
|
5597
|
+
}
|
|
4508
5598
|
// src/step-invocation/invoker.ts
|
|
4509
5599
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
4510
|
-
import { z as
|
|
4511
|
-
var StepObservabilitySchema =
|
|
4512
|
-
metadata:
|
|
4513
|
-
events:
|
|
4514
|
-
subSteps:
|
|
4515
|
-
name:
|
|
4516
|
-
startedAt:
|
|
4517
|
-
durationMs:
|
|
4518
|
-
status:
|
|
4519
|
-
error:
|
|
5600
|
+
import { z as z12 } from "zod";
|
|
5601
|
+
var StepObservabilitySchema = z12.object({
|
|
5602
|
+
metadata: z12.record(z12.string(), z12.unknown()).optional(),
|
|
5603
|
+
events: z12.array(z12.unknown()).optional(),
|
|
5604
|
+
subSteps: z12.array(z12.object({
|
|
5605
|
+
name: z12.string(),
|
|
5606
|
+
startedAt: z12.number(),
|
|
5607
|
+
durationMs: z12.number(),
|
|
5608
|
+
status: z12.enum(["completed", "failed"]),
|
|
5609
|
+
error: z12.string().optional()
|
|
4520
5610
|
})).optional()
|
|
4521
5611
|
}).passthrough();
|
|
4522
|
-
var StepResultEnvelopeSchema =
|
|
4523
|
-
|
|
4524
|
-
ok:
|
|
4525
|
-
output:
|
|
5612
|
+
var StepResultEnvelopeSchema = z12.discriminatedUnion("ok", [
|
|
5613
|
+
z12.object({
|
|
5614
|
+
ok: z12.literal(true),
|
|
5615
|
+
output: z12.unknown().optional(),
|
|
4526
5616
|
observability: StepObservabilitySchema.optional()
|
|
4527
5617
|
}),
|
|
4528
|
-
|
|
4529
|
-
ok:
|
|
4530
|
-
kind:
|
|
4531
|
-
error:
|
|
5618
|
+
z12.object({
|
|
5619
|
+
ok: z12.literal(false),
|
|
5620
|
+
kind: z12.enum(["protocol", "user-step"]),
|
|
5621
|
+
error: z12.string().optional()
|
|
4532
5622
|
})
|
|
4533
5623
|
]);
|
|
4534
|
-
var StepPauseEnvelopeSchema =
|
|
4535
|
-
pauseId:
|
|
5624
|
+
var StepPauseEnvelopeSchema = z12.object({
|
|
5625
|
+
pauseId: z12.string().min(1),
|
|
4536
5626
|
pauseRequest: StepPauseRequestSchema,
|
|
4537
5627
|
observability: StepObservabilitySchema.optional()
|
|
4538
5628
|
});
|
|
@@ -4617,8 +5707,9 @@ function makeStreamSplitters(opts, resultToken) {
|
|
|
4617
5707
|
const isSentinel = (line) => line.startsWith(resultSentinel) || line.startsWith(pauseSentinel);
|
|
4618
5708
|
const make = (sink, filterSentinel) => {
|
|
4619
5709
|
if (!sink)
|
|
4620
|
-
return { onChunk: undefined, flush: () => {} };
|
|
5710
|
+
return { onChunk: undefined, flush: () => {}, consumed: () => 0 };
|
|
4621
5711
|
let buf = "";
|
|
5712
|
+
let consumed = 0;
|
|
4622
5713
|
return {
|
|
4623
5714
|
onChunk: (chunk) => {
|
|
4624
5715
|
buf += chunk;
|
|
@@ -4627,6 +5718,7 @@ function makeStreamSplitters(opts, resultToken) {
|
|
|
4627
5718
|
`)) !== -1) {
|
|
4628
5719
|
const line = buf.slice(0, nl);
|
|
4629
5720
|
buf = buf.slice(nl + 1);
|
|
5721
|
+
consumed += line.length + 1;
|
|
4630
5722
|
if (filterSentinel && isSentinel(line))
|
|
4631
5723
|
continue;
|
|
4632
5724
|
sink(line);
|
|
@@ -4637,10 +5729,12 @@ function makeStreamSplitters(opts, resultToken) {
|
|
|
4637
5729
|
return;
|
|
4638
5730
|
const line = buf;
|
|
4639
5731
|
buf = "";
|
|
5732
|
+
consumed += line.length;
|
|
4640
5733
|
if (filterSentinel && isSentinel(line))
|
|
4641
5734
|
return;
|
|
4642
5735
|
sink(line);
|
|
4643
|
-
}
|
|
5736
|
+
},
|
|
5737
|
+
consumed: () => consumed
|
|
4644
5738
|
};
|
|
4645
5739
|
};
|
|
4646
5740
|
const stdout = make(opts?.onStdout, true);
|
|
@@ -4651,7 +5745,8 @@ function makeStreamSplitters(opts, resultToken) {
|
|
|
4651
5745
|
flush: () => {
|
|
4652
5746
|
stdout.flush();
|
|
4653
5747
|
stderr.flush();
|
|
4654
|
-
}
|
|
5748
|
+
},
|
|
5749
|
+
stdoutConsumedChars: () => stdout.consumed()
|
|
4655
5750
|
};
|
|
4656
5751
|
}
|
|
4657
5752
|
async function classifyRunnerOutcome(sandbox, output, resultToken) {
|
|
@@ -4695,6 +5790,100 @@ ${tail}` : ""}`
|
|
|
4695
5790
|
}
|
|
4696
5791
|
};
|
|
4697
5792
|
}
|
|
5793
|
+
async function pollDurableResult(sandbox, runnerPid, resultToken, opts) {
|
|
5794
|
+
const signal = opts?.signal;
|
|
5795
|
+
const resultFile = stepResultFilePath(resultToken);
|
|
5796
|
+
const POLL_MS = 1000;
|
|
5797
|
+
const deadlineMs = opts?.deadlineMs ?? 45 * 60000;
|
|
5798
|
+
const DEADLINE = Date.now() + deadlineMs;
|
|
5799
|
+
const CMD_TIMEOUT_MS = 1e4;
|
|
5800
|
+
const UNREACHABLE_LIMIT = 20;
|
|
5801
|
+
let unreachableStreak = 0;
|
|
5802
|
+
const readResult = async () => {
|
|
5803
|
+
const r = await sandbox.commands.run(`cat ${resultFile} 2>/dev/null || true`, { timeoutMs: CMD_TIMEOUT_MS }).catch(() => "unreachable");
|
|
5804
|
+
if (r === "unreachable")
|
|
5805
|
+
return "unreachable";
|
|
5806
|
+
return r.stdout ? parseStepResult(r.stdout, resultToken) ?? null : null;
|
|
5807
|
+
};
|
|
5808
|
+
for (let attempt = 1;; attempt++) {
|
|
5809
|
+
if (signal?.aborted) {
|
|
5810
|
+
return { ok: false, error: { kind: "runner-exit", message: "reconnectStep aborted (run re-paused)", exitCode: 1 } };
|
|
5811
|
+
}
|
|
5812
|
+
const fromFile = await readResult();
|
|
5813
|
+
if (fromFile !== null && fromFile !== "unreachable")
|
|
5814
|
+
return fromFile;
|
|
5815
|
+
const probe = await sandbox.commands.run(`test -d /proc/${runnerPid} && echo alive || echo dead`, { timeoutMs: CMD_TIMEOUT_MS }).catch(() => null);
|
|
5816
|
+
if (fromFile === "unreachable" && probe === null) {
|
|
5817
|
+
unreachableStreak++;
|
|
5818
|
+
if (unreachableStreak >= UNREACHABLE_LIMIT) {
|
|
5819
|
+
return {
|
|
5820
|
+
ok: false,
|
|
5821
|
+
error: {
|
|
5822
|
+
kind: "runner-exit",
|
|
5823
|
+
message: `sandbox stopped responding while waiting for runner pid ${runnerPid} to emit a result (${UNREACHABLE_LIMIT} consecutive failed polls)`,
|
|
5824
|
+
exitCode: 1
|
|
5825
|
+
}
|
|
5826
|
+
};
|
|
5827
|
+
}
|
|
5828
|
+
} else {
|
|
5829
|
+
unreachableStreak = 0;
|
|
5830
|
+
}
|
|
5831
|
+
const dead = (probe?.stdout ?? "").includes("dead");
|
|
5832
|
+
process.stderr.write(`[recover] poll ${attempt}: result-file=absent runner=${dead ? "dead" : "alive"} pid=${runnerPid}
|
|
5833
|
+
`);
|
|
5834
|
+
if (dead) {
|
|
5835
|
+
const finalRead = await readResult();
|
|
5836
|
+
if (finalRead !== null && finalRead !== "unreachable")
|
|
5837
|
+
return finalRead;
|
|
5838
|
+
return {
|
|
5839
|
+
ok: false,
|
|
5840
|
+
error: { kind: "runner-exit", message: `runner pid ${runnerPid} exited without writing a result file`, exitCode: 1 }
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
if (Date.now() > DEADLINE) {
|
|
5844
|
+
return {
|
|
5845
|
+
ok: false,
|
|
5846
|
+
error: {
|
|
5847
|
+
kind: "runner-exit",
|
|
5848
|
+
message: `timed out after ${Math.round(deadlineMs / 60000)}m waiting for runner pid ${runnerPid} to emit a result`,
|
|
5849
|
+
exitCode: 1
|
|
5850
|
+
}
|
|
5851
|
+
};
|
|
5852
|
+
}
|
|
5853
|
+
await new Promise((r) => setTimeout(r, POLL_MS));
|
|
5854
|
+
}
|
|
5855
|
+
}
|
|
5856
|
+
async function recoverLogsAndResult(sandbox, runnerPid, resultToken, liveSplitters, opts, pollOpts) {
|
|
5857
|
+
const result = await pollDurableResult(sandbox, runnerPid, resultToken, pollOpts);
|
|
5858
|
+
if (sandbox.files.read && opts?.onStdout) {
|
|
5859
|
+
const fullLog = await sandbox.files.read(stepLogFilePath(resultToken)).catch(() => null);
|
|
5860
|
+
if (fullLog != null) {
|
|
5861
|
+
const already = liveSplitters.stdoutConsumedChars();
|
|
5862
|
+
const tail = fullLog.length > already ? fullLog.slice(already) : "";
|
|
5863
|
+
if (tail.length > 0) {
|
|
5864
|
+
const backfill = makeStreamSplitters(opts, resultToken);
|
|
5865
|
+
backfill.onStdout?.(tail);
|
|
5866
|
+
backfill.flush();
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
return result;
|
|
5871
|
+
}
|
|
5872
|
+
async function readRunnerPid(sandbox, resultToken) {
|
|
5873
|
+
const ATTEMPTS = 3;
|
|
5874
|
+
const RETRY_DELAY_MS = 2000;
|
|
5875
|
+
for (let attempt = 1;attempt <= ATTEMPTS; attempt++) {
|
|
5876
|
+
try {
|
|
5877
|
+
const r = await sandbox.commands.run(`cat ${stepPidFilePath(resultToken)} 2>/dev/null || true`, { timeoutMs: 1e4 });
|
|
5878
|
+
const pid = Number.parseInt(r.stdout.trim(), 10);
|
|
5879
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
5880
|
+
return pid;
|
|
5881
|
+
} catch {}
|
|
5882
|
+
if (attempt < ATTEMPTS)
|
|
5883
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
5884
|
+
}
|
|
5885
|
+
return null;
|
|
5886
|
+
}
|
|
4698
5887
|
async function prepareStepLaunch(sandbox, request, opts) {
|
|
4699
5888
|
const resultToken = randomBytes2(16).toString("hex");
|
|
4700
5889
|
await Promise.all([
|
|
@@ -4712,15 +5901,23 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
4712
5901
|
const splitters = makeStreamSplitters(opts, resultToken);
|
|
4713
5902
|
let result;
|
|
4714
5903
|
try {
|
|
4715
|
-
result = await sandbox.commands.run(RUNNER_COMMAND
|
|
4716
|
-
envs,
|
|
5904
|
+
result = await sandbox.commands.run(`set -o pipefail; echo $$ > ${stepPidFilePath(resultToken)}; ${RUNNER_COMMAND} | tee ${stepLogFilePath(resultToken)}`, {
|
|
5905
|
+
envs: { ...envs, HOME: "/root", IS_SANDBOX: "1" },
|
|
5906
|
+
sudo: true,
|
|
4717
5907
|
timeoutMs: 0,
|
|
4718
5908
|
...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
|
|
4719
5909
|
...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
|
|
4720
5910
|
});
|
|
4721
5911
|
} catch (e) {
|
|
4722
|
-
if (e instanceof SandboxUnavailableError)
|
|
4723
|
-
|
|
5912
|
+
if (e instanceof SandboxUnavailableError) {
|
|
5913
|
+
if (e.retryable)
|
|
5914
|
+
throw e;
|
|
5915
|
+
const runnerPid = await readRunnerPid(sandbox, resultToken);
|
|
5916
|
+
if (runnerPid === null)
|
|
5917
|
+
throw e;
|
|
5918
|
+
opts?.onStreamDegraded?.({ error: e, runnerPid, resultToken });
|
|
5919
|
+
return recoverLogsAndResult(sandbox, runnerPid, resultToken, splitters, opts, opts?.recoveryDeadlineMs !== undefined ? { deadlineMs: opts.recoveryDeadlineMs } : undefined);
|
|
5920
|
+
}
|
|
4724
5921
|
const ce = e;
|
|
4725
5922
|
result = {
|
|
4726
5923
|
stdout: ce.stdout ?? ce.result?.stdout ?? "",
|
|
@@ -4737,8 +5934,9 @@ async function launchStep(sandbox, request, opts) {
|
|
|
4737
5934
|
}
|
|
4738
5935
|
const { resultToken, envs } = await prepareStepLaunch(sandbox, request, opts);
|
|
4739
5936
|
const splitters = makeStreamSplitters(opts, resultToken);
|
|
4740
|
-
const proc = await sandbox.commands.runBackground(RUNNER_COMMAND
|
|
4741
|
-
envs,
|
|
5937
|
+
const proc = await sandbox.commands.runBackground(`set -o pipefail; ${RUNNER_COMMAND} | tee ${stepLogFilePath(resultToken)}`, {
|
|
5938
|
+
envs: { ...envs, HOME: "/root", IS_SANDBOX: "1" },
|
|
5939
|
+
sudo: true,
|
|
4742
5940
|
timeoutMs: 0,
|
|
4743
5941
|
...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
|
|
4744
5942
|
...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
|
|
@@ -4747,9 +5945,16 @@ async function launchStep(sandbox, request, opts) {
|
|
|
4747
5945
|
runnerPid: proc.pid,
|
|
4748
5946
|
resultToken,
|
|
4749
5947
|
async wait() {
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
5948
|
+
try {
|
|
5949
|
+
const result = await proc.wait();
|
|
5950
|
+
splitters.flush();
|
|
5951
|
+
return classifyRunnerOutcome(sandbox, result, resultToken);
|
|
5952
|
+
} catch (e) {
|
|
5953
|
+
if (e instanceof SandboxUnavailableError)
|
|
5954
|
+
throw e;
|
|
5955
|
+
opts?.onStreamDegraded?.({ error: e, runnerPid: proc.pid, resultToken });
|
|
5956
|
+
return recoverLogsAndResult(sandbox, proc.pid, resultToken, splitters, opts);
|
|
5957
|
+
}
|
|
4753
5958
|
}
|
|
4754
5959
|
};
|
|
4755
5960
|
}
|
|
@@ -4759,9 +5964,11 @@ async function reconnectStep(sandbox, resume, opts) {
|
|
|
4759
5964
|
}
|
|
4760
5965
|
const { runnerPid, resultToken } = resume;
|
|
4761
5966
|
const splitters = makeStreamSplitters(opts, resultToken);
|
|
4762
|
-
|
|
5967
|
+
const signal = opts?.signal;
|
|
5968
|
+
const pollDeadlineMs = opts?.pollDeadlineMs;
|
|
5969
|
+
let attached = null;
|
|
4763
5970
|
try {
|
|
4764
|
-
|
|
5971
|
+
attached = await sandbox.commands.connectProcess(runnerPid, {
|
|
4765
5972
|
...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
|
|
4766
5973
|
...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
|
|
4767
5974
|
});
|
|
@@ -4769,22 +5976,20 @@ async function reconnectStep(sandbox, resume, opts) {
|
|
|
4769
5976
|
if (e instanceof SandboxUnavailableError)
|
|
4770
5977
|
throw e;
|
|
4771
5978
|
}
|
|
4772
|
-
const attached = proc;
|
|
4773
5979
|
return {
|
|
4774
5980
|
runnerPid,
|
|
4775
5981
|
resultToken,
|
|
4776
5982
|
async wait() {
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
}
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
5983
|
+
try {
|
|
5984
|
+
const result = await pollDurableResult(sandbox, runnerPid, resultToken, {
|
|
5985
|
+
...signal ? { signal } : {},
|
|
5986
|
+
...pollDeadlineMs !== undefined ? { deadlineMs: pollDeadlineMs } : {}
|
|
5987
|
+
});
|
|
5988
|
+
splitters.flush();
|
|
5989
|
+
return result;
|
|
5990
|
+
} finally {
|
|
5991
|
+
await attached?.kill().catch(() => {});
|
|
4785
5992
|
}
|
|
4786
|
-
splitters.flush();
|
|
4787
|
-
return classifyRunnerOutcome(sandbox, output, resultToken);
|
|
4788
5993
|
}
|
|
4789
5994
|
};
|
|
4790
5995
|
}
|
|
@@ -4895,7 +6100,7 @@ async function serveStep(handler) {
|
|
|
4895
6100
|
process.exit(exitCode);
|
|
4896
6101
|
}
|
|
4897
6102
|
// src/agent/run-agent.ts
|
|
4898
|
-
import { z as
|
|
6103
|
+
import { z as z13 } from "zod";
|
|
4899
6104
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4900
6105
|
|
|
4901
6106
|
// src/agent/agent-context.ts
|
|
@@ -4949,29 +6154,36 @@ Names like \`note.created\` / \`brief.posted\` surface in the Workbench;
|
|
|
4949
6154
|
|
|
4950
6155
|
## Writing workflow / agent code — the SDK
|
|
4951
6156
|
|
|
4952
|
-
\`@agent-compose/sdk\` is installed in \`/workspace
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
6157
|
+
\`@agent-compose/sdk\` is installed in \`/workspace\`. **To author a workflow,
|
|
6158
|
+
ALWAYS run \`/ac:generate-workflow\`** (and \`/ac:generate-agent\` for an agent
|
|
6159
|
+
step) instead of writing source from memory — the skill scaffolds the correct,
|
|
6160
|
+
current shape. Then \`agentc register <file.ts>\` (or \`/ac:register\`).
|
|
4956
6161
|
|
|
4957
|
-
|
|
4958
|
-
|
|
6162
|
+
The skill writes **step-form** (a builder of discrete, durable \`.step()\`s).
|
|
6163
|
+
The legacy run-form (\`defineWorkflow({ run(ctx, sandbox) { … } })\`) has been
|
|
6164
|
+
REMOVED from the SDK — registering one fails with an error. Step-form is the
|
|
6165
|
+
only shape: durable per-step replay, and pause only works there.
|
|
4959
6166
|
|
|
4960
|
-
## Pausing to ask the human
|
|
6167
|
+
## Pausing to ask the human
|
|
4961
6168
|
|
|
4962
|
-
|
|
4963
|
-
|
|
6169
|
+
To ask a human and get an answer back, use the **\`AskUserQuestion\`** tool if
|
|
6170
|
+
you have it; otherwise run **\`agentc pause\`**:
|
|
4964
6171
|
|
|
4965
6172
|
agentc pause --reason "Notion returned 401 — connect Notion to continue" \\
|
|
4966
6173
|
--option retry --option skip
|
|
4967
6174
|
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
6175
|
+
**Both BLOCK and hand you the answer inline.** While you wait, the run is
|
|
6176
|
+
suspended — your sandbox is frozen and compute stops, so a pause is free while
|
|
6177
|
+
the human decides. When they answer, the call RETURNS with their decision: the
|
|
6178
|
+
\`AskUserQuestion\` tool result, or \`agentc pause\`'s output
|
|
6179
|
+
(\`▶ Resumed. The human answered: …\`), carries it.
|
|
6180
|
+
|
|
6181
|
+
**Then USE that answer to finish your work — do NOT end your turn.** This is NOT
|
|
6182
|
+
fire-and-forget, and the answer does NOT arrive in a later message: it comes
|
|
6183
|
+
back right where you called it, on the SAME turn. The shape is: ask → the call
|
|
6184
|
+
blocks → it returns the human's answer → you act on it and produce your result.
|
|
6185
|
+
Never end your turn before the call returns, never guess an answer, and never
|
|
6186
|
+
proceed without one.
|
|
4975
6187
|
|
|
4976
6188
|
Reach for it the moment you hit — or foresee — any of these:
|
|
4977
6189
|
- **A wall only a human can clear:** a 401/403, a missing credential, an
|
|
@@ -4996,13 +6208,174 @@ request **without** an Authorization header and the platform adds it. Don't try
|
|
|
4996
6208
|
to read or exfiltrate tokens; they aren't here. The "Connectors & access"
|
|
4997
6209
|
section below (when present) lists exactly which providers this run can reach.
|
|
4998
6210
|
|
|
6211
|
+
## Computer Use — you have a real desktop, and it is already running
|
|
6212
|
+
|
|
6213
|
+
**This machine has a graphical desktop.** Every session machine does — terminal
|
|
6214
|
+
sessions included — and the platform brings it UP AT BOOT, before your first
|
|
6215
|
+
turn: an X server on \`DISPLAY=:0\`, the openbox window manager, wallpaper and a
|
|
6216
|
+
panel. You do not start it, you do not wait for a human to open it, and you do
|
|
6217
|
+
not need a viewer. Go straight to driving it.
|
|
6218
|
+
|
|
6219
|
+
(The one exception, and it is rare: an image built without the GUI stack has no
|
|
6220
|
+
display at all, and \`DISPLAY=:0 xdotool getdisplaygeometry\` errors outright.
|
|
6221
|
+
That single case is the only one where this section does not apply — a
|
|
6222
|
+
screenshot showing only wallpaper is NOT it, and neither is an app that failed
|
|
6223
|
+
to start.)
|
|
6224
|
+
|
|
6225
|
+
**This is how you SEE anything.** Any question of the form "does it render?",
|
|
6226
|
+
"is the page actually working?", "did the markers show up?", "what does it look
|
|
6227
|
+
like?" is answered by opening it on this desktop and screenshotting it — not by
|
|
6228
|
+
reasoning about the code, and not by a headless render (which proves the process
|
|
6229
|
+
starts, not that the thing draws). Verify visually before you report visually.
|
|
6230
|
+
|
|
6231
|
+
- **Input** — \`xdotool\` against \`DISPLAY=:0\`: \`DISPLAY=:0 xdotool mousemove <x> <y>\`,
|
|
6232
|
+
\`DISPLAY=:0 xdotool click 1\` (1=left, 3=right), \`DISPLAY=:0 xdotool type 'text'\`,
|
|
6233
|
+
\`DISPLAY=:0 xdotool key Return\` (also \`ctrl+c\`, \`Tab\`, \`super\`, …).
|
|
6234
|
+
- **Screenshots** — \`scrot\` (or ImageMagick's \`import\`):
|
|
6235
|
+
\`DISPLAY=:0 scrot /tmp/screen.png\`, then READ the PNG to see the screen,
|
|
6236
|
+
before and after you act. A screenshot is your only eyes here.
|
|
6237
|
+
- **Apps + windows** — a plain X session. Launch in the background:
|
|
6238
|
+
\`DISPLAY=:0 <app> &\`. Two things that trip agents up, both normal:
|
|
6239
|
+
- a GUI app needs a **beat to map its window** — screenshot, and if you see
|
|
6240
|
+
only wallpaper, wait a couple of seconds and screenshot again before
|
|
6241
|
+
concluding anything;
|
|
6242
|
+
- **Chromium needs \`--no-sandbox\`** in this environment (nested sandbox).
|
|
6243
|
+
The whole recipe for looking at a local page:
|
|
6244
|
+
\`DISPLAY=:0 chromium --no-sandbox --disable-gpu --start-maximized <url> &\`
|
|
6245
|
+
then \`sleep 5\`, then \`DISPLAY=:0 scrot /tmp/screen.png\` and read it.
|
|
6246
|
+
If a window still never appears, read the app's own log (\`/tmp/*.log\`) — the
|
|
6247
|
+
desktop is not the thing that failed. Do NOT abandon it for a headless
|
|
6248
|
+
screenshot: headless cannot tell you what the human will see.
|
|
6249
|
+
- **A human can watch** — the session header carries a **Desktop** button in the
|
|
6250
|
+
dashboard, and what a teammate sees there is exactly this display. The desktop
|
|
6251
|
+
runs whether or not anyone is looking; never wait for a viewer.
|
|
6252
|
+
|
|
6253
|
+
Nothing here changes the credentials rule above: tokens are injected at the
|
|
6254
|
+
network layer, never present on the desktop or in any file you can read — so
|
|
6255
|
+
there is nothing to type, paste, or screenshot a credential from.
|
|
6256
|
+
|
|
6257
|
+
## Recording a demo — the desktop, captured to a video the human can play
|
|
6258
|
+
|
|
6259
|
+
"Record a demo of you using X" is a normal ask, and this machine does it:
|
|
6260
|
+
start a screen recording, drive the app with \`xdotool\` exactly as in Computer
|
|
6261
|
+
Use, stop the recording, and report the file. (For a LIVE view no recording is
|
|
6262
|
+
needed — the session header's **Desktop** button already streams this display
|
|
6263
|
+
to any teammate watching; a recording is the durable, replayable artifact.
|
|
6264
|
+
Both modes exist; say so when it matters.)
|
|
6265
|
+
|
|
6266
|
+
**ffmpeg is NOT pre-installed** — install it first, once per machine:
|
|
6267
|
+
|
|
6268
|
+
sudo apt-get update -q && sudo apt-get install -y -q ffmpeg
|
|
6269
|
+
|
|
6270
|
+
(drop \`sudo\` if you are already root). Then the whole recipe:
|
|
6271
|
+
|
|
6272
|
+
DISPLAY=:0 ffmpeg -f x11grab \\
|
|
6273
|
+
-video_size "$(DISPLAY=:0 xdotool getdisplaygeometry | tr ' ' x)" \\
|
|
6274
|
+
-framerate 10 -i :0 -c:v libvpx -b:v 1M -deadline realtime -cpu-used 8 \\
|
|
6275
|
+
demo.webm &
|
|
6276
|
+
FFMPEG_PID=$!
|
|
6277
|
+
# ... drive the app with xdotool, screenshotting as you go ...
|
|
6278
|
+
kill -INT "$FFMPEG_PID" && wait "$FFMPEG_PID"
|
|
6279
|
+
|
|
6280
|
+
The gotchas, each one earned:
|
|
6281
|
+
- **Stop with SIGINT (\`kill -INT\`), never SIGKILL** — ffmpeg finalizes the
|
|
6282
|
+
file on SIGINT; a hard kill truncates the encode mid-write.
|
|
6283
|
+
- **Record WebM (matroska-family), not MP4** — mp4 writes its moov atom at the
|
|
6284
|
+
END, so a killed or crashed encode leaves an UNPLAYABLE file; webm stays
|
|
6285
|
+
playable up to the last written frame and plays natively in the browser.
|
|
6286
|
+
MP4's only edge is compatibility with some external players — transcode
|
|
6287
|
+
afterwards if you truly need it, never record straight to it.
|
|
6288
|
+
- **\`-video_size\` must match the real screen** — x11grab does not default to
|
|
6289
|
+
it; read the geometry from \`xdotool getdisplaygeometry\` as above.
|
|
6290
|
+
- **10–12 fps is right for a screen demo** — small files, legible UI motion;
|
|
6291
|
+
this is not video production.
|
|
6292
|
+
- **Write to the drive, not /tmp** — the recording must land in your working
|
|
6293
|
+
directory to persist and show up in Files; a file in /tmp dies with the
|
|
6294
|
+
sandbox.
|
|
6295
|
+
- When you stop, **TELL the human the exact drive path** of the video — a
|
|
6296
|
+
recording they cannot find might as well not exist.
|
|
6297
|
+
|
|
6298
|
+
## Previews — register every server you serve (cloud sessions)
|
|
6299
|
+
|
|
6300
|
+
In a cloud session, a dev server listening on a port becomes a hosted,
|
|
6301
|
+
member-gated URL the human can open — but ONLY if you register it:
|
|
6302
|
+
|
|
6303
|
+
agentc preview open <port> [--name <label>] [--path </landing>]
|
|
6304
|
+
# hosted URL + an "Open preview" card
|
|
6305
|
+
agentc preview list # the registry — what is live right now
|
|
6306
|
+
agentc preview close <port> # take one down
|
|
6307
|
+
|
|
6308
|
+
(\`agentc preview announce\` is the same verb as \`open\` — announce what you
|
|
6309
|
+
serve.) \`--name\` is the human-readable label; \`--path\` is where the app
|
|
6310
|
+
should open (e.g. \`/dashboard\`) — the card and every chip land the human
|
|
6311
|
+
there instead of a bare \`/\`.
|
|
6312
|
+
|
|
6313
|
+
Register EVERY server you start for a human, the moment it is listening, and
|
|
6314
|
+
tell them the URL the command printed. The registry is the only discoverable
|
|
6315
|
+
record of what this machine serves: an unregistered server keeps running, but
|
|
6316
|
+
nobody — not the human, not the assistant — can find its URL, and when the
|
|
6317
|
+
sandbox recycles it is gone without a trace. Never guess or hand out a raw
|
|
6318
|
+
port; the hosted URL from \`agentc preview open\` is the only address that
|
|
6319
|
+
works outside this machine. (Outside a cloud session the command errors
|
|
6320
|
+
honestly — there is no session sandbox to expose.)
|
|
6321
|
+
|
|
6322
|
+
What registration buys you: the human sees each registered preview as a card
|
|
6323
|
+
in the conversation and a row in the session's Previews menu — MANY at once,
|
|
6324
|
+
one per port — and the assistant resolves "open the preview" from this same
|
|
6325
|
+
registry (its \`list_previews\` read), so what you register is exactly what
|
|
6326
|
+
gets opened. On deployments with subdomain previews the hosted URL is a real
|
|
6327
|
+
origin of its own — absolute asset paths and client-side routing work, the
|
|
6328
|
+
whole app is navigable — so serve normally and let the platform address it;
|
|
6329
|
+
never rewrite your app to a path prefix.
|
|
6330
|
+
|
|
4999
6331
|
## Tools in this environment
|
|
5000
6332
|
|
|
5001
6333
|
- \`agentc\` — Agent Compose CLI (your primary interface; authed from env)
|
|
5002
6334
|
- \`@agent-compose/sdk\` — installed in /workspace for writing workflows
|
|
5003
6335
|
- \`/ac:*\` Claude Code skills — slash commands for the above
|
|
5004
6336
|
- \`archil\` (factory drive), \`rtk\`, \`bun\`
|
|
6337
|
+
- \`xdotool\` / \`scrot\` — drive + screenshot the desktop (if this machine has one; see Computer Use)
|
|
5005
6338
|
- A world-writable \`/workspace\` working directory`;
|
|
6339
|
+
function buildAddedSessionBrief(p) {
|
|
6340
|
+
const dashboardSection = p.dashboardUrl === null ? "" : `
|
|
6341
|
+
|
|
6342
|
+
## Dashboard
|
|
6343
|
+
|
|
6344
|
+
Teammates follow this conversation (and the rest of the factory) in the
|
|
6345
|
+
Agent Compose dashboard: ${p.dashboardUrl}`;
|
|
6346
|
+
return `# Connected to Agent Compose
|
|
6347
|
+
|
|
6348
|
+
Agent Compose is your team's agent platform: durable conversations,
|
|
6349
|
+
workflow runs, and shared factory drives where humans and agents work
|
|
6350
|
+
together. THIS terminal's claude-code session is connected to Agent
|
|
6351
|
+
Compose conversation \`${p.conversationId}\` on ${p.serverUrl}.
|
|
6352
|
+
|
|
6353
|
+
That conversation is a LIVE, READ-ONLY MIRROR of this terminal session:
|
|
6354
|
+
teammates read along in the dashboard as the work happens, but they cannot
|
|
6355
|
+
message you through it — anything posted there is answered by the server
|
|
6356
|
+
with a notice and never reaches this terminal. Everything you do here is
|
|
6357
|
+
mirrored automatically; you have an audience, not a channel.
|
|
6358
|
+
|
|
6359
|
+
## The \`agentc\` toolbelt
|
|
6360
|
+
|
|
6361
|
+
The \`agentc\` CLI works from this shell. It is already authenticated on
|
|
6362
|
+
this machine via the bridge credential fallback — no keys to manage,
|
|
6363
|
+
commands just work:
|
|
6364
|
+
|
|
6365
|
+
agentc list # registered workflows
|
|
6366
|
+
agentc logs <run-id> # a run's logs
|
|
6367
|
+
agentc invoke <workflow> -i '<json>' # dispatch a workflow
|
|
6368
|
+
agentc events list # read the factory timeline
|
|
6369
|
+
|
|
6370
|
+
## Scope — this is your LOCAL machine
|
|
6371
|
+
|
|
6372
|
+
The files here are YOURS: no factory drive is mounted in this session,
|
|
6373
|
+
and nothing you write locally lands on a shared drive by itself. Cloud
|
|
6374
|
+
drive/branch semantics (per-run directories on the factory drive, drive
|
|
6375
|
+
branches, persist-by-default outputs) apply only to cloud sessions —
|
|
6376
|
+
not here.${dashboardSection}
|
|
6377
|
+
`;
|
|
6378
|
+
}
|
|
5006
6379
|
function renderConnectorsSection(connectors) {
|
|
5007
6380
|
if (connectors.length === 0)
|
|
5008
6381
|
return "";
|
|
@@ -5108,29 +6481,29 @@ function stripFrontmatter(content) {
|
|
|
5108
6481
|
return end === -1 ? content : content.slice(end + 4).trimStart();
|
|
5109
6482
|
}
|
|
5110
6483
|
function zodTypeName(schema) {
|
|
5111
|
-
if (schema instanceof
|
|
6484
|
+
if (schema instanceof z13.ZodString)
|
|
5112
6485
|
return "string";
|
|
5113
|
-
if (schema instanceof
|
|
6486
|
+
if (schema instanceof z13.ZodNumber)
|
|
5114
6487
|
return "number";
|
|
5115
|
-
if (schema instanceof
|
|
6488
|
+
if (schema instanceof z13.ZodBoolean)
|
|
5116
6489
|
return "boolean";
|
|
5117
|
-
if (schema instanceof
|
|
6490
|
+
if (schema instanceof z13.ZodArray)
|
|
5118
6491
|
return `${zodTypeName(schema.element)}[]`;
|
|
5119
|
-
if (schema instanceof
|
|
6492
|
+
if (schema instanceof z13.ZodEnum)
|
|
5120
6493
|
return schema.options.map((o) => `"${o}"`).join(" | ");
|
|
5121
|
-
if (schema instanceof
|
|
6494
|
+
if (schema instanceof z13.ZodOptional)
|
|
5122
6495
|
return `${zodTypeName(schema.unwrap())} (optional)`;
|
|
5123
|
-
if (schema instanceof
|
|
6496
|
+
if (schema instanceof z13.ZodNullable)
|
|
5124
6497
|
return `${zodTypeName(schema.unwrap())} | null`;
|
|
5125
|
-
if (schema instanceof
|
|
6498
|
+
if (schema instanceof z13.ZodLiteral)
|
|
5126
6499
|
return `"${String(schema.value)}"`;
|
|
5127
|
-
if (schema instanceof
|
|
6500
|
+
if (schema instanceof z13.ZodObject)
|
|
5128
6501
|
return "object";
|
|
5129
6502
|
return "any";
|
|
5130
6503
|
}
|
|
5131
6504
|
function buildResponseFormatAppendix(schema) {
|
|
5132
6505
|
const header = ["## Response Format", "", "When you emit `exit_signal: true` in your `<status>` block, also include a `<response>` block containing JSON immediately after `<status>`:", ""];
|
|
5133
|
-
const shape = schema instanceof
|
|
6506
|
+
const shape = schema instanceof z13.ZodObject ? schema.shape : null;
|
|
5134
6507
|
const body = shape ? (() => {
|
|
5135
6508
|
const entries = Object.entries(shape);
|
|
5136
6509
|
const example = Object.fromEntries(entries.map(([k, v]) => [k, `<${zodTypeName(v)}>`]));
|
|
@@ -5220,6 +6593,7 @@ async function agent(opts) {
|
|
|
5220
6593
|
});
|
|
5221
6594
|
} : undefined;
|
|
5222
6595
|
const mode = opts.mode ?? "auto";
|
|
6596
|
+
const processors = [createAskHumanProcessor(), ...opts.processors ?? []];
|
|
5223
6597
|
try {
|
|
5224
6598
|
return await agentLoop({
|
|
5225
6599
|
runtime: (runtimeOpts) => opts.runtime.create(opts.sandbox, { ...runtimeOpts, agentManual: buildAgentContextDoc(process.env) }),
|
|
@@ -5248,7 +6622,7 @@ async function agent(opts) {
|
|
|
5248
6622
|
} } : {},
|
|
5249
6623
|
...opts.onAgentEvent ? { onAgentEvent: opts.onAgentEvent } : {},
|
|
5250
6624
|
...opts.onIteration ? { onIteration: opts.onIteration } : {},
|
|
5251
|
-
...
|
|
6625
|
+
...processors.length ? { processors } : {},
|
|
5252
6626
|
requestContext: opts.requestContext ?? RequestContext.fromReserved({
|
|
5253
6627
|
teamId: "",
|
|
5254
6628
|
runId: "",
|
|
@@ -5267,6 +6641,692 @@ async function agent(opts) {
|
|
|
5267
6641
|
inboxAbort.abort();
|
|
5268
6642
|
}
|
|
5269
6643
|
}
|
|
6644
|
+
// src/display.ts
|
|
6645
|
+
var DISPLAY_MARKER_KEY = "acDisplay";
|
|
6646
|
+
var DISPLAY_MARKER_VERSION = "v1";
|
|
6647
|
+
var PLAN_MAX_ENTRIES = 64;
|
|
6648
|
+
var PLAN_ENTRY_MAX_CHARS = 1000;
|
|
6649
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6650
|
+
var PLAN_STATUSES = new Set(["pending", "in_progress", "completed"]);
|
|
6651
|
+
function clampPlanEntries(raw) {
|
|
6652
|
+
if (!Array.isArray(raw))
|
|
6653
|
+
return [];
|
|
6654
|
+
const entries = [];
|
|
6655
|
+
for (const e of raw) {
|
|
6656
|
+
if (typeof e !== "object" || e === null)
|
|
6657
|
+
continue;
|
|
6658
|
+
const { content, status } = e;
|
|
6659
|
+
if (typeof content !== "string" || content.trim().length === 0)
|
|
6660
|
+
continue;
|
|
6661
|
+
if (typeof status !== "string" || !PLAN_STATUSES.has(status))
|
|
6662
|
+
continue;
|
|
6663
|
+
entries.push({ content: content.slice(0, PLAN_ENTRY_MAX_CHARS), status });
|
|
6664
|
+
if (entries.length >= PLAN_MAX_ENTRIES)
|
|
6665
|
+
break;
|
|
6666
|
+
}
|
|
6667
|
+
return entries;
|
|
6668
|
+
}
|
|
6669
|
+
var TABLE_MAX_COLUMNS = 12;
|
|
6670
|
+
var TABLE_MAX_ROWS = 100;
|
|
6671
|
+
var TABLE_CELL_MAX_CHARS = 200;
|
|
6672
|
+
var IMAGE_PATH_MAX_CHARS = 512;
|
|
6673
|
+
function clampCellString(s) {
|
|
6674
|
+
return s.length > TABLE_CELL_MAX_CHARS ? `${s.slice(0, TABLE_CELL_MAX_CHARS - 1)}…` : s;
|
|
6675
|
+
}
|
|
6676
|
+
function clampCell(value) {
|
|
6677
|
+
if (typeof value === "string")
|
|
6678
|
+
return clampCellString(value);
|
|
6679
|
+
if (typeof value === "number")
|
|
6680
|
+
return Number.isFinite(value) ? value : null;
|
|
6681
|
+
if (typeof value === "boolean")
|
|
6682
|
+
return value;
|
|
6683
|
+
return null;
|
|
6684
|
+
}
|
|
6685
|
+
function clampTableData(raw) {
|
|
6686
|
+
let rawColumns;
|
|
6687
|
+
let rawRows;
|
|
6688
|
+
if (Array.isArray(raw)) {
|
|
6689
|
+
rawRows = raw;
|
|
6690
|
+
} else if (typeof raw === "object" && raw !== null) {
|
|
6691
|
+
rawColumns = raw.columns;
|
|
6692
|
+
rawRows = raw.rows;
|
|
6693
|
+
} else {
|
|
6694
|
+
return null;
|
|
6695
|
+
}
|
|
6696
|
+
if (!Array.isArray(rawRows))
|
|
6697
|
+
return null;
|
|
6698
|
+
let truncated = false;
|
|
6699
|
+
const columns = [];
|
|
6700
|
+
const seenKeys = new Set;
|
|
6701
|
+
const pushColumn = (key, label) => {
|
|
6702
|
+
if (seenKeys.has(key))
|
|
6703
|
+
return;
|
|
6704
|
+
seenKeys.add(key);
|
|
6705
|
+
if (columns.length >= TABLE_MAX_COLUMNS) {
|
|
6706
|
+
truncated = true;
|
|
6707
|
+
return;
|
|
6708
|
+
}
|
|
6709
|
+
columns.push({ sourceKey: key, key: clampCellString(key), label: clampCellString(label) });
|
|
6710
|
+
};
|
|
6711
|
+
if (rawColumns !== undefined) {
|
|
6712
|
+
if (!Array.isArray(rawColumns))
|
|
6713
|
+
return null;
|
|
6714
|
+
for (const c of rawColumns) {
|
|
6715
|
+
if (typeof c !== "object" || c === null)
|
|
6716
|
+
continue;
|
|
6717
|
+
const { key, label } = c;
|
|
6718
|
+
if (typeof key !== "string" || key.length === 0)
|
|
6719
|
+
continue;
|
|
6720
|
+
pushColumn(key, typeof label === "string" && label.length > 0 ? label : key);
|
|
6721
|
+
}
|
|
6722
|
+
} else {
|
|
6723
|
+
for (const row of rawRows) {
|
|
6724
|
+
if (typeof row !== "object" || row === null || Array.isArray(row))
|
|
6725
|
+
continue;
|
|
6726
|
+
for (const key of Object.keys(row))
|
|
6727
|
+
pushColumn(key, key);
|
|
6728
|
+
}
|
|
6729
|
+
}
|
|
6730
|
+
if (columns.length === 0)
|
|
6731
|
+
return null;
|
|
6732
|
+
const rows = [];
|
|
6733
|
+
for (const rawRow of rawRows) {
|
|
6734
|
+
if (typeof rawRow !== "object" || rawRow === null || Array.isArray(rawRow))
|
|
6735
|
+
continue;
|
|
6736
|
+
if (rows.length >= TABLE_MAX_ROWS) {
|
|
6737
|
+
truncated = true;
|
|
6738
|
+
break;
|
|
6739
|
+
}
|
|
6740
|
+
const row = {};
|
|
6741
|
+
for (const c of columns)
|
|
6742
|
+
row[c.key] = clampCell(rawRow[c.sourceKey]);
|
|
6743
|
+
rows.push(row);
|
|
6744
|
+
}
|
|
6745
|
+
if (rows.length === 0)
|
|
6746
|
+
return null;
|
|
6747
|
+
return { columns: columns.map(({ key, label }) => ({ key, label })), rows, truncated };
|
|
6748
|
+
}
|
|
6749
|
+
var CHART_MAX_SERIES = 4;
|
|
6750
|
+
var CHART_MAX_POINTS_PER_SERIES = 60;
|
|
6751
|
+
var CHART_LABEL_MAX_CHARS = 40;
|
|
6752
|
+
function clampChartSeries(raw) {
|
|
6753
|
+
if (!Array.isArray(raw))
|
|
6754
|
+
return [];
|
|
6755
|
+
const series = [];
|
|
6756
|
+
for (const s of raw) {
|
|
6757
|
+
if (series.length >= CHART_MAX_SERIES)
|
|
6758
|
+
break;
|
|
6759
|
+
if (typeof s !== "object" || s === null)
|
|
6760
|
+
continue;
|
|
6761
|
+
const { label, points } = s;
|
|
6762
|
+
if (typeof label !== "string" || label.trim().length === 0)
|
|
6763
|
+
continue;
|
|
6764
|
+
if (!Array.isArray(points))
|
|
6765
|
+
continue;
|
|
6766
|
+
const clamped = [];
|
|
6767
|
+
for (const p of points) {
|
|
6768
|
+
if (clamped.length >= CHART_MAX_POINTS_PER_SERIES)
|
|
6769
|
+
break;
|
|
6770
|
+
if (typeof p !== "object" || p === null)
|
|
6771
|
+
continue;
|
|
6772
|
+
const { x, y } = p;
|
|
6773
|
+
if (typeof y !== "number" || !Number.isFinite(y))
|
|
6774
|
+
continue;
|
|
6775
|
+
if (typeof x === "number" && Number.isFinite(x))
|
|
6776
|
+
clamped.push({ x, y });
|
|
6777
|
+
else if (typeof x === "string" && x.length > 0)
|
|
6778
|
+
clamped.push({ x: x.slice(0, CHART_LABEL_MAX_CHARS), y });
|
|
6779
|
+
}
|
|
6780
|
+
if (clamped.length === 0)
|
|
6781
|
+
continue;
|
|
6782
|
+
series.push({ label: label.slice(0, CHART_LABEL_MAX_CHARS), points: clamped });
|
|
6783
|
+
}
|
|
6784
|
+
return series;
|
|
6785
|
+
}
|
|
6786
|
+
function clampChartAxisLabel(raw) {
|
|
6787
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.slice(0, CHART_LABEL_MAX_CHARS) : undefined;
|
|
6788
|
+
}
|
|
6789
|
+
var ASK_PROMPT_MAX_CHARS = 1000;
|
|
6790
|
+
var ASK_MAX_OPTIONS = 8;
|
|
6791
|
+
var ASK_OPTION_ID_MAX_CHARS = 100;
|
|
6792
|
+
var ASK_OPTION_LABEL_MAX_CHARS = 200;
|
|
6793
|
+
function clampAskOptions(raw) {
|
|
6794
|
+
if (!Array.isArray(raw))
|
|
6795
|
+
return [];
|
|
6796
|
+
const options = [];
|
|
6797
|
+
const seen = new Set;
|
|
6798
|
+
for (const o of raw) {
|
|
6799
|
+
if (options.length >= ASK_MAX_OPTIONS)
|
|
6800
|
+
break;
|
|
6801
|
+
let id = null;
|
|
6802
|
+
let label = null;
|
|
6803
|
+
if (typeof o === "string" && o.trim().length > 0) {
|
|
6804
|
+
id = o;
|
|
6805
|
+
label = o;
|
|
6806
|
+
} else if (typeof o === "object" && o !== null) {
|
|
6807
|
+
const cand = o;
|
|
6808
|
+
if (typeof cand.id === "string" && cand.id.length > 0 && typeof cand.label === "string" && cand.label.length > 0) {
|
|
6809
|
+
id = cand.id;
|
|
6810
|
+
label = cand.label;
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6813
|
+
if (id === null || label === null)
|
|
6814
|
+
continue;
|
|
6815
|
+
const clampedId = id.slice(0, ASK_OPTION_ID_MAX_CHARS);
|
|
6816
|
+
if (seen.has(clampedId))
|
|
6817
|
+
continue;
|
|
6818
|
+
seen.add(clampedId);
|
|
6819
|
+
options.push({ id: clampedId, label: label.slice(0, ASK_OPTION_LABEL_MAX_CHARS) });
|
|
6820
|
+
}
|
|
6821
|
+
return options;
|
|
6822
|
+
}
|
|
6823
|
+
function serializeDisplayMarker(marker) {
|
|
6824
|
+
return JSON.stringify({ [DISPLAY_MARKER_KEY]: DISPLAY_MARKER_VERSION, ...marker });
|
|
6825
|
+
}
|
|
6826
|
+
function parseDisplayMarker(line) {
|
|
6827
|
+
const trimmed = line.trim();
|
|
6828
|
+
if (!trimmed.startsWith("{") || !trimmed.includes(DISPLAY_MARKER_KEY))
|
|
6829
|
+
return null;
|
|
6830
|
+
let obj;
|
|
6831
|
+
try {
|
|
6832
|
+
const parsed = JSON.parse(trimmed);
|
|
6833
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
6834
|
+
return null;
|
|
6835
|
+
obj = parsed;
|
|
6836
|
+
} catch {
|
|
6837
|
+
return null;
|
|
6838
|
+
}
|
|
6839
|
+
if (obj[DISPLAY_MARKER_KEY] !== DISPLAY_MARKER_VERSION)
|
|
6840
|
+
return null;
|
|
6841
|
+
const str = (key) => {
|
|
6842
|
+
const v = obj[key];
|
|
6843
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
6844
|
+
};
|
|
6845
|
+
switch (obj.kind) {
|
|
6846
|
+
case "run": {
|
|
6847
|
+
const runId = str("runId");
|
|
6848
|
+
return runId && UUID_RE.test(runId) ? { kind: "run", runId } : null;
|
|
6849
|
+
}
|
|
6850
|
+
case "changes": {
|
|
6851
|
+
const runId = str("runId");
|
|
6852
|
+
if (!runId || !UUID_RE.test(runId))
|
|
6853
|
+
return null;
|
|
6854
|
+
return {
|
|
6855
|
+
kind: "changes",
|
|
6856
|
+
runId,
|
|
6857
|
+
...str("factorySlug") ? { factorySlug: str("factorySlug") } : {},
|
|
6858
|
+
endedAt: str("endedAt") ?? null
|
|
6859
|
+
};
|
|
6860
|
+
}
|
|
6861
|
+
case "document": {
|
|
6862
|
+
const path = str("path");
|
|
6863
|
+
if (!path)
|
|
6864
|
+
return null;
|
|
6865
|
+
return {
|
|
6866
|
+
kind: "document",
|
|
6867
|
+
path,
|
|
6868
|
+
...str("factorySlug") ? { factorySlug: str("factorySlug") } : {},
|
|
6869
|
+
preview: str("preview") ?? null
|
|
6870
|
+
};
|
|
6871
|
+
}
|
|
6872
|
+
case "plan": {
|
|
6873
|
+
const entries = clampPlanEntries(obj.entries);
|
|
6874
|
+
return entries.length > 0 ? { kind: "plan", entries } : null;
|
|
6875
|
+
}
|
|
6876
|
+
case "image": {
|
|
6877
|
+
const path = str("path");
|
|
6878
|
+
if (!path || path.length > IMAGE_PATH_MAX_CHARS)
|
|
6879
|
+
return null;
|
|
6880
|
+
return {
|
|
6881
|
+
kind: "image",
|
|
6882
|
+
path,
|
|
6883
|
+
...str("factorySlug") ? { factorySlug: str("factorySlug") } : {}
|
|
6884
|
+
};
|
|
6885
|
+
}
|
|
6886
|
+
case "table": {
|
|
6887
|
+
const data = clampTableData({ columns: obj.columns, rows: obj.rows });
|
|
6888
|
+
if (!data)
|
|
6889
|
+
return null;
|
|
6890
|
+
return { kind: "table", ...data, truncated: data.truncated || obj.truncated === true };
|
|
6891
|
+
}
|
|
6892
|
+
case "chart": {
|
|
6893
|
+
if (obj.chartKind !== "bar" && obj.chartKind !== "line")
|
|
6894
|
+
return null;
|
|
6895
|
+
const series = clampChartSeries(obj.series);
|
|
6896
|
+
if (series.length === 0)
|
|
6897
|
+
return null;
|
|
6898
|
+
const xLabel = clampChartAxisLabel(obj.xLabel);
|
|
6899
|
+
const yLabel = clampChartAxisLabel(obj.yLabel);
|
|
6900
|
+
return {
|
|
6901
|
+
kind: "chart",
|
|
6902
|
+
chartKind: obj.chartKind,
|
|
6903
|
+
series,
|
|
6904
|
+
...xLabel !== undefined ? { xLabel } : {},
|
|
6905
|
+
...yLabel !== undefined ? { yLabel } : {}
|
|
6906
|
+
};
|
|
6907
|
+
}
|
|
6908
|
+
case "ask": {
|
|
6909
|
+
const askId = str("askId");
|
|
6910
|
+
const rawPrompt = str("prompt");
|
|
6911
|
+
if (!askId || !UUID_RE.test(askId) || !rawPrompt)
|
|
6912
|
+
return null;
|
|
6913
|
+
const options = clampAskOptions(obj.options);
|
|
6914
|
+
return {
|
|
6915
|
+
kind: "ask",
|
|
6916
|
+
askId,
|
|
6917
|
+
prompt: rawPrompt.slice(0, ASK_PROMPT_MAX_CHARS),
|
|
6918
|
+
...options.length > 0 ? { options } : {},
|
|
6919
|
+
freeText: true
|
|
6920
|
+
};
|
|
6921
|
+
}
|
|
6922
|
+
default:
|
|
6923
|
+
return null;
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6926
|
+
function findDisplayMarker(text) {
|
|
6927
|
+
for (const line of text.split(`
|
|
6928
|
+
`)) {
|
|
6929
|
+
const marker = parseDisplayMarker(line);
|
|
6930
|
+
if (marker)
|
|
6931
|
+
return marker;
|
|
6932
|
+
}
|
|
6933
|
+
return null;
|
|
6934
|
+
}
|
|
6935
|
+
var SHELL_STOPPERS = new Set(["|", "||", "&&", ";", ">", ">>", "<", "2>", "2>>", "&"]);
|
|
6936
|
+
function shellWords(command) {
|
|
6937
|
+
const words = [];
|
|
6938
|
+
let current = "";
|
|
6939
|
+
let started = false;
|
|
6940
|
+
let quote2 = null;
|
|
6941
|
+
for (let i = 0;i < command.length; i += 1) {
|
|
6942
|
+
const ch = command[i];
|
|
6943
|
+
if (quote2) {
|
|
6944
|
+
if (ch === quote2)
|
|
6945
|
+
quote2 = null;
|
|
6946
|
+
else
|
|
6947
|
+
current += ch;
|
|
6948
|
+
continue;
|
|
6949
|
+
}
|
|
6950
|
+
if (ch === "'" || ch === '"') {
|
|
6951
|
+
quote2 = ch;
|
|
6952
|
+
started = true;
|
|
6953
|
+
continue;
|
|
6954
|
+
}
|
|
6955
|
+
if (ch === " " || ch === "\t" || ch === `
|
|
6956
|
+
`) {
|
|
6957
|
+
if (started) {
|
|
6958
|
+
words.push(current);
|
|
6959
|
+
current = "";
|
|
6960
|
+
started = false;
|
|
6961
|
+
}
|
|
6962
|
+
continue;
|
|
6963
|
+
}
|
|
6964
|
+
current += ch;
|
|
6965
|
+
started = true;
|
|
6966
|
+
}
|
|
6967
|
+
if (started)
|
|
6968
|
+
words.push(current);
|
|
6969
|
+
return words;
|
|
6970
|
+
}
|
|
6971
|
+
var VALUE_FLAGS = new Set([
|
|
6972
|
+
"--factory",
|
|
6973
|
+
"--input",
|
|
6974
|
+
"--entries",
|
|
6975
|
+
"--url",
|
|
6976
|
+
"--api-key",
|
|
6977
|
+
"--limit",
|
|
6978
|
+
"--thread",
|
|
6979
|
+
"--content-type",
|
|
6980
|
+
"--file",
|
|
6981
|
+
"--out",
|
|
6982
|
+
"--revision",
|
|
6983
|
+
"--ended-at",
|
|
6984
|
+
"--data",
|
|
6985
|
+
"--kind",
|
|
6986
|
+
"--from",
|
|
6987
|
+
"--to",
|
|
6988
|
+
"--prompt",
|
|
6989
|
+
"--options",
|
|
6990
|
+
"--approver"
|
|
6991
|
+
]);
|
|
6992
|
+
function isShellControlWord(w) {
|
|
6993
|
+
if (/^\d*>&\d+$/.test(w))
|
|
6994
|
+
return false;
|
|
6995
|
+
return SHELL_STOPPERS.has(w) || /^\d*>>?(&\d+)?$/.test(w) || w === "<" || w.startsWith("$(") || w.startsWith("`");
|
|
6996
|
+
}
|
|
6997
|
+
function detectAgentcInvocation(command) {
|
|
6998
|
+
const words = shellWords(command);
|
|
6999
|
+
const start = words.findIndex((w) => w === "agentc" || w.endsWith("/agentc"));
|
|
7000
|
+
if (start < 0)
|
|
7001
|
+
return null;
|
|
7002
|
+
if (words.some(isShellControlWord))
|
|
7003
|
+
return null;
|
|
7004
|
+
for (let i = 0;i < start; i += 1) {
|
|
7005
|
+
const w = words[i];
|
|
7006
|
+
const isEnvAssign = /^[A-Za-z_][A-Za-z0-9_]*=/.test(w);
|
|
7007
|
+
const isShellInvoker = /^(?:[\w./-]*\/)?(?:sh|bash|zsh|dash)$/.test(w);
|
|
7008
|
+
if (!isEnvAssign && !isShellInvoker && !w.startsWith("-"))
|
|
7009
|
+
return null;
|
|
7010
|
+
}
|
|
7011
|
+
const args = [];
|
|
7012
|
+
const flags = new Map;
|
|
7013
|
+
for (let i = start + 1;i < words.length; i += 1) {
|
|
7014
|
+
const w = words[i];
|
|
7015
|
+
if (SHELL_STOPPERS.has(w))
|
|
7016
|
+
break;
|
|
7017
|
+
if (w.startsWith("--")) {
|
|
7018
|
+
if (VALUE_FLAGS.has(w) && i + 1 < words.length && !SHELL_STOPPERS.has(words[i + 1])) {
|
|
7019
|
+
flags.set(w, words[i + 1]);
|
|
7020
|
+
i += 1;
|
|
7021
|
+
}
|
|
7022
|
+
continue;
|
|
7023
|
+
}
|
|
7024
|
+
args.push(w);
|
|
7025
|
+
}
|
|
7026
|
+
const factorySlug = flags.get("--factory");
|
|
7027
|
+
const withSlug = factorySlug ? { factorySlug } : {};
|
|
7028
|
+
const uuid = (v) => v && UUID_RE.test(v) ? v : null;
|
|
7029
|
+
if (args[0] === "display") {
|
|
7030
|
+
switch (args[1]) {
|
|
7031
|
+
case "run": {
|
|
7032
|
+
const runId = uuid(args[2]);
|
|
7033
|
+
return runId ? { verb: "display-run", runId } : null;
|
|
7034
|
+
}
|
|
7035
|
+
case "changes": {
|
|
7036
|
+
const runId = uuid(args[2]);
|
|
7037
|
+
return runId ? { verb: "display-changes", runId, ...withSlug } : null;
|
|
7038
|
+
}
|
|
7039
|
+
case "document":
|
|
7040
|
+
return args[2] ? { verb: "display-document", path: args[2], ...withSlug } : null;
|
|
7041
|
+
case "plan":
|
|
7042
|
+
return { verb: "display-plan" };
|
|
7043
|
+
case "image":
|
|
7044
|
+
return args[2] ? { verb: "display-image", path: args[2], ...withSlug } : null;
|
|
7045
|
+
case "preview":
|
|
7046
|
+
return args[2] ? { verb: "display-preview", path: args[2] } : null;
|
|
7047
|
+
case "table": {
|
|
7048
|
+
const file = flags.get("--file");
|
|
7049
|
+
const hasData = flags.has("--data");
|
|
7050
|
+
if (file && !hasData)
|
|
7051
|
+
return { verb: "display-table", source: "file", path: file };
|
|
7052
|
+
if (hasData && !file)
|
|
7053
|
+
return { verb: "display-table", source: "inline" };
|
|
7054
|
+
return null;
|
|
7055
|
+
}
|
|
7056
|
+
case "chart": {
|
|
7057
|
+
const chartKind = flags.get("--kind");
|
|
7058
|
+
return chartKind === "bar" || chartKind === "line" ? { verb: "display-chart", chartKind } : null;
|
|
7059
|
+
}
|
|
7060
|
+
case "diff": {
|
|
7061
|
+
const from = flags.get("--from");
|
|
7062
|
+
const to = flags.get("--to");
|
|
7063
|
+
if (!args[2] || !from || !to)
|
|
7064
|
+
return null;
|
|
7065
|
+
if (!REVISION_SELECTOR_RE.test(from) || !REVISION_SELECTOR_RE.test(to))
|
|
7066
|
+
return null;
|
|
7067
|
+
return { verb: "display-diff", path: args[2], from, to };
|
|
7068
|
+
}
|
|
7069
|
+
case "ask": {
|
|
7070
|
+
const prompt = flags.get("--prompt");
|
|
7071
|
+
if (!prompt || prompt.trim().length === 0)
|
|
7072
|
+
return null;
|
|
7073
|
+
const approver = flags.get("--approver");
|
|
7074
|
+
return {
|
|
7075
|
+
verb: "display-ask",
|
|
7076
|
+
prompt: prompt.slice(0, ASK_PROMPT_MAX_CHARS),
|
|
7077
|
+
...approver ? { approver } : {}
|
|
7078
|
+
};
|
|
7079
|
+
}
|
|
7080
|
+
default:
|
|
7081
|
+
return null;
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
if (args[0] === "run" && args[1] === "dispatch" && args[2]) {
|
|
7085
|
+
let input;
|
|
7086
|
+
const rawInput = flags.get("--input");
|
|
7087
|
+
if (rawInput !== undefined) {
|
|
7088
|
+
try {
|
|
7089
|
+
input = JSON.parse(rawInput);
|
|
7090
|
+
} catch {}
|
|
7091
|
+
}
|
|
7092
|
+
return { verb: "run-dispatch", templateName: args[2], ...input !== undefined ? { input } : {} };
|
|
7093
|
+
}
|
|
7094
|
+
if (args[0] === "run" && args[1] === "get") {
|
|
7095
|
+
const runId = uuid(args[2]);
|
|
7096
|
+
return runId ? { verb: "run-get", runId } : null;
|
|
7097
|
+
}
|
|
7098
|
+
if (args[0] === "files" && args[1] === "read" && args[2]) {
|
|
7099
|
+
return { verb: "files-read", path: args[2], ...withSlug };
|
|
7100
|
+
}
|
|
7101
|
+
return null;
|
|
7102
|
+
}
|
|
7103
|
+
var DOCUMENT_PREVIEW_CHARS = 240;
|
|
7104
|
+
var DOCUMENT_CONTENT_CHARS = 4000;
|
|
7105
|
+
function commandOf(toolInput) {
|
|
7106
|
+
if (typeof toolInput !== "object" || toolInput === null)
|
|
7107
|
+
return null;
|
|
7108
|
+
const cmd = toolInput.command;
|
|
7109
|
+
if (typeof cmd === "string")
|
|
7110
|
+
return cmd;
|
|
7111
|
+
if (Array.isArray(cmd) && cmd.every((c) => typeof c === "string"))
|
|
7112
|
+
return cmd.join(" ");
|
|
7113
|
+
return null;
|
|
7114
|
+
}
|
|
7115
|
+
function textOf(output) {
|
|
7116
|
+
if (typeof output === "string")
|
|
7117
|
+
return output;
|
|
7118
|
+
if (typeof output !== "object" || output === null)
|
|
7119
|
+
return null;
|
|
7120
|
+
const o = output;
|
|
7121
|
+
if (typeof o.output === "string")
|
|
7122
|
+
return o.output;
|
|
7123
|
+
if (typeof o.text === "string")
|
|
7124
|
+
return o.text;
|
|
7125
|
+
if (typeof o.content === "string")
|
|
7126
|
+
return o.content;
|
|
7127
|
+
return null;
|
|
7128
|
+
}
|
|
7129
|
+
function runIdIn(text) {
|
|
7130
|
+
for (const line of [...text.split(`
|
|
7131
|
+
`)].reverse()) {
|
|
7132
|
+
const trimmed = line.trim();
|
|
7133
|
+
if (trimmed.startsWith("{")) {
|
|
7134
|
+
try {
|
|
7135
|
+
const parsed = JSON.parse(trimmed);
|
|
7136
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
7137
|
+
const runId = parsed.runId;
|
|
7138
|
+
if (typeof runId === "string" && UUID_RE.test(runId)) {
|
|
7139
|
+
return { runId, parsed };
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
} catch {}
|
|
7143
|
+
}
|
|
7144
|
+
}
|
|
7145
|
+
const m = /run ([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i.exec(text);
|
|
7146
|
+
return m ? { runId: m[1], parsed: null } : null;
|
|
7147
|
+
}
|
|
7148
|
+
function createDisplayPromoter(ctx = {}) {
|
|
7149
|
+
const pending = new Map;
|
|
7150
|
+
const factoryRef = (slug) => {
|
|
7151
|
+
if (slug)
|
|
7152
|
+
return { factorySlug: slug };
|
|
7153
|
+
if (ctx.factoryId)
|
|
7154
|
+
return { factoryId: ctx.factoryId };
|
|
7155
|
+
return {};
|
|
7156
|
+
};
|
|
7157
|
+
return {
|
|
7158
|
+
promoteToolUse(msg) {
|
|
7159
|
+
if (msg.toolName === "TodoWrite") {
|
|
7160
|
+
const todos = msg.toolInput?.todos;
|
|
7161
|
+
const entries = clampPlanEntries(todos);
|
|
7162
|
+
if (entries.length === 0)
|
|
7163
|
+
return null;
|
|
7164
|
+
return [
|
|
7165
|
+
{ type: "tool_call", toolName: msg.toolName, input: msg.toolInput, toolCallId: msg.toolUseId },
|
|
7166
|
+
{ type: "plan", entries }
|
|
7167
|
+
];
|
|
7168
|
+
}
|
|
7169
|
+
const command = commandOf(msg.toolInput);
|
|
7170
|
+
if (!command || !command.includes("agentc"))
|
|
7171
|
+
return null;
|
|
7172
|
+
const invocation = detectAgentcInvocation(command);
|
|
7173
|
+
if (!invocation)
|
|
7174
|
+
return null;
|
|
7175
|
+
pending.set(msg.toolUseId, { invocation });
|
|
7176
|
+
const call = (toolName, input) => [
|
|
7177
|
+
{ type: "tool_call", toolName, input: { ...input, command }, toolCallId: msg.toolUseId }
|
|
7178
|
+
];
|
|
7179
|
+
switch (invocation.verb) {
|
|
7180
|
+
case "display-run":
|
|
7181
|
+
case "run-get":
|
|
7182
|
+
return call("display_run", { runId: invocation.runId });
|
|
7183
|
+
case "display-changes":
|
|
7184
|
+
return call("display_changes", { runId: invocation.runId, ...factoryRef(invocation.factorySlug) });
|
|
7185
|
+
case "display-document":
|
|
7186
|
+
case "files-read":
|
|
7187
|
+
return call("display_document", { path: invocation.path, ...factoryRef(invocation.factorySlug) });
|
|
7188
|
+
case "display-plan":
|
|
7189
|
+
return call("display_plan", {});
|
|
7190
|
+
case "display-image":
|
|
7191
|
+
return call("display_image", { path: invocation.path, ...factoryRef(invocation.factorySlug) });
|
|
7192
|
+
case "display-preview":
|
|
7193
|
+
return call("display_preview", { path: invocation.path, ...factoryRef(undefined) });
|
|
7194
|
+
case "display-table":
|
|
7195
|
+
return call("display_table", {
|
|
7196
|
+
source: invocation.source,
|
|
7197
|
+
...invocation.path !== undefined ? { path: invocation.path } : {},
|
|
7198
|
+
...invocation.source === "file" ? factoryRef(undefined) : {}
|
|
7199
|
+
});
|
|
7200
|
+
case "display-chart":
|
|
7201
|
+
return call("display_chart", { chartKind: invocation.chartKind });
|
|
7202
|
+
case "display-diff":
|
|
7203
|
+
return call("display_diff", {
|
|
7204
|
+
path: invocation.path,
|
|
7205
|
+
from: invocation.from,
|
|
7206
|
+
to: invocation.to,
|
|
7207
|
+
...factoryRef(undefined)
|
|
7208
|
+
});
|
|
7209
|
+
case "display-ask":
|
|
7210
|
+
return call("display_ask", { prompt: invocation.prompt });
|
|
7211
|
+
case "run-dispatch":
|
|
7212
|
+
return call("dispatch_workflow", {
|
|
7213
|
+
templateName: invocation.templateName,
|
|
7214
|
+
...invocation.input !== undefined ? { input: invocation.input } : {}
|
|
7215
|
+
});
|
|
7216
|
+
}
|
|
7217
|
+
},
|
|
7218
|
+
promoteToolResult(msg) {
|
|
7219
|
+
const entry = pending.get(msg.toolUseId);
|
|
7220
|
+
if (!entry)
|
|
7221
|
+
return null;
|
|
7222
|
+
pending.delete(msg.toolUseId);
|
|
7223
|
+
const isError = msg.isError === true;
|
|
7224
|
+
const result = (output) => ({
|
|
7225
|
+
type: "tool_result",
|
|
7226
|
+
toolCallId: msg.toolUseId,
|
|
7227
|
+
output,
|
|
7228
|
+
isError
|
|
7229
|
+
});
|
|
7230
|
+
const text = textOf(msg.output);
|
|
7231
|
+
if (isError || text === null)
|
|
7232
|
+
return [result(msg.output)];
|
|
7233
|
+
const inv = entry.invocation;
|
|
7234
|
+
switch (inv.verb) {
|
|
7235
|
+
case "display-run": {
|
|
7236
|
+
const marker = findDisplayMarker(text);
|
|
7237
|
+
return [result(marker?.kind === "run" ? { runId: marker.runId } : { runId: inv.runId })];
|
|
7238
|
+
}
|
|
7239
|
+
case "run-get": {
|
|
7240
|
+
const found = runIdIn(text);
|
|
7241
|
+
return [result(found?.parsed ?? { runId: found?.runId ?? inv.runId })];
|
|
7242
|
+
}
|
|
7243
|
+
case "display-changes": {
|
|
7244
|
+
const marker = findDisplayMarker(text);
|
|
7245
|
+
const m = marker?.kind === "changes" ? marker : null;
|
|
7246
|
+
return [result({
|
|
7247
|
+
runId: m?.runId ?? inv.runId,
|
|
7248
|
+
...factoryRef(m?.factorySlug ?? inv.factorySlug),
|
|
7249
|
+
endedAt: m?.endedAt ?? null
|
|
7250
|
+
})];
|
|
7251
|
+
}
|
|
7252
|
+
case "display-document": {
|
|
7253
|
+
const marker = findDisplayMarker(text);
|
|
7254
|
+
const m = marker?.kind === "document" ? marker : null;
|
|
7255
|
+
return [result({
|
|
7256
|
+
path: m?.path ?? inv.path,
|
|
7257
|
+
...factoryRef(m?.factorySlug ?? inv.factorySlug),
|
|
7258
|
+
preview: m?.preview ?? null
|
|
7259
|
+
})];
|
|
7260
|
+
}
|
|
7261
|
+
case "files-read":
|
|
7262
|
+
return [result({
|
|
7263
|
+
path: inv.path,
|
|
7264
|
+
...factoryRef(inv.factorySlug),
|
|
7265
|
+
preview: text.slice(0, DOCUMENT_PREVIEW_CHARS) || null,
|
|
7266
|
+
content: text.slice(0, DOCUMENT_CONTENT_CHARS)
|
|
7267
|
+
})];
|
|
7268
|
+
case "display-plan": {
|
|
7269
|
+
const marker = findDisplayMarker(text);
|
|
7270
|
+
if (marker?.kind !== "plan")
|
|
7271
|
+
return [result(msg.output)];
|
|
7272
|
+
return [
|
|
7273
|
+
result({ entries: marker.entries.length }),
|
|
7274
|
+
{ type: "plan", entries: marker.entries }
|
|
7275
|
+
];
|
|
7276
|
+
}
|
|
7277
|
+
case "display-image": {
|
|
7278
|
+
const marker = findDisplayMarker(text);
|
|
7279
|
+
const m = marker?.kind === "image" ? marker : null;
|
|
7280
|
+
return [result({
|
|
7281
|
+
path: m?.path ?? inv.path,
|
|
7282
|
+
...factoryRef(m?.factorySlug ?? inv.factorySlug)
|
|
7283
|
+
})];
|
|
7284
|
+
}
|
|
7285
|
+
case "display-table": {
|
|
7286
|
+
if (inv.source === "file")
|
|
7287
|
+
return [result(msg.output)];
|
|
7288
|
+
const marker = findDisplayMarker(text);
|
|
7289
|
+
if (marker?.kind !== "table")
|
|
7290
|
+
return [result(msg.output)];
|
|
7291
|
+
return [result({ columns: marker.columns, rows: marker.rows, truncated: marker.truncated })];
|
|
7292
|
+
}
|
|
7293
|
+
case "display-chart": {
|
|
7294
|
+
const marker = findDisplayMarker(text);
|
|
7295
|
+
if (marker?.kind !== "chart")
|
|
7296
|
+
return [result(msg.output)];
|
|
7297
|
+
return [result({
|
|
7298
|
+
chartKind: marker.chartKind,
|
|
7299
|
+
series: marker.series,
|
|
7300
|
+
...marker.xLabel !== undefined ? { xLabel: marker.xLabel } : {},
|
|
7301
|
+
...marker.yLabel !== undefined ? { yLabel: marker.yLabel } : {}
|
|
7302
|
+
})];
|
|
7303
|
+
}
|
|
7304
|
+
case "display-ask": {
|
|
7305
|
+
if (inv.approver !== undefined)
|
|
7306
|
+
return [result(msg.output)];
|
|
7307
|
+
const marker = findDisplayMarker(text);
|
|
7308
|
+
if (marker?.kind !== "ask")
|
|
7309
|
+
return [result(msg.output)];
|
|
7310
|
+
return [result({
|
|
7311
|
+
askId: marker.askId,
|
|
7312
|
+
prompt: marker.prompt,
|
|
7313
|
+
...marker.options !== undefined ? { options: marker.options } : {},
|
|
7314
|
+
freeText: marker.freeText
|
|
7315
|
+
})];
|
|
7316
|
+
}
|
|
7317
|
+
case "display-preview":
|
|
7318
|
+
case "display-diff":
|
|
7319
|
+
return [result(msg.output)];
|
|
7320
|
+
case "run-dispatch": {
|
|
7321
|
+
const found = runIdIn(text);
|
|
7322
|
+
if (!found)
|
|
7323
|
+
return [result(msg.output)];
|
|
7324
|
+
return [result(found.parsed ?? { runId: found.runId })];
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
}
|
|
7328
|
+
};
|
|
7329
|
+
}
|
|
5270
7330
|
// src/runtimes/openai-desktop.ts
|
|
5271
7331
|
import OpenAI from "openai";
|
|
5272
7332
|
import sharp from "sharp";
|
|
@@ -5300,9 +7360,10 @@ class OpenAIDesktopRunner {
|
|
|
5300
7360
|
{ type: "input_text", text: opts.prompt }
|
|
5301
7361
|
]
|
|
5302
7362
|
}];
|
|
7363
|
+
const responses = this.openai.responses;
|
|
5303
7364
|
let previousResponseId;
|
|
5304
7365
|
for (let turn = 0;turn < MAX_TURNS; turn++) {
|
|
5305
|
-
const response = await
|
|
7366
|
+
const response = await responses.create({
|
|
5306
7367
|
model: "computer-use-preview",
|
|
5307
7368
|
tools: [{ type: "computer_use_preview", name: "computer", display_width: DISPLAY_WIDTH, display_height: DISPLAY_HEIGHT, environment: "linux" }],
|
|
5308
7369
|
input: messages,
|