@agent-compose/sdk 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -189
- package/dist/agent/agent-context.d.ts +3 -3
- package/dist/agent/agent-loop.d.ts +4 -5
- package/dist/agent/perf-sampler.d.ts +27 -2
- package/dist/agent/run-agent.d.ts +1 -1
- package/dist/client.d.ts +105 -52
- package/dist/directives.d.ts +3 -3
- package/dist/display.d.ts +7 -0
- package/dist/errors.d.ts +1 -1
- package/dist/generated/agentc-commands.d.ts +34 -0
- package/dist/index.d.ts +12 -12
- package/dist/index.js +716 -203
- package/dist/request-context/request-context.d.ts +1 -1
- package/dist/runtimes/_cli-agent.d.ts +182 -68
- package/dist/runtimes/claude-code.d.ts +60 -1
- package/dist/runtimes/claude.d.ts +1 -1
- package/dist/runtimes/codex.d.ts +94 -6
- package/dist/runtimes/codex.mid-turn-hook.test.d.ts +10 -0
- package/dist/runtimes/openai-desktop.js +686 -199
- package/dist/runtimes/opencode.d.ts +48 -11
- package/dist/runtimes/opencode.test.d.ts +14 -0
- package/dist/sandbox/baked-clis.d.ts +75 -0
- package/dist/sandbox/exec-stream.d.ts +1 -2
- package/dist/sandbox/network-policy.d.ts +23 -5
- package/dist/sandbox.d.ts +4 -2
- package/dist/step-invocation/protocol.d.ts +3 -4
- package/dist/step-invocation/server.d.ts +2 -2
- package/dist/step-invocation/types.d.ts +1 -1
- package/dist/types/api-conversations.d.ts +442 -29
- package/dist/types/api-factory.d.ts +78 -8
- package/dist/types/api-projects.d.ts +480 -0
- package/dist/types/api-runs.d.ts +8 -0
- package/dist/types/api-scopes.d.ts +32 -3
- package/dist/types/conversation-stream.d.ts +5 -0
- package/dist/types/execution-context.d.ts +1 -1
- package/dist/types/protocol.d.ts +65 -2
- package/dist/types/runtime.d.ts +9 -2
- package/dist/types/workflow-metadata.d.ts +2 -4
- package/dist/types/workflow-plan.d.ts +1 -3
- package/dist/utils/bundler.d.ts +23 -0
- package/dist/workflow-steps/observability.d.ts +2 -3
- package/dist/workflow-steps/runner.d.ts +5 -8
- package/dist/workflow-steps/types.d.ts +8 -10
- package/dist/workflow-steps/workflow.d.ts +2 -1
- package/dist/workflows/engine.d.ts +3 -5
- package/dist/workflows/invoke-child.d.ts +2 -2
- package/package.json +2 -2
- package/src/agent/agent-context.ts +168 -125
- package/src/agent/agent-loop.ts +5 -4
- package/src/agent/perf-sampler.ts +54 -3
- package/src/agent/run-agent.ts +1 -1
- package/src/client.ts +191 -71
- package/src/directives.ts +3 -3
- package/src/display.ts +12 -0
- package/src/errors.ts +1 -0
- package/src/generated/agentc-commands.ts +571 -0
- package/src/index.ts +54 -21
- package/src/pause/pause-core.ts +2 -1
- package/src/request-context/request-context.ts +1 -1
- package/src/runtimes/_cli-agent.ts +306 -122
- package/src/runtimes/claude-code.ts +179 -9
- package/src/runtimes/claude.ts +1 -1
- package/src/runtimes/codex.ts +188 -19
- package/src/runtimes/opencode.ts +195 -26
- package/src/sandbox/baked-clis.ts +86 -0
- package/src/sandbox/exec-stream.ts +1 -2
- package/src/sandbox/network-policy.ts +51 -7
- package/src/sandbox/providers/e2b.ts +3 -3
- package/src/sandbox/providers/vercel.ts +6 -6
- package/src/sandbox.ts +8 -2
- package/src/step-invocation/invoker.ts +2 -6
- package/src/step-invocation/protocol.ts +3 -4
- package/src/step-invocation/server.ts +2 -2
- package/src/types/api-conversations.ts +366 -23
- package/src/types/api-factory.ts +74 -8
- package/src/types/api-projects.ts +443 -0
- package/src/types/api-runs.ts +5 -0
- package/src/types/api-scopes.ts +32 -3
- package/src/types/conversation-stream.ts +5 -0
- package/src/types/execution-context.ts +1 -1
- package/src/types/protocol.ts +67 -1
- package/src/types/runtime.ts +8 -2
- package/src/types/sandbox-environment.ts +1 -2
- package/src/types/workflow-metadata.ts +2 -4
- package/src/types/workflow-plan.ts +1 -3
- package/src/utils/bundler.ts +88 -19
- package/src/workflow-steps/observability.ts +2 -3
- package/src/workflow-steps/runner.ts +5 -8
- package/src/workflow-steps/types.ts +8 -10
- package/src/workflow-steps/workflow.ts +2 -1
- package/src/workflows/engine.ts +3 -5
- package/src/workflows/invoke-child.ts +2 -2
- package/dist/generated/verb-synopsis.d.ts +0 -34
- package/dist/pause/__tests__/errors.test.d.ts +0 -1
- package/dist/pause/__tests__/wrappers.test.d.ts +0 -1
- package/dist/step-invocation/__tests__/protocol.test.d.ts +0 -1
- package/src/generated/verb-synopsis.ts +0 -544
|
@@ -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 `
|
|
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
|
|
@@ -241,13 +241,16 @@ export function probeDeathVerdict(
|
|
|
241
241
|
return null;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
-
/** The
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
|
|
244
|
+
/** The CLI's stderr complaint when the thread it was told to resume does
|
|
245
|
+
* not exist on this machine: claude's `--resume <id>` "No conversation
|
|
246
|
+
* found with session ID" (the 2026-08-18 split-brain forensic) and
|
|
247
|
+
* opencode's `--session <id>` "Error: Session not found" (in the CLI's
|
|
248
|
+
* red-bold ANSI dressing; 1.18.34). When a nonzero exit's stderr carries
|
|
249
|
+
* it, the transport surfaces the stderr as the turn's LAST error event
|
|
250
|
+
* even though a bare result error already streamed — the consumer's
|
|
251
|
+
* classifier needs the specific complaint, not the generic
|
|
252
|
+
* `error_during_execution` token. */
|
|
253
|
+
export const RESUME_TARGET_MISSING_STDERR = /no conversation found with session id|error:\s*(?:\x1b\[[0-9;]*m)*\s*session not found/i;
|
|
251
254
|
|
|
252
255
|
/** One durable-trio reading, parsed from the probe exec's single line. */
|
|
253
256
|
export interface TurnProbeReading {
|
|
@@ -336,18 +339,24 @@ export function livenessVerdictFromReading(reading: TurnProbeReading | null): Ru
|
|
|
336
339
|
return "dead";
|
|
337
340
|
}
|
|
338
341
|
|
|
339
|
-
// ── Mid-turn
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
342
|
+
// ── Mid-turn input (the delivery half of injectUserMessage) ─────────────────
|
|
343
|
+
// A message for a RUNNING turn is appended as one line to the turn's durable
|
|
344
|
+
// INBOX file; the DELIVERED counter file beside it is the ack the server
|
|
345
|
+
// trusts, advancing only once the line was handed to the CLI. Two transports
|
|
346
|
+
// hand it over (CliAgentSpec.midTurnInput):
|
|
347
|
+
// - "stdin-stream": the durable transport's stdin is a FIFO fed by an
|
|
348
|
+
// in-guest subshell — the prompt line first, then any complete inbox
|
|
349
|
+
// lines. A steering-capable CLI (claude -p --input-format stream-json)
|
|
350
|
+
// folds a message that arrives while the turn runs into the RUNNING turn
|
|
351
|
+
// at its next tool boundary — verified live against claude 2.1.233. The
|
|
352
|
+
// feeder checks for the CLI's terminal `result` line BEFORE each delivery
|
|
353
|
+
// (a message that races the result stays owed for the follow-up turn,
|
|
354
|
+
// never half-consumed) and stops on it; closing the FIFO is what EOFs the
|
|
355
|
+
// CLI's stdin and lets it exit.
|
|
356
|
+
// - "tool-hook": the CLI runs a platform hook after every tool call it
|
|
357
|
+
// completes (codex: a PostToolUse command hook); the hook forwards the
|
|
358
|
+
// unfed inbox lines to the model as additional context and advances the
|
|
359
|
+
// counter. The CLI's stdin stays the plain prompt file.
|
|
351
360
|
|
|
352
361
|
/** Feeder poll cadence (seconds — it is a shell `sleep`; GNU sleep accepts
|
|
353
362
|
* fractions and the durable transport only runs on the GNU guest image). */
|
|
@@ -364,10 +373,40 @@ export const INJECT_ACK_POLL_ATTEMPTS = 30;
|
|
|
364
373
|
export const INJECT_ACK_POLL_SECONDS = "0.5";
|
|
365
374
|
export const INJECT_ACK_EXEC_TIMEOUT_MS = 25_000;
|
|
366
375
|
|
|
376
|
+
/** How the guest recognises the CLI's terminal `result` event in the durable
|
|
377
|
+
* .out: a line that carries the `"type":"result"` pair and does not open
|
|
378
|
+
* with another event's type, as an awk condition over `$0` (POSIX awk, so
|
|
379
|
+
* dash, busybox and the macOS test hosts read it alike). The CLI does not
|
|
380
|
+
* keep its key order: 2.1.212 wrote `{"type":"result",…` but 2.1.278 and
|
|
381
|
+
* later write the pair near the END of the object
|
|
382
|
+
* (`{"duration_api_ms":…,"result":"…","type":"result","duration_ms":…}`), so
|
|
383
|
+
* the anchored prefix the resident contract used matched nothing on the
|
|
384
|
+
* pinned 2.1.283. Every other event the CLI writes opens with its own type
|
|
385
|
+
* (`{"type":"assistant",…`, `{"type":"stream_event",…`), which keeps a tool
|
|
386
|
+
* call whose input holds the same pair from counting; tool output and model
|
|
387
|
+
* text ride inside JSON strings, where the quotes are escaped. */
|
|
388
|
+
export const CLI_RESULT_LINE_AWK =
|
|
389
|
+
'index($0, "\\"type\\":\\"result\\"") > 0 && '
|
|
390
|
+
+ '(index($0, "{\\"type\\":\\"") != 1 || index($0, "{\\"type\\":\\"result\\"") == 1)';
|
|
391
|
+
|
|
392
|
+
/** An awk program over the lines on stdin (or the files after it) that
|
|
393
|
+
* applies `action` to each result line and runs `end` at the end. */
|
|
394
|
+
function resultLineAwk(action: string, end: string): string {
|
|
395
|
+
return `awk ${shellQuote(`${CLI_RESULT_LINE_AWK} { ${action} } END { ${end} }`)}`;
|
|
396
|
+
}
|
|
397
|
+
|
|
367
398
|
/** The in-guest feeder fragment, prepended to the detached wrapper script.
|
|
368
399
|
* Runs inside the setsid session (group-kill reaps it) and keys its loop to
|
|
369
400
|
* the wrapper's own pid (`$$`), like the heartbeat subshell. Exported for
|
|
370
|
-
* tests.
|
|
401
|
+
* tests.
|
|
402
|
+
*
|
|
403
|
+
* The forward is GUARDED: the fed and delivered counters advance only
|
|
404
|
+
* when `sed` wrote the lines. The loop keys on the wrapper, which outlives
|
|
405
|
+
* a CLI that died without a result line by the sentinel write and the
|
|
406
|
+
* exit push; in that window the FIFO has no reader, `sed` dies of SIGPIPE,
|
|
407
|
+
* and an unguarded counter still advanced, so the ack exited 0 for a line
|
|
408
|
+
* nobody could read and the settle sealed it (review C1). Guarded, the ack
|
|
409
|
+
* sees no advance and exits 4 or 5, and the message stays owed. */
|
|
371
410
|
export function streamInputFeederFragment(paths: {
|
|
372
411
|
promptPath: string; fifoPath: string; inboxPath: string;
|
|
373
412
|
deliveredPath: string; outPath: string;
|
|
@@ -376,11 +415,11 @@ export function streamInputFeederFragment(paths: {
|
|
|
376
415
|
return `rm -f ${q(paths.fifoPath)}; mkfifo ${q(paths.fifoPath)}; `
|
|
377
416
|
+ `: > ${q(paths.inboxPath)}; echo 0 > ${q(paths.deliveredPath)}; `
|
|
378
417
|
+ `( cat ${q(paths.promptPath)}; ac_fed=0; while kill -0 "$$" 2>/dev/null; do `
|
|
379
|
-
+ `if tail -c ${INJECT_FEEDER_RESULT_TAIL_BYTES} ${q(paths.outPath)} 2>/dev/null |
|
|
418
|
+
+ `if tail -c ${INJECT_FEEDER_RESULT_TAIL_BYTES} ${q(paths.outPath)} 2>/dev/null | ${resultLineAwk("f = 1", "exit f ? 0 : 1")}; then break; fi; `
|
|
380
419
|
+ `ac_lines=$(wc -l < ${q(paths.inboxPath)} 2>/dev/null || echo 0); ac_lines=\${ac_lines:-0}; `
|
|
381
420
|
+ `if [ "$ac_lines" -gt "$ac_fed" ]; then `
|
|
382
|
-
+ `sed -n "$((ac_fed+1)),$((ac_lines))p" ${q(paths.inboxPath)}
|
|
383
|
-
+ `echo "$ac_fed" > ${q(paths.deliveredPath)}; fi; `
|
|
421
|
+
+ `sed -n "$((ac_fed+1)),$((ac_lines))p" ${q(paths.inboxPath)} && { ac_fed=$ac_lines; `
|
|
422
|
+
+ `echo "$ac_fed" > ${q(paths.deliveredPath)}; }; fi; `
|
|
384
423
|
+ `sleep ${INJECT_FEEDER_POLL_SECONDS}; done ) > ${q(paths.fifoPath)} 2>/dev/null </dev/null & `;
|
|
385
424
|
}
|
|
386
425
|
|
|
@@ -395,18 +434,44 @@ export function streamInputFeederFragment(paths: {
|
|
|
395
434
|
* activity) MUST wrap through here — the wrapper rides only the wire to
|
|
396
435
|
* the model; the stored conversation row keeps the user's raw text.
|
|
397
436
|
* The text is pinned by tests: change it deliberately or not at all. */
|
|
398
|
-
export function wrapMidTurnUserMessage(text: string): string {
|
|
399
|
-
|
|
437
|
+
export function wrapMidTurnUserMessage(text: string, opts?: MidTurnEnvelopeOptions): string {
|
|
438
|
+
const relayedFrom = opts?.relayedFrom?.trim();
|
|
439
|
+
// A person's own words RELAYED by the agent that owns the worker's thread
|
|
440
|
+
// (review D2, 2026-09-25): the envelope names who said them, because they
|
|
441
|
+
// are not the session user's words and "The user sent" would misattribute
|
|
442
|
+
// them. The relayed row's own text carries the SAID marker and its frame.
|
|
443
|
+
// The thread agent that owns this worker, in its own words (owner decision
|
|
444
|
+
// 2026-09-27): not the session user either. The row's own text closes with
|
|
445
|
+
// its trust frame.
|
|
446
|
+
const head = relayedFrom
|
|
447
|
+
? `A message relaying ${relayedFrom}'s own words arrived while you were working:\n\n`
|
|
448
|
+
: opts?.fromOwnerAgent
|
|
449
|
+
? "The agent that owns your thread sent a message while you were working:\n\n"
|
|
450
|
+
: "The user sent a new message while you were working:\n\n";
|
|
451
|
+
return head
|
|
400
452
|
+ text
|
|
401
453
|
+ "\n\nAddress the message above as you continue this turn.";
|
|
402
454
|
}
|
|
403
455
|
|
|
456
|
+
/** Who a mid-turn message speaks for, when it is not the session user.
|
|
457
|
+
* `relayedFrom` = the display name of the person whose own words an agent
|
|
458
|
+
* relayed verbatim; `fromOwnerAgent` = the thread agent that owns this
|
|
459
|
+
* worker, in its own words; both absent = the session user's own message. */
|
|
460
|
+
export interface MidTurnEnvelopeOptions {
|
|
461
|
+
relayedFrom?: string | null;
|
|
462
|
+
fromOwnerAgent?: boolean;
|
|
463
|
+
}
|
|
464
|
+
|
|
404
465
|
/** The one delivery-ack exec `injectUserMessage` runs: append the message
|
|
405
466
|
* line to the durable inbox, then wait for the feeder's delivered counter
|
|
406
|
-
* to cover it. Exit 0 =
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
467
|
+
* to cover it. Exit 0 = written into the CLI's stdin pipe pre-result,
|
|
468
|
+
* which is NOT "the model saw it": the CLI surfaces the line at its next
|
|
469
|
+
* tool boundary, so a kill before then loses it, and the settle's kill
|
|
470
|
+
* terminals hold the anchor at the pre-inject dispatch head for that
|
|
471
|
+
* reason (review D8). 4 = the runner died first; 5 = not forwarded within
|
|
472
|
+
* the ack window (the feeder is not forwarding: it stopped on the result,
|
|
473
|
+
* the resident turn gate is closed, or the guest is crawling). Both
|
|
474
|
+
* non-zero exits mean "leave the message owed". Exported for tests. */
|
|
410
475
|
export function injectAppendAndAckCommand(args: {
|
|
411
476
|
line: string; target: number; pid: number;
|
|
412
477
|
inboxPath: string; deliveredPath: string;
|
|
@@ -420,6 +485,71 @@ export function injectAppendAndAckCommand(args: {
|
|
|
420
485
|
+ `sleep ${INJECT_ACK_POLL_SECONDS}; i=$((i+1)); done; exit 5`;
|
|
421
486
|
}
|
|
422
487
|
|
|
488
|
+
// ── Mid-turn input transports (CliAgentSpec.midTurnInput) ───────────────────
|
|
489
|
+
|
|
490
|
+
/** stdin is a live message stream: the FIFO feeder above delivers the prompt
|
|
491
|
+
* line first and inbox lines after (claude: `--input-format stream-json`). */
|
|
492
|
+
export interface StdinStreamInputSpec {
|
|
493
|
+
transport: "stdin-stream";
|
|
494
|
+
/** Serialise the opening prompt into ONE stream-input stdin line
|
|
495
|
+
* (claude: a stream-json user message). No trailing newline — the
|
|
496
|
+
* transport owns line framing. */
|
|
497
|
+
promptLine(prompt: string): string;
|
|
498
|
+
/** Serialise one mid-turn user message into ONE stdin line. No
|
|
499
|
+
* trailing newline. */
|
|
500
|
+
messageLine(text: string): string;
|
|
501
|
+
/** Serialise ONE in-band interrupt control line (claude: a stream-json
|
|
502
|
+
* `control_request` with subtype "interrupt") — the ESC equivalent.
|
|
503
|
+
* The CLI's control layer handles these immediately, MID-STEP
|
|
504
|
+
* included: the running tool call aborts (its tool_result records the
|
|
505
|
+
* harness's own rejection text), the run ends with an
|
|
506
|
+
* `error_during_execution` result within ~100ms, and the session file
|
|
507
|
+
* stays `--resume`-able with the whole turn context. Verified live
|
|
508
|
+
* against claude 2.1.236. Absent → the runtime has no in-band
|
|
509
|
+
* interrupt; callers fall back to kill semantics. */
|
|
510
|
+
interruptLine?(requestId: string): string;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** The CLI hands the inbox to the model itself, through a hook it runs after
|
|
514
|
+
* each tool call it completes (codex: the platform's PostToolUse command
|
|
515
|
+
* hook, CODEX_MID_TURN_HOOK_COMMAND in codex.ts). The launch creates the
|
|
516
|
+
* inbox and exports its paths into the CLI's environment
|
|
517
|
+
* (toolHookInboxFragment); the hook reads them back, prints the unfed lines
|
|
518
|
+
* as its additional context for the model and advances the delivered
|
|
519
|
+
* counter. stdin stays the plain prompt file, so there is no in-band
|
|
520
|
+
* interrupt on this transport. */
|
|
521
|
+
export interface ToolHookInputSpec {
|
|
522
|
+
transport: "tool-hook";
|
|
523
|
+
/** Serialise one mid-turn user message into ONE inbox line the hook can
|
|
524
|
+
* splice into its JSON output without decoding: a JSON string literal. */
|
|
525
|
+
messageLine(text: string): string;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export type MidTurnInputSpec = StdinStreamInputSpec | ToolHookInputSpec;
|
|
529
|
+
|
|
530
|
+
/** The stdin-stream transport of a spec, or undefined. The FIFO feeder, the
|
|
531
|
+
* resident harness, the prewarm lane and the in-band interrupt exist only
|
|
532
|
+
* for this transport; the inject lane itself reads `midTurnInput`. */
|
|
533
|
+
export function stdinStreamInputOf(spec: Pick<CliAgentSpec, "midTurnInput">): StdinStreamInputSpec | undefined {
|
|
534
|
+
return spec.midTurnInput?.transport === "stdin-stream" ? spec.midTurnInput : undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** The environment the tool-hook lane hands the CLI, and so the hook: codex
|
|
538
|
+
* runs a command hook with the CLI process's own environment (codex-rs
|
|
539
|
+
* hooks/src/registry.rs Hooks::new, engine/command_runner.rs at
|
|
540
|
+
* rust-v0.160.0). The turn's inbox and delivered-counter paths. */
|
|
541
|
+
export const MID_TURN_INBOX_ENV = "AC_MID_TURN_INBOX";
|
|
542
|
+
export const MID_TURN_DELIVERED_ENV = "AC_MID_TURN_DELIVERED";
|
|
543
|
+
|
|
544
|
+
/** The tool-hook lane's launch fragment, prepended to the detached wrapper
|
|
545
|
+
* script (the shell the CLI starts in): a fresh inbox, the counter at 0,
|
|
546
|
+
* and both paths exported for the hook. Exported for tests. */
|
|
547
|
+
export function toolHookInboxFragment(paths: { inboxPath: string; deliveredPath: string }): string {
|
|
548
|
+
const q = shellQuote;
|
|
549
|
+
return `: > ${q(paths.inboxPath)}; echo 0 > ${q(paths.deliveredPath)}; `
|
|
550
|
+
+ `export ${MID_TURN_INBOX_ENV}=${q(paths.inboxPath)} ${MID_TURN_DELIVERED_ENV}=${q(paths.deliveredPath)}; `;
|
|
551
|
+
}
|
|
552
|
+
|
|
423
553
|
// ── Reap = kill AND CONFIRM (2026-08-18 incident: codex writer lock) ────────
|
|
424
554
|
// A reaped runner must be CONFIRMED dead before the turn unwinds. codex holds
|
|
425
555
|
// a kernel flock on its thread store (`~/.codex/thread-writer-locks/<id>.lock`)
|
|
@@ -467,14 +597,19 @@ export function reapKillAndConfirmCommand(pid: number): string {
|
|
|
467
597
|
// clean sentinel — the probe measured 625ms), and a guest idle watchdog
|
|
468
598
|
// reaps the group if the server's durable record is ever lost. Turn
|
|
469
599
|
// accounting rides two guest numbers next to the inbox:
|
|
470
|
-
// `<prompt>.served` — turns DELIVERED into this process (1 at launch
|
|
471
|
-
//
|
|
600
|
+
// `<prompt>.served` — turns DELIVERED into this process (1 at launch;
|
|
601
|
+
// each adoption rebases it to the result lines it
|
|
602
|
+
// counted + 1, so a result the CLI wrote on its
|
|
603
|
+
// own, such as a background task's wake turn,
|
|
604
|
+
// never desyncs it; the server mirrors it durably
|
|
472
605
|
// as the runner stamp's `turnsServed`);
|
|
473
|
-
// result lines in .out — turns FINISHED (
|
|
474
|
-
// server-side stream-json parse is
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
606
|
+
// result lines in .out — turns FINISHED (CLI_RESULT_LINE_AWK — hint-
|
|
607
|
+
// grade; the server-side stream-json parse is
|
|
608
|
+
// authoritative).
|
|
609
|
+
// idle ⇔ results ≥ served. Mid-turn injects ride the same inbox WITHOUT
|
|
610
|
+
// bumping served, and fold into the running turn producing no result of
|
|
611
|
+
// their own. The feeder forwards inbox lines only while results < served,
|
|
612
|
+
// so a line that raced the result is never fed into an idle CLI.
|
|
478
613
|
|
|
479
614
|
/** Guest idle TTL for a resident with no new inbox line: hot window (180s)
|
|
480
615
|
* + margin, so the park gate's graceful `.end` normally wins and the
|
|
@@ -482,11 +617,14 @@ export function reapKillAndConfirmCommand(pid: number): string {
|
|
|
482
617
|
export const RESIDENT_IDLE_TTL_S = 240;
|
|
483
618
|
/** Watchdog poll cadence (seconds). */
|
|
484
619
|
export const RESIDENT_WATCHDOG_POLL_S = 15;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
* the
|
|
488
|
-
*
|
|
489
|
-
|
|
620
|
+
|
|
621
|
+
/** Shell command printing how many result lines the durable .out holds —
|
|
622
|
+
* the guest's turn accounting (feeder gate, idle check, adoption parity).
|
|
623
|
+
* Prints nothing when the file is missing: callers default an empty count
|
|
624
|
+
* to 0. */
|
|
625
|
+
export function cliResultCountCommand(outPath: string): string {
|
|
626
|
+
return `${resultLineAwk("n++", "print n + 0")} ${shellQuote(outPath)} 2>/dev/null`;
|
|
627
|
+
}
|
|
490
628
|
|
|
491
629
|
/** One bounded exec printing the count of result lines at or past
|
|
492
630
|
* `fromByte` (0-based) in the durable .out. Always exits 0; stdout is the
|
|
@@ -496,41 +634,82 @@ export function residentResultCountCommand(outPath: string, fromByte = 0): strin
|
|
|
496
634
|
const src = fromByte > 0
|
|
497
635
|
? `tail -c +${fromByte + 1} ${q(outPath)} 2>/dev/null`
|
|
498
636
|
: `cat ${q(outPath)} 2>/dev/null`;
|
|
499
|
-
return `ac_res=$(${src} |
|
|
637
|
+
return `ac_res=$(${src} | ${resultLineAwk("n++", "print n + 0")}); echo "\${ac_res:-0}"`;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** One bounded exec reporting the LAST result line in the durable .out
|
|
641
|
+
* window [`fromByte`, `toByte`): prints `none` (no result line), `error`
|
|
642
|
+
* (its `is_error` is true) or `ok`. Always exits 0 and never prints the
|
|
643
|
+
* line itself. The harvest's confirm for a resident result the tailer never
|
|
644
|
+
* parsed (review C2): a count alone cannot say whether the turn failed. The
|
|
645
|
+
* whole line is searched for `"is_error":true`, wherever the CLI put it
|
|
646
|
+
* (key order is not fixed). `toByte` bounds the window to bytes a drain
|
|
647
|
+
* already walked past, so a result the drain never reached (or a line still
|
|
648
|
+
* being written past its line-aligned offset) is not confirmed; omitted,
|
|
649
|
+
* the window runs to the end of the file. A window with `toByte <= fromByte`
|
|
650
|
+
* is empty. */
|
|
651
|
+
export function residentLastResultCommand(outPath: string, fromByte = 0, toByte?: number): string {
|
|
652
|
+
const q = shellQuote;
|
|
653
|
+
const read = fromByte > 0
|
|
654
|
+
? `tail -c +${fromByte + 1} ${q(outPath)} 2>/dev/null`
|
|
655
|
+
: `cat ${q(outPath)} 2>/dev/null`;
|
|
656
|
+
const src = toByte === undefined ? read : `${read} | head -c ${Math.max(0, toByte - fromByte)}`;
|
|
657
|
+
return `${src} | ${resultLineAwk("last = $0",
|
|
658
|
+
'if (last == "") print "none"; else if (index(last, "\\"is_error\\":true") > 0) print "error"; else print "ok"')}`;
|
|
500
659
|
}
|
|
501
660
|
|
|
502
661
|
/** The resident feeder: `streamInputFeederFragment` with the result-break
|
|
503
|
-
* REPLACED by the end-file break
|
|
504
|
-
*
|
|
505
|
-
*
|
|
662
|
+
* REPLACED by the end-file break (verified live by the spec's probe: two
|
|
663
|
+
* messages, one process, one session file, graceful exit on `.end`), plus
|
|
664
|
+
* the TURN GATE: inbox lines are forwarded only while a delivered turn is
|
|
665
|
+
* still unanswered (the result count in .out is below `.served`),
|
|
666
|
+
* the same two numbers the idle predicate reads. Without the gate, a line
|
|
667
|
+
* appended after the turn's result line but before the server settled the
|
|
668
|
+
* turn was fed into the idle CLI, started a turn nobody tails, and was
|
|
669
|
+
* acked as delivered: the message was never answered, and the extra result
|
|
670
|
+
* line broke the next adoption's parity. With the gate that line stays
|
|
671
|
+
* unfed, the inject ack exits 5, and the message stays owed for the
|
|
672
|
+
* follow-up turn. Adoption bumps `.served` BEFORE it appends, so the next
|
|
673
|
+
* turn's prompt still flows. The count is a full-file grep, re-run
|
|
674
|
+
* only on a poll that has unfed lines AND a changed (.out size, .served)
|
|
675
|
+
* pair, so an idle resident holding a gated line never rescans. The
|
|
676
|
+
* forward is guarded like the plain feeder's: the counters advance only
|
|
677
|
+
* when `sed` wrote the lines into the FIFO. */
|
|
506
678
|
export function residentFeederFragment(paths: {
|
|
507
679
|
promptPath: string; fifoPath: string; inboxPath: string;
|
|
508
|
-
deliveredPath: string; endPath: string;
|
|
680
|
+
deliveredPath: string; endPath: string; outPath: string; servedPath: string;
|
|
509
681
|
}): string {
|
|
510
682
|
const q = shellQuote;
|
|
511
683
|
return `rm -f ${q(paths.fifoPath)}; mkfifo ${q(paths.fifoPath)}; `
|
|
512
684
|
+ `: > ${q(paths.inboxPath)}; echo 0 > ${q(paths.deliveredPath)}; rm -f ${q(paths.endPath)}; `
|
|
513
|
-
+ `( cat ${q(paths.promptPath)}; ac_fed=0; while kill -0 "$$" 2>/dev/null; do `
|
|
685
|
+
+ `( cat ${q(paths.promptPath)}; ac_fed=0; ac_gk=; ac_gr=0; while kill -0 "$$" 2>/dev/null; do `
|
|
514
686
|
+ `if [ -f ${q(paths.endPath)} ]; then break; fi; `
|
|
515
687
|
+ `ac_lines=$(wc -l < ${q(paths.inboxPath)} 2>/dev/null || echo 0); ac_lines=\${ac_lines:-0}; `
|
|
516
688
|
+ `if [ "$ac_lines" -gt "$ac_fed" ]; then `
|
|
517
|
-
+ `
|
|
518
|
-
+ `
|
|
689
|
+
+ `ac_gs=$(wc -c < ${q(paths.outPath)} 2>/dev/null); ac_gv=$(cat ${q(paths.servedPath)} 2>/dev/null); ac_gv=\${ac_gv:-1}; `
|
|
690
|
+
+ `if [ "$ac_gs:$ac_gv" != "$ac_gk" ]; then ac_gk="$ac_gs:$ac_gv"; `
|
|
691
|
+
+ `ac_gr=$(${cliResultCountCommand(paths.outPath)}); ac_gr=\${ac_gr:-0}; fi; `
|
|
692
|
+
+ `if [ "$ac_gr" -lt "$ac_gv" ]; then `
|
|
693
|
+
+ `sed -n "$((ac_fed+1)),$((ac_lines))p" ${q(paths.inboxPath)} && { ac_fed=$ac_lines; `
|
|
694
|
+
+ `echo "$ac_fed" > ${q(paths.deliveredPath)}; }; fi; fi; `
|
|
519
695
|
+ `sleep ${INJECT_FEEDER_POLL_SECONDS}; done ) > ${q(paths.fifoPath)} 2>/dev/null </dev/null & `;
|
|
520
696
|
}
|
|
521
697
|
|
|
522
|
-
/** Sets `ac_idle` (1 = between turns: every delivered turn has its result
|
|
523
|
-
*
|
|
524
|
-
*
|
|
698
|
+
/** Sets `ac_idle` (1 = between turns: every delivered turn has its result).
|
|
699
|
+
* Embedded by the watchdog and the resident heartbeat gate: one idle
|
|
700
|
+
* predicate, stated once. Unfed inbox lines do not make a resident busy:
|
|
701
|
+
* the feeder's turn gate forwards a line only while results < served, so
|
|
702
|
+
* a line still unfed once results >= served is held until the next
|
|
703
|
+
* adoption bumps `.served` (a message that raced the result, owed on the
|
|
704
|
+
* server). Reading it as work would keep the heartbeat fresh and the
|
|
705
|
+
* watchdog quiet for as long as the line sat there. */
|
|
525
706
|
export function residentIdleCheckFragment(paths: {
|
|
526
|
-
outPath: string;
|
|
707
|
+
outPath: string; servedPath: string;
|
|
527
708
|
}): string {
|
|
528
709
|
const q = shellQuote;
|
|
529
|
-
return `ac_res=$(
|
|
710
|
+
return `ac_res=$(${cliResultCountCommand(paths.outPath)}); ac_res=\${ac_res:-0}; `
|
|
530
711
|
+ `ac_srv=$(cat ${q(paths.servedPath)} 2>/dev/null); ac_srv=\${ac_srv:-1}; `
|
|
531
|
-
+ `
|
|
532
|
-
+ `ac_del=$(cat ${q(paths.deliveredPath)} 2>/dev/null); ac_del=\${ac_del:-0}; `
|
|
533
|
-
+ `if [ "$ac_res" -ge "$ac_srv" ] && [ "$ac_in" -eq "$ac_del" ]; then ac_idle=1; else ac_idle=0; fi; `;
|
|
712
|
+
+ `if [ "$ac_res" -ge "$ac_srv" ]; then ac_idle=1; else ac_idle=0; fi; `;
|
|
534
713
|
}
|
|
535
714
|
|
|
536
715
|
/** Guest idle watchdog: reap the whole process group after
|
|
@@ -554,7 +733,7 @@ export function residentIdleCheckFragment(paths: {
|
|
|
554
733
|
* guest rule about not killing, and journal advance is required, not mere
|
|
555
734
|
* process existence). */
|
|
556
735
|
export function residentIdleWatchdogFragment(paths: {
|
|
557
|
-
outPath: string;
|
|
736
|
+
outPath: string; servedPath: string;
|
|
558
737
|
}): string {
|
|
559
738
|
return `( ac_idle_s=0; while kill -0 "$$" 2>/dev/null; do `
|
|
560
739
|
+ residentIdleCheckFragment(paths)
|
|
@@ -621,8 +800,10 @@ function parsePid(raw: string): number {
|
|
|
621
800
|
/** Recover the detached runner's pid from its durable pidfile after the
|
|
622
801
|
* launch exec's STREAM died (deadline_exceeded, canceled context, any wire
|
|
623
802
|
* fault). Reads over the file transport with a short retry so the race
|
|
624
|
-
* where the launch shell is still writing the pidfile is absorbed.
|
|
625
|
-
|
|
803
|
+
* where the launch shell is still writing the pidfile is absorbed.
|
|
804
|
+
* Exported for the server's workflow-lane launch (turn-workflow/tailer.ts
|
|
805
|
+
* launchDetachedRunner), which recovers the same way. */
|
|
806
|
+
export async function recoverLaunchPid(read: (path: string) => Promise<string>, pidPath: string): Promise<number> {
|
|
626
807
|
for (let attempt = 0; attempt < LAUNCH_PID_RECOVERY_ATTEMPTS; attempt++) {
|
|
627
808
|
try {
|
|
628
809
|
const pid = parsePid(await read(pidPath));
|
|
@@ -662,6 +843,23 @@ export function sessionEnvSourceFragment(relPath: string | undefined): string {
|
|
|
662
843
|
return `[ -f "$HOME/${relPath}" ] && . "$HOME/${relPath}" >/dev/null 2>&1 || true; `;
|
|
663
844
|
}
|
|
664
845
|
|
|
846
|
+
/** Shell command printing a fingerprint of the session env file's current
|
|
847
|
+
* bytes (POSIX `cksum`), or `absent` when there is no file. A resident
|
|
848
|
+
* records it next to its prompt immediately BEFORE sourcing the file, and
|
|
849
|
+
* an adoption compares the file's fingerprint at that moment with it: a
|
|
850
|
+
* fresh launch would source the file as it is now, the resident only ever
|
|
851
|
+
* saw the one it started with. Recording before the source keeps the
|
|
852
|
+
* comparison conservative — a write that lands between the two reads as a
|
|
853
|
+
* change. The guest file is the one truth every replica writes to, so the
|
|
854
|
+
* verdict never depends on which server process wrote it. */
|
|
855
|
+
export function sessionEnvFingerprintCommand(relPath: string): string {
|
|
856
|
+
if (!SESSION_ENV_FILE_SAFE.test(relPath) || relPath.includes("..")) {
|
|
857
|
+
throw new Error(`unsafe session env path: ${relPath}`);
|
|
858
|
+
}
|
|
859
|
+
// One group, so a redirect after it (`> <prompt>.envsum`) takes either arm.
|
|
860
|
+
return `{ { cksum < "$HOME/${relPath}"; } 2>/dev/null || echo absent; }`;
|
|
861
|
+
}
|
|
862
|
+
|
|
665
863
|
/** The per-turn model-credential source fragment (`RuntimeOptions.
|
|
666
864
|
* credEnvFile` — an ABSOLUTE guest path), or "" when none is configured.
|
|
667
865
|
* Sourced AFTER the session env file so the turn actor's credential wins.
|
|
@@ -825,9 +1023,9 @@ export interface CliAgentSpec {
|
|
|
825
1023
|
* `effort` is present only when the caller configured a reasoning effort
|
|
826
1024
|
* AND the spec has a real knob for it (see `CliReasoningEffort`).
|
|
827
1025
|
* `streamInput` is true when the durable transport is feeding stdin as a
|
|
828
|
-
* live message stream (
|
|
829
|
-
* then the FIFO the feeder writes, and the CLI must be
|
|
830
|
-
* stream-input mode (claude: `--input-format stream-json`). */
|
|
1026
|
+
* live message stream (the "stdin-stream" `midTurnInput` transport):
|
|
1027
|
+
* `promptPath` is then the FIFO the feeder writes, and the CLI must be
|
|
1028
|
+
* invoked in its stream-input mode (claude: `--input-format stream-json`). */
|
|
831
1029
|
buildCommand(args: { promptPath: string; sessionId?: string; model?: string; cwd?: string; effort?: CliReasoningEffort; streamInput?: boolean }): string;
|
|
832
1030
|
/** Map one parsed JSONL stdout event to `AgentMessage`s. The base emits
|
|
833
1031
|
* `init`/`done`/`error` lifecycle itself, so a spec maps only content +
|
|
@@ -858,37 +1056,15 @@ export interface CliAgentSpec {
|
|
|
858
1056
|
* `extractSessionId`/`mapEvent` members are used only as the version-mismatch
|
|
859
1057
|
* fallback. Absent → the spec is JSONL-only (legacy path always). */
|
|
860
1058
|
acp?: { command: string; args: string[]; env?: Record<string, string> };
|
|
861
|
-
/** Mid-turn
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
|
|
870
|
-
* FIFO, which is what ends the CLI process (stream-input CLIs exit on
|
|
871
|
-
* stdin EOF, not after a result). Absent → the prompt file is the whole
|
|
872
|
-
* stdin, exactly as before. */
|
|
873
|
-
streamInput?: {
|
|
874
|
-
/** Serialise the opening prompt into ONE stream-input stdin line
|
|
875
|
-
* (claude: a stream-json user message). No trailing newline — the
|
|
876
|
-
* transport owns line framing. */
|
|
877
|
-
promptLine(prompt: string): string;
|
|
878
|
-
/** Serialise one mid-turn user message into ONE stdin line. No
|
|
879
|
-
* trailing newline. */
|
|
880
|
-
messageLine(text: string): string;
|
|
881
|
-
/** Serialise ONE in-band interrupt control line (claude: a stream-json
|
|
882
|
-
* `control_request` with subtype "interrupt") — the ESC equivalent.
|
|
883
|
-
* The CLI's control layer handles these immediately, MID-STEP
|
|
884
|
-
* included: the running tool call aborts (its tool_result records the
|
|
885
|
-
* harness's own rejection text), the run ends with an
|
|
886
|
-
* `error_during_execution` result within ~100ms, and the session file
|
|
887
|
-
* stays `--resume`-able with the whole turn context. Verified live
|
|
888
|
-
* against claude 2.1.236. Absent → the runtime has no in-band
|
|
889
|
-
* interrupt; callers fall back to kill semantics. */
|
|
890
|
-
interruptLine?(requestId: string): string;
|
|
891
|
-
};
|
|
1059
|
+
/** Mid-turn INPUT support (the delivery half of `injectUserMessage`): how
|
|
1060
|
+
* a line appended to the turn's durable inbox reaches the CLI while its
|
|
1061
|
+
* turn runs — see the transports above. Engages only on the durable
|
|
1062
|
+
* detached transport (the inbox, the delivered counter and the feeder or
|
|
1063
|
+
* hook live in the guest; single-exec transports keep the plain
|
|
1064
|
+
* prompt-file stdin). Absent → the runtime cannot take a message
|
|
1065
|
+
* mid-turn: every inject answers "unsupported" and the message waits for
|
|
1066
|
+
* the turn boundary. */
|
|
1067
|
+
midTurnInput?: MidTurnInputSpec;
|
|
892
1068
|
}
|
|
893
1069
|
|
|
894
1070
|
/** The CURRENT turn's mid-turn injection state (durable stream-input
|
|
@@ -1001,19 +1177,20 @@ export class CliAgentRunner implements ModelExecutionContract {
|
|
|
1001
1177
|
/** Deliver one user message INTO the live turn — see the contract doc
|
|
1002
1178
|
* (types/runtime.ts). Appends a stream-input line to the turn's durable
|
|
1003
1179
|
* inbox and waits for the in-guest feeder's delivered-counter ack; only
|
|
1004
|
-
* an acked forward (pre-result, into the CLI's stdin)
|
|
1005
|
-
* "delivered"
|
|
1006
|
-
*
|
|
1007
|
-
* the
|
|
1008
|
-
*
|
|
1009
|
-
|
|
1180
|
+
* an acked forward (pre-result, written into the CLI's stdin pipe)
|
|
1181
|
+
* reports "delivered", and even then the model reads the line only at
|
|
1182
|
+
* its next tool boundary (review D8). Guest exit 5 (ack window exhausted
|
|
1183
|
+
* with the runner still alive: the feeder is not forwarding) reports
|
|
1184
|
+
* "pending": the appended line may still be forwarded later, but it was
|
|
1185
|
+
* NOT seen yet. Never throws. */
|
|
1186
|
+
async injectUserMessage(text: string, opts?: MidTurnEnvelopeOptions): Promise<"delivered" | "pending" | "closed" | "unsupported"> {
|
|
1010
1187
|
const inject = this.turnInject;
|
|
1011
|
-
const
|
|
1012
|
-
if (!inject || !
|
|
1188
|
+
const input = this.spec.midTurnInput;
|
|
1189
|
+
if (!inject || !input || this.guestDetached) return "unsupported";
|
|
1013
1190
|
// The address-this envelope (wrapMidTurnUserMessage): a raw injected
|
|
1014
1191
|
// line carries no framing, and the model continues its narration
|
|
1015
1192
|
// without acknowledging the message.
|
|
1016
|
-
return this.appendTurnStreamLine(inject,
|
|
1193
|
+
return this.appendTurnStreamLine(inject, input.messageLine(wrapMidTurnUserMessage(text, opts)));
|
|
1017
1194
|
}
|
|
1018
1195
|
|
|
1019
1196
|
/** Request an in-band step interrupt of the LIVE turn — the ESC
|
|
@@ -1027,7 +1204,7 @@ export class CliAgentRunner implements ModelExecutionContract {
|
|
|
1027
1204
|
* throws. */
|
|
1028
1205
|
async interruptTurn(): Promise<"delivered" | "pending" | "closed" | "unsupported"> {
|
|
1029
1206
|
const inject = this.turnInject;
|
|
1030
|
-
const interruptLine = this.spec
|
|
1207
|
+
const interruptLine = stdinStreamInputOf(this.spec)?.interruptLine;
|
|
1031
1208
|
if (!inject || !interruptLine || this.guestDetached) return "unsupported";
|
|
1032
1209
|
return this.appendTurnStreamLine(inject, interruptLine(`itr-${Date.now().toString(36)}`));
|
|
1033
1210
|
}
|
|
@@ -1055,7 +1232,7 @@ export class CliAgentRunner implements ModelExecutionContract {
|
|
|
1055
1232
|
{ timeoutMs: INJECT_ACK_EXEC_TIMEOUT_MS });
|
|
1056
1233
|
if (res.exitCode === 0) return "delivered";
|
|
1057
1234
|
// 5 = ack window exhausted, runner alive: the line is in the inbox
|
|
1058
|
-
// and
|
|
1235
|
+
// and the feeder is not forwarding it (stopped, or gated).
|
|
1059
1236
|
// 4 = the runner died first; anything else is a fault — both closed.
|
|
1060
1237
|
return res.exitCode === 5 ? "pending" : "closed";
|
|
1061
1238
|
} catch { return "closed"; }
|
|
@@ -1507,25 +1684,27 @@ export class CliAgentRunner implements ModelExecutionContract {
|
|
|
1507
1684
|
this.turnPendingLineEnd = null;
|
|
1508
1685
|
this.nudgePending = false;
|
|
1509
1686
|
this.wakeTailWatchdog = null;
|
|
1510
|
-
// Mid-turn
|
|
1511
|
-
//
|
|
1512
|
-
// transport (the
|
|
1513
|
-
// single-exec transports keep the plain
|
|
1514
|
-
|
|
1515
|
-
|
|
1687
|
+
// Mid-turn input engages only where BOTH halves exist: a spec that
|
|
1688
|
+
// declares a mid-turn input transport AND the durable detached
|
|
1689
|
+
// transport (the inbox, the delivered counter and the feeder or hook
|
|
1690
|
+
// live in the guest; single-exec transports keep the plain
|
|
1691
|
+
// prompt-file stdin).
|
|
1692
|
+
const durable = this.sandbox.commands.runBackground !== undefined;
|
|
1693
|
+
const stdinStream = durable ? stdinStreamInputOf(this.spec) : undefined;
|
|
1694
|
+
const toolHook = durable && this.spec.midTurnInput?.transport === "tool-hook";
|
|
1516
1695
|
const fifoPath = `${promptPath}.fifo`;
|
|
1517
|
-
const promptWrite = this.sandbox.files.write(promptPath,
|
|
1518
|
-
? `${
|
|
1696
|
+
const promptWrite = this.sandbox.files.write(promptPath, stdinStream
|
|
1697
|
+
? `${stdinStream.promptLine(opts.prompt)}\n`
|
|
1519
1698
|
: this.spec.promptPayload(opts.prompt));
|
|
1520
1699
|
const cmd = this.spec.buildCommand({
|
|
1521
1700
|
// Stream-input turns read stdin from the feeder's FIFO; the prompt
|
|
1522
1701
|
// file is the feeder's first delivery, not the CLI's stdin.
|
|
1523
|
-
promptPath:
|
|
1702
|
+
promptPath: stdinStream ? fifoPath : promptPath,
|
|
1524
1703
|
sessionId: opts.sessionId,
|
|
1525
1704
|
model: this.model,
|
|
1526
1705
|
cwd: this.options.cwd,
|
|
1527
1706
|
effort: this.configEffort,
|
|
1528
|
-
...(
|
|
1707
|
+
...(stdinStream ? { streamInput: true } : {}),
|
|
1529
1708
|
});
|
|
1530
1709
|
|
|
1531
1710
|
// Frame guard (see _jsonl-guard.ts): the CLI's stdout is piped through
|
|
@@ -1643,14 +1822,18 @@ export class CliAgentRunner implements ModelExecutionContract {
|
|
|
1643
1822
|
// supersede group-kill still reaps this subshell with the tree.
|
|
1644
1823
|
const busyPath = `${promptPath}.busy`;
|
|
1645
1824
|
const tasksPath = `${promptPath}.tasks`;
|
|
1646
|
-
// Mid-turn
|
|
1825
|
+
// Mid-turn input: the stdin-stream feeder subshell (see
|
|
1647
1826
|
// streamInputFeederFragment) rides at the FRONT of the detached
|
|
1648
|
-
// script so the FIFO exists before the CLI opens it as stdin
|
|
1827
|
+
// script so the FIFO exists before the CLI opens it as stdin; the
|
|
1828
|
+
// tool-hook lane creates the inbox here and exports its paths into
|
|
1829
|
+
// the CLI's environment, for the hook the CLI runs at each tool step.
|
|
1649
1830
|
const inboxPath = `${promptPath}.inbox`;
|
|
1650
1831
|
const deliveredPath = `${promptPath}.delivered`;
|
|
1651
|
-
const feeder =
|
|
1832
|
+
const feeder = stdinStream
|
|
1652
1833
|
? streamInputFeederFragment({ promptPath, fifoPath, inboxPath, deliveredPath, outPath })
|
|
1653
|
-
:
|
|
1834
|
+
: toolHook
|
|
1835
|
+
? toolHookInboxFragment({ inboxPath, deliveredPath })
|
|
1836
|
+
: "";
|
|
1654
1837
|
// Perf sampling rides the same beat: every 6th beat (~60s) the
|
|
1655
1838
|
// subshell burst-reads /proc and rewrites the `.perf` token the
|
|
1656
1839
|
// durable-trio probe carries as its trailing field (perf-sampler.ts).
|
|
@@ -1747,8 +1930,9 @@ export class CliAgentRunner implements ModelExecutionContract {
|
|
|
1747
1930
|
// durable files without this process's memory.
|
|
1748
1931
|
this.options.onDetachedLaunch?.({ promptPath, sentinel, pid });
|
|
1749
1932
|
// Arm mid-turn injection now that the detached runner (and its
|
|
1750
|
-
// feeder) exist; cleared with the turn in the finally
|
|
1751
|
-
|
|
1933
|
+
// feeder or hook inbox) exist; cleared with the turn in the finally
|
|
1934
|
+
// below.
|
|
1935
|
+
if (stdinStream !== undefined || toolHook) {
|
|
1752
1936
|
this.turnInject = { inboxPath, deliveredPath, pid, appended: 0, chain: Promise.resolve() };
|
|
1753
1937
|
}
|
|
1754
1938
|
|