@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.
- package/dist/agent/agent-context.d.ts +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +288 -43
- package/dist/processors/ask-human.d.ts +30 -0
- package/dist/processors/ask-human.test.d.ts +1 -0
- package/dist/processors/index.d.ts +1 -0
- package/dist/runtimes/_cli-agent.d.ts +9 -0
- package/dist/runtimes/cursor.d.ts +9 -0
- package/dist/runtimes/droid.d.ts +9 -0
- package/dist/runtimes/openai-desktop.js +277 -43
- package/dist/runtimes/opencode.d.ts +25 -0
- package/dist/runtimes/vercel.js +11 -1
- package/dist/step-invocation/invoker.d.ts +14 -1
- package/dist/step-invocation/protocol.d.ts +8 -0
- package/dist/types/sandbox.d.ts +9 -0
- package/dist/types/workflow.d.ts +12 -0
- package/dist/utils/errors.d.ts +9 -1
- package/package.json +1 -1
- package/src/agent/agent-context.ts +23 -16
- package/src/agent/agent-loop.ts +9 -2
- package/src/agent/run-agent.ts +9 -1
- package/src/index.ts +13 -0
- package/src/processors/ask-human.ts +136 -0
- package/src/processors/index.ts +5 -0
- package/src/runtimes/_cli-agent.ts +12 -2
- package/src/runtimes/cursor.ts +59 -0
- package/src/runtimes/droid.ts +63 -0
- package/src/runtimes/opencode.ts +61 -0
- package/src/sandbox.ts +12 -0
- package/src/step-invocation/invoker.ts +185 -23
- package/src/step-invocation/protocol.ts +11 -0
- package/src/types/sandbox.ts +9 -0
- package/src/types/workflow.ts +12 -0
- package/src/utils/errors.ts +19 -2
|
@@ -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
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
},
|
|
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 ${
|
|
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
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
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
|
-
|
|
4992
|
+
const signal = opts?.signal;
|
|
4993
|
+
let attached = null;
|
|
4763
4994
|
try {
|
|
4764
|
-
|
|
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
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
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
|
|
4953
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4958
|
-
\`agentc register <file.ts>\` (or \`/ac:register\`).
|
|
5188
|
+
## Pausing to ask the human
|
|
4959
5189
|
|
|
4960
|
-
|
|
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
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
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
|
-
...
|
|
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
|