@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.
@@ -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 {
@@ -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>>;
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-compose/sdk",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "Client library for agent-compose — define agents, runtimes, and workflows, and invoke them against an agent-compose server.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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` — the agent shelled out to the CLI during this turn, which
466
- // dropped a durable pause-request marker in the state dir. Honor it like a
467
- // self-pause: stage the pause the NEXT boundary takes, carrying the agent's
468
- // question as the reason and any offered choices as the pause payload (the
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