@jini-ai/daemon 0.5.4 → 0.6.1

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.
@@ -2,7 +2,7 @@ import type { MessageAttachmentImage } from '../attachment-content.js';
2
2
  import type { spawn as nodeSpawn, ChildProcess } from 'node:child_process';
3
3
  import type { Principal } from '@jini-ai/core';
4
4
  import type { RunAgentPayload, RunErrorPayload } from '@jini-ai/protocol';
5
- import type { applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, AcpMcpServerInput, AcpPermissionHandler, PromptAugmenter } from '@jini-ai/agent-runtime';
5
+ import type { AgentRuntimeState, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, AcpMcpServerInput, AcpPermissionHandler, PromptAugmenter } from '@jini-ai/agent-runtime';
6
6
  import type { createCommandInvocation, listProcessSnapshots, ProcessSnapshot, StopProcessesResult } from '@jini-ai/platform';
7
7
  import type { RunByteJournal } from '../continuation/journal.js';
8
8
  import type { RunRetrySideEffectState } from '../run/core/index.js';
@@ -194,7 +194,27 @@ export interface AgentExecutor {
194
194
  * `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
195
195
  */
196
196
  run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
197
+ /**
198
+ * Delivers a message the human sent while `runId` is going straight into its live agent process
199
+ * (see {@link UserMessageDelivery} for each outcome). A stream-json agent is interrupted first and
200
+ * gets the message once the CLI acknowledges the interrupt, so this settles in milliseconds (at
201
+ * most `INTERRUPT_ACK_TIMEOUT_MS`); it never waits on the agent's reply.
202
+ */
203
+ sendUserMessage(requiredArgs: {
204
+ readonly runId: string;
205
+ readonly text: string;
206
+ }): Promise<UserMessageDelivery>;
197
207
  }
208
+ /**
209
+ * What {@link AgentExecutor.sendUserMessage} did with a mid-run message:
210
+ * - `'delivered'` — the running turn was interrupted and the message written into the live
211
+ * session; the agent answers it next, in the same process and session.
212
+ * - `'unsupported'` — the run is live, but its agent has no way to take input mid-run (its prompt
213
+ * is plain text or an RPC request). The host should interrupt and send it as the next turn.
214
+ * - `'not-running'` — no live process will accept it: an unknown run, a finished one, or one whose
215
+ * last turn already ended and closed stdin. The run is ending; the host should send it next.
216
+ */
217
+ export type UserMessageDelivery = 'delivered' | 'unsupported' | 'not-running';
198
218
  /** Process discovery is a getter; process collection and stopping accept explicit host inputs. */
199
219
  export type CollectProcessTreePidsPort = (args: {
200
220
  readonly processes: ProcessSnapshot[];
@@ -212,11 +232,11 @@ export interface TerminateChildTreeDeps {
212
232
  * Which step reported a contained failure through `onCleanupFailure` (SEC-007).
213
233
  *
214
234
  * The first three come from {@link terminateChildTreeBestEffort} (process-tree teardown). The last
215
- * two are the two fallible steps that sit between a child's `'close'` and `finish()` — staged-file
216
- * removal and the host's own `classifyFailure` — neither of which may prevent the terminal
217
- * transition, and neither of which may fail silently either. See each close handler.
235
+ * three cover tool cancellation, staged-file removal and the host's own `classifyFailure`.
236
+ * These fallible steps between a child's `'close'` and `finish()` must never prevent the terminal
237
+ * transition, and none may fail silently either. See each close handler.
218
238
  */
219
- export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure' | 'staged-file-cleanup' | 'failure-classification';
239
+ export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure' | 'tool-cancellation' | 'staged-file-cleanup' | 'failure-classification';
220
240
  export interface AgentCleanupFailureContext {
221
241
  readonly runId: string;
222
242
  readonly phase: AgentCleanupFailurePhase;
@@ -237,6 +257,8 @@ export interface StdinCloseHandle {
237
257
  closeStdinOnce(): void;
238
258
  /** Journals a sent-to-stdin byte chunk, queued through the same FIFO {@link wireChildLifecycle} already uses for emitted events. No-op when no journal was configured (see `CreateAgentExecutorOptions.journal`). */
239
259
  recordSentBytes(content: string): void;
260
+ /** Interrupts this child's turn, then writes a mid-run user message into its open stdin — see {@link AgentExecutor.sendUserMessage}. */
261
+ sendUserMessage(text: string): Promise<UserMessageDelivery>;
240
262
  }
241
263
  /**
242
264
  * Gap 3 (capability-routed continuation transport) — host-owned config for the
@@ -374,6 +396,13 @@ export interface McpJsonInjectionOptions {
374
396
  readonly removeDir?: ({ path }: {
375
397
  readonly path: string;
376
398
  }) => Promise<void>;
399
+ /** `'codex-toml'` only. Links the run home's sessions/archived_sessions to the operator's
400
+ * persistent rollout store. Config and auth remain isolated and are removed after the run.
401
+ * @default Native mkdir (0700) and directory symlinks; errors fail staging before spawn. */
402
+ readonly linkSessionStore?: (required: {
403
+ runHome: string;
404
+ sourceHome: string;
405
+ }, optional?: {}) => Promise<void>;
377
406
  }
378
407
  /** One `.mcp.json` `mcpServers` entry — the shape Claude Code's own config schema expects. */
379
408
  export interface McpJsonServerEntry {
@@ -542,6 +571,9 @@ export interface FailureClassificationContext {
542
571
  */
543
572
  export type ClassifyFailure = (context: FailureClassificationContext) => boolean | Promise<boolean>;
544
573
  export interface CreateAgentExecutorOptions {
574
+ /** Host environment snapshot; baseline spawn keys and source config paths are allowlisted. */
575
+ readonly hostEnv?: NodeJS.ProcessEnv;
576
+ readonly runtimeState?: AgentRuntimeState;
545
577
  readonly lifecycle: RunLifecycle;
546
578
  /** Resolves/pins the concrete starting model against the actual launch scope. */
547
579
  readonly resolveModelForLaunch?: typeof resolveModelForLaunch;
@@ -39,11 +39,15 @@ function translateUsagePayload(rawEvent) {
39
39
  const rawUsage = isRecord(rawEvent.usage) ? rawEvent.usage : undefined;
40
40
  const usage = extractUsageTokens({ rawUsage: rawUsage });
41
41
  const costUsd = asOptionalNumber(rawEvent.costUsd);
42
+ const cumulativeCostUsd = asOptionalNumber(rawEvent.cumulativeCostUsd);
43
+ const costSessionId = asOptionalString(rawEvent.costSessionId);
42
44
  const durationMs = asOptionalNumber(rawEvent.durationMs);
43
45
  return {
44
46
  type: 'usage',
45
47
  ...(usage !== undefined ? { usage } : {}),
46
48
  ...(costUsd !== undefined ? { costUsd } : {}),
49
+ ...(cumulativeCostUsd !== undefined ? { cumulativeCostUsd } : {}),
50
+ ...(costSessionId !== undefined ? { costSessionId } : {}),
47
51
  ...(durationMs !== undefined ? { durationMs } : {}),
48
52
  };
49
53
  }
@@ -118,6 +118,21 @@ interface WireChildLifecycleContext extends TerminateChildTreeDeps {
118
118
  * @overallScore 100/100
119
119
  */
120
120
  export declare function wireChildLifecycle(ctx: WireChildLifecycleContext): StdinCloseHandle;
121
+ /**
122
+ * Whether a def's live process can take another user message mid-run: only a stream-json stdin
123
+ * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
124
+ * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
125
+ * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
126
+ */
127
+ export declare function acceptsMidRunUserMessages({ def }: {
128
+ readonly def: RuntimeAgentDef;
129
+ }): boolean;
130
+ /**
131
+ * How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
132
+ * anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
133
+ * the ceiling only bounds an agent that ignores control requests.
134
+ */
135
+ export declare const INTERRUPT_ACK_TIMEOUT_MS = 5000;
121
136
  /**
122
137
  * Writes the initial user turn to the child's stdin per `def.promptInputFormat`
123
138
  * (both branches only run for `promptViaStdin: true` defs — the only shape
@@ -125,8 +140,8 @@ export declare function wireChildLifecycle(ctx: WireChildLifecycleContext): Stdi
125
140
  * - `'text'` (default): the raw prompt buffer, then stdin is closed —
126
141
  * matches `RuntimeAgentDef.promptInputFormat`'s own doc.
127
142
  * - `'stream-json'`: one JSONL line wrapping the prompt as an Anthropic
128
- * user message; stdin is deliberately left open (a real multi-turn
129
- * caller would inject further messages) — v1 has no such caller, so
143
+ * user message; stdin is deliberately left open so a message the human
144
+ * sends mid-run can follow it (`StdinCloseHandle.sendUserMessage`), and
130
145
  * {@link wireChildLifecycle}'s `turn_end` handling closes it once the
131
146
  * agent's own stream reports the turn ended.
132
147
  * @param def - The resolved agent def (only `.promptInputFormat` is read).
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { createModelReceiptTracker } from './model-receipts.js';
2
3
  import { messageContentWithImages } from '../attachment-content.js';
3
4
  import { redactSecrets, } from '@jini-ai/core';
@@ -8,7 +9,7 @@ import { resolveContinuationTransport, } from '../continuation/continuation-tran
8
9
  import { resultContent, } from '../delegated-tool-bridge.js';
9
10
  import { extractResultMedia, } from '../tool-result-media.js';
10
11
  import { translateAgentRuntimeEvent, translateAcpError, applyAgentTranslationSideEffects, } from './event-translation.js';
11
- import { errorMessage, } from './values.js';
12
+ import { errorMessage, isRecord, } from './values.js';
12
13
  import { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, bridgeUnavailableMessage, } from './mcp-bridge.js';
13
14
  /** Carry PID only on structured status so the host can record its OS start identity early.
14
15
  * A PID alone is never proof of death; hosts verify PID reuse before native continuation. */
@@ -25,7 +26,7 @@ function statusWithChildIdentity({ payload, childPid }, _optional = {}) {
25
26
  * parser's own documented behavior. Never called for `streamFormat:
26
27
  * 'plain'` — `wireChildLifecycle` handles that format inline with no
27
28
  * parser at all (see `ChildDrivenStreamFormat`'s doc).
28
- * @param def - The resolved agent def (only `.eventParser` is read beyond `streamFormat`).
29
+ * @param def - The resolved agent def (`.eventParser` and `.usageCostBasis` configure parser behavior).
29
30
  * @param streamFormat - `def.streamFormat`, already narrowed by {@link isSupportedStreamFormat}.
30
31
  * @param onEvent - Sink the parser calls once per parsed (or malformed-raw) event.
31
32
  * @returns A `{feed, flush}` handle for the chosen parser.
@@ -35,7 +36,7 @@ function statusWithChildIdentity({ payload, childPid }, _optional = {}) {
35
36
  function createStreamHandlerForDef(def, streamFormat, onEvent) {
36
37
  switch (streamFormat) {
37
38
  case 'claude-stream-json':
38
- return createClaudeStreamHandler({ onEvent: onEvent });
39
+ return createClaudeStreamHandler({ onEvent: onEvent }, { costBasis: def.usageCostBasis ?? 'run' });
39
40
  case 'copilot-stream-json':
40
41
  return createCopilotStreamHandler({ onEvent: onEvent });
41
42
  case 'qoder-stream-json':
@@ -68,7 +69,7 @@ export function defaultCleanupFailureSink(context) {
68
69
  /**
69
70
  * Reports a contained post-close failure through the host's sink, absorbing a throwing sink.
70
71
  *
71
- * A diagnostic sink is host code too, and the whole point of the two callers below is that nothing
72
+ * A diagnostic sink is host code too, and the whole point of these callers is that nothing
72
73
  * between `'close'` and `finish()` can strand the run — a sink that throws must not reintroduce
73
74
  * exactly that. Same reasoning `run-lifecycle.ts`'s `handleInactivityTimeout` already applies to its
74
75
  * own `onInternalError`.
@@ -273,6 +274,37 @@ export function wireChildLifecycle(ctx) {
273
274
  // consumed by a turn-end injection decision. See `ContinuationOptions`'s doc for why this is
274
275
  // only ever acted on when a host has explicitly allowlisted the tool's name.
275
276
  let pendingToolUse;
277
+ // Observe the canonical lifecycle, including delegated calls that bypass the CLI parser.
278
+ // A single latest tool_use cannot represent parallel tools at an interrupt boundary.
279
+ const unresolvedToolCalls = new Set();
280
+ const interruptedToolCalls = new Set();
281
+ const toolSubscription = lifecycle.stream({ runId, onEvent: (event) => {
282
+ if (event.kind !== 'agent')
283
+ return;
284
+ if (event.payload.type === 'tool_use')
285
+ unresolvedToolCalls.add(event.payload.id);
286
+ if (event.payload.type === 'tool_result')
287
+ unresolvedToolCalls.delete(event.payload.toolUseId);
288
+ } }).catch(error => {
289
+ // Observing tools is fallible I/O too: report it, but never strand close/finish on a replay
290
+ // failure or leave an unhandled rejection while the child is still running.
291
+ reportPostCloseFailure(ctx.onCleanupFailure, { runId, phase: 'tool-cancellation', pid: child.pid, error });
292
+ return null;
293
+ });
294
+ /** Run on the same FIFO as parsed results, after the aborted segment has drained.
295
+ * O(t) in unresolved calls; completed results are never replaced with cancellation. */
296
+ async function cancelUnresolvedToolCalls(ids) {
297
+ await toolSubscription;
298
+ for (const toolUseId of ids ?? [...unresolvedToolCalls]) {
299
+ if (!unresolvedToolCalls.has(toolUseId))
300
+ continue;
301
+ await lifecycle.emit({ runId, input: { event: 'agent', data: {
302
+ type: 'tool_result', toolUseId, content: 'Tool execution cancelled.', isError: true,
303
+ } } }).catch(error => {
304
+ reportPostCloseFailure(ctx.onCleanupFailure, { runId, phase: 'tool-cancellation', pid: child.pid, error });
305
+ });
306
+ }
307
+ }
276
308
  // Bridge guard (see `expectsJiniBridge`): checked on the first init frame only. Once the bridge is
277
309
  // found unavailable, the child is being stopped and nothing it still prints is forwarded.
278
310
  let bridgeChecked = !ctx.expectsJiniBridge;
@@ -311,6 +343,71 @@ export function wireChildLifecycle(ctx) {
311
343
  stdinClosed = true;
312
344
  child.stdin?.end();
313
345
  }
346
+ // Mid-run interrupts (see `sendUserMessage` below): each sent `interrupt` control request waits
347
+ // here, by request id, for the CLI's `control_response`. Settled with `true` on the ack, `false`
348
+ // when the process exits first. Bounded by the mid-run messages in flight on this one run.
349
+ const pendingInterruptAcks = new Map();
350
+ // Set once this run has interrupted its agent: from then on a turn the interrupt aborted
351
+ // (`aborted_*` stop reason) is not the run's last — the message that follows starts a new one.
352
+ let interruptSent = false;
353
+ // Claude Code's background tasks still running (a subagent or shell it started in the
354
+ // background), from its latest `background_tasks_changed` line, which lists them all. While any
355
+ // runs, an `end_turn` is not the run's end: the CLI keeps going until they finish and reports on
356
+ // them, so stdin must stay open for mid-run messages. Closing it there (2026-10-10, plannie chat)
357
+ // made every message sent while two subagents worked come back 'not-running' and wait for the run
358
+ // to end — and at EOF the CLI stops background tasks at its own wait ceiling (10 minutes).
359
+ let backgroundTaskCount = 0;
360
+ // Set when a turn ended and stdin stayed open only for background tasks: no turn is running, so a
361
+ // mid-run message has nothing to interrupt, and the last task finishing closes stdin (below).
362
+ let heldForBackgroundTasks = false;
363
+ // stdout carry-over for the control lines below; holds at most the current partial line.
364
+ let controlScanBuffer = '';
365
+ /** Resolves the waiting interrupt this `control_response` line acknowledges, if any. @complexity O(line length). */
366
+ function settleInterruptAck(line) {
367
+ const requestId = controlResponseRequestId({ line: line });
368
+ const settle = requestId === undefined ? undefined : pendingInterruptAcks.get(requestId);
369
+ if (!settle)
370
+ return;
371
+ pendingInterruptAcks.delete(requestId);
372
+ settle(true);
373
+ }
374
+ /**
375
+ * Tracks a `background_tasks_changed` line. The last background task finishing while stdin is
376
+ * held only for them closes it, exactly as the turn end would have: the CLI then answers what it
377
+ * still owes (the tasks' report) and exits. @complexity O(line length).
378
+ */
379
+ function updateBackgroundTasks(line) {
380
+ const count = backgroundTaskListLength({ line: line });
381
+ if (count === undefined)
382
+ return;
383
+ backgroundTaskCount = count;
384
+ if (count === 0 && heldForBackgroundTasks) {
385
+ heldForBackgroundTasks = false;
386
+ closeStdinOnce();
387
+ }
388
+ }
389
+ /**
390
+ * Reads the stdout lines a mid-run message depends on: interrupt acks and background-task
391
+ * changes. Only agents that take mid-run messages are scanned. A chunk with no newline is only
392
+ * appended, so a long line split across many chunks is split once, not once per chunk.
393
+ * @complexity O(chunk length) amortized.
394
+ */
395
+ function scanControlLines(text) {
396
+ if (!acceptsMidRunUserMessages({ def: def }))
397
+ return;
398
+ controlScanBuffer += text;
399
+ if (!text.includes('\n'))
400
+ return;
401
+ const lines = controlScanBuffer.split('\n');
402
+ controlScanBuffer = lines.pop() ?? '';
403
+ for (const line of lines) {
404
+ // Both checked: either word can also appear inside the other kind of line's text.
405
+ if (line.includes('"control_response"'))
406
+ settleInterruptAck(line);
407
+ if (line.includes('"background_tasks_changed"'))
408
+ updateBackgroundTasks(line);
409
+ }
410
+ }
314
411
  /**
315
412
  * Writes a structured (never string-concatenated — see `ContinuationOptions`'s doc on the
316
413
  * prompt-injection stakes here) tool_result JSONL line, mirroring the shape
@@ -330,6 +427,26 @@ export function wireChildLifecycle(ctx) {
330
427
  if (journal)
331
428
  enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
332
429
  }
430
+ /**
431
+ * Whether a `turn_end` leaves stdin open for a live agent that takes mid-run messages — and why:
432
+ * `'background-tasks'` when only background tasks keep the run going (no turn runs until the next
433
+ * message or task report), `'turn-continues'` for the reasons below, `'close'` otherwise. Open
434
+ * because the run is not over: a `tool_use` stop (the CLI runs the tool itself and carries on, and a
435
+ * message sent while the tool runs must still reach it), any end while an interrupt still waits
436
+ * for its ack (the message is about to be written), and the end of a turn an interrupt aborted
437
+ * (the message written after it starts the next turn), and any end while background tasks still
438
+ * run (see `backgroundTaskCount`). Every other end closes stdin, so the CLI exits once it has
439
+ * answered everything written before EOF.
440
+ */
441
+ function stdinHoldAfterTurnEnd(stopReason) {
442
+ if (!acceptsMidRunUserMessages({ def: def }))
443
+ return 'close';
444
+ if (stopReason === 'tool_use' || pendingInterruptAcks.size > 0)
445
+ return 'turn-continues';
446
+ if (interruptSent && stopReason !== undefined && stopReason.startsWith('aborted'))
447
+ return 'turn-continues';
448
+ return backgroundTaskCount > 0 ? 'background-tasks' : 'close';
449
+ }
333
450
  /**
334
451
  * Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
335
452
  * `ToolExecutor` and keep stdin open (gap 3), or close stdin exactly as every version of this
@@ -337,6 +454,11 @@ export function wireChildLifecycle(ctx) {
337
454
  * unconfigured or the pending tool isn't allowlisted).
338
455
  */
339
456
  function handleTurnEnd(stopReason) {
457
+ if (stopReason?.startsWith('aborted'))
458
+ enqueueEmit(async () => {
459
+ await cancelUnresolvedToolCalls([...interruptedToolCalls]);
460
+ interruptedToolCalls.clear();
461
+ });
340
462
  const toolUse = pendingToolUse;
341
463
  const shouldInject = stopReason === 'tool_use' &&
342
464
  toolUse !== undefined &&
@@ -344,7 +466,10 @@ export function wireChildLifecycle(ctx) {
344
466
  resolveContinuationTransport({ def: def }) === 'stdin-injection' &&
345
467
  continuation.autonomousToolNames.has(toolUse.name);
346
468
  if (!shouldInject) {
347
- closeStdinOnce();
469
+ const hold = stdinHoldAfterTurnEnd(stopReason);
470
+ heldForBackgroundTasks = hold === 'background-tasks';
471
+ if (hold === 'close')
472
+ closeStdinOnce();
348
473
  return;
349
474
  }
350
475
  pendingToolUse = undefined;
@@ -497,6 +622,10 @@ export function wireChildLifecycle(ctx) {
497
622
  return;
498
623
  }
499
624
  enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
625
+ // Before the parser: an ack and the aborted turn's end can share a chunk, and the turn end must
626
+ // see that no interrupt is still waiting (see `stdinHoldAfterTurnEnd`); a background task
627
+ // starting and the turn's end can share one too.
628
+ scanControlLines(text);
500
629
  // Non-null: `streamHandler` is only ever null when `streamFormat === 'plain'` (see its
501
630
  // construction above), the branch this statement is provably unreachable from.
502
631
  streamHandler.feed({ chunk: text });
@@ -522,6 +651,9 @@ export function wireChildLifecycle(ctx) {
522
651
  void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
523
652
  } });
524
653
  child.on('close', (code, signal) => {
654
+ for (const settle of pendingInterruptAcks.values())
655
+ settle(false);
656
+ pendingInterruptAcks.clear();
525
657
  void (async () => {
526
658
  // Not wrapped in try/catch: all 4 supported parser factories'
527
659
  // flush() implementations already internally guard their own
@@ -536,6 +668,11 @@ export function wireChildLifecycle(ctx) {
536
668
  // `finish()`'s `'end'` event, exactly like every live-path emit already is.
537
669
  flushBufferedStdout();
538
670
  await emitQueue;
671
+ if (cancelRequested)
672
+ await cancelUnresolvedToolCalls();
673
+ const subscribedTools = await toolSubscription;
674
+ if (subscribedTools?.kind === 'ok')
675
+ subscribedTools.unsubscribe();
539
676
  unsubscribeCancel();
540
677
  // Both of the next two steps are guarded: neither a failed cleanup nor a rejecting host
541
678
  // classifier may prevent the terminal transition below — see each helper's own doc.
@@ -554,14 +691,114 @@ export function wireChildLifecycle(ctx) {
554
691
  await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
555
692
  })();
556
693
  });
694
+ /**
695
+ * A mid-run message: first an `interrupt` control request on the still-open stdin, so the agent
696
+ * stops generating (or abandons the tool it is running) at once instead of finishing its whole
697
+ * reply; then, once the CLI acknowledges, the message as the next user line, which the CLI
698
+ * answers straight away in the same session; then the run's own `user_message` event through the
699
+ * same FIFO as the agent's output, so the message sits in the stream where it took effect.
700
+ * An ack that never comes (an agent that ignores control requests) only delays the write by
701
+ * {@link INTERRUPT_ACK_TIMEOUT_MS}: the CLI then folds the line in at its next step. Once
702
+ * `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
703
+ * reach — `'not-running'`. Between turns, while stdin is held only for background tasks, there is
704
+ * no generation to stop: the line is written at once and starts the next turn.
705
+ */
706
+ async function sendUserMessage(text) {
707
+ if (!acceptsMidRunUserMessages({ def: def }))
708
+ return 'unsupported';
709
+ const stdin = child.stdin;
710
+ if (stdinClosed || !stdin)
711
+ return 'not-running';
712
+ if (heldForBackgroundTasks) {
713
+ heldForBackgroundTasks = false;
714
+ writeUserMessage(stdin, text);
715
+ return 'delivered';
716
+ }
717
+ const requestId = `jini-interrupt-${randomUUID()}`;
718
+ interruptSent = true;
719
+ // Capture before requesting the interrupt. Its result can precede the acknowledgement, or
720
+ // arrive after the next segment starts: neither ordering may cancel that segment's new tools.
721
+ enqueueEmit(async () => {
722
+ await toolSubscription;
723
+ for (const id of interruptedToolCalls)
724
+ if (!unresolvedToolCalls.has(id))
725
+ interruptedToolCalls.delete(id);
726
+ for (const id of unresolvedToolCalls)
727
+ interruptedToolCalls.add(id);
728
+ });
729
+ const acked = new Promise((resolve) => pendingInterruptAcks.set(requestId, resolve));
730
+ stdin.write(`${JSON.stringify({ type: 'control_request', request_id: requestId, request: { subtype: 'interrupt' } })}\n`, 'utf8');
731
+ let timer;
732
+ const timedOut = new Promise((resolve) => {
733
+ timer = setTimeout(() => resolve(true), INTERRUPT_ACK_TIMEOUT_MS);
734
+ });
735
+ const processAlive = await Promise.race([acked, timedOut]);
736
+ clearTimeout(timer);
737
+ pendingInterruptAcks.delete(requestId);
738
+ if (!processAlive || stdinClosed)
739
+ return 'not-running';
740
+ writeUserMessage(stdin, text);
741
+ return 'delivered';
742
+ }
743
+ /** Writes a mid-run message as the next user line, journals it, and emits its `user_message` run event. */
744
+ function writeUserMessage(stdin, text) {
745
+ stdin.write(`${JSON.stringify(streamJsonUserMessage({ content: [{ type: 'text', text: text }] }))}\n`, 'utf8');
746
+ if (journal)
747
+ enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(text) }));
748
+ enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
749
+ }
557
750
  return {
558
751
  closeStdinOnce,
559
752
  recordSentBytes(content) {
560
753
  if (journal)
561
754
  enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
562
755
  },
756
+ sendUserMessage,
563
757
  };
