@jini-ai/daemon 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +92 -0
  2. package/dist/agent-executor.d.ts +714 -42
  3. package/dist/agent-executor.d.ts.map +1 -1
  4. package/dist/agent-executor.js +1416 -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 five 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 five has exactly one
410
+ * implementation here see {@link buildMcpBridgeDelivery}, which is the single dispatch point.
411
+ * The interface name predates the other four 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,177 @@ 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
+ * Mechanism 5 of 5 — `'codex-toml'`'s serialization step. Builds the `[mcp_servers.jini]` TOML
574
+ * table (plus, when the entry carries any env vars, a separate `[mcp_servers.jini.env]` table)
575
+ * Codex's own config schema expects.
576
+ *
577
+ * Confirmed against a real installed Codex CLI (0.151.0), not assumed from docs: round-tripping
578
+ * `codex mcp add <name> --env K=V -- <cmd> <args>` against a scratch `CODEX_HOME` and reading back
579
+ * `config.toml` produced exactly this shape (`command`/`args` as TOML strings/array in the main
580
+ * table, env vars in a nested `.env` table) — see `source-map.md` for the transcript.
581
+ * @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
582
+ * @returns A TOML fragment with no leading/trailing blank-line padding — {@link buildCodexHomeConfigToml} owns spacing when combining it with existing content.
583
+ * @complexity O(n) in the number of argv/env entries.
584
+ * @overallScore 100/100
585
+ */
586
+ export declare function buildCodexMcpServerToml(entry: McpJsonServerEntry): string;
587
+ /**
588
+ * Builds the full `config.toml` a run's scratch `CODEX_HOME` gets: the real Codex home's own
589
+ * config, verbatim, with this run's `[mcp_servers.jini]` table appended.
590
+ *
591
+ * **Append-only by design, not a parse-and-merge.** `mergeMcpJsonContent`/`mergeEnvContentMcpConfig`
592
+ * above can safely parse-merge-reserialize because their formats have a JS-native parser
593
+ * (`JSON.parse`); this driver has no TOML parser in its dependency graph (see
594
+ * `buildCodexMcpServerToml`'s doc), and every other setting a real Codex install carries — model
595
+ * choice, sandbox policy, the trusted-project list, the operator's own other MCP servers — must
596
+ * survive a spawn byte-for-byte. Appending preserves all of it; the one failure mode this trades
597
+ * away is a PRE-EXISTING `[mcp_servers.jini]` table in the operator's own config, which would
598
+ * produce a duplicate TOML key Codex rejects at startup. Accepted as vanishingly unlikely — `jini`
599
+ * is this integration's own reserved server name (see {@link JINI_MCP_SERVER_KEY}), never suggested
600
+ * to an operator for their own config — rather than solved with a full TOML parser for one
601
+ * collision case.
602
+ * @param existingRaw - The real Codex home's `config.toml` content, or `undefined` when it does not
603
+ * exist (a fresh Codex install — degrades to "start from just this run's block", matching
604
+ * {@link mergeMcpJsonContent}'s own "missing file" handling).
605
+ * @param entry - The shared bridge entry.
606
+ * @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
607
+ * @complexity O(n) in the existing config's length.
608
+ * @overallScore 100/100
609
+ */
610
+ export declare function buildCodexHomeConfigToml(existingRaw: string | undefined, entry: McpJsonServerEntry): string;
611
+ /**
612
+ * Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
613
+ * never where it writes. Resolved against the daemon HOST process's own environment (`hostEnv`,
614
+ * `process.env` at the real call site), not a run's sandboxed spawn env: `CODEX_HOME` is not in
615
+ * `BASELINE_AGENT_ENV_KEYS`, so a spawned child never inherits it anyway, and the whole point here
616
+ * is finding wherever the *operator's actual* Codex install lives, which is a host-machine fact.
617
+ * @param hostEnv - The daemon process's own environment.
618
+ * @returns `hostEnv.CODEX_HOME` when set to a non-blank value (matching Codex's own resolution
619
+ * order), else the CLI's documented default, `~/.codex`.
620
+ * @complexity O(1).
621
+ * @overallScore 100/100
622
+ */
623
+ export declare function resolveSourceCodexHomeDir(hostEnv: NodeJS.ProcessEnv): string;
624
+ /**
625
+ * What one run's MCP bridge turns into, discriminated by the delivery mechanism its def declared.
626
+ * Exactly one variant is produced per run, and each variant carries only what its own consumer
627
+ * needs — so a consumer cannot accidentally read another mechanism's payload.
628
+ */
629
+ export type McpBridgeDelivery =
630
+ /** `'claude-mcp-json'` (claude, codebuddy): a `.mcp.json` staged into the run cwd, whose path the def's `buildArgs` passes as `--mcp-config`. */
631
+ {
632
+ readonly kind: 'claude-mcp-json';
633
+ readonly mcpJsonPath: string;
634
+ readonly serverEntry: McpJsonServerEntry;
635
+ }
636
+ /** `'acp-merge'` (the 9 ACP-native defs): `mcpServers` entries for the ACP `session/new` params. */
637
+ | {
638
+ readonly kind: 'acp-merge';
639
+ readonly mcpServers: readonly AcpMcpServerInput[];
640
+ }
641
+ /** `'opencode-env-content'` / `'mimo-env-content'` (opencode, mimo): one spawn-env variable carrying the serialised config. */
642
+ | {
643
+ readonly kind: 'env-content';
644
+ readonly envVarName: string;
645
+ readonly serverEntry: McpJsonServerEntry;
646
+ }
647
+ /**
648
+ * `'codex-toml'` (codex): no path yet — unlike `'claude-mcp-json'`'s `mcpJsonPath`, the scratch
649
+ * `CODEX_HOME` directory is created with `fs.mkdtemp` (a real, non-deterministic filesystem
650
+ * effect — see `McpJsonInjectionOptions.mkdtemp`'s own doc for why), so it cannot be computed by
651
+ * this delivery's pure, synchronous dispatch. `prepareCodexHomeIfNeeded` stages it separately and
652
+ * reports the resulting path back into `childEnv.CODEX_HOME` directly, never through this type.
653
+ */
654
+ | {
655
+ readonly kind: 'codex-toml';
656
+ readonly serverEntry: McpJsonServerEntry;
657
+ };
658
+ /**
659
+ * **The single dispatch point from an `externalMcpInjection` strategy to its delivery mechanism.**
660
+ * Pure and synchronous — the one effectful input (the per-run bearer credential) arrives already
661
+ * resolved, so every strategy's mapping is directly assertable without touching the filesystem,
662
+ * the environment, or a keystore.
663
+ *
664
+ * Keyed off the declared *strategy*, never off `def.id`: a def gets a working bridge by declaring a
665
+ * mechanism, not by being named in this file. That is what makes the 9 `'acp-merge'` defs work
666
+ * without any of their own files being touched.
667
+ *
668
+ * @param input.cwd - The run's working directory; only `'claude-mcp-json'` uses it, to place this
669
+ * run's own config file (see {@link mcpJsonPathForRun}) — never `cwd/.mcp.json` itself.
670
+ * @param input.runId - Scopes the bridge child to this run.
671
+ * @param input.strategy - The def's declared `externalMcpInjection`, or `undefined` for a def with no native MCP transport.
672
+ * @param input.options - The host's bridge options, or `undefined` when the host never configured injection.
673
+ * @param input.credential - Already-resolved bearer token, or `undefined` to omit `JINI_DAEMON_TOKEN` entirely.
674
+ * @returns `null` when this run delivers nothing — an unconfigured host, or a def declaring no
675
+ * strategy — which is byte-identical to this feature not existing.
676
+ * @complexity O(1).
677
+ * @overallScore 100/100
678
+ */
679
+ export declare function buildMcpBridgeDelivery(input: {
680
+ readonly cwd: string;
681
+ readonly runId: string;
682
+ readonly strategy: RuntimeAgentDef['externalMcpInjection'];
683
+ readonly options: McpJsonInjectionOptions | undefined;
684
+ readonly credential: string | undefined;
685
+ }): McpBridgeDelivery | null;
686
+ /** A staged, run-scoped Codex `CODEX_HOME` — the directory-holding analogue of {@link PreparedPromptFile}/{@link PreparedAgentLogFile} from `@jini-ai/agent-runtime`. */
687
+ export type PreparedCodexHome = {
688
+ /** Absolute path to hand to the spawned child as its `CODEX_HOME` env var. */
689
+ readonly path: string;
690
+ /** 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. */
691
+ readonly cleanup: () => Promise<void>;
692
+ };
328
693
  /**
329
694
  * Gap 4 of the run/chat orchestration Final Recommendation: what
330
695
  * `classifyFailure` (see `CreateAgentExecutorOptions.classifyFailure`) is
@@ -368,6 +733,41 @@ export interface FailureClassificationContext {
368
733
  * nothing a classifier could meaningfully examine.
369
734
  */
