@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.
@@ -19,7 +19,7 @@ import type { SandboxProvider } from "../types/sandbox.js";
19
19
  * that credentials are network-injected (never in the env). The live
20
20
  * "Connectors & access" section is appended per-run by `buildAgentContextDoc`.
21
21
  */
22
- export declare const AGENT_COMPOSE_MANUAL = "# Working inside an Agent Compose sandbox\n\nYou are an agent running in a per-run sandbox on the Agent Compose platform.\nUse the **`agentc` CLI** and the **`@agent-compose/sdk`** for everything below \u2014\ndo NOT hand-roll raw HTTP/curl calls against the platform API. The CLI is on\nyour PATH and already authenticated from the environment\n(`AGENT_COMPOSE_URL` / `AGENT_COMPOSE_API_KEY` / `AGENT_COMPOSE_FACTORY` are\ninjected for this run), so commands just work \u2014 no login, no keys to manage.\n\nThe `/ac:*` skills are installed as Claude Code slash commands (`/ac:invoke`,\n`/ac:events`, `/ac:logs`, `/ac:register`, \u2026) \u2014 reach for them too.\n\n## Files \u2014 your outputs persist by default\n\nYour working directory defaults to **`$AGENT_COMPOSE_RUN_DIR`** \u2014 a per-run\ndirectory on the shared factory drive\n(`$AGENT_COMPOSE_FACTORY_DIR/<workflow>/<version>/<run-id>/`) the platform\ncreates and attributes to this run. **Files you write here persist by\ndefault** \u2014 they show up in the dashboard's Files tab and the run's Artifacts\ncard, with no API calls to save them. The dir already exists and is writable.\n\nNeed throwaway scratch \u2014 heavy build output, package caches, temp files?\n`cd /tmp` (or any path outside `/factory`): anything off the factory drive is\nephemeral and discarded when the sandbox ends. In short: **stay in your working\ndir to keep something, `cd` out to throw it away.**\n\nThe whole shared drive is POSIX-mounted at `/factory`; the dashboard-visible\nroot is `$AGENT_COMPOSE_FACTORY_DIR` (`/factory/files`). Earlier versions and\nruns live in sibling dirs under\n`$AGENT_COMPOSE_FACTORY_DIR/$AGENT_COMPOSE_WORKFLOW/` \u2014 read them for prior\ncontext. Other workflows' dirs are present but not your concern.\n\n## Events \u2014 the factory timeline\n\nRecord something on the run/factory timeline (the dashboard renders these)\nwith the CLI \u2014 your run id is `$RUN_ID`:\n\n agentc events send \"$RUN_ID\" <name> --summary \"<one line>\" [--body '<json>']\n\nNames like `note.created` / `brief.posted` surface in the Workbench;\n`agentc events list` reads them back. `/ac:events` is the skill equivalent.\n\n## Runs\n\n agentc list # registered workflows (/ac:list)\n agentc logs \"$RUN_ID\" # a run's logs (/ac:logs)\n agentc invoke <workflow> -i '<json>' # dispatch a workflow (/ac:invoke)\n\n## Writing workflow / agent code \u2014 the SDK\n\n`@agent-compose/sdk` is installed in `/workspace` \u2014 import it from any script\nyou write there:\n\n import { defineWorkflow, agent, AgentComposeClient } from \"@agent-compose/sdk\";\n\nUse `/ac:generate-workflow` / `/ac:generate-agent` to scaffold, then\n`agentc register <file.ts>` (or `/ac:register`).\n\n## Pausing to ask the human \u2014 `agentc pause`\n\nWhen you can't or shouldn't proceed without a human, run `agentc pause`, then\n**END YOUR TURN**:\n\n agentc pause --reason \"Notion returned 401 \u2014 connect Notion to continue\" \\\n --option retry --option skip\n\n`agentc pause` does NOT block and does NOT print the answer. It records your\nquestion and returns immediately. The moment you end your turn, the run pauses\n(your sandbox is snapshotted and compute stops while the human decides) and the\nhuman's answer is delivered to you as your **next message** \u2014 you pick up\nexactly where you left off, with the answer in hand. So: ask, end your turn,\nand wait. Do NOT keep working, do NOT call more tools, and do NOT mark the task\ncomplete after pausing.\n\nReach for it the moment you hit \u2014 or foresee \u2014 any of these:\n- **A wall only a human can clear:** a 401/403, a missing credential, an\n unconnected provider, a host the network refuses. Do NOT retry blindly or try\n to work around it \u2014 pause and say what needs enabling.\n- **A durable or outward-facing action that needs sign-off:** registering a\n workflow, deploying, sending email/messages, deleting or overwriting shared\n data, spending money. Prepare everything, then pause for approval BEFORE you\n commit it.\n- **A judgment call only the human can settle:** an under-specified request,\n several valid paths, a conflict with existing state, missing input only they have.\n\nYou compose the `--reason` (the ask) yourself; pass `--option` choices when\nthere are clear ones, omit them for a free-form answer. Each agent pauses\nindependently \u2014 pausing doesn't stop the others.\n\n## Credentials\n\nConnector credentials (Google, GitHub, \u2026) are NEVER in your environment.\nThey're injected at the network layer when you call an allowed host \u2014 make the\nrequest **without** an Authorization header and the platform adds it. Don't try\nto read or exfiltrate tokens; they aren't here. The \"Connectors & access\"\nsection below (when present) lists exactly which providers this run can reach.\n\n## Tools in this environment\n\n- `agentc` \u2014 Agent Compose CLI (your primary interface; authed from env)\n- `@agent-compose/sdk` \u2014 installed in /workspace for writing workflows\n- `/ac:*` Claude Code skills \u2014 slash commands for the above\n- `archil` (factory drive), `rtk`, `bun`\n- A world-writable `/workspace` working directory";
22
+ export declare const AGENT_COMPOSE_MANUAL = "# Working inside an Agent Compose sandbox\n\nYou are an agent running in a per-run sandbox on the Agent Compose platform.\nUse the **`agentc` CLI** and the **`@agent-compose/sdk`** for everything below \u2014\ndo NOT hand-roll raw HTTP/curl calls against the platform API. The CLI is on\nyour PATH and already authenticated from the environment\n(`AGENT_COMPOSE_URL` / `AGENT_COMPOSE_API_KEY` / `AGENT_COMPOSE_FACTORY` are\ninjected for this run), so commands just work \u2014 no login, no keys to manage.\n\nThe `/ac:*` skills are installed as Claude Code slash commands (`/ac:invoke`,\n`/ac:events`, `/ac:logs`, `/ac:register`, \u2026) \u2014 reach for them too.\n\n## Files \u2014 your outputs persist by default\n\nYour working directory defaults to **`$AGENT_COMPOSE_RUN_DIR`** \u2014 a per-run\ndirectory on the shared factory drive\n(`$AGENT_COMPOSE_FACTORY_DIR/<workflow>/<version>/<run-id>/`) the platform\ncreates and attributes to this run. **Files you write here persist by\ndefault** \u2014 they show up in the dashboard's Files tab and the run's Artifacts\ncard, with no API calls to save them. The dir already exists and is writable.\n\nNeed throwaway scratch \u2014 heavy build output, package caches, temp files?\n`cd /tmp` (or any path outside `/factory`): anything off the factory drive is\nephemeral and discarded when the sandbox ends. In short: **stay in your working\ndir to keep something, `cd` out to throw it away.**\n\nThe whole shared drive is POSIX-mounted at `/factory`; the dashboard-visible\nroot is `$AGENT_COMPOSE_FACTORY_DIR` (`/factory/files`). Earlier versions and\nruns live in sibling dirs under\n`$AGENT_COMPOSE_FACTORY_DIR/$AGENT_COMPOSE_WORKFLOW/` \u2014 read them for prior\ncontext. Other workflows' dirs are present but not your concern.\n\n## Events \u2014 the factory timeline\n\nRecord something on the run/factory timeline (the dashboard renders these)\nwith the CLI \u2014 your run id is `$RUN_ID`:\n\n agentc events send \"$RUN_ID\" <name> --summary \"<one line>\" [--body '<json>']\n\nNames like `note.created` / `brief.posted` surface in the Workbench;\n`agentc events list` reads them back. `/ac:events` is the skill equivalent.\n\n## Runs\n\n agentc list # registered workflows (/ac:list)\n agentc logs \"$RUN_ID\" # a run's logs (/ac:logs)\n agentc invoke <workflow> -i '<json>' # dispatch a workflow (/ac:invoke)\n\n## Writing workflow / agent code \u2014 the SDK\n\n`@agent-compose/sdk` is installed in `/workspace`. **To author a workflow,\nALWAYS run `/ac:generate-workflow`** (and `/ac:generate-agent` for an agent\nstep) instead of writing source from memory \u2014 the skill scaffolds the correct,\ncurrent shape. Then `agentc register <file.ts>` (or `/ac:register`).\n\nThe skill writes **step-form** (a builder of discrete, durable `.step()`s).\nNever write the legacy run-form (`defineWorkflow({ run(ctx, sandbox) { \u2026 } })`):\nit is one opaque step, so any failure or resume re-runs the whole body \u2014 and\n**pause does not work in run-form**.\n\n## Pausing to ask the human\n\nTo ask a human and get an answer back, use the **`AskUserQuestion`** tool if\nyou have it; otherwise run **`agentc pause`**:\n\n agentc pause --reason \"Notion returned 401 \u2014 connect Notion to continue\" \\\n --option retry --option skip\n\n**Both BLOCK and hand you the answer inline.** While you wait, the run is\nsuspended \u2014 your sandbox is frozen and compute stops, so a pause is free while\nthe human decides. When they answer, the call RETURNS with their decision: the\n`AskUserQuestion` tool result, or `agentc pause`'s output\n(`\u25B6 Resumed. The human answered: \u2026`), carries it.\n\n**Then USE that answer to finish your work \u2014 do NOT end your turn.** This is NOT\nfire-and-forget, and the answer does NOT arrive in a later message: it comes\nback right where you called it, on the SAME turn. The shape is: ask \u2192 the call\nblocks \u2192 it returns the human's answer \u2192 you act on it and produce your result.\nNever end your turn before the call returns, never guess an answer, and never\nproceed without one.\n\nReach for it the moment you hit \u2014 or foresee \u2014 any of these:\n- **A wall only a human can clear:** a 401/403, a missing credential, an\n unconnected provider, a host the network refuses. Do NOT retry blindly or try\n to work around it \u2014 pause and say what needs enabling.\n- **A durable or outward-facing action that needs sign-off:** registering a\n workflow, deploying, sending email/messages, deleting or overwriting shared\n data, spending money. Prepare everything, then pause for approval BEFORE you\n commit it.\n- **A judgment call only the human can settle:** an under-specified request,\n several valid paths, a conflict with existing state, missing input only they have.\n\nYou compose the `--reason` (the ask) yourself; pass `--option` choices when\nthere are clear ones, omit them for a free-form answer. Each agent pauses\nindependently \u2014 pausing doesn't stop the others.\n\n## Credentials\n\nConnector credentials (Google, GitHub, \u2026) are NEVER in your environment.\nThey're injected at the network layer when you call an allowed host \u2014 make the\nrequest **without** an Authorization header and the platform adds it. Don't try\nto read or exfiltrate tokens; they aren't here. The \"Connectors & access\"\nsection below (when present) lists exactly which providers this run can reach.\n\n## Tools in this environment\n\n- `agentc` \u2014 Agent Compose CLI (your primary interface; authed from env)\n- `@agent-compose/sdk` \u2014 installed in /workspace for writing workflows\n- `/ac:*` Claude Code skills \u2014 slash commands for the above\n- `archil` (factory drive), `rtk`, `bun`\n- A world-writable `/workspace` working directory";
23
23
  /**
24
24
  * One connector this run can reach, as the agent should see it. Strictly
25
25
  * NON-SECRET — hosts, methods, paths, identity only. The access token is
package/dist/index.d.ts CHANGED
@@ -23,7 +23,7 @@ export type { WorkflowPlan, WorkflowStepPlan } from "./types/workflow-plan.js";
23
23
  export type { BaseExecutionContext, InvokeChild } from "./types/execution-context.js";
24
24
  export { RequestContext, ReservedKeyError, NonSerialisableValueError, AC_RESERVED_PREFIX, AC_TEAM_ID, AC_RUN_ID, AC_WORKFLOW_ID, AC_FACTORY_ID, AC_API_KEY_SCOPES, AC_PARENT_RUN_ID, AC_ABORT_SIGNAL, } from "./request-context/index.js";
25
25
  export type { RequestContextReserved, RequestContextWire, } from "./request-context/index.js";
26
- export { Verdict, runProcessorChain, denyTools, humanApproval, requireScope, redactPattern, createGatePauseProcessor, } from "./processors/index.js";
26
+ export { Verdict, runProcessorChain, denyTools, humanApproval, requireScope, redactPattern, createGatePauseProcessor, createAskHumanProcessor, ASK_USER_QUESTION_TOOL, } from "./processors/index.js";
27
27
  export type { Processor, ProcessorContext, ProcessorVerdict, ToolCall, GatePausePolicy, GatePauseApproval, GatePauseConnection, } from "./processors/index.js";
28
28
  export type { AgentMessage, AgentMessageInit, AgentMessageText, AgentMessageThinking, AgentMessageToolUse, AgentMessageToolResult, AgentMessageDone, AgentMessageError, AgentMessageUsage, AgentStatus, } from "./types/protocol.js";
29
29
  export type { SandboxProvider, DesktopSandboxProvider, } from "./types/sandbox.js";
@@ -47,6 +47,15 @@ export { default as codexRuntime } from "./runtimes/codex.js";
47
47
  export { createAmpRuntime } from "./runtimes/amp.js";
48
48
  export type { AmpRuntimeConfig } from "./runtimes/amp.js";
49
49
  export { default as ampRuntime } from "./runtimes/amp.js";
50
+ export { createOpencodeRuntime, opencodeSpec } from "./runtimes/opencode.js";
51
+ export type { OpencodeRuntimeConfig } from "./runtimes/opencode.js";
52
+ export { default as opencodeRuntime } from "./runtimes/opencode.js";
53
+ export { createCursorRuntime, cursorSpec } from "./runtimes/cursor.js";
54
+ export type { CursorRuntimeConfig } from "./runtimes/cursor.js";
55
+ export { default as cursorRuntime } from "./runtimes/cursor.js";
56
+ export { createDroidRuntime, droidSpec } from "./runtimes/droid.js";
57
+ export type { DroidRuntimeConfig } from "./runtimes/droid.js";
58
+ export { default as droidRuntime } from "./runtimes/droid.js";
50
59
  export { bashTool, codingTools, editTool, readTool, writeTool } from "./tools/index.js";
51
60
  export type { CodingTool } from "./tools/index.js";
52
61
  export type { RunEvent } from "./types/events.js";
package/dist/index.js CHANGED
@@ -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: "",
@@ -5290,6 +5524,8 @@ export {
5290
5524
  parseNameVersion,
5291
5525
  parseAgentStatus,
5292
5526
  parseAgentResponse,
5527
+ opencodeSpec,
5528
+ opencode_default as opencodeRuntime,
5293
5529
  makeSandboxProvider,
5294
5530
  makeDesktopSandboxProvider,
5295
5531
  listVercelRuntimeModels,
@@ -5309,17 +5545,25 @@ export {
5309
5545
  e2bMachineSpec,
5310
5546
  e2bBaseTemplate,
5311
5547
  e2bAgentEnvTemplate,
5548
+ droidSpec,
5549
+ droid_default as droidRuntime,
5312
5550
  denyTools,
5313
5551
  deleteSandboxSnapshot,
5314
5552
  defineWorkflow,
5315
5553
  defineStep,
5316
5554
  defineSandboxEnvironment,
5317
5555
  defineRuntime,
5556
+ cursorSpec,
5557
+ cursor_default as cursorRuntime,
5318
5558
  createVercelRuntime,
5319
5559
  createSandbox,
5560
+ createOpencodeRuntime,
5320
5561
  createGatePauseProcessor,
5562
+ createDroidRuntime,
5563
+ createCursorRuntime,
5321
5564
  createCodexRuntime,
5322
5565
  createClaudeRuntime,
5566
+ createAskHumanProcessor,
5323
5567
  createAmpRuntime,
5324
5568
  codingTools,
5325
5569
  codex_default as codexRuntime,
@@ -5369,6 +5613,7 @@ export {
5369
5613
  AgentMessageSchema,
5370
5614
  AgentComposeError,
5371
5615
  AgentComposeClient,
5616
+ ASK_USER_QUESTION_TOOL,
5372
5617
  AGENT_COMPOSE_TAG,
5373
5618
  AGENT_COMPOSE_MANUAL,
5374
5619
  AC_WORKFLOW_ID,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Ask-human processor (ADR-0028).
3
+ *
4
+ * Makes "ask a human" a FIRST-CLASS agent affordance: when the agent calls the
5
+ * built-in `AskUserQuestion` tool, this short-circuits it into a SERVER pause
6
+ * (`requestPauseAndAwait`) — the run freezes (compute stops), the question +
7
+ * options land on the human's pause feed, and the human's answer comes back as
8
+ * the tool result. No `agentc pause` CLI for the model to remember, and no
9
+ * dependency on prompt discipline: the moment the agent asks, the run pauses.
10
+ *
11
+ * Loud by construction — the danger this fixes is a pause that SILENTLY doesn't
12
+ * happen (a stale in-sandbox CLI, a non-E2B substrate, an auth error) letting
13
+ * the agent proceed as if it had an answer:
14
+ * - pause cannot be created (server reject) → `Verdict.abort` ENDS the agent
15
+ * loop with a WorkflowError. The run fails loud; it never guesses an answer.
16
+ * - pause expires / is cancelled → the tool result says NO answer came and to
17
+ * not assume one.
18
+ *
19
+ * Lives in the shared `gateToolCall` chain, so one implementation covers every
20
+ * runtime (the Claude Agent SDK `PreToolUse` hook and the ACP permission path).
21
+ * No run credential in the env (local / non-sandbox) → no-op: `AskUserQuestion`
22
+ * passes through untouched so a dev invocation isn't hard-failed.
23
+ */
24
+ import type { Processor } from "./processor.js";
25
+ import type { GatePauseConnection } from "./gate-pause.js";
26
+ /** The Claude built-in tool an agent uses to ask the user a question. */
27
+ export declare const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
28
+ export declare function createAskHumanProcessor(opts?: {
29
+ connection?: GatePauseConnection;
30
+ }): Processor;
@@ -0,0 +1 @@
1
+ export {};
@@ -4,3 +4,4 @@ export { runProcessorChain } from "./runner.js";
4
4
  export { denyTools, humanApproval, requireScope, redactPattern, } from "./builtins.js";