564
758
  }
759
+ /**
760
+ * Whether a def's live process can take another user message mid-run: only a stream-json stdin
761
+ * transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
762
+ * Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
763
+ * reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
764
+ */
765
+ export function acceptsMidRunUserMessages({ def }) {
766
+ return def.promptViaStdin === true && def.promptInputFormat === 'stream-json';
767
+ }
768
+ /**
769
+ * How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
770
+ * anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
771
+ * the ceiling only bounds an agent that ignores control requests.
772
+ */
773
+ export const INTERRUPT_ACK_TIMEOUT_MS = 5_000;
774
+ /** The `request_id` of a stream-json `control_response` line, or `undefined` for anything else. */
775
+ function controlResponseRequestId({ line }) {
776
+ try {
777
+ const parsed = JSON.parse(line);
778
+ if (!isRecord(parsed) || parsed.type !== 'control_response' || !isRecord(parsed.response))
779
+ return undefined;
780
+ return typeof parsed.response.request_id === 'string' ? parsed.response.request_id : undefined;
781
+ }
782
+ catch {
783
+ return undefined;
784
+ }
785
+ }
786
+ /** How many tasks a `background_tasks_changed` line lists, or `undefined` for any other line. */
787
+ function backgroundTaskListLength({ line }) {
788
+ try {
789
+ const parsed = JSON.parse(line);
790
+ if (!isRecord(parsed) || parsed.type !== 'system' || parsed.subtype !== 'background_tasks_changed')
791
+ return undefined;
792
+ return Array.isArray(parsed.tasks) ? parsed.tasks.length : undefined;
793
+ }
794
+ catch {
795
+ return undefined;
796
+ }
797
+ }
798
+ /** The stream-json stdin envelope for one user message — the initial prompt and every mid-run one. */
799
+ function streamJsonUserMessage({ content }) {
800
+ return { type: 'user', message: { role: 'user', content: content } };
801
+ }
565
802
  /**
566
803
  * Wires an ACP child to a run. Unlike the JSON-stream path, ACP owns the
567
804
  * prompt protocol and reports its parsed events through `attachAcpSession`'s
@@ -773,8 +1010,8 @@ function wirePiRpcLifecycle(ctx) {
773
1010
  * - `'text'` (default): the raw prompt buffer, then stdin is closed —
774
1011
  * matches `RuntimeAgentDef.promptInputFormat`'s own doc.
775
1012
  * - `'stream-json'`: one JSONL line wrapping the prompt as an Anthropic
776
- * user message; stdin is deliberately left open (a real multi-turn
777
- * caller would inject further messages) — v1 has no such caller, so
1013
+ * user message; stdin is deliberately left open so a message the human
1014
+ * sends mid-run can follow it (`StdinCloseHandle.sendUserMessage`), and
778
1015
  * {@link wireChildLifecycle}'s `turn_end` handling closes it once the
779
1016
  * agent's own stream reports the turn ended.
780
1017
  * @param def - The resolved agent def (only `.promptInputFormat` is read).
@@ -789,7 +1026,7 @@ export function writePromptToStdin(def, child, prompt, handle) {
789
1026
  if (!stdin)
790
1027
  return;
791
1028
  if (def.promptInputFormat === 'stream-json') {
792
- const line = JSON.stringify({ type: 'user', message: { role: 'user', content: messageContentWithImages({ prompt, images: handle.imageContents ?? [] }, {}) } });
1029
+ const line = JSON.stringify(streamJsonUserMessage({ content: messageContentWithImages({ prompt, images: handle.imageContents ?? [] }, {}) }));
793
1030
  stdin.write(`${line}\n`, 'utf8');
794
1031
  handle.recordSentBytes(prompt);
795
1032
  return;
@@ -89,8 +89,8 @@
89
89
  * a second, independently-built (and rejected) classifier that once lived in
90
90
  * this module.
91
91
  */
