@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.
Files changed (126) hide show
  1. package/README.md +66 -39
  2. package/dist/agent/__tests__/runtime-json-schema.test.d.ts +10 -0
  3. package/dist/agent/agent-context.d.ts +21 -1
  4. package/dist/agent/agent-loop.d.ts +24 -1
  5. package/dist/client.d.ts +338 -534
  6. package/dist/directives.d.ts +112 -0
  7. package/dist/display.d.ts +242 -0
  8. package/dist/errors.d.ts +24 -1
  9. package/dist/index.d.ts +34 -13
  10. package/dist/index.js +2984 -861
  11. package/dist/pause/wrappers.d.ts +31 -9
  12. package/dist/processors/ask-human.d.ts +30 -0
  13. package/dist/processors/ask-human.test.d.ts +1 -0
  14. package/dist/processors/index.d.ts +1 -0
  15. package/dist/runtimes/_acp-client.d.ts +46 -1
  16. package/dist/runtimes/_cli-agent.d.ts +58 -4
  17. package/dist/runtimes/_jsonl-guard.d.ts +103 -0
  18. package/dist/runtimes/amp.d.ts +2 -2
  19. package/dist/runtimes/claude-code.d.ts +59 -0
  20. package/dist/runtimes/claude-code.test.d.ts +14 -0
  21. package/dist/runtimes/claude.d.ts +16 -0
  22. package/dist/runtimes/claude.test.d.ts +8 -0
  23. package/dist/runtimes/codex.d.ts +9 -3
  24. package/dist/runtimes/cursor.d.ts +9 -0
  25. package/dist/runtimes/droid.d.ts +9 -0
  26. package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
  27. package/dist/runtimes/openai-desktop.js +2922 -861
  28. package/dist/runtimes/opencode.d.ts +25 -0
  29. package/dist/runtimes/vercel.js +22 -1
  30. package/dist/sandbox/devbox.d.ts +42 -0
  31. package/dist/sandbox/exec-stream.d.ts +14 -0
  32. package/dist/sandbox/network-policy.d.ts +100 -0
  33. package/dist/sandbox/provider-def.d.ts +79 -0
  34. package/dist/sandbox/providers/desktop.d.ts +10 -0
  35. package/dist/sandbox/providers/e2b.d.ts +17 -0
  36. package/dist/sandbox/providers/local.d.ts +11 -0
  37. package/dist/sandbox/providers/vercel.d.ts +18 -0
  38. package/dist/sandbox/registry.d.ts +45 -0
  39. package/dist/sandbox/sizes.d.ts +68 -0
  40. package/dist/sandbox.d.ts +24 -299
  41. package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
  42. package/dist/step-invocation/invoker.d.ts +24 -1
  43. package/dist/step-invocation/protocol.d.ts +13 -0
  44. package/dist/types/api-compliance.d.ts +71 -0
  45. package/dist/types/api-conversations.d.ts +492 -0
  46. package/dist/types/api-factory.d.ts +309 -0
  47. package/dist/types/api-projects.d.ts +131 -0
  48. package/dist/types/api-runs.d.ts +377 -0
  49. package/dist/types/api-scopes.d.ts +102 -0
  50. package/dist/types/conversation-stream.d.ts +191 -0
  51. package/dist/types/execution-context.d.ts +12 -2
  52. package/dist/types/protocol.d.ts +30 -1
  53. package/dist/types/sandbox-environment.d.ts +8 -5
  54. package/dist/types/sandbox.d.ts +79 -0
  55. package/dist/types/workflow-metadata.d.ts +33 -8
  56. package/dist/types/workflow-plan.d.ts +10 -0
  57. package/dist/types/workflow.d.ts +18 -193
  58. package/dist/utils/bundler.d.ts +12 -1
  59. package/dist/utils/errors.d.ts +9 -1
  60. package/dist/workflow-steps/index.d.ts +1 -1
  61. package/dist/workflow-steps/observability.d.ts +8 -1
  62. package/dist/workflow-steps/runner.d.ts +3 -3
  63. package/dist/workflow-steps/step.d.ts +15 -1
  64. package/dist/workflow-steps/types.d.ts +19 -5
  65. package/dist/workflow-steps/workflow.d.ts +22 -1
  66. package/dist/workflows/engine.d.ts +3 -2
  67. package/dist/workflows/invoke-child.d.ts +2 -2
  68. package/package.json +1 -1
  69. package/src/agent/agent-context.ts +206 -16
  70. package/src/agent/agent-loop.ts +40 -4
  71. package/src/agent/run-agent.ts +9 -1
  72. package/src/client.ts +909 -621
  73. package/src/directives.ts +184 -0
  74. package/src/display.ts +788 -0
  75. package/src/errors.ts +39 -0
  76. package/src/index.ts +117 -10
  77. package/src/pause/wrappers.ts +44 -9
  78. package/src/processors/ask-human.ts +136 -0
  79. package/src/processors/index.ts +5 -0
  80. package/src/runtimes/_acp-client.ts +72 -3
  81. package/src/runtimes/_cli-agent.ts +171 -38
  82. package/src/runtimes/_jsonl-guard.ts +219 -0
  83. package/src/runtimes/claude-code.ts +246 -0
  84. package/src/runtimes/claude.ts +32 -2
  85. package/src/runtimes/codex.ts +55 -3
  86. package/src/runtimes/cursor.ts +59 -0
  87. package/src/runtimes/droid.ts +63 -0
  88. package/src/runtimes/openai-desktop.ts +59 -14
  89. package/src/runtimes/opencode.ts +61 -0
  90. package/src/sandbox/devbox.ts +48 -0
  91. package/src/sandbox/exec-stream.ts +48 -0
  92. package/src/sandbox/network-policy.ts +181 -0
  93. package/src/sandbox/provider-def.ts +94 -0
  94. package/src/sandbox/providers/desktop.ts +57 -0
  95. package/src/sandbox/providers/e2b.ts +354 -0
  96. package/src/sandbox/providers/local.ts +106 -0
  97. package/src/sandbox/providers/vercel.ts +331 -0
  98. package/src/sandbox/registry.ts +198 -0
  99. package/src/sandbox/sizes.ts +95 -0
  100. package/src/sandbox.ts +59 -1263
  101. package/src/step-invocation/invoker.ts +319 -34
  102. package/src/step-invocation/protocol.ts +19 -0
  103. package/src/types/api-compliance.ts +79 -0
  104. package/src/types/api-conversations.ts +522 -0
  105. package/src/types/api-factory.ts +336 -0
  106. package/src/types/api-projects.ts +140 -0
  107. package/src/types/api-runs.ts +412 -0
  108. package/src/types/api-scopes.ts +102 -0
  109. package/src/types/conversation-stream.ts +231 -0
  110. package/src/types/execution-context.ts +10 -2
  111. package/src/types/protocol.ts +33 -0
  112. package/src/types/sandbox-environment.ts +28 -9
  113. package/src/types/sandbox.ts +78 -0
  114. package/src/types/workflow-metadata.ts +35 -8
  115. package/src/types/workflow-plan.ts +11 -0
  116. package/src/types/workflow.ts +25 -280
  117. package/src/utils/bundler.ts +32 -5
  118. package/src/utils/errors.ts +34 -2
  119. package/src/workflow-steps/index.ts +1 -0
  120. package/src/workflow-steps/observability.ts +19 -8
  121. package/src/workflow-steps/runner.ts +4 -4
  122. package/src/workflow-steps/step.ts +49 -1
  123. package/src/workflow-steps/types.ts +20 -5
  124. package/src/workflow-steps/workflow.ts +22 -1
  125. package/src/workflows/engine.ts +3 -2
  126. 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