370
735
  export type ClassifyFailure = (context: FailureClassificationContext) => boolean | Promise<boolean>;
736
+ /**
737
+ * Default ceiling on the `'until-close'` stdout accumulator (see `RuntimeStdoutPolicy` in
738
+ * `@jini-ai/agent-runtime`), in bytes of received UTF-8.
739
+ *
740
+ * A buffered def holds its child's entire stdout in one in-memory string until the process closes,
741
+ * which is exactly what makes the accumulator a denial-of-service surface: the child is a
742
+ * prompt-influenced agent CLI this driver already treats as potentially adversarial (SEC-001), and
743
+ * nothing obliges it to ever close or to stop emitting. Without a ceiling one run could exhaust the
744
+ * daemon's heap and take every unrelated run in the process down with it.
745
+ *
746
+ * 8 MiB is chosen to sit far above any real buffered-agent transcript (antigravity's print-mode
747
+ * output — the only `'until-close'` def — is a few KiB of auth prompt and result text) while staying
748
+ * small enough that a hostile child cannot meaningfully pressure the heap. A host that genuinely
749
+ * needs more passes `CreateAgentExecutorOptions.bufferedStdoutMaxBytes`.
750
+ */
751
+ export declare const DEFAULT_BUFFERED_STDOUT_MAX_BYTES: number;
752
+ /** The side-effect signals {@link applyAgentTranslationSideEffects} reports through, one callback per signal so a caller only wires the ones it actually tracks. */
753
+ interface AgentTranslationSideEffectSink {
754
+ readonly onSessionId: (sessionId: string) => void;
755
+ readonly onToolCall: () => void;
756
+ readonly onUserVisibleOutput: () => void;
757
+ }
758
+ /**
759
+ * Applies one already-translated `'agent'`-kind event's side-effect signals — a captured session id
760
+ * (gap 5), and the `toolCallSeen`/`userVisibleOutputSeen` pair every `wire*Lifecycle` driver tracks
761
+ * for `FailureClassificationContext.sideEffects` — through `sink`. Extracted from `wireAcpLifecycle`'s
762
+ * `send()`, where this exact three-level-deep nesting (session-id check, then tool_use/else-if
763
+ * delta-length check) was that function's largest single cognitive-complexity contributor. Pure
764
+ * except for calling the injected `sink` callbacks.
765
+ * @param payload - The translated event's `RunAgentPayload`.
766
+ * @param sessionId - The translation's optional captured session id, or `undefined`.
767
+ * @param sink - The driver-specific effects to apply.
768
+ * @complexity O(1).
769
+ */
770
+ export declare function applyAgentTranslationSideEffects(payload: RunAgentPayload, sessionId: string | undefined, sink: AgentTranslationSideEffectSink): void;
371
771
  export interface CreateAgentExecutorOptions {
372
772
  readonly lifecycle: RunLifecycle;
373
773
  /** @default the real `@jini-ai/agent-runtime` registry lookup */
@@ -399,6 +799,14 @@ export interface CreateAgentExecutorOptions {
399
799
  * @default the real `@jini-ai/agent-runtime` prompt-file stager
400
800
  */
401
801
  readonly preparePromptFileForAgent?: typeof preparePromptFileForAgent;
802
+ /**
803
+ * Stages a `needsAgentLogFile` def's (antigravity) diagnostic-log path
804
+ * before `buildArgs` runs. Same real-filesystem/injectable-for-tests deal
805
+ * as `preparePromptFileForAgent` above, and a no-op for every def that
806
+ * did not opt in.
807
+ * @default the real `@jini-ai/agent-runtime` log-file stager
808
+ */
809
+ readonly prepareAgentLogFile?: typeof prepareAgentLogFile;
402
810
  /** @default the real `@jini-ai/platform` process-snapshot enumerator */
403
811
  readonly listProcessSnapshots?: typeof listProcessSnapshots;
404
812
  /** @default the real `@jini-ai/platform` descendant-PID collector */
@@ -445,6 +853,23 @@ export interface CreateAgentExecutorOptions {
445
853
  * `command`/`daemonUrl` this package could assume on a caller's behalf.
446
854
  */
447
855
  readonly mcpJsonInjection?: McpJsonInjectionOptions;
856
+ /**
857
+ * Ceiling on how many bytes of a `'until-close'` def's stdout this driver will hold in memory
858
+ * before it stops accumulating and reports the shortfall — see
859
+ * {@link DEFAULT_BUFFERED_STDOUT_MAX_BYTES} for the threat this closes and why 8 MiB.
860
+ * @default {@link DEFAULT_BUFFERED_STDOUT_MAX_BYTES}
861
+ */
862
+ readonly bufferedStdoutMaxBytes?: number;
863
+ /**
864
+ * Host-owned system-prompt overlay — see `prompt-augmenter.ts`'s own doc for why this seam
865
+ * exists (product-specific discovery/behavior instructions that don't belong in the engine).
866
+ * When present, `systemOverlay()` is called once per `run()` and its result (if non-null) is
867
+ * threaded through to `buildArgs` as `RuntimeBuildOptions.systemPromptOverlay` — a def with no
868
+ * append-system-prompt mechanism ignores it.
869
+ * @default undefined — no overlay is computed and no def sees `systemPromptOverlay`,
870
+ * byte-identical to pre-this-option behavior.
871
+ */
872
+ readonly promptAugmenter?: PromptAugmenter;
448
873
  }
449
874
  /**
450
875
  * Creates the `AgentExecutor` reference implementation: an in-process
@@ -461,6 +886,253 @@ export interface CreateAgentExecutorOptions {
461
886
  * @complexity `run()`'s own setup is O(1); steady-state cost is the chosen stream parser's.
462
887
  * @overallScore 100/100
463
888
  */
889
+ /** `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. */
890
+ type FailBeforeSpawn = (runId: string, code: AgentExecutorErrorCode, message: string) => Promise<never>;
891
+ interface ResolvedDefAndFormat {
892
+ readonly def: RuntimeAgentDef;
893
+ readonly streamFormat: SupportedStreamFormat;
894
+ }
895
+ /** Phase 1: registry lookup + `assessAgentExecutorCompatibility` guard. */
896
+ export declare function resolveDefAndStreamFormat(input: Pick<AgentExecutorRunInput, 'runId' | 'agentId'>, deps: {
897
+ readonly getAgentDef: typeof getAgentDef;
898
+ readonly failBeforeSpawn: FailBeforeSpawn;
899
+ }): Promise<ResolvedDefAndFormat>;
900
+ /** Phase 2: image-prompt-delivery augmentation + argv-budget guard for argv-bound defs. */
901
+ export declare function resolveImageDeliveryAndArgvBudget(input: {
902
+ readonly runId: string;
903
+ readonly def: RuntimeAgentDef;
904
+ readonly prompt: string;
905
+ readonly imagePaths: readonly string[] | undefined;
906
+ readonly extraAllowedDirs: readonly string[] | undefined;
907
+ }, deps: {
908
+ readonly failBeforeSpawn: FailBeforeSpawn;
909
+ }): Promise<ImagePromptDelivery>;
910
+ /**
911
+ * Phase 3a: the subprocess environment this run's launch resolution and spawn should use — the
912
+ * caller-supplied escape hatch verbatim, or the deny-by-default `BASELINE_AGENT_ENV_KEYS` allowlist.
913
+ * Pure.
914
+ */
915
+ export declare function resolveRunEnv(input: Pick<AgentExecutorRunInput, 'env' | 'credentialEnv'>, hostEnv: NodeJS.ProcessEnv): Record<string, string>;
916
+ /** A resolved launch whose `launchPath` is confirmed non-null — see {@link resolveLaunch}. */
917
+ type ConfirmedAgentLaunchResolution = AgentLaunchResolution & {
918
+ readonly launchPath: string;
919
+ };
920
+ /** Phase 3b: launch-path resolution + binary-not-resolved guard. */
921
+ export declare function resolveLaunch(input: {
922
+ readonly runId: string;
923
+ readonly def: RuntimeAgentDef;
924
+ readonly resolvedEnv: Record<string, string>;
925
+ }, deps: {
926
+ readonly resolveAgentLaunch: typeof resolveAgentLaunch;
927
+ readonly failBeforeSpawn: FailBeforeSpawn;
928
+ }): Promise<ConfirmedAgentLaunchResolution>;
929
+ /** Phase 4a: stage a `promptViaFile` def's prompt to a temp file (a no-op for every other def). */
930
+ export declare function stagePromptFile(input: {
931
+ readonly runId: string;
932
+ readonly def: RuntimeAgentDef;
933
+ readonly prompt: string;
934
+ }, deps: {
935
+ readonly preparePromptFileForAgent: typeof preparePromptFileForAgent;
936
+ readonly failBeforeSpawn: FailBeforeSpawn;
937
+ }): Promise<PreparedPromptFile | null>;
938
+ /** Phase 4b: stage a `needsAgentLogFile` def's diagnostic-log path (a no-op for every other def). */
939
+ export declare function stageLogFile(input: {
940
+ readonly runId: string;
941
+ readonly def: RuntimeAgentDef;
942
+ readonly preparedPromptFile: PreparedPromptFile | null;
943
+ }, deps: {
944
+ readonly prepareAgentLogFile: typeof prepareAgentLogFile;
945
+ readonly failBeforeSpawn: FailBeforeSpawn;
946
+ }): Promise<PreparedAgentLogFile | null>;
947
+ /** Phase 5: resolves this run's MCP bridge delivery (credential resolution + {@link buildMcpBridgeDelivery}). */
948
+ export declare function resolveMcpBridgeForRun(input: {
949
+ readonly runId: string;
950
+ readonly cwd: string;
951
+ readonly def: RuntimeAgentDef;
952
+ }, deps: {
953
+ readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
954
+ readonly cleanupStagedFiles: () => Promise<void>;
955
+ readonly failBeforeSpawn: FailBeforeSpawn;
956
+ }): Promise<McpBridgeDelivery | null>;
957
+ /**
958
+ * Phase 6a/10c: the subprocess environment every env-riding MCP mechanism uses — mechanism 3+4
959
+ * (`'opencode-env-content'`/`'mimo-env-content'`, merged into whatever the host already set there,
960
+ * never a CLI argument: the config embeds `JINI_DAEMON_TOKEN`, and process arguments are readable
961
+ * by any other local user through `ps`) and mechanism 5 (`'codex-toml'`, `CODEX_HOME` relocation).
962
+ * Pure — `codexHomeDir` arrives already staged by {@link prepareCodexHomeIfNeeded}, which is the
963
+ * one part of this mechanism that is NOT pure (a real `mkdtemp`).
964
+ * @param spawnEnv - The env every other spawn-time step (launch-path resolution, `applyAgentLaunchEnv`) already computed.
965
+ * @param mcpBridge - This run's resolved bridge delivery, or `null` for an unconfigured host / no-strategy def.
966
+ * @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).
967
+ * @complexity O(1) plus `mergeEnvContentMcpConfig`'s own `JSON.parse`/`JSON.stringify` cost.
968
+ * @overallScore 100/100
969
+ */
970
+ export declare function computeChildEnv(spawnEnv: NodeJS.ProcessEnv, mcpBridge: McpBridgeDelivery | null, codexHomeDir?: string): NodeJS.ProcessEnv;
971
+ /**
972
+ * Phase 6b: the `RuntimeContext` `buildArgs` receives — `undefined` unless a file, bridge path, or
973
+ * session id was staged. Pure.
974
+ *
975
+ * `resumeSessionId`/`newSessionId` round-trip a prior run's `RunEndPayload.sessionRef` (see
976
+ * `@jini-ai/protocol`'s doc on that field) back into this run's `RuntimeContext`, letting a
977
+ * `resumesSessionViaCli` def (e.g. claude) continue its own CLI session across turns instead of
978
+ * spawning cold every time. Either one alone must still produce a context — a run supplying ONLY a
979
+ * session id, with no prompt/log file staged and no claude-mcp-json bridge, is exactly the common
980
+ * case for a resumed turn.
981
+ */
982
+ export declare function computeRuntimeContext(preparedPromptFile: PreparedPromptFile | null, preparedLogFile: PreparedAgentLogFile | null, mcpBridge: McpBridgeDelivery | null, resumeSessionId?: string | null, newSessionId?: string): RuntimeContext | undefined;
983
+ /** 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. */
984
+ export declare function buildAgentBuildArgsOptions(input: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode'>, systemPromptOverlay: string | null | undefined): RuntimeBuildOptions | undefined;
985
+ /** Phase 9b: calls the def's `buildArgs`, releasing staged resources and failing the run on a throw. */
986
+ export declare function buildRunArgs(input: {
987
+ readonly runId: string;
988
+ readonly def: RuntimeAgentDef;
989
+ readonly imageDelivery: ImagePromptDelivery;
990
+ readonly imagePaths: readonly string[] | undefined;
991
+ readonly runInput: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode'>;
992
+ readonly systemPromptOverlay: string | null | undefined;
993
+ readonly runtimeContext: RuntimeContext | undefined;
994
+ }, deps: {
995
+ readonly releaseStagedResources: () => Promise<void>;
996
+ readonly failBeforeSpawn: FailBeforeSpawn;
997
+ }): Promise<string[]>;
998
+ /** 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). */
999
+ export declare function writeMcpJsonIfNeeded(input: {
1000
+ readonly runId: string;
1001
+ readonly cwd: string;
1002
+ readonly def: RuntimeAgentDef;
1003
+ readonly mcpBridge: McpBridgeDelivery | null;
1004
+ }, deps: {
1005
+ readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
1006
+ readonly releaseStagedResources: () => Promise<void>;
1007
+ readonly failBeforeSpawn: FailBeforeSpawn;
1008
+ }): Promise<string | undefined>;
1009
+ /**
1010
+ * Phase 10b: mechanism 5 of 5's one effect — stages this run's scratch `CODEX_HOME` directory,
1011
+ * returning the prepared handle `cleanupStagedFiles` should later release (`null` for every other
1012
+ * mechanism, or for an unconfigured host — matching {@link writeMcpJsonIfNeeded}'s identical gate).
1013
+ * @param input.def - Only used for its `id`, in the failure message.
1014
+ * @param input.mcpBridge - This run's resolved bridge delivery — a no-op unless its `kind` is `'codex-toml'`.
1015
+ * @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.
1016
+ * @complexity O(1) plus {@link prepareCodexHomeForRun}'s own cost.
1017
+ * @overallScore 100/100
1018
+ */
1019
+ export declare function prepareCodexHomeIfNeeded(input: {
1020
+ readonly runId: string;
1021
+ readonly def: RuntimeAgentDef;
1022
+ readonly mcpBridge: McpBridgeDelivery | null;
1023
+ }, deps: {
1024
+ readonly mcpJsonInjection: McpJsonInjectionOptions | undefined;
1025
+ readonly hostEnv: NodeJS.ProcessEnv;
1026
+ readonly releaseStagedResources: () => Promise<void>;
1027
+ readonly failBeforeSpawn: FailBeforeSpawn;
1028
+ }): Promise<PreparedCodexHome | null>;
1029
+ /** 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. */
1030
+ export declare function guardWindowsCommandLineBudget(input: {
1031
+ readonly runId: string;
1032
+ readonly def: RuntimeAgentDef;
1033
+ readonly launchPath: string;
1034
+ readonly args: readonly string[];
1035
+ }, deps: {
1036
+ readonly releaseStagedResources: () => Promise<void>;
1037
+ readonly failBeforeSpawn: FailBeforeSpawn;
1038
+ }): Promise<void>;
1039
+ /** {@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. */
1040
+ export type SpawnAgentChildProcessResult = {
1041
+ readonly kind: 'ok';
1042
+ readonly child: ChildProcess;
1043
+ } | {
1044
+ readonly kind: 'error';
1045
+ readonly error: unknown;
1046
+ };
1047
+ /**
1048
+ * Phase 12: the real `node:child_process.spawn` call.
1049
+ *
1050
+ * **Deliberately synchronous, unlike every other phase in this file.** A spawned child can emit
1051
+ * `'error'` on the very next microtask tick (Node schedules it eagerly on some failure modes, and a
1052
+ * test harness simulating "the child emits 'error' before 'spawn'" does so explicitly via
1053
+ * `queueMicrotask`). `run()` must register its `'error'` listeners (`wireChildLifecycle`'s safety net,
1054
+ * then `waitForSpawnOrError`'s `child.once('error', reject)`) in the *same synchronous turn* as this
1055
+ * spawn call — Node's `EventEmitter` throws synchronously when `'error'` fires with zero listeners
1056
+ * attached. Wrapping this call in an `async function` and `await`ing it (as every other phase here
1057
+ * does) would insert a microtask tick between spawn and listener registration, occasionally losing
1058
+ * that race — confirmed by a real test failure during this refactor (an uncaught `EventEmitter`
1059
+ * `'error'` exception) before this function was changed back to a plain, unawaited call returning a
1060
+ * result object instead of throwing/rejecting.
1061
+ * @returns `{kind:'ok', child}` on success, `{kind:'error', error}` on a synchronous throw from `spawn`
1062
+ * — `run()` itself is responsible for cleanup and `failBeforeSpawn` on the error variant, both of
1063
+ * which are safe to make asynchronous since no child (and hence no listener race) exists yet.
1064
+ * @complexity O(1) plus `spawn`'s own cost.
1065
+ */
1066
+ export declare function spawnAgentChildProcess(input: {
1067
+ readonly cwd: string;
1068
+ readonly childEnv: NodeJS.ProcessEnv;
1069
+ readonly invocation: ReturnType<typeof createCommandInvocation>;
1070
+ }, deps: {
1071
+ readonly spawn: typeof nodeSpawn;
1072
+ }): SpawnAgentChildProcessResult;
1073
+ /** 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`. */
1074
+ export declare function isStdinDrivenFormat(streamFormat: SupportedStreamFormat): streamFormat is ChildDrivenStreamFormat;
1075
+ /** Phase 13: awaits spawn confirmation, routing a failure through the same `failBeforeSpawn` shape every earlier guard uses. */
1076
+ export declare function confirmChildSpawned(input: {
1077
+ readonly runId: string;
1078
+ readonly def: RuntimeAgentDef;
1079
+ readonly child: ChildProcess;
1080
+ }, deps: {
1081
+ readonly releaseStagedResources: () => Promise<void>;
1082
+ readonly failBeforeSpawn: FailBeforeSpawn;
1083
+ }): Promise<void>;
1084
+ /** 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. */
1085
+ export declare function armHandoffWatcher(runtimeLockHold: RuntimeLockHold | undefined, handoffInput: {
1086
+ readonly logFilePath: string | undefined;
1087
+ readonly model: string | undefined;
1088
+ readonly processExited: AbortSignal;
1089
+ }, release: () => void): void;
1090
+ interface RunAcpDispatchInput {
1091
+ readonly runId: string;
1092
+ readonly agentId: string;
1093
+ readonly child: ChildProcess;
1094
+ readonly prompt: string;
1095
+ readonly cwd: string;
1096
+ readonly model: string | undefined;
1097
+ readonly imagePaths: readonly string[];
1098
+ readonly envFormat: 'array' | 'map' | undefined;
1099
+ readonly mcpBridge: McpBridgeDelivery | null;
1100
+ }
1101
+ interface RunAcpDispatchDeps extends TerminateChildTreeDeps {
1102
+ readonly lifecycle: RunLifecycle;
1103
+ readonly attachAcpSession: typeof attachAcpSession;
1104
+ readonly onPermissionRequest: AcpPermissionHandler | undefined;
1105
+ readonly onCleanupFailure: (context: AgentCleanupFailureContext) => void;
1106
+ readonly cleanupStagedFiles: () => Promise<void>;
1107
+ readonly journal: RunByteJournal | undefined;
1108
+ readonly classifyFailure: ClassifyFailure | undefined;
1109
+ readonly releaseStagedResources: () => Promise<void>;
1110
+ readonly failBeforeSpawn: FailBeforeSpawn;
1111
+ }
1112
+ /** Phase 15 (ACP branch): attaches the ACP session, escalating process-tree teardown and failing the run through `failBeforeSpawn` on an attach-time throw. */
1113
+ export declare function runAcpDispatch(input: RunAcpDispatchInput, deps: RunAcpDispatchDeps): Promise<void>;
1114
+ interface RunPiRpcDispatchInput {
1115
+ readonly runId: string;
1116
+ readonly agentId: string;
1117
+ readonly child: ChildProcess;
1118
+ readonly prompt: string;
1119
+ readonly cwd: string;
1120
+ readonly model: string | undefined;
1121
+ readonly imagePaths: readonly string[];
1122
+ readonly uploadRoot: string | undefined;
1123
+ }
1124
+ interface RunPiRpcDispatchDeps extends TerminateChildTreeDeps {
1125
+ readonly lifecycle: RunLifecycle;
1126
+ readonly attachPiRpcSession: typeof attachPiRpcSession;
1127
+ readonly onCleanupFailure: (context: AgentCleanupFailureContext) => void;
1128
+ readonly cleanupStagedFiles: () => Promise<void>;
1129
+ readonly journal: RunByteJournal | undefined;
1130
+ readonly classifyFailure: ClassifyFailure | undefined;
1131
+ readonly releaseStagedResources: () => Promise<void>;
1132
+ readonly failBeforeSpawn: FailBeforeSpawn;
1133
+ }
1134
+ /** Phase 15 (pi-rpc branch): same discipline as {@link runAcpDispatch}, for the one `'pi-rpc'` def. */
1135
+ export declare function runPiRpcDispatch(input: RunPiRpcDispatchInput, deps: RunPiRpcDispatchDeps): Promise<void>;
464
1136
  export declare function createAgentExecutor(options: CreateAgentExecutorOptions): AgentExecutor;
465
1137
  export {};
466
1138
  //# sourceMappingURL=agent-executor.d.ts.map