@jini-ai/daemon 0.2.1 → 0.3.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.
Files changed (77) hide show
  1. package/README.md +92 -0
  2. package/dist/agent-executor.d.ts +864 -42
  3. package/dist/agent-executor.d.ts.map +1 -1
  4. package/dist/agent-executor.js +1672 -351
  5. package/dist/agent-executor.js.map +1 -1
  6. package/dist/continuation/index.d.ts +1 -0
  7. package/dist/continuation/index.d.ts.map +1 -1
  8. package/dist/continuation/index.js +1 -0
  9. package/dist/continuation/index.js.map +1 -1
  10. package/dist/continuation/run-scoped-context-store.d.ts +79 -0
  11. package/dist/continuation/run-scoped-context-store.d.ts.map +1 -0
  12. package/dist/continuation/run-scoped-context-store.js +56 -0
  13. package/dist/continuation/run-scoped-context-store.js.map +1 -0
  14. package/dist/continuation/run-start-handler.d.ts +39 -9
  15. package/dist/continuation/run-start-handler.d.ts.map +1 -1
  16. package/dist/continuation/run-start-handler.js +12 -2
  17. package/dist/continuation/run-start-handler.js.map +1 -1
  18. package/dist/delegated-tool-bridge.d.ts +8 -0
  19. package/dist/delegated-tool-bridge.d.ts.map +1 -1
  20. package/dist/delegated-tool-bridge.js +117 -1
  21. package/dist/delegated-tool-bridge.js.map +1 -1
  22. package/dist/event-log.d.ts +18 -105
  23. package/dist/event-log.d.ts.map +1 -1
  24. package/dist/event-log.js +0 -17
  25. package/dist/event-log.js.map +1 -1
  26. package/dist/frontend-capability-tools.d.ts +1 -1
  27. package/dist/frontend-capability-tools.js +1 -1
  28. package/dist/frontend-session-registry.d.ts.map +1 -1
  29. package/dist/frontend-session-registry.js +26 -10
  30. package/dist/frontend-session-registry.js.map +1 -1
  31. package/dist/image-prompt-delivery.d.ts +56 -0
  32. package/dist/image-prompt-delivery.d.ts.map +1 -0
  33. package/dist/image-prompt-delivery.js +104 -0
  34. package/dist/image-prompt-delivery.js.map +1 -0
  35. package/dist/index.d.ts +2 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/remote-tool-bridge.d.ts +45 -0
  40. package/dist/remote-tool-bridge.d.ts.map +1 -0
  41. package/dist/remote-tool-bridge.js +23 -0
  42. package/dist/remote-tool-bridge.js.map +1 -0
  43. package/dist/routines/routine-store.js +1 -1
  44. package/dist/routines/schedule.d.ts +0 -8
  45. package/dist/routines/schedule.d.ts.map +1 -1
  46. package/dist/routines/schedule.js +71 -44
  47. package/dist/routines/schedule.js.map +1 -1
  48. package/dist/routines/scheduler.d.ts +43 -0
  49. package/dist/routines/scheduler.d.ts.map +1 -1
  50. package/dist/routines/scheduler.js +160 -120
  51. package/dist/routines/scheduler.js.map +1 -1
  52. package/dist/run/core/retry.d.ts.map +1 -1
  53. package/dist/run/core/retry.js +47 -35
  54. package/dist/run/core/retry.js.map +1 -1
  55. package/dist/run/diagnostics/diagnostics.d.ts.map +1 -1
  56. package/dist/run/diagnostics/diagnostics.js +149 -91
  57. package/dist/run/diagnostics/diagnostics.js.map +1 -1
  58. package/dist/run-lifecycle.d.ts +84 -4
  59. package/dist/run-lifecycle.d.ts.map +1 -1
  60. package/dist/run-lifecycle.js +378 -116
  61. package/dist/run-lifecycle.js.map +1 -1
  62. package/dist/terminal-session.d.ts +1 -1
  63. package/dist/terminal-session.d.ts.map +1 -1
  64. package/dist/terminal-session.js +1 -1
  65. package/dist/tool-executor.d.ts +26 -6
  66. package/dist/tool-executor.d.ts.map +1 -1
  67. package/dist/tool-executor.js +220 -55
  68. package/dist/tool-executor.js.map +1 -1
  69. package/dist/tool-result-media.d.ts +79 -0
  70. package/dist/tool-result-media.d.ts.map +1 -0
  71. package/dist/tool-result-media.js +80 -0
  72. package/dist/tool-result-media.js.map +1 -0
  73. package/dist/tool-result-surfaces.d.ts +78 -0
  74. package/dist/tool-result-surfaces.d.ts.map +1 -0
  75. package/dist/tool-result-surfaces.js +92 -0
  76. package/dist/tool-result-surfaces.js.map +1 -0
  77. package/package.json +18 -9
@@ -9,7 +9,7 @@
9
9
  * a real `node:child_process` spawn, feeding both `RunLifecycle.emit()` and
10
10
  * this package's own `@jini-ai/protocol` event envelope.
11
11
  *
12
- * ## v1 scope: 23 of 24 registered agent defs
12
+ * ## v1 scope: all 24 registered agent defs
13
13
  *
14
14
  * `@jini-ai/agent-runtime`'s registry ships 24 built-in defs across four
15
15
  * `streamFormat` families. The JSON-stream-parser family — the four
@@ -25,14 +25,14 @@
25
25
  * `mapPiRpcEvent` `send()` call site — no new translation code was needed),
26
26
  * so only the driver wiring (spawn → attach → cancel → finish) was new for it.
27
27
  *
28
- * 4 of the 5 `streamFormat: 'plain'` defs — grok-build, aider, deepseek,
29
- * qwen — are also driven, per
28
+ * All 5 `streamFormat: 'plain'` defs — grok-build, aider, deepseek, qwen,
29
+ * antigravity — are also driven, per
30
30
  * `ADS-memory/reports/proposals/PROP-plain-format-agent-driving-2026-07-21.md`'s
