@agent-compose/sdk 0.7.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 (116) 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 +24 -12
  10. package/dist/index.js +3545 -1667
  11. package/dist/pause/wrappers.d.ts +31 -9
  12. package/dist/runtimes/_acp-client.d.ts +46 -1
  13. package/dist/runtimes/_cli-agent.d.ts +49 -4
  14. package/dist/runtimes/_jsonl-guard.d.ts +103 -0
  15. package/dist/runtimes/amp.d.ts +2 -2
  16. package/dist/runtimes/claude-code.d.ts +59 -0
  17. package/dist/runtimes/claude-code.test.d.ts +14 -0
  18. package/dist/runtimes/claude.d.ts +16 -0
  19. package/dist/runtimes/claude.test.d.ts +8 -0
  20. package/dist/runtimes/codex.d.ts +9 -3
  21. package/dist/runtimes/cursor.d.ts +2 -2
  22. package/dist/runtimes/droid.d.ts +2 -2
  23. package/dist/runtimes/jsonl-guard.test.d.ts +19 -0
  24. package/dist/runtimes/openai-desktop.js +2691 -864
  25. package/dist/runtimes/opencode.d.ts +2 -2
  26. package/dist/runtimes/vercel.js +12 -1
  27. package/dist/sandbox/devbox.d.ts +42 -0
  28. package/dist/sandbox/exec-stream.d.ts +14 -0
  29. package/dist/sandbox/network-policy.d.ts +100 -0
  30. package/dist/sandbox/provider-def.d.ts +79 -0
  31. package/dist/sandbox/providers/desktop.d.ts +10 -0
  32. package/dist/sandbox/providers/e2b.d.ts +17 -0
  33. package/dist/sandbox/providers/local.d.ts +11 -0
  34. package/dist/sandbox/providers/vercel.d.ts +18 -0
  35. package/dist/sandbox/registry.d.ts +45 -0
  36. package/dist/sandbox/sizes.d.ts +68 -0
  37. package/dist/sandbox.d.ts +24 -299
  38. package/dist/step-invocation/__tests__/foreground-recovery.test.d.ts +1 -0
  39. package/dist/step-invocation/invoker.d.ts +10 -0
  40. package/dist/step-invocation/protocol.d.ts +5 -0
  41. package/dist/types/api-compliance.d.ts +71 -0
  42. package/dist/types/api-conversations.d.ts +492 -0
  43. package/dist/types/api-factory.d.ts +309 -0
  44. package/dist/types/api-projects.d.ts +131 -0
  45. package/dist/types/api-runs.d.ts +377 -0
  46. package/dist/types/api-scopes.d.ts +102 -0
  47. package/dist/types/conversation-stream.d.ts +191 -0
  48. package/dist/types/execution-context.d.ts +12 -2
  49. package/dist/types/protocol.d.ts +30 -1
  50. package/dist/types/sandbox-environment.d.ts +8 -5
  51. package/dist/types/sandbox.d.ts +74 -4
  52. package/dist/types/workflow-metadata.d.ts +33 -8
  53. package/dist/types/workflow-plan.d.ts +10 -0
  54. package/dist/types/workflow.d.ts +18 -205
  55. package/dist/utils/bundler.d.ts +12 -1
  56. package/dist/workflow-steps/index.d.ts +1 -1
  57. package/dist/workflow-steps/observability.d.ts +8 -1
  58. package/dist/workflow-steps/runner.d.ts +3 -3
  59. package/dist/workflow-steps/step.d.ts +15 -1
  60. package/dist/workflow-steps/types.d.ts +19 -5
  61. package/dist/workflow-steps/workflow.d.ts +22 -1
  62. package/dist/workflows/engine.d.ts +3 -2
  63. package/dist/workflows/invoke-child.d.ts +2 -2
  64. package/package.json +1 -1
  65. package/src/agent/agent-context.ts +186 -3
  66. package/src/agent/agent-loop.ts +31 -2
  67. package/src/client.ts +909 -621
  68. package/src/directives.ts +184 -0
  69. package/src/display.ts +788 -0
  70. package/src/errors.ts +39 -0
  71. package/src/index.ts +104 -10
  72. package/src/pause/wrappers.ts +44 -9
  73. package/src/runtimes/_acp-client.ts +72 -3
  74. package/src/runtimes/_cli-agent.ts +159 -36
  75. package/src/runtimes/_jsonl-guard.ts +219 -0
  76. package/src/runtimes/claude-code.ts +246 -0
  77. package/src/runtimes/claude.ts +32 -2
  78. package/src/runtimes/codex.ts +55 -3
  79. package/src/runtimes/openai-desktop.ts +59 -14
  80. package/src/sandbox/devbox.ts +48 -0
  81. package/src/sandbox/exec-stream.ts +48 -0
  82. package/src/sandbox/network-policy.ts +181 -0
  83. package/src/sandbox/provider-def.ts +94 -0
  84. package/src/sandbox/providers/desktop.ts +57 -0
  85. package/src/sandbox/providers/e2b.ts +354 -0
  86. package/src/sandbox/providers/local.ts +106 -0
  87. package/src/sandbox/providers/vercel.ts +331 -0
  88. package/src/sandbox/registry.ts +198 -0
  89. package/src/sandbox/sizes.ts +95 -0
  90. package/src/sandbox.ts +59 -1275
  91. package/src/step-invocation/invoker.ts +151 -28
  92. package/src/step-invocation/protocol.ts +8 -0
  93. package/src/types/api-compliance.ts +79 -0
  94. package/src/types/api-conversations.ts +522 -0
  95. package/src/types/api-factory.ts +336 -0
  96. package/src/types/api-projects.ts +140 -0
  97. package/src/types/api-runs.ts +412 -0
  98. package/src/types/api-scopes.ts +102 -0
  99. package/src/types/conversation-stream.ts +231 -0
  100. package/src/types/execution-context.ts +10 -2
  101. package/src/types/protocol.ts +33 -0
  102. package/src/types/sandbox-environment.ts +28 -9
  103. package/src/types/sandbox.ts +73 -4
  104. package/src/types/workflow-metadata.ts +35 -8
  105. package/src/types/workflow-plan.ts +11 -0
  106. package/src/types/workflow.ts +25 -292
  107. package/src/utils/bundler.ts +32 -5
  108. package/src/utils/errors.ts +16 -1
  109. package/src/workflow-steps/index.ts +1 -0
  110. package/src/workflow-steps/observability.ts +19 -8
  111. package/src/workflow-steps/runner.ts +4 -4
  112. package/src/workflow-steps/step.ts +49 -1
  113. package/src/workflow-steps/types.ts +20 -5
  114. package/src/workflow-steps/workflow.ts +22 -1
  115. package/src/workflows/engine.ts +3 -2
  116. package/src/workflows/invoke-child.ts +2 -2
@@ -717,8 +717,19 @@ async function corePause(req, coord, kind = "custom") {
717
717
 
718
718
  // src/utils/errors.ts
719
719
  function formatError(err) {
720
- if (!(err instanceof Error))
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
+ }
721
731
  return String(err);
732
+ }
722
733
  const parts = [err.message];
723
734
  const seen = new Set([err]);
724
735
  let cause = err.cause;
@@ -916,9 +927,6 @@ async function listVercelRuntimeModels() {
916
927
  const { models } = await gateway.getAvailableModels();
917
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));
918
929
  }
919
- // src/types/workflow.ts
920
- import { z as z4 } from "zod";
921
-
922
930
  // src/workflow-steps/types.ts
923
931
  var WORKFLOW_BRAND = Symbol.for("@agent-compose/sdk.Workflow");
924
932
 
@@ -939,6 +947,8 @@ function extractMetadata(source) {
939
947
  out.snapshots = Object.freeze({ ...source.snapshots });
940
948
  if (source.resources !== undefined)
941
949
  out.resources = Object.freeze({ ...source.resources });
950
+ if (source.mergePolicy !== undefined)
951
+ out.mergePolicy = source.mergePolicy;
942
952
  if (source.processors !== undefined)
943
953
  out.processors = Object.freeze([...source.processors]);
944
954
  if (source.connectors !== undefined)
@@ -999,61 +1009,39 @@ function isWorkflow(value) {
999
1009
  }
1000
1010
 
1001
1011
  // src/types/workflow.ts
1002
- function compileRunForm(def, metadata) {
1003
- const unknownSchema = z4.unknown();
1004
- const inputSchema = def.input ?? unknownSchema;
1005
- const outputSchema = def.output ?? unknownSchema;
1006
- const step = {
1007
- name: "run",
1008
- input: inputSchema,
1009
- output: outputSchema,
1010
- run: async (stepCtx) => {
1011
- const workflowCtx = {
1012
- input: stepCtx.input,
1013
- run: stepCtx.run,
1014
- requestContext: stepCtx.requestContext,
1015
- invokeChild: stepCtx.invokeChild,
1016
- setMetadata: stepCtx.setMetadata,
1017
- step: stepCtx.step,
1018
- agentEvents: stepCtx.agentEvents,
1019
- pause: stepCtx.pause,
1020
- sleep: stepCtx.sleep,
1021
- waitForEvent: stepCtx.waitForEvent,
1022
- processors: metadata.processors ?? []
1023
- };
1024
- const sandbox = stepCtx.sandbox;
1025
- if (!sandbox)
1026
- throw new Error("legacy run-form workflow requires a sandbox in StepContext");
1027
- return def.run(workflowCtx, sandbox);
1028
- }
1029
- };
1030
- const workflow = {
1031
- id: "@run-form",
1032
- input: inputSchema,
1033
- output: outputSchema,
1034
- steps: Object.freeze([step]),
1035
- metadata
1036
- };
1037
- Object.defineProperty(workflow, WORKFLOW_BRAND, { value: true, enumerable: false });
1038
- return Object.freeze(workflow);
1039
- }
1040
1012
  function defineWorkflow(def) {
1041
- if ("run" in def)
1042
- 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
+ }
1043
1016
  return createStepWorkflow(def);
1044
1017
  }
1045
1018
  // src/types/sandbox-environment.ts
