@agent-compose/sdk 0.6.0 → 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.
@@ -0,0 +1,9 @@
1
+ import { type CliAgentSpec } from "./_cli-agent.js";
2
+ export declare const cursorSpec: CliAgentSpec;
3
+ export interface CursorRuntimeConfig {
4
+ /** Cursor model id; ACP mode ignores it (account default) — applies to the `-p` fallback. */
5
+ model?: string;
6
+ }
7
+ export declare function createCursorRuntime(config?: CursorRuntimeConfig): import("../index.js").AgentRuntime<import("../index.js").SandboxProvider>;
8
+ declare const _default: import("../index.js").AgentRuntime<import("../index.js").SandboxProvider>;
9
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { type CliAgentSpec } from "./_cli-agent.js";
2
+ export declare const droidSpec: CliAgentSpec;
3
+ export interface DroidRuntimeConfig {
4
+ /** `custom:<displayName>-<index>` matching the provisioned settings.json. */
5
+ model?: string;
6
+ }
7
+ export declare function createDroidRuntime(config?: DroidRuntimeConfig): import("../index.js").AgentRuntime<import("../index.js").SandboxProvider>;
8
+ declare const _default: import("../index.js").AgentRuntime<import("../index.js").SandboxProvider>;
9
+ export default _default;
@@ -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
@@ -1195,6 +1205,74 @@ function createGatePauseProcessor(opts) {
1195
1205
  }
1196
1206
  };
1197
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
+ }
1198
1276
  // src/client.ts
1199
1277
  import { ofetch } from "ofetch";
1200
1278
 