- return err instanceof Error ? err.message : String(err);
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
- return compileRunForm(def, extractMetadata(def));
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
- return defineWorkflow({
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
- run: async (_ctx, sandbox) => env.setup(sandbox)
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
- constructor(status, message) {
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 getFactoryFile(path, opts) {
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
- return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/files/content?${q2}`, { responseType: "text" });
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 listSecrets(workflowName, opts) {
1561
- const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
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
- async listFactorySecrets(opts) {
1961
+ listFactorySecrets(opts) {
1577
1962
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
1578
- const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`);
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* streamRunLogs(runId, opts) {
1600
- const headers = { Authorization: `Bearer ${this.apiKey}` };
1601
- if (opts?.lastEventId && opts.lastEventId > 0) {
1602
- headers["Last-Event-ID"] = String(opts.lastEventId);
1603
- }
1604
- const res = await fetch(`${this.baseUrl}/api/v1/workflows/${encodeURIComponent(runId)}/stream`, {
1605
- headers,
1606
- ...opts?.signal ? { signal: opts.signal } : {}
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
- if (!res.ok || !res.body) {
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
- // src/utils/bundler.ts
1623
- import { createHash as createHash2 } from "node:crypto";
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
- async function bundle(path, label) {
1654
- const bun = globalThis;
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
- return result.outputs[0].text();
1664
- }
1665
- async function extractFromBundle(source, label, pick) {
1666
- const tmpPath = `/tmp/agentc-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`;
1667
- try {
1668
- const loaded = await importSourceModule(source, tmpPath);
1669
- try {
1670
- return pick(loaded.mod.default);
1671
- } finally {
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
- var DEFINE_WORKFLOW_CALLEE = /^(?:defineWorkflow|defineSandboxEnvironment)\d*$/;
1679
- function isDefineWorkflowCall(node) {
1680
- if (node.callee.type === "Identifier") {
1681
- return DEFINE_WORKFLOW_CALLEE.test(node.callee.name);
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
- if (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") {
1684
- return isDefineWorkflowCall(node.callee.object);
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
- return false;
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 = workflowPlan(workflow.steps.map((step, index) => ({ index, name: step.name })));
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 ? 1 : 8);
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: z10.toJSONSchema(opts.responseSchema) } } : {}
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 now2() {
3267
+ function now3() {
2445
3268
  return new Date().toISOString();
2446
3269
  }
2447
3270
  function translateMessage(message) {
2448
- const ts = now2();
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: now2() };
3488
+ yield { type: "done", sessionId: opts.sessionId ?? "", timestamp: now3() };
2648
3489
  } else {
2649
- yield { type: "error", text, timestamp: now2() };
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/_acp-client.ts
2667
- import {
2668
- ClientSideConnection,
2669
- RequestError
2670
- } from "@agentclientprotocol/sdk";
2671
- function now3() {
2672
- return new Date().toISOString();
2673
- }
2674
- var ACP_PROTOCOL_VERSION = 1;
2675
- var OPTION_KIND_FOR_VERDICT = {
2676
- allow: ["allow_once", "allow_always"],
2677
- deny: ["reject_once", "reject_always"]
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
- return { output: parts.filter((p) => p !== "").join(`
2702
- `), diffs };
2703
- }
2704
- function mapLocations(locations) {
2705
- if (!locations || locations.length === 0)
2706
- return;
2707
- return locations.map((l) => l.line != null ? { path: l.path, line: l.line } : { path: l.path });
2708
- }
2709
- function terminalToolResult(args) {
2710
- const { output, diffs } = renderToolContent(args.content);
2711
- const locations = mapLocations(args.locations);
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 messages;
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
- class AcpClientPeer {
2780
- deps;
2781
- conn;
2782
- updates = new AsyncQueue;
2783
- sessionId;
2784
- pendingPause = null;
2785
- cancelled = false;
2786
- abortPrompt = null;
2787
- constructor(deps) {
2788
- this.deps = deps;
2789
- const handler = {
2790
- sessionUpdate: (params) => {
2791
- for (const msg of normalizeSessionUpdate(params.update))
2792
- this.updates.push(msg);
2793
- },
2794
- requestPermission: (params) => this.onRequestPermission(params)
2795
- };
2796
- this.conn = new ClientSideConnection((_agent) => handler, this.deps.stream);
2797
- if (this.deps.signal) {
2798
- if (this.deps.signal.aborted)
2799
- this.cancel();
2800
- else
2801
- this.deps.signal.addEventListener("abort", () => this.cancel(), { once: true });
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
- }, ACP_HANDSHAKE_TIMEOUT_MS);
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 ${ACP_HANDSHAKE_TIMEOUT_MS}ms)`, timestamp: now4() };
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 = `/tmp/ac-${this.spec.kind}-${this.options.agentId ?? "agent"}-${opts.iteration ?? 0}.in`;
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 runPromise = this.sandbox.commands.run(cmd, {
3881
+ const runOpts = {
3177
3882
  ...this.options.cwd ? { cwd: this.options.cwd } : {},
3178
- onStdout
3179
- }).then((res2) => {
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 res2;
3185
- }, (err) => {
3893
+ return res;
3894
+ };
3895
+ const fail = (err) => {
3896
+ exited = true;
3186
3897
  lines.close();
3187
3898
  throw err;
3188
- });
3189
- for await (const line of lines) {
3190
- let parsed;
3191
- try {
3192
- parsed = JSON.parse(line);
3193
- } catch {
3194
- continue;
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 sid = this.spec.extractSessionId(parsed);
3197
- if (sid)
3198
- sessionId = sid;
3199
- for (const msg of this.spec.mapEvent(parsed)) {
3200
- if (msg.type === "error")
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/sandbox.ts
3370
- import { promises as fs2 } from "node:fs";
3371
- import { dirname as dirname2 } from "node:path";
3372
- import { spawn } from "node:child_process";
3373
- import { Readable, Writable } from "node:stream";
3374
- import { Sandbox, SandboxNotFoundError, RateLimitError } from "e2b";
3375
- import { Sandbox as Desktop } from "@e2b/desktop";
3376
- import pRetry from "p-retry";
3377
-
3378
- // src/sandbox-errors.ts
3379
- var SANDBOX_UNAVAILABLE_PREFIX = "[sandbox-unavailable";
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
- class SandboxUnavailableError extends Error {
3382
- retryable;
3383
- sandboxId;
3384
- constructor(detail, opts) {
3385
- super(`${SANDBOX_UNAVAILABLE_PREFIX}:${opts.retryable ? "retryable" : "terminal"}] ${detail}`);
3386
- this.name = "SandboxUnavailableError";
3387
- this.retryable = opts.retryable;
3388
- this.sandboxId = opts.sandboxId;
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
- // src/sandbox.ts
3393
- var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
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
- async function parseSseExecStream(body, opts) {
3565
- let stdout = "", stderr = "", exitCode = 0, exited = false;
3566
- const reader = body.getReader(), decoder = new TextDecoder;
3567
- let buf = "";
3568
- while (true) {
3569
- const { done, value } = await reader.read();
3570
- if (done)
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
- if (exited)
3600
- break;
3601
- }
3602
- if (!exited)
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
- return { ...policy, allow };
3636
- }
3637
- function toE2bNetwork(policy) {
3638
- if (policy === "allow-all")
3639
- return {};
3640
- if (policy === "deny-all")
3641
- return { denyOut: ({ allTraffic }) => [allTraffic] };
3642
- const allowHosts = Array.isArray(policy.allow) ? [...policy.allow] : Object.keys(policy.allow ?? {});
3643
- const subnetAllow = policy.subnets?.allow ?? [];
3644
- const allowOut = [...new Set([...allowHosts, ...subnetAllow])];
3645
- const network = {
3646
- denyOut: ({ allTraffic }) => [allTraffic]
3647
- };
3648
- if (allowOut.length > 0)
3649
- network.allowOut = allowOut;
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 pRetry(async (attempt) => {
4745
+ const finished = await pRetry2(async (attempt) => {
3685
4746
  const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
3686
- if (attempt > 1) {
3687
- stdout = "";
3688
- stderr = "";
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
- stdout += log.data;
3693
- opts?.onStdout?.(log.data);
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
- stderr += log.data;
3696
- opts?.onStderr?.(log.data);
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
- requiredEnv: {
3816
- VERCEL_ACCESS_TOKEN: "Vercel access token — vercel.com/account/tokens",
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.filter((_, idx) => !this.ackedSeqs.has(idx));
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 z11 } from "zod";
4511
- var StepObservabilitySchema = z11.object({
4512
- metadata: z11.record(z11.string(), z11.unknown()).optional(),
4513
- events: z11.array(z11.unknown()).optional(),
4514
- subSteps: z11.array(z11.object({
4515
- name: z11.string(),
4516
- startedAt: z11.number(),
4517
- durationMs: z11.number(),
4518
- status: z11.enum(["completed", "failed"]),
4519
- error: z11.string().optional()
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 = z11.discriminatedUnion("ok", [
4523
- z11.object({
4524
- ok: z11.literal(true),
4525
- output: z11.unknown().optional(),
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
- z11.object({
4529
- ok: z11.literal(false),
4530
- kind: z11.enum(["protocol", "user-step"]),
4531
- error: z11.string().optional()
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 = z11.object({
4535
- pauseId: z11.string().min(1),
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
- throw e;
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
- const result = await proc.wait();
4751
- splitters.flush();
4752
- return classifyRunnerOutcome(sandbox, result, resultToken);
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
- let proc = null;
5967
+ const signal = opts?.signal;
5968
+ const pollDeadlineMs = opts?.pollDeadlineMs;
5969
+ let attached = null;
4763
5970
  try {
4764
- proc = await sandbox.commands.connectProcess(runnerPid, {
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
- let output = { stdout: "", stderr: "", exitCode: 0 };
4778
- if (attached) {
4779
- try {
4780
- output = await attached.wait();
4781
- } catch (e) {
4782
- if (e instanceof SandboxUnavailableError)
4783
- throw e;
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 z12 } from "zod";
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\` — import it from any script
4953
- you write there:
4954
-
4955
- import { defineWorkflow, agent, AgentComposeClient } from "@agent-compose/sdk";
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
- Use \`/ac:generate-workflow\` / \`/ac:generate-agent\` to scaffold, then
4958
- \`agentc register <file.ts>\` (or \`/ac:register\`).
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 — \`agentc pause\`
6167
+ ## Pausing to ask the human
4961
6168
 
4962
- When you can't or shouldn't proceed without a human, run \`agentc pause\`, then
4963
- **END YOUR TURN**:
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
- \`agentc pause\` does NOT block and does NOT print the answer. It records your
4969
- question and returns immediately. The moment you end your turn, the run pauses
4970
- (your sandbox is snapshotted and compute stops while the human decides) and the
4971
- human's answer is delivered to you as your **next message** — you pick up
4972
- exactly where you left off, with the answer in hand. So: ask, end your turn,
4973
- and wait. Do NOT keep working, do NOT call more tools, and do NOT mark the task
4974
- complete after pausing.
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 z12.ZodString)
6484
+ if (schema instanceof z13.ZodString)
5112
6485
  return "string";
5113
- if (schema instanceof z12.ZodNumber)
6486
+ if (schema instanceof z13.ZodNumber)
5114
6487
  return "number";
5115
- if (schema instanceof z12.ZodBoolean)
6488
+ if (schema instanceof z13.ZodBoolean)
5116
6489
  return "boolean";
5117
- if (schema instanceof z12.ZodArray)
6490
+ if (schema instanceof z13.ZodArray)
5118
6491
  return `${zodTypeName(schema.element)}[]`;
5119
- if (schema instanceof z12.ZodEnum)
6492
+ if (schema instanceof z13.ZodEnum)
5120
6493
  return schema.options.map((o) => `"${o}"`).join(" | ");
5121
- if (schema instanceof z12.ZodOptional)
6494
+ if (schema instanceof z13.ZodOptional)
5122
6495
  return `${zodTypeName(schema.unwrap())} (optional)`;
5123
- if (schema instanceof z12.ZodNullable)
6496
+ if (schema instanceof z13.ZodNullable)
5124
6497
  return `${zodTypeName(schema.unwrap())} | null`;
5125
- if (schema instanceof z12.ZodLiteral)
6498
+ if (schema instanceof z13.ZodLiteral)
5126
6499
  return `"${String(schema.value)}"`;
5127
- if (schema instanceof z12.ZodObject)
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 z12.ZodObject ? schema.shape : null;
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
- ...opts.processors?.length ? { processors: opts.processors } : {},
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 this.openai.responses.create({
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,