@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
|
@@ -9,7 +9,6 @@ import type { Processor } from "../processors/processor.js";
|
|
|
9
9
|
import { type BoundaryPauseFn } from "../pause/pause-core.js";
|
|
10
10
|
import { RequestContext } from "../request-context/request-context.js";
|
|
11
11
|
import { type SteerPayload } from "./steer-control.js";
|
|
12
|
-
import { type LocalPauseRequest } from "./local-pause-request.js";
|
|
13
12
|
export declare const DEFAULT_CLAUDE_MODEL = "claude-fable-5";
|
|
14
13
|
export declare function parseAgentStatus(text: string): AgentStatus | null;
|
|
15
14
|
export interface AgentLoopResult<TResponse = unknown> {
|
|
@@ -148,13 +147,6 @@ export interface AgentLoopOpts<TResponse = unknown> {
|
|
|
148
147
|
* The boundary consumes it once per check, and only when no steer is
|
|
149
148
|
* already staged. */
|
|
150
149
|
consumeSteerPending?: () => SteerPayload | null;
|
|
151
|
-
/** Take-once read of a local `agentc pause` request this agent dropped in the
|
|
152
|
-
* state dir during its turn (the CLI writes it; see local-pause-request.ts).
|
|
153
|
-
* Returns the request (reason + offered options) or null. The loop turns it
|
|
154
|
-
* into a staged self-pause the next boundary takes — the same snapshot-release
|
|
155
|
-
* path as `needs_input`, but triggered by an explicit `agentc pause` call so
|
|
156
|
-
* it is honored regardless of `mode`. */
|
|
157
|
-
consumeLocalPauseRequest?: () => LocalPauseRequest | null;
|
|
158
150
|
/** PR 7: `auto` (autonomous, default) or `hitl` (can be steer-paused). The
|
|
159
151
|
* boundary is inert unless `hitl`. */
|
|
160
152
|
mode?: "auto" | "hitl";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-sandbox pause client (ADR-0028). The single way an agent pauses its OWN
|
|
3
|
+
* run: `agentc pause` and the gate-pause processor both call
|
|
4
|
+
* `requestPauseAndAwait`, which drives the server pause API and blocks until a
|
|
5
|
+
* human resolves it.
|
|
6
|
+
*
|
|
7
|
+
* Create-then-poll, NOT a held connection:
|
|
8
|
+
* 1. POST /pauses creates the pending row. The server wakes the step
|
|
9
|
+
* activity, which freezes the live VM in place (E2B native suspend). The
|
|
10
|
+
* poll loop below is part of that frozen process — between polls it costs
|
|
11
|
+
* ZERO compute while the run is parked.
|
|
12
|
+
* 2. GET /pauses/:id long-polls (each request a bounded ~5s server-side
|
|
13
|
+
* LISTEN race) until the row is terminal, then returns the human's answer.
|
|
14
|
+
*
|
|
15
|
+
* There is no marker file and no exception threaded through workflow code — the
|
|
16
|
+
* pause is a server operation, so nothing a `try/catch` can swallow.
|
|
17
|
+
*/
|
|
18
|
+
export interface PauseDecision {
|
|
19
|
+
status: "resolved" | "expired" | "cancelled";
|
|
20
|
+
/** The human's answer (present on `resolved`). Caller-defined shape; the
|
|
21
|
+
* dashboard sends `{ decision: string }`. Null on expiry/cancel. */
|
|
22
|
+
decision: unknown;
|
|
23
|
+
}
|
|
24
|
+
export interface RequestPauseOptions {
|
|
25
|
+
/** `AGENT_COMPOSE_URL` — the server base URL injected into the sandbox. */
|
|
26
|
+
baseUrl: string;
|
|
27
|
+
/** `AGENT_COMPOSE_RUN_TOKEN` — the run credential the agent already carries. */
|
|
28
|
+
token: string;
|
|
29
|
+
/** `RUN_ID`. */
|
|
30
|
+
runId: string;
|
|
31
|
+
/** Human-readable reason shown on the pause feed / approval UI. */
|
|
32
|
+
reason: string;
|
|
33
|
+
/** Optional pause TTL; the workflow auto-expires the pause after this. */
|
|
34
|
+
ttlMs?: number;
|
|
35
|
+
/** Optional decision options the human picks from (e.g. approve / deny). */
|
|
36
|
+
options?: Array<{
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
}>;
|
|
40
|
+
/** The action under review (e.g. the tool call), surfaced to the human. */
|
|
41
|
+
action?: Record<string, unknown>;
|
|
42
|
+
/** Optional second-key resume route. */
|
|
43
|
+
correlationKey?: string;
|
|
44
|
+
/** Abort the wait (the agent loop's signal). */
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
/** Inject a custom fetch (tests). Defaults to global fetch. */
|
|
48
|
+
type FetchFn = typeof fetch;
|
|
49
|
+
export declare function requestPauseAndAwait(opts: RequestPauseOptions, fetchImpl?: FetchFn): Promise<PauseDecision>;
|
|
50
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export type { WorkflowPlan, WorkflowStepPlan } from "./types/workflow-plan.js";
|
|
|
23
23
|
export type { BaseExecutionContext, InvokeChild } from "./types/execution-context.js";
|
|
24
24
|
export { RequestContext, ReservedKeyError, NonSerialisableValueError, AC_RESERVED_PREFIX, AC_TEAM_ID, AC_RUN_ID, AC_WORKFLOW_ID, AC_FACTORY_ID, AC_API_KEY_SCOPES, AC_PARENT_RUN_ID, AC_ABORT_SIGNAL, } from "./request-context/index.js";
|
|
25
25
|
export type { RequestContextReserved, RequestContextWire, } from "./request-context/index.js";
|
|
26
|
-
export { Verdict, runProcessorChain, denyTools, humanApproval, requireScope, redactPattern, } from "./processors/index.js";
|
|
27
|
-
export type { Processor, ProcessorContext, ProcessorVerdict, ToolCall, } from "./processors/index.js";
|
|
26
|
+
export { Verdict, runProcessorChain, denyTools, humanApproval, requireScope, redactPattern, createGatePauseProcessor, } from "./processors/index.js";
|
|
27
|
+
export type { Processor, ProcessorContext, ProcessorVerdict, ToolCall, GatePausePolicy, GatePauseApproval, GatePauseConnection, } from "./processors/index.js";
|
|
28
28
|
export type { AgentMessage, AgentMessageInit, AgentMessageText, AgentMessageThinking, AgentMessageToolUse, AgentMessageToolResult, AgentMessageDone, AgentMessageError, AgentMessageUsage, AgentStatus, } from "./types/protocol.js";
|
|
29
29
|
export type { SandboxProvider, DesktopSandboxProvider, } from "./types/sandbox.js";
|
|
30
30
|
export { AgentComposeClient } from "./client.js";
|
|
@@ -59,14 +59,14 @@ export type { WorkflowResult, RunWorkflowOptions, EngineSubsystem } from "./work
|
|
|
59
59
|
export { buildInvokeChild } from "./workflows/invoke-child.js";
|
|
60
60
|
export { defineStep, isWorkflow, runWorkflowSteps, runWorkflowSingleStep, StepValidationError, WorkflowInputValidationError, WorkflowOutputValidationError, } from "./workflow-steps/index.js";
|
|
61
61
|
export type { Step, StepContext, StepRunResult, Workflow, DefineStepOpts, StepWorkflowDefinition, WorkflowBuilder, RunWorkflowStepsOpts, RunWorkflowStepsResult, RunWorkflowSingleStepOpts, RunWorkflowSingleStepResult, StepObservability, SubStepEvent, } from "./workflow-steps/index.js";
|
|
62
|
-
export { invokeStep, serveStep, parseStepResult, buildStepEnvs, StepExecutionError, STEP_RESULT_PREFIX, STEP_PAUSE_PREFIX, STEP_ENV, RUNNER_BUNDLE_PATH, RUNNER_COMMAND, stepInputPath, requestContextPath, } from "./step-invocation/index.js";
|
|
62
|
+
export { invokeStep, launchStep, reconnectStep, serveStep, parseStepResult, buildStepEnvs, StepExecutionError, STEP_RESULT_PREFIX, STEP_PAUSE_PREFIX, STEP_ENV, RUNNER_BUNDLE_PATH, RUNNER_COMMAND, stepInputPath, requestContextPath, } from "./step-invocation/index.js";
|
|
63
63
|
export { PauseError, PauseExpiredError, PauseSchemaError, PauseRequestError, } from "./pause/errors.js";
|
|
64
64
|
export type { PauseErrorCode } from "./pause/errors.js";
|
|
65
65
|
export type { PauseRequest } from "./pause/pause-core.js";
|
|
66
66
|
export type { WaitForEventRequest } from "./pause/wrappers.js";
|
|
67
|
-
export {
|
|
68
|
-
export type {
|
|
69
|
-
export type { StepRequest, StepResult, StepInvocationError, StepPauseRequest, StepHandler, StepHandlerResult, ServeStepRequest, } from "./step-invocation/index.js";
|
|
67
|
+
export { requestPauseAndAwait } from "./agent/pause-client.js";
|
|
68
|
+
export type { PauseDecision, RequestPauseOptions } from "./agent/pause-client.js";
|
|
69
|
+
export type { StepRequest, StepResult, StepInvocationError, StepPauseRequest, StepHandler, StepHandlerResult, ServeStepRequest, RunningStep, InvokeStepOptions, } from "./step-invocation/index.js";
|
|
70
70
|
export { agentLoop, parseAgentStatus, DEFAULT_CLAUDE_MODEL } from "./agent/agent-loop.js";
|
|
71
71
|
export type { AgentLifecycleEvent, AgentLoopOpts, AgentLoopResult } from "./agent/agent-loop.js";
|
|
72
72
|
export { agent } from "./agent/run-agent.js";
|
package/dist/index.js
CHANGED
|
@@ -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 {
|
|
@@ -5103,7 +5269,6 @@ async function agent(opts) {
|
|
|
5103
5269
|
}
|
|
5104
5270
|
export {
|
|
5105
5271
|
writeTool,
|
|
5106
|
-
writeLocalPauseRequest,
|
|
5107
5272
|
writeAgentContext,
|
|
5108
5273
|
stepInputPath,
|
|
5109
5274
|
snapshotResolves,
|
|
@@ -5113,8 +5278,10 @@ export {
|
|
|
5113
5278
|
runWorkflow,
|
|
5114
5279
|
runProcessorChain,
|
|
5115
5280
|
requireScope,
|
|
5281
|
+
requestPauseAndAwait,
|
|
5116
5282
|
requestContextPath,
|
|
5117
5283
|
redactPattern,
|
|
5284
|
+
reconnectStep,
|
|
5118
5285
|
reconnectSandbox,
|
|
5119
5286
|
readTool,
|
|
5120
5287
|
parseStepResult,
|
|
@@ -5127,6 +5294,7 @@ export {
|
|
|
5127
5294
|
makeDesktopSandboxProvider,
|
|
5128
5295
|
listVercelRuntimeModels,
|
|
5129
5296
|
listOwnedSandboxes,
|
|
5297
|
+
launchStep,
|
|
5130
5298
|
killSandboxById,
|
|
5131
5299
|
killAllSandboxes,
|
|
5132
5300
|
isWorkflow,
|
|
@@ -5149,10 +5317,10 @@ export {
|
|
|
5149
5317
|
defineRuntime,
|
|
5150
5318
|
createVercelRuntime,
|
|
5151
5319
|
createSandbox,
|
|
5320
|
+
createGatePauseProcessor,
|
|
5152
5321
|
createCodexRuntime,
|
|
5153
5322
|
createClaudeRuntime,
|
|
5154
5323
|
createAmpRuntime,
|
|
5155
|
-
consumeLocalPauseRequest,
|
|
5156
5324
|
codingTools,
|
|
5157
5325
|
codex_default as codexRuntime,
|
|
5158
5326
|
claude_default as claudeRuntime,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate-pause processor (ADR-0028).
|
|
3
|
+
*
|
|
4
|
+
* A pre-tool gate that, when its `policy` flags a tool call as needing human
|
|
5
|
+
* approval, pauses the run as a SERVER operation (`requestPauseAndAwait`) and
|
|
6
|
+
* blocks until a human answers — then allows or denies the tool.
|
|
7
|
+
*
|
|
8
|
+
* It pauses through the server pause API (`requestPauseAndAwait`), so the pause
|
|
9
|
+
* is a server operation no `try/catch` can swallow. It does NOT use today's
|
|
10
|
+
* `ctx.pause`, which still throws `PauseSignal` — exactly the swallowable path
|
|
11
|
+
* ADR-0028 supersedes for agents. (Routing `ctx.pause` itself through this same
|
|
12
|
+
* server pause is the natural unification — ADR-0028 open question — at which
|
|
13
|
+
* point "the pause API" and "ctx.pause" become one thing.) Because it lives in
|
|
14
|
+
* the shared `gateToolCall` chain, one implementation covers every runtime — the
|
|
15
|
+
* Claude Agent SDK `PreToolUse` hook and the ACP `session/request_permission`
|
|
16
|
+
* path both route through it.
|
|
17
|
+
*
|
|
18
|
+
* Opt-in: the workflow/agent supplies the `policy` (which may be a heuristic or
|
|
19
|
+
* an async classifier agent). The connection defaults to the run credential in
|
|
20
|
+
* the sandbox env; absent it (local / non-sandbox), the gate is a no-op and
|
|
21
|
+
* tool calls pass through untouched.
|
|
22
|
+
*/
|
|
23
|
+
import type { Processor, ProcessorContext, ToolCall } from "./processor.js";
|
|
24
|
+
export interface GatePauseApproval {
|
|
25
|
+
/** Human-readable question shown on the approval UI. */
|
|
26
|
+
reason: string;
|
|
27
|
+
/** Decision options the human picks from. Defaults to Approve / Deny. */
|
|
28
|
+
options?: Array<{
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
/** Decide whether a tool call needs human approval. Return `false` to let it
|
|
34
|
+
* through untouched, or an approval request to pause the run until a human
|
|
35
|
+
* answers. May be async (e.g. a small classifier agent). */
|
|
36
|
+
export type GatePausePolicy = (call: ToolCall, ctx: ProcessorContext) => (false | GatePauseApproval) | Promise<false | GatePauseApproval>;
|
|
37
|
+
export interface GatePauseConnection {
|
|
38
|
+
baseUrl: string;
|
|
39
|
+
token: string;
|
|
40
|
+
runId: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function createGatePauseProcessor(opts: {
|
|
43
|
+
policy: GatePausePolicy;
|
|
44
|
+
/** Override the server connection (defaults to the sandbox run-credential env). */
|
|
45
|
+
connection?: GatePauseConnection;
|
|
46
|
+
}): Processor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,3 +2,5 @@ export type { Processor, ProcessorContext, ProcessorVerdict, ToolCall, } from ".
|
|
|
2
2
|
export { Verdict } from "./processor.js";
|
|
3
3
|
export { runProcessorChain } from "./runner.js";
|
|
4
4
|
export { denyTools, humanApproval, requireScope, redactPattern, } from "./builtins.js";
|
|
5
|
+
export { createGatePauseProcessor } from "./gate-pause.js";
|
|
6
|
+
export type { GatePausePolicy, GatePauseApproval, GatePauseConnection } from "./gate-pause.js";
|