@agent-compose/sdk 0.8.5 → 0.8.7

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.
Files changed (100) hide show
  1. package/README.md +213 -189
  2. package/dist/agent/agent-context.d.ts +3 -3
  3. package/dist/agent/agent-loop.d.ts +6 -5
  4. package/dist/agent/perf-sampler.d.ts +27 -2
  5. package/dist/agent/run-agent.d.ts +1 -1
  6. package/dist/client.d.ts +119 -54
  7. package/dist/directives.d.ts +3 -3
  8. package/dist/display.d.ts +7 -0
  9. package/dist/errors.d.ts +1 -1
  10. package/dist/generated/agentc-commands.d.ts +34 -0
  11. package/dist/index.d.ts +12 -12
  12. package/dist/index.js +771 -204
  13. package/dist/request-context/request-context.d.ts +1 -1
  14. package/dist/runtimes/_cli-agent.d.ts +185 -68
  15. package/dist/runtimes/_reported-model.d.ts +16 -0
  16. package/dist/runtimes/claude-code.d.ts +60 -1
  17. package/dist/runtimes/claude.d.ts +1 -1
  18. package/dist/runtimes/codex.d.ts +94 -6
  19. package/dist/runtimes/codex.mid-turn-hook.test.d.ts +10 -0
  20. package/dist/runtimes/model-report.test.d.ts +14 -0
  21. package/dist/runtimes/openai-desktop.js +741 -200
  22. package/dist/runtimes/opencode.d.ts +48 -11
  23. package/dist/runtimes/opencode.test.d.ts +14 -0
  24. package/dist/sandbox/baked-clis.d.ts +75 -0
  25. package/dist/sandbox/exec-stream.d.ts +1 -2
  26. package/dist/sandbox/network-policy.d.ts +23 -5
  27. package/dist/sandbox.d.ts +4 -2
  28. package/dist/step-invocation/protocol.d.ts +3 -4
  29. package/dist/step-invocation/server.d.ts +2 -2
  30. package/dist/step-invocation/types.d.ts +1 -1
  31. package/dist/types/api-conversations.d.ts +442 -29
  32. package/dist/types/api-factory.d.ts +99 -10
  33. package/dist/types/api-projects.d.ts +521 -0
  34. package/dist/types/api-runs.d.ts +83 -0
  35. package/dist/types/api-scopes.d.ts +32 -3
  36. package/dist/types/conversation-stream.d.ts +5 -0
  37. package/dist/types/execution-context.d.ts +1 -1
  38. package/dist/types/protocol.d.ts +86 -2
  39. package/dist/types/runtime.d.ts +9 -2
  40. package/dist/types/workflow-metadata.d.ts +2 -4
  41. package/dist/types/workflow-plan.d.ts +1 -3
  42. package/dist/utils/bundler.d.ts +23 -0
  43. package/dist/workflow-steps/observability.d.ts +2 -3
  44. package/dist/workflow-steps/runner.d.ts +5 -8
  45. package/dist/workflow-steps/types.d.ts +8 -10
  46. package/dist/workflow-steps/workflow.d.ts +2 -1
  47. package/dist/workflows/engine.d.ts +3 -5
  48. package/dist/workflows/invoke-child.d.ts +2 -2
  49. package/package.json +2 -2
  50. package/src/agent/agent-context.ts +168 -125
  51. package/src/agent/agent-loop.ts +7 -6
  52. package/src/agent/perf-sampler.ts +54 -3
  53. package/src/agent/run-agent.ts +1 -1
  54. package/src/client.ts +226 -71
  55. package/src/directives.ts +3 -3
  56. package/src/display.ts +12 -0
  57. package/src/errors.ts +1 -0
  58. package/src/generated/agentc-commands.ts +571 -0
  59. package/src/index.ts +57 -21
  60. package/src/pause/pause-core.ts +2 -1
  61. package/src/request-context/request-context.ts +1 -1
  62. package/src/runtimes/_cli-agent.ts +318 -122
  63. package/src/runtimes/_reported-model.ts +24 -0
  64. package/src/runtimes/claude-code.ts +195 -12
  65. package/src/runtimes/claude.ts +9 -2
  66. package/src/runtimes/codex.ts +188 -19
  67. package/src/runtimes/opencode.ts +195 -26
  68. package/src/sandbox/baked-clis.ts +86 -0
  69. package/src/sandbox/exec-stream.ts +1 -2
  70. package/src/sandbox/network-policy.ts +51 -7
  71. package/src/sandbox/providers/e2b.ts +3 -3
  72. package/src/sandbox/providers/vercel.ts +6 -6
  73. package/src/sandbox.ts +8 -2
  74. package/src/step-invocation/invoker.ts +2 -6
  75. package/src/step-invocation/protocol.ts +3 -4
  76. package/src/step-invocation/server.ts +2 -2
  77. package/src/types/api-conversations.ts +366 -23
  78. package/src/types/api-factory.ts +95 -10
  79. package/src/types/api-projects.ts +477 -0
  80. package/src/types/api-runs.ts +73 -0
  81. package/src/types/api-scopes.ts +32 -3
  82. package/src/types/conversation-stream.ts +5 -0
  83. package/src/types/execution-context.ts +1 -1
  84. package/src/types/protocol.ts +91 -2
  85. package/src/types/runtime.ts +8 -2
  86. package/src/types/sandbox-environment.ts +1 -2
  87. package/src/types/workflow-metadata.ts +2 -4
  88. package/src/types/workflow-plan.ts +1 -3
  89. package/src/utils/bundler.ts +88 -19
  90. package/src/workflow-steps/observability.ts +2 -3
  91. package/src/workflow-steps/runner.ts +5 -8
  92. package/src/workflow-steps/types.ts +8 -10
  93. package/src/workflow-steps/workflow.ts +2 -1
  94. package/src/workflows/engine.ts +3 -5
  95. package/src/workflows/invoke-child.ts +2 -2
  96. package/dist/generated/verb-synopsis.d.ts +0 -34
  97. package/dist/pause/__tests__/errors.test.d.ts +0 -1
  98. package/dist/pause/__tests__/wrappers.test.d.ts +0 -1
  99. package/dist/step-invocation/__tests__/protocol.test.d.ts +0 -1
  100. package/src/generated/verb-synopsis.ts +0 -544
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * RequestContext — one typed bag carrying tenant identity + freeform
3
3
  * per-run state across the boundary between server, dispatch, runner,
