@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
|
@@ -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
|
-
|
|
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: "",
|
|
@@ -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;
|
package/dist/runtimes/vercel.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
|
|
@@ -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">
|
|
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;
|
package/dist/types/sandbox.d.ts
CHANGED
|
@@ -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
|
package/dist/types/workflow.d.ts
CHANGED
|
@@ -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. */
|
package/dist/utils/errors.d.ts
CHANGED
|
@@ -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