92
- export { type SupportedStreamFormat, type AgentExecutorCompatibility, type AgentRuntimeEventTranslation, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, type McpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, type FailureClassificationContext, type ClassifyFailure, type CreateAgentExecutorOptions, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult, } from './contracts.js';
93
- export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
92
+ export { type SupportedStreamFormat, type AgentExecutorCompatibility, type AgentRuntimeEventTranslation, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, type McpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, type FailureClassificationContext, type ClassifyFailure, type CreateAgentExecutorOptions, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult, } from './contracts.js';
93
+ export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
94
94
  export { extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, applyAgentTranslationSideEffects, } from './event-translation.js';
95
95
  export { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, resolveMcpBridgeForRun, writeMcpJsonIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, } from './mcp-bridge.js';
96
96
  export { DEFAULT_BUFFERED_STDOUT_MAX_BYTES, runAcpDispatch, runPiRpcDispatch, } from './events.js';
@@ -90,7 +90,7 @@
90
90
  * this module.
91
91
  */
92
92
  export { AgentExecutorError, } from './contracts.js';
93
- export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
93
+ export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, selectAgentExecutorHostEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
94
94
  export { extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, applyAgentTranslationSideEffects, } from './event-translation.js';
95
95
  export { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, resolveMcpBridgeForRun, writeMcpJsonIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, } from './mcp-bridge.js';