1019
+ import { z as z4 } from "zod";
1046
1020
  function defineSandboxEnvironment(env) {
1047
1021
  if (typeof env.setup !== "function") {
1048
1022
  throw new Error(`defineSandboxEnvironment(${env.name}): 'setup' must be a function`);
1049
1023
  }
1050
- return defineWorkflow({
1024
+ const input = z4.unknown();
1025
+ const output = z4.unknown();
1026
+ return createStepWorkflow({
1027
+ id: env.name,
1051
1028
  ...env.description !== undefined ? { description: env.description } : {},
1052
1029
  ...env.resources !== undefined ? { resources: env.resources } : {},
1030
+ input,
1031
+ output,
1053
1032
  snapshots: env.snapshots ?? { saveLatest: true },
1054
- environmentBuild: true,
1055
- run: async (_ctx, sandbox) => env.setup(sandbox)
1056
- });
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();
1057
1045
  }
1058
1046
  // src/processors/processor.ts
1059
1047
  var Verdict = {
@@ -1279,12 +1267,32 @@ import { ofetch } from "ofetch";
1279
1267
  // src/errors.ts
1280
1268
  class AgentComposeError extends Error {
1281
1269
  status;
1282
- constructor(status, message) {
1270
+ retryAfterMs;
1271
+ code;
1272
+ capability;
1273
+ constructor(status, message, opts) {
1283
1274
  super(message);
1284
1275
  this.status = status;
1285
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;
1286
1283
  }
1287
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
+ }
1288
1296
 
1289
1297
  // src/sse.ts
1290
1298
  async function* parseSseStream(body) {
@@ -1336,6 +1344,19 @@ async function* parseSseStream(body) {
1336
1344
  }
1337
1345
  }
1338
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
+
1339
1360
  // src/client.ts
1340
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;
1341
1362
  var DEFAULT_FACTORY = "default";
@@ -1372,10 +1393,39 @@ class AgentComposeClient {
1372
1393
  headers: { Authorization: `Bearer ${apiKey}` },
1373
1394
  async onResponseError({ response }) {
1374
1395
  const body = response._data;
1375
- 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
+ });
1376
1401
  }
1377
1402
  });
1378
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
+ }
1379
1429
  register(payload) {
1380
1430
  const { factorySlug = DEFAULT_FACTORY, ...body } = payload;
1381
1431
  return this.fetch(templatePath(factorySlug), { method: "POST", body });
@@ -1495,6 +1545,22 @@ class AgentComposeClient {
1495
1545
  getRun(runId) {
1496
1546
  return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}`);
1497
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
+ }
1498
1564
  async getRunTimeline(runId, opts) {
1499
1565
  const q2 = new URLSearchParams;
1500
1566
  if (opts?.limit !== undefined)
@@ -1504,6 +1570,9 @@ class AgentComposeClient {
1504
1570
  const body = await this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/timeline${q2.toString() ? `?${q2}` : ""}`);
1505
1571
  return body.events;
1506
1572
  }
1573
+ async getRunFunding(runId) {
1574
+ return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/funding`);
1575
+ }
1507
1576
  async reportEvent(runId, input) {
1508
1577
  const body = {
1509
1578
  ...input,
@@ -1547,12 +1616,151 @@ class AgentComposeClient {
1547
1616
  created: wire.created
1548
1617
  };
1549
1618
  }
1550
- async getFactoryFile(path, opts) {
1619
+ async getFactoryFileBytes(path, opts) {
1551
1620
  const factorySlug = opts?.factorySlug ?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined) ?? DEFAULT_FACTORY;
1552
1621
  const q2 = new URLSearchParams({ path });
1553
1622
  if (opts?.revision !== undefined)
1554
1623
  q2.set("revision", String(opts.revision));
1555
- 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}`);
1556
1764
  }
1557
1765
  async listMembers() {
1558
1766
  const body = await this.fetch("/api/v1/team/members");
@@ -1598,6 +1806,88 @@ class AgentComposeClient {
1598
1806
  const body = await this.fetch(path);
1599
1807
  return body.templates;
1600
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
+ }
1601
1891
  async listFactories() {
1602
1892
  const body = await this.fetch("/api/v1/factories");
1603
1893
  return body.factories;
@@ -1635,15 +1925,32 @@ class AgentComposeClient {
1635
1925
  body: { key, value }
1636
1926
  });
1637
1927
  }
1638
- async listSecrets(workflowName, opts) {
1639
- const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
1640
- const body = await this.fetch(templatePath(factorySlug, workflowName, "secrets"));
1928
+ async listSecretEntries(path) {
1929
+ const body = await this.fetch(path);
1641
1930
  return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
1642
1931
  }
1932
+ listSecrets(workflowName, opts) {
1933
+ const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
1934
+ return this.listSecretEntries(templatePath(factorySlug, workflowName, "secrets"));
1935
+ }
1643
1936
  deleteSecret(workflowName, key, opts) {
1644
1937
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
1645
1938
  return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
1646
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
+ }
1647
1954
  setFactorySecret(key, value, opts) {
1648
1955
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
1649
1956
  return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`, {
@@ -1651,10 +1958,9 @@ class AgentComposeClient {
1651
1958
  body: { key, value }
1652
1959
  });
1653
1960
  }
1654
- async listFactorySecrets(opts) {
1961
+ listFactorySecrets(opts) {
1655
1962
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
1656
- const body = await this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`);
1657
- return body.secrets.map((s) => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
1963
+ return this.listSecretEntries(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`);
1658
1964
  }
1659
1965
  deleteFactorySecret(key, opts) {
1660
1966
  const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
@@ -1674,96 +1980,557 @@ class AgentComposeClient {
1674
1980
  cancelRun(runId) {
1675
1981
  return this.fetch(`/api/v1/workflows/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
1676
1982
  }
1677
- async* streamRunLogs(runId, opts) {
1678
- const headers = { Authorization: `Bearer ${this.apiKey}` };
1679
- if (opts?.lastEventId && opts.lastEventId > 0) {
1680
- headers["Last-Event-ID"] = String(opts.lastEventId);
1681
- }
1682
- const res = await fetch(`${this.baseUrl}/api/v1/workflows/${encodeURIComponent(runId)}/stream`, {
1683
- headers,
1684
- ...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
+ }
1685
1992
  });
1686
- if (!res.ok || !res.body) {
1687
- let message = res.statusText;
1688
- try {
1689
- const body = await res.json();
1690
- if (body.error)
1691
- message = body.error;
1692
- } catch {}
1693
- throw new AgentComposeError(res.status, message);
1694
- }
1695
- for await (const ev of parseSseStream(res.body)) {
1993
+ return body.session;
1994
+ }
1995
+ async approveComplianceSession(sessionId) {
1996
+ const body = await this.fetch(`/api/v1/compliance/sessions/${encodeURIComponent(sessionId)}/approve`, { method: "POST" });
1997
+ return body.session;
1998
+ }
1999
+ async revokeComplianceSession(sessionId) {
2000
+ const body = await this.fetch(`/api/v1/compliance/sessions/${encodeURIComponent(sessionId)}/revoke`, { method: "POST" });
2001
+ return body.session;
2002
+ }
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}` : ""}`);
2012
+ }
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}` : ""}`);
2020
+ }
2021
+ async* streamRunLogs(runId, opts) {
2022
+ const path = `/api/v1/workflows/${encodeURIComponent(runId)}/stream`;
2023
+ for await (const ev of this.openSseStream(path, opts)) {
1696
2024
  yield ev.data;
1697
2025
  }
1698
2026
  }
1699
2027
  }
1700
- // src/utils/bundler.ts
1701
- import { createHash as createHash2 } from "node:crypto";
1702
- import { parse as babelParse } from "@babel/parser";
1703
-
1704
- // src/utils/source-loader.ts
1705
- import { dirname } from "path";
1706
- import { writeFile, rm, mkdir } from "node:fs/promises";
1707
- var TMP_DIR = process.env.TMP_DIR ?? "/tmp/agent-compose";
1708
- var LATEST_VERSION = "$LATEST";
1709
- async function importSourceModule(source, tmpPath) {
1710
- await mkdir(dirname(tmpPath), { recursive: true });
1711
- await writeFile(tmpPath, source, "utf8");
1712
- const mod = await import(tmpPath);
1713
- const cleanup = () => rm(tmpPath, { force: true });
1714
- return { mod, cleanup };
1715
- }
1716
-
1717
- // src/types/workflow-plan.ts
1718
- function workflowPlan(steps) {
1719
- return { steps };
1720
- }
1721
-
1722
- // src/utils/bundler.ts
1723
- var BUNDLER_VERSION = 2;
1724
-
1725
- class WorkflowSourceValidationError extends Error {
1726
- constructor(message) {
1727
- super(message);
1728
- this.name = "WorkflowSourceValidationError";
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 };
1729
2058
  }
1730
- }
1731
- async function bundle(path, label) {
1732
- const bun = globalThis;
1733
- if (!bun.Bun?.build)
1734
- throw new Error("bundleWorkflow requires the Bun runtime (Bun.build)");
1735
- const result = await bun.Bun.build({ entrypoints: [path], format: "esm", target: "node" });
1736
- if (!result.success) {
1737
- throw new Error(`Failed to bundle ${label}:
1738
- ${result.logs.map((l) => l.message).join(`
1739
- `)}`);
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 } : {} };
1740
2068
  }
1741
- return result.outputs[0].text();
1742
- }
1743
- async function extractFromBundle(source, label, pick) {
1744
- const tmpPath = `/tmp/agentc-${label}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`;
1745
- try {
1746
- const loaded = await importSourceModule(source, tmpPath);
1747
- try {
1748
- return pick(loaded.mod.default);
1749
- } finally {
1750
- await loaded.cleanup();
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;
1751
2094
  }
1752
- } catch (err) {
1753
- throw new WorkflowSourceValidationError(`Bundled ${label} could not be evaluated: ${err instanceof Error ? err.message : String(err)}`);
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 } : {} };
1754
2114
  }
2115
+ return null;
1755
2116
  }
1756
- var DEFINE_WORKFLOW_CALLEE = /^(?:defineWorkflow|defineSandboxEnvironment)\d*$/;
1757
- function isDefineWorkflowCall(node) {
1758
- if (node.callee.type === "Identifier") {
1759
- return DEFINE_WORKFLOW_CALLEE.test(node.callee.name);
1760
- }
1761
- if (node.callee.type === "MemberExpression" && node.callee.object.type === "CallExpression") {
1762
- return isDefineWorkflowCall(node.callee.object);
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 {}
1763
2137
  }
1764
- return false;
2138
+ return out;
1765
2139
  }
