@agent-compose/sdk 0.5.9 → 0.6.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-loop.d.ts +0 -8
- package/dist/agent/pause-client.d.ts +50 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +269 -101
- 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 +2 -0
- package/dist/runtimes/openai-desktop.js +265 -99
- 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 +36 -0
- package/dist/types/runtime.d.ts +7 -0
- package/dist/types/sandbox.d.ts +45 -0
- package/package.json +1 -1
- package/src/agent/agent-loop.ts +4 -31
- package/src/agent/pause-client.ts +108 -0
- package/src/agent/run-agent.ts +7 -6
- package/src/index.ts +14 -4
- package/src/processors/gate-pause.ts +94 -0
- package/src/processors/index.ts +6 -0
- package/src/runtimes/_cli-agent.ts +1 -3
- package/src/runtimes/claude.ts +10 -6
- package/src/sandbox.ts +66 -3
- package/src/step-invocation/index.ts +2 -1
- package/src/step-invocation/invoker.ts +195 -84
- package/src/types/runtime.ts +7 -0
- package/src/types/sandbox.ts +44 -0
- 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
|
@@ -1103,6 +1103,98 @@ function redactPattern(pattern, replacement = "[REDACTED]") {
|
|
|
1103
1103
|
}
|
|
1104
1104
|
};
|
|
1105
1105
|
}
|
|
1106
|
+
// src/agent/pause-client.ts
|
|
1107
|
+
var isAbort = (signal) => Boolean(signal?.aborted);
|
|
1108
|
+
async function requestPauseAndAwait(opts, fetchImpl = fetch) {
|
|
1109
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
1110
|
+
const headers = {
|
|
1111
|
+
authorization: `Bearer ${opts.token}`,
|
|
1112
|
+
"content-type": "application/json",
|
|
1113
|
+
accept: "application/json"
|
|
1114
|
+
};
|
|
1115
|
+
const createRes = await fetchImpl(`${base}/api/v1/runs/${opts.runId}/pauses`, {
|
|
1116
|
+
method: "POST",
|
|
1117
|
+
headers,
|
|
1118
|
+
body: JSON.stringify({
|
|
1119
|
+
reason: opts.reason,
|
|
1120
|
+
...opts.ttlMs !== undefined ? { ttlMs: opts.ttlMs } : {},
|
|
1121
|
+
...opts.options ? { options: opts.options } : {},
|
|
1122
|
+
...opts.action ? { action: opts.action } : {},
|
|
1123
|
+
...opts.correlationKey ? { correlationKey: opts.correlationKey } : {}
|
|
1124
|
+
}),
|
|
1125
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
1126
|
+
});
|
|
1127
|
+
if (!createRes.ok) {
|
|
1128
|
+
const text = await createRes.text().catch(() => "");
|
|
1129
|
+
throw new Error(`pause create failed (${createRes.status}): ${text.slice(0, 300)}`);
|
|
1130
|
+
}
|
|
1131
|
+
const { pauseId } = await createRes.json();
|
|
1132
|
+
const pollUrl = `${base}/api/v1/runs/${opts.runId}/pauses/${pauseId}`;
|
|
1133
|
+
let backoffMs = 0;
|
|
1134
|
+
for (;; ) {
|
|
1135
|
+
if (isAbort(opts.signal))
|
|
1136
|
+
throw new Error("pause wait aborted");
|
|
1137
|
+
if (backoffMs > 0)
|
|
1138
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
1139
|
+
let res;
|
|
1140
|
+
try {
|
|
1141
|
+
res = await fetchImpl(pollUrl, { method: "GET", headers, ...opts.signal ? { signal: opts.signal } : {} });
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
if (isAbort(opts.signal))
|
|
1144
|
+
throw err;
|
|
1145
|
+
backoffMs = Math.min(8000, (backoffMs || 500) * 2);
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (!res.ok) {
|
|
1149
|
+
backoffMs = Math.min(8000, (backoffMs || 500) * 2);
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
backoffMs = 0;
|
|
1153
|
+
const body = await res.json();
|
|
1154
|
+
if (body.status === "pending")
|
|
1155
|
+
continue;
|
|
1156
|
+
return { status: body.status, decision: body.resumePayload ?? null };
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/processors/gate-pause.ts
|
|
1161
|
+
var DENY_ANSWERS = new Set(["deny", "no", "reject", "decline", "block"]);
|
|
1162
|
+
function answerText(decision) {
|
|
1163
|
+
const raw = decision !== null && typeof decision === "object" && "decision" in decision ? decision.decision : decision;
|
|
1164
|
+
return typeof raw === "string" ? raw.trim() : raw == null ? "" : JSON.stringify(raw);
|
|
1165
|
+
}
|
|
1166
|
+
function createGatePauseProcessor(opts) {
|
|
1167
|
+
return {
|
|
1168
|
+
name: "gate-pause",
|
|
1169
|
+
async processToolCall(call, ctx) {
|
|
1170
|
+
const approval = await opts.policy(call, ctx);
|
|
1171
|
+
if (!approval)
|
|
1172
|
+
return Verdict.continue(call);
|
|
1173
|
+
const conn = opts.connection ?? {
|
|
1174
|
+
baseUrl: process.env.AGENT_COMPOSE_URL ?? "",
|
|
1175
|
+
token: process.env.AGENT_COMPOSE_RUN_TOKEN ?? "",
|
|
1176
|
+
runId: process.env.RUN_ID ?? ""
|
|
1177
|
+
};
|
|
1178
|
+
if (!conn.baseUrl || !conn.token || !conn.runId)
|
|
1179
|
+
return Verdict.continue(call);
|
|
1180
|
+
const decision = await requestPauseAndAwait({
|
|
1181
|
+
baseUrl: conn.baseUrl,
|
|
1182
|
+
token: conn.token,
|
|
1183
|
+
runId: conn.runId,
|
|
1184
|
+
reason: approval.reason,
|
|
1185
|
+
action: { tool: call.toolName, input: call.toolInput },
|
|
1186
|
+
options: approval.options ?? [{ id: "approve", label: "Approve" }, { id: "deny", label: "Deny" }],
|
|
1187
|
+
signal: ctx.abortSignal
|
|
1188
|
+
});
|
|
1189
|
+
const answer = answerText(decision.decision);
|
|
1190
|
+
if (decision.status === "resolved" && !DENY_ANSWERS.has(answer.toLowerCase())) {
|
|
1191
|
+
return Verdict.continue(call);
|
|
1192
|
+
}
|
|
1193
|
+
const why = decision.status === "resolved" ? `denied: ${answer}` : `${decision.status} with no approval`;
|
|
1194
|
+
return Verdict.deny(`Human ${why}. Tool "${call.toolName}" was not run — adjust course or ask again.`);
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1106
1198
|
// src/client.ts
|
|
1107
1199
|
import { ofetch } from "ofetch";
|
|
1108
1200
|
|
|
@@ -2178,19 +2270,6 @@ Re-emit the COMPLETE corrected <response> JSON now: every required field present
|
|
|
2178
2270
|
}
|
|
2179
2271
|
lastResponseText = responseText;
|
|
2180
2272
|
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
2273
|
const rawResponse = opts.responseSchema ? parseAgentResponse(responseText) ?? parseRawJsonResponse(responseText) : null;
|
|
2195
2274
|
const validateResponse = (target) => {
|
|
2196
2275
|
const parsed = opts.responseSchema.safeParse(target);
|
|
@@ -2523,6 +2602,9 @@ class ClaudeRunner {
|
|
|
2523
2602
|
else
|
|
2524
2603
|
opts.signal.addEventListener("abort", onLoopAbort, { once: true });
|
|
2525
2604
|
}
|
|
2605
|
+
const systemPromptAppend = [this.config.claudeMdContent, this.options.agentManual].filter((s) => Boolean(s && s.trim())).join(`
|
|
2606
|
+
|
|
2607
|
+
`);
|
|
2526
2608
|
try {
|
|
2527
2609
|
let emittedAssistantText = false;
|
|
2528
2610
|
for await (const message of query({
|
|
@@ -2537,9 +2619,9 @@ class ClaudeRunner {
|
|
|
2537
2619
|
effort: this.config.effort,
|
|
2538
2620
|
cwd: this.options.cwd,
|
|
2539
2621
|
abortController: queryAbort,
|
|
2540
|
-
env: { ...process.env, ...this.config.env ?? {}
|
|
2622
|
+
env: { ...process.env, ...this.config.env ?? {} },
|
|
2541
2623
|
pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
|
|
2542
|
-
...
|
|
2624
|
+
...systemPromptAppend ? { systemPrompt: { type: "preset", preset: "claude_code", append: systemPromptAppend } } : {},
|
|
2543
2625
|
skills: this.config.skills ?? "all",
|
|
2544
2626
|
resume: opts.sessionId,
|
|
2545
2627
|
mcpServers: this.config.mcpServers,
|
|
@@ -3051,7 +3133,7 @@ class CliAgentRunner {
|
|
|
3051
3133
|
throw new Error("spawnAcpProcess called on a provider without commands.spawnDuplex");
|
|
3052
3134
|
}
|
|
3053
3135
|
const cmd = `${acp.command} ${acp.args.map(shellQuote).join(" ")}`.trim();
|
|
3054
|
-
const agentEnv = { ...acp.env
|
|
3136
|
+
const agentEnv = { ...acp.env };
|
|
3055
3137
|
const proc = spawnDuplex.call(this.sandbox.commands, cmd, {
|
|
3056
3138
|
...this.options.cwd ? { cwd: this.options.cwd } : {},
|
|
3057
3139
|
...Object.keys(agentEnv).length > 0 ? { envs: agentEnv } : {}
|
|
@@ -3401,13 +3483,64 @@ function withSnapshotRetry(p) {
|
|
|
3401
3483
|
const snapshot = p.snapshot.bind(p);
|
|
3402
3484
|
return { ...p, snapshot: () => withSandboxRetry(snapshot) };
|
|
3403
3485
|
}
|
|
3486
|
+
function wrapE2bBackgroundProcess(handle) {
|
|
3487
|
+
return {
|
|
3488
|
+
pid: handle.pid,
|
|
3489
|
+
async wait() {
|
|
3490
|
+
try {
|
|
3491
|
+
const r = await handle.wait();
|
|
3492
|
+
return { exitCode: r.exitCode ?? 0, stdout: r.stdout, stderr: r.stderr };
|
|
3493
|
+
} catch (e) {
|
|
3494
|
+
const ce = e;
|
|
3495
|
+
if (typeof ce.exitCode === "number") {
|
|
3496
|
+
return {
|
|
3497
|
+
exitCode: ce.exitCode,
|
|
3498
|
+
stdout: typeof ce.stdout === "string" ? ce.stdout : "",
|
|
3499
|
+
stderr: typeof ce.stderr === "string" ? ce.stderr : ""
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
throw e;
|
|
3503
|
+
}
|
|
3504
|
+
},
|
|
3505
|
+
async kill() {
|
|
3506
|
+
await handle.kill();
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3404
3510
|
function makeE2bSandboxProvider(sb) {
|
|
3511
|
+
const base = makeSandboxProvider(sb);
|
|
3405
3512
|
return {
|
|
3406
|
-
...
|
|
3513
|
+
...base,
|
|
3514
|
+
commands: {
|
|
3515
|
+
...base.commands,
|
|
3516
|
+
async runBackground(cmd, opts) {
|
|
3517
|
+
const { sudo, onStdout, onStderr, ...rest } = opts ?? {};
|
|
3518
|
+
const handle = await sb.commands.run(cmd, {
|
|
3519
|
+
...rest,
|
|
3520
|
+
background: true,
|
|
3521
|
+
...sudo ? { user: "root" } : {},
|
|
3522
|
+
...onStdout ? { onStdout } : {},
|
|
3523
|
+
...onStderr ? { onStderr } : {}
|
|
3524
|
+
});
|
|
3525
|
+
return wrapE2bBackgroundProcess(handle);
|
|
3526
|
+
},
|
|
3527
|
+
async connectProcess(pid, opts) {
|
|
3528
|
+
const handle = await sb.commands.connect(pid, {
|
|
3529
|
+
...opts?.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {},
|
|
3530
|
+
...opts?.onStdout ? { onStdout: opts.onStdout } : {},
|
|
3531
|
+
...opts?.onStderr ? { onStderr: opts.onStderr } : {}
|
|
3532
|
+
});
|
|
3533
|
+
return wrapE2bBackgroundProcess(handle);
|
|
3534
|
+
}
|
|
3535
|
+
},
|
|
3407
3536
|
async snapshot() {
|
|
3408
3537
|
const { snapshotId } = await sb.createSnapshot();
|
|
3409
3538
|
return { snapshotId };
|
|
3410
3539
|
},
|
|
3540
|
+
async pauseProcess() {
|
|
3541
|
+
await sb.pause();
|
|
3542
|
+
return { resumeHandle: sb.sandboxId };
|
|
3543
|
+
},
|
|
3411
3544
|
async updateNetworkPolicy(policy) {
|
|
3412
3545
|
await sb.updateNetwork(toE2bNetwork(policy));
|
|
3413
3546
|
}
|
|
@@ -4478,25 +4611,11 @@ function parseStepResult(stdout, resultToken) {
|
|
|
4478
4611
|
const message = baseMessage;
|
|
4479
4612
|
return { ok: false, error: { kind, message } };
|
|
4480
4613
|
}
|
|
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
|
-
};
|
|
4614
|
+
function makeStreamSplitters(opts, resultToken) {
|
|
4496
4615
|
const resultSentinel = stepResultLinePrefix(resultToken);
|
|
4497
4616
|
const pauseSentinel = stepPauseLinePrefix(resultToken);
|
|
4498
4617
|
const isSentinel = (line) => line.startsWith(resultSentinel) || line.startsWith(pauseSentinel);
|
|
4499
|
-
const
|
|
4618
|
+
const make = (sink, filterSentinel) => {
|
|
4500
4619
|
if (!sink)
|
|
4501
4620
|
return { onChunk: undefined, flush: () => {} };
|
|
4502
4621
|
let buf = "";
|
|
@@ -4524,29 +4643,19 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
4524
4643
|
}
|
|
4525
4644
|
};
|
|
4526
4645
|
};
|
|
4527
|
-
const
|
|
4528
|
-
const
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
const ce = e;
|
|
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);
|
|
4646
|
+
const stdout = make(opts?.onStdout, true);
|
|
4647
|
+
const stderr = make(opts?.onStderr, false);
|
|
4648
|
+
return {
|
|
4649
|
+
...stdout.onChunk ? { onStdout: stdout.onChunk } : {},
|
|
4650
|
+
...stderr.onChunk ? { onStderr: stderr.onChunk } : {},
|
|
4651
|
+
flush: () => {
|
|
4652
|
+
stdout.flush();
|
|
4653
|
+
stderr.flush();
|
|
4654
|
+
}
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
async function classifyRunnerOutcome(sandbox, output, resultToken) {
|
|
4658
|
+
const parsed = parseStepResult(output.stdout, resultToken);
|
|
4550
4659
|
if (parsed)
|
|
4551
4660
|
return parsed;
|
|
4552
4661
|
const fileRead = await sandbox.commands.run(`cat ${stepResultFilePath(resultToken)} 2>/dev/null || true`).catch(() => null);
|
|
@@ -4555,9 +4664,9 @@ async function invokeStep(sandbox, request, opts) {
|
|
|
4555
4664
|
if (fromFile)
|
|
4556
4665
|
return fromFile;
|
|
4557
4666
|
}
|
|
4558
|
-
if (
|
|
4559
|
-
const stderrTail =
|
|
4560
|
-
const stdoutTail =
|
|
4667
|
+
if (output.exitCode !== 0) {
|
|
4668
|
+
const stderrTail = output.stderr.trim().slice(-2000);
|
|
4669
|
+
const stdoutTail = output.stdout.trim().slice(-2000);
|
|
4561
4670
|
const details = [
|
|
4562
4671
|
stderrTail ? `stderr:
|
|
4563
4672
|
${stderrTail}` : "",
|
|
@@ -4569,13 +4678,13 @@ ${stdoutTail}` : ""
|
|
|
4569
4678
|
ok: false,
|
|
4570
4679
|
error: {
|
|
4571
4680
|
kind: "runner-exit",
|
|
4572
|
-
message: `runner subprocess exited ${
|
|
4681
|
+
message: `runner subprocess exited ${output.exitCode} before emitting a step result${details ? `
|
|
4573
4682
|
${details}` : ""}`,
|
|
4574
|
-
exitCode:
|
|
4683
|
+
exitCode: output.exitCode
|
|
4575
4684
|
}
|
|
4576
4685
|
};
|
|
4577
4686
|
}
|
|
4578
|
-
const tail =
|
|
4687
|
+
const tail = output.stdout.trim().slice(-500);
|
|
4579
4688
|
return {
|
|
4580
4689
|
ok: false,
|
|
4581
4690
|
error: {
|
|
@@ -4586,6 +4695,99 @@ ${tail}` : ""}`
|
|
|
4586
4695
|
}
|
|
4587
4696
|
};
|
|
4588
4697
|
}
|
|
4698
|
+
async function prepareStepLaunch(sandbox, request, opts) {
|
|
4699
|
+
const resultToken = randomBytes2(16).toString("hex");
|
|
4700
|
+
await Promise.all([
|
|
4701
|
+
sandbox.files.write(stepInputPath(request.stepIndex), JSON.stringify(request.input)),
|
|
4702
|
+
sandbox.files.write(requestContextPath(request.stepIndex), JSON.stringify(request.requestContext))
|
|
4703
|
+
]);
|
|
4704
|
+
const envs = {
|
|
4705
|
+
...opts?.envs ?? {},
|
|
4706
|
+
...buildStepEnvs({ runId: request.runId, stepIndex: request.stepIndex, resultToken, isResume: opts?.isResume })
|
|
4707
|
+
};
|
|
4708
|
+
return { resultToken, envs };
|
|
4709
|
+
}
|
|
4710
|
+
async function invokeStep(sandbox, request, opts) {
|
|
4711
|
+
const { resultToken, envs } = await prepareStepLaunch(sandbox, request, opts);
|
|
4712
|
+
const splitters = makeStreamSplitters(opts, resultToken);
|
|
4713
|
+
let result;
|
|
4714
|
+
try {
|
|
4715
|
+
result = await sandbox.commands.run(RUNNER_COMMAND, {
|
|
4716
|
+
envs,
|
|
4717
|
+
timeoutMs: 0,
|
|
4718
|
+
...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
|
|
4719
|
+
...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
|
|
4720
|
+
});
|
|
4721
|
+
} catch (e) {
|
|
4722
|
+
if (e instanceof SandboxUnavailableError)
|
|
4723
|
+
throw e;
|
|
4724
|
+
const ce = e;
|
|
4725
|
+
result = {
|
|
4726
|
+
stdout: ce.stdout ?? ce.result?.stdout ?? "",
|
|
4727
|
+
stderr: ce.stderr ?? ce.result?.stderr ?? (e instanceof Error ? e.message : String(e)),
|
|
4728
|
+
exitCode: ce.exitCode ?? ce.result?.exitCode ?? 1
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
splitters.flush();
|
|
4732
|
+
return classifyRunnerOutcome(sandbox, result, resultToken);
|
|
4733
|
+
}
|
|
4734
|
+
async function launchStep(sandbox, request, opts) {
|
|
4735
|
+
if (!sandbox.commands.runBackground) {
|
|
4736
|
+
throw new Error("launchStep requires a provider with background-command support (commands.runBackground)");
|
|
4737
|
+
}
|
|
4738
|
+
const { resultToken, envs } = await prepareStepLaunch(sandbox, request, opts);
|
|
4739
|
+
const splitters = makeStreamSplitters(opts, resultToken);
|
|
4740
|
+
const proc = await sandbox.commands.runBackground(RUNNER_COMMAND, {
|
|
4741
|
+
envs,
|
|
4742
|
+
timeoutMs: 0,
|
|
4743
|
+
...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
|
|
4744
|
+
...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
|
|
4745
|
+
});
|
|
4746
|
+
return {
|
|
4747
|
+
runnerPid: proc.pid,
|
|
4748
|
+
resultToken,
|
|
4749
|
+
async wait() {
|
|
4750
|
+
const result = await proc.wait();
|
|
4751
|
+
splitters.flush();
|
|
4752
|
+
return classifyRunnerOutcome(sandbox, result, resultToken);
|
|
4753
|
+
}
|
|
4754
|
+
};
|
|
4755
|
+
}
|
|
4756
|
+
async function reconnectStep(sandbox, resume, opts) {
|
|
4757
|
+
if (!sandbox.commands.connectProcess) {
|
|
4758
|
+
throw new Error("reconnectStep requires a provider with background-command support (commands.connectProcess)");
|
|
4759
|
+
}
|
|
4760
|
+
const { runnerPid, resultToken } = resume;
|
|
4761
|
+
const splitters = makeStreamSplitters(opts, resultToken);
|
|
4762
|
+
let proc = null;
|
|
4763
|
+
try {
|
|
4764
|
+
proc = await sandbox.commands.connectProcess(runnerPid, {
|
|
4765
|
+
...splitters.onStdout ? { onStdout: splitters.onStdout } : {},
|
|
4766
|
+
...splitters.onStderr ? { onStderr: splitters.onStderr } : {}
|
|
4767
|
+
});
|
|
4768
|
+
} catch (e) {
|
|
4769
|
+
if (e instanceof SandboxUnavailableError)
|
|
4770
|
+
throw e;
|
|
4771
|
+
}
|
|
4772
|
+
const attached = proc;
|
|
4773
|
+
return {
|
|
4774
|
+
runnerPid,
|
|
4775
|
+
resultToken,
|
|
4776
|
+
async wait() {
|
|
4777
|
+
let output = { stdout: "", stderr: "", exitCode: 0 };
|
|
4778
|
+
if (attached) {
|
|
4779
|
+
try {
|
|
4780
|
+
output = await attached.wait();
|
|
4781
|
+
} catch (e) {
|
|
4782
|
+
if (e instanceof SandboxUnavailableError)
|
|
4783
|
+
throw e;
|
|
4784
|
+
}
|
|
4785
|
+
}
|
|
4786
|
+
splitters.flush();
|
|
4787
|
+
return classifyRunnerOutcome(sandbox, output, resultToken);
|
|
4788
|
+
}
|
|
4789
|
+
};
|
|
4790
|
+
}
|
|
4589
4791
|
// src/step-invocation/server.ts
|
|
4590
4792
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4591
4793
|
function writeSentinelLine(line) {
|
|
@@ -4692,41 +4894,6 @@ async function serveStep(handler) {
|
|
|
4692
4894
|
await emitResult(token, payload);
|
|
4693
4895
|
process.exit(exitCode);
|
|
4694
4896
|
}
|
|
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
4897
|
// src/agent/run-agent.ts
|
|
4731
4898
|
import { z as z12 } from "zod";
|
|
4732
4899
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -5055,7 +5222,7 @@ async function agent(opts) {
|
|
|
5055
5222
|
const mode = opts.mode ?? "auto";
|
|
5056
5223
|
try {
|
|
5057
5224
|
return await agentLoop({
|
|
5058
|
-
runtime: (runtimeOpts) => opts.runtime.create(opts.sandbox, runtimeOpts),
|
|
5225
|
+
runtime: (runtimeOpts) => opts.runtime.create(opts.sandbox, { ...runtimeOpts, agentManual: buildAgentContextDoc(process.env) }),
|
|
5059
5226
|
agentId,
|
|
5060
5227
|
...opts.label !== undefined ? { label: opts.label } : {},
|
|
5061
5228
|
...opts.budget?.turnsPerIteration !== undefined ? { turnsPerIteration: opts.budget.turnsPerIteration } : {},
|
|
@@ -5093,7 +5260,6 @@ async function agent(opts) {
|
|
|
5093
5260
|
...inbox ? { inbox } : {},
|
|
5094
5261
|
mode,
|
|
5095
5262
|
consumeSteerPending: () => consumeSteerPending(agentId),
|
|
5096
|
-
consumeLocalPauseRequest: () => consumeLocalPauseRequest(agentId),
|
|
5097
5263
|
...steerPause ? { pause: steerPause } : {}
|
|
5098
5264
|
});
|
|
5099
5265
|
} finally {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* to test in isolation.
|
|
19
19
|
*/
|
|
20
20
|
export { STEP_RESULT_PREFIX, STEP_PAUSE_PREFIX, STEP_ENV, RUNNER_BUNDLE_PATH, RUNNER_COMMAND, stepInputPath, requestContextPath, } from "./protocol.js";
|
|
21
|
-
export { invokeStep, parseStepResult, buildStepEnvs } from "./invoker.js";
|
|
21
|
+
export { invokeStep, launchStep, reconnectStep, parseStepResult, buildStepEnvs } from "./invoker.js";
|
|
22
|
+
export type { RunningStep, InvokeStepOptions } from "./invoker.js";
|
|
22
23
|
export { serveStep } from "./server.js";
|
|
23
24
|
export type { StepHandler, ServeStepRequest, StepHandlerResult } from "./server.js";
|
|
24
25
|
export { StepExecutionError } from "./types.js";
|
|
@@ -65,4 +65,40 @@ export interface InvokeStepOptions {
|
|
|
65
65
|
onStdout?: (line: string) => void;
|
|
66
66
|
onStderr?: (line: string) => void;
|
|
67
67
|
}
|
|
68
|
+
/** A step running as a background command (ADR-0028). The activity races its
|
|
69
|
+
* `wait()` against a server pause request; on a pause it freezes the VM
|
|
70
|
+
* (`pauseProcess`) and persists `runnerPid` + `resultToken` so the resume
|
|
71
|
+
* activity can `reconnectStep(...)` to the SAME process — no re-run. */
|
|
72
|
+
export interface RunningStep<TOutput = unknown> {
|
|
73
|
+
/** OS pid of the background runner inside the VM — the reconnect handle. */
|
|
74
|
+
runnerPid: number;
|
|
75
|
+
/** Per-invocation token keying the durable result file the runner writes. */
|
|
76
|
+
resultToken: string;
|
|
77
|
+
/** Await the runner's exit and classify its output into a `StepResult`. */
|
|
78
|
+
wait(): Promise<StepResult<TOutput>>;
|
|
79
|
+
}
|
|
68
80
|
export declare function invokeStep<TOutput = unknown>(sandbox: SandboxProvider, request: StepRequest, opts?: InvokeStepOptions): Promise<StepResult<TOutput>>;
|
|
81
|
+
/**
|
|
82
|
+
* Launch the step runner as a BACKGROUND command (ADR-0028) and return a handle
|
|
83
|
+
* the activity drives: it races `wait()` against a server pause request and, on
|
|
84
|
+
* a pause, freezes the VM (`pauseProcess`) and persists `runnerPid` +
|
|
85
|
+
* `resultToken` so `reconnectStep` can continue the SAME process — no re-run.
|
|
86
|
+
* Requires a provider with `commands.runBackground` (E2B); the foreground
|
|
87
|
+
* `invokeStep` is the path for providers without it (Vercel).
|
|
88
|
+
*/
|
|
89
|
+
export declare function launchStep<TOutput = unknown>(sandbox: SandboxProvider, request: StepRequest, opts?: InvokeStepOptions): Promise<RunningStep<TOutput>>;
|
|
90
|
+
/**
|
|
91
|
+
* Resume a previously-paused background runner (ADR-0028) and return a
|
|
92
|
+
* `RunningStep` handle — uniform with `launchStep` so the activity can race
|
|
93
|
+
* `wait()` against a fresh pause request (a resumed step can pause again). After
|
|
94
|
+
* the workflow reconnects the suspended VM (`Sandbox.connect` auto-resumes it),
|
|
95
|
+
* this re-attaches to the still-running runner by `runnerPid`; `wait()` awaits
|
|
96
|
+
* its exit (event-driven — no polling) and classifies the output (the durable
|
|
97
|
+
* result file is authoritative on this path). If the runner already exited
|
|
98
|
+
* during resume, the re-attach fails and `wait()` classifies from the file.
|
|
99
|
+
*/
|
|
100
|
+
export declare function reconnectStep<TOutput = unknown>(sandbox: SandboxProvider, resume: {
|
|
101
|
+
runnerPid: number;
|
|
102
|
+
resultToken: string;
|
|
103
|
+
stepIndex: number;
|
|
104
|
+
}, opts?: Pick<InvokeStepOptions, "onStdout" | "onStderr">): Promise<RunningStep<TOutput>>;
|
package/dist/types/runtime.d.ts
CHANGED
|
@@ -29,6 +29,13 @@ export interface RuntimeOptions {
|
|
|
29
29
|
/** Agent id and label for processor context / adapter logs. */
|
|
30
30
|
agentId?: string;
|
|
31
31
|
iteration?: number;
|
|
32
|
+
/** The platform manual (file conventions, connectors & access, how to pause).
|
|
33
|
+
* `agent()` builds it per-run (`buildAgentContextDoc`) and threads it here so
|
|
34
|
+
* a runtime that supports a system-prompt append (the claude runtime) injects
|
|
35
|
+
* it directly — instead of relying on the agent to `cat` the on-disk
|
|
36
|
+
* AGENTS.md/CLAUDE.md, which the Agent SDK doesn't auto-load and which can
|
|
37
|
+
* fail to write on a read-only/degraded working dir. */
|
|
38
|
+
agentManual?: string;
|
|
32
39
|
/** The run's pause boundary, threaded from the agent loop so a runtime-driven
|
|
33
40
|
* pre-tool gate (e.g. the ACP `session/request_permission` path through
|
|
34
41
|
* `gateToolCall`) can raise a human-approval `ctx.pause`. The runtime binds it
|
package/dist/types/sandbox.d.ts
CHANGED
|
@@ -21,6 +21,29 @@ export interface SandboxCommandResult {
|
|
|
21
21
|
stdout: string;
|
|
22
22
|
stderr: string;
|
|
23
23
|
}
|
|
24
|
+
/** A long-running command launched in the background (ADR-0028). Unlike
|
|
25
|
+
* `commands.run` (which awaits completion on one connection), a background
|
|
26
|
+
* command keeps running inside the VM independent of the launching
|
|
27
|
+
* connection: it survives `pauseProcess()`/resume and is re-attachable by
|
|
28
|
+
* `pid` after a fresh `Sandbox.connect`. This is what lets the platform
|
|
29
|
+
* freeze an agent mid-turn for a human-in-the-loop pause and continue the
|
|
30
|
+
* SAME process on resume — no re-run.
|
|
31
|
+
*
|
|
32
|
+
* Implemented ONLY by process-resume-capable providers (E2B); the presence
|
|
33
|
+
* of `commands.runBackground` IS the capability flag, paired with
|
|
34
|
+
* `pauseProcess`. Providers without it leave both undefined and pause via
|
|
35
|
+
* `snapshot()` + re-run instead. */
|
|
36
|
+
export interface SandboxBackgroundProcess {
|
|
37
|
+
/** OS pid inside the VM — the durable handle used to reconnect after a
|
|
38
|
+
* pause/resume cycle via `commands.connectProcess(pid)`. */
|
|
39
|
+
pid: number;
|
|
40
|
+
/** Resolve when the process exits, with its buffered result. Live output
|
|
41
|
+
* streams to the `onStdout`/`onStderr` passed at launch / connect time.
|
|
42
|
+
* Does NOT throw on a non-zero exit — the result carries `exitCode`. */
|
|
43
|
+
wait(): Promise<SandboxCommandResult>;
|
|
44
|
+
/** Force-terminate the process. */
|
|
45
|
+
kill(): Promise<void>;
|
|
46
|
+
}
|
|
24
47
|
/** A spawned long-lived command with a writable stdin and readable stdout,
|
|
25
48
|
* exposed as byte web-streams. Unlike `commands.run` (which buffers to
|
|
26
49
|
* completion and exposes stdout only via an `onStdout` callback), a duplex
|
|
@@ -78,6 +101,16 @@ export interface SandboxProvider {
|
|
|
78
101
|
* view). The vercel/e2b providers (server→sandbox) leave it undefined; an
|
|
79
102
|
* ACP caller that finds it absent falls back to the JSONL transport. */
|
|
80
103
|
spawnDuplex?(cmd: string, opts?: SandboxSpawnDuplexOptions): SandboxDuplexProcess;
|
|
104
|
+
/** Launch a command in the background and return immediately with a
|
|
105
|
+
* reconnectable handle (ADR-0028). The process survives the launching
|
|
106
|
+
* connection dropping AND a `pauseProcess()`/resume cycle. OPTIONAL —
|
|
107
|
+
* only process-resume providers (E2B) implement it; its presence (paired
|
|
108
|
+
* with `pauseProcess`) is the native-pause capability flag. */
|
|
109
|
+
runBackground?(cmd: string, opts?: SandboxCommandRunOptions): Promise<SandboxBackgroundProcess>;
|
|
110
|
+
/** Re-attach to a background command by `pid` after a fresh
|
|
111
|
+
* `Sandbox.connect` (the resume half of `runBackground`). OPTIONAL,
|
|
112
|
+
* E2B-only. Throws if no process with that pid is running. */
|
|
113
|
+
connectProcess?(pid: number, opts?: Pick<SandboxCommandRunOptions, "onStdout" | "onStderr" | "timeoutMs">): Promise<SandboxBackgroundProcess>;
|
|
81
114
|
};
|
|
82
115
|
files: {
|
|
83
116
|
write(path: string, content: string): Promise<void>;
|
|
@@ -95,6 +128,18 @@ export interface SandboxProvider {
|
|
|
95
128
|
snapshotId: string;
|
|
96
129
|
sizeBytes?: number;
|
|
97
130
|
}>;
|
|
131
|
+
/** Suspend the live VM in place and return a handle to resume it (ADR-0027).
|
|
132
|
+
* Present ONLY on process-resume-capable providers (E2B via `sandbox.pause()`,
|
|
133
|
+
* returning the sandbox id; resume is `Sandbox.connect(handle)`, which
|
|
134
|
+
* auto-resumes the paused VM). Unlike `snapshot()` — which captures an FS
|
|
135
|
+
* image, kills the origin, and re-runs the step from a fresh sandbox — a
|
|
136
|
+
* process-resume pause FREEZES the live process (zero compute) and continues
|
|
137
|
+
* it exactly where it blocked. The presence of this method IS the capability
|
|
138
|
+
* flag: providers without native VM-suspend leave it undefined and fall back
|
|
139
|
+
* to `snapshot()` + re-run. */
|
|
140
|
+
pauseProcess?(): Promise<{
|
|
141
|
+
resumeHandle: string;
|
|
142
|
+
}>;
|
|
98
143
|
/** Replace the live sandbox's egress policy in place — so the server can
|
|
99
144
|
* push a freshly resolved policy (with re-minted connector access tokens)
|
|
100
145
|
* before each step instead of relying on the policy baked at create.
|
package/package.json
CHANGED
package/src/agent/agent-loop.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { RequestContext } from "../request-context/request-context.js";
|
|
|
15
15
|
import { PauseManager } from "../pause/manager.js";
|
|
16
16
|
import { PauseSignal, isPauseSignal } from "../pause/pause-core.js";
|
|
17
17
|
import { SteerDecisionSchema, type SteerDecision, type SteerPayload } from "./steer-control.js";
|
|
18
|
-
import { type LocalPauseRequest } from "./local-pause-request.js";
|
|
19
18
|
|
|
20
19
|
export const DEFAULT_CLAUDE_MODEL = "claude-fable-5";
|
|
21
20
|
|
|
@@ -154,13 +153,6 @@ export interface AgentLoopOpts<TResponse = unknown> {
|
|
|
154
153
|
* The boundary consumes it once per check, and only when no steer is
|
|
155
154
|
* already staged. */
|
|
156
155
|
consumeSteerPending?: () => SteerPayload | null;
|
|
157
|
-
/** Take-once read of a local `agentc pause` request this agent dropped in the
|
|
158
|
-
* state dir during its turn (the CLI writes it; see local-pause-request.ts).
|
|
159
|
-
* Returns the request (reason + offered options) or null. The loop turns it
|
|
160
|
-
* into a staged self-pause the next boundary takes — the same snapshot-release
|
|
161
|
-
* path as `needs_input`, but triggered by an explicit `agentc pause` call so
|
|
162
|
-
* it is honored regardless of `mode`. */
|
|
163
|
-
consumeLocalPauseRequest?: () => LocalPauseRequest | null;
|
|
164
156
|
/** PR 7: `auto` (autonomous, default) or `hitl` (can be steer-paused). The
|
|
165
157
|
* boundary is inert unless `hitl`. */
|
|
166
158
|
mode?: "auto" | "hitl";
|
|
@@ -462,29 +454,10 @@ export async function agentLoop<TResponse = unknown>(opts: AgentLoopOpts<TRespon
|
|
|
462
454
|
|
|
463
455
|
let status = parseAgentStatus(responseText);
|
|
464
456
|
|
|
465
|
-
// `agentc pause`
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
// dashboard renders them as buttons). Checked BEFORE the settle / needs_input
|
|
470
|
-
// paths and independent of the <status> block — the common case is an agent
|
|
471
|
-
// that called the tool and ended its turn with no status at all, which would
|
|
472
|
-
// otherwise fall through to the empty-output re-prompt below. UNGATED by
|
|
473
|
-
// `mode`: an explicit `agentc pause` is a deliberate ask, not the `needs_input`
|
|
474
|
-
// heuristic that only `hitl` agents may trigger.
|
|
475
|
-
if (opts.pause && pendingSteerPause === null) {
|
|
476
|
-
const localPause = opts.consumeLocalPauseRequest?.() ?? null;
|
|
477
|
-
if (localPause) {
|
|
478
|
-
pendingSteerPause = {
|
|
479
|
-
reason: localPause.reason,
|
|
480
|
-
correlationKey: null,
|
|
481
|
-
...(localPause.options && localPause.options.length > 0 ? { payload: { options: localPause.options } } : {}),
|
|
482
|
-
at: Date.now(),
|
|
483
|
-
};
|
|
484
|
-
blockerStreak = null; // an explicit ask is not a stuck loop
|
|
485
|
-
continue; // pause fires at the next boundary
|
|
486
|
-
}
|
|
487
|
-
}
|
|
457
|
+
// ADR-0028 — `agentc pause` is no longer a marker the loop consumes here. It
|
|
458
|
+
// is a server operation: the CLI blocks on the pause API and the run is
|
|
459
|
+
// frozen in place by the step activity, with no loop involvement. The steer
|
|
460
|
+
// (human-driven) and `needs_input` (self-pause) paths below are unaffected.
|
|
488
461
|
|
|
489
462
|
// Inline safeParse (instead of letting parseAgentResponse validate)
|
|
490
463
|
// so a schema failure surfaces via lastResponseValidationError on
|