96
96
  export { DEFAULT_BUFFERED_STDOUT_MAX_BYTES, runAcpDispatch, runPiRpcDispatch, } from './events.js';
@@ -1,5 +1,5 @@
1
1
  import { spawn as nodeSpawn, type ChildProcess } from 'node:child_process';
2
- import { getAgentDef, resolveAgentLaunch, prepareAgentLogFile, preparePromptFileForAgent, type AgentLaunchResolution, type PreparedAgentLogFile, type PreparedPromptFile, type RuntimeAgentDef, type RuntimeBuildOptions, type RuntimeContext } from '@jini-ai/agent-runtime';
2
+ import { type AgentRuntimeState, getAgentDef, resolveAgentLaunch, prepareAgentLogFile, preparePromptFileForAgent, type AgentLaunchResolution, type PreparedAgentLogFile, type PreparedPromptFile, type RuntimeAgentDef, type RuntimeBuildOptions, type RuntimeContext } from '@jini-ai/agent-runtime';
3
3
  import { createCommandInvocation } from '@jini-ai/platform';
4
4
  import { type ImagePromptDelivery } from '../image-prompt-delivery.js';
5
5
  import { type SupportedStreamFormat, type ChildDrivenStreamFormat, type AgentExecutorCompatibility, type AgentExecutorRunInput, type AgentExecutor, type McpBridgeDelivery, type CreateAgentExecutorOptions, type FailBeforeSpawn, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult } from './contracts.js';