31
- * recommended "Option B": no structured stream parser at all. Every raw
32
- * `child.stdout` chunk is forwarded verbatim as a `text_delta` `'agent'`
33
- * event, live, as it arrives — never buffered until close (see
34
- * `wireChildLifecycle`'s `streamFormat === 'plain'` branch). Prompt delivery
35
- * across the 4 is not uniform: qwen already fit the pre-existing stdin-only
31
+ * recommended "Option B": no structured stream parser at all. By default
32
+ * every raw `child.stdout` chunk is forwarded verbatim as a `text_delta`
33
+ * `'agent'` event, live, as it arrives (see `wireChildLifecycle`'s
34
+ * `streamFormat === 'plain'` branch). Prompt delivery across the 5 is not
35
+ * uniform: qwen and antigravity already fit the pre-existing stdin-only
36
36
  * guard; grok-build stages the prompt to a temp file via
37
37
  * `preparePromptFileForAgent` (its path threaded into `buildArgs` through a
38
38
  * `RuntimeContext`, cleaned up after the child exits on every path,
@@ -41,16 +41,27 @@
41
41
  * Windows CreateProcess command-line-expansion guards
42
42
  * (`checkWindowsCmdShimCommandLineBudget`/`checkWindowsDirectExeCommandLineBudget`).
43
43
  *
44
- * The 5th plain def, **antigravity, is deliberately still rejected.** It
45
- * needs two concerns unrelated to `streamFormat: 'plain'` itself — buffering
46
- * stdout until close so a leaked OAuth URL can be suppressed before it
47
- * reaches the client, and a cross-run lock serializing writes to its shared
48
- * `settings.json` model-selection file that the proposal doc explicitly
49
- * scoped out to its own follow-up (see that doc's §2c/§3). `run()` guards it
50
- * with its own `def.id === 'antigravity'` check, ahead of (and independent
51
- * of) the generic plain-format prompt-delivery/dispatch logic. `run()`
52
- * rejects cleanly (never a bare throw) with an `AgentExecutorError` for any
53
- * def outside the supported 23 see `isSupportedStreamFormat`.
44
+ * ## Antigravity's two extra needs, met declaratively
45
+ *
46
+ * Antigravity was the one def this driver rejected outright, for two reasons
47
+ * the proposal doc (§2c) scoped out to a follow-up: `agy` can print an OAuth
48
+ * sign-in URL to stdout and *still exit 0*, so live streaming leaks it; and
49
+ * its model choice is written into one process-global `settings.json` that
50
+ * `agy` reads on its own startup, so two concurrent runs race on it.
51
+ *
52
+ * Both are now met through **declarative `RuntimeAgentDef` fields this driver
53
+ * reads generically** `needsAgentLogFile`, `stdoutPolicy`, `runtimeLock`
54
+ * not a `def.id === 'antigravity'` branch. That mirrors how all 14 of the
55
+ * def's other optional behavior flags (`promptViaFile`, `authProbe`,
56
+ * `capturesSessionIdFromStream`, …) already work, and it is a deliberate
57
+ * divergence from OD's own `server.ts`, which hardcodes `def.id ===
58
+ * 'antigravity'` twice. The three fields are no-ops for the other 23 defs,
59
+ * none of which declares any of them — so nothing else's behavior changed.
60
+ *
61
+ * `run()` still rejects cleanly (never a bare throw) with an
62
+ * `AgentExecutorError` for any def whose `streamFormat` or prompt-delivery
63
+ * shape this driver does not implement — see `isSupportedStreamFormat` and
64
+ * `assessAgentExecutorCompatibility`.
54
65
  *
55
66
  * ## Invariant
56
67
  *
@@ -72,24 +83,37 @@
72
83
  * deliberately never ported (see `run/core/failure-taxonomy.ts`'s own doc and
73
84
  * `source-map.md`). The real zero-config classifier lives in `@jini-ai/daemon`'s
74
85
  * `run/core/retry.ts` (`resumableFromProcessExit`/`classifyProcessExitFailure`)
75
- * and is wired in by `@jini-ai/node-host`'s `createLocalNodeDaemon` — see that
86
+ * and is wired in by `@jini-ai/server`'s `createLocalNodeDaemon` — see that
76
87
  * package's own source-map.md, and `run/core/retry.ts`'s own doc for the
77
88
  * classification policy and its 2026-07-22 merge-time reconciliation against
78
89
  * a second, independently-built (and rejected) classifier that once lived in
79
90
  * this module.
80
91
  */
81
- import { spawn as nodeSpawn } from 'node:child_process';
92
+ import { spawn as nodeSpawn, type ChildProcess } from 'node:child_process';
82
93
  import type { Principal } from '@jini-ai/core';
83
94
  import type { RunAgentPayload, RunErrorPayload } from '@jini-ai/protocol';
84
- import { applyAgentLaunchEnv, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, preparePromptFileForAgent, type AcpPermissionHandler } from '@jini-ai/agent-runtime';
85
- import { collectProcessTreePids, createCommandInvocation, listProcessSnapshots, stopProcesses } from '@jini-ai/platform';
95
+ import { applyAgentLaunchEnv, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, type AcpMcpServerInput, type AcpPermissionHandler, type AgentLaunchResolution, type PreparedAgentLogFile, type PreparedPromptFile, type PromptAugmenter, type RuntimeAgentDef, type RuntimeBuildOptions, type RuntimeContext, type RuntimeLockHold } from '@jini-ai/agent-runtime';
96
+ import { collectProcessTreePids, createCommandInvocation, listProcessSnapshots, stopProcesses, type ProcessSnapshot, type StopProcessesResult } from '@jini-ai/platform';
86
97
  import type { RunByteJournal } from './continuation/journal.js';
98
+ import { type ImagePromptDelivery } from './image-prompt-delivery.js';
87
99
  import type { RunRetrySideEffectState } from './run/core/index.js';
88
100
  import type { ToolExecutor } from './tool-executor.js';
89
101
  import type { RunLifecycle } from './run-lifecycle.js';
90
102
  declare const SUPPORTED_STREAM_FORMATS: readonly ["claude-stream-json", "json-event-stream", "copilot-stream-json", "qoder-stream-json", "acp-json-rpc", "pi-rpc", "plain"];
91
103
  /** The families of the registry's stream-format this driver implements — see module doc. */
92
104
  export type SupportedStreamFormat = (typeof SUPPORTED_STREAM_FORMATS)[number];
105
+ type JsonStreamFormat = Exclude<SupportedStreamFormat, 'acp-json-rpc' | 'pi-rpc' | 'plain'>;
106
+ /**
107
+ * `streamFormat` values `wireChildLifecycle` drives directly off raw
108
+ * `child.stdout` `'data'` events: the 4 JSON-stream-parser formats (fed
109
+ * through a real `feed()`/`flush()` state machine via
110
+ * {@link createStreamHandlerForDef}) plus `'plain'` (no parser at all —
111
+ * each chunk is forwarded verbatim as a `text_delta`, see that function's
112
+ * doc). Distinct from `'acp-json-rpc'`/`'pi-rpc'`, which own their own
113
+ * JSON-RPC prompt/event protocol and get their own
114
+ * `wireAcpLifecycle`/`wirePiRpcLifecycle` wiring instead.
115
+ */
116
+ type ChildDrivenStreamFormat = JsonStreamFormat | 'plain';
93
117
  /**
94
118
  * Narrows a `RuntimeAgentDef.streamFormat` string to the supported
95
119
  * families.
@@ -99,6 +123,52 @@ export type SupportedStreamFormat = (typeof SUPPORTED_STREAM_FORMATS)[number];
99
123
  * @overallScore 100/100
100
124
  */
101
125
  export declare function isSupportedStreamFormat(value: string): value is SupportedStreamFormat;
126
+ /**
127
+ * Whether `run()` can drive a given def, and — when it can — its `streamFormat` already narrowed for
128
+ * the dispatch logic that follows. The narrowing rides along deliberately: it is what lets `run()`
129
+ * delegate every compatibility guard here without then re-checking the format to satisfy the type
130
+ * system, which would leave an unreachable branch behind.
131
+ */
132
+ export type AgentExecutorCompatibility = {
133
+ readonly supported: true;
134
+ readonly streamFormat: SupportedStreamFormat;
135
+ } | {
136
+ readonly supported: false;
137
+ readonly reason: string;
138
+ };
139
+ /**
140
+ * The single source of truth for whether this executor can drive a def.
141
+ *
142
+ * It exists because that knowledge was previously reachable only by *calling* `run()` and inspecting
143
+ * the failure. Anything that lists agents for a user to pick from — a discovery route, an agent
144
+ * picker, a CLI healthcheck — needs the same answer *before* a run exists, and had no way to ask it.
145
+ * The observable symptom was a consumer advertising an agent that its own executor then rejected the
146
+ * instant it was selected.
147
+ *
148
+ * `run()` consumes this rather than re-checking the conditions itself, so the discovery-time answer
149
+ * and the run-time guards cannot disagree. A predicate that merely duplicated the guards would be
150
+ * the same bug in a second location.
151
+ *
152
+ * @param def - The def to assess. Must be the **full** `RuntimeAgentDef`, not a projected
153
+ * `DetectedAgent`: that type omits `maxPromptArgBytes`, one of the three prompt-delivery signals
154
+ * checked here, so the argv-bound defs (`aider`, `deepseek`) would be misjudged as unsupported.
155
+ * @returns A discriminated result — see {@link AgentExecutorCompatibility}. The `reason` text is
156
+ * operator-facing and is what `run()` reports as its `AGENT_RUNTIME_UNSUPPORTED` message.
157
+ * @complexity O(1) — fixed field checks.
158
+ * @overallScore 100/100
159
+ */
160
+ export declare function assessAgentExecutorCompatibility(def: RuntimeAgentDef): AgentExecutorCompatibility;
161
+ /**
162
+ * Whether `run()` can actually drive this def — the discovery-time counterpart to the guards inside
163
+ * `run()`, so a consumer never offers a user an agent that fails the moment it is selected.
164
+ *
165
+ * @param def - The full `RuntimeAgentDef`; see {@link assessAgentExecutorCompatibility} for why a
166
+ * projected `DetectedAgent` is not sufficient.
167
+ * @returns `true` when this executor would attempt the run.
168
+ * @complexity O(1).
169
+ * @overallScore 100/100
170
+ */
171
+ export declare function isAgentExecutorSupported(def: RuntimeAgentDef): boolean;
102
172
  /**
103
173
  * Result of translating one parsed stream event into this engine's
104
174
  * vocabulary. `'agent'` is the common case (forward as a `RunAgentPayload`
@@ -143,6 +213,23 @@ export type AgentRuntimeEventTranslation = {
143
213
  } | {
144
214
  readonly kind: 'ignored';
145
215
  };
216
+ /**
217
+ * Narrows a parsed `usage` event's `usage` sub-object (`{input_tokens?, output_tokens?}`) — the one
218
+ * piece of {@link translateUsagePayload} with real nested branching (an optional container holding
219
+ * two optional numeric fields), extracted so that function reads as a flat field-by-field mapping.
220
+ * @param rawUsage - `rawEvent.usage` once already narrowed to a record, or `undefined` when absent/malformed.
221
+ * @returns `undefined` when neither token count is present — matching `translateUsagePayload`'s
222
+ * original "omit the whole `usage` field rather than emit an empty object" behavior.
223
+ * @complexity O(1).
224
+ */
225
+ export declare function extractUsageTokens(rawUsage: Record<string, unknown> | undefined): {
226
+ input_tokens?: number;
227
+ output_tokens?: number;
228
+ } | undefined;
229
+ export declare function translateStatusEvent(rawEvent: Record<string, unknown>): AgentRuntimeEventTranslation;
230
+ export declare function translateToolResultEvent(rawEvent: Record<string, unknown>): AgentRuntimeEventTranslation;
231
+ export declare function translateErrorEvent(rawEvent: Record<string, unknown>): AgentRuntimeEventTranslation;
232
+ export declare function translateTurnEndEvent(rawEvent: Record<string, unknown>): AgentRuntimeEventTranslation;
146
233
  /**
147
234
  * Narrows one parser-emitted `{type, ...}` record into this engine's
148
235
  * `RunAgentPayload` union (or the `error`/`turn-end`/`ignored` routing
@@ -162,7 +249,7 @@ export type AgentRuntimeEventTranslation = {
162
249
  *
163
250
  * @param rawEvent - One event as delivered to a stream parser's `onEvent` callback.
164
251
  * @returns The routing + payload this event maps to.
165
- * @complexity O(1) — one discriminant switch, no iteration.
252
+ * @complexity O(1) — one table lookup, no iteration.
166
253
  * @overallScore 100/100
167
254
  */
168
255
  export declare function translateAgentRuntimeEvent(rawEvent: unknown): AgentRuntimeEventTranslation;
@@ -190,7 +277,16 @@ export interface AgentExecutorRunInput {
190
277
  * A host that wants a run to NOT auto-approve every action passes `'restricted'` here.
191
278
  */
192
279
  readonly permissionMode?: 'bypass' | 'restricted';
193
- /** Host-validated image files forwarded through argv, ACP, or pi-rpc. */
280
+ /**
281
+ * Host-validated image files. How each one actually reaches the model
282
+ * depends on `def.imageDelivery` (see `@jini-ai/agent-runtime`'s `types.ts`):
283
+ * a `'native'` def forwards these through its own protocol (argv, ACP
284
+ * `resource_link` blocks, or pi-rpc's base64 `images` field); a
285
+ * `'prompt-path'` def (currently just `claude`) instead gets them named in
286
+ * the prompt text and its allowed directories widened, via
287
+ * `image-prompt-delivery.ts#applyImagePromptDelivery`, called once near
288
+ * the top of `run()` below.
289
+ */
194
290
  readonly imagePaths?: readonly string[];
195
291
  /** Additional host-validated directories the runtime may read. */
196
292
  readonly extraAllowedDirs?: readonly string[];
@@ -210,9 +306,27 @@ export interface AgentExecutorRunInput {
210
306
  * coding-agent CLI is prompt-influenced and must be treated as potentially adversarial; it must
211
307
  * not inherit secrets the daemon process happens to hold for unrelated reasons. See SEC-001
212
308
  * (`ADS-memory/reports/proposals/PROP-agent-subprocess-env-allowlist-2026-07-21.md`) and locked
213
- * architecture decision C8 (`foundry/docs/jini-port/extraction-plan.md`).
309
+ * architecture decision C8 (`ADS-memory/reports/jini-port/extraction-plan.md`).
214
310
  */
215
311
  readonly env?: NodeJS.ProcessEnv;
312
+ /**
313
+ * Stored session id for this (conversation, agent) pair from a prior run's
314
+ * `RunEndPayload.sessionRef` (see `@jini-ai/protocol`'s doc on that field) — set when the host
315
+ * wants this run to continue that CLI session instead of starting cold. Forwarded verbatim into
316
+ * `RuntimeContext.resumeSessionId`; a `resumesSessionViaCli` def (see
317
+ * `@jini-ai/agent-runtime`'s `types.ts`) reads it to pass its CLI's own resume flag, in which case
318
+ * the host should send only the latest user turn as `prompt`, not the full transcript. `null` and
319
+ * omitted are equivalent: no resume target for this run.
320
+ */
321
+ readonly resumeSessionId?: string | null;
322
+ /**
323
+ * A fresh id the host mints and persists when starting a session it wants resumable on a later
324
+ * turn (i.e. no `resumeSessionId` is available yet). Forwarded verbatim into
325
+ * `RuntimeContext.newSessionId`; a `resumesSessionViaCli` def passes it to its CLI's own
326
+ * "start with this id" flag so a later turn's `resumeSessionId` can continue the same underlying
327
+ * CLI session. Ignored by defs that don't declare `resumesSessionViaCli`.
328
+ */
329
+ readonly newSessionId?: string;
216
330
  }
217
331
  export interface AgentExecutor {
218
332
  /**
@@ -226,12 +340,30 @@ export interface AgentExecutor {
226
340
  */
227
341
  run(input: AgentExecutorRunInput): Promise<void>;
228
342
  }
229
- /** Which caller invoked {@link terminateChildTreeBestEffort} — carried through to `onCleanupFailure` (SEC-007) for diagnosis. */
230
- export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure';
343
+ interface TerminateChildTreeDeps {
344
+ readonly listProcessSnapshots: () => Promise<ProcessSnapshot[]>;
345
+ readonly collectProcessTreePids: typeof collectProcessTreePids;
346
+ readonly stopProcesses: (pids: Array<number | null | undefined>) => Promise<StopProcessesResult>;
347
+ }
348
+ /**
349
+ * Which step reported a contained failure through `onCleanupFailure` (SEC-007).
350
+ *
351
+ * The first three come from {@link terminateChildTreeBestEffort} (process-tree teardown). The last
352
+ * two are the two fallible steps that sit between a child's `'close'` and `finish()` — staged-file
353
+ * removal and the host's own `classifyFailure` — neither of which may prevent the terminal
354
+ * transition, and neither of which may fail silently either. See each close handler.
355
+ */
356
+ export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure' | 'staged-file-cleanup' | 'failure-classification';
231
357
  export interface AgentCleanupFailureContext {
232
358
  readonly runId: string;
233
359
  readonly phase: AgentCleanupFailurePhase;
234
- readonly pid: number;
360
+ /**
361
+ * The child's pid. `undefined` only for the post-close phases on a child that never had one
362
+ * assigned (a spawn that produced no process): those phases are about this run's own bookkeeping
363
+ * rather than about signalling a process, so an absent pid is reportable rather than a
364
+ * contradiction.
365
+ */
366
+ readonly pid: number | undefined;
235
367
  readonly error: unknown;
236
368
  }
237
369
  /**
@@ -270,15 +402,14 @@ export interface ContinuationOptions {
270
402
  * already resolves `'mcp-callback'` for every def with `externalMcpInjection !== undefined`, but
271
403
  * nothing in this file ever *acted* on that resolution — `execute_delegated_tool`
272
404
  * (`@jini-ai/mcp`'s `../server/tools/delegated-tool.ts`) only does anything useful once the spawned
273
- * CLI's own client actually launches `jini-mcp` as its MCP server subprocess, and the only
274
- * `externalMcpInjection` strategy that mechanism is wired for here is `'claude-mcp-json'`
275
- * (`claude`/`codebuddy` see `@jini-ai/agent-runtime`'s `types.ts` doc on the other three
276
- * strategies: `'acp-merge'` delivers `mcpServers` through the ACP `session/new` params
277
- * `wireAcpLifecycle`/`attachAcpSession` already carry, and `'opencode-env-content'`/
278
- * `'mimo-env-content'` deliver through spawn-env content, neither of which needs or wants a
279
- * written file a future task wiring those two would extend `wireAcpLifecycle`'s existing
280
- * `envFormat`/`mcpServers` passthrough or `applyAgentLaunchEnv`'s env composition respectively,
281
- * not this function).
405
+ * CLI's own client actually launches `jini-mcp` as its MCP server subprocess.
406
+ *
407
+ * **All six declared strategies are wired.** These options describe *one* bridge server
408
+ * (`command`/`args`/`daemonUrl`/`credential`); which transport carries it to a given child is that
409
+ * def's own `externalMcpInjection` declaration, and each of the six has exactly one
410
+ * implementation here see {@link buildMcpBridgeDelivery}, which is the single dispatch point.
411
+ * The interface name predates the other five mechanisms and is kept for API compatibility with
412
+ * `@jini-ai/server`'s `agentExecutor` passthrough; it is no longer `.mcp.json`-specific.
282
413
  *
283
414
  * **Host-resolved, not this package's to know.** `command`/`daemonUrl` have no default the way
284
415
  * `journal`/`continuation`/`classifyFailure` don't either — there is no "real" install layout or
@@ -292,10 +423,64 @@ export interface McpJsonInjectionOptions {
292
423
  readonly args?: readonly string[];
293
424
  /** The daemon's own loopback base URL the spawned `jini-mcp` process calls back into via `JINI_DAEMON_URL` (see `packages/mcp/src/bin/serve.ts`'s `DAEMON_URL_ENV_VAR`). */
294
425
  readonly daemonUrl: string;
295
- /** Reads an existing `.mcp.json` at the given absolute path so this driver merges rather than clobbers a project's own file. Rejecting (ENOENT or otherwise) is treated as "no existing file" — see `writeMcpJsonForRun`. @default the real `fs.promises.readFile` (utf8) */
426
+ /**
427
+ * Mints the bearer credential this run's `jini-mcp` child presents on its callbacks, delivered to
428
+ * the child through `JINI_DAEMON_TOKEN`. Omit it and the child is spawned exactly as before, with
429
+ * no token env var at all — so this is additive for every existing host.
430
+ *
431
+ * **A resolver, not a string, and deliberately so.** A host's `McpJsonInjectionOptions` is built
432
+ * once when it composes its executor — before any run exists. A plain string field could therefore
433
+ * only ever carry one boot-wide secret shared by every run, which defeats the point: the reason to
434
+ * hand the child a credential at all is that it can be scoped to the one run it was spawned for and
435
+ * stop working when that run ends. Taking `runId` here is what makes a per-run credential
436
+ * expressible.
437
+ *
438
+ * May be async so a host can mint through a keystore or signing service. Resolution happens in
439
+ * `writeMcpJsonForRun`, which is already async and already effectful; `buildMcpJsonServerEntry`
440
+ * stays pure and synchronous and receives the resolved value.
441
+ *
442
+ * Never hand this the host's own inbound API token. The child is the least-trusted participant in
443
+ * the run — it is reachable by whatever the spawned CLI does — so its credential should authorize
444
+ * its own callback route and nothing else.
445
+ *
446
+ * @throws Anything the host's own minting throws. `run()` turns a rejection into a pre-spawn
447
+ * `AGENT_SPAWN_FAILED` failure rather than spawning a child that cannot authenticate.
448
+ */
449
+ readonly credential?: (runId: string) => string | Promise<string>;
450
+ /** Reads the project's own `cwd/.mcp.json` so this driver merges its servers in rather than dropping them. Rejecting (ENOENT or otherwise) is treated as "no existing file" — see `writeMcpJsonForRun`. This file is only ever *read*. @default the real `fs.promises.readFile` (utf8) */
296
451
  readonly readFile?: (path: string) => Promise<string>;
297
- /** Writes the merged `.mcp.json` content back out. @default the real `fs.promises.writeFile` (utf8) */
452
+ /** Writes the merged content to this run's own config path (see {@link mcpJsonPathForRun}), never to the project's `.mcp.json`. @default the real `fs.promises.writeFile` (utf8) */
298
453
  readonly writeFile?: (path: string, content: string) => Promise<void>;
454
+ /**
455
+ * Removes this run's config file once the run is over — it holds a live per-run bearer token, so
456
+ * leaving it behind is the same class of confidentiality gap as a leaked prompt file (see
457
+ * `WireChildLifecycleContext.cleanupStagedFiles`). Called on the close handler and on every
458
+ * pre-spawn/spawn-failure path, and a rejection is reported rather than allowed to strand the run.
459
+ * @default `fs.promises.rm(path, { force: true })` — already-gone is success, not an error.
460
+ */
461
+ readonly removeFile?: (path: string) => Promise<void>;
462
+ /**
463
+ * `'codex-toml'` only. Creates a fresh, randomly-named directory `prepareCodexHomeForRun` stages
464
+ * as a run's scratch `CODEX_HOME`. **Must be non-deterministic (a real `mkdtemp`, not a
465
+ * caller-computed path)** — unlike `mcpJsonPathForRun`'s deterministic path inside the run's own
466
+ * `cwd`, this directory holds a copy of the operator's real Codex login credential, and
467
+ * `os.tmpdir()` is a shared location on a multi-user host: a guessable name there is a real
468
+ * pre-plant/symlink target for another local user. `fs.mkdtemp`'s random suffix plus its `0700`
469
+ * directory mode is the actual confidentiality control, matching the same reasoning
470
+ * `@jini-ai/agent-runtime`'s `log-file.ts`/`prompt-file.ts` already apply to their own staged temp
471
+ * dirs.
472
+ * @param prefix - A caller-composed, run-id-derived prefix (already sanitized) for the mkdtemp
473
+ * template; the real suffix mkdtemp appends is what makes the path unpredictable.
474
+ * @default `fs.mkdtemp(path.join(os.tmpdir(), prefix))`
475
+ */
476
+ readonly mkdtemp?: (prefix: string) => Promise<string>;
477
+ /**
478
+ * `'codex-toml'` only. Recursively removes the scratch `CODEX_HOME` directory `mkdtemp` above
479
+ * created — the directory-level analogue of `removeFile`, needed because this mechanism stages a
480
+ * whole directory (`config.toml` plus a copied `auth.json`), not one file.
481
+ * @default `fs.rm(path, { recursive: true, force: true })` — already-gone is success, not an error.
482
+ */
483
+ readonly removeDir?: (path: string) => Promise<void>;
299
484
  }
300
485
  /** One `.mcp.json` `mcpServers` entry — the shape Claude Code's own config schema expects. */
301
486
  interface McpJsonServerEntry {
@@ -304,15 +489,24 @@ interface McpJsonServerEntry {
304
489
  readonly env: {
305
490
  readonly JINI_RUN_ID: string;
306
491
  readonly JINI_DAEMON_URL: string;
492
+ /** Present only when the host supplied a `credential` resolver — see {@link McpJsonInjectionOptions.credential}. */
493
+ readonly JINI_DAEMON_TOKEN?: string;
307
494
  };
308
495
  }
309
496
  /**
310
- * Builds this run's `mcpServers.jini` entry — pure, so every field mapping is directly
311
- * assertable without touching the filesystem.
497
+ * Builds this run's `mcpServers.jini` entry — pure and synchronous, so every field mapping is
498
+ * directly assertable without touching the filesystem. The credential arrives already resolved:
499
+ * `McpJsonInjectionOptions.credential` is a possibly-async per-run resolver, and awaiting it is
500
+ * `writeMcpJsonForRun`'s job, which keeps the effect out of this function.
501
+ *
502
+ * @param runId - The run this entry scopes its child to.
503
+ * @param options - `command`/`args`/`daemonUrl` from the host's injection options.
504
+ * @param credential - The already-resolved bearer token, or `undefined` to omit `JINI_DAEMON_TOKEN`
505
+ * entirely. Omitting produces byte-identical output to before this parameter existed.
312
506
  * @complexity O(1).
313
507
  * @overallScore 100/100
314
508
  */
315
- export declare function buildMcpJsonServerEntry(runId: string, options: Pick<McpJsonInjectionOptions, 'command' | 'args' | 'daemonUrl'>): McpJsonServerEntry;
509
+ export declare function buildMcpJsonServerEntry(runId: string, options: Pick<McpJsonInjectionOptions, 'command' | 'args' | 'daemonUrl'>, credential?: string): McpJsonServerEntry;
316
510
  /**
317
511
  * Merges {@link JINI_MCP_SERVER_KEY} into an existing `.mcp.json`'s `mcpServers` map, preserving
318
512
  * every other key and every other registered server untouched. A missing (`existingRaw ===
@@ -325,6 +519,226 @@ export declare function buildMcpJsonServerEntry(runId: string, options: Pick<Mcp
325
519
  * @overallScore 100/100
326
520
  */
327
521
  export declare function mergeMcpJsonContent(existingRaw: string | undefined, serverEntry: McpJsonServerEntry): string;
522
+ /**
523
+ * Mechanism 2 of 5 — `'acp-merge'`. Re-shapes the same bridge entry into the `mcpServers` element
524
+ * an ACP `session/new` call carries, for the 9 ACP-native defs declaring this strategy (amr, devin,
525
+ * hermes, kilo, kimi, kiro, reasonix, trae-cli, vibe). Pure.
526
+ *
527
+ * `env` is emitted as a plain object on purpose: `@jini-ai/agent-runtime`'s
528
+ * `buildAcpSessionNewParams` already normalises a plain-object `env` into either the
529
+ * `[{name, value}]` array form or the `{"KEY": "val"}` map form according to each def's own
530
+ * `acpMcpEnvFormat`, so the per-vendor wire-shape difference stays in the one place that already
531
+ * owns it rather than being re-decided here.
532
+ *
533
+ * **The credential travels in `env`, never in `args`.** An ACP agent spawns this server itself and
534
+ * applies `env` to that child's environment; a token in `args` would land in the child's process
535
+ * arguments, readable by any other local user via `ps`. Same rule as the `.mcp.json` path.
536
+ *
537
+ * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
538
+ * @returns A single-element list — this driver contributes exactly its own bridge server and never
539
+ * removes or rewrites servers a def or host added by other means.
540
+ * @complexity O(1).
541
+ * @overallScore 100/100
542
+ */
543
+ export declare function buildAcpMcpBridgeServers(entry: McpJsonServerEntry): AcpMcpServerInput[];
544
+ /**
545
+ * Serialises the bridge entry into the OpenCode-schema config JSON that `OPENCODE_CONFIG_CONTENT`
546
+ * / `MIMOCODE_CONFIG_CONTENT` carries, merging into whatever the host already put in that variable
547
+ * rather than replacing it — the same "merge, never clobber" discipline
548
+ * {@link mergeMcpJsonContent} applies to `.mcp.json`, and for the same reason: a host may already
549
+ * be handing the CLI the *user's* configured MCP servers through this exact variable, and
550
+ * overwriting it would silently delete them.
551
+ *
552
+ * A missing, empty, or unparseable-as-a-JSON-object existing value degrades to "start from an empty
553
+ * document". Overwriting an unparseable value is deliberate and matches `mergeMcpJsonContent`: this
554
+ * driver did not create it, cannot safely repair it, and must not block the run on it.
555
+ *
556
+ * Emitted per server: `{type: 'local', command: [<command>, ...<args>], environment: {...},
557
+ * enabled: true}` — the shape `@jini-ai/mcp`'s own `buildOpenCodeMcpConfigContent` emits for a
558
+ * stdio server, so both producers stay schema-compatible.
559
+ *
560
+ * **The credential lands in `environment`, i.e. the MCP child's env — never in `command`.** OpenCode
561
+ * spawns the bridge from `command`, so a token placed there would be visible in `ps` output to
562
+ * every other local user. This is the same constraint that keeps `JINI_DAEMON_TOKEN` out of argv on
563
+ * the `.mcp.json` and ACP paths.
564
+ *
565
+ * @param existingRaw - Whatever the spawn env already held for this variable, or `undefined`.
566
+ * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
567
+ * @returns The full JSON string to set as the env var's value.
568
+ * @complexity O(1) plus `JSON.parse`/`JSON.stringify` over a small config document.
569
+ * @overallScore 100/100
570
+ */
571
+ export declare function mergeEnvContentMcpConfig(existingRaw: string | undefined, entry: McpJsonServerEntry): string;
572
+ /**
573
+ * Merges a staged system-prompt overlay file's path into the `instructions` array of the same
574
+ * OpenCode-schema config document {@link mergeEnvContentMcpConfig} writes `mcp` into — for a
575
+ * `systemPromptDelivery: { strategy: 'config-instructions-file' }` def (`opencode` today).
576
+ *
577
+ * Confirmed live (2026-09-01, opencode-cli 1.17.10), not inferred from docs alone:
578
+ * 1. `instructions` is honored — a run configured with it visibly followed the file's directive
579
+ * (a required exact-token prefix), while an identical run without it did not.
580
+ * 2. It appends, never replaces: the same run that followed the custom instruction ALSO still
581
+ * answered correctly using opencode's own baked-in environment-context system prompt (asked
582
+ * for its cwd, with nothing about cwd anywhere in the custom instructions file) — proof
583
+ * opencode's own defaults survive alongside a custom `instructions` entry, not just proof the
584
+ * file was read at all.
585
+ * 3. Adding this key alongside `mcp` in the same `OPENCODE_CONFIG_CONTENT` document disturbs
586
+ * neither: in one combined run, the MCP bridge still got its connection attempt (logged
587
+ * `key=jini type=local`) AND the custom instruction was still followed — same as running each
588
+ * key alone.
589
+ * 4. `instructions` is re-read fresh from the env on every spawn, including a `-s <id>`-resumed
590
+ * turn (proved by swapping in a second instructions file between two turns of one resumed
591
+ * session and seeing the second turn immediately reflect it while still recalling
592
+ * conversation memory from turn one) — so this mechanism is safe to redeliver every turn like
593
+ * `'append-flag'`/`'env-var'`, exempt from the prompt-prefix fallback's create-only gating
594
+ * (see {@link resolveSystemPromptOverlayDelivery}'s doc): nothing here is ever baked into
595
+ * opencode's own persisted session state the way re-injecting fallback prompt text would be.
596
+ *
597
+ * @param existingRaw - Whatever the spawn env already held for this variable (already possibly
598
+ * carrying `mcp`, if `mergeEnvContentMcpConfig` ran first on the same value — order between the two
599
+ * doesn't matter, each only touches its own top-level key), or `undefined`.
600
+ * @param instructionsFilePath - The staged overlay file's absolute path (see
601
+ * {@link prepareSystemPromptOverlayFileIfNeeded}).
602
+ * @returns The full JSON string to set as the env var's value. Appends to, never clobbers, any
603
+ * `instructions` entries already present — the same "merge, never clobber" discipline
604
+ * {@link mergeEnvContentMcpConfig} applies to `mcp`, in case a host is already using this same
605
+ * config-content variable to carry the operator's own instruction files.
606
+ * @complexity O(1) plus `JSON.parse`/`JSON.stringify` over a small config document.
607
+ * @overallScore 100/100
608
+ */
609
+ export declare function mergeEnvContentInstructions(existingRaw: string | undefined, instructionsFilePath: string): string;
610
+ /**
611
+ * Mechanism 5 of 5 — `'codex-toml'`'s serialization step. Builds the `[mcp_servers.jini]` TOML
612
+ * table (plus, when the entry carries any env vars, a separate `[mcp_servers.jini.env]` table)
613
+ * Codex's own config schema expects.
614
+ *
615
+ * Confirmed against a real installed Codex CLI (0.151.0), not assumed from docs: round-tripping
616
+ * `codex mcp add <name> --env K=V -- <cmd> <args>` against a scratch `CODEX_HOME` and reading back
617
+ * `config.toml` produced exactly this shape (`command`/`args` as TOML strings/array in the main
618
+ * table, env vars in a nested `.env` table) — see `source-map.md` for the transcript.
619
+ * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
620
+ * @returns A TOML fragment with no leading/trailing blank-line padding — {@link buildCodexHomeConfigToml} owns spacing when combining it with existing content.
621
+ * @complexity O(n) in the number of argv/env entries.
622
+ * @overallScore 100/100
623
+ */
624
+ export declare function buildCodexMcpServerToml(entry: McpJsonServerEntry): string;
625
+ /**
626
+ * Builds the full `config.toml` a run's scratch `CODEX_HOME` gets: the real Codex home's own
627
+ * config, verbatim, with this run's `[mcp_servers.jini]` table appended.
628
+ *
629
+ * **Append-only by design, not a parse-and-merge.** `mergeMcpJsonContent`/`mergeEnvContentMcpConfig`
630
+ * above can safely parse-merge-reserialize because their formats have a JS-native parser
631
+ * (`JSON.parse`); this driver has no TOML parser in its dependency graph (see
632
+ * `buildCodexMcpServerToml`'s doc), and every other setting a real Codex install carries — model
633
+ * choice, sandbox policy, the trusted-project list, the operator's own other MCP servers — must
634
+ * survive a spawn byte-for-byte. Appending preserves all of it; the one failure mode this trades
635
+ * away is a PRE-EXISTING `[mcp_servers.jini]` table in the operator's own config, which would
636
+ * produce a duplicate TOML key Codex rejects at startup. Accepted as vanishingly unlikely — `jini`
637
+ * is this integration's own reserved server name (see {@link JINI_MCP_SERVER_KEY}), never suggested
638
+ * to an operator for their own config — rather than solved with a full TOML parser for one
639
+ * collision case.
640
+ * @param existingRaw - The real Codex home's `config.toml` content, or `undefined` when it does not
641
+ * exist (a fresh Codex install — degrades to "start from just this run's block", matching
642
+ * {@link mergeMcpJsonContent}'s own "missing file" handling).
643
+ * @param entry - The shared bridge entry.
644
+ * @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
645
+ * @complexity O(n) in the existing config's length.
646
+ * @overallScore 100/100
647
+ */
648
+ export declare function buildCodexHomeConfigToml(existingRaw: string | undefined, entry: McpJsonServerEntry): string;
649
+ /**
650
+ * Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
651
+ * never where it writes. Resolved against the daemon HOST process's own environment (`hostEnv`,
652
+ * `process.env` at the real call site), not a run's sandboxed spawn env: `CODEX_HOME` is not in
653
+ * `BASELINE_AGENT_ENV_KEYS`, so a spawned child never inherits it anyway, and the whole point here
654
+ * is finding wherever the *operator's actual* Codex install lives, which is a host-machine fact.
655
+ * @param hostEnv - The daemon process's own environment.
656
+ * @returns `hostEnv.CODEX_HOME` when set to a non-blank value (matching Codex's own resolution
657
+ * order), else the CLI's documented default, `~/.codex`.
658
+ * @complexity O(1).
659
+ * @overallScore 100/100
660
+ */
661
+ export declare function resolveSourceCodexHomeDir(hostEnv: NodeJS.ProcessEnv): string;
662
+ /**
663
+ * What one run's MCP bridge turns into, discriminated by the delivery mechanism its def declared.
664
+ * Exactly one variant is produced per run, and each variant carries only what its own consumer
665
+ * needs — so a consumer cannot accidentally read another mechanism's payload.
666
+ */
667
+ export type McpBridgeDelivery =
668
+ /** `'claude-mcp-json'` (claude, codebuddy): a `.mcp.json` staged into the run cwd, whose path the def's `buildArgs` passes as `--mcp-config`. */
669
+ {
670
+ readonly kind: 'claude-mcp-json';
671
+ readonly mcpJsonPath: string;
672
+ readonly serverEntry: McpJsonServerEntry;
673
+ }
674
+ /** `'acp-merge'` (the 9 ACP-native defs): `mcpServers` entries for the ACP `session/new` params. */
675
+ | {
676
+ readonly kind: 'acp-merge';
677
+ readonly mcpServers: readonly AcpMcpServerInput[];
678
+ }
679
+ /** `'opencode-env-content'` / `'mimo-env-content'` (opencode, mimo): one spawn-env variable carrying the serialised config. */
680
+ | {
681
+ readonly kind: 'env-content';
682
+ readonly envVarName: string;
683
+ readonly serverEntry: McpJsonServerEntry;
684
+ }
685
+ /**
686
+ * `'codex-toml'` (codex): no path yet — unlike `'claude-mcp-json'`'s `mcpJsonPath`, the scratch
687
+ * `CODEX_HOME` directory is created with `fs.mkdtemp` (a real, non-deterministic filesystem
688
+ * effect — see `McpJsonInjectionOptions.mkdtemp`'s own doc for why), so it cannot be computed by
689
+ * this delivery's pure, synchronous dispatch. `prepareCodexHomeIfNeeded` stages it separately and
690
+ * reports the resulting path back into `childEnv.CODEX_HOME` directly, never through this type.
691
+ */
692
+ | {
693
+ readonly kind: 'codex-toml';
694
+ readonly serverEntry: McpJsonServerEntry;
695
+ }
696
+ /**
697
+ * `'env-passthrough'` (antigravity): the bridge entry's `env` triple (`JINI_RUN_ID`/
698
+ * `JINI_DAEMON_URL`/`JINI_DAEMON_TOKEN`) is set directly on the spawned CLI's own OS
699
+ * environment — no config document, no file, no CLI-specific schema. Correct only because this
700
+ * strategy's CLI already inherits its own env down to the stdio MCP child it launches for a
701
+ * server registered once, globally, out of band (see `types.ts`'s own doc on this strategy).
702
+ */
703
+ | {
704
+ readonly kind: 'env-passthrough';
705
+ readonly serverEntry: McpJsonServerEntry;
706
+ };
707
+ /**
708
+ * **The single dispatch point from an `externalMcpInjection` strategy to its delivery mechanism.**
709
+ * Pure and synchronous — the one effectful input (the per-run bearer credential) arrives already
710
+ * resolved, so every strategy's mapping is directly assertable without touching the filesystem,
711
+ * the environment, or a keystore.
712
+ *
713
+ * Keyed off the declared *strategy*, never off `def.id`: a def gets a working bridge by declaring a
714
+ * mechanism, not by being named in this file. That is what makes the 9 `'acp-merge'` defs work
715
+ * without any of their own files being touched.
716
+ *
717
+ * @param input.cwd - The run's working directory; only `'claude-mcp-json'` uses it, to place this
718
+ * run's own config file (see {@link mcpJsonPathForRun}) — never `cwd/.mcp.json` itself.
719
+ * @param input.runId - Scopes the bridge child to this run.
720
+ * @param input.strategy - The def's declared `externalMcpInjection`, or `undefined` for a def with no native MCP transport.
721
+ * @param input.options - The host's bridge options, or `undefined` when the host never configured injection.
722
+ * @param input.credential - Already-resolved bearer token, or `undefined` to omit `JINI_DAEMON_TOKEN` entirely.
723
+ * @returns `null` when this run delivers nothing — an unconfigured host, or a def declaring no
724
+ * strategy — which is byte-identical to this feature not existing.
725
+ * @complexity O(1).
726
+ * @overallScore 100/100
727
+ */
728
+ export declare function buildMcpBridgeDelivery(input: {
729
+ readonly cwd: string;
730
+ readonly runId: string;
731
+ readonly strategy: RuntimeAgentDef['externalMcpInjection'];
732
+ readonly options: McpJsonInjectionOptions | undefined;
733
+ readonly credential: string | undefined;
734
+ }): McpBridgeDelivery | null;
735
+ /** A staged, run-scoped Codex `CODEX_HOME` — the directory-holding analogue of {@link PreparedPromptFile}/{@link PreparedAgentLogFile} from `@jini-ai/agent-runtime`. */
736
+ export type PreparedCodexHome = {
737
+ /** Absolute path to hand to the spawned child as its `CODEX_HOME` env var. */
738
+ readonly path: string;
739
+ /** Recursively removes the staged directory — the live `auth.json` copy it may hold makes this a confidentiality cleanup, not just tidiness. Safe to call more than once. */
740
+ readonly cleanup: () => Promise<void>;
741
+ };
328
742
  /**
329
743
  * Gap 4 of the run/chat orchestration Final Recommendation: what
330
744
  * `classifyFailure` (see `CreateAgentExecutorOptions.classifyFailure`) is
@@ -368,6 +782,41 @@ export interface FailureClassificationContext {
368
782
  * nothing a classifier could meaningfully examine.
369
783
  */
370
784
  export type ClassifyFailure = (context: FailureClassificationContext) => boolean | Promise<boolean>;
785
+ /**
786
+ * Default ceiling on the `'until-close'` stdout accumulator (see `RuntimeStdoutPolicy` in
787
+ * `@jini-ai/agent-runtime`), in bytes of received UTF-8.
788
+ *
789
+ * A buffered def holds its child's entire stdout in one in-memory string until the process closes,
790
+ * which is exactly what makes the accumulator a denial-of-service surface: the child is a
791
+ * prompt-influenced agent CLI this driver already treats as potentially adversarial (SEC-001), and
792
+ * nothing obliges it to ever close or to stop emitting. Without a ceiling one run could exhaust the
793
+ * daemon's heap and take every unrelated run in the process down with it.
794
+ *
795
+ * 8 MiB is chosen to sit far above any real buffered-agent transcript (antigravity's print-mode
796
+ * output — the only `'until-close'` def — is a few KiB of auth prompt and result text) while staying
797
+ * small enough that a hostile child cannot meaningfully pressure the heap. A host that genuinely
798
+ * needs more passes `CreateAgentExecutorOptions.bufferedStdoutMaxBytes`.
799
+ */
800
+ export declare const DEFAULT_BUFFERED_STDOUT_MAX_BYTES: number;
801
+ /** The side-effect signals {@link applyAgentTranslationSideEffects} reports through, one callback per signal so a caller only wires the ones it actually tracks. */
802
+ interface AgentTranslationSideEffectSink {
803
+ readonly onSessionId: (sessionId: string) => void;
804
+ readonly onToolCall: () => void;
805
+ readonly onUserVisibleOutput: () => void;
806
+ }
807
+ /**
808
+ * Applies one already-translated `'agent'`-kind event's side-effect signals — a captured session id
809
+ * (gap 5), and the `toolCallSeen`/`userVisibleOutputSeen` pair every `wire*Lifecycle` driver tracks
810
+ * for `FailureClassificationContext.sideEffects` — through `sink`. Extracted from `wireAcpLifecycle`'s
811
+ * `send()`, where this exact three-level-deep nesting (session-id check, then tool_use/else-if
812
+ * delta-length check) was that function's largest single cognitive-complexity contributor. Pure
813
+ * except for calling the injected `sink` callbacks.
814
+ * @param payload - The translated event's `RunAgentPayload`.
815
+ * @param sessionId - The translation's optional captured session id, or `undefined`.
816
+ * @param sink - The driver-specific effects to apply.
817
+ * @complexity O(1).
818
+ */
819
+ export declare function applyAgentTranslationSideEffects(payload: RunAgentPayload, sessionId: string | undefined, sink: AgentTranslationSideEffectSink): void;
371
820
  export interface CreateAgentExecutorOptions {
372
821
  readonly lifecycle: RunLifecycle;
373
822
  /** @default the real `@jini-ai/agent-runtime` registry lookup */
@@ -399,6 +848,14 @@ export interface CreateAgentExecutorOptions {
399
848
  * @default the real `@jini-ai/agent-runtime` prompt-file stager
400
849
  */
401
850
  readonly preparePromptFileForAgent?: typeof preparePromptFileForAgent;
851
+ /**
852
+ * Stages a `needsAgentLogFile` def's (antigravity) diagnostic-log path
853
+ * before `buildArgs` runs. Same real-filesystem/injectable-for-tests deal
854
+ * as `preparePromptFileForAgent` above, and a no-op for every def that
855
+ * did not opt in.
856
+ * @default the real `@jini-ai/agent-runtime` log-file stager
857
+ */
858
+ readonly prepareAgentLogFile?: typeof prepareAgentLogFile;
402
859
  /** @default the real `@jini-ai/platform` process-snapshot enumerator */
403
860
  readonly listProcessSnapshots?: typeof listProcessSnapshots;
404
861
  /** @default the real `@jini-ai/platform` descendant-PID collector */
@@ -445,6 +902,23 @@ export interface CreateAgentExecutorOptions {
445
902
  * `command`/`daemonUrl` this package could assume on a caller's behalf.
446
903
  */
447
904
  readonly mcpJsonInjection?: McpJsonInjectionOptions;
905
+ /**
906
+ * Ceiling on how many bytes of a `'until-close'` def's stdout this driver will hold in memory
907
+ * before it stops accumulating and reports the shortfall — see
908
+ * {@link DEFAULT_BUFFERED_STDOUT_MAX_BYTES} for the threat this closes and why 8 MiB.
909
+ * @default {@link DEFAULT_BUFFERED_STDOUT_MAX_BYTES}
910
+ */
911
+ readonly bufferedStdoutMaxBytes?: number;
912
+ /**
913
+ * Host-owned system-prompt overlay — see `prompt-augmenter.ts`'s own doc for why this seam
914
+ * exists (product-specific discovery/behavior instructions that don't belong in the engine).
915
+ * When present, `systemOverlay()` is called once per `run()` and its result (if non-null) is
916
+ * threaded through to `buildArgs` as `RuntimeBuildOptions.systemPromptOverlay` — a def with no
917
+ * append-system-prompt mechanism ignores it.
918
+ * @default undefined — no overlay is computed and no def sees `systemPromptOverlay`,
919
+ * byte-identical to pre-this-option behavior.
920
+ */
921
+ readonly promptAugmenter?: PromptAugmenter;
448
922
  }
449
923
  /**
450
924
  * Creates the `AgentExecutor` reference implementation: an in-process
@@ -461,6 +935,354 @@ export interface CreateAgentExecutorOptions {
461
935
  * @complexity `run()`'s own setup is O(1); steady-state cost is the chosen stream parser's.
462
936
  * @overallScore 100/100
463
937
  */
938
+ /** `createAgentExecutor`'s internal `failBeforeSpawn` — a nested closure over `lifecycle`, so every extracted `run()` phase below takes it as an explicit injected collaborator rather than reaching for a module-level one. */
939
+ type FailBeforeSpawn = (runId: string, code: AgentExecutorErrorCode, message: string) => Promise<never>;
940
+ interface ResolvedDefAndFormat {
941
+ readonly def: RuntimeAgentDef;
942
+ readonly streamFormat: SupportedStreamFormat;
943
+ }
944
+ /** Phase 1: registry lookup + `assessAgentExecutorCompatibility` guard. */
945
+ export declare function resolveDefAndStreamFormat(input: Pick<AgentExecutorRunInput, 'runId' | 'agentId'>, deps: {
946
+ readonly getAgentDef: typeof getAgentDef;
947
+ readonly failBeforeSpawn: FailBeforeSpawn;
948
+ }): Promise<ResolvedDefAndFormat>;
949
+ /** Phase 2: image-prompt-delivery augmentation + argv-budget guard for argv-bound defs. */
950
+ export declare function resolveImageDeliveryAndArgvBudget(input: {
951
+ readonly runId: string;
952
+ readonly def: RuntimeAgentDef;
953
+ readonly prompt: string;
954
+ readonly imagePaths: readonly string[] | undefined;
955
+ readonly extraAllowedDirs: readonly string[] | undefined;
956
+ }, deps: {
957
+ readonly failBeforeSpawn: FailBeforeSpawn;
958
+ }): Promise<ImagePromptDelivery>;
959
+ /**
960
+ * Phase 3a: the subprocess environment this run's launch resolution and spawn should use — the
961
+ * caller-supplied escape hatch verbatim, or the deny-by-default `BASELINE_AGENT_ENV_KEYS` allowlist.
962
+ * Pure.
963
+ */
964
+ export declare function resolveRunEnv(input: Pick<AgentExecutorRunInput, 'env' | 'credentialEnv'>, hostEnv: NodeJS.ProcessEnv): Record<string, string>;
965
+ /** A resolved launch whose `launchPath` is confirmed non-null — see {@link resolveLaunch}. */
966
+ type ConfirmedAgentLaunchResolution = AgentLaunchResolution & {
967
+ readonly launchPath: string;
968
+ };
969
+ /** Phase 3b: launch-path resolution + binary-not-resolved guard. */
970
+ export declare function resolveLaunch(input: {
971
+ readonly runId: string;
972
+ readonly def: RuntimeAgentDef;
973
+ readonly resolvedEnv: Record<string, string>;
974
+ }, deps: {
975
+ readonly resolveAgentLaunch: typeof resolveAgentLaunch;
976
+ readonly failBeforeSpawn: FailBeforeSpawn;
977
+ }): Promise<ConfirmedAgentLaunchResolution>;
978
+ /** Phase 4a: stage a `promptViaFile` def's prompt to a temp file (a no-op for every other def). */
979
+ export declare function stagePromptFile(input: {
980
+ readonly runId: string;
981
+ readonly def: RuntimeAgentDef;
982
+ readonly prompt: string;
983
+ }, deps: {
984
+ readonly preparePromptFileForAgent: typeof preparePromptFileForAgent;
985
+ readonly failBeforeSpawn: FailBeforeSpawn;
986
+ }): Promise<PreparedPromptFile | null>;
987
+ /** Phase 4b: stage a `needsAgentLogFile` def's diagnostic-log path (a no-op for every other def). */
988
+ export declare function stageLogFile(input: {
989
+ readonly runId: string;
990
+ readonly def: RuntimeAgentDef;
991
+ readonly preparedPromptFile: PreparedPromptFile | null;
992
+ }, deps: {
993
+ readonly prepareAgentLogFile: typeof prepareAgentLogFile;
994
+ readonly failBeforeSpawn: FailBeforeSpawn;
995
+ }): Promise<PreparedAgentLogFile | null>;
996
+ /** Phase 5: resolves this run's MCP bridge delivery (credential resolution + {@link buildMcpBridgeDelivery}). */
997
+ export declare function resolveMcpBridgeForRun(input: {
998
+ readonly runId: string;
999
+ readonly cwd: string;
1000
+ readonly def: RuntimeAgentDef;
1001
+ }, deps: {
1002
+ readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
1003
+ readonly cleanupStagedFiles: () => Promise<void>;
1004
+ readonly failBeforeSpawn: FailBeforeSpawn;
1005
+ }): Promise<McpBridgeDelivery | null>;
1006
+ /**
1007
+ * Phase 6a/10c: the subprocess environment every env-riding mechanism uses — mechanism 3+4
1008
+ * (`'opencode-env-content'`/`'mimo-env-content'`, merged into whatever the host already set there,
1009
+ * never a CLI argument: the config embeds `JINI_DAEMON_TOKEN`, and process arguments are readable
1010
+ * by any other local user through `ps`), mechanism 5 (`'codex-toml'`, `CODEX_HOME` relocation),
1011
+ * mechanism 6 (`'env-passthrough'`, the bridge entry's flat env vars set directly with no carrier
1012
+ * document — see {@link McpBridgeDelivery}'s own doc), a
1013
+ * `systemPromptDelivery: 'env-var'` def's overlay (`reasonix`'s `REASONIX_ACP_SYSTEM_APPEND` today
1014
+ * — see `resolveSystemPromptOverlayDelivery`'s own doc), and a `'config-instructions-file'` def's
1015
+ * staged overlay file (`opencode` today — see {@link mergeEnvContentInstructions}'s own doc). Pure
1016
+ * — `codexHomeDir` and `stagedInstructionsFile` arrive already staged by
1017
+ * {@link prepareCodexHomeIfNeeded} and {@link prepareSystemPromptOverlayFileIfNeeded} respectively,
1018
+ * the only parts of this mechanism that are NOT pure (real `mkdtemp`/`writeFile` calls).
1019
+ * @param spawnEnv - The env every other spawn-time step (launch-path resolution, `applyAgentLaunchEnv`) already computed.
1020
+ * @param mcpBridge - This run's resolved bridge delivery, or `null` for an unconfigured host / no-strategy def.
1021
+ * @param codexHomeDir - The staged scratch `CODEX_HOME` path for a `'codex-toml'` def, or `undefined` for every other run (including a `'codex-toml'` def when `mcpJsonInjection` was never configured — see `prepareCodexHomeIfNeeded`'s own gate).
1022
+ * @param systemPromptEnvOverrides - `resolveSystemPromptOverlayDelivery`'s `envOverrides` — `{}` (default) for every def but an `'env-var'`-strategy one with an overlay present, in which case it carries that one var. Applied after `codexHomeDir`, so it can never be shadowed by it — the two never share a key (`CODEX_HOME` vs. e.g. `REASONIX_ACP_SYSTEM_APPEND`), so the ordering is a documentation choice, not a correctness one.
1023
+ * @param stagedInstructionsFile - `varName` (from the def's own `systemPromptDelivery` declaration) and the staged overlay file's `path`, or `undefined` for every def but a `'config-instructions-file'` one with an overlay present. Merged into `varName`'s value AFTER the `mcp` merge above (reading `envContentApplied`, not the original `spawnEnv`, for that same key) so both a `mcp` entry and an `instructions` entry from the two mechanisms survive together in one document — confirmed live this coexistence is safe (see {@link mergeEnvContentInstructions}'s doc).
1024
+ * @complexity O(1) plus `mergeEnvContentMcpConfig`'s and `mergeEnvContentInstructions`'s own `JSON.parse`/`JSON.stringify` cost.
1025
+ * @overallScore 100/100
1026
+ */
1027
+ export declare function computeChildEnv(spawnEnv: NodeJS.ProcessEnv, mcpBridge: McpBridgeDelivery | null, codexHomeDir?: string, systemPromptEnvOverrides?: Readonly<Record<string, string>>, stagedInstructionsFile?: {
1028
+ readonly varName: string;
1029
+ readonly path: string;
1030
+ }): NodeJS.ProcessEnv;
1031
+ /**
1032
+ * Phase 6b: the `RuntimeContext` `buildArgs` receives — `undefined` unless a file, bridge path, or
1033
+ * session id was staged. Pure.
1034
+ *
1035
+ * `resumeSessionId`/`newSessionId` round-trip a prior run's `RunEndPayload.sessionRef` (see
1036
+ * `@jini-ai/protocol`'s doc on that field) back into this run's `RuntimeContext`, letting a
1037
+ * `resumesSessionViaCli` def (e.g. claude) continue its own CLI session across turns instead of
1038
+ * spawning cold every time. Either one alone must still produce a context — a run supplying ONLY a
1039
+ * session id, with no prompt/log file staged and no claude-mcp-json bridge, is exactly the common
1040
+ * case for a resumed turn.
1041
+ */
1042
+ export declare function computeRuntimeContext(preparedPromptFile: PreparedPromptFile | null, preparedLogFile: PreparedAgentLogFile | null, mcpBridge: McpBridgeDelivery | null, resumeSessionId?: string | null, newSessionId?: string): RuntimeContext | undefined;
1043
+ /** Phase 9a: the def's `buildArgs` 4th argument — `undefined` when the run selects no model/reasoning/permissionMode/overlay at all (byte-identical to omitting the argument). Pure. */
1044
+ export declare function buildAgentBuildArgsOptions(input: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode'>, systemPromptOverlay: string | null | undefined): RuntimeBuildOptions | undefined;
1045
+ /**
1046
+ * **The single dispatch point from a computed system-prompt overlay to its delivery mechanism** —
1047
+ * see `RuntimeAgentDef.systemPromptDelivery`'s own doc for the declared shape. Pure and
1048
+ * synchronous, mirroring {@link buildMcpBridgeDelivery}'s "keyed off the declared strategy, never
1049
+ * off the def's id" contract: a def earns overlay delivery by declaring a strategy, not by being
1050
+ * named in this file. That is what makes every def with no declaration work via the fallback
1051
+ * without any of their own files being touched.
1052
+ *
1053
+ * The fallback (no declared strategy — every def but `claude` today) prefixes the overlay directly
1054
+ * onto the composed prompt text, clearly delimited from the user's own request. It is gated on
1055
+ * session state, not merely on whether an overlay exists: a def that carries its own conversation
1056
+ * memory across spawns (`resumesSessionViaCli` / `resumesSessionViaAcpLoad`) persists whatever its
1057
+ * session-creating turn sends it — see `RuntimeContext.resumeSessionId`'s own doc: its presence on
1058
+ * a run means "continue a prior session", not "start one". Prefixing on every later turn of that
1059
+ * same session would therefore bake the overlay into the CLI's own stored history again and again,
1060
+ * compounding without bound turn over turn. So the fallback prefixes only when there is no resume
1061
+ * target yet (the session's own first turn, or a def with no session memory at all, which never
1062
+ * replays anything back at the CLI and so gets it on every turn).
1063
+ *
1064
+ * `'append-flag'` and `'env-var'` defs are the opposite case: the flag/env var is a fresh,
1065
+ * un-stored per-spawn directive — never part of what a resumed session replays — so it is set on
1066
+ * every turn unconditionally, exactly `claude`'s pre-existing (now-centralized) behavior before
1067
+ * this function existed.
1068
+ *
1069
+ * @param input.defId - Looks up this def's probed capabilities for an `'append-flag'` strategy's
1070
+ * `capabilityKey`. Otherwise unused — the dispatch itself is keyed off `systemPromptDelivery`, per
1071
+ * this function's own doc above, never off the id.
1072
+ * @param input.systemPromptDelivery - The def's declared strategy, or `undefined` for the fallback.
1073
+ * @param input.resumesSessionViaCli - The def's own flag (see `RuntimeAgentDef`'s doc).
1074
+ * @param input.resumesSessionViaAcpLoad - The def's own flag (see `RuntimeAgentDef`'s doc).
1075
+ * @param input.overlay - The computed `PromptAugmenter.systemOverlay()` result. `null`/`undefined`/
1076
+ * empty short-circuits to "no delivery" — byte-identical to no `PromptAugmenter` configured at all.
1077
+ * @param input.prompt - The composed prompt `buildArgs` would otherwise receive verbatim.
1078
+ * @param input.resumeSessionId - This run's `RuntimeContext.resumeSessionId`; presence means an
1079
+ * existing session is being continued, not created.
1080
+ * @returns The (possibly prefixed) prompt to hand `buildArgs`, any extra argv to append to
1081
+ * whatever `buildArgs` itself returns, and any env var overrides to merge into the spawn env
1082
+ * (`{}` for every strategy but `'env-var'`).
1083
+ * @complexity O(n) in the overlay/prompt lengths — string concatenation only, no I/O.
1084
+ * @overallScore 100/100
1085
+ */
1086
+ export declare function resolveSystemPromptOverlayDelivery(input: {
1087
+ readonly defId: string;
1088
+ readonly systemPromptDelivery: RuntimeAgentDef['systemPromptDelivery'];
1089
+ readonly resumesSessionViaCli: boolean | undefined;
1090
+ readonly resumesSessionViaAcpLoad: boolean | undefined;
1091
+ readonly overlay: string | null | undefined;
1092
+ readonly prompt: string;
1093
+ readonly resumeSessionId: string | null | undefined;
1094
+ }): {
1095
+ readonly prompt: string;
1096
+ readonly extraArgs: readonly string[];
1097
+ readonly envOverrides: Readonly<Record<string, string>>;
1098
+ };
1099
+ /** Phase 9b: calls the def's `buildArgs`, releasing staged resources and failing the run on a throw. */
1100
+ export declare function buildRunArgs(input: {
1101
+ readonly runId: string;
1102
+ readonly def: RuntimeAgentDef;
1103
+ readonly imageDelivery: ImagePromptDelivery;
1104
+ readonly imagePaths: readonly string[] | undefined;
1105
+ readonly runInput: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode'>;
1106
+ readonly systemPromptOverlay: string | null | undefined;
1107
+ readonly runtimeContext: RuntimeContext | undefined;
1108
+ }, deps: {
1109
+ readonly releaseStagedResources: () => Promise<void>;
1110
+ readonly failBeforeSpawn: FailBeforeSpawn;
1111
+ }): Promise<{
1112
+ readonly args: string[];
1113
+ readonly envOverrides: Readonly<Record<string, string>>;
1114
+ }>;
1115
+ /** Phase 10: mechanism 1 of 5's one effect — stages this run's own `.mcp.json`, returning the path `cleanupStagedFiles` should later remove (`undefined` for every other mechanism / unconfigured host). */
1116
+ export declare function writeMcpJsonIfNeeded(input: {
1117
+ readonly runId: string;
1118
+ readonly cwd: string;
1119
+ readonly def: RuntimeAgentDef;
1120
+ readonly mcpBridge: McpBridgeDelivery | null;
1121
+ }, deps: {
1122
+ readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
1123
+ readonly releaseStagedResources: () => Promise<void>;
1124
+ readonly failBeforeSpawn: FailBeforeSpawn;
1125
+ }): Promise<string | undefined>;
1126
+ /** {@link prepareSystemPromptOverlayFileIfNeeded}'s result. */
1127
+ export type PreparedSystemPromptOverlayFile = {
1128
+ /** Absolute path to the staged file, ready to merge into a `config-instructions-file` def's `instructions` array. */
1129
+ readonly path: string;
1130
+ /** Recursively removes the staged directory. Safe to call more than once. */
1131
+ readonly cleanup: () => Promise<void>;
1132
+ };
1133
+ /**
1134
+ * Phase 10b1: `systemPromptDelivery: { strategy: 'config-instructions-file' }`'s one effect —
1135
+ * stages the computed overlay to a fresh, run-scoped temp file, so `computeChildEnv` has a real
1136
+ * path to merge into that def's `instructions` config array (see
1137
+ * {@link mergeEnvContentInstructions}'s own doc for the live verification this mechanism rests on).
1138
+ * `null` for every other strategy, an unset `systemPromptDelivery`, or no overlay present at all —
1139
+ * byte-identical to before this mechanism existed, matching {@link writeMcpJsonIfNeeded}'s and
1140
+ * {@link prepareCodexHomeIfNeeded}'s identical no-op-when-inapplicable gate.
1141
+ *
1142
+ * `opencode`'s `instructions` field only accepts a file path or a remote URL — confirmed live
1143
+ * (2026-09-01): a literal instruction string in the array is silently ignored (no error, just never
1144
+ * honored), so an inline-text shortcut is not available and this staging step is load-bearing, not
1145
+ * a defensive extra.
1146
+ * @param input.def - Only used for its `id`, in the failure message, and its `systemPromptDelivery` declaration.
1147
+ * @param input.overlay - The computed `PromptAugmenter.systemOverlay()` result for this run.
1148
+ * @complexity O(1) plus one directory creation and one file write.
1149
+ * @overallScore 100/100
1150
+ */
1151
+ export declare function prepareSystemPromptOverlayFileIfNeeded(input: {
1152
+ readonly runId: string;
1153
+ readonly def: RuntimeAgentDef;
1154
+ readonly overlay: string | null | undefined;
1155
+ }, deps: {
1156
+ readonly releaseStagedResources: () => Promise<void>;
1157
+ readonly failBeforeSpawn: FailBeforeSpawn;
1158
+ }): Promise<PreparedSystemPromptOverlayFile | null>;
1159
+ /**
1160
+ * Phase 10b: mechanism 5 of 5's one effect — stages this run's scratch `CODEX_HOME` directory,
1161
+ * returning the prepared handle `cleanupStagedFiles` should later release (`null` for every other
1162
+ * mechanism, or for an unconfigured host — matching {@link writeMcpJsonIfNeeded}'s identical gate).
1163
+ * @param input.def - Only used for its `id`, in the failure message.
1164
+ * @param input.mcpBridge - This run's resolved bridge delivery — a no-op unless its `kind` is `'codex-toml'`.
1165
+ * @param deps.hostEnv - The daemon's own environment, threaded through to {@link resolveSourceCodexHomeDir} rather than read from a module-level `process.env` so this phase stays testable with an injected env.
1166
+ * @complexity O(1) plus {@link prepareCodexHomeForRun}'s own cost.
1167
+ * @overallScore 100/100
1168
+ */
1169
+ export declare function prepareCodexHomeIfNeeded(input: {
1170
+ readonly runId: string;
1171
+ readonly def: RuntimeAgentDef;
1172
+ readonly mcpBridge: McpBridgeDelivery | null;
1173
+ }, deps: {
1174
+ readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
1175
+ readonly hostEnv: NodeJS.ProcessEnv;
1176
+ readonly releaseStagedResources: () => Promise<void>;
1177
+ readonly failBeforeSpawn: FailBeforeSpawn;
1178
+ }): Promise<PreparedCodexHome | null>;
1179
+ /** Phase 11: post-`buildArgs` guard for argv-bound defs whose resolved binary is a Windows shim/.exe — a no-op off-Windows and for non-argv-bound defs. */
1180
+ export declare function guardWindowsCommandLineBudget(input: {
1181
+ readonly runId: string;
1182
+ readonly def: RuntimeAgentDef;
1183
+ readonly launchPath: string;
1184
+ readonly args: readonly string[];
1185
+ }, deps: {
1186
+ readonly releaseStagedResources: () => Promise<void>;
1187
+ readonly failBeforeSpawn: FailBeforeSpawn;
1188
+ }): Promise<void>;
1189
+ /** {@link spawnAgentChildProcess}'s result — a discriminated union rather than a thrown/rejected outcome so the call can stay synchronous; see that function's doc for why. */
1190
+ export type SpawnAgentChildProcessResult = {
1191
+ readonly kind: 'ok';
1192
+ readonly child: ChildProcess;
1193
+ } | {
1194
+ readonly kind: 'error';
1195
+ readonly error: unknown;
1196
+ };
1197
+ /**
1198
+ * Phase 12: the real `node:child_process.spawn` call.
1199
+ *
1200
+ * **Deliberately synchronous, unlike every other phase in this file.** A spawned child can emit
1201
+ * `'error'` on the very next microtask tick (Node schedules it eagerly on some failure modes, and a
1202
+ * test harness simulating "the child emits 'error' before 'spawn'" does so explicitly via
1203
+ * `queueMicrotask`). `run()` must register its `'error'` listeners (`wireChildLifecycle`'s safety net,
1204
+ * then `waitForSpawnOrError`'s `child.once('error', reject)`) in the *same synchronous turn* as this
1205
+ * spawn call — Node's `EventEmitter` throws synchronously when `'error'` fires with zero listeners
1206
+ * attached. Wrapping this call in an `async function` and `await`ing it (as every other phase here
1207
+ * does) would insert a microtask tick between spawn and listener registration, occasionally losing
1208
+ * that race — confirmed by a real test failure during this refactor (an uncaught `EventEmitter`
1209
+ * `'error'` exception) before this function was changed back to a plain, unawaited call returning a
1210
+ * result object instead of throwing/rejecting.
1211
+ * @returns `{kind:'ok', child}` on success, `{kind:'error', error}` on a synchronous throw from `spawn`
1212
+ * — `run()` itself is responsible for cleanup and `failBeforeSpawn` on the error variant, both of
1213
+ * which are safe to make asynchronous since no child (and hence no listener race) exists yet.
1214
+ * @complexity O(1) plus `spawn`'s own cost.
1215
+ */
1216
+ export declare function spawnAgentChildProcess(input: {
1217
+ readonly cwd: string;
1218
+ readonly childEnv: NodeJS.ProcessEnv;
1219
+ readonly invocation: ReturnType<typeof createCommandInvocation>;
1220
+ }, deps: {
1221
+ readonly spawn: typeof nodeSpawn;
1222
+ }): SpawnAgentChildProcessResult;
1223
+ /** Named predicate replacing an inline `streamFormat === 'acp-json-rpc' || streamFormat === 'pi-rpc'` check — the two formats that own their own prompt/event protocol and skip `wireChildLifecycle`. */
1224
+ export declare function isStdinDrivenFormat(streamFormat: SupportedStreamFormat): streamFormat is ChildDrivenStreamFormat;
1225
+ /** Phase 13: awaits spawn confirmation, routing a failure through the same `failBeforeSpawn` shape every earlier guard uses. */
1226
+ export declare function confirmChildSpawned(input: {
1227
+ readonly runId: string;
1228
+ readonly def: RuntimeAgentDef;
1229
+ readonly child: ChildProcess;
1230
+ }, deps: {
1231
+ readonly releaseStagedResources: () => Promise<void>;
1232
+ readonly failBeforeSpawn: FailBeforeSpawn;
1233
+ }): Promise<void>;
1234
+ /** Phase 14: starts a `runtimeLock` def's handoff watcher once a live process exists to consume the locked side effect — a no-op when the def declared no `waitForHandoff`. Deliberately not awaited; see `RuntimeLockHold.waitForHandoff`'s own doc. */
1235
+ export declare function armHandoffWatcher(runtimeLockHold: RuntimeLockHold | undefined, handoffInput: {
1236
+ readonly logFilePath: string | undefined;
1237
+ readonly model: string | undefined;
1238
+ readonly processExited: AbortSignal;
1239
+ }, release: () => void): void;
1240
+ interface RunAcpDispatchInput {
1241
+ readonly runId: string;
1242
+ readonly agentId: string;
1243
+ readonly child: ChildProcess;
1244
+ readonly prompt: string;
1245
+ readonly cwd: string;
1246
+ readonly model: string | undefined;
1247
+ readonly imagePaths: readonly string[];
1248
+ readonly envFormat: 'array' | 'map' | undefined;
1249
+ readonly mcpBridge: McpBridgeDelivery | null;
1250
+ }
1251
+ interface RunAcpDispatchDeps extends TerminateChildTreeDeps {
1252
+ readonly lifecycle: RunLifecycle;
1253
+ readonly attachAcpSession: typeof attachAcpSession;
1254
+ readonly onPermissionRequest: AcpPermissionHandler | undefined;
1255
+ readonly onCleanupFailure: (context: AgentCleanupFailureContext) => void;
1256
+ readonly cleanupStagedFiles: () => Promise<void>;
1257
+ readonly journal: RunByteJournal | undefined;
1258
+ readonly classifyFailure: ClassifyFailure | undefined;
1259
+ readonly releaseStagedResources: () => Promise<void>;
1260
+ readonly failBeforeSpawn: FailBeforeSpawn;
1261
+ }
1262
+ /** Phase 15 (ACP branch): attaches the ACP session, escalating process-tree teardown and failing the run through `failBeforeSpawn` on an attach-time throw. */
1263
+ export declare function runAcpDispatch(input: RunAcpDispatchInput, deps: RunAcpDispatchDeps): Promise<void>;
1264
+ interface RunPiRpcDispatchInput {
1265
+ readonly runId: string;
1266
+ readonly agentId: string;
1267
+ readonly child: ChildProcess;
1268
+ readonly prompt: string;
1269
+ readonly cwd: string;
1270
+ readonly model: string | undefined;
1271
+ readonly imagePaths: readonly string[];
1272
+ readonly uploadRoot: string | undefined;
1273
+ }
1274
+ interface RunPiRpcDispatchDeps extends TerminateChildTreeDeps {
1275
+ readonly lifecycle: RunLifecycle;
1276
+ readonly attachPiRpcSession: typeof attachPiRpcSession;
1277
+ readonly onCleanupFailure: (context: AgentCleanupFailureContext) => void;
1278
+ readonly cleanupStagedFiles: () => Promise<void>;
1279
+ readonly journal: RunByteJournal | undefined;
1280
+ readonly classifyFailure: ClassifyFailure | undefined;
1281
+ readonly releaseStagedResources: () => Promise<void>;
1282
+ readonly failBeforeSpawn: FailBeforeSpawn;
1283
+ }
1284
+ /** Phase 15 (pi-rpc branch): same discipline as {@link runAcpDispatch}, for the one `'pi-rpc'` def. */
1285
+ export declare function runPiRpcDispatch(input: RunPiRpcDispatchInput, deps: RunPiRpcDispatchDeps): Promise<void>;
464
1286
  export declare function createAgentExecutor(options: CreateAgentExecutorOptions): AgentExecutor;
465
1287
  export {};
466
1288
  //# sourceMappingURL=agent-executor.d.ts.map