@agent-compose/sdk 0.5.9 → 0.7.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 (49) hide show
  1. package/dist/agent/agent-context.d.ts +1 -1
  2. package/dist/agent/agent-loop.d.ts +0 -8
  3. package/dist/agent/pause-client.d.ts +50 -0
  4. package/dist/index.d.ts +15 -6
  5. package/dist/index.js +537 -124
  6. package/dist/processors/ask-human.d.ts +30 -0
  7. package/dist/processors/ask-human.test.d.ts +1 -0
  8. package/dist/processors/gate-pause.d.ts +46 -0
  9. package/dist/processors/gate-pause.test.d.ts +1 -0
  10. package/dist/processors/index.d.ts +3 -0
  11. package/dist/runtimes/_cli-agent.d.ts +9 -0
  12. package/dist/runtimes/cursor.d.ts +9 -0
  13. package/dist/runtimes/droid.d.ts +9 -0
  14. package/dist/runtimes/openai-desktop.js +522 -122
  15. package/dist/runtimes/opencode.d.ts +25 -0
  16. package/dist/runtimes/vercel.js +11 -1
  17. package/dist/step-invocation/__tests__/background-invoker.test.d.ts +1 -0
  18. package/dist/step-invocation/index.d.ts +2 -1
  19. package/dist/step-invocation/invoker.d.ts +49 -0
  20. package/dist/step-invocation/protocol.d.ts +8 -0
  21. package/dist/types/runtime.d.ts +7 -0
  22. package/dist/types/sandbox.d.ts +54 -0
  23. package/dist/types/workflow.d.ts +12 -0
  24. package/dist/utils/errors.d.ts +9 -1
  25. package/package.json +1 -1
  26. package/src/agent/agent-context.ts +23 -16
  27. package/src/agent/agent-loop.ts +13 -33
  28. package/src/agent/pause-client.ts +108 -0
  29. package/src/agent/run-agent.ts +16 -7
  30. package/src/index.ts +27 -4
  31. package/src/processors/ask-human.ts +136 -0
  32. package/src/processors/gate-pause.ts +94 -0
  33. package/src/processors/index.ts +11 -0
  34. package/src/runtimes/_cli-agent.ts +13 -5
  35. package/src/runtimes/claude.ts +10 -6
  36. package/src/runtimes/cursor.ts +59 -0
  37. package/src/runtimes/droid.ts +63 -0
  38. package/src/runtimes/opencode.ts +61 -0
  39. package/src/sandbox.ts +78 -3
  40. package/src/step-invocation/index.ts +2 -1
  41. package/src/step-invocation/invoker.ts +359 -86
  42. package/src/step-invocation/protocol.ts +11 -0
  43. package/src/types/runtime.ts +7 -0
  44. package/src/types/sandbox.ts +53 -0
  45. package/src/types/workflow.ts +12 -0
  46. package/src/utils/errors.ts +19 -2
  47. package/dist/agent/local-pause-request.d.ts +0 -49
  48. package/src/agent/local-pause-request.ts +0 -90
  49. /package/dist/agent/{local-pause-request.test.d.ts → pause-client.test.d.ts} +0 -0
@@ -717,7 +717,17 @@ async function corePause(req, coord, kind = "custom") {
717
717
 
718
718
  // src/utils/errors.ts
719
719
  function formatError(err) {
720
- return err instanceof Error ? err.message : String(err);
720
+ if (!(err instanceof Error))
721
+ return String(err);
722
+ const parts = [err.message];
723
+ const seen = new Set([err]);
724
+ let cause = err.cause;
725
+ while (cause != null && !seen.has(cause)) {
726
+ seen.add(cause);
727
+ parts.push(cause instanceof Error ? cause.message : String(cause));
728
+ cause = cause instanceof Error ? cause.cause : undefined;
729
+ }
730
+ return parts.join(": ");
721
731
  }
722
732
 
723
733
  // src/runtimes/vercel.ts
@@ -1103,6 +1113,166 @@ function redactPattern(pattern, replacement = "[REDACTED]") {
1103
1113
  }
1104
1114
  };
1105
1115
  }