@@ -53,6 +53,13 @@ export declare function assessAgentExecutorCompatibility({ def }: {
53
53
  export declare function isAgentExecutorSupported({ def }: {
54
54
  readonly def: RuntimeAgentDef;
55
55
  }): boolean;
56
+ /** Snapshot only host configuration used by spawn and config staging; credentials stay per-run.
57
+ * Returns an independent allowlisted record. @complexity O(1), fixed key list. */
58
+ export declare function selectAgentExecutorHostEnv({ env }: {
59
+ env: NodeJS.ProcessEnv;
60
+ }, { runtimeState }?: {
61
+ runtimeState?: AgentRuntimeState;
62
+ }): NodeJS.ProcessEnv;
56
63
  interface ResolvedDefAndFormat {
57
64
  readonly def: RuntimeAgentDef;
58
65
  readonly streamFormat: SupportedStreamFormat;
@@ -236,6 +243,8 @@ export declare function resolveSystemPromptOverlayDelivery(input: {
236
243
  readonly overlay: string | null | undefined;
237
244
  readonly prompt: string;
238
245
  readonly resumeSessionId: string | null | undefined;
246
+ }, { state }?: {
247
+ state?: AgentRuntimeState;
239
248
  }): SystemPromptOverlayDelivery;
240
249
  /**
241
250
  * Phase 9b: calls the def's `buildArgs`, releasing staged resources and failing the run on a throw.
@@ -370,6 +379,6 @@ export declare function confirmChildSpawned({ input, deps }: {
370
379
  * @complexity `run()`'s own setup is O(1); steady-state cost is the chosen stream parser's.
371
380
  * @overallScore 100/100
372
381
  */
373
- export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
382
+ export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "runtimeState" | "hostEnv" | "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
374
383
  export {};
375
384
  //# sourceMappingURL=launch.d.ts.map