@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.
- package/README.md +92 -0
- package/dist/agent-executor.d.ts +864 -42
- package/dist/agent-executor.d.ts.map +1 -1
- package/dist/agent-executor.js +1672 -351
- package/dist/agent-executor.js.map +1 -1
- package/dist/continuation/index.d.ts +1 -0
- package/dist/continuation/index.d.ts.map +1 -1
- package/dist/continuation/index.js +1 -0
- package/dist/continuation/index.js.map +1 -1
- package/dist/continuation/run-scoped-context-store.d.ts +79 -0
- package/dist/continuation/run-scoped-context-store.d.ts.map +1 -0
- package/dist/continuation/run-scoped-context-store.js +56 -0
- package/dist/continuation/run-scoped-context-store.js.map +1 -0
- package/dist/continuation/run-start-handler.d.ts +39 -9
- package/dist/continuation/run-start-handler.d.ts.map +1 -1
- package/dist/continuation/run-start-handler.js +12 -2
- package/dist/continuation/run-start-handler.js.map +1 -1
- package/dist/delegated-tool-bridge.d.ts +8 -0
- package/dist/delegated-tool-bridge.d.ts.map +1 -1
- package/dist/delegated-tool-bridge.js +117 -1
- package/dist/delegated-tool-bridge.js.map +1 -1
- package/dist/event-log.d.ts +18 -105
- package/dist/event-log.d.ts.map +1 -1
- package/dist/event-log.js +0 -17
- package/dist/event-log.js.map +1 -1
- package/dist/frontend-capability-tools.d.ts +1 -1
- package/dist/frontend-capability-tools.js +1 -1
- package/dist/frontend-session-registry.d.ts.map +1 -1
- package/dist/frontend-session-registry.js +26 -10
- package/dist/frontend-session-registry.js.map +1 -1
- package/dist/image-prompt-delivery.d.ts +56 -0
- package/dist/image-prompt-delivery.d.ts.map +1 -0
- package/dist/image-prompt-delivery.js +104 -0
- package/dist/image-prompt-delivery.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/remote-tool-bridge.d.ts +45 -0
- package/dist/remote-tool-bridge.d.ts.map +1 -0
- package/dist/remote-tool-bridge.js +23 -0
- package/dist/remote-tool-bridge.js.map +1 -0
- package/dist/routines/routine-store.js +1 -1
- package/dist/routines/schedule.d.ts +0 -8
- package/dist/routines/schedule.d.ts.map +1 -1
- package/dist/routines/schedule.js +71 -44
- package/dist/routines/schedule.js.map +1 -1
- package/dist/routines/scheduler.d.ts +43 -0
- package/dist/routines/scheduler.d.ts.map +1 -1
- package/dist/routines/scheduler.js +160 -120
- package/dist/routines/scheduler.js.map +1 -1
- package/dist/run/core/retry.d.ts.map +1 -1
- package/dist/run/core/retry.js +47 -35
- package/dist/run/core/retry.js.map +1 -1
- package/dist/run/diagnostics/diagnostics.d.ts.map +1 -1
- package/dist/run/diagnostics/diagnostics.js +149 -91
- package/dist/run/diagnostics/diagnostics.js.map +1 -1
- package/dist/run-lifecycle.d.ts +84 -4
- package/dist/run-lifecycle.d.ts.map +1 -1
- package/dist/run-lifecycle.js +378 -116
- package/dist/run-lifecycle.js.map +1 -1
- package/dist/terminal-session.d.ts +1 -1
- package/dist/terminal-session.d.ts.map +1 -1
- package/dist/terminal-session.js +1 -1
- package/dist/tool-executor.d.ts +26 -6
- package/dist/tool-executor.d.ts.map +1 -1
- package/dist/tool-executor.js +220 -55
- package/dist/tool-executor.js.map +1 -1
- package/dist/tool-result-media.d.ts +79 -0
- package/dist/tool-result-media.d.ts.map +1 -0
- package/dist/tool-result-media.js +80 -0
- package/dist/tool-result-media.js.map +1 -0
- package/dist/tool-result-surfaces.d.ts +78 -0
- package/dist/tool-result-surfaces.d.ts.map +1 -0
- package/dist/tool-result-surfaces.js +92 -0
- package/dist/tool-result-surfaces.js.map +1 -0
- package/package.json +18 -9
package/dist/agent-executor.d.ts
CHANGED
|
@@ -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:
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
32
|
-
* `child.stdout` chunk is forwarded verbatim as a `text_delta`
|
|
33
|
-
* event, live, as it arrives
|
|
34
|
-
* `
|
|
35
|
-
*
|
|
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
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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/
|
|
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
|
|
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
|
-
/**
|
|
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 (`
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* `
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* `
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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'
|
|
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
|