4
- * workflow body, agent loop, and (future) processors.
4
+ * workflow body, agent loop, and processors.
5
5
  *
6
6
  * Two halves:
7
7
  *
@@ -4,7 +4,7 @@
4
4
  * contract.
5
5
  *
6
6
  * This is a different *mechanism* from the other runtimes: `claudeRuntime`
7
- * drives the Anthropic Agent SDK and `vercelRuntime` drives the Vercel AI SDK,
7
+ * drives the Anthropic Agent SDK and `createVercelRuntime` drives the Vercel AI SDK,
8
8
  * but a CLI-agent runtime spawns the provider's own CLI (`codex exec --json`,
9
9
  * `amp -x --stream-json`) — the CLI brings its own agent loop + tools, and we
10
10
  * only stream-parse the events it prints. Codex and Amp are the first two; this
@@ -125,12 +125,15 @@ export interface ProbeDeathState {
125
125
  }
126
126
  /** The death-door decision, pure for tests. Null = keep polling. */
127
127
  export declare function probeDeathVerdict(s: ProbeDeathState, nowMs: number): "dead-gone" | "dead-silent" | null;
128
- /** The claude CLI's stderr complaint when `--resume <id>` names a thread
129
- * that does not exist on this machine (the 2026-08-18 split-brain
130
- * forensic). When a nonzero exit's stderr carries it, the transport
131
- * surfaces the stderr as the turn's LAST error event even though a bare
132
- * result error already streamed — the consumer's classifier needs the
133
- * specific complaint, not the generic `error_during_execution` token. */
128
+ /** The CLI's stderr complaint when the thread it was told to resume does
129
+ * not exist on this machine: claude's `--resume <id>` "No conversation
130
+ * found with session ID" (the 2026-08-18 split-brain forensic) and
131
+ * opencode's `--session <id>` "Error: Session not found" (in the CLI's
132
+ * red-bold ANSI dressing; 1.18.34). When a nonzero exit's stderr carries
133
+ * it, the transport surfaces the stderr as the turn's LAST error event
134
+ * even though a bare result error already streamed — the consumer's
135
+ * classifier needs the specific complaint, not the generic
136
+ * `error_during_execution` token. */
134
137
  export declare const RESUME_TARGET_MISSING_STDERR: RegExp;
135
138
  /** One durable-trio reading, parsed from the probe exec's single line. */
