@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.
@@ -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 { writeLocalPauseRequest, consumeLocalPauseRequest } from "./agent/local-pause-request.js";
68
- export type { LocalPauseRequest, PauseOption } from "./agent/local-pause-request.js";
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 ?? {}, ...this.options.agentId ? { AGENT_COMPOSE_AGENT_ID: this.options.agentId } : {} },
2622
+ env: { ...process.env, ...this.config.env ?? {} },
2541
2623
  pathToClaudeCodeExecutable: this.config.pathToClaudeCodeExecutable ?? process.env.CLAUDE_CODE_EXECUTABLE ?? DEFAULT_CLAUDE_PATH,
2542
- ...this.config.claudeMdContent ? { systemPrompt: { type: "preset", preset: "claude_code", append: this.config.claudeMdContent } } : {},
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, ...this.options.agentId ? { AGENT_COMPOSE_AGENT_ID: this.options.agentId } : {} };
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
- ...makeSandboxProvider(sb),
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
- async function invokeStep(sandbox, request, opts) {
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 makeLineSplitter = (sink, filterSentinel) => {
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 stdoutSplitter = makeLineSplitter(opts?.onStdout, true);
4528
- const stderrSplitter = makeLineSplitter(opts?.onStderr, false);
4529
- let result;
4530
- try {
4531
- result = await sandbox.commands.run(RUNNER_COMMAND, {
4532
- envs,
4533
- timeoutMs: 0,
4534
- ...stdoutSplitter.onChunk ? { onStdout: stdoutSplitter.onChunk } : {},
4535
- ...stderrSplitter.onChunk ? { onStderr: stderrSplitter.onChunk } : {}
4536
- });
4537
- } catch (e) {
4538
- if (e instanceof SandboxUnavailableError)
4539
- throw e;
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 (result.exitCode !== 0) {
4559
- const stderrTail = result.stderr.trim().slice(-2000);
4560
- const stdoutTail = result.stdout.trim().slice(-2000);
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 ${result.exitCode} before emitting a step result${details ? `
4681
+ message: `runner subprocess exited ${output.exitCode} before emitting a step result${details ? `
4573
4682
  ${details}` : ""}`,
4574
- exitCode: result.exitCode
4683
+ exitCode: output.exitCode
4575
4684
  }
4576
4685
  };
4577
4686
  }
4578
- const tail = result.stdout.trim().slice(-500);
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";