1766
- function assertDefaultExportIsDefineWorkflow(source, label) {
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;
2532
+ }
2533
+ function assertDefaultExportIsDefineWorkflow(source, label) {
1767
2534
  let ast;
1768
2535
  try {
1769
2536
  ast = babelParse(source, {
@@ -1809,6 +2576,19 @@ function findDefaultExportExpression(body) {
1809
2576
  function sha256(text) {
1810
2577
  return createHash2("sha256").update(text, "utf8").digest("hex");
1811
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
+ }
1812
2592
  async function bundleWorkflow(workflowPath, overrides) {
1813
2593
  const label = `"${workflowPath}"`;
1814
2594
  const source = await bundle(workflowPath, `workflow ${label}`);
@@ -1818,7 +2598,7 @@ async function bundleWorkflow(workflowPath, overrides) {
1818
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.`);
1819
2599
  }
1820
2600
  const { metadata } = workflow;
1821
- const plan = workflowPlan(workflow.steps.map((step, index) => ({ index, name: step.name })));
2601
+ const plan = extractWorkflowPlan(workflow);
1822
2602
  const manifest = {
1823
2603
  definitionBrand: true,
1824
2604
  sourceHash: sha256(source),
@@ -1837,6 +2617,7 @@ async function bundleWorkflow(workflowPath, overrides) {
1837
2617
  ...outputSchema !== undefined ? { outputSchema } : {},
1838
2618
  ...metadata.snapshots !== undefined ? { snapshots: metadata.snapshots } : {},
1839
2619
  ...metadata.resources !== undefined ? { resources: metadata.resources } : {},
2620
+ ...metadata.mergePolicy !== undefined ? { mergePolicy: metadata.mergePolicy } : {},
1840
2621
  ...metadata.connectors !== undefined ? { connectors: metadata.connectors } : {},
1841
2622
  ...metadata.connectorOperation !== undefined ? { connectorOperation: metadata.connectorOperation } : {},
1842
2623
  ...metadata.invokePolicy !== undefined ? { invokePolicy: metadata.invokePolicy } : {},
@@ -2107,6 +2888,10 @@ function parseAgentStatus(text) {
2107
2888
  }
2108
2889
  }
2109
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
+ }
2110
2895
  function truncate(value) {
2111
2896
  return value.length > MESSAGE_PREVIEW_CHARS ? `${value.slice(0, MESSAGE_PREVIEW_CHARS)}…` : value;
2112
2897
  }
@@ -2184,7 +2969,7 @@ async function agentLoop(opts) {
2184
2969
  requestContext,
2185
2970
  agentId,
2186
2971
  ...opts.pause ? { pause: opts.pause } : {},
2187
- ...opts.responseSchema ? { outputFormat: { type: "json_schema", schema: z10.toJSONSchema(opts.responseSchema) } } : {}
2972
+ ...opts.responseSchema ? { outputFormat: { type: "json_schema", schema: runtimeJsonSchema(opts.responseSchema) } } : {}
2188
2973
  });
2189
2974
  if (processors.some((p) => p.processToolCall) && !client.supportsToolCallProcessor) {
2190
2975
  process.stderr.write(`${logLabel} processToolCall registered but runtime does not support pre-tool-use gating — hook is dormant
@@ -2318,6 +3103,8 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
2318
3103
  signal: loopAbort.signal,
2319
3104
  ...opts.inbox ? { inboxStream: opts.inbox } : {}
2320
3105
  })) {
3106
+ if (rawMsg.type === "text_delta" || rawMsg.type === "usage_delta")
3107
+ continue;
2321
3108
  const outputVerdict = await runProcessorChain(processors, (p) => p.processOutput, rawMsg, procCtx);
2322
3109
  if (outputVerdict.kind === "abort") {
2323
3110
  loopAbort.abort();
@@ -2329,6 +3116,8 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
2329
3116
  continue;
2330
3117
  }
2331
3118
  const msg = outputVerdict.value;
3119
+ if (msg.type === "text_delta" || msg.type === "usage_delta")
3120
+ continue;
2332
3121
  opts.onAgentEvent?.(iteration, msg);
2333
3122
  const summary = summarizeAgentMessage(msg);
2334
3123
  opts.onAgentLifecycleEvent?.({
@@ -2456,74 +3245,30 @@ raw: ${JSON.stringify(rawResponse).slice(0, 400)}
2456
3245
  await writeSettledState(result);
2457
3246
  opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "success", iterations: maxIterations, durationMs: Date.now() - startedAt });
2458
3247
  return result;
2459
- } catch (err) {
2460
- if (isPauseSignal(err))
2461
- throw err;
2462
- opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "failed", iterations: completedIterations, durationMs: Date.now() - startedAt, failureReason: err instanceof Error ? err.message : String(err) });
2463
- throw err;
2464
- }
2465
- }
2466
- function parseRawJsonResponse(text) {
2467
- const trimmed = text.trim();
2468
- if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
2469
- return null;
2470
- try {
2471
- return JSON.parse(trimmed);
2472
- } catch {
2473
- return null;
2474
- }
2475
- }
2476
-
2477
- // src/agent/async-queue.ts
2478
- class AsyncQueue {
2479
- buffer = [];
2480
- resolvers = [];
2481
- done = false;
2482
- push(value) {
2483
- if (this.done)
2484
- return;
2485
- const resolve = this.resolvers.shift();
2486
- if (resolve) {
2487
- resolve({ value, done: false });
2488
- } else {
2489
- this.buffer.push(value);
2490
- }
2491
- }
2492
- close() {
2493
- if (this.done)
2494
- return;
2495
- this.done = true;
2496
- while (this.resolvers.length > 0) {
2497
- const resolve = this.resolvers.shift();
2498
- resolve({ value: undefined, done: true });
2499
- }
2500
- }
2501
- [Symbol.asyncIterator]() {
2502
- return {
2503
- next: () => {
2504
- const buffered = this.buffer.shift();
2505
- if (buffered !== undefined)
2506
- return Promise.resolve({ value: buffered, done: false });
2507
- if (this.done)
2508
- return Promise.resolve({ value: undefined, done: true });
2509
- return new Promise((resolve) => {
2510
- this.resolvers.push(resolve);
2511
- });
2512
- },
2513
- return: () => {
2514
- this.close();
2515
- return Promise.resolve({ value: undefined, done: true });
2516
- }
2517
- };
3248
+ } catch (err) {
3249
+ if (isPauseSignal(err))
3250
+ throw err;
3251
+ opts.onAgentLifecycleEvent?.({ event: "agent.settled", at: Date.now(), agentId, label, outcome: "failed", iterations: completedIterations, durationMs: Date.now() - startedAt, failureReason: err instanceof Error ? err.message : String(err) });
3252
+ throw err;
3253
+ }
3254
+ }
3255
+ function parseRawJsonResponse(text) {
3256
+ const trimmed = text.trim();
3257
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
3258
+ return null;
3259
+ try {
3260
+ return JSON.parse(trimmed);
3261
+ } catch {
3262
+ return null;
2518
3263
  }
2519
3264
  }
2520
3265
 
2521
3266
  // src/runtimes/claude.ts
2522
- function now2() {
3267
+ function now3() {
2523
3268
  return new Date().toISOString();
2524
3269
  }
2525
3270
  function translateMessage(message) {
2526
- const ts = now2();
3271
+ const ts = now3();
2527
3272
  const msgs = [];
2528
3273
  if (message.type === "system" && message.subtype === "init") {
2529
3274
  msgs.push({ type: "init", sessionId: String(message.session_id ?? ""), timestamp: ts });
@@ -2597,6 +3342,22 @@ function resolveThinking(model, thinking) {
2597
3342
  return;
2598
3343
  return thinking;
2599
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
+ }
2600
3361
  var DEFAULT_CLAUDE_PATH = "/usr/local/bin/claude";
2601
3362
 
2602
3363
  class ClaudeRunner {
@@ -2683,6 +3444,7 @@ class ClaudeRunner {
2683
3444
  const systemPromptAppend = [this.config.claudeMdContent, this.options.agentManual].filter((s) => Boolean(s && s.trim())).join(`
2684
3445
 
2685
3446
  `);
3447
+ const stderrTail = new StderrTail;
2686
3448
  try {
2687
3449
  let emittedAssistantText = false;
2688
3450
  for await (const message of query({
@@ -2697,6 +3459,7 @@ class ClaudeRunner {
2697
3459
  effort: this.config.effort,
2698
3460
  cwd: this.options.cwd,
2699
3461
  abortController: queryAbort,
3462
+ stderr: (data) => stderrTail.append(data),
2700
3463
  env: { ...process.env, ...this.config.env ?? {} },
2701
3464
  pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
2702
3465
  ...systemPromptAppend ? { systemPrompt: { type: "preset", preset: "claude_code", append: systemPromptAppend } } : {},
@@ -2714,7 +3477,7 @@ class ClaudeRunner {
2714
3477
  continue;
2715
3478
  if (msg.type === "text" && raw.type === "assistant")
2716
3479
  emittedAssistantText = true;
2717
- yield msg;
3480
+ yield msg.type === "error" ? { ...msg, text: stderrTail.decorate(msg.text) } : msg;
2718
3481
  }
2719
3482
  if (raw.type === "result" && inboxQueue)
2720
3483
  inboxQueue.close();
@@ -2722,9 +3485,9 @@ class ClaudeRunner {
2722
3485
  } catch (err) {
2723
3486
  const text = formatError(err);
2724
3487
  if (/maximum number of turns/i.test(text)) {
2725
- yield { type: "done", sessionId: opts.sessionId ?? "", timestamp: now2() };
3488
+ yield { type: "done", sessionId: opts.sessionId ?? "", timestamp: now3() };
2726
3489
  } else {
2727
- yield { type: "error", text, timestamp: now2() };
3490
+ yield { type: "error", text: stderrTail.decorate(text), timestamp: now3() };
2728
3491
  }
2729
3492
  } finally {
2730
3493
  inboxQueue?.close();
@@ -2736,274 +3499,117 @@ function createClaudeRuntime(config = {}) {
2736
3499
  return defineRuntime({
2737
3500
  create: (sandbox, opts) => new ClaudeRunner(sandbox, opts, config)
2738
3501
  });
2739
- }
2740
- var claude_default = createClaudeRuntime();
2741
- // src/runtimes/_cli-agent.ts
2742
- import { ndJsonStream } from "@agentclientprotocol/sdk";
2743
-
2744
- // src/runtimes/_acp-client.ts
2745
- import {
2746
- ClientSideConnection,
2747
- RequestError
2748
- } from "@agentclientprotocol/sdk";
2749
- function now3() {
2750
- return new Date().toISOString();
2751
- }
2752
- var ACP_PROTOCOL_VERSION = 1;
2753
- var OPTION_KIND_FOR_VERDICT = {
2754
- allow: ["allow_once", "allow_always"],
2755
- deny: ["reject_once", "reject_always"]
2756
- };
2757
- function contentBlockText(block) {
2758
- if (!block)
2759
- return "";
2760
- return block.type === "text" ? block.text : "";
2761
- }
2762
- function renderToolContent(content) {
2763
- const parts = [];
2764
- const diffs = [];
2765
- for (const block of content ?? []) {
2766
- if (block.type === "content") {
2767
- parts.push(contentBlockText(block.content));
2768
- } else if (block.type === "diff") {
2769
- diffs.push({ path: block.path, oldText: block.oldText ?? null, newText: block.newText });
2770
- const header = block.oldText == null ? `--- (new file)
2771
- +++ ${block.path}` : `--- ${block.path}
2772
- +++ ${block.path}`;
2773
- parts.push(`${header}
2774
- ${block.newText}`);
2775
- } else if (block.type === "terminal") {
2776
- parts.push(`[terminal ${block.terminalId}]`);
2777
- }
2778
- }
2779
- return { output: parts.filter((p) => p !== "").join(`
2780
- `), diffs };
2781
- }
2782
- function mapLocations(locations) {
2783
- if (!locations || locations.length === 0)
2784
- return;
2785
- return locations.map((l) => l.line != null ? { path: l.path, line: l.line } : { path: l.path });
2786
- }
2787
- function terminalToolResult(args) {
2788
- const { output, diffs } = renderToolContent(args.content);
2789
- const locations = mapLocations(args.locations);
2790
- return {
2791
- type: "tool_result",
2792
- toolUseId: args.toolCallId,
2793
- output,
2794
- isError: args.status === "failed",
2795
- timestamp: args.timestamp,
2796
- ...diffs.length > 0 ? { diffs } : {},
2797
- ...locations ? { locations } : {}
2798
- };
2799
- }
2800
- function normalizeSessionUpdate(update) {
2801
- const ts = now3();
2802
- switch (update.sessionUpdate) {
2803
- case "agent_message_chunk":
2804
- return [{ type: "text", text: contentBlockText(update.content), timestamp: ts }];
2805
- case "agent_thought_chunk":
2806
- return [{ type: "thinking", text: contentBlockText(update.content), timestamp: ts }];
2807
- case "tool_call": {
2808
- const messages = [{
2809
- type: "tool_use",
2810
- toolName: update.title || update.kind || "tool",
2811
- toolInput: update.rawInput ?? {},
2812
- toolUseId: update.toolCallId,
2813
- timestamp: ts
2814
- }];
2815
- if (update.status === "completed" || update.status === "failed") {
2816
- messages.push(terminalToolResult({
2817
- toolCallId: update.toolCallId,
2818
- status: update.status,
2819
- content: update.content,
2820
- locations: update.locations,
2821
- timestamp: ts
2822
- }));
2823
- }
2824
- return messages;
2825
- }
2826
- case "tool_call_update": {
2827
- if (update.status !== "completed" && update.status !== "failed")
2828
- return [];
2829
- return [terminalToolResult({
2830
- toolCallId: update.toolCallId,
2831
- status: update.status,
2832
- content: update.content,
2833
- locations: update.locations,
2834
- timestamp: ts
2835
- })];
2836
- }
2837
- case "plan":
2838
- return [{
2839
- type: "plan",
2840
- entries: update.entries.map((e) => ({ content: e.content, priority: e.priority, status: e.status })),
2841
- timestamp: ts
2842
- }];
2843
- case "user_message_chunk":
2844
- case "available_commands_update":
2845
- case "current_mode_update":
2846
- case "usage_update":
2847
- case "plan_update":
2848
- case "plan_removed":
2849
- case "config_option_update":
2850
- case "session_info_update":
2851
- return [];
2852
- default:
2853
- return [];
2854
- }
2855
- }
2856
-
2857
- class AcpClientPeer {
2858
- deps;
2859
- conn;
2860
- updates = new AsyncQueue;
2861
- sessionId;
2862
- pendingPause = null;
2863
- cancelled = false;
2864
- abortPrompt = null;
2865
- constructor(deps) {
2866
- this.deps = deps;
2867
- const handler = {
2868
- sessionUpdate: (params) => {
2869
- for (const msg of normalizeSessionUpdate(params.update))
2870
- this.updates.push(msg);
2871
- },
2872
- requestPermission: (params) => this.onRequestPermission(params)
2873
- };
2874
- this.conn = new ClientSideConnection((_agent) => handler, this.deps.stream);
2875
- if (this.deps.signal) {
2876
- if (this.deps.signal.aborted)
2877
- this.cancel();
2878
- else
2879
- this.deps.signal.addEventListener("abort", () => this.cancel(), { once: true });
2880
- }
2881
- }
2882
- async initialize() {
2883
- const res = await this.conn.initialize({
2884
- protocolVersion: ACP_PROTOCOL_VERSION,
2885
- clientCapabilities: {
2886
- fs: { readTextFile: false, writeTextFile: false },
2887
- terminal: false
2888
- }
2889
- });
2890
- return { protocolVersion: res.protocolVersion };
2891
- }
2892
- async startSession(cwd) {
2893
- const res = await this.conn.newSession({ cwd, mcpServers: [] });
2894
- this.sessionId = res.sessionId;
2895
- return res.sessionId;
2896
- }
2897
- async* prompt(text) {
2898
- if (!this.sessionId)
2899
- throw new Error("AcpClientPeer.prompt called before startSession");
2900
- const updates = new AsyncQueue;
2901
- this.updates = updates;
2902
- const aborted = new Promise((resolve) => {
2903
- this.abortPrompt = () => {
2904
- resolve();
2905
- updates.close();
2906
- };
2907
- });
2908
- if (this.cancelled)
2909
- this.abortPrompt();
2910
- const promptPromise = this.conn.prompt({ sessionId: this.sessionId, prompt: [{ type: "text", text }] }).then((res) => {
2911
- updates.close();
2912
- return { ok: true, res };
2913
- }, (err) => {
2914
- updates.close();
2915
- return { ok: false, err };
2916
- });
2917
- for await (const msg of updates) {
2918
- yield msg;
2919
- }
2920
- if (this.pendingPause !== null) {
2921
- const pause = this.pendingPause;
2922
- this.pendingPause = null;
2923
- this.abortPrompt = null;
2924
- throw pause;
2925
- }
2926
- const settled = await Promise.race([
2927
- promptPromise.then((o) => ({ kind: "prompt", o })),
2928
- aborted.then(() => ({ kind: "aborted" }))
2929
- ]);
2930
- this.abortPrompt = null;
2931
- if (settled.kind === "aborted")
2932
- return;
2933
- const outcome = settled.o;
2934
- if (!outcome.ok) {
2935
- yield { type: "error", text: formatError(outcome.err), timestamp: now3() };
2936
- return;
2937
- }
2938
- const stopReason = outcome.res.stopReason;
2939
- if (stopReason === "refusal") {
2940
- yield { type: "error", text: "agent refused the request", timestamp: now3() };
2941
- return;
2942
- }
2943
- const usage = outcome.res.usage;
2944
- if (usage) {
2945
- yield {
2946
- type: "usage",
2947
- inputTokens: usage.inputTokens,
2948
- outputTokens: usage.outputTokens,
2949
- cacheReadTokens: usage.cachedReadTokens ?? 0,
2950
- cacheCreationTokens: usage.cachedWriteTokens ?? 0,
2951
- durationMs: 0,
2952
- numTurns: 1,
2953
- timestamp: now3()
2954
- };
2955
- }
2956
- }
2957
- get currentSessionId() {
2958
- return this.sessionId;
2959
- }
2960
- cancel() {
2961
- if (this.cancelled) {
2962
- this.abortPrompt?.();
2963
- return;
2964
- }
2965
- this.cancelled = true;
2966
- if (this.sessionId) {
2967
- this.conn.cancel({ sessionId: this.sessionId }).catch(() => {});
2968
- }
2969
- this.abortPrompt?.();
2970
- }
2971
- close() {
2972
- this.abortPrompt?.();
2973
- this.updates.close();
3502
+ }
3503
+ var claude_default = createClaudeRuntime();
3504
+ // src/runtimes/_cli-agent.ts
3505
+ import { ndJsonStream } from "@agentclientprotocol/sdk";
3506
+
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;
2974
3519
  }
2975
- async onRequestPermission(params) {
2976
- if (this.cancelled || this.deps.signal?.aborted) {
2977
- return { outcome: { outcome: "cancelled" } };
2978
- }
2979
- const tc = params.toolCall;
2980
- const call = {
2981
- toolName: tc.title || tc.kind || "tool",
2982
- toolInput: tc.rawInput ?? {},
2983
- toolUseId: tc.toolCallId
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]";
3531
+ }
3532
+ return head + `
3533
+
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.]";
2984
3535
  };
2985
- let verdict;
2986
- try {
2987
- verdict = await this.deps.gateToolCall(call, this.deps.procCtx());
2988
- } catch (err) {
2989
- if (isPauseSignal(err)) {
2990
- this.pendingPause = err;
2991
- this.cancel();
2992
- return { outcome: { outcome: "cancelled" } };
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;
2993
3559
  }
2994
- throw err;
2995
- }
2996
- if (verdict.kind === "abort") {
2997
- this.cancel();
2998
- return { outcome: { outcome: "cancelled" } };
2999
- }
3000
- const wantedKinds = verdict.kind === "allow" ? OPTION_KIND_FOR_VERDICT.allow : OPTION_KIND_FOR_VERDICT.deny;
3001
- const option = params.options.find((o) => wantedKinds.includes(o.kind)) ?? params.options[0];
3002
- if (!option) {
3003
- return { outcome: { outcome: "cancelled" } };
3004
3560
  }
3005
- return { outcome: { outcome: "selected", optionId: option.optionId } };
3006
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`;
3007
3613
  }
3008
3614
 
3009
3615
  // src/runtimes/_cli-agent.ts
@@ -3013,6 +3619,11 @@ function now4() {
3013
3619
  function shellQuote(value) {
3014
3620
  return `'${value.replace(/'/g, `'\\''`)}'`;
3015
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
+ }
3016
3627
  var ACP_FALLBACK = Symbol("acp-fallback");
3017
3628
  var ACP_HANDSHAKE_TIMEOUT_MS = Number(process.env.AC_ACP_HANDSHAKE_TIMEOUT_MS) || 1e4;
3018
3629
  var ACP_TURN_IDLE_TIMEOUT_MS = Number(process.env.AC_ACP_TURN_IDLE_TIMEOUT_MS) || 120000;
@@ -3035,6 +3646,7 @@ class CliAgentRunner {
3035
3646
  options;
3036
3647
  spec;
3037
3648
  configModel;
3649
+ configEffort;
3038
3650
  kind;
3039
3651
  get acpCapable() {
3040
3652
  return this.spec.acp !== undefined && this.acpTransportReady && typeof this.sandbox.commands.spawnDuplex === "function";
@@ -3046,11 +3658,12 @@ class CliAgentRunner {
3046
3658
  acpSession;
3047
3659
  currentAcpProcCtx;
3048
3660
  acpTransportReady = ACP_TRANSPORT_READY;
3049
- constructor(sandbox, options, spec, configModel) {
3661
+ constructor(sandbox, options, spec, configModel, configEffort) {
3050
3662
  this.sandbox = sandbox;
3051
3663
  this.options = options;
3052
3664
  this.spec = spec;
3053
3665
  this.configModel = configModel;
3666
+ this.configEffort = configEffort;
3054
3667
  this.kind = spec.kind;
3055
3668
  }
3056
3669
  enableAcpForTesting() {
@@ -3230,14 +3843,27 @@ class CliAgentRunner {
3230
3843
  let sessionId = opts.sessionId;
3231
3844
  let sawError = false;
3232
3845
  try {
3846
+ if (opts.signal?.aborted)
3847
+ return;
3233
3848
  await this.ensureInstalled();
3234
- 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);
3235
3850
  await this.sandbox.files.write(promptPath, this.spec.promptPayload(opts.prompt));
3236
3851
  const cmd = this.spec.buildCommand({
3237
3852
  promptPath,
3238
3853
  sessionId: opts.sessionId,
3239
3854
  model: this.model,
3240
- 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
3241
3867
  });
3242
3868
  const lines = new AsyncQueue;
3243
3869
  let buf = "";
@@ -3252,51 +3878,83 @@ class CliAgentRunner {
3252
3878
  lines.push(line);
3253
3879
  }
3254
3880
  };
3255
- const runPromise = this.sandbox.commands.run(cmd, {
3881
+ const runOpts = {
3256
3882
  ...this.options.cwd ? { cwd: this.options.cwd } : {},
3257
- onStdout
3258
- }).then((res2) => {
3883
+ onStdout,
3884
+ timeoutMs: 0
3885
+ };
3886
+ let exited = false;
3887
+ const settle = (res) => {
3888
+ exited = true;
3259
3889
  const tail = buf.trim();
3260
3890
  if (tail)
3261
3891
  lines.push(tail);
3262
3892
  lines.close();
3263
- return res2;
3264
- }, (err) => {
3893
+ return res;
3894
+ };
3895
+ const fail = (err) => {
3896
+ exited = true;
3265
3897
  lines.close();
3266
3898
  throw err;
3267
- });
3268
- for await (const line of lines) {
3269
- let parsed;
3270
- try {
3271
- parsed = JSON.parse(line);
3272
- } catch {
3273
- 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
+ }
3274
3936
  }
3275
- const sid = this.spec.extractSessionId(parsed);
3276
- if (sid)
3277
- sessionId = sid;
3278
- for (const msg of this.spec.mapEvent(parsed)) {
3279
- if (msg.type === "error")
3280
- sawError = true;
3281
- 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;
3282
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(() => {});
3283
3949
  }
3284
- const res = await runPromise;
3285
- if (res.exitCode !== 0 && !sawError) {
3286
- const tail = (res.stderr ?? "").slice(-2000);
3287
- yield { type: "error", text: `${this.spec.kind} exited with code ${res.exitCode}${tail ? `: ${tail}` : ""}`, timestamp: now4() };
3288
- return;
3289
- }
3290
- if (!sawError)
3291
- yield { type: "done", sessionId: sessionId ?? "", timestamp: now4() };
3292
3950
  } catch (err) {
3293
3951
  yield { type: "error", text: formatError(err), timestamp: now4() };
3294
3952
  }
3295
3953
  }
3296
3954
  }
3297
- function createCliAgentRuntime(spec, configModel) {
3955
+ function createCliAgentRuntime(spec, configModel, configEffort) {
3298
3956
  return defineRuntime({
3299
- create: (sandbox, opts) => new CliAgentRunner(sandbox, opts, spec, configModel)
3957
+ create: (sandbox, opts) => new CliAgentRunner(sandbox, opts, spec, configModel, configEffort)
3300
3958
  });
3301
3959
  }
3302
3960
 
@@ -3305,6 +3963,12 @@ function now5() {
3305
3963
  return new Date().toISOString();
3306
3964
  }
3307
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
+ }
3308
3972
  var codexSpec = {
3309
3973
  kind: "codex",
3310
3974
  authEnv: "CODEX_API_KEY",
@@ -3316,12 +3980,13 @@ var codexSpec = {
3316
3980
  },
3317
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)',
3318
3982
  promptPayload: (prompt) => prompt,
3319
- buildCommand: ({ promptPath, sessionId, model, cwd }) => {
3983
+ buildCommand: ({ promptPath, sessionId, model, cwd, effort }) => {
3320
3984
  const flags = [
3321
3985
  "--json",
3322
3986
  "--skip-git-repo-check",
3323
3987
  "--dangerously-bypass-approvals-and-sandbox",
3324
3988
  ...model ? ["-m", shellQuote(model)] : [],
3989
+ ...effort ? ["-c", `model_reasoning_effort=${effort}`] : [],
3325
3990
  ...cwd ? ["-C", shellQuote(cwd)] : []
3326
3991
  ].join(" ");
3327
3992
  const exec = sessionId ? `codex exec resume ${shellQuote(sessionId)} ${flags}` : `codex exec ${flags}`;
@@ -3351,6 +4016,16 @@ var codexSpec = {
3351
4016
  const failed = item.status === "failed" || typeof item.exit_code === "number" && item.exit_code !== 0;
3352
4017
  return [{ type: "tool_result", toolUseId: id, output: String(item.aggregated_output ?? item.output ?? ""), isError: failed, timestamp: ts }];
3353
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
+ }
3354
4029
  if (p.type === "item.completed") {
3355
4030
  return [{ type: "tool_use", toolName: itype || "item", toolInput: item, toolUseId: String(item.id ?? ""), timestamp: ts }];
3356
4031
  }
@@ -3380,7 +4055,7 @@ var codexSpec = {
3380
4055
  }
3381
4056
  };
3382
4057
  function createCodexRuntime(config = {}) {
3383
- return createCliAgentRuntime(codexSpec, config.model);
4058
+ return createCliAgentRuntime(codexSpec, config.model, config.effort);
3384
4059
  }
3385
4060
  var codex_default = createCodexRuntime();
3386
4061
  // src/runtimes/amp.ts
@@ -3520,59 +4195,278 @@ function createDroidRuntime(config = {}) {
3520
4195
  return createCliAgentRuntime(droidSpec, config.model ?? droidSpec.defaultModel);
3521
4196
  }
3522
4197
  var droid_default = createDroidRuntime();
3523
- // src/sandbox.ts
3524
- import { promises as fs2 } from "node:fs";
3525
- import { dirname as dirname2 } from "node:path";
3526
- import { spawn } from "node:child_process";
3527
- import { Readable, Writable } from "node:stream";
3528
- import { Sandbox, SandboxNotFoundError, RateLimitError } from "e2b";
3529
- import { Sandbox as Desktop } from "@e2b/desktop";
3530
- import pRetry from "p-retry";
3531
-
3532
- // src/sandbox-errors.ts
3533
- var SANDBOX_UNAVAILABLE_PREFIX = "[sandbox-unavailable";
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
+ }
3534
4383
 
3535
- class SandboxUnavailableError extends Error {
3536
- retryable;
3537
- sandboxId;
3538
- constructor(detail, opts) {
3539
- super(`${SANDBOX_UNAVAILABLE_PREFIX}:${opts.retryable ? "retryable" : "terminal"}] ${detail}`);
3540
- this.name = "SandboxUnavailableError";
3541
- this.retryable = opts.retryable;
3542
- 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;
3543
4462
  }
4463
+ if (!exited)
4464
+ throw new Error("Sandbox exec stream ended without an exit event");
4465
+ return { exitCode, stdout, stderr };
3544
4466
  }
3545
-
3546
- // src/sandbox.ts
3547
- var AGENT_COMPOSE_TAG = process.env.AGENT_COMPOSE_TAG ?? `agent-compose-${process.env.AGENT_COMPOSE_ENV ?? "dev"}`;
3548
- var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
3549
- var SANDBOX_VCPUS = {
3550
- "2vcpu-4gb": 2,
3551
- "4vcpu-8gb": 4,
3552
- "8vcpu-16gb": 8,
3553
- "32vcpu-64gb": 32
3554
- };
3555
- var DEFAULT_SANDBOX_SIZE = "2vcpu-4gb";
3556
- var E2B_TEMPLATE_SIZES = [
3557
- "2vcpu-4gb",
3558
- "4vcpu-8gb"
3559
- ];
3560
- function isE2bSupportedSize(size) {
3561
- return E2B_TEMPLATE_SIZES.includes(size);
3562
- }
3563
- function e2bMachineSpec(size) {
3564
- const cpuCount = SANDBOX_VCPUS[size];
3565
- return { cpuCount, memoryMB: cpuCount * 2048 };
3566
- }
3567
- function e2bBaseTemplate(size) {
3568
- return `agent-compose-base-${size}`;
3569
- }
3570
- function e2bAgentEnvTemplate(size) {
3571
- return `agent-env-${size}`;
3572
- }
3573
- function isPlatformE2bTemplateAlias(id) {
3574
- return E2B_TEMPLATE_SIZES.some((size) => id === e2bBaseTemplate(size) || id === e2bAgentEnvTemplate(size));
3575
- }
4467
+ // src/sandbox/providers/e2b.ts
4468
+ import { Sandbox } from "e2b";
4469
+ import pRetry from "p-retry";
3576
4470
  function makeSandboxProvider(sb) {
3577
4471
  const commands = sb.commands;
3578
4472
  return {
@@ -3619,27 +4513,15 @@ function makeSandboxProvider(sb) {
3619
4513
  },
3620
4514
  async kill() {
3621
4515
  await sb.kill();
4516
+ },
4517
+ getHost(port) {
4518
+ return sb.getHost(port);
3622
4519
  }
3623
4520
  };
3624
4521
  }
3625
4522
  function e2bMaxSandboxMs() {
3626
4523
  return Number(process.env.E2B_MAX_SANDBOX_MS) || 60 * 60 * 1000;
3627
4524
  }
3628
- function isTransientSandboxError(error) {
3629
- if (error instanceof SandboxNotFoundError || error instanceof RateLimitError)
3630
- return true;
3631
- const message = error instanceof Error ? error.message : String(error ?? "");
3632
- 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);
3633
- }
3634
- function withSandboxRetry(fn, shouldRetry = isTransientSandboxError) {
3635
- return pRetry(fn, { retries: 4, minTimeout: 400, factor: 2, shouldRetry });
3636
- }
3637
- function withSnapshotRetry(p) {
3638
- if (!p.snapshot)
3639
- return p;
3640
- const snapshot = p.snapshot.bind(p);
3641
- return { ...p, snapshot: () => withSandboxRetry(snapshot) };
3642
- }
3643
4525
  function wrapE2bBackgroundProcess(handle) {
3644
4526
  return {
3645
4527
  pid: handle.pid,
@@ -3664,6 +4546,22 @@ function wrapE2bBackgroundProcess(handle) {
3664
4546
  }
3665
4547
  };
3666
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
+ }
3667
4565
  function makeE2bSandboxProvider(sb) {
3668
4566
  const base = makeSandboxProvider(sb);
3669
4567
  return {
@@ -3690,6 +4588,24 @@ function makeE2bSandboxProvider(sb) {
3690
4588
  return wrapE2bBackgroundProcess(handle);
3691
4589
  }
3692
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
+ },
3693
4609
  async snapshot() {
3694
4610
  const { snapshotId } = await sb.createSnapshot();
3695
4611
  return { snapshotId };
@@ -3698,11 +4614,64 @@ function makeE2bSandboxProvider(sb) {
3698
4614
  await sb.pause();
3699
4615
  return { resumeHandle: sb.sandboxId };
3700
4616
  },
4617
+ async extendLifetime(ms) {
4618
+ await sb.setTimeout(ms);
4619
+ },
3701
4620
  async updateNetworkPolicy(policy) {
3702
4621
  await sb.updateNetwork(toE2bNetwork(policy));
3703
4622
  }
3704
4623
  };
3705
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";
3706
4675
  function makeDesktopSandboxProvider(sb) {
3707
4676
  return {
3708
4677
  ...makeSandboxProvider(sb),
@@ -3718,104 +4687,39 @@ function makeDesktopSandboxProvider(sb) {
3718
4687
  drag: (from, to) => sb.drag(from, to)
3719
4688
  };
3720
4689
  }
3721
- async function parseSseExecStream(body, opts) {
3722
- let stdout = "", stderr = "", exitCode = 0, exited = false;
3723
- const reader = body.getReader(), decoder = new TextDecoder;
3724
- let buf = "";
3725
- while (true) {
3726
- const { done, value } = await reader.read();
3727
- if (done)
3728
- break;
3729
- buf += decoder.decode(value, { stream: true });
3730
- const lines = buf.replace(/\r\n/g, `
3731
- `).split(`
3732
- `);
3733
- buf = lines.pop() ?? "";
3734
- for (const line of lines) {
3735
- if (!line.startsWith("data: "))
3736
- continue;
3737
- let event;
3738
- try {
3739
- event = JSON.parse(line.slice(6));
3740
- } catch (err) {
3741
- throw new Error(`Malformed sandbox exec event: ${err instanceof Error ? err.message : String(err)}`);
3742
- }
3743
- if (event.type === "stdout") {
3744
- stdout += event.data ?? "";
3745
- opts?.onStdout?.(event.data ?? "");
3746
- } else if (event.type === "stderr") {
3747
- stderr += event.data ?? "";
3748
- opts?.onStderr?.(event.data ?? "");
3749
- } else if (event.type === "exit") {
3750
- exitCode = event.exitCode ?? 0;
3751
- exited = true;
3752
- } else if (event.type === "error") {
3753
- throw new Error(`Sandbox exec error: ${event.data}`);
3754
- }
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.");
3755
4697
  }
3756
- if (exited)
3757
- break;
3758
- }
3759
- if (!exited)
3760
- throw new Error("Sandbox exec stream ended without an exit event");
3761
- return { exitCode, stdout, stderr };
3762
- }
3763
- var DOT_SEGMENT_PATH_RE2 = ".*(?:^|/|%2[fF])(?:\\.|%2[eE]){2}(?:/|%2[fF]|$).*";
3764
- var DOT_SEGMENT_RE = /(?:^|\/|%2f)(?:\.|%2e){2}(?:\/|%2f|$)/i;
3765
- function assertSafePathPrefix(domain, prefix) {
3766
- if (!prefix.startsWith("/") || DOT_SEGMENT_RE.test(prefix)) {
3767
- throw new Error(`Invalid Tier-2 pathPrefix ${JSON.stringify(prefix)} for "${domain}": prefixes must start with "/" and contain no dot-segments`);
3768
- }
3769
- }
3770
- function toVercelNetworkPolicy(policy) {
3771
- if (typeof policy === "string" || !policy.allow || Array.isArray(policy.allow)) {
3772
- return policy;
3773
- }
3774
- const allow = {};
3775
- for (const [domain, rules] of Object.entries(policy.allow)) {
3776
- const translated = rules.flatMap((rule) => {
3777
- const { requestRules, ...rest } = rule;
3778
- const methods = requestRules?.methods ?? [];
3779
- const prefixes = requestRules?.pathPrefixes ?? [];
3780
- if (methods.length === 0 && prefixes.length === 0)
3781
- return [rest];
3782
- const methodMatch = methods.length > 0 ? { method: methods } : {};
3783
- if (prefixes.length === 0)
3784
- return [{ ...rest, match: methodMatch }];
3785
- for (const prefix of prefixes)
3786
- assertSafePathPrefix(domain, prefix);
3787
- return prefixes.map((prefix) => ({ ...rest, match: { ...methodMatch, path: { startsWith: prefix } } }));
3788
- });
3789
- const hasPathGate = rules.some((rule) => (rule.requestRules?.pathPrefixes?.length ?? 0) > 0);
3790
- 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
+ }));
3791
4702
  }
3792
- return { ...policy, allow };
3793
- }
3794
- function toE2bNetwork(policy) {
3795
- if (policy === "allow-all")
3796
- return {};
3797
- if (policy === "deny-all")
3798
- return { denyOut: ({ allTraffic }) => [allTraffic] };
3799
- const allowHosts = Array.isArray(policy.allow) ? [...policy.allow] : Object.keys(policy.allow ?? {});
3800
- const subnetAllow = policy.subnets?.allow ?? [];
3801
- const allowOut = [...new Set([...allowHosts, ...subnetAllow])];
3802
- const network = {
3803
- denyOut: ({ allTraffic }) => [allTraffic]
3804
- };
3805
- if (allowOut.length > 0)
3806
- network.allowOut = allowOut;
3807
- if (!Array.isArray(policy.allow) && policy.allow) {
3808
- const rules = {};
3809
- for (const [host, hostRules] of Object.entries(policy.allow)) {
3810
- const hostTransforms = hostRules.flatMap((rule) => (rule.transform ?? []).filter((t) => t.headers && Object.keys(t.headers).length > 0).map((t) => ({ transform: { headers: t.headers } })));
3811
- if (hostTransforms.length > 0)
3812
- rules[host] = hostTransforms;
3813
- }
3814
- if (Object.keys(rules).length > 0)
3815
- 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;
3816
4718
  }
3817
- return network;
3818
4719
  }
4720
+
4721
+ // src/sandbox/providers/vercel.ts
4722
+ var VERCEL_VM_LIFETIME_WINDOW_MS = 6 * 60 * 60 * 1000;
3819
4723
  var VERCEL_MAX_TAGS = 5;
3820
4724
  function buildVercelTags(metadata) {
3821
4725
  const tags = { ...metadata, executor: AGENT_COMPOSE_TAG };
@@ -3838,23 +4742,33 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
3838
4742
  let stderr = "";
3839
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));
3840
4744
  const collect = async () => {
3841
- await pRetry(async (attempt) => {
4745
+ const finished = await pRetry2(async (attempt) => {
3842
4746
  const h = attempt === 1 ? handle : await sb.getCommand(handle.cmdId);
3843
- if (attempt > 1) {
3844
- stdout = "";
3845
- stderr = "";
3846
- }
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
+ };
3847
4753
  for await (const log of h.logs()) {
3848
4754
  if (log.stream === "stdout") {
3849
- stdout += log.data;
3850
- 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
+ }
3851
4761
  } else {
3852
- stderr += log.data;
3853
- 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
+ }
3854
4768
  }
3855
4769
  }
4770
+ return await h.wait();
3856
4771
  }, { retries: 3, minTimeout: 1000, factor: 2 });
3857
- const finished = await handle.wait();
3858
4772
  return { exitCode: finished.exitCode, stdout, stderr };
3859
4773
  };
3860
4774
  let timer;
@@ -3878,6 +4792,12 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
3878
4792
  files: {
3879
4793
  async write(path, content) {
3880
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");
3881
4801
  }
3882
4802
  },
3883
4803
  async kill() {
@@ -3897,6 +4817,84 @@ function makeVercelSandboxProvider(sb, globalEnvs) {
3897
4817
  }
3898
4818
  };
3899
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";
3900
4898
  function makeLocalSandboxProvider() {
3901
4899
  return {
3902
4900
  sandboxId: "local",
@@ -3955,155 +4953,44 @@ function makeLocalSandboxProvider() {
3955
4953
  throw err;
3956
4954
  }
3957
4955
  }
3958
- };
3959
- }
3960
- },
3961
- files: {
3962
- async write(path, content) {
3963
- await fs2.mkdir(dirname2(path), { recursive: true });
3964
- await fs2.writeFile(path, content);
3965
- },
3966
- async read(path) {
3967
- return await fs2.readFile(path, "utf8");
3968
- }
3969
- },
3970
- async kill() {}
3971
- };
3972
- }
3973
- var SANDBOX_PROVIDERS = {
3974
- vercel: {
3975
- requiredEnv: {
3976
- VERCEL_ACCESS_TOKEN: "Vercel access token — vercel.com/account/tokens",
3977
- VERCEL_TEAM_ID: "Vercel team ID — vercel.com/account/settings",
3978
- VERCEL_PROJECT_ID: "Vercel project ID — vercel.com/[team]/[project]/settings"
3979
- },
3980
- create: async (opts, env) => {
3981
- const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
3982
- const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
3983
- const np = opts.networkPolicy ? { networkPolicy: toVercelNetworkPolicy(opts.networkPolicy) } : {};
3984
- const tags = buildVercelTags(opts.metadata);
3985
- const tmpl = opts.template ?? process.env.VERCEL_DEFAULT_SNAPSHOT;
3986
- const resources = { vcpus: SANDBOX_VCPUS[opts.size ?? DEFAULT_SANDBOX_SIZE] };
3987
- 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 });
3988
- return makeVercelSandboxProvider(sb, opts.envs);
3989
- },
3990
- reconnect: async (sandboxId) => {
3991
- const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
3992
- const creds = {
3993
- token: process.env.VERCEL_ACCESS_TOKEN ?? "",
3994
- teamId: process.env.VERCEL_TEAM_ID ?? "",
3995
- projectId: process.env.VERCEL_PROJECT_ID ?? ""
3996
- };
3997
- const sb = await VercelSandbox.get({ name: sandboxId, ...creds }).catch((err) => {
3998
- throw new SandboxUnavailableError(err instanceof Error ? err.message : String(err), { retryable: true, sandboxId });
3999
- });
4000
- return makeVercelSandboxProvider(sb);
4001
- },
4002
- getActiveCount: async (env) => (await SANDBOX_PROVIDERS.vercel.listOwned(env)).length,
4003
- listOwned: async (env) => {
4004
- const { Sandbox: VercelSandbox } = await import("@vercel/sandbox");
4005
- const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
4006
- const since = Date.now() - VERCEL_VM_LIFETIME_WINDOW_MS;
4007
- const out = [];
4008
- let cursor;
4009
- for (let page = 0;page < 200; page++) {
4010
- 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 });
4011
- for (const sb of result.sandboxes) {
4012
- if (sb.createdAt < since)
4013
- return out;
4014
- if (sb.status === "running") {
4015
- out.push({ sandboxId: sb.name, createdAt: new Date(sb.createdAt), metadata: sb.tags ?? {} });
4016
- }
4017
- }
4018
- if (result.pagination.next === null)
4019
- return out;
4020
- cursor = result.pagination.next;
4021
- }
4022
- throw new Error("Vercel listSandboxes exceeded 200 pages (10k sandboxes) within the lifetime window — refuse to silently truncate");
4023
- },
4024
- deleteSnapshot: async (snapshotId, env) => {
4025
- const { Snapshot } = await import("@vercel/sandbox");
4026
- const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
4027
- const snap = await Snapshot.get({ snapshotId, ...creds });
4028
- await snap.delete();
4029
- },
4030
- snapshotExists: async (snapshotId, env) => {
4031
- const { Snapshot } = await import("@vercel/sandbox");
4032
- const creds = { token: env.VERCEL_ACCESS_TOKEN, teamId: env.VERCEL_TEAM_ID, projectId: env.VERCEL_PROJECT_ID };
4033
- try {
4034
- await Snapshot.get({ snapshotId, ...creds });
4035
- return true;
4036
- } catch (err) {
4037
- const message = err instanceof Error ? err.message : String(err ?? "");
4038
- const status = err?.status ?? err?.statusCode;
4039
- if (status === 404 || /\b404\b|not[\s_-]?found|no such snapshot/i.test(message))
4040
- return false;
4041
- throw err;
4042
- }
4043
- }
4044
- },
4045
- e2b: {
4046
- requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
4047
- create: async ({ template, timeoutMs, networkPolicy, size, ...rest }) => {
4048
- const maxMs = e2bMaxSandboxMs();
4049
- if (timeoutMs > maxMs) {
4050
- 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.`);
4051
- }
4052
- const sandboxOpts = {
4053
- ...rest,
4054
- timeoutMs: Math.min(timeoutMs, maxMs),
4055
- ...networkPolicy ? { network: toE2bNetwork(networkPolicy) } : {}
4056
- };
4057
- const resolvedSize = size ?? DEFAULT_SANDBOX_SIZE;
4058
- const tmpl = template ?? (isE2bSupportedSize(resolvedSize) ? e2bBaseTemplate(resolvedSize) : undefined) ?? process.env.E2B_DEFAULT_TEMPLATE;
4059
- return makeE2bSandboxProvider(await (tmpl ? Sandbox.create(tmpl, sandboxOpts) : Sandbox.create(sandboxOpts)));
4060
- },
4061
- reconnect: async (sandboxId) => makeE2bSandboxProvider(await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY ?? "", timeoutMs: 3600000 })),
4062
- killAll: async () => {
4063
- const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG } } });
4064
- const sandboxes = [];
4065
- while (paginator.hasNext)
4066
- sandboxes.push(...await paginator.nextItems());
4067
- if (sandboxes.length === 0)
4068
- return;
4069
- await Promise.all(sandboxes.map((s) => Sandbox.kill(s.sandboxId)));
4070
- console.info(`[sandbox] killed ${sandboxes.length} stale E2B sandboxes (tag: ${AGENT_COMPOSE_TAG})`);
4071
- },
4072
- getActiveCount: async () => (await SANDBOX_PROVIDERS.e2b.listOwned({})).length,
4073
- listOwned: async () => {
4074
- const paginator = Sandbox.list({ query: { metadata: { executor: AGENT_COMPOSE_TAG }, state: ["running"] } });
4075
- const out = [];
4076
- while (paginator.hasNext) {
4077
- const items = await pRetry(() => paginator.nextItems(), { retries: 3, minTimeout: 500, factor: 2 });
4078
- for (const sb of items) {
4079
- out.push({
4080
- sandboxId: sb.sandboxId,
4081
- createdAt: new Date(sb.startedAt),
4082
- metadata: sb.metadata ?? {}
4083
- });
4084
- }
4956
+ };
4085
4957
  }
4086
- return out;
4087
4958
  },
4088
- deleteSnapshot: async (snapshotId, env) => {
4089
- await Sandbox.deleteSnapshot(snapshotId, { apiKey: env.E2B_API_KEY });
4090
- }
4091
- },
4092
- "e2b-desktop": {
4093
- requiredEnv: { E2B_API_KEY: "E2B API key — e2b.dev/dashboard" },
4094
- create: async ({ template, timeoutMs, networkPolicy, size: _size, ...rest }) => {
4095
- if (!template)
4096
- throw new Error("E2B Desktop provider requires an explicit `template` (Dockerfile-based — no default base image)");
4097
- if (networkPolicy && networkPolicy !== "allow-all") {
4098
- 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.");
4959
+ files: {
4960
+ async write(path, content) {
4961
+ await fs2.mkdir(dirname2(path), { recursive: true });
4962
+ await fs2.writeFile(path, content);
4963
+ },
4964
+ async read(path) {
4965
+ return await fs2.readFile(path, "utf8");
4099
4966
  }
4100
- return makeDesktopSandboxProvider(await Desktop.create(template, {
4101
- ...rest,
4102
- timeoutMs: Math.min(timeoutMs, e2bMaxSandboxMs())
4103
- }));
4104
- }
4105
- }
4967
+ },
4968
+ async kill() {}
4969
+ };
4970
+ }
4971
+ // src/sandbox/registry.ts
4972
+ import { SandboxNotFoundError, RateLimitError } from "e2b";
4973
+ import pRetry3 from "p-retry";
4974
+ var SANDBOX_PROVIDERS = {
4975
+ vercel: vercelProviderDef,
4976
+ e2b: e2bProviderDef,
4977
+ "e2b-desktop": e2bDesktopProviderDef
4106
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
+ }
4107
4994
  async function createSandbox(provider, opts) {
4108
4995
  const def = SANDBOX_PROVIDERS[provider];
4109
4996
  if (!def)
@@ -4274,7 +5161,7 @@ class StepObservabilityCollector {
4274
5161
  await this.drainInFlight();
4275
5162
  const hasMetadata = Object.keys(this.metadata).length > 0;
4276
5163
  const hasSubSteps = this.subSteps.length > 0;
4277
- 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));
4278
5165
  const hasEvents = remainingEvents.length > 0;