@@ -2075,7 +2153,7 @@ async function agentLoop(opts) {
2075
2153
  const logLabel = opts.label ?? "[Agent Loop]";
2076
2154
  const startedAt = Date.now();
2077
2155
  const turnsPerIteration = opts.turnsPerIteration;
2078
- const maxIterations = opts.maxIterations ?? (turnsPerIteration === undefined ? 1 : 8);
2156
+ const maxIterations = opts.maxIterations ?? (turnsPerIteration === undefined ? 3 : 8);
2079
2157
  let schemaRetriesLeft = 10;
2080
2158
  const processors = opts.processors ?? [];
2081
2159
  const requestContext = opts.requestContext ?? RequestContext.fromReserved({
@@ -2937,6 +3015,7 @@ function shellQuote(value) {
2937
3015
  }
2938
3016
  var ACP_FALLBACK = Symbol("acp-fallback");
2939
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;
2940
3019
  var ACP_TRANSPORT_READY = true;
2941
3020
  async function withHandshakeTimeout(p, ms) {
2942
3021
  let timer;
@@ -3104,7 +3183,7 @@ class CliAgentRunner {
3104
3183
  watchdog = setTimeout(() => {
3105
3184
  stalled = true;
3106
3185
  peer.cancel();
3107
- }, ACP_HANDSHAKE_TIMEOUT_MS);
3186
+ }, ACP_TURN_IDLE_TIMEOUT_MS);
3108
3187
  };
3109
3188
  try {
3110
3189
  armWatchdog();
@@ -3121,7 +3200,7 @@ class CliAgentRunner {
3121
3200
  if (stalled && !sawError) {
3122
3201
  await this.teardownAcp(proc, peer);
3123
3202
  this.acpSession = undefined;
3124
- 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() };
3125
3204
  return;
3126
3205
  }
3127
3206
  if (!sawError)
@@ -3366,6 +3445,81 @@ function createAmpRuntime(config = {}) {
3366
3445
  return createCliAgentRuntime(ampSpec, config.model);
3367
3446
  }
3368
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();
3369
3523
  // src/sandbox.ts
3370
3524
  import { promises as fs2 } from "node:fs";
3371
3525
  import { dirname as dirname2 } from "node:path";
@@ -3458,6 +3612,9 @@ function makeSandboxProvider(sb) {
3458
3612
  files: {
3459
3613
  async write(path, content) {
3460
3614
  await sb.files.write(path, content);
3615
+ },
3616
+ async read(path) {
3617
+ return await sb.files.read(path);
3461
3618
  }
3462
3619
  },
3463
3620
  async kill() {
@@ -3805,6 +3962,9 @@ function makeLocalSandboxProvider() {
3805
3962
  async write(path, content) {
3806
3963
  await fs2.mkdir(dirname2(path), { recursive: true });
3807
3964
  await fs2.writeFile(path, content);
3965
+ },
3966
+ async read(path) {
3967
+ return await fs2.readFile(path, "utf8");
3808
3968
  }
3809
3969
  },
3810
3970
  async kill() {}
@@ -4505,6 +4665,9 @@ function requestContextPath(stepIndex) {
4505
4665
  function stepResultFilePath(token) {
4506
4666
  return `/tmp/wf/step-result-${token}.json`;
4507
4667
  }
4668
+ function stepLogFilePath(token) {
4669
+ return `/tmp/wf/step-log-${token}.log`;
4670
+ }
4508
4671
  // src/step-invocation/invoker.ts
4509
4672
  import { randomBytes as randomBytes2 } from "node:crypto";
4510
4673
  import { z as z11 } from "zod";
@@ -4617,8 +4780,9 @@ function makeStreamSplitters(opts, resultToken) {
4617
4780
  const isSentinel = (line) => line.startsWith(resultSentinel) || line.startsWith(pauseSentinel);
4618
4781
  const make = (sink, filterSentinel) => {
4619
4782
  if (!sink)
4620
- return { onChunk: undefined, flush: () => {} };
4783
+ return { onChunk: undefined, flush: () => {}, consumed: () => 0 };
4621
4784
  let buf = "";
4785
+ let consumed = 0;
4622
4786
  return {
4623
4787
  onChunk: (chunk) => {
4624
4788
  buf += chunk;
@@ -4627,6 +4791,7 @@ function makeStreamSplitters(opts, resultToken) {
4627
4791
  `)) !== -1) {
4628
4792
  const line = buf.slice(0, nl);
4629
4793
  buf = buf.slice(nl + 1);
4794
+ consumed += line.length + 1;
4630
4795
  if (filterSentinel && isSentinel(line))
4631
4796
  continue;
4632
4797
  sink(line);
@@ -4637,10 +4802,12 @@ function makeStreamSplitters(opts, resultToken) {
4637
4802
  return;
4638
4803
  const line = buf;
4639
4804
  buf = "";
4805
+ consumed += line.length;
4640
4806
  if (filterSentinel && isSentinel(line))
4641
4807
  return;
4642
4808
  sink(line);
4643
- }
4809
+ },
4810
+ consumed: () => consumed
4644
4811
  };
4645
4812
  };
4646
4813
  const stdout = make(opts?.onStdout, true);
@@ -4651,7 +4818,8 @@ function makeStreamSplitters(opts, resultToken) {
4651
4818
  flush: () => {
4652
4819
  stdout.flush();
4653
4820
  stderr.flush();
4654
- }
4821
+ },
4822
+ stdoutConsumedChars: () => stdout.consumed()
4655
4823
  };
4656
4824
  }
4657
4825
  async function classifyRunnerOutcome(sandbox, output, resultToken) {
@@ -4695,6 +4863,59 @@ ${tail}` : ""}`
4695
4863
  }
4696
4864
  };
4697
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
+ }
4698
4919
  async function prepareStepLaunch(sandbox, request, opts) {
4699
4920
  const resultToken = randomBytes2(16).toString("hex");
4700
4921
  await Promise.all([
@@ -4713,7 +4934,8 @@ async function invokeStep(sandbox, request, opts) {
4713
4934
  let result;
4714
4935
  try {
4715
4936
  result = await sandbox.commands.run(RUNNER_COMMAND, {
4716
- envs,
4937
+ envs: { ...envs, HOME: "/root", IS_SANDBOX: "1" },
4938
+ sudo: true,
4717
4939
  timeoutMs: 0,
4718
4940
  ...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
4719
4941
  ...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
@@ -4737,8 +4959,9 @@ async function launchStep(sandbox, request, opts) {
4737
4959
  }
4738
4960
  const { resultToken, envs } = await prepareStepLaunch(sandbox, request, opts);
4739
4961
  const splitters = makeStreamSplitters(opts, resultToken);
4740
- const proc = await sandbox.commands.runBackground(RUNNER_COMMAND, {
4741
- envs,
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,
4742
4965
  timeoutMs: 0,
4743
4966
  ...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
4744
4967
  ...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
@@ -4747,9 +4970,16 @@ async function launchStep(sandbox, request, opts) {
4747
4970
  runnerPid: proc.pid,
4748
4971
  resultToken,
4749
4972
  async wait() {
4750
- const result = await proc.wait();
4751
- splitters.flush();
4752
- return classifyRunnerOutcome(sandbox, result, resultToken);
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
+ }
4753
4983
  }
4754
4984
  };
4755
4985
  }
@@ -4759,9 +4989,10 @@ async function reconnectStep(sandbox, resume, opts) {
4759
4989
  }
4760
4990
  const { runnerPid, resultToken } = resume;
4761
4991
  const splitters = makeStreamSplitters(opts, resultToken);
4762
- let proc = null;
4992
+ const signal = opts?.signal;
4993
+ let attached = null;
4763
4994
  try {
4764
- proc = await sandbox.commands.connectProcess(runnerPid, {
4995
+ attached = await sandbox.commands.connectProcess(runnerPid, {
4765
4996
  ...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
4766
4997
  ...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
4767
4998
  });
@@ -4769,22 +5000,17 @@ async function reconnectStep(sandbox, resume, opts) {
4769
5000
  if (e instanceof SandboxUnavailableError)
4770
5001
  throw e;
4771
5002
  }
4772
- const attached = proc;
4773
5003
  return {
4774
5004
  runnerPid,
4775
5005
  resultToken,
4776
5006
  async wait() {
4777
- let output = { stdout: "", stderr: "", exitCode: 0 };
4778
- if (attached) {
4779
- try {
4780
- output = await attached.wait();
4781
- } catch (e) {
4782
- if (e instanceof SandboxUnavailableError)
4783
- throw e;
4784
- }
5007
+ try {
5008
+ const result = await pollDurableResult(sandbox, runnerPid, resultToken, signal);
5009
+ splitters.flush();
5010
+ return result;
5011
+ } finally {
5012
+ await attached?.kill().catch(() => {});
4785
5013
  }
4786
- splitters.flush();
4787
- return classifyRunnerOutcome(sandbox, output, resultToken);
4788
5014
  }
4789
5015
  };
4790
5016
  }
@@ -4949,29 +5175,36 @@ Names like \`note.created\` / \`brief.posted\` surface in the Workbench;
4949
5175
 
4950
5176
  ## Writing workflow / agent code — the SDK
4951
5177
 
4952
- \`@agent-compose/sdk\` is installed in \`/workspace\` — import it from any script
4953
- you write there:
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\`).
4954
5182
 
4955
- import { defineWorkflow, agent, AgentComposeClient } from "@agent-compose/sdk";
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**.
4956
5187
 
4957
- Use \`/ac:generate-workflow\` / \`/ac:generate-agent\` to scaffold, then
4958
- \`agentc register <file.ts>\` (or \`/ac:register\`).
5188
+ ## Pausing to ask the human
4959
5189
 
4960
- ## Pausing to ask the human — \`agentc pause\`
4961
-
4962
- When you can't or shouldn't proceed without a human, run \`agentc pause\`, then
4963
- **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\`**:
4964
5192
 
4965
5193
  agentc pause --reason "Notion returned 401 — connect Notion to continue" \\
4966
5194
  --option retry --option skip
4967
5195
 
4968
- \`agentc pause\` does NOT block and does NOT print the answer. It records your
4969
- question and returns immediately. The moment you end your turn, the run pauses
4970
- (your sandbox is snapshotted and compute stops while the human decides) and the
4971
- human's answer is delivered to you as your **next message** — you pick up
4972
- exactly where you left off, with the answer in hand. So: ask, end your turn,
4973
- and wait. Do NOT keep working, do NOT call more tools, and do NOT mark the task
4974
- complete after pausing.
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.
4975
5208
 
4976
5209
  Reach for it the moment you hit — or foresee — any of these:
4977
5210
  - **A wall only a human can clear:** a 401/403, a missing credential, an
@@ -5220,6 +5453,7 @@ async function agent(opts) {
5220
5453
  });
5221
5454
  } : undefined;
5222
5455
  const mode = opts.mode ?? "auto";
5456
+ const processors = [createAskHumanProcessor(), ...opts.processors ?? []];
5223
5457
  try {
5224
5458
  return await agentLoop({
5225
5459
  runtime: (runtimeOpts) => opts.runtime.create(opts.sandbox, { ...runtimeOpts, agentManual: buildAgentContextDoc(process.env) }),
@@ -5248,7 +5482,7 @@ async function agent(opts) {
5248
5482
  } } : {},
5249
5483
  ...opts.onAgentEvent ? { onAgentEvent: opts.onAgentEvent } : {},
5250
5484
  ...opts.onIteration ? { onIteration: opts.onIteration } : {},
5251
- ...opts.processors?.length ? { processors: opts.processors } : {},
5485
+ ...processors.length ? { processors } : {},
5252
5486
  requestContext: opts.requestContext ?? RequestContext.fromReserved({
5253
5487
  teamId: "",
5254
5488
  runId: "",
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenCode CLI runtime — drives sst's `opencode` agentic CLI inside the sandbox.
3
+ * OpenCode speaks ACP natively (`opencode acp`, protocolVersion 1 — verified
4
+ * live on E2B), so the runner drives it over ACP; the JSONL members below are
5
+ * only the version-mismatch fallback (vestigial for a v1 agent).
6
+ *
7
+ * Auth + model via OpenRouter: set OPENROUTER_API_KEY (a factory/workflow
8
+ * secret) and use a model id like `openrouter/z-ai/glm-5.2`. The runtime
9
+ * installs the `opencode-ai` CLI on demand; pair with
10
+ * `snapshots: { bootFrom: "reuse" }` to install once and boot from the capture.
11
+ *
12
+ * Verified live on E2B (2026-06-30): `npm i -g opencode-ai` (v1.17.12);
13
+ * `opencode run --model openrouter/z-ai/glm-5.2` drove a GLM-5.2 turn through
14
+ * OpenRouter; `opencode acp` answered the ACP `initialize` handshake with
15
+ * protocolVersion 1.
16
+ */
17
+ import { type CliAgentSpec } from "./_cli-agent.js";
18
+ export declare const opencodeSpec: CliAgentSpec;
19
+ export interface OpencodeRuntimeConfig {
20
+ /** OpenRouter-prefixed model id (default `openrouter/z-ai/glm-5.2`). */
21
+ model?: string;
22
+ }
23
+ export declare function createOpencodeRuntime(config?: OpencodeRuntimeConfig): import("../index.js").AgentRuntime<import("../index.js").SandboxProvider>;
24
+ declare const _default: import("../index.js").AgentRuntime<import("../index.js").SandboxProvider>;
25
+ export default _default;
@@ -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
@@ -64,6 +64,17 @@ export interface InvokeStepOptions {
64
64
  * the caller's job; the activity batches and inserts at step completion. */
65
65
  onStdout?: (line: string) => void;
66
66
  onStderr?: (line: string) => void;
67
+ /** Called when the LIVE output stream fails mid-run (e.g. E2B's connect-web
68
+ * transport throws `received unsupported compressed output` on a large
69
+ * compressed frame) and the invoker falls back to recovering the result +
70
+ * full logs from the durable token-keyed files. The run is NOT failed — this
71
+ * is the hook to emit a structured alert so the degradation is visible/paged.
72
+ * Best-effort: keep it cheap and non-throwing. */
73
+ onStreamDegraded?: (info: {
74
+ error: unknown;
75
+ runnerPid: number;
76
+ resultToken: string;
77
+ }) => void;
67
78
  }
68
79
  /** A step running as a background command (ADR-0028). The activity races its
69
80
  * `wait()` against a server pause request; on a pause it freezes the VM
@@ -101,4 +112,6 @@ export declare function reconnectStep<TOutput = unknown>(sandbox: SandboxProvide
101
112
  runnerPid: number;
102
113
  resultToken: string;
103
114
  stepIndex: number;
104
- }, opts?: Pick<InvokeStepOptions, "onStdout" | "onStderr">): Promise<RunningStep<TOutput>>;
115
+ }, opts?: Pick<InvokeStepOptions, "onStdout" | "onStderr"> & {
116
+ signal?: AbortSignal;
117
+ }): Promise<RunningStep<TOutput>>;
@@ -60,3 +60,11 @@ export declare function requestContextPath(stepIndex: number): string;
60
60
  * drop the tail of a heavy stdout stream — the invoker falls back to
61
61
  * reading this file when no sentinel is found on stdout. */
62
62
  export declare function stepResultFilePath(token: string): string;
63
+ /** Sandbox-side path where the runner's stdout is tee'd as a durable LOG file,
64
+ * keyed by the per-invocation token. The live output rides E2B's connect-web
65
+ * command stream, which THROWS on a compressed large frame (gRPC-web cannot
66
+ * decode message compression) and kills the feed mid-run. This file, read back
67
+ * over the envd HTTP file transport (compression-immune), lets the invoker
68
+ * recover the FULL logs after such a fault instead of losing the tail — the
69
+ * log-side analogue of `stepResultFilePath` for the result. */
70
+ export declare function stepLogFilePath(token: string): string;
@@ -114,6 +114,15 @@ export interface SandboxProvider {
114
114
  };
115
115
  files: {
116
116
  write(path: string, content: string): Promise<void>;
117
+ /** Read a file's text content over the provider's FILE transport. On E2B this
118
+ * is the envd HTTP API (`Sandbox.files.read`), a DIFFERENT transport from
119
+ * `commands` — so a large readback is immune to the connect-web gRPC
120
+ * message-compression that can abort `commands.run` output on a big frame
121
+ * ("received unsupported compressed output"). This is what lets `launchStep`
122
+ * recover the full logs + result after a live-stream fault. OPTIONAL —
123
+ * implemented where durable file-readback is needed (E2B, local); providers
124
+ * that never drive the recovery path (Vercel — foreground only) may omit it. */
125
+ read?(path: string): Promise<string>;
117
126
  };
118
127
  kill(): Promise<void>;
119
128
  /** Capture the running sandbox's state as a reusable snapshot. Vercel and E2B
@@ -90,6 +90,12 @@ export interface WorkflowDefinition<TOutput = unknown, TInput extends Record<str
90
90
  /** Same as `input`, for the workflow's return value. Captured into
91
91
  * `outputSchema` metadata and rendered in the IO panel. */
92
92
  output?: z.ZodType<TOutput>;
93
+ /**
94
+ * @deprecated Legacy run-form. Prefer step-form — the
95
+ * `.step(defineStep(...))` builder — for per-step durability/replay and
96
+ * working pause. A run-form body compiles to one opaque step
97
+ * (`compileRunForm`), so any failure/resume re-runs the whole body.
98
+ */
93
99
  run: WorkflowFn<TOutput, TInput>;
94
100
  /**
95
101
  * All snapshot config — boot source plus capture mode.
@@ -210,6 +216,12 @@ export interface WorkflowDefinition<TOutput = unknown, TInput extends Record<str
210
216
  * stores the step plan; runner subprocesses execute one step at a time
211
217
  * via the StepInvocation seam.
212
218
  */
219
+ /**
220
+ * @deprecated Run-form is legacy. Use the step-form overload —
221
+ * `defineWorkflow({ id, input, output }).step(defineStep(...)).build()` — for
222
+ * durable, replayable steps and working pause. Run-form compiles to a single
223
+ * opaque step (`compileRunForm`); there is no per-step replay.
224
+ */
213
225
  export declare function defineWorkflow<TOutput = unknown, TInput extends Record<string, unknown> = Record<string, unknown>>(def: WorkflowDefinition<TOutput, TInput>): Workflow<TInput, TOutput>;
214
226
  export declare function defineWorkflow<TInput, TOutput>(def: StepWorkflowDefinition<TInput, TOutput>): import("../workflow-steps/workflow.js").WorkflowBuilder<TInput, TInput>;
215
227
  /** Observability-only lifecycle hooks — passed to the workflow engine, not workflow authors. */
@@ -1,2 +1,10 @@
1
- /** Convert any thrown value to a string message. */
1
+ /** Convert any thrown value to a string message, INCLUDING its `.cause` chain.
2
+ *
3
+ * Many wrapped errors carry the real reason on `.cause` and only a generic
4
+ * summary on `.message` — the Temporal SDK's "Failed to start Workflow" is the
5
+ * canonical example (its `.cause` is the actual gRPC rejection, e.g. "search
6
+ * attribute X is not defined"). Returning `.message` alone swallowed that, so
7
+ * failures surfaced as opaque one-liners. Walk the chain and join the messages
8
+ * so the root cause is always visible. Cycle-guarded against self-referential
9
+ * `cause` links. */
2
10
  export declare function formatError(err: unknown): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.",
5
5
  "license": "MIT",
6
6
  "repository": {