1116
+ // src/agent/pause-client.ts
1117
+ var isAbort = (signal) => Boolean(signal?.aborted);
1118
+ async function requestPauseAndAwait(opts, fetchImpl = fetch) {
1119
+ const base = opts.baseUrl.replace(/\/+$/, "");
1120
+ const headers = {
1121
+ authorization: `Bearer ${opts.token}`,
1122
+ "content-type": "application/json",
1123
+ accept: "application/json"
1124
+ };
1125
+ const createRes = await fetchImpl(`${base}/api/v1/runs/${opts.runId}/pauses`, {
1126
+ method: "POST",
1127
+ headers,
1128
+ body: JSON.stringify({
1129
+ reason: opts.reason,
1130
+ ...opts.ttlMs !== undefined ? { ttlMs: opts.ttlMs } : {},
1131
+ ...opts.options ? { options: opts.options } : {},
1132
+ ...opts.action ? { action: opts.action } : {},
1133
+ ...opts.correlationKey ? { correlationKey: opts.correlationKey } : {}
1134
+ }),
1135
+ ...opts.signal ? { signal: opts.signal } : {}
1136
+ });
1137
+ if (!createRes.ok) {
1138
+ const text = await createRes.text().catch(() => "");
1139
+ throw new Error(`pause create failed (${createRes.status}): ${text.slice(0, 300)}`);
1140
+ }
1141
+ const { pauseId } = await createRes.json();
1142
+ const pollUrl = `${base}/api/v1/runs/${opts.runId}/pauses/${pauseId}`;
1143
+ let backoffMs = 0;
1144
+ for (;; ) {
1145
+ if (isAbort(opts.signal))
1146
+ throw new Error("pause wait aborted");
1147
+ if (backoffMs > 0)
1148
+ await new Promise((r) => setTimeout(r, backoffMs));
1149
+ let res;
1150
+ try {
1151
+ res = await fetchImpl(pollUrl, { method: "GET", headers, ...opts.signal ? { signal: opts.signal } : {} });
1152
+ } catch (err) {
1153
+ if (isAbort(opts.signal))
1154
+ throw err;
1155
+ backoffMs = Math.min(8000, (backoffMs || 500) * 2);
1156
+ continue;
1157
+ }
1158
+ if (!res.ok) {
1159
+ backoffMs = Math.min(8000, (backoffMs || 500) * 2);
1160
+ continue;
1161
+ }
1162
+ backoffMs = 0;
1163
+ const body = await res.json();
1164
+ if (body.status === "pending")
1165
+ continue;
1166
+ return { status: body.status, decision: body.resumePayload ?? null };
1167
+ }
1168
+ }
1169
+
1170
+ // src/processors/gate-pause.ts
1171
+ var DENY_ANSWERS = new Set(["deny", "no", "reject", "decline", "block"]);
1172
+ function answerText(decision) {
1173
+ const raw = decision !== null && typeof decision === "object" && "decision" in decision ? decision.decision : decision;
1174
+ return typeof raw === "string" ? raw.trim() : raw == null ? "" : JSON.stringify(raw);
1175
+ }
1176
+ function createGatePauseProcessor(opts) {
1177
+ return {
1178
+ name: "gate-pause",
1179
+ async processToolCall(call, ctx) {
1180
+ const approval = await opts.policy(call, ctx);
1181
+ if (!approval)
1182
+ return Verdict.continue(call);
1183
+ const conn = opts.connection ?? {
1184
+ baseUrl: process.env.AGENT_COMPOSE_URL ?? "",
1185
+ token: process.env.AGENT_COMPOSE_RUN_TOKEN ?? "",
1186
+ runId: process.env.RUN_ID ?? ""
1187
+ };
1188
+ if (!conn.baseUrl || !conn.token || !conn.runId)
1189
+ return Verdict.continue(call);
1190
+ const decision = await requestPauseAndAwait({
1191
+ baseUrl: conn.baseUrl,
1192
+ token: conn.token,
1193
+ runId: conn.runId,
1194
+ reason: approval.reason,
1195
+ action: { tool: call.toolName, input: call.toolInput },
1196
+ options: approval.options ?? [{ id: "approve", label: "Approve" }, { id: "deny", label: "Deny" }],
1197
+ signal: ctx.abortSignal
1198
+ });
1199
+ const answer = answerText(decision.decision);
1200
+ if (decision.status === "resolved" && !DENY_ANSWERS.has(answer.toLowerCase())) {
1201
+ return Verdict.continue(call);
1202
+ }
1203
+ const why = decision.status === "resolved" ? `denied: ${answer}` : `${decision.status} with no approval`;
1204
+ return Verdict.deny(`Human ${why}. Tool "${call.toolName}" was not run — adjust course or ask again.`);
1205
+ }
1206
+ };
1207
+ }
1208
+ // src/processors/ask-human.ts
1209
+ var ASK_USER_QUESTION_TOOL = "AskUserQuestion";
1210
+ function parseAsk(input) {
1211
+ const questions = Array.isArray(input.questions) ? input.questions : [];
1212
+ const first = questions[0] ?? {};
1213
+ const head = typeof first.question === "string" && first.question.trim() ? first.question.trim() : "The agent needs your input to continue.";
1214
+ const extra = questions.length > 1 ? ` (+${questions.length - 1} more question${questions.length > 2 ? "s" : ""})` : "";
1215
+ const options = Array.isArray(first.options) ? first.options.map((o) => String(o?.label ?? "").trim()).filter(Boolean).map((label) => ({ id: label, label })) : [];
1216
+ return { reason: head + extra, options };
1217
+ }
1218
+ function answerText2(decision) {
1219
+ const raw = decision !== null && typeof decision === "object" && "decision" in decision ? decision.decision : decision;
1220
+ return typeof raw === "string" ? raw.trim() : raw == null ? "" : JSON.stringify(raw);
1221
+ }
1222
+ function parseAgentcPause(command) {
1223
+ if (!/(^|\s|&&|;|\|)\s*agentc\s+pause(\s|$)/.test(command))
1224
+ return null;
1225
+ const r = command.match(/--reason(?:=|\s+)(?:"([^"]*)"|'([^']*)'|(\S+))/);
1226
+ const reason = (r?.[1] ?? r?.[2] ?? r?.[3] ?? "The agent needs your input to continue.").trim();
1227
+ const options = [...command.matchAll(/--option(?:=|\s+)(?:"([^"]*)"|'([^']*)'|(\S+))/g)].map((m) => (m[1] ?? m[2] ?? m[3] ?? "").trim()).filter(Boolean).map((label) => ({ id: label, label }));
1228
+ return { reason, options };
1229
+ }
1230
+ function extractAsk(call) {
1231
+ if (call.toolName === ASK_USER_QUESTION_TOOL)
1232
+ return parseAsk(call.toolInput);
1233
+ if (call.toolName === "Bash") {
1234
+ const cmd = call.toolInput?.command;
1235
+ return typeof cmd === "string" ? parseAgentcPause(cmd) : null;
1236
+ }
1237
+ return null;
1238
+ }
1239
+ function createAskHumanProcessor(opts = {}) {
1240
+ return {
1241
+ name: "ask-human",
1242
+ async processToolCall(call, ctx) {
1243
+ const ask = extractAsk(call);
1244
+ if (!ask)
1245
+ return Verdict.continue(call);
1246
+ const conn = opts.connection ?? {
1247
+ baseUrl: process.env.AGENT_COMPOSE_URL ?? "",
1248
+ token: process.env.AGENT_COMPOSE_RUN_TOKEN ?? "",
1249
+ runId: process.env.RUN_ID ?? ""
1250
+ };
1251
+ if (!conn.baseUrl || !conn.token || !conn.runId)
1252
+ return Verdict.continue(call);
1253
+ let decision;
1254
+ try {
1255
+ decision = await requestPauseAndAwait({
1256
+ baseUrl: conn.baseUrl,
1257
+ token: conn.token,
1258
+ runId: conn.runId,
1259
+ reason: ask.reason,
1260
+ ...ask.options.length ? { options: ask.options } : {},
1261
+ action: { tool: call.toolName, input: call.toolInput },
1262
+ signal: ctx.abortSignal
1263
+ });
1264
+ } catch (err) {
1265
+ const msg = err instanceof Error ? err.message : String(err);
1266
+ return Verdict.abort(`Could not ask the human — the run could not be paused (${msg}). Stopping rather than guessing an answer.`);
1267
+ }
1268
+ if (decision.status === "resolved") {
1269
+ const answer = answerText2(decision.decision);
1270
+ return Verdict.deny(`The human answered: ${answer || "(no text returned)"}. Continue using this answer.`);
1271
+ }
1272
+ return Verdict.deny(`No answer came back (${decision.status}). Do NOT assume an answer — ask again, or stop and report exactly what you need from a human.`);
1273
+ }
1274
+ };
1275
+ }
1106
1276
  // src/client.ts
1107
1277
  import { ofetch } from "ofetch";
1108
1278
 
@@ -1983,7 +2153,7 @@ async function agentLoop(opts) {
1983
2153
  const logLabel = opts.label ?? "[Agent Loop]";
1984
2154
  const startedAt = Date.now();
1985
2155
  const turnsPerIteration = opts.turnsPerIteration;
1986
- const maxIterations = opts.maxIterations ?? (turnsPerIteration === undefined ? 1 : 8);
2156
+ const maxIterations = opts.maxIterations ?? (turnsPerIteration === undefined ? 3 : 8);
1987
2157
  let schemaRetriesLeft = 10;
1988
2158
  const processors = opts.processors ?? [];
1989
2159
  const requestContext = opts.requestContext ?? RequestContext.fromReserved({
@@ -2178,19 +2348,6 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
2178
2348
  }
2179
2349
  lastResponseText = responseText;
2180
2350
  let status = parseAgentStatus(responseText);
2181
- if (opts.pause && pendingSteerPause === null) {
2182
- const localPause = opts.consumeLocalPauseRequest?.() ?? null;
2183
- if (localPause) {
2184
- pendingSteerPause = {
2185
- reason: localPause.reason,
2186
- correlationKey: null,
2187
- ...localPause.options && localPause.options.length > 0 ? { payload: { options: localPause.options } } : {},
2188
- at: Date.now()
2189
- };
2190
- blockerStreak = null;
2191
- continue;
2192
- }
2193
- }
2194
2351
  const rawResponse = opts.responseSchema ? parseAgentResponse(responseText) ?? parseRawJsonResponse(responseText) : null;
2195
2352
  const validateResponse = (target) => {
2196
2353
  const parsed = opts.responseSchema.safeParse(target);
@@ -2523,6 +2680,9 @@ class ClaudeRunner {
2523
2680
  else
2524
2681
  opts.signal.addEventListener("abort", onLoopAbort, { once: true });
2525
2682
  }
2683
+ const systemPromptAppend = [this.config.claudeMdContent, this.options.agentManual].filter((s) => Boolean(s && s.trim())).join(`
2684
+
2685
+ `);
2526
2686
  try {
2527
2687
  let emittedAssistantText = false;
2528
2688
  for await (const message of query({
@@ -2537,9 +2697,9 @@ class ClaudeRunner {
2537
2697
  effort: this.config.effort,
2538
2698
  cwd: this.options.cwd,
2539
2699
  abortController: queryAbort,
2540
- env: { ...process.env, ...this.config.env ?? {}, ...this.options.agentId ? { AGENT_COMPOSE_AGENT_ID: this.options.agentId } : {} },
2700
+ env: { ...process.env, ...this.config.env ?? {} },
2541
2701
  pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
2542
- ...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
2702
+ ...systemPromptAppend ? { systemPrompt: { type: "preset", preset: "claude_code", append: systemPromptAppend } } : {},
2543
2703
  skills: this.config.skills ?? "all",
2544
2704
  resume: opts.sessionId,
2545
2705
  mcpServers: this.config.mcpServers,
@@ -2855,6 +3015,7 @@ function shellQuote(value) {
2855
3015
  }
2856
3016
  var ACP_FALLBACK = Symbol("acp-fallback");
2857
3017
  var ACP_HANDSHAKE_TIMEOUT_MS = Number(process.env.AC_ACP_HANDSHAKE_TIMEOUT_MS) || 1e4;
3018
+ var ACP_TURN_IDLE_TIMEOUT_MS = Number(process.env.AC_ACP_TURN_IDLE_TIMEOUT_MS) || 120000;
2858
3019
  var ACP_TRANSPORT_READY = true;
2859
3020
  async function withHandshakeTimeout(p, ms) {
2860
3021
  let timer;
@@ -3022,7 +3183,7 @@ class CliAgentRunner {
3022
3183
  watchdog = setTimeout(() => {
3023
3184
  stalled = true;
3024
3185
  peer.cancel();
3025
- }, ACP_HANDSHAKE_TIMEOUT_MS);
3186
+ }, ACP_TURN_IDLE_TIMEOUT_MS);
3026
3187
  };
3027
3188
  try {
3028
3189
  armWatchdog();
@@ -3039,7 +3200,7 @@ class CliAgentRunner {
3039
3200
  if (stalled && !sawError) {
3040
3201
  await this.teardownAcp(proc, peer);
3041
3202
  this.acpSession = undefined;
3042
- yield { type: "error", text: `${this.spec.kind} prompt turn stalled (no activity for ${ACP_HANDSHAKE_TIMEOUT_MS}ms)`, timestamp: now4() };
3203
+ yield { type: "error", text: `${this.spec.kind} prompt turn stalled (no activity for ${ACP_TURN_IDLE_TIMEOUT_MS}ms)`, timestamp: now4() };
3043
3204
  return;
3044
3205
  }
3045
3206
  if (!sawError)
@@ -3051,7 +3212,7 @@ class CliAgentRunner {
3051
3212
  throw new Error("spawnAcpProcess called on a provider without commands.spawnDuplex");
3052
3213
  }
3053
3214
  const cmd = `${acp.command} ${acp.args.map(shellQuote).join(" ")}`.trim();
3054
- const agentEnv = { ...acp.env, ...this.options.agentId ? { AGENT_COMPOSE_AGENT_ID: this.options.agentId } : {} };
3215
+ const agentEnv = { ...acp.env };
3055
3216
  const proc = spawnDuplex.call(this.sandbox.commands, cmd, {
3056
3217
  ...this.options.cwd ? { cwd: this.options.cwd } : {},
3057
3218
  ...Object.keys(agentEnv).length > 0 ? { envs: agentEnv } : {}
@@ -3284,6 +3445,81 @@ function createAmpRuntime(config = {}) {
3284
3445
  return createCliAgentRuntime(ampSpec, config.model);
3285
3446
  }
3286
3447
  var amp_default = createAmpRuntime();
3448
+ // src/runtimes/opencode.ts
3449
+ function now7() {
3450
+ return new Date().toISOString();
3451
+ }
3452
+ var opencodeSpec = {
3453
+ kind: "opencode",
3454
+ authEnv: "OPENROUTER_API_KEY",
3455
+ bin: "opencode",
3456
+ defaultModel: "openrouter/z-ai/glm-5.2",
3457
+ acp: { command: "opencode", args: ["acp"] },
3458
+ install: 'sudo npm install -g opencode-ai && (command -v opencode >/dev/null 2>&1 || sudo ln -sf "$(npm prefix -g)/bin/opencode" /usr/local/bin/opencode)',
3459
+ promptPayload: (prompt) => prompt,
3460
+ buildCommand: ({ promptPath, model, cwd }) => `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}opencode run ${model ? `--model ${shellQuote(model)} ` : ""}"$(cat ${shellQuote(promptPath)})"`,
3461
+ extractSessionId: () => {
3462
+ return;
3463
+ },
3464
+ mapEvent: (p) => {
3465
+ const ts = now7();
3466
+ const text = typeof p.text === "string" ? p.text : typeof p.content === "string" ? p.content : "";
3467
+ return text ? [{ type: "text", text, timestamp: ts }] : [];
3468
+ }
3469
+ };
3470
+ function createOpencodeRuntime(config = {}) {
3471
+ return createCliAgentRuntime(opencodeSpec, config.model ?? opencodeSpec.defaultModel);
3472
+ }
3473
+ var opencode_default = createOpencodeRuntime();
3474
+ // src/runtimes/cursor.ts
3475
+ function now8() {
3476
+ return new Date().toISOString();
3477
+ }
3478
+ var cursorSpec = {
3479
+ kind: "cursor",
3480
+ authEnv: "CURSOR_API_KEY",
3481
+ bin: "cursor-agent",
3482
+ defaultModel: "auto",
3483
+ acp: { command: "cursor-agent", args: ["acp"] },
3484
+ install: 'curl https://cursor.com/install -fsS | bash && (command -v cursor-agent >/dev/null 2>&1 || sudo ln -sf "$HOME/.local/bin/cursor-agent" /usr/local/bin/cursor-agent)',
3485
+ promptPayload: (prompt) => prompt,
3486
+ buildCommand: ({ promptPath, model, cwd }) => `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}cursor-agent -p --force ${model ? `--model ${shellQuote(model)} ` : ""}--output-format text "$(cat ${shellQuote(promptPath)})"`,
3487
+ extractSessionId: () => {
3488
+ return;
3489
+ },
3490
+ mapEvent: (p) => {
3491
+ const ts = now8();
3492
+ const text = typeof p.text === "string" ? p.text : typeof p.content === "string" ? p.content : "";
3493
+ return text ? [{ type: "text", text, timestamp: ts }] : [];
3494
+ }
3495
+ };
3496
+ function createCursorRuntime(config = {}) {
3497
+ return createCliAgentRuntime(cursorSpec, config.model ?? cursorSpec.defaultModel);
3498
+ }
3499
+ var cursor_default = createCursorRuntime();
3500
+ // src/runtimes/droid.ts
3501
+ function now9() {
3502
+ return new Date().toISOString();
3503
+ }
3504
+ var droidSpec = {
3505
+ kind: "droid",
3506
+ authEnv: "OPENROUTER_API_KEY",
3507
+ bin: "droid",
3508
+ defaultModel: "custom:GLM-5.2-OR-0",
3509
+ install: 'curl -fsSL https://app.factory.ai/cli | sh && (command -v droid >/dev/null 2>&1 || sudo ln -sf "$HOME/.local/bin/droid" /usr/local/bin/droid)',
3510
+ promptPayload: (prompt) => prompt,
3511
+ buildCommand: ({ promptPath, model, cwd }) => `${cwd ? `cd ${shellQuote(cwd)} && ` : ""}droid exec --auto medium ${model ? `--model ${shellQuote(model)} ` : ""}--output-format json -f ${shellQuote(promptPath)}`,
3512
+ extractSessionId: (p) => typeof p.session_id === "string" ? p.session_id : undefined,
3513
+ mapEvent: (p) => {
3514
+ const ts = now9();
3515
+ const text = p.type === "result" && typeof p.result === "string" ? p.result : typeof p.text === "string" ? p.text : typeof p.content === "string" ? p.content : "";
3516
+ return text ? [{ type: "text", text, timestamp: ts }] : [];
3517
+ }
3518
+ };
3519
+ function createDroidRuntime(config = {}) {
3520
+ return createCliAgentRuntime(droidSpec, config.model ?? droidSpec.defaultModel);
3521
+ }
3522
+ var droid_default = createDroidRuntime();
3287
3523
  // src/sandbox.ts
3288
3524
  import { promises as fs2 } from "node:fs";
3289
3525
  import { dirname as dirname2 } from "node:path";
@@ -3376,6 +3612,9 @@ function makeSandboxProvider(sb) {
3376
3612
  files: {
3377
3613
  async write(path, content) {
3378
3614
  await sb.files.write(path, content);
3615
+ },
3616
+ async read(path) {
3617
+ return await sb.files.read(path);
3379
3618
  }
3380
3619
  },
3381
3620
  async kill() {
@@ -3401,13 +3640,64 @@ function withSnapshotRetry(p) {
3401
3640
  const snapshot = p.snapshot.bind(p);
3402
3641
  return { ...p, snapshot: () => withSandboxRetry(snapshot) };
3403
3642
  }
3643
+ function wrapE2bBackgroundProcess(handle) {
3644
+ return {
3645
+ pid: handle.pid,
3646
+ async wait() {
3647
+ try {
3648
+ const r = await handle.wait();
3649
+ return { exitCode: r.exitCode ?? 0, stdout: r.stdout, stderr: r.stderr };
3650
+ } catch (e) {
3651
+ const ce = e;
3652
+ if (typeof ce.exitCode === "number") {
3653
+ return {
3654
+ exitCode: ce.exitCode,
3655
+ stdout: typeof ce.stdout === "string" ? ce.stdout : "",
3656
+ stderr: typeof ce.stderr === "string" ? ce.stderr : ""
3657
+ };
3658
+ }
3659
+ throw e;
3660
+ }
3661
+ },
3662
+ async kill() {
3663
+ await handle.kill();
3664
+ }
3665
+ };
3666
+ }
3404
3667
  function makeE2bSandboxProvider(sb) {
3668
+ const base = makeSandboxProvider(sb);
3405
3669
  return {
3406
- ...makeSandboxProvider(sb),
3670
+ ...base,
3671
+ commands: {
3672
+ ...base.commands,
3673
+ async runBackground(cmd, opts) {
3674
+ const { sudo, onStdout, onStderr, ...rest } = opts ?? {};
3675
+ const handle = await sb.commands.run(cmd, {
3676
+ ...rest,
3677
+ background: true,
3678
+ ...sudo ? { user: "root" } : {},
3679
+ ...onStdout ? { onStdout } : {},
3680
+ ...onStderr ? { onStderr } : {}
3681
+ });
3682
+ return wrapE2bBackgroundProcess(handle);
3683
+ },
3684
+ async connectProcess(pid, opts) {
3685
+ const handle = await sb.commands.connect(pid, {
3686
+ ...opts?.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {},
3687
+ ...opts?.onStdout ? { onStdout: opts.onStdout } : {},
3688
+ ...opts?.onStderr ? { onStderr: opts.onStderr } : {}
3689
+ });
3690
+ return wrapE2bBackgroundProcess(handle);
3691
+ }
3692
+ },
3407
3693
  async snapshot() {
3408
3694
  const { snapshotId } = await sb.createSnapshot();
3409
3695
  return { snapshotId };
3410
3696
  },
3697
+ async pauseProcess() {
3698
+ await sb.pause();
3699
+ return { resumeHandle: sb.sandboxId };
3700
+ },
3411
3701
  async updateNetworkPolicy(policy) {
3412
3702
  await sb.updateNetwork(toE2bNetwork(policy));
3413
3703
  }
@@ -3672,6 +3962,9 @@ function makeLocalSandboxProvider() {
3672
3962
  async write(path, content) {
3673
3963
  await fs2.mkdir(dirname2(path), { recursive: true });
3674
3964
  await fs2.writeFile(path, content);
3965
+ },
3966
+ async read(path) {
3967
+ return await fs2.readFile(path, "utf8");
3675
3968
  }
3676
3969
  },
3677
3970
  async kill() {}
@@ -4372,6 +4665,9 @@ function requestContextPath(stepIndex) {
4372
4665
  function stepResultFilePath(token) {
4373
4666
  return `/tmp/wf/step-result-${token}.json`;
4374
4667
  }
4668
+ function stepLogFilePath(token) {
4669
+ return `/tmp/wf/step-log-${token}.log`;
4670
+ }
4375
4671
  // src/step-invocation/invoker.ts
4376
4672
  import { randomBytes as randomBytes2 } from "node:crypto";
4377
4673
  import { z as z11 } from "zod";
@@ -4478,28 +4774,15 @@ function parseStepResult(stdout, resultToken) {
4478
4774
  const message = baseMessage;
4479
4775
  return { ok: false, error: { kind, message } };
4480
4776
  }
4481
- async function invokeStep(sandbox, request, opts) {
4482
- const resultToken = randomBytes2(16).toString("hex");
4483
- await Promise.all([
4484
- sandbox.files.write(stepInputPath(request.stepIndex), JSON.stringify(request.input)),
4485
- sandbox.files.write(requestContextPath(request.stepIndex), JSON.stringify(request.requestContext))
4486
- ]);
4487
- const envs = {
4488
- ...opts?.envs ?? {},
4489
- ...buildStepEnvs({
4490
- runId: request.runId,
4491
- stepIndex: request.stepIndex,
4492
- resultToken,
4493
- isResume: opts?.isResume
4494
- })
4495
- };
4777
+ function makeStreamSplitters(opts, resultToken) {
4496
4778
  const resultSentinel = stepResultLinePrefix(resultToken);
4497
4779
  const pauseSentinel = stepPauseLinePrefix(resultToken);
4498
4780
  const isSentinel = (line) => line.startsWith(resultSentinel) || line.startsWith(pauseSentinel);
4499
- const makeLineSplitter = (sink, filterSentinel) => {
4781
+ const make = (sink, filterSentinel) => {
4500
4782
  if (!sink)
4501
- return { onChunk: undefined, flush: () => {} };
4783
+ return { onChunk: undefined, flush: () => {}, consumed: () => 0 };
4502
4784
  let buf = "";
4785
+ let consumed = 0;
4503
4786
  return {
4504
4787
  onChunk: (chunk) => {
4505
4788
  buf += chunk;
@@ -4508,6 +4791,7 @@ async function invokeStep(sandbox, request, opts) {
4508
4791
  `)) !== -1) {
4509
4792
  const line = buf.slice(0, nl);
4510
4793
  buf = buf.slice(nl + 1);
4794
+ consumed += line.length + 1;
4511
4795
  if (filterSentinel && isSentinel(line))
4512
4796
  continue;
4513
4797
  sink(line);
@@ -4518,35 +4802,28 @@ async function invokeStep(sandbox, request, opts) {
4518
4802
  return;
4519
4803
  const line = buf;
4520
4804
  buf = "";
4805
+ consumed += line.length;
4521
4806
  if (filterSentinel && isSentinel(line))
4522
4807
  return;
4523
4808
  sink(line);
4524
- }
4809
+ },
4810
+ consumed: () => consumed
4525
4811
  };
4526
4812
  };
4527
- const stdoutSplitter = makeLineSplitter(opts?.onStdout, true);
4528
- const stderrSplitter = makeLineSplitter(opts?.onStderr, false);
4529
- let result;
4530
- try {
4531
- result = await sandbox.commands.run(RUNNER_COMMAND, {
4532
- envs,
4533
- timeoutMs: 0,
4534
- ...stdoutSplitter.onChunk ? { onStdout: stdoutSplitter.onChunk } : {},
4535
- ...stderrSplitter.onChunk ? { onStderr: stderrSplitter.onChunk } : {}
4536
- });
4537
- } catch (e) {
4538
- if (e instanceof SandboxUnavailableError)
4539
- throw e;
4540
- const ce = e;
4541
- result = {
4542
- stdout: ce.stdout ?? ce.result?.stdout ?? "",
4543
- stderr: ce.stderr ?? ce.result?.stderr ?? (e instanceof Error ? e.message : String(e)),
4544
- exitCode: ce.exitCode ?? ce.result?.exitCode ?? 1
4545
- };
4546
- }
4547
- stdoutSplitter.flush();
4548
- stderrSplitter.flush();
4549
- const parsed = parseStepResult(result.stdout, resultToken);
4813
+ const stdout = make(opts?.onStdout, true);
4814
+ const stderr = make(opts?.onStderr, false);
4815
+ return {
4816
+ ...stdout.onChunk ? { onStdout: stdout.onChunk } : {},
4817
+ ...stderr.onChunk ? { onStderr: stderr.onChunk } : {},
4818
+ flush: () => {
4819
+ stdout.flush();
4820
+ stderr.flush();
4821
+ },
4822
+ stdoutConsumedChars: () => stdout.consumed()
4823
+ };
4824
+ }
4825
+ async function classifyRunnerOutcome(sandbox, output, resultToken) {
4826
+ const parsed = parseStepResult(output.stdout, resultToken);
4550
4827
  if (parsed)
4551
4828
  return parsed;
4552
4829
  const fileRead = await sandbox.commands.run(`cat ${stepResultFilePath(resultToken)} 2>/dev/null || true`).catch(() => null);
@@ -4555,9 +4832,9 @@ async function invokeStep(sandbox, request, opts) {
4555
4832
  if (fromFile)
4556
4833
  return fromFile;
4557
4834
  }
4558
- if (result.exitCode !== 0) {
4559
- const stderrTail = result.stderr.trim().slice(-2000);
4560
- const stdoutTail = result.stdout.trim().slice(-2000);
4835
+ if (output.exitCode !== 0) {
4836
+ const stderrTail = output.stderr.trim().slice(-2000);
4837
+ const stdoutTail = output.stdout.trim().slice(-2000);
4561
4838
  const details = [
4562
4839
  stderrTail ? `stderr:
4563
4840
  ${stderrTail}` : "",
@@ -4569,13 +4846,13 @@ ${stdoutTail}` : ""
4569
4846
  ok: false,
4570
4847
  error: {
4571
4848
  kind: "runner-exit",
4572
- message: `runner subprocess exited ${result.exitCode} before emitting a step result${details ? `
4849
+ message: `runner subprocess exited ${output.exitCode} before emitting a step result${details ? `
4573
4850
  ${details}` : ""}`,
4574
- exitCode: result.exitCode
4851
+ exitCode: output.exitCode
4575
4852
  }
4576
4853
  };
4577
4854
  }
4578
- const tail = result.stdout.trim().slice(-500);
4855
+ const tail = output.stdout.trim().slice(-500);
4579
4856
  return {
4580
4857
  ok: false,
4581
4858
  error: {
@@ -4586,6 +4863,157 @@ ${tail}` : ""}`
4586
4863
  }
4587
4864
  };
4588
4865
  }
4866
+ async function pollDurableResult(sandbox, runnerPid, resultToken, signal) {
4867
+ const resultFile = stepResultFilePath(resultToken);
4868
+ const POLL_MS = 1000;
4869
+ const DEADLINE = Date.now() + 45 * 60000;
4870
+ 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;
4873
+ };
4874
+ for (let attempt = 1;; attempt++) {
4875
+ if (signal?.aborted) {
4876
+ return { ok: false, error: { kind: "runner-exit", message: "reconnectStep aborted (run re-paused)", exitCode: 1 } };
4877
+ }
4878
+ const fromFile = await readResult();
4879
+ if (fromFile)
4880
+ return fromFile;
4881
+ const probe = await sandbox.commands.run(`test -d /proc/${runnerPid} && echo alive || echo dead`).catch(() => null);
4882
+ const dead = (probe?.stdout ?? "").includes("dead");
4883
+ process.stderr.write(`[recover] poll ${attempt}: result-file=absent runner=${dead ? "dead" : "alive"} pid=${runnerPid}
4884
+ `);
4885
+ if (dead) {
4886
+ const finalRead = await readResult();
4887
+ if (finalRead)
4888
+ return finalRead;
4889
+ return {
4890
+ ok: false,
4891
+ error: { kind: "runner-exit", message: `runner pid ${runnerPid} exited without writing a result file`, exitCode: 1 }
4892
+ };
4893
+ }
4894
+ if (Date.now() > DEADLINE) {
4895
+ return {
4896
+ ok: false,
4897
+ error: { kind: "runner-exit", message: `timed out after 45m waiting for runner pid ${runnerPid} to emit a result`, exitCode: 1 }
4898
+ };
4899
+ }
4900
+ await new Promise((r) => setTimeout(r, POLL_MS));
4901
+ }
4902
+ }
4903
+ async function recoverLogsAndResult(sandbox, runnerPid, resultToken, liveSplitters, opts) {
4904
+ const result = await pollDurableResult(sandbox, runnerPid, resultToken);
4905
+ if (sandbox.files.read && opts?.onStdout) {
4906
+ const fullLog = await sandbox.files.read(stepLogFilePath(resultToken)).catch(() => null);
4907
+ if (fullLog != null) {
4908
+ const already = liveSplitters.stdoutConsumedChars();
4909
+ const tail = fullLog.length > already ? fullLog.slice(already) : "";
4910
+ if (tail.length > 0) {
4911
+ const backfill = makeStreamSplitters(opts, resultToken);
4912
+ backfill.onStdout?.(tail);
4913
+ backfill.flush();
4914
+ }
4915
+ }
4916
+ }
4917
+ return result;
4918
+ }
4919
+ async function prepareStepLaunch(sandbox, request, opts) {
4920
+ const resultToken = randomBytes2(16).toString("hex");
4921
+ await Promise.all([
4922
+ sandbox.files.write(stepInputPath(request.stepIndex), JSON.stringify(request.input)),
4923
+ sandbox.files.write(requestContextPath(request.stepIndex), JSON.stringify(request.requestContext))
4924
+ ]);
4925
+ const envs = {
4926
+ ...opts?.envs ?? {},
4927
+ ...buildStepEnvs({ runId: request.runId, stepIndex: request.stepIndex, resultToken, isResume: opts?.isResume })
4928
+ };
4929
+ return { resultToken, envs };
4930
+ }
4931
+ async function invokeStep(sandbox, request, opts) {
4932
+ const { resultToken, envs } = await prepareStepLaunch(sandbox, request, opts);
4933
+ const splitters = makeStreamSplitters(opts, resultToken);
4934
+ let result;
4935
+ try {
4936
+ result = await sandbox.commands.run(RUNNER_COMMAND, {
4937
+ envs: { ...envs, HOME: "/root", IS_SANDBOX: "1" },
4938
+ sudo: true,
4939
+ timeoutMs: 0,
4940
+ ...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
4941
+ ...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
4942
+ });
4943
+ } catch (e) {
4944
+ if (e instanceof SandboxUnavailableError)
4945
+ throw e;
4946
+ const ce = e;
4947
+ result = {
4948
+ stdout: ce.stdout ?? ce.result?.stdout ?? "",
4949
+ stderr: ce.stderr ?? ce.result?.stderr ?? (e instanceof Error ? e.message : String(e)),
4950
+ exitCode: ce.exitCode ?? ce.result?.exitCode ?? 1
4951
+ };
4952
+ }
4953
+ splitters.flush();
4954
+ return classifyRunnerOutcome(sandbox, result, resultToken);
4955
+ }
4956
+ async function launchStep(sandbox, request, opts) {
4957
+ if (!sandbox.commands.runBackground) {
4958
+ throw new Error("launchStep requires a provider with background-command support (commands.runBackground)");
4959
+ }
4960
+ const { resultToken, envs } = await prepareStepLaunch(sandbox, request, opts);
4961
+ const splitters = makeStreamSplitters(opts, resultToken);
4962
+ const proc = await sandbox.commands.runBackground(`set -o pipefail; ${RUNNER_COMMAND} | tee ${stepLogFilePath(resultToken)}`, {
4963
+ envs: { ...envs, HOME: "/root", IS_SANDBOX: "1" },
4964
+ sudo: true,
4965
+ timeoutMs: 0,
4966
+ ...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
4967
+ ...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
4968
+ });
4969
+ return {
4970
+ runnerPid: proc.pid,
4971
+ resultToken,
4972
+ async wait() {
4973
+ try {
4974
+ const result = await proc.wait();
4975
+ splitters.flush();
4976
+ return classifyRunnerOutcome(sandbox, result, resultToken);
4977
+ } catch (e) {
4978
+ if (e instanceof SandboxUnavailableError)
4979
+ throw e;
4980
+ opts?.onStreamDegraded?.({ error: e, runnerPid: proc.pid, resultToken });
4981
+ return recoverLogsAndResult(sandbox, proc.pid, resultToken, splitters, opts);
4982
+ }
4983
+ }
4984
+ };
4985
+ }
4986
+ async function reconnectStep(sandbox, resume, opts) {
4987
+ if (!sandbox.commands.connectProcess) {
4988
+ throw new Error("reconnectStep requires a provider with background-command support (commands.connectProcess)");
4989
+ }
4990
+ const { runnerPid, resultToken } = resume;
4991
+ const splitters = makeStreamSplitters(opts, resultToken);
4992
+ const signal = opts?.signal;
4993
+ let attached = null;
4994
+ try {
4995
+ attached = await sandbox.commands.connectProcess(runnerPid, {
4996
+ ...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
4997
+ ...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
4998
+ });
4999
+ } catch (e) {
5000
+ if (e instanceof SandboxUnavailableError)
5001
+ throw e;
5002
+ }
5003
+ return {
5004
+ runnerPid,
5005
+ resultToken,
5006
+ async wait() {
5007
+ try {
5008
+ const result = await pollDurableResult(sandbox, runnerPid, resultToken, signal);
5009
+ splitters.flush();
5010
+ return result;
5011
+ } finally {
5012
+ await attached?.kill().catch(() => {});
5013
+ }
5014
+ }
5015
+ };
5016
+ }
4589
5017
  // src/step-invocation/server.ts
4590
5018
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4591
5019
  function writeSentinelLine(line) {
@@ -4692,41 +5120,6 @@ async function serveStep(handler) {
4692
5120
  await emitResult(token, payload);
4693
5121
  process.exit(exitCode);
4694
5122
  }
4695
- // src/agent/local-pause-request.ts
4696
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
4697
- import { join as join3 } from "node:path";
4698
- import { randomBytes as randomBytes3 } from "node:crypto";
4699
- var UNSCOPED = "_unscoped";
4700
- var requestsDir = () => join3(getStateDir(), "pause-requests");
4701
- var markerPath = (agentId) => join3(requestsDir(), `${slug(agentId) || UNSCOPED}.json`);
4702
- function slug(agentId) {
4703
- return (agentId ?? "").replace(/[^a-zA-Z0-9_.-]/g, "_");
4704
- }
4705
- function writeLocalPauseRequest(agentId, req) {
4706
- mkdirSync2(requestsDir(), { recursive: true });
4707
- const final = markerPath(agentId);
4708
- const tmp = `${final}.${randomBytes3(6).toString("hex")}.tmp`;
4709
- writeFileSync2(tmp, JSON.stringify(req), "utf8");
4710
- renameSync(tmp, final);
4711
- }
4712
- function consumeLocalPauseRequest(agentId) {
4713
- const paths = [...new Set([markerPath(agentId), markerPath(null)])];
4714
- for (const path of paths) {
4715
- if (!existsSync(path))
4716
- continue;
4717
- try {
4718
- const raw = JSON.parse(readFileSync2(path, "utf8"));
4719
- rmSync(path, { force: true });
4720
- if (raw && typeof raw === "object" && typeof raw.reason === "string" && raw.reason.trim().length > 0) {
4721
- const r = raw;
4722
- return { reason: r.reason.trim(), ...Array.isArray(r.options) && r.options.length > 0 ? { options: r.options } : {} };
4723
- }
4724
- } catch {
4725
- rmSync(path, { force: true });
4726
- }
4727
- }
4728
- return null;
4729
- }
4730
5123
  // src/agent/run-agent.ts
4731
5124
  import { z as z12 } from "zod";
4732
5125
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -4782,29 +5175,36 @@ Names like \`note.created\` / \`brief.posted\` surface in the Workbench;
4782
5175
 
4783
5176
  ## Writing workflow / agent code — the SDK
4784
5177
 
4785
- \`@agent-compose/sdk\` is installed in \`/workspace\` — import it from any script
4786
- you write there:
4787
-
4788
- import { defineWorkflow, agent, AgentComposeClient } from "@agent-compose/sdk";
5178
+ \`@agent-compose/sdk\` is installed in \`/workspace\`. **To author a workflow,
5179
+ ALWAYS run \`/ac:generate-workflow\`** (and \`/ac:generate-agent\` for an agent
5180
+ step) instead of writing source from memory — the skill scaffolds the correct,
5181
+ current shape. Then \`agentc register <file.ts>\` (or \`/ac:register\`).
4789
5182
 
4790
- Use \`/ac:generate-workflow\` / \`/ac:generate-agent\` to scaffold, then
4791
- \`agentc register <file.ts>\` (or \`/ac:register\`).
5183
+ 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**.
4792
5187
 
4793
- ## Pausing to ask the human — \`agentc pause\`
5188
+ ## Pausing to ask the human
4794
5189
 
4795
- When you can't or shouldn't proceed without a human, run \`agentc pause\`, then
4796
- **END YOUR TURN**:
5190
+ To ask a human and get an answer back, use the **\`AskUserQuestion\`** tool if
5191
+ you have it; otherwise run **\`agentc pause\`**:
4797
5192
 
4798
5193
  agentc pause --reason "Notion returned 401 — connect Notion to continue" \\
4799
5194
  --option retry --option skip
4800
5195
 
4801
- \`agentc pause\` does NOT block and does NOT print the answer. It records your
4802
- question and returns immediately. The moment you end your turn, the run pauses
4803
- (your sandbox is snapshotted and compute stops while the human decides) and the
4804
- human's answer is delivered to you as your **next message** — you pick up
4805
- exactly where you left off, with the answer in hand. So: ask, end your turn,
4806
- and wait. Do NOT keep working, do NOT call more tools, and do NOT mark the task
4807
- complete after pausing.
5196
+ **Both BLOCK and hand you the answer inline.** While you wait, the run is
5197
+ suspended — your sandbox is frozen and compute stops, so a pause is free while
5198
+ the human decides. When they answer, the call RETURNS with their decision: the
5199
+ \`AskUserQuestion\` tool result, or \`agentc pause\`'s output
5200
+ (\`▶ Resumed. The human answered: …\`), carries it.
5201
+
5202
+ **Then USE that answer to finish your work — do NOT end your turn.** This is NOT
5203
+ fire-and-forget, and the answer does NOT arrive in a later message: it comes
5204
+ back right where you called it, on the SAME turn. The shape is: ask → the call
5205
+ blocks → it returns the human's answer → you act on it and produce your result.
5206
+ Never end your turn before the call returns, never guess an answer, and never
5207
+ proceed without one.
4808
5208
 
4809
5209
  Reach for it the moment you hit — or foresee — any of these:
4810
5210
  - **A wall only a human can clear:** a 401/403, a missing credential, an
@@ -5053,9 +5453,10 @@ async function agent(opts) {
5053
5453
  });
5054
5454
  } : undefined;
5055
5455
  const mode = opts.mode ?? "auto";
5456
+ const processors = [createAskHumanProcessor(), ...opts.processors ?? []];
5056
5457
  try {
5057
5458
  return await agentLoop({
5058
- runtime: (runtimeOpts) => opts.runtime.create(opts.sandbox, runtimeOpts),
5459
+ runtime: (runtimeOpts) => opts.runtime.create(opts.sandbox, { ...runtimeOpts, agentManual: buildAgentContextDoc(process.env) }),
5059
5460
  agentId,
5060
5461
  ...opts.label !== undefined ? { label: opts.label } : {},
5061
5462
  ...opts.budget?.turnsPerIteration !== undefined ? { turnsPerIteration: opts.budget.turnsPerIteration } : {},
@@ -5081,7 +5482,7 @@ async function agent(opts) {
5081
5482
  } } : {},
5082
5483
  ...opts.onAgentEvent ? { onAgentEvent: opts.onAgentEvent } : {},
5083
5484
  ...opts.onIteration ? { onIteration: opts.onIteration } : {},
5084
- ...opts.processors?.length ? { processors: opts.processors } : {},
5485
+ ...processors.length ? { processors } : {},
5085
5486
  requestContext: opts.requestContext ?? RequestContext.fromReserved({
5086
5487
  teamId: "",
5087
5488
  runId: "",
@@ -5093,7 +5494,6 @@ async function agent(opts) {
5093
5494
  ...inbox ? { inbox } : {},
5094
5495
  mode,
5095
5496
  consumeSteerPending: () => consumeSteerPending(agentId),
5096
- consumeLocalPauseRequest: () => consumeLocalPauseRequest(agentId),
5097
5497
  ...steerPause ? { pause: steerPause } : {}
5098
5498
  });
5099
5499
  } finally {