4279
5166
  if (!hasMetadata && !hasEvents && !hasSubSteps)
4280
5167
  return;
@@ -4307,6 +5194,8 @@ function scopedMemoize(scope) {
4307
5194
  }
4308
5195
 
4309
5196
  // src/pause/wrappers.ts
5197
+ import { z as z11 } from "zod";
5198
+ var DecisionResultSchema = z11.object({ decision: z11.string() });
4310
5199
  function buildPauseWrappers(pause) {
4311
5200
  return {
4312
5201
  sleep(durationMs) {
@@ -4324,6 +5213,20 @@ function buildPauseWrappers(pause) {
4324
5213
  ...req.schema !== undefined ? { schema: req.schema } : {},
4325
5214
  ...req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}
4326
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");
4327
5230
  }
4328
5231
  };
4329
5232
  }
@@ -4626,14 +5529,35 @@ function buildInvokeChild(runId, opts = {}) {
4626
5529
  });
4627
5530
  }
4628
5531
  // src/workflow-steps/step.ts
5532
+ var CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
4629
5533
  function defineStep(opts) {
4630
5534
  if (!opts.name)
4631
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
+ }
4632
5554
  return {
4633
5555
  name: opts.name,
4634
5556
  input: opts.input,
4635
5557
  output: opts.output,
4636
- 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 }))) } : {}
4637
5561
  };
