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