5
5
  export { createGatePauseProcessor } from "./gate-pause.js";
6
6
  export type { GatePausePolicy, GatePauseApproval, GatePauseConnection } from "./gate-pause.js";
7
+ export { createAskHumanProcessor, ASK_USER_QUESTION_TOOL } from "./ask-human.js";
@@ -34,6 +34,15 @@ export declare function shellQuote(value: string): string;
34
34
  * route to JSONL exactly like a handshake error. Overridable via the env for
35
35
  * ops tuning; defaults sane. */
36
36
  export declare const ACP_HANDSHAKE_TIMEOUT_MS: number;
37
+ /** Idle deadline for a PROMPT TURN (distinct from the handshake gate above). A
38
+ * turn is killed only if it goes fully SILENT for this long — the deadline is
39
+ * re-armed on every streamed message, so a long, *streaming* turn never trips
40
+ * it. This must be generous: a reasoning model (GLM, gpt-5-codex) can think for
41
+ * tens of seconds between tool calls with no wire activity, which is NOT a hang.
42
+ * The 10s handshake timeout was far too tight here and killed live GLM turns
43
+ * mid-report. Only a genuinely wedged CLI (the Gemini-style hang) should trip
44
+ * this. Overridable via the env for ops tuning. */
45
+ export declare const ACP_TURN_IDLE_TIMEOUT_MS: number;
37
46
  /** Readiness gate for the LIVE ACP attempt. The duplex-stdin transport in
38
47
  * `spawnAcpProcess` is now real (`commands.spawnDuplex` on the local provider),
39
48
  * so the agent's `initialize` request bytes are delivered and the handshake can