4638
5562
  }
4639
5563
  // src/step-invocation/protocol.ts
@@ -4665,37 +5589,40 @@ function requestContextPath(stepIndex) {
4665
5589
  function stepResultFilePath(token) {
4666
5590
  return `/tmp/wf/step-result-${token}.json`;
4667
5591
  }
5592
+ function stepPidFilePath(token) {
5593
+ return `/tmp/wf/step-pid-${token}.pid`;
5594
+ }
4668
5595
  function stepLogFilePath(token) {
4669
5596
  return `/tmp/wf/step-log-${token}.log`;
4670
5597
  }
4671
5598
  // src/step-invocation/invoker.ts
4672
5599
  import { randomBytes as randomBytes2 } from "node:crypto";
4673
- import { z as z11 } from "zod";
4674
- var StepObservabilitySchema = z11.object({
4675
- metadata: z11.record(z11.string(), z11.unknown()).optional(),
4676
- events: z11.array(z11.unknown()).optional(),
4677
- subSteps: z11.array(z11.object({
4678
- name: z11.string(),
4679
- startedAt: z11.number(),
4680
- durationMs: z11.number(),
4681
- status: z11.enum(["completed", "failed"]),
4682
- 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()
4683
5610
  })).optional()
4684
5611
  }).passthrough();
4685
- var StepResultEnvelopeSchema = z11.discriminatedUnion("ok", [
4686
- z11.object({
4687
- ok: z11.literal(true),
4688
- output: z11.unknown().optional(),
5612
+ var StepResultEnvelopeSchema = z12.discriminatedUnion("ok", [
5613
+ z12.object({
5614
+ ok: z12.literal(true),
5615
+ output: z12.unknown().optional(),
4689
5616
  observability: StepObservabilitySchema.optional()
4690
5617
  }),
4691
- z11.object({
4692
- ok: z11.literal(false),
4693
- kind: z11.enum(["protocol", "user-step"]),
4694
- error: z11.string().optional()
5618
+ z12.object({
5619
+ ok: z12.literal(false),
5620
+ kind: z12.enum(["protocol", "user-step"]),
5621
+ error: z12.string().optional()
4695
5622
  })
4696
5623
  ]);
4697
- var StepPauseEnvelopeSchema = z11.object({
4698
- pauseId: z11.string().min(1),
5624
+ var StepPauseEnvelopeSchema = z12.object({
5625
+ pauseId: z12.string().min(1),
4699
5626
  pauseRequest: StepPauseRequestSchema,
4700
5627
  observability: StepObservabilitySchema.optional()
4701
5628
  });
@@ -4863,28 +5790,50 @@ ${tail}` : ""}`
4863
5790
  }
4864
5791
  };
4865
5792
  }
4866
- async function pollDurableResult(sandbox, runnerPid, resultToken, signal) {
5793
+ async function pollDurableResult(sandbox, runnerPid, resultToken, opts) {
5794
+ const signal = opts?.signal;
4867
5795
  const resultFile = stepResultFilePath(resultToken);
4868
5796
  const POLL_MS = 1000;
4869
- const DEADLINE = Date.now() + 45 * 60000;
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;
4870
5802
  const readResult = async () => {
4871
- const r = await sandbox.commands.run(`cat ${resultFile} 2>/dev/null || true`).catch(() => null);
4872
- return r?.stdout ? parseStepResult(r.stdout, resultToken) ?? null : null;
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;
4873
5807
  };
4874
5808
  for (let attempt = 1;; attempt++) {
4875
5809
  if (signal?.aborted) {
4876
5810
  return { ok: false, error: { kind: "runner-exit", message: "reconnectStep aborted (run re-paused)", exitCode: 1 } };
4877
5811
  }
4878
5812
  const fromFile = await readResult();
4879
- if (fromFile)
5813
+ if (fromFile !== null && fromFile !== "unreachable")
4880
5814
  return fromFile;
4881
- const probe = await sandbox.commands.run(`test -d /proc/${runnerPid} && echo alive || echo dead`).catch(() => null);
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
+ }
4882
5831
  const dead = (probe?.stdout ?? "").includes("dead");
4883
5832
  process.stderr.write(`[recover] poll ${attempt}: result-file=absent runner=${dead ? "dead" : "alive"} pid=${runnerPid}
4884
5833
  `);
4885
5834
  if (dead) {
4886
5835
  const finalRead = await readResult();
4887
- if (finalRead)
5836
+ if (finalRead !== null && finalRead !== "unreachable")
4888
5837
  return finalRead;
4889
5838
  return {
4890
5839
  ok: false,
@@ -4894,14 +5843,18 @@ async function pollDurableResult(sandbox, runnerPid, resultToken, signal) {
4894
5843
  if (Date.now() > DEADLINE) {
4895
5844
  return {
4896
5845
  ok: false,
4897
- error: { kind: "runner-exit", message: `timed out after 45m waiting for runner pid ${runnerPid} to emit a result`, exitCode: 1 }
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
+ }
4898
5851
  };
4899
5852
  }
4900
5853
  await new Promise((r) => setTimeout(r, POLL_MS));
4901
5854
  }
4902
5855
  }
4903
- async function recoverLogsAndResult(sandbox, runnerPid, resultToken, liveSplitters, opts) {
4904
- const result = await pollDurableResult(sandbox, runnerPid, resultToken);
5856
+ async function recoverLogsAndResult(sandbox, runnerPid, resultToken, liveSplitters, opts, pollOpts) {
5857
+ const result = await pollDurableResult(sandbox, runnerPid, resultToken, pollOpts);
4905
5858
  if (sandbox.files.read && opts?.onStdout) {
4906
5859
  const fullLog = await sandbox.files.read(stepLogFilePath(resultToken)).catch(() => null);
4907
5860
  if (fullLog != null) {
@@ -4916,6 +5869,21 @@ async function recoverLogsAndResult(sandbox, runnerPid, resultToken, liveSplitte
4916
5869
  }
4917
5870
  return result;
4918
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
+ }
4919
5887
  async function prepareStepLaunch(sandbox, request, opts) {
4920
5888
  const resultToken = randomBytes2(16).toString("hex");
4921
5889
  await Promise.all([
@@ -4933,7 +5901,7 @@ async function invokeStep(sandbox, request, opts) {
4933
5901
  const splitters = makeStreamSplitters(opts, resultToken);
4934
5902
  let result;
4935
5903
  try {
4936
- result = await sandbox.commands.run(RUNNER_COMMAND, {
5904
+ result = await sandbox.commands.run(`set -o pipefail; echo $$ > ${stepPidFilePath(resultToken)}; ${RUNNER_COMMAND} | tee ${stepLogFilePath(resultToken)}`, {
4937
5905
  envs: { ...envs, HOME: "/root", IS_SANDBOX: "1" },
4938
5906
  sudo: true,
4939
5907
  timeoutMs: 0,
@@ -4941,8 +5909,15 @@ async function invokeStep(sandbox, request, opts) {
4941
5909
  ...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
4942
5910
  });
4943
5911
  } catch (e) {
4944
- if (e instanceof SandboxUnavailableError)
4945
- 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
+ }
4946
5921
  const ce = e;
4947
5922
  result = {
4948
5923
  stdout: ce.stdout ?? ce.result?.stdout ?? "",
@@ -4990,6 +5965,7 @@ async function reconnectStep(sandbox, resume, opts) {
4990
5965
  const { runnerPid, resultToken } = resume;
4991
5966
  const splitters = makeStreamSplitters(opts, resultToken);
4992
5967
  const signal = opts?.signal;
5968
+ const pollDeadlineMs = opts?.pollDeadlineMs;
4993
5969
  let attached = null;
4994
5970
  try {
4995
5971
  attached = await sandbox.commands.connectProcess(runnerPid, {
@@ -5005,7 +5981,10 @@ async function reconnectStep(sandbox, resume, opts) {
5005
5981
  resultToken,
5006
5982
  async wait() {
5007
5983
  try {
5008
- const result = await pollDurableResult(sandbox, runnerPid, resultToken, signal);
5984
+ const result = await pollDurableResult(sandbox, runnerPid, resultToken, {
5985
+ ...signal ? { signal } : {},
5986
+ ...pollDeadlineMs !== undefined ? { deadlineMs: pollDeadlineMs } : {}
5987
+ });
5009
5988
  splitters.flush();
5010
5989
  return result;
5011
5990
  } finally {
@@ -5121,7 +6100,7 @@ async function serveStep(handler) {
5121
6100
  process.exit(exitCode);
5122
6101
  }
5123
6102
  // src/agent/run-agent.ts
5124
- import { z as z12 } from "zod";
6103
+ import { z as z13 } from "zod";
5125
6104
  import { randomUUID as randomUUID2 } from "node:crypto";
5126
6105
 
5127
6106
  // src/agent/agent-context.ts
@@ -5181,9 +6160,9 @@ step) instead of writing source from memory — the skill scaffolds the correct,
5181
6160
  current shape. Then \`agentc register <file.ts>\` (or \`/ac:register\`).
5182
6161
 
5183
6162
  The skill writes **step-form** (a builder of discrete, durable \`.step()\`s).
5184
- Never write the legacy run-form (\`defineWorkflow({ run(ctx, sandbox) { … } })\`):
5185
- it is one opaque step, so any failure or resume re-runs the whole body — and
5186
- **pause does not work in run-form**.
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.
5187
6166
 
5188
6167
  ## Pausing to ask the human
5189
6168
 
@@ -5229,13 +6208,174 @@ request **without** an Authorization header and the platform adds it. Don't try
5229
6208
  to read or exfiltrate tokens; they aren't here. The "Connectors & access"
5230
6209
  section below (when present) lists exactly which providers this run can reach.
5231
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
+
5232
6331
  ## Tools in this environment
5233
6332
 
5234
6333
  - \`agentc\` — Agent Compose CLI (your primary interface; authed from env)
5235
6334
  - \`@agent-compose/sdk\` — installed in /workspace for writing workflows
5236
6335
  - \`/ac:*\` Claude Code skills — slash commands for the above
5237
6336
  - \`archil\` (factory drive), \`rtk\`, \`bun\`
6337
+ - \`xdotool\` / \`scrot\` — drive + screenshot the desktop (if this machine has one; see Computer Use)
5238
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
+ }
5239
6379
  function renderConnectorsSection(connectors) {
5240
6380
  if (connectors.length === 0)
5241
6381
  return "";
@@ -5341,29 +6481,29 @@ function stripFrontmatter(content) {
5341
6481
  return end === -1 ? content : content.slice(end + 4).trimStart();
5342
6482
  }
5343
6483
  function zodTypeName(schema) {
5344
- if (schema instanceof z12.ZodString)
6484
+ if (schema instanceof z13.ZodString)
5345
6485
  return "string";
5346
- if (schema instanceof z12.ZodNumber)
6486
+ if (schema instanceof z13.ZodNumber)
5347
6487
  return "number";
5348
- if (schema instanceof z12.ZodBoolean)
6488
+ if (schema instanceof z13.ZodBoolean)
5349
6489
  return "boolean";
5350
- if (schema instanceof z12.ZodArray)
6490
+ if (schema instanceof z13.ZodArray)
5351
6491
  return `${zodTypeName(schema.element)}[]`;
5352
- if (schema instanceof z12.ZodEnum)
6492
+ if (schema instanceof z13.ZodEnum)
5353
6493
  return schema.options.map((o) => `"${o}"`).join(" | ");
5354
- if (schema instanceof z12.ZodOptional)
6494
+ if (schema instanceof z13.ZodOptional)
5355
6495
  return `${zodTypeName(schema.unwrap())} (optional)`;
5356
- if (schema instanceof z12.ZodNullable)
6496
+ if (schema instanceof z13.ZodNullable)
5357
6497
  return `${zodTypeName(schema.unwrap())} | null`;
5358
- if (schema instanceof z12.ZodLiteral)
6498
+ if (schema instanceof z13.ZodLiteral)
5359
6499
  return `"${String(schema.value)}"`;
5360
- if (schema instanceof z12.ZodObject)
6500
+ if (schema instanceof z13.ZodObject)
5361
6501
  return "object";
5362
6502
  return "any";
5363
6503
  }
5364
6504
  function buildResponseFormatAppendix(schema) {
5365
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>`:", ""];
5366
- const shape = schema instanceof z12.ZodObject ? schema.shape : null;
6506
+ const shape = schema instanceof z13.ZodObject ? schema.shape : null;
5367
6507
  const body = shape ? (() => {
5368
6508
  const entries = Object.entries(shape);
5369
6509
  const example = Object.fromEntries(entries.map(([k, v]) => [k, `<${zodTypeName(v)}>`]));
@@ -5501,6 +6641,692 @@ async function agent(opts) {
5501
6641
  inboxAbort.abort();
5502
6642
  }
5503
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
+ }
5504
7330
  // src/runtimes/openai-desktop.ts
5505
7331
  import OpenAI from "openai";
5506
7332
  import sharp from "sharp";
@@ -5534,9 +7360,10 @@ class OpenAIDesktopRunner {
5534
7360
  { type: "input_text", text: opts.prompt }
5535
7361
  ]
5536
7362
  }];
7363
+ const responses = this.openai.responses;
5537
7364
  let previousResponseId;
5538
7365
  for (let turn = 0;turn < MAX_TURNS; turn++) {
5539
- const response = await this.openai.responses.create({
7366
+ const response = await responses.create({
5540
7367
  model: "computer-use-preview",
5541
7368
  tools: [{ type: "computer_use_preview", name: "computer", display_width: DISPLAY_WIDTH, display_height: DISPLAY_HEIGHT, environment: "linux" }],
5542
7369
  input: messages,