136
139
  export interface TurnProbeReading {
@@ -193,10 +196,31 @@ export declare const INJECT_FEEDER_RESULT_TAIL_BYTES = 262144;
193
196
  export declare const INJECT_ACK_POLL_ATTEMPTS = 30;
194
197
  export declare const INJECT_ACK_POLL_SECONDS = "0.5";
195
198
  export declare const INJECT_ACK_EXEC_TIMEOUT_MS = 25000;
199
+ /** How the guest recognises the CLI's terminal `result` event in the durable
200
+ * .out: a line that carries the `"type":"result"` pair and does not open
201
+ * with another event's type, as an awk condition over `$0` (POSIX awk, so
202
+ * dash, busybox and the macOS test hosts read it alike). The CLI does not
203
+ * keep its key order: 2.1.212 wrote `{"type":"result",…` but 2.1.278 and
204
+ * later write the pair near the END of the object
205
+ * (`{"duration_api_ms":…,"result":"…","type":"result","duration_ms":…}`), so
206
+ * the anchored prefix the resident contract used matched nothing on the
207
+ * pinned 2.1.283. Every other event the CLI writes opens with its own type
208
+ * (`{"type":"assistant",…`, `{"type":"stream_event",…`), which keeps a tool
209
+ * call whose input holds the same pair from counting; tool output and model
210
+ * text ride inside JSON strings, where the quotes are escaped. */
211
+ export declare const CLI_RESULT_LINE_AWK: string;
196
212
  /** The in-guest feeder fragment, prepended to the detached wrapper script.
197
213
  * Runs inside the setsid session (group-kill reaps it) and keys its loop to
198
214
  * the wrapper's own pid (`$$`), like the heartbeat subshell. Exported for
199
- * tests. */
215
+ * tests.
216
+ *
217
+ * The forward is GUARDED: the fed and delivered counters advance only
218
+ * when `sed` wrote the lines. The loop keys on the wrapper, which outlives
219
+ * a CLI that died without a result line by the sentinel write and the
220
+ * exit push; in that window the FIFO has no reader, `sed` dies of SIGPIPE,
221
+ * and an unguarded counter still advanced, so the ack exited 0 for a line
222
+ * nobody could read and the settle sealed it (review C1). Guarded, the ack
223
+ * sees no advance and exits 4 or 5, and the message stays owed. */
200
224
  export declare function streamInputFeederFragment(paths: {
201
225
  promptPath: string;
202
226
  fifoPath: string;
@@ -215,13 +239,28 @@ export declare function streamInputFeederFragment(paths: {
215
239
  * activity) MUST wrap through here — the wrapper rides only the wire to
216
240
  * the model; the stored conversation row keeps the user's raw text.
217
241
  * The text is pinned by tests: change it deliberately or not at all. */
218
- export declare function wrapMidTurnUserMessage(text: string): string;
242
+ export declare function wrapMidTurnUserMessage(text: string, opts?: MidTurnEnvelopeOptions): string;
243
+ /** Who a mid-turn message speaks for, when it is not the session user.
244
+ * `relayedFrom` = the display name of the person whose own words an agent
245
+ * relayed verbatim; `fromOwnerAgent` = the thread agent that owns this
246
+ * worker, in its own words; `platformNotice` = a system notice the
247
+ * platform posted to wake the conversation (nobody's words); all absent =
248
+ * the session user's own message. */
249
+ export interface MidTurnEnvelopeOptions {
250
+ relayedFrom?: string | null;
251
+ fromOwnerAgent?: boolean;
252
+ platformNotice?: boolean;
253
+ }
219
254
  /** The one delivery-ack exec `injectUserMessage` runs: append the message
220
255
  * line to the durable inbox, then wait for the feeder's delivered counter
221
- * to cover it. Exit 0 = delivered into the CLI's stdin pre-result; 4 = the
222
- * runner died first; 5 = not delivered within the ack window (feeder
223
- * stopped on the result, or the guest is crawling) — both non-zero exits
224
- * mean "leave the message owed". Exported for tests. */
256
+ * to cover it. Exit 0 = written into the CLI's stdin pipe pre-result,
257
+ * which is NOT "the model saw it": the CLI surfaces the line at its next
258
+ * tool boundary, so a kill before then loses it, and the settle's kill
259
+ * terminals hold the anchor at the pre-inject dispatch head for that
260
+ * reason (review D8). 4 = the runner died first; 5 = not forwarded within
261
+ * the ack window (the feeder is not forwarding: it stopped on the result,
262
+ * the resident turn gate is closed, or the guest is crawling). Both
263
+ * non-zero exits mean "leave the message owed". Exported for tests. */
225
264
  export declare function injectAppendAndAckCommand(args: {
226
265
  line: string;
227
266
  target: number;
@@ -229,6 +268,60 @@ export declare function injectAppendAndAckCommand(args: {
229
268
  inboxPath: string;
230
269
  deliveredPath: string;
231
270
  }): string;
271
+ /** stdin is a live message stream: the FIFO feeder above delivers the prompt
272
+ * line first and inbox lines after (claude: `--input-format stream-json`). */
273
+ export interface StdinStreamInputSpec {
274
+ transport: "stdin-stream";
275
+ /** Serialise the opening prompt into ONE stream-input stdin line
276
+ * (claude: a stream-json user message). No trailing newline — the
277
+ * transport owns line framing. */
278
+ promptLine(prompt: string): string;
279
+ /** Serialise one mid-turn user message into ONE stdin line. No
280
+ * trailing newline. */
281
+ messageLine(text: string): string;
282
+ /** Serialise ONE in-band interrupt control line (claude: a stream-json
283
+ * `control_request` with subtype "interrupt") — the ESC equivalent.
284
+ * The CLI's control layer handles these immediately, MID-STEP
285
+ * included: the running tool call aborts (its tool_result records the
286
+ * harness's own rejection text), the run ends with an
287
+ * `error_during_execution` result within ~100ms, and the session file
288
+ * stays `--resume`-able with the whole turn context. Verified live
289
+ * against claude 2.1.236. Absent → the runtime has no in-band
290
+ * interrupt; callers fall back to kill semantics. */
291
+ interruptLine?(requestId: string): string;
292
+ }
293
+ /** The CLI hands the inbox to the model itself, through a hook it runs after
294
+ * each tool call it completes (codex: the platform's PostToolUse command
295
+ * hook, CODEX_MID_TURN_HOOK_COMMAND in codex.ts). The launch creates the
296
+ * inbox and exports its paths into the CLI's environment
297
+ * (toolHookInboxFragment); the hook reads them back, prints the unfed lines
298
+ * as its additional context for the model and advances the delivered
299
+ * counter. stdin stays the plain prompt file, so there is no in-band
300
+ * interrupt on this transport. */
301
+ export interface ToolHookInputSpec {
302
+ transport: "tool-hook";
303
+ /** Serialise one mid-turn user message into ONE inbox line the hook can
304
+ * splice into its JSON output without decoding: a JSON string literal. */
305
+ messageLine(text: string): string;
306
+ }
307
+ export type MidTurnInputSpec = StdinStreamInputSpec | ToolHookInputSpec;
308
+ /** The stdin-stream transport of a spec, or undefined. The FIFO feeder, the
309
+ * resident harness, the prewarm lane and the in-band interrupt exist only
310
+ * for this transport; the inject lane itself reads `midTurnInput`. */
311
+ export declare function stdinStreamInputOf(spec: Pick<CliAgentSpec, "midTurnInput">): StdinStreamInputSpec | undefined;
312
+ /** The environment the tool-hook lane hands the CLI, and so the hook: codex
313
+ * runs a command hook with the CLI process's own environment (codex-rs
314
+ * hooks/src/registry.rs Hooks::new, engine/command_runner.rs at
315
+ * rust-v0.160.0). The turn's inbox and delivered-counter paths. */
316
+ export declare const MID_TURN_INBOX_ENV = "AC_MID_TURN_INBOX";
317
+ export declare const MID_TURN_DELIVERED_ENV = "AC_MID_TURN_DELIVERED";
318
+ /** The tool-hook lane's launch fragment, prepended to the detached wrapper
319
+ * script (the shell the CLI starts in): a fresh inbox, the counter at 0,
320
+ * and both paths exported for the hook. Exported for tests. */
321
+ export declare function toolHookInboxFragment(paths: {
322
+ inboxPath: string;
323
+ deliveredPath: string;
324
+ }): string;
232
325
  /** SIGTERM grace before escalation: attempts × sleep = 5s. */
233
326
  export declare const REAP_TERM_WAIT_ATTEMPTS = 20;
234
327
  /** Post-SIGKILL confirm window: attempts × sleep = 2s (a KILLed process only
@@ -251,33 +344,63 @@ export declare function reapKillAndConfirmCommand(pid: number): string;
251
344
  export declare const RESIDENT_IDLE_TTL_S = 240;
252
345
  /** Watchdog poll cadence (seconds). */
253
346
  export declare const RESIDENT_WATCHDOG_POLL_S = 15;
254
- /** Anchored prefix of the CLI's terminal result event line. Anchoring keeps
255
- * the guest-side counts honest against tool output that merely CONTAINS
256
- * the substring; a serialization-order change fails SAFE (parity check
257
- * refuses adoption → cold launch — a latency cost, never correctness). */
258
- export declare const RESIDENT_RESULT_LINE_PREFIX = "{\"type\":\"result\"";
347
+ /** Shell command printing how many result lines the durable .out holds —
348
+ * the guest's turn accounting (feeder gate, idle check, adoption parity).
349
+ * Prints nothing when the file is missing: callers default an empty count
350
+ * to 0. */
351
+ export declare function cliResultCountCommand(outPath: string): string;
259
352
  /** One bounded exec printing the count of result lines at or past
260
353
  * `fromByte` (0-based) in the durable .out. Always exits 0; stdout is the
261
354
  * count (a missing file reads as 0). Hint-grade by design. */
262
355
  export declare function residentResultCountCommand(outPath: string, fromByte?: number): string;
356
+ /** One bounded exec reporting the LAST result line in the durable .out
357
+ * window [`fromByte`, `toByte`): prints `none` (no result line), `error`
358
+ * (its `is_error` is true) or `ok`. Always exits 0 and never prints the
359
+ * line itself. The harvest's confirm for a resident result the tailer never
360
+ * parsed (review C2): a count alone cannot say whether the turn failed. The
361
+ * whole line is searched for `"is_error":true`, wherever the CLI put it
362
+ * (key order is not fixed). `toByte` bounds the window to bytes a drain
363
+ * already walked past, so a result the drain never reached (or a line still
364
+ * being written past its line-aligned offset) is not confirmed; omitted,
365
+ * the window runs to the end of the file. A window with `toByte <= fromByte`
366
+ * is empty. */
367
+ export declare function residentLastResultCommand(outPath: string, fromByte?: number, toByte?: number): string;
263
368
  /** The resident feeder: `streamInputFeederFragment` with the result-break
264
- * REPLACED by the end-file break — the ONLY guest-contract change the
265
- * resident shape needs (verified live by the spec's probe: two messages,
266
- * one process, one session file, graceful exit on `.end`). */
369
+ * REPLACED by the end-file break (verified live by the spec's probe: two
370
+ * messages, one process, one session file, graceful exit on `.end`), plus
371
+ * the TURN GATE: inbox lines are forwarded only while a delivered turn is
372
+ * still unanswered (the result count in .out is below `.served`),
373
+ * the same two numbers the idle predicate reads. Without the gate, a line
374
+ * appended after the turn's result line but before the server settled the
375
+ * turn was fed into the idle CLI, started a turn nobody tails, and was
376
+ * acked as delivered: the message was never answered, and the extra result
377
+ * line broke the next adoption's parity. With the gate that line stays
378
+ * unfed, the inject ack exits 5, and the message stays owed for the
379
+ * follow-up turn. Adoption bumps `.served` BEFORE it appends, so the next
380
+ * turn's prompt still flows. The count is a full-file grep, re-run
381
+ * only on a poll that has unfed lines AND a changed (.out size, .served)
382
+ * pair, so an idle resident holding a gated line never rescans. The
383
+ * forward is guarded like the plain feeder's: the counters advance only
384
+ * when `sed` wrote the lines into the FIFO. */
267
385
  export declare function residentFeederFragment(paths: {
268
386
  promptPath: string;
269
387
  fifoPath: string;
270
388
  inboxPath: string;
271
389
  deliveredPath: string;
272
390
  endPath: string;
391
+ outPath: string;
392
+ servedPath: string;
273
393
  }): string;
274
- /** Sets `ac_idle` (1 = between turns: every delivered turn has its result
275
- * and the inbox is fully fed). Embedded by the watchdog and the resident
276
- * heartbeat gate — one idle predicate, stated once. */
394
+ /** Sets `ac_idle` (1 = between turns: every delivered turn has its result).
395
+ * Embedded by the watchdog and the resident heartbeat gate: one idle
396
+ * predicate, stated once. Unfed inbox lines do not make a resident busy:
397
+ * the feeder's turn gate forwards a line only while results < served, so
398
+ * a line still unfed once results >= served is held until the next
399
+ * adoption bumps `.served` (a message that raced the result, owed on the
400
+ * server). Reading it as work would keep the heartbeat fresh and the
401
+ * watchdog quiet for as long as the line sat there. */
277
402
  export declare function residentIdleCheckFragment(paths: {
278
403
  outPath: string;
279
- inboxPath: string;
280
- deliveredPath: string;
281
404
  servedPath: string;
282
405
  }): string;
283
406
  /** Guest idle watchdog: reap the whole process group after
@@ -302,8 +425,6 @@ export declare function residentIdleCheckFragment(paths: {
302
425
  * process existence). */
303
426
  export declare function residentIdleWatchdogFragment(paths: {
304
427
  outPath: string;
305
- inboxPath: string;
306
- deliveredPath: string;
307
428
  servedPath: string;
308
429
  }): string;
309
430
  /** Exit-event doorbell with the turnId read from a guest FILE at push time
@@ -341,6 +462,13 @@ export declare const INSTALL_PROBE_TIMEOUT_MS = 30000;
341
462
  * detached launch — the session images bake only `claude`, so codex/
342
463
  * opencode/cursor/droid MUST be installable at launch on a fresh machine. */
343
464
  export declare const CLI_INSTALL_TIMEOUT_MS = 300000;
465
+ /** Recover the detached runner's pid from its durable pidfile after the
466
+ * launch exec's STREAM died (deadline_exceeded, canceled context, any wire
467
+ * fault). Reads over the file transport with a short retry so the race
468
+ * where the launch shell is still writing the pidfile is absorbed.
469
+ * Exported for the server's workflow-lane launch (turn-workflow/tailer.ts
470
+ * launchDetachedRunner), which recovers the same way. */
471
+ export declare function recoverLaunchPid(read: (path: string) => Promise<string>, pidPath: string): Promise<number>;
344
472
  /** Single-quote a value for safe interpolation into a `sh -c` command line. */
345
473
  export declare function shellQuote(value: string): string;
346
474
  /** $HOME-relative path of the platform-managed session env file. The server
@@ -354,6 +482,16 @@ export declare const SESSION_ENV_FILE_RELPATH = ".agent-compose/session-env.sh";
354
482
  * when no env file is configured. Errors are swallowed — a missing or
355
483
  * unreadable file must never fail a turn. */
356
484
  export declare function sessionEnvSourceFragment(relPath: string | undefined): string;
485
+ /** Shell command printing a fingerprint of the session env file's current
486
+ * bytes (POSIX `cksum`), or `absent` when there is no file. A resident
487
+ * records it next to its prompt immediately BEFORE sourcing the file, and
488
+ * an adoption compares the file's fingerprint at that moment with it: a
489
+ * fresh launch would source the file as it is now, the resident only ever
490
+ * saw the one it started with. Recording before the source keeps the
491
+ * comparison conservative — a write that lands between the two reads as a
492
+ * change. The guest file is the one truth every replica writes to, so the
493
+ * verdict never depends on which server process wrote it. */
494
+ export declare function sessionEnvFingerprintCommand(relPath: string): string;
357
495
  /** The per-turn model-credential source fragment (`RuntimeOptions.
358
496
  * credEnvFile` — an ABSOLUTE guest path), or "" when none is configured.
359
497
  * Sourced AFTER the session env file so the turn actor's credential wins.
@@ -458,9 +596,9 @@ export interface CliAgentSpec {
458
596
  * `effort` is present only when the caller configured a reasoning effort
459
597
  * AND the spec has a real knob for it (see `CliReasoningEffort`).
460
598
  * `streamInput` is true when the durable transport is feeding stdin as a
461
- * live message stream (see `CliAgentSpec.streamInput`): `promptPath` is
462
- * then the FIFO the feeder writes, and the CLI must be invoked in its
463
- * stream-input mode (claude: `--input-format stream-json`). */
599
+ * live message stream (the "stdin-stream" `midTurnInput` transport):
600
+ * `promptPath` is then the FIFO the feeder writes, and the CLI must be
601
+ * invoked in its stream-input mode (claude: `--input-format stream-json`). */
464
602
  buildCommand(args: {
465
603
  promptPath: string;
466
604
  sessionId?: string;
@@ -502,37 +640,15 @@ export interface CliAgentSpec {
502
640
  args: string[];
503
641
  env?: Record<string, string>;
504
642
  };
505
- /** Mid-turn STREAM-INPUT support (the delivery half of
506
- * `injectUserMessage`). When present AND the durable detached transport
507
- * is in play, the CLI's stdin is fed from a FIFO by an in-guest feeder:
508
- * the prompt line first, then any lines appended to the turn's durable
509
- * inbox file — so a steering-capable CLI (claude: queued user input is
510
- * folded into the RUNNING turn at the next tool boundary; verified live
511
- * against claude 2.1.233) receives user messages while the turn runs.
512
- * The feeder stops at the CLI's terminal result line (a message that
513
- * races the result is NOT forwarded — it stays owed) and closes the
514
- * FIFO, which is what ends the CLI process (stream-input CLIs exit on
515
- * stdin EOF, not after a result). Absent → the prompt file is the whole
516
- * stdin, exactly as before. */
517
- streamInput?: {
518
- /** Serialise the opening prompt into ONE stream-input stdin line
519
- * (claude: a stream-json user message). No trailing newline — the
520
- * transport owns line framing. */
521
- promptLine(prompt: string): string;
522
- /** Serialise one mid-turn user message into ONE stdin line. No
523
- * trailing newline. */
524
- messageLine(text: string): string;
525
- /** Serialise ONE in-band interrupt control line (claude: a stream-json
526
- * `control_request` with subtype "interrupt") — the ESC equivalent.
527
- * The CLI's control layer handles these immediately, MID-STEP
528
- * included: the running tool call aborts (its tool_result records the
529
- * harness's own rejection text), the run ends with an
530
- * `error_during_execution` result within ~100ms, and the session file
531
- * stays `--resume`-able with the whole turn context. Verified live
532
- * against claude 2.1.236. Absent → the runtime has no in-band
533
- * interrupt; callers fall back to kill semantics. */
534
- interruptLine?(requestId: string): string;
535
- };
643
+ /** Mid-turn INPUT support (the delivery half of `injectUserMessage`): how
644
+ * a line appended to the turn's durable inbox reaches the CLI while its
645
+ * turn runs — see the transports above. Engages only on the durable
646
+ * detached transport (the inbox, the delivered counter and the feeder or
647
+ * hook live in the guest; single-exec transports keep the plain
648
+ * prompt-file stdin). Absent → the runtime cannot take a message
649
+ * mid-turn: every inject answers "unsupported" and the message waits for
650
+ * the turn boundary. */
651
+ midTurnInput?: MidTurnInputSpec;
536
652
  }
537
653
  export declare class CliAgentRunner implements ModelExecutionContract {
538
654
  private readonly sandbox;
@@ -611,12 +727,13 @@ export declare class CliAgentRunner implements ModelExecutionContract {
611
727
  /** Deliver one user message INTO the live turn — see the contract doc
612
728
  * (types/runtime.ts). Appends a stream-input line to the turn's durable
613
729
  * inbox and waits for the in-guest feeder's delivered-counter ack; only
614
- * an acked forward (pre-result, into the CLI's stdin) reports
615
- * "delivered". Guest exit 5 (ack window exhausted with the runner still
616
- * alive — a CLI that is not draining stdin mid-step) reports "pending":
617
- * the appended line may still be read when the current step finishes,
618
- * but it was NOT seen yet. Never throws. */
619
- injectUserMessage(text: string): Promise<"delivered" | "pending" | "closed" | "unsupported">;
730
+ * an acked forward (pre-result, written into the CLI's stdin pipe)
731
+ * reports "delivered", and even then the model reads the line only at
732
+ * its next tool boundary (review D8). Guest exit 5 (ack window exhausted
733
+ * with the runner still alive: the feeder is not forwarding) reports
734
+ * "pending": the appended line may still be forwarded later, but it was
735
+ * NOT seen yet. Never throws. */
736
+ injectUserMessage(text: string, opts?: MidTurnEnvelopeOptions): Promise<"delivered" | "pending" | "closed" | "unsupported">;
620
737
  /** Request an in-band step interrupt of the LIVE turn — the ESC
621
738
  * equivalent; see the contract doc (types/runtime.ts). Appends the
622
739
  * spec's interrupt control line to the turn's durable inbox; the guest
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A model id as a harness names it — the one derivation behind every
3
+ * `model_report` the Claude runtimes emit (AgentMessageModelReport): the
4
+ * `model` on claude-code's `system`/`init` event and the `message.model` on
5
+ * each `assistant` event, which the Agent SDK's messages mirror.
6
+ *
7
+ * - claude-code's `[1m]`-style suffix (`claude-opus-4-8[1m]`) is the CLI's
8
+ * context-window marker: a variant flag on the same model, not another
9
+ * model, so the report drops it;
10
+ * - `<synthetic>` is the harness's own voice (slash-command stdout,
11
+ * advisories) and names no model;
12
+ * - anything that is not a non-empty string names none.
13
+ *
14
+ * Machine-format rules only; nothing here reads words.
15
+ */
16
+ export declare function reportedModelId(raw: unknown): string | undefined;
@@ -33,7 +33,7 @@
33
33
  * sessions this is the gateway-minted session virtual key pointed at the
34
34
  * token-metering gateway's Anthropic passthrough (ADR-0039).
35
35
  */
36
- import type { AgentMessage, AgentMessageCompaction, AgentMessageTaskNotification, AgentMessageTaskProgress } from "../index.js";
36
+ import type { AgentMessage, AgentMessageCompaction, AgentMessageModelUsage, AgentMessagePlanLimits, AgentMessageTaskNotification, AgentMessageTaskProgress } from "../index.js";
37
37
  import { type CliAgentSpec, type CliReasoningEffort } from "./_cli-agent.js";
38
38
  /** Pinned Claude Code ACP adapter (Zed's npm shim — `claude` has no native
39
39
  * ACP mode). Pinned EXACT, not a range: the adapter's README warns of
@@ -82,6 +82,18 @@ export declare function parseSystemTaskProgress(p: Record<string, unknown>, time
82
82
  * compact_result stays unmapped). Pure and tolerant over untrusted harness
83
83
  * JSON. Exported for tests. */
84
84
  export declare function parseSystemCompaction(p: Record<string, unknown>, timestamp: string): AgentMessageCompaction | null;
85
+ /** One `rate_limit_event` mapped onto the structured plan-limits message,
86
+ * or null when the event names no recognisable status. Pure and tolerant
87
+ * over untrusted harness JSON: every field is forwarded only when present
88
+ * and well-typed; nothing is defaulted or invented. Exported for tests. */
89
+ export declare function parsePlanLimits(p: Record<string, unknown>, timestamp: string): AgentMessagePlanLimits | null;
90
+ /** The terminal result's per-model report (`result.modelUsage`, keyed by
91
+ * the raw model string) mapped onto the protocol's per-model shape, or
92
+ * undefined when the result carries none. Counts the harness did not
93
+ * report stay absent (the four classes default to 0 only when the entry
94
+ * exists at all — an entry IS a report of that model). Pure; exported for
95
+ * tests. */
96
+ export declare function parseModelUsage(raw: unknown): Record<string, AgentMessageModelUsage> | undefined;
85
97
  /** Claude Code's real reasoning knob is its own `--effort <level>` flag
86
98
  * (low|medium|high|xhigh|max — verified against `claude -p --help`). The
87
99
  * CliReasoningEffort union IS the CLI's vocabulary, so the level rides the
@@ -90,6 +102,53 @@ export declare function parseSystemCompaction(p: Record<string, unknown>, timest
90
102
  * `MAX_THINKING_TOKENS` env mapping is gone: the CLI deprecated it (treated
91
103
  * as on/off on current models) and it could never express xhigh/max. */
92
104
  export declare const CLAUDE_CODE_EFFORT_LEVELS: readonly CliReasoningEffort[];
105
+ /** The Bash PreToolUse hook rtk installs for Claude Code (`rtk init -g
106
+ * --hook-only` writes exactly this entry — matcher `Bash`, command
107
+ * `rtk hook claude` — into ~/.claude/settings.json; the image bakes
108
+ * RTK_VERSION, sandbox/baked-clis.ts, which this payload was run against).
109
+ * `rtk hook claude` reads the PreToolUse JSON on stdin and, when it has a
110
+ * filter for the command, answers with `updatedInput` rewriting `git
111
+ * status` to `rtk git status` (ls, find, grep, test runners, bun, curl,
112
+ * docker, …), so the model reads rtk's compact output. A command it
113
+ * cannot compress — an unknown tool, a pipe into one, command or process
114
+ * substitution, a heredoc, a file redirect, anything already prefixed
115
+ * `rtk`, or a `RTK_DISABLED=1` prefix — gets no output and exit 0, which
116
+ * Claude Code treats as "no decision": the original command runs
117
+ * unchanged (all verified against the binary).
118
+ *
119
+ * The wrapper fails OPEN both ways. `command -v` covers images without
120
+ * rtk (the devbox, a bare Vercel VM): there a bare `rtk hook claude` would
121
+ * fail every Bash call's hook with exit 127 — non-blocking, but a stderr
122
+ * warning per call; rtk's own legacy shell hook degraded the same way
123
+ * ("binary not found: exit 0"). The trailing `exit 0` covers an rtk that
124
+ * cannot answer: Claude Code treats a PreToolUse hook's exit 2 as a BLOCK
125
+ * of the tool call with stderr fed to the model, and clap exits 2 with
126
+ * its usage for a subcommand it does not know — which is how the codex
127
+ * hook (RTK_CODEX_HOOK_COMMAND, codex.ts) blocked every codex shell
128
+ * command on 2026-10-03, when the image's rtk was a cache-served 0.45.0.
129
+ * rtk's hooks never exit non-zero on purpose, so a non-zero exit is a
130
+ * broken rtk, and the compressor must never cost the worker its shell:
131
+ * the exit code is dropped, and the smoke gate's rtk-hook-rewrite check
132
+ * is what proves the rewrite itself. */
133
+ export declare const RTK_BASH_HOOK_COMMAND = "command -v rtk >/dev/null 2>&1 && rtk hook claude; exit 0";
134
+ /** Settings every platform `claude` launch passes as `--settings` (inline
135
+ * JSON). Claude Code MERGES hook entries across its settings sources
136
+ * instead of replacing them (user → project → local → flag → managed), so
137
+ * this rides beside whatever the machine's own ~/.claude/settings.json
138
+ * carries, and nothing is written to that file — the right outcome, since
139
+ * the server never owns it: home carry tar-restores it whole across
140
+ * machines and the image capture strips it as personal state. */
141
+ export declare const CLAUDE_CODE_PLATFORM_SETTINGS: {
142
+ readonly hooks: {
143
+ readonly PreToolUse: readonly [{
144
+ readonly matcher: "Bash";
145
+ readonly hooks: readonly [{
146
+ readonly type: "command";
147
+ readonly command: "command -v rtk >/dev/null 2>&1 && rtk hook claude; exit 0";
148
+ }];
149
+ }];
150
+ };
151
+ };
93
152
  export declare const claudeCodeSpec: CliAgentSpec;
94
153
  export interface ClaudeCodeRuntimeConfig {
95
154
  /** Claude model id (`--model`). Omit to use the CLI's configured default. */
@@ -1,4 +1,4 @@
1
- /** Claude Agent SDK runtime — replaces the old Claude CLI subprocess runtime. */
1
+ /** Claude Agent SDK runtime. */
2
2
  import { type ThinkingConfig } from "@anthropic-ai/claude-agent-sdk";
3
3
  import type { AgentMessage, ModelExecutionContract, RuntimeOptions, SandboxProvider, ToolCallGateResult } from "../index.js";
4
4
  import type { ProcessorContext, ToolCall } from "../processors/processor.js";
@@ -5,14 +5,102 @@
5
5
  * only stream-parse what it prints.
6
6
  *
7
7
  * Auth: set `OPENAI_API_KEY` (or `CODEX_API_KEY`) in the sandbox env via a
8
- * workflow secret. The runtime installs the `codex` CLI (`@openai/codex`) on
9
- * demand — no image baking needed; pair with `snapshots: { bootFrom: "reuse" }`
10
- * to install once and boot from the captured snapshot on every run after.
8
+ * workflow secret. The E2B session image bakes the pinned `codex` CLI
9
+ * (`@openai/codex@CODEX_CLI_VERSION`); on any other machine the runtime
10
+ * installs that same version on demand (pair with
11
+ * `snapshots: { bootFrom: "reuse" }` to install once and boot from the
12
+ * captured snapshot on every run after).
11
13
  *
12
14
  * Verified against codex-cli 0.124.0: `codex exec --json` + resume-by-thread,
13
- * with the command_execution / reasoning / agent_message item shapes below.
15
+ * with the command_execution / reasoning / agent_message item shapes below;
16
+ * the plan's `todo_list` item against codex-rs rust-v0.153.4 and
17
+ * rust-v0.159.2 (see codexPlanMessages); the command_execution /
18
+ * agent_message / turn.completed shapes re-verified live against 0.160.0
19
+ * (the pin), whose plan-tool sources are byte-identical to rust-v0.159.2.
14
20
  */
15
21
  import { type CliAgentSpec, type CliReasoningEffort } from "./_cli-agent.js";
22
+ /** The Bash PreToolUse hook rtk installs for Codex (`rtk init -g --codex`
23
+ * writes exactly this entry — matcher `Bash`, command `rtk hook codex` —
24
+ * into $CODEX_HOME/hooks.json). `rtk hook codex` exists since rtk 0.50.0;
25
+ * the image bakes RTK_VERSION (sandbox/baked-clis.ts), which this payload
26
+ * was run against. Codex's shell tool matches the `Bash` matcher (codex-rs
27
+ * rust-v0.160.0, the pinned CODEX_CLI_VERSION; identical at
28
+ * rust-v0.159.2), and the hook runs under `$SHELL -lc` with the PreToolUse
29
+ * JSON on stdin. `rtk hook codex` answers `permissionDecision: "allow"` +
30
+ * `updatedInput` rewriting `git status` to `rtk git status` when it has a
31
+ * filter for the command; Codex applies the replacement before its own
32
+ * approval and sandbox checks. For a command it cannot compress (an
33
+ * unknown tool, a pipe into one, substitutions, heredocs, redirects, a
34
+ * `RTK_DISABLED=1` prefix, an unknown permission mode) it prints nothing
35
+ * and exits 0, and Codex runs the original unchanged.
36
+ *
37
+ * The wrapper fails OPEN both ways, like RTK_BASH_HOOK_COMMAND
38
+ * (claude-code.ts). `command -v` covers an image without rtk (the devbox,
39
+ * a bare Vercel VM): silent, exit 0, so Codex runs the command instead of
40
+ * failing every shell call's hook with 127. The trailing `exit 0` covers
41
+ * an rtk that cannot answer: Codex treats a PreToolUse hook's exit 2 with
42
+ * stderr as a BLOCK of the tool call (codex-rs hooks/src/events/
43
+ * pre_tool_use.rs at rust-v0.160.0 — the model reads "Command blocked by
44
+ * PreToolUse hook: <stderr>"), and clap exits 2 with its usage on stderr
45
+ * for a subcommand it does not know. That was 2026-10-03: the image's rtk
46
+ * was a cache-served 0.45.0 with no `hook codex`, and this hook — `exec`
47
+ * handing rtk's exit code to Codex — blocked every shell command of every
48
+ * codex session. rtk's hooks never exit non-zero on purpose (they fail
49
+ * open with no stdout), so a non-zero exit is always a broken rtk, and the
50
+ * compressor must never cost the worker its shell: the exit code is
51
+ * dropped, and the smoke gate's codex-rtk-hook-rewrite check is what
52
+ * proves the rewrite itself. */
53
+ export declare const RTK_CODEX_HOOK_COMMAND = "command -v rtk >/dev/null 2>&1 && rtk hook codex; exit 0";
54
+ /** The PostToolUse command hook that carries a mid-turn message into a
55
+ * RUNNING codex turn (the 2026-10-02 relayed-steer incident: three of the
56
+ * owner's instructions waited 48-51 minutes for a codex build to end).
57
+ * Codex runs it, under `$SHELL -lc` with the event JSON on stdin and the
58
+ * codex process's own environment, after every tool call it completes
59
+ * (codex-rs core/src/tools/registry.rs → hook_runtime.rs at rust-v0.160.0,
60
+ * the pin). The launch wrapper exported the turn's inbox and delivered-
61
+ * counter paths into that environment (sdk _cli-agent.ts
62
+ * toolHookInboxFragment); the hook forwards the inbox lines past the
63
+ * counter as the hook's `additionalContext`, which codex records as
64
+ * developer context in the live turn's history before the model's next
65
+ * request (hook_runtime.rs record_additional_contexts), then advances the
66
+ * counter — the same ack the server's inject lane trusts for the stdin
67
+ * lane. Not a platform turn (no inbox in the environment): silent, exit 0.
68
+ * Codex's default spill threshold for a hook's additional context is 2,500
69
+ * tokens (hooks/src/output_spill.rs); a longer message reaches the model
70
+ * as a preview plus a file pointer, which a steer never is. */
71
+ export declare const CODEX_MID_TURN_HOOK_COMMAND: string;
72
+ /** $CODEX_HOME/hooks.json for every platform Codex session (the server
73
+ * writes it at boot, session-runtime-config.ts). Codex only RUNS a
74
+ * user-level hook it has persisted trust for — the TUI's review prompt
75
+ * has no headless counterpart, and `--dangerously-bypass-hook-trust` would
76
+ * run a cloned repository's `.codex/hooks.json` unreviewed too — so the
77
+ * server also writes the trust record for each of these exact hooks into
78
+ * the config.toml it generates (sandbox/codex-hooks.ts). Verified live
79
+ * against codex 0.159.2 and 0.160.0 for the rtk hook: with the record,
80
+ * `codex exec` rewrote `git status` through rtk with no bypass flag;
81
+ * without it, the hook was skipped.
82
+ *
83
+ * The PostToolUse group carries NO matcher: codex runs a matcher-less hook
84
+ * after every tool it completes (hooks/src/events/common.rs
85
+ * matches_matcher: an absent matcher is a match), so a mid-turn message
86
+ * lands at the next tool step whatever the tool was. */
87
+ export declare const CODEX_PLATFORM_HOOKS: {
88
+ readonly hooks: {
89
+ readonly PreToolUse: readonly [{
90
+ readonly matcher: "Bash";
91
+ readonly hooks: readonly [{
92
+ readonly type: "command";
93
+ readonly command: "command -v rtk >/dev/null 2>&1 && rtk hook codex; exit 0";
94
+ }];
95
+ }];
96
+ readonly PostToolUse: readonly [{
97
+ readonly hooks: readonly [{
98
+ readonly type: "command";
99
+ readonly command: string;
100
+ }];
101
+ }];
102
+ };
103
+ };
16
104
  /** True when an error-item message is a known codex advisory (log-level
17
105
  * noise), not a real error. Exported for tests. */
18
106
  export declare function isCodexAdvisoryNoise(text: string): boolean;
@@ -28,8 +116,8 @@ export declare function codexWriterLockPreflight(threadId: string, waitAttempts?
28
116
  * `mapEvent` is the golden the ACP normaliser is asserted equal to. Not part
29
117
  * of the public runtime surface — `createCodexRuntime` stays the entry point. */
30
118
  export declare const codexSpec: CliAgentSpec;
31
- /** The effort levels codex actually has (`model_reasoning_effort`):
32
- * low|medium|high|xhigh — no "max" (that level is Claude Code's alone). */
119
+ /** The effort levels this runtime passes to codex (`model_reasoning_effort`):
120
+ * low|medium|high|xhigh, the levels every codex model takes. */
33
121
  export type CodexReasoningEffort = Exclude<CliReasoningEffort, "max">;
34
122
  export interface CodexRuntimeConfig {
35
123
  /** Codex model id (`-m`). Omit to use the codex CLI's configured default. */
@@ -0,0 +1,10 @@
1
+ /**
2
+ * codex's mid-turn hook, run for real: the PostToolUse command hook
3
+ * (CODEX_MID_TURN_HOOK_COMMAND) executed under `sh` the way codex runs a
4
+ * command hook — the event JSON on stdin, the codex process's environment —
5
+ * against a constructed inbox, and the launch fragment that gives the codex
6
+ * process that environment (toolHookInboxFragment). These pin what the
7
+ * guest DOES: the JSON codex parses, the delivered counter the server's ack
8
+ * trusts, and silence everywhere else.
9
+ */
10
+ export {};
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The harness's own report of the model it runs (AgentMessageModelReport,
3
+ * owner 2026-10-06: "what model actually did the work"), one case per
4
+ * branch of the one derivation (_reported-model.ts) and of the specs that
5
+ * emit it:
6
+ * - claude-code's `system`/`init` names the session's model, minus the
7
+ * CLI's `[1m]` context-window marker; an init without one names none;
8
+ * - a top-level `assistant` event names the model the API answered with,
9
+ * before its content; the harness's own voice (`<synthetic>`) and a
10
+ * subagent's event (`parent_tool_use_id`) name none;
11
+ * - codex's and opencode's streams carry no model: their specs emit no
12
+ * report for the events that would carry one.
13
+ */
14
+ export {};