@jini-ai/daemon 0.3.8 → 0.5.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.
- package/CHANGELOG.md +119 -0
- package/README.md +50 -8
- package/dist/agent-executor/contracts.d.ts +736 -0
- package/dist/agent-executor/contracts.js +28 -0
- package/dist/agent-executor/event-translation.d.ts +87 -0
- package/dist/agent-executor/event-translation.js +230 -0
- package/dist/agent-executor/events.d.ts +190 -0
- package/dist/agent-executor/events.js +848 -0
- package/dist/agent-executor/handoff.d.ts +18 -0
- package/dist/agent-executor/handoff.js +16 -0
- package/dist/agent-executor/index.d.ts +98 -0
- package/dist/agent-executor/index.js +98 -0
- package/dist/agent-executor/launch.d.ts +375 -0
- package/dist/agent-executor/launch.js +1001 -0
- package/dist/agent-executor/mcp-bridge.d.ts +338 -0
- package/dist/agent-executor/mcp-bridge.js +835 -0
- package/dist/agent-executor/values.d.ts +5 -0
- package/dist/agent-executor/values.js +13 -0
- package/dist/agent-executor.d.ts +375 -176
- package/dist/agent-executor.js +388 -335
- package/dist/artifacts/index.d.ts +14 -0
- package/dist/artifacts/index.js +14 -0
- package/dist/artifacts/manifest.d.ts +90 -0
- package/dist/artifacts/manifest.js +229 -0
- package/dist/artifacts/publication-guard.d.ts +28 -0
- package/dist/artifacts/publication-guard.js +77 -0
- package/dist/artifacts/runtime-compat.d.ts +37 -0
- package/dist/artifacts/runtime-compat.js +33 -0
- package/dist/artifacts/store.d.ts +85 -0
- package/dist/artifacts/store.js +92 -0
- package/dist/artifacts/stub-guard.d.ts +73 -0
- package/dist/artifacts/stub-guard.js +198 -0
- package/dist/artifacts/text-suppression.d.ts +68 -0
- package/dist/artifacts/text-suppression.js +184 -0
- package/dist/close-status.d.ts +11 -8
- package/dist/close-status.js +31 -19
- package/dist/continuation/continuation-transport.d.ts +3 -1
- package/dist/continuation/continuation-transport.js +1 -1
- package/dist/continuation/journal.d.ts +12 -5
- package/dist/continuation/journal.js +4 -4
- package/dist/continuation/run-scoped-context-store.d.ts +13 -4
- package/dist/continuation/run-scoped-context-store.js +6 -6
- package/dist/continuation/run-start-handler.d.ts +1 -1
- package/dist/continuation/run-start-handler.js +3 -16
- package/dist/delegated-tool-bridge.d.ts +7 -3
- package/dist/delegated-tool-bridge.js +32 -31
- package/dist/event-log.d.ts +7 -5
- package/dist/event-log.js +12 -11
- package/dist/frontend-capability-tools.d.ts +1 -1
- package/dist/frontend-capability-tools.js +3 -2
- package/dist/frontend-session-registry.d.ts +32 -9
- package/dist/frontend-session-registry.js +12 -11
- package/dist/http/active-context.d.ts +105 -0
- package/dist/http/active-context.js +67 -0
- package/dist/http/agents.d.ts +68 -0
- package/dist/http/agents.js +12 -0
- package/dist/http/attachments.d.ts +618 -0
- package/dist/http/attachments.js +1034 -0
- package/dist/http/cancel-owned-runs.d.ts +49 -0
- package/dist/http/cancel-owned-runs.js +35 -0
- package/dist/http/component-catalog.d.ts +84 -0
- package/dist/http/component-catalog.js +64 -0
- package/dist/http/connectors.d.ts +209 -0
- package/dist/http/connectors.js +409 -0
- package/dist/http/daemon-status.d.ts +76 -0
- package/dist/http/daemon-status.js +55 -0
- package/dist/http/db-ops.d.ts +123 -0
- package/dist/http/db-ops.js +189 -0
- package/dist/http/delegated-tools.d.ts +113 -0
- package/dist/http/delegated-tools.js +230 -0
- package/dist/http/frontend-control.d.ts +136 -0
- package/dist/http/frontend-control.js +79 -0
- package/dist/http/frontend-sessions.d.ts +110 -0
- package/dist/http/frontend-sessions.js +180 -0
- package/dist/http/health.d.ts +95 -0
- package/dist/http/health.js +40 -0
- package/dist/http/host-tools.d.ts +214 -0
- package/dist/http/host-tools.js +329 -0
- package/dist/http/index.d.ts +52 -0
- package/dist/http/index.js +27 -0
- package/dist/http/media.d.ts +146 -0
- package/dist/http/media.js +346 -0
- package/dist/http/memory.d.ts +265 -0
- package/dist/http/memory.js +328 -0
- package/dist/http/model-proxy.d.ts +49 -0
- package/dist/http/model-proxy.js +642 -0
- package/dist/http/remote-run-events.d.ts +70 -0
- package/dist/http/remote-run-events.js +224 -0
- package/dist/http/research.d.ts +48 -0
- package/dist/http/research.js +315 -0
- package/dist/http/route-manifest.d.ts +35 -0
- package/dist/http/route-manifest.js +103 -0
- package/dist/http/routines.d.ts +120 -0
- package/dist/http/routines.js +298 -0
- package/dist/http/run-credentials.d.ts +73 -0
- package/dist/http/run-credentials.js +71 -0
- package/dist/http/runs.d.ts +91 -0
- package/dist/http/runs.js +221 -0
- package/dist/http/terminals.d.ts +138 -0
- package/dist/http/terminals.js +325 -0
- package/dist/http/tool-catalog.d.ts +78 -0
- package/dist/http/tool-catalog.js +66 -0
- package/dist/http/workspace-root.d.ts +77 -0
- package/dist/http/workspace-root.js +63 -0
- package/dist/http/xai.d.ts +90 -0
- package/dist/http/xai.js +655 -0
- package/dist/image-prompt-delivery.d.ts +13 -3
- package/dist/image-prompt-delivery.js +5 -5
- package/dist/index.d.ts +556 -4
- package/dist/index.js +556 -4
- package/dist/legacy-data-migration.d.ts +59 -8
- package/dist/legacy-data-migration.js +38 -33
- package/dist/read-only-tools.d.ts +130 -0
- package/dist/read-only-tools.js +115 -0
- package/dist/remote-tool-bridge.d.ts +11 -5
- package/dist/remote-tool-bridge.js +15 -15
- package/dist/routines/index.d.ts +1 -1
- package/dist/routines/index.js +1 -1
- package/dist/routines/routine-store.d.ts +34 -12
- package/dist/routines/routine-store.js +21 -20
- package/dist/routines/schedule.d.ts +48 -10
- package/dist/routines/schedule.js +21 -21
- package/dist/routines/scheduler.d.ts +32 -11
- package/dist/routines/scheduler.js +61 -57
- package/dist/routines/types.d.ts +10 -3
- package/dist/run/core/result.d.ts +6 -2
- package/dist/run/core/result.js +3 -3
- package/dist/run/core/retry.d.ts +17 -4
- package/dist/run/core/retry.js +7 -11
- package/dist/run/diagnostics/diagnostics.d.ts +14 -4
- package/dist/run/diagnostics/diagnostics.js +7 -7
- package/dist/run/index.d.ts +1 -1
- package/dist/run/index.js +1 -1
- package/dist/run-credentials.d.ts +138 -0
- package/dist/run-credentials.js +152 -0
- package/dist/run-lifecycle.d.ts +92 -33
- package/dist/run-lifecycle.js +81 -74
- package/dist/scheduler.d.ts +8 -0
- package/dist/scheduler.js +2 -0
- package/dist/session-coordination.d.ts +115 -0
- package/dist/session-coordination.js +111 -0
- package/dist/store/agent-sessions/errors.d.ts +8 -0
- package/dist/store/agent-sessions/errors.js +10 -0
- package/dist/store/agent-sessions/index.d.ts +12 -0
- package/dist/store/agent-sessions/index.js +19 -0
- package/dist/store/agent-sessions/pglite.d.ts +12 -0
- package/dist/store/agent-sessions/pglite.js +7 -0
- package/dist/store/agent-sessions/ports.d.ts +24 -0
- package/dist/store/agent-sessions/ports.js +2 -0
- package/dist/store/agent-sessions/postgres.d.ts +12 -0
- package/dist/store/agent-sessions/postgres.js +7 -0
- package/dist/store/agent-sessions/schema.d.ts +3 -0
- package/dist/store/agent-sessions/schema.js +23 -0
- package/dist/store/agent-sessions/sql.d.ts +35 -0
- package/dist/store/agent-sessions/sql.js +59 -0
- package/dist/store/agent-sessions/sqlite.d.ts +117 -0
- package/dist/store/agent-sessions/sqlite.js +128 -0
- package/dist/store/event-log/sqlite.d.ts +75 -0
- package/dist/store/event-log/sqlite.js +230 -0
- package/dist/surface-exchanges.d.ts +97 -0
- package/dist/surface-exchanges.js +153 -0
- package/dist/terminal-session.d.ts +43 -11
- package/dist/terminal-session.js +22 -20
- package/dist/tokens.js +4 -4
- package/dist/tool-audit.d.ts +114 -0
- package/dist/tool-audit.js +95 -0
- package/dist/tool-executor.d.ts +22 -7
- package/dist/tool-executor.js +16 -16
- package/dist/tool-result-media.d.ts +3 -1
- package/dist/tool-result-media.js +1 -1
- package/dist/tool-result-surfaces.d.ts +3 -1
- package/dist/tool-result-surfaces.js +1 -1
- package/package.json +127 -9
package/dist/agent-executor.js
CHANGED
|
@@ -81,10 +81,10 @@
|
|
|
81
81
|
* byte-identical to pre-gap-4 behavior — every `'failed'` outcome
|
|
82
82
|
* resumable:false). OD's ~20-vendor-CLI text-matching failure classifier was
|
|
83
83
|
* deliberately never ported (see `run/core/failure-taxonomy.ts`'s own doc and
|
|
84
|
-
* `
|
|
84
|
+
* `archived provenance ledger`). The real zero-config classifier lives in `@jini-ai/daemon`'s
|
|
85
85
|
* `run/core/retry.ts` (`resumableFromProcessExit`/`classifyProcessExitFailure`)
|
|
86
86
|
* and is wired in by `@jini-ai/server`'s `createLocalNodeDaemon` — see that
|
|
87
|
-
* package's own
|
|
87
|
+
* package's own archived provenance ledger, and `run/core/retry.ts`'s own doc for the
|
|
88
88
|
* classification policy and its 2026-07-22 merge-time reconciliation against
|
|
89
89
|
* a second, independently-built (and rejected) classifier that once lived in
|
|
90
90
|
* this module.
|
|
@@ -118,7 +118,8 @@ const SUPPORTED_STREAM_FORMATS = [
|
|
|
118
118
|
* @complexity O(1) — fixed membership check.
|
|
119
119
|
* @overallScore 100/100
|
|
120
120
|
*/
|
|
121
|
-
export function isSupportedStreamFormat(
|
|
121
|
+
export function isSupportedStreamFormat(args) {
|
|
122
|
+
const { value } = args;
|
|
122
123
|
return SUPPORTED_STREAM_FORMATS.includes(value);
|
|
123
124
|
}
|
|
124
125
|
/**
|
|
@@ -142,14 +143,15 @@ export function isSupportedStreamFormat(value) {
|
|
|
142
143
|
* @complexity O(1) — fixed field checks.
|
|
143
144
|
* @overallScore 100/100
|
|
144
145
|
*/
|
|
145
|
-
export function assessAgentExecutorCompatibility(def) {
|
|
146
|
-
const
|
|
147
|
-
if (!isSupportedStreamFormat(
|
|
146
|
+
export function assessAgentExecutorCompatibility({ def }) {
|
|
147
|
+
const format = { value: def.streamFormat };
|
|
148
|
+
if (!isSupportedStreamFormat(format)) {
|
|
148
149
|
return {
|
|
149
150
|
supported: false,
|
|
150
|
-
reason: `AgentExecutor: agent "${def.id}" has streamFormat "${
|
|
151
|
+
reason: `AgentExecutor: agent "${def.id}" has streamFormat "${format.value}", which is not implemented in v1 — only ${SUPPORTED_STREAM_FORMATS.join(', ')} are supported`,
|
|
151
152
|
};
|
|
152
153
|
}
|
|
154
|
+
const streamFormat = format.value;
|
|
153
155
|
if (streamFormat !== 'acp-json-rpc' &&
|
|
154
156
|
def.promptViaStdin !== true &&
|
|
155
157
|
def.promptViaFile !== true &&
|
|
@@ -171,8 +173,8 @@ export function assessAgentExecutorCompatibility(def) {
|
|
|
171
173
|
* @complexity O(1).
|
|
172
174
|
* @overallScore 100/100
|
|
173
175
|
*/
|
|
174
|
-
export function isAgentExecutorSupported(def) {
|
|
175
|
-
return assessAgentExecutorCompatibility(def).supported;
|
|
176
|
+
export function isAgentExecutorSupported({ def }) {
|
|
177
|
+
return assessAgentExecutorCompatibility({ def: def }).supported;
|
|
176
178
|
}
|
|
177
179
|
/**
|
|
178
180
|
* Selects and constructs the real stream-parser handler for a supported
|
|
@@ -194,13 +196,13 @@ export function isAgentExecutorSupported(def) {
|
|
|
194
196
|
function createStreamHandlerForDef(def, streamFormat, onEvent) {
|
|
195
197
|
switch (streamFormat) {
|
|
196
198
|
case 'claude-stream-json':
|
|
197
|
-
return createClaudeStreamHandler(onEvent);
|
|
199
|
+
return createClaudeStreamHandler({ onEvent: onEvent });
|
|
198
200
|
case 'copilot-stream-json':
|
|
199
|
-
return createCopilotStreamHandler(onEvent);
|
|
201
|
+
return createCopilotStreamHandler({ onEvent: onEvent });
|
|
200
202
|
case 'qoder-stream-json':
|
|
201
|
-
return createQoderStreamHandler(onEvent);
|
|
203
|
+
return createQoderStreamHandler({ onEvent: onEvent });
|
|
202
204
|
case 'json-event-stream':
|
|
203
|
-
return createJsonEventStreamHandler(def.eventParser ?? '', onEvent);
|
|
205
|
+
return createJsonEventStreamHandler({ kind: def.eventParser ?? '', onEvent: onEvent });
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
208
|
function isRecord(value) {
|
|
@@ -224,7 +226,7 @@ function asOptionalNumber(value) {
|
|
|
224
226
|
* original "omit the whole `usage` field rather than emit an empty object" behavior.
|
|
225
227
|
* @complexity O(1).
|
|
226
228
|
*/
|
|
227
|
-
export function extractUsageTokens(rawUsage) {
|
|
229
|
+
export function extractUsageTokens({ rawUsage }) {
|
|
228
230
|
if (!rawUsage)
|
|
229
231
|
return undefined;
|
|
230
232
|
const inputTokens = asOptionalNumber(rawUsage.input_tokens);
|
|
@@ -250,7 +252,7 @@ export function extractUsageTokens(rawUsage) {
|
|
|
250
252
|
*/
|
|
251
253
|
function translateUsagePayload(rawEvent) {
|
|
252
254
|
const rawUsage = isRecord(rawEvent.usage) ? rawEvent.usage : undefined;
|
|
253
|
-
const usage = extractUsageTokens(rawUsage);
|
|
255
|
+
const usage = extractUsageTokens({ rawUsage: rawUsage });
|
|
254
256
|
const costUsd = asOptionalNumber(rawEvent.costUsd);
|
|
255
257
|
const durationMs = asOptionalNumber(rawEvent.durationMs);
|
|
256
258
|
return {
|
|
@@ -260,7 +262,7 @@ function translateUsagePayload(rawEvent) {
|
|
|
260
262
|
...(durationMs !== undefined ? { durationMs } : {}),
|
|
261
263
|
};
|
|
262
264
|
}
|
|
263
|
-
export function translateStatusEvent(rawEvent) {
|
|
265
|
+
export function translateStatusEvent({ rawEvent }) {
|
|
264
266
|
const model = asOptionalString(rawEvent.model);
|
|
265
267
|
const ttftMs = asOptionalNumber(rawEvent.ttftMs);
|
|
266
268
|
const detail = asOptionalString(rawEvent.detail);
|
|
@@ -308,7 +310,7 @@ function translateToolInputDeltaEvent(rawEvent) {
|
|
|
308
310
|
},
|
|
309
311
|
};
|
|
310
312
|
}
|
|
311
|
-
export function translateToolResultEvent(rawEvent) {
|
|
313
|
+
export function translateToolResultEvent({ rawEvent }) {
|
|
312
314
|
const isError = typeof rawEvent.isError === 'boolean' ? rawEvent.isError : undefined;
|
|
313
315
|
return {
|
|
314
316
|
kind: 'agent',
|
|
@@ -326,7 +328,7 @@ function translateUsageEvent(rawEvent) {
|
|
|
326
328
|
function translateRawEvent(rawEvent) {
|
|
327
329
|
return { kind: 'agent', payload: { type: 'raw', line: asString(rawEvent.line) } };
|
|
328
330
|
}
|
|
329
|
-
export function translateErrorEvent(rawEvent) {
|
|
331
|
+
export function translateErrorEvent({ rawEvent }) {
|
|
330
332
|
const code = asOptionalString(rawEvent.code);
|
|
331
333
|
const message = asString(rawEvent.message, 'Unknown agent error');
|
|
332
334
|
return {
|
|
@@ -334,7 +336,7 @@ export function translateErrorEvent(rawEvent) {
|
|
|
334
336
|
payload: { message, ...(code !== undefined ? { error: { code, message } } : {}) },
|
|
335
337
|
};
|
|
336
338
|
}
|
|
337
|
-
export function translateTurnEndEvent(rawEvent) {
|
|
339
|
+
export function translateTurnEndEvent({ rawEvent }) {
|
|
338
340
|
// Claude-specific per-turn boundary. Not forwarded as an 'agent'
|
|
339
341
|
// event (no RunAgentPayload variant represents it) — run() reacts to
|
|
340
342
|
// it directly to close stdin (or, for gap 3, decide whether to inject
|
|
@@ -350,17 +352,17 @@ export function translateTurnEndEvent(rawEvent) {
|
|
|
350
352
|
* `translateAgentRuntimeEvent` itself is just a lookup plus the two upfront guards.
|
|
351
353
|
*/
|
|
352
354
|
const EVENT_TYPE_TRANSLATORS = {
|
|
353
|
-
status: translateStatusEvent,
|
|
355
|
+
status: rawEvent => translateStatusEvent({ rawEvent }),
|
|
354
356
|
text_delta: translateTextDeltaEvent,
|
|
355
357
|
thinking_start: translateThinkingStartEvent,
|
|
356
358
|
thinking_delta: translateThinkingDeltaEvent,
|
|
357
359
|
tool_use: translateToolUseEvent,
|
|
358
360
|
tool_input_delta: translateToolInputDeltaEvent,
|
|
359
|
-
tool_result: translateToolResultEvent,
|
|
361
|
+
tool_result: rawEvent => translateToolResultEvent({ rawEvent }),
|
|
360
362
|
usage: translateUsageEvent,
|
|
361
363
|
raw: translateRawEvent,
|
|
362
|
-
error: translateErrorEvent,
|
|
363
|
-
turn_end: translateTurnEndEvent,
|
|
364
|
+
error: rawEvent => translateErrorEvent({ rawEvent }),
|
|
365
|
+
turn_end: rawEvent => translateTurnEndEvent({ rawEvent }),
|
|
364
366
|
};
|
|
365
367
|
/**
|
|
366
368
|
* Narrows one parser-emitted `{type, ...}` record into this engine's
|
|
@@ -384,7 +386,7 @@ const EVENT_TYPE_TRANSLATORS = {
|
|
|
384
386
|
* @complexity O(1) — one table lookup, no iteration.
|
|
385
387
|
* @overallScore 100/100
|
|
386
388
|
*/
|
|
387
|
-
export function translateAgentRuntimeEvent(rawEvent) {
|
|
389
|
+
export function translateAgentRuntimeEvent({ rawEvent }) {
|
|
388
390
|
if (!isRecord(rawEvent) || typeof rawEvent.type !== 'string') {
|
|
389
391
|
return { kind: 'ignored' };
|
|
390
392
|
}
|
|
@@ -394,7 +396,7 @@ export function translateAgentRuntimeEvent(rawEvent) {
|
|
|
394
396
|
/** Thrown by `AgentExecutor.run()` on every failure path — never a bare `Error`, so callers can branch on `.code` instead of parsing `.message`. */
|
|
395
397
|
export class AgentExecutorError extends Error {
|
|
396
398
|
code;
|
|
397
|
-
constructor(code, message) {
|
|
399
|
+
constructor({ code, message }) {
|
|
398
400
|
super(message);
|
|
399
401
|
this.name = 'AgentExecutorError';
|
|
400
402
|
this.code = code;
|
|
@@ -435,7 +437,8 @@ const BASELINE_AGENT_ENV_KEYS = [
|
|
|
435
437
|
// run /login" despite real credentials existing on disk/keychain. Confirmed by bisection against
|
|
436
438
|
// a real authenticated `claude` install: `BASELINE_AGENT_ENV_KEYS` alone fails, adding back every
|
|
437
439
|
// `CLAUDE_CODE_*`/`CLAUDECODE` var still fails, `LOGNAME`/`SSH_AUTH_SOCK` alone still fail, but
|
|
438
|
-
// `USER` alone flips it to success.
|
|
440
|
+
// `USER` alone flips it to success. Login lookup requires the user identity
|
|
441
|
+
// as well as the home directory.
|
|
439
442
|
'USER',
|
|
440
443
|
'SystemRoot', 'windir', 'ComSpec', 'PATHEXT', // Windows-only; harmless no-ops elsewhere
|
|
441
444
|
];
|
|
@@ -485,13 +488,13 @@ async function terminateChildTree(deps, child) {
|
|
|
485
488
|
if (child.pid == null)
|
|
486
489
|
return;
|
|
487
490
|
const processes = await deps.listProcessSnapshots();
|
|
488
|
-
const pids = deps.collectProcessTreePids(processes, [child.pid]);
|
|
489
|
-
await deps.stopProcesses(pids);
|
|
491
|
+
const pids = deps.collectProcessTreePids({ processes, rootPids: [child.pid] });
|
|
492
|
+
await deps.stopProcesses({ pids });
|
|
490
493
|
}
|
|
491
494
|
/** Default sink when a host does not supply `onCleanupFailure`: still observable, never silent. Redacted per SEC-007 (a spawn/permission error can embed paths/host detail). */
|
|
492
495
|
function defaultCleanupFailureSink(context) {
|
|
493
496
|
// eslint-disable-next-line no-console
|
|
494
|
-
console.error(`[@jini-ai/daemon] agent-executor: process-tree cleanup failed for run "${context.runId}" (${context.phase}, pid=${context.pid})`, redactSecrets(errorMessage(context.error)));
|
|
497
|
+
console.error(`[@jini-ai/daemon] agent-executor: process-tree cleanup failed for run "${context.runId}" (${context.phase}, pid=${context.pid})`, redactSecrets({ input: errorMessage(context.error) }));
|
|
495
498
|
}
|
|
496
499
|
/**
|
|
497
500
|
* Reports a contained post-close failure through the host's sink, absorbing a throwing sink.
|
|
@@ -599,6 +602,31 @@ function receivedJournalEntry(channel, content) {
|
|
|
599
602
|
return { content, provenance: { source: 'agent', channel }, trust: 'untrusted' };
|
|
600
603
|
}
|
|
601
604
|
const JINI_MCP_SERVER_KEY = 'jini';
|
|
605
|
+
/** The run-error code a run gets when its CLI reports the injected `jini` bridge as anything but connected. */
|
|
606
|
+
export const MCP_BRIDGE_UNAVAILABLE = 'MCP_BRIDGE_UNAVAILABLE';
|
|
607
|
+
/**
|
|
608
|
+
* Judges a CLI's startup report of the injected bridge. Returns the bridge's status (`'missing'`
|
|
609
|
+
* when the list omits it) when it is not `'connected'`, and `undefined` when it is connected or
|
|
610
|
+
* when `rawEvent` is not an init status carrying an MCP server list (nothing to judge).
|
|
611
|
+
*
|
|
612
|
+
* `'pending'` counts as a failure on purpose: the CLI decides which MCP tools a session has at
|
|
613
|
+
* startup, so a bridge that is not connected by then never joins this run.
|
|
614
|
+
* @param rawEvent - One parsed stream event, as `@jini-ai/agent-runtime`'s parser emits it.
|
|
615
|
+
* @complexity O(n) in the number of reported servers.
|
|
616
|
+
*/
|
|
617
|
+
export function unavailableJiniBridgeStatus({ rawEvent }) {
|
|
618
|
+
if (!isRecord(rawEvent) || rawEvent.type !== 'status' || rawEvent.label !== 'initializing')
|
|
619
|
+
return undefined;
|
|
620
|
+
if (!Array.isArray(rawEvent.mcpServers))
|
|
621
|
+
return undefined;
|
|
622
|
+
const bridge = rawEvent.mcpServers.find((server) => isRecord(server) && server.name === JINI_MCP_SERVER_KEY);
|
|
623
|
+
const status = isRecord(bridge) ? String(bridge.status) : 'missing';
|
|
624
|
+
return status === 'connected' ? undefined : status;
|
|
625
|
+
}
|
|
626
|
+
/** The user-readable reason a run stopped because its bridge did not connect. */
|
|
627
|
+
function bridgeUnavailableMessage(status) {
|
|
628
|
+
return `The assistant's tools did not load (MCP bridge "${JINI_MCP_SERVER_KEY}" status: ${status}). The run was stopped instead of continuing without them.`;
|
|
629
|
+
}
|
|
602
630
|
/**
|
|
603
631
|
* Builds this run's `mcpServers.jini` entry — pure and synchronous, so every field mapping is
|
|
604
632
|
* directly assertable without touching the filesystem. The credential arrives already resolved:
|
|
@@ -606,17 +634,19 @@ const JINI_MCP_SERVER_KEY = 'jini';
|
|
|
606
634
|
* `writeMcpJsonForRun`'s job, which keeps the effect out of this function.
|
|
607
635
|
*
|
|
608
636
|
* @param runId - The run this entry scopes its child to.
|
|
609
|
-
* @param options - `command`/`args`/`daemonUrl` from the host's injection options.
|
|
637
|
+
* @param options - `command`/`args`/`daemonUrl`/`env` from the host's injection options.
|
|
610
638
|
* @param credential - The already-resolved bearer token, or `undefined` to omit `JINI_DAEMON_TOKEN`
|
|
611
639
|
* entirely. Omitting produces byte-identical output to before this parameter existed.
|
|
612
640
|
* @complexity O(1).
|
|
613
641
|
* @overallScore 100/100
|
|
614
642
|
*/
|
|
615
|
-
export function buildMcpJsonServerEntry(runId, options, credential) {
|
|
643
|
+
export function buildMcpJsonServerEntry({ runId, options }, { credential } = {}) {
|
|
644
|
+
const hostEnv = Object.entries(options.env ?? {}).filter(([key]) => !key.startsWith('JINI_'));
|
|
616
645
|
return {
|
|
617
646
|
command: options.command,
|
|
618
647
|
args: options.args !== undefined ? [...options.args] : [],
|
|
619
648
|
env: {
|
|
649
|
+
...Object.fromEntries(hostEnv),
|
|
620
650
|
JINI_RUN_ID: runId,
|
|
621
651
|
JINI_DAEMON_URL: options.daemonUrl,
|
|
622
652
|
...(credential !== undefined ? { JINI_DAEMON_TOKEN: credential } : {}),
|
|
@@ -634,7 +664,7 @@ export function buildMcpJsonServerEntry(runId, options, credential) {
|
|
|
634
664
|
* @complexity O(1) plus `JSON.parse`/`JSON.stringify`'s own cost on a small config file.
|
|
635
665
|
* @overallScore 100/100
|
|
636
666
|
*/
|
|
637
|
-
export function mergeMcpJsonContent(existingRaw, serverEntry) {
|
|
667
|
+
export function mergeMcpJsonContent({ existingRaw, serverEntry }) {
|
|
638
668
|
let doc = {};
|
|
639
669
|
if (existingRaw !== undefined) {
|
|
640
670
|
try {
|
|
@@ -671,7 +701,7 @@ export function mergeMcpJsonContent(existingRaw, serverEntry) {
|
|
|
671
701
|
* @complexity O(1).
|
|
672
702
|
* @overallScore 100/100
|
|
673
703
|
*/
|
|
674
|
-
export function buildAcpMcpBridgeServers(entry) {
|
|
704
|
+
export function buildAcpMcpBridgeServers({ entry }) {
|
|
675
705
|
return [
|
|
676
706
|
{
|
|
677
707
|
type: 'stdio',
|
|
@@ -719,7 +749,7 @@ const ENV_CONTENT_VAR_BY_STRATEGY = {
|
|
|
719
749
|
* @complexity O(1) plus `JSON.parse`/`JSON.stringify` over a small config document.
|
|
720
750
|
* @overallScore 100/100
|
|
721
751
|
*/
|
|
722
|
-
export function mergeEnvContentMcpConfig(existingRaw, entry) {
|
|
752
|
+
export function mergeEnvContentMcpConfig({ existingRaw, entry }) {
|
|
723
753
|
let doc = {};
|
|
724
754
|
if (existingRaw !== undefined && existingRaw.length > 0) {
|
|
725
755
|
try {
|
|
@@ -780,7 +810,7 @@ export function mergeEnvContentMcpConfig(existingRaw, entry) {
|
|
|
780
810
|
* @complexity O(1) plus `JSON.parse`/`JSON.stringify` over a small config document.
|
|
781
811
|
* @overallScore 100/100
|
|
782
812
|
*/
|
|
783
|
-
export function mergeEnvContentInstructions(existingRaw, instructionsFilePath) {
|
|
813
|
+
export function mergeEnvContentInstructions({ existingRaw, instructionsFilePath }) {
|
|
784
814
|
let doc = {};
|
|
785
815
|
if (existingRaw !== undefined && existingRaw.length > 0) {
|
|
786
816
|
try {
|
|
@@ -825,6 +855,10 @@ function escapeTomlBasicString(value) {
|
|
|
825
855
|
function tomlString(value) {
|
|
826
856
|
return `"${escapeTomlBasicString(value)}"`;
|
|
827
857
|
}
|
|
858
|
+
/** A TOML key: bare when TOML allows it, quoted otherwise — host-supplied bridge env names are not guaranteed bare-safe. */
|
|
859
|
+
function tomlKey(key) {
|
|
860
|
+
return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlString(key);
|
|
861
|
+
}
|
|
828
862
|
/**
|
|
829
863
|
* Codex's per-tool deadline for the Jini bridge, in seconds: `@jini-ai/mcp`'s
|
|
830
864
|
* `DEFAULT_DELEGATED_TOOL_TIMEOUT_MS` (6 min, mirrored because this package does not depend on
|
|
@@ -843,19 +877,19 @@ const CODEX_TOOL_TIMEOUT_SEC = 6 * 60 + 40;
|
|
|
843
877
|
* Confirmed against a real installed Codex CLI (0.151.0), not assumed from docs: round-tripping
|
|
844
878
|
* `codex mcp add <name> --env K=V -- <cmd> <args>` against a scratch `CODEX_HOME` and reading back
|
|
845
879
|
* `config.toml` produced exactly this shape (`command`/`args` as TOML strings/array in the main
|
|
846
|
-
* table, env vars in a nested `.env` table) — see `
|
|
880
|
+
* table, env vars in a nested `.env` table) — see `archived provenance ledger` for the transcript.
|
|
847
881
|
* @param entry - The shared bridge entry from {@link buildMcpJsonServerEntry}.
|
|
848
882
|
* @returns A TOML fragment with no leading/trailing blank-line padding — {@link buildCodexHomeConfigToml} owns spacing when combining it with existing content.
|
|
849
883
|
* @complexity O(n) in the number of argv/env entries.
|
|
850
884
|
* @overallScore 100/100
|
|
851
885
|
*/
|
|
852
|
-
export function buildCodexMcpServerToml(entry) {
|
|
886
|
+
export function buildCodexMcpServerToml({ entry }) {
|
|
853
887
|
const argsLiteral = entry.args.map(tomlString).join(', ');
|
|
854
888
|
const serverTable = `[mcp_servers.${JINI_MCP_SERVER_KEY}]\ncommand = ${tomlString(entry.command)}\nargs = [${argsLiteral}]\n` +
|
|
855
889
|
`tool_timeout_sec = ${CODEX_TOOL_TIMEOUT_SEC}\n`;
|
|
856
890
|
const envLines = Object.entries(entry.env)
|
|
857
891
|
.filter((pair) => typeof pair[1] === 'string')
|
|
858
|
-
.map(([key, value]) => `${key} = ${tomlString(value)}`);
|
|
892
|
+
.map(([key, value]) => `${tomlKey(key)} = ${tomlString(value)}`);
|
|
859
893
|
if (envLines.length === 0)
|
|
860
894
|
return serverTable;
|
|
861
895
|
return `${serverTable}\n[mcp_servers.${JINI_MCP_SERVER_KEY}.env]\n${envLines.join('\n')}\n`;
|
|
@@ -975,10 +1009,10 @@ function stripExistingJiniMcpServerTable(existingRaw) {
|
|
|
975
1009
|
* @returns The full text to write to the scratch `CODEX_HOME`'s `config.toml`.
|
|
976
1010
|
* @complexity O(n) in the existing config's length.
|
|
977
1011
|
*/
|
|
978
|
-
export function buildCodexHomeConfigToml(existingRaw, entry) {
|
|
1012
|
+
export function buildCodexHomeConfigToml({ existingRaw, entry }) {
|
|
979
1013
|
const base = stripExistingJiniMcpServerTable(existingRaw ?? '');
|
|
980
1014
|
const separator = base.length === 0 ? '' : base.endsWith('\n') ? '\n' : '\n\n';
|
|
981
|
-
return `${base}${separator}${buildCodexMcpServerToml(entry)}`;
|
|
1015
|
+
return `${base}${separator}${buildCodexMcpServerToml({ entry: entry })}`;
|
|
982
1016
|
}
|
|
983
1017
|
/**
|
|
984
1018
|
* Where `'codex-toml'` reads the operator's REAL Codex config from, to seed a run's scratch copy —
|
|
@@ -992,7 +1026,7 @@ export function buildCodexHomeConfigToml(existingRaw, entry) {
|
|
|
992
1026
|
* @complexity O(1).
|
|
993
1027
|
* @overallScore 100/100
|
|
994
1028
|
*/
|
|
995
|
-
export function resolveSourceCodexHomeDir(hostEnv) {
|
|
1029
|
+
export function resolveSourceCodexHomeDir({ hostEnv }) {
|
|
996
1030
|
const override = hostEnv.CODEX_HOME;
|
|
997
1031
|
return override !== undefined && override.trim().length > 0 ? override : join(homedir(), '.codex');
|
|
998
1032
|
}
|
|
@@ -1021,12 +1055,12 @@ export function buildMcpBridgeDelivery(input) {
|
|
|
1021
1055
|
const { cwd, runId, strategy, options, credential } = input;
|
|
1022
1056
|
if (options === undefined || strategy === undefined)
|
|
1023
1057
|
return null;
|
|
1024
|
-
const serverEntry = buildMcpJsonServerEntry(runId, options, credential);
|
|
1058
|
+
const serverEntry = buildMcpJsonServerEntry({ runId: runId, options: options }, credential === undefined ? {} : { credential });
|
|
1025
1059
|
switch (strategy) {
|
|
1026
1060
|
case 'claude-mcp-json':
|
|
1027
1061
|
return { kind: 'claude-mcp-json', mcpJsonPath: mcpJsonPathForRun(cwd, runId), serverEntry };
|
|
1028
1062
|
case 'acp-merge':
|
|
1029
|
-
return { kind: 'acp-merge', mcpServers: buildAcpMcpBridgeServers(serverEntry) };
|
|
1063
|
+
return { kind: 'acp-merge', mcpServers: buildAcpMcpBridgeServers({ entry: serverEntry }) };
|
|
1030
1064
|
case 'opencode-env-content':
|
|
1031
1065
|
case 'mimo-env-content':
|
|
1032
1066
|
return { kind: 'env-content', envVarName: ENV_CONTENT_VAR_BY_STRATEGY[strategy], serverEntry };
|
|
@@ -1036,13 +1070,13 @@ export function buildMcpBridgeDelivery(input) {
|
|
|
1036
1070
|
return { kind: 'env-passthrough', serverEntry };
|
|
1037
1071
|
}
|
|
1038
1072
|
}
|
|
1039
|
-
function defaultReadMcpJsonFile(path) {
|
|
1073
|
+
function defaultReadMcpJsonFile({ path }) {
|
|
1040
1074
|
return fsPromises.readFile(path, 'utf8');
|
|
1041
1075
|
}
|
|
1042
|
-
function defaultWriteMcpJsonFile(path, content) {
|
|
1076
|
+
function defaultWriteMcpJsonFile({ path, content }) {
|
|
1043
1077
|
return fsPromises.writeFile(path, content, 'utf8');
|
|
1044
1078
|
}
|
|
1045
|
-
function defaultRemoveMcpJsonFile(path) {
|
|
1079
|
+
function defaultRemoveMcpJsonFile({ path }) {
|
|
1046
1080
|
return fsPromises.rm(path, { force: true });
|
|
1047
1081
|
}
|
|
1048
1082
|
/**
|
|
@@ -1102,19 +1136,19 @@ async function writeMcpJsonForRun(cwd, delivery, options) {
|
|
|
1102
1136
|
const writeFileFn = options.writeFile ?? defaultWriteMcpJsonFile;
|
|
1103
1137
|
let existingRaw;
|
|
1104
1138
|
try {
|
|
1105
|
-
existingRaw = await readFileFn(join(cwd, '.mcp.json'));
|
|
1139
|
+
existingRaw = await readFileFn({ path: join(cwd, '.mcp.json') });
|
|
1106
1140
|
}
|
|
1107
1141
|
catch {
|
|
1108
1142
|
// No existing file (ENOENT — the common case) or unreadable for any other reason: both
|
|
1109
1143
|
// degrade to "start fresh", matching mergeMcpJsonContent's own doc.
|
|
1110
1144
|
existingRaw = undefined;
|
|
1111
1145
|
}
|
|
1112
|
-
await writeFileFn(delivery.mcpJsonPath, mergeMcpJsonContent(existingRaw, delivery.serverEntry));
|
|
1146
|
+
await writeFileFn({ path: delivery.mcpJsonPath, content: mergeMcpJsonContent({ existingRaw: existingRaw, serverEntry: delivery.serverEntry }) });
|
|
1113
1147
|
}
|
|
1114
|
-
function defaultMkdtempCodexHome(prefix) {
|
|
1148
|
+
function defaultMkdtempCodexHome({ prefix }) {
|
|
1115
1149
|
return fsPromises.mkdtemp(join(tmpdir(), prefix));
|
|
1116
1150
|
}
|
|
1117
|
-
function defaultRemoveCodexHomeDir(path) {
|
|
1151
|
+
function defaultRemoveCodexHomeDir({ path }) {
|
|
1118
1152
|
return fsPromises.rm(path, { recursive: true, force: true });
|
|
1119
1153
|
}
|
|
1120
1154
|
function resolveCodexHomeSeams(options) {
|
|
@@ -1161,21 +1195,21 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
|
1161
1195
|
// too, not just path separators, so a run id like `../../etc/evil` cannot leave even a cosmetic
|
|
1162
1196
|
// `..` substring in the mkdtemp prefix.
|
|
1163
1197
|
const safeRunId = runId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 80) || 'run';
|
|
1164
|
-
const dir = await seams.mkdtemp(`jini-codex-home-${safeRunId}-`);
|
|
1198
|
+
const dir = await seams.mkdtemp({ prefix: `jini-codex-home-${safeRunId}-` });
|
|
1165
1199
|
try {
|
|
1166
1200
|
let existingConfigRaw;
|
|
1167
1201
|
try {
|
|
1168
|
-
existingConfigRaw = await seams.readFile(join(sourceCodexHomeDir, 'config.toml'));
|
|
1202
|
+
existingConfigRaw = await seams.readFile({ path: join(sourceCodexHomeDir, 'config.toml') });
|
|
1169
1203
|
}
|
|
1170
1204
|
catch {
|
|
1171
1205
|
// No config yet (fresh Codex install) or unreadable — start from just this run's block,
|
|
1172
1206
|
// matching writeMcpJsonForRun's identical "missing file" handling.
|
|
1173
1207
|
existingConfigRaw = undefined;
|
|
1174
1208
|
}
|
|
1175
|
-
await seams.writeFile(join(dir, 'config.toml'), buildCodexHomeConfigToml(existingConfigRaw, entry));
|
|
1209
|
+
await seams.writeFile({ path: join(dir, 'config.toml'), content: buildCodexHomeConfigToml({ existingRaw: existingConfigRaw, entry: entry }) });
|
|
1176
1210
|
try {
|
|
1177
|
-
const authRaw = await seams.readFile(join(sourceCodexHomeDir, 'auth.json'));
|
|
1178
|
-
await seams.writeFile(join(dir, 'auth.json'), authRaw);
|
|
1211
|
+
const authRaw = await seams.readFile({ path: join(sourceCodexHomeDir, 'auth.json') });
|
|
1212
|
+
await seams.writeFile({ path: join(dir, 'auth.json'), content: authRaw });
|
|
1179
1213
|
}
|
|
1180
1214
|
catch {
|
|
1181
1215
|
// No stored login (or unreadable) — the spawned CLI runs unauthenticated. Confirmed above:
|
|
@@ -1183,7 +1217,7 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
|
1183
1217
|
}
|
|
1184
1218
|
}
|
|
1185
1219
|
catch (err) {
|
|
1186
|
-
await seams.removeDir(dir).catch(() => {
|
|
1220
|
+
await seams.removeDir({ path: dir }).catch(() => {
|
|
1187
1221
|
// Best-effort only — the original error below is what the caller must see either way.
|
|
1188
1222
|
});
|
|
1189
1223
|
throw err;
|
|
@@ -1191,14 +1225,14 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
|
1191
1225
|
return {
|
|
1192
1226
|
path: dir,
|
|
1193
1227
|
cleanup: async () => {
|
|
1194
|
-
await seams.removeDir(dir);
|
|
1228
|
+
await seams.removeDir({ path: dir });
|
|
1195
1229
|
},
|
|
1196
1230
|
};
|
|
1197
1231
|
}
|
|
1198
|
-
function defaultMkdtempClaudeConfigDir(prefix) {
|
|
1232
|
+
function defaultMkdtempClaudeConfigDir({ prefix }) {
|
|
1199
1233
|
return fsPromises.mkdtemp(join(tmpdir(), prefix));
|
|
1200
1234
|
}
|
|
1201
|
-
function defaultRemoveClaudeConfigDir(path) {
|
|
1235
|
+
function defaultRemoveClaudeConfigDir({ path }) {
|
|
1202
1236
|
return fsPromises.rm(path, { recursive: true, force: true });
|
|
1203
1237
|
}
|
|
1204
1238
|
function resolveClaudeConfigDirSeams(options) {
|
|
@@ -1221,7 +1255,7 @@ function resolveClaudeConfigDirSeams(options) {
|
|
|
1221
1255
|
* default, `~/.claude`.
|
|
1222
1256
|
* @complexity O(1).
|
|
1223
1257
|
*/
|
|
1224
|
-
export function resolveSourceClaudeConfigDir(hostEnv) {
|
|
1258
|
+
export function resolveSourceClaudeConfigDir({ hostEnv }) {
|
|
1225
1259
|
const override = hostEnv.CLAUDE_CONFIG_DIR;
|
|
1226
1260
|
return override !== undefined && override.trim().length > 0 ? override : join(homedir(), '.claude');
|
|
1227
1261
|
}
|
|
@@ -1274,12 +1308,12 @@ export function resolveSourceClaudeConfigDir(hostEnv) {
|
|
|
1274
1308
|
*/
|
|
1275
1309
|
async function prepareClaudeConfigDirForRun(runId, sourceConfigDir, seams) {
|
|
1276
1310
|
const safeRunId = runId.replace(/[^A-Za-z0-9_-]/g, '-').slice(0, 80) || 'run';
|
|
1277
|
-
const dir = await seams.mkdtemp(`jini-claude-config-${safeRunId}-`);
|
|
1311
|
+
const dir = await seams.mkdtemp({ prefix: `jini-claude-config-${safeRunId}-` });
|
|
1278
1312
|
try {
|
|
1279
1313
|
const credentialsPath = join(sourceConfigDir, '.credentials.json');
|
|
1280
1314
|
let credentialsRaw;
|
|
1281
1315
|
try {
|
|
1282
|
-
credentialsRaw = await seams.readFile(credentialsPath);
|
|
1316
|
+
credentialsRaw = await seams.readFile({ path: credentialsPath });
|
|
1283
1317
|
}
|
|
1284
1318
|
catch {
|
|
1285
1319
|
// No file-based credential to copy (the common macOS-Keychain-only case) — see this
|
|
@@ -1287,11 +1321,11 @@ async function prepareClaudeConfigDirForRun(runId, sourceConfigDir, seams) {
|
|
|
1287
1321
|
credentialsRaw = undefined;
|
|
1288
1322
|
}
|
|
1289
1323
|
if (credentialsRaw !== undefined) {
|
|
1290
|
-
await seams.writeFile(join(dir, '.credentials.json'), credentialsRaw);
|
|
1324
|
+
await seams.writeFile({ path: join(dir, '.credentials.json'), content: credentialsRaw });
|
|
1291
1325
|
}
|
|
1292
1326
|
}
|
|
1293
1327
|
catch (err) {
|
|
1294
|
-
await seams.removeDir(dir).catch(() => {
|
|
1328
|
+
await seams.removeDir({ path: dir }).catch(() => {
|
|
1295
1329
|
// Best-effort only — the original error below is what the caller must see either way.
|
|
1296
1330
|
});
|
|
1297
1331
|
throw err;
|
|
@@ -1299,7 +1333,7 @@ async function prepareClaudeConfigDirForRun(runId, sourceConfigDir, seams) {
|
|
|
1299
1333
|
return {
|
|
1300
1334
|
path: dir,
|
|
1301
1335
|
cleanup: async () => {
|
|
1302
|
-
await seams.removeDir(dir);
|
|
1336
|
+
await seams.removeDir({ path: dir });
|
|
1303
1337
|
},
|
|
1304
1338
|
};
|
|
1305
1339
|
}
|
|
@@ -1357,7 +1391,7 @@ function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
|
|
|
1357
1391
|
* **Deliberately un-hygiened for v1**: no ANSI/terminal-control-sequence
|
|
1358
1392
|
* stripping is applied (there is no Jini equivalent of OD's
|
|
1359
1393
|
* `TerminalControlSequenceStripper` yet) — a documented decision, not an
|
|
1360
|
-
* oversight; see `packages/daemon/
|
|
1394
|
+
* oversight; see `packages/daemon/archived provenance ledger`'s 2026-07-21 addition for
|
|
1361
1395
|
* the reasoning.
|
|
1362
1396
|
*
|
|
1363
1397
|
* *When* those chunks leave is the def's call, via `def.stdoutPolicy`:
|
|
@@ -1405,6 +1439,10 @@ function wireChildLifecycle(ctx) {
|
|
|
1405
1439
|
// consumed by a turn-end injection decision. See `ContinuationOptions`'s doc for why this is
|
|
1406
1440
|
// only ever acted on when a host has explicitly allowlisted the tool's name.
|
|
1407
1441
|
let pendingToolUse;
|
|
1442
|
+
// Bridge guard (see `expectsJiniBridge`): checked on the first init frame only. Once the bridge is
|
|
1443
|
+
// found unavailable, the child is being stopped and nothing it still prints is forwarded.
|
|
1444
|
+
let bridgeChecked = !ctx.expectsJiniBridge;
|
|
1445
|
+
let bridgeUnavailable = false;
|
|
1408
1446
|
// `def.stdoutPolicy` read once, up front, so the per-chunk handler below is a single boolean
|
|
1409
1447
|
// test rather than a repeated union narrowing. `undefined` (every def but antigravity) means
|
|
1410
1448
|
// live — see this function's own doc.
|
|
@@ -1454,7 +1492,7 @@ function wireChildLifecycle(ctx) {
|
|
|
1454
1492
|
});
|
|
1455
1493
|
stdin.write(`${line}\n`, 'utf8');
|
|
1456
1494
|
if (journal)
|
|
1457
|
-
enqueueEmit(() => journal.record(runId, sentJournalEntry(content)));
|
|
1495
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
|
|
1458
1496
|
}
|
|
1459
1497
|
/**
|
|
1460
1498
|
* Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
|
|
@@ -1467,7 +1505,7 @@ function wireChildLifecycle(ctx) {
|
|
|
1467
1505
|
const shouldInject = stopReason === 'tool_use' &&
|
|
1468
1506
|
toolUse !== undefined &&
|
|
1469
1507
|
continuation !== undefined &&
|
|
1470
|
-
resolveContinuationTransport(def) === 'stdin-injection' &&
|
|
1508
|
+
resolveContinuationTransport({ def: def }) === 'stdin-injection' &&
|
|
1471
1509
|
continuation.autonomousToolNames.has(toolUse.name);
|
|
1472
1510
|
if (!shouldInject) {
|
|
1473
1511
|
closeStdinOnce();
|
|
@@ -1490,40 +1528,50 @@ function wireChildLifecycle(ctx) {
|
|
|
1490
1528
|
// the CPU-starved-and-silent condition the watchdog exists to catch. Resumed in `finally` so a
|
|
1491
1529
|
// genuinely stalled stretch *after* this tool settles is still caught — see
|
|
1492
1530
|
// `RunLifecycle.suspendSlowRunNotice`'s own doc.
|
|
1493
|
-
lifecycle.suspendSlowRunNotice(runId);
|
|
1531
|
+
lifecycle.suspendSlowRunNotice({ runId: runId });
|
|
1494
1532
|
try {
|
|
1495
|
-
const result = await continuation.toolExecutor.execute(continuation.principal, run, toolUse.name, toolUse.input);
|
|
1496
|
-
content = resultContent(result);
|
|
1533
|
+
const result = await continuation.toolExecutor.execute({ principal: continuation.principal, run: run, toolId: toolUse.name, input: toolUse.input });
|
|
1534
|
+
content = resultContent({ result: result });
|
|
1497
1535
|
isError = result.status !== 'completed';
|
|
1498
|
-
media = extractResultMedia(result.output).media;
|
|
1536
|
+
media = extractResultMedia({ output: result.output }).media;
|
|
1499
1537
|
}
|
|
1500
1538
|
catch (error) {
|
|
1501
1539
|
content = errorMessage(error);
|
|
1502
1540
|
isError = true;
|
|
1503
1541
|
}
|
|
1504
1542
|
finally {
|
|
1505
|
-
lifecycle.resumeSlowRunNotice(runId);
|
|
1543
|
+
lifecycle.resumeSlowRunNotice({ runId: runId });
|
|
1506
1544
|
}
|
|
1507
|
-
await lifecycle.emit(runId, {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1545
|
+
await lifecycle.emit({ runId: runId, input: {
|
|
1546
|
+
event: 'agent',
|
|
1547
|
+
data: {
|
|
1548
|
+
type: 'tool_result',
|
|
1549
|
+
toolUseId: toolUse.id,
|
|
1550
|
+
content,
|
|
1551
|
+
...(isError ? { isError: true } : {}),
|
|
1552
|
+
...(media.length > 0 ? { media } : {}),
|
|
1553
|
+
},
|
|
1554
|
+
} });
|
|
1517
1555
|
injectToolResultLine(toolUse.id, content, isError);
|
|
1518
1556
|
});
|
|
1519
1557
|
}
|
|
1520
1558
|
const streamHandler = streamFormat === 'plain'
|
|
1521
1559
|
? null
|
|
1522
1560
|
: createStreamHandlerForDef(def, streamFormat, (rawEvent) => {
|
|
1523
|
-
|
|
1561
|
+
if (bridgeUnavailable)
|
|
1562
|
+
return;
|
|
1563
|
+
const translation = translateAgentRuntimeEvent({ rawEvent: rawEvent });
|
|
1524
1564
|
if (translation.kind === 'agent') {
|
|
1525
1565
|
if (translation.sessionId !== undefined)
|
|
1526
1566
|
capturedSessionId = translation.sessionId;
|
|
1567
|
+
if (!bridgeChecked && translation.payload.type === 'status' && translation.payload.label === 'initializing') {
|
|
1568
|
+
bridgeChecked = true;
|
|
1569
|
+
const bridgeStatus = unavailableJiniBridgeStatus({ rawEvent: rawEvent });
|
|
1570
|
+
if (bridgeStatus !== undefined) {
|
|
1571
|
+
stopForUnavailableBridge(translation.payload, bridgeStatus);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1527
1575
|
if (translation.payload.type === 'tool_use') {
|
|
1528
1576
|
pendingToolUse = { id: translation.payload.id, name: translation.payload.name, input: translation.payload.input };
|
|
1529
1577
|
toolCallSeen = true;
|
|
@@ -1532,15 +1580,28 @@ function wireChildLifecycle(ctx) {
|
|
|
1532
1580
|
translation.payload.delta.length > 0) {
|
|
1533
1581
|
userVisibleOutputSeen = true;
|
|
1534
1582
|
}
|
|
1535
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: translation.payload }));
|
|
1583
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
|
|
1536
1584
|
}
|
|
1537
1585
|
else if (translation.kind === 'error') {
|
|
1538
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'error', data: translation.payload }));
|
|
1586
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
|
|
1539
1587
|
}
|
|
1540
1588
|
else if (translation.kind === 'turn-end') {
|
|
1541
1589
|
handleTurnEnd(translation.stopReason);
|
|
1542
1590
|
}
|
|
1543
1591
|
});
|
|
1592
|
+
/**
|
|
1593
|
+
* Ends a run whose CLI started without the `jini` bridge: emits the init status and a
|
|
1594
|
+
* {@link MCP_BRIDGE_UNAVAILABLE} error, then stops the child's process tree before the model can
|
|
1595
|
+
* answer with no host tools. The close handler then finishes the run `'failed'` whatever exit code
|
|
1596
|
+
* the child reports.
|
|
1597
|
+
*/
|
|
1598
|
+
function stopForUnavailableBridge(initStatus, bridgeStatus) {
|
|
1599
|
+
bridgeUnavailable = true;
|
|
1600
|
+
const message = bridgeUnavailableMessage(bridgeStatus);
|
|
1601
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: initStatus } }));
|
|
1602
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: { message, error: { code: MCP_BRIDGE_UNAVAILABLE, message } } } }));
|
|
1603
|
+
void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
|
|
1604
|
+
}
|
|
1544
1605
|
/**
|
|
1545
1606
|
* Emits the accumulated `'until-close'` stdout — sanitized — as exactly one raw `'stdout'` echo
|
|
1546
1607
|
* plus one `text_delta`, through the same `enqueueEmit` FIFO queue every other emit uses, so the
|
|
@@ -1555,7 +1616,7 @@ function wireChildLifecycle(ctx) {
|
|
|
1555
1616
|
function flushBufferedStdout() {
|
|
1556
1617
|
if (bufferedStdout.length === 0 && droppedStdoutBytes === 0)
|
|
1557
1618
|
return;
|
|
1558
|
-
const safe = sanitizeBufferedStdout ? sanitizeBufferedStdout(bufferedStdout) : bufferedStdout;
|
|
1619
|
+
const safe = sanitizeBufferedStdout ? sanitizeBufferedStdout({ fullText: bufferedStdout }) : bufferedStdout;
|
|
1559
1620
|
bufferedStdout = '';
|
|
1560
1621
|
bufferedStdoutBytes = 0;
|
|
1561
1622
|
const text = droppedStdoutBytes > 0
|
|
@@ -1564,13 +1625,13 @@ function wireChildLifecycle(ctx) {
|
|
|
1564
1625
|
droppedStdoutBytes = 0;
|
|
1565
1626
|
if (text.length === 0)
|
|
1566
1627
|
return;
|
|
1567
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
|
|
1568
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: { type: 'text_delta', delta: text } }));
|
|
1628
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
|
|
1629
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'text_delta', delta: text } } }));
|
|
1569
1630
|
}
|
|
1570
1631
|
child.stdout?.on('data', (chunk) => {
|
|
1571
1632
|
const text = chunk.toString('utf8');
|
|
1572
1633
|
if (journal)
|
|
1573
|
-
enqueueEmit(() => journal.record(runId, receivedJournalEntry('stdout', text)));
|
|
1634
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stdout', text) }));
|
|
1574
1635
|
if (streamFormat === 'plain') {
|
|
1575
1636
|
if (text.length > 0)
|
|
1576
1637
|
userVisibleOutputSeen = true;
|
|
@@ -1592,20 +1653,20 @@ function wireChildLifecycle(ctx) {
|
|
|
1592
1653
|
bufferedStdoutBytes += chunkBytes;
|
|
1593
1654
|
return;
|
|
1594
1655
|
}
|
|
1595
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
|
|
1596
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: { type: 'text_delta', delta: text } }));
|
|
1656
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
|
|
1657
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'text_delta', delta: text } } }));
|
|
1597
1658
|
return;
|
|
1598
1659
|
}
|
|
1599
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
|
|
1660
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
|
|
1600
1661
|
// Non-null: `streamHandler` is only ever null when `streamFormat === 'plain'` (see its
|
|
1601
1662
|
// construction above), the branch this statement is provably unreachable from.
|
|
1602
|
-
streamHandler.feed(text);
|
|
1663
|
+
streamHandler.feed({ chunk: text });
|
|
1603
1664
|
});
|
|
1604
1665
|
child.stderr?.on('data', (chunk) => {
|
|
1605
1666
|
const text = chunk.toString('utf8');
|
|
1606
1667
|
if (journal)
|
|
1607
|
-
enqueueEmit(() => journal.record(runId, receivedJournalEntry('stderr', text)));
|
|
1608
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'stderr', data: { chunk: text } }));
|
|
1668
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stderr', text) }));
|
|
1669
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stderr', data: { chunk: text } } }));
|
|
1609
1670
|
});
|
|
1610
1671
|
// EPIPE-tolerant: a fast-exiting child that closes its stdin read end
|
|
1611
1672
|
// before every queued write lands must not crash the host process with
|
|
@@ -1617,10 +1678,10 @@ function wireChildLifecycle(ctx) {
|
|
|
1617
1678
|
// EventEmitter throws on an unheard 'error' otherwise. The real outcome
|
|
1618
1679
|
// is still decided by 'close' below.
|
|
1619
1680
|
child.on('error', () => { });
|
|
1620
|
-
const unsubscribeCancel = lifecycle.onCancelRequested(runId, () => {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1681
|
+
const unsubscribeCancel = lifecycle.onCancelRequested({ runId: runId, listener: () => {
|
|
1682
|
+
cancelRequested = true;
|
|
1683
|
+
void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
|
|
1684
|
+
} });
|
|
1624
1685
|
child.on('close', (code, signal) => {
|
|
1625
1686
|
void (async () => {
|
|
1626
1687
|
// Not wrapped in try/catch: all 4 supported parser factories'
|
|
@@ -1640,8 +1701,9 @@ function wireChildLifecycle(ctx) {
|
|
|
1640
1701
|
// Both of the next two steps are guarded: neither a failed cleanup nor a rejecting host
|
|
1641
1702
|
// classifier may prevent the terminal transition below — see each helper's own doc.
|
|
1642
1703
|
await cleanupStagedFilesSafely(ctx);
|
|
1643
|
-
const status = classifyRunCloseStatus({ cancelRequested, code, signal });
|
|
1644
|
-
|
|
1704
|
+
const status = bridgeUnavailable ? 'failed' : classifyRunCloseStatus({ cancelRequested, code }, { signal });
|
|
1705
|
+
// A bridge failure is not resumable: resuming would start the same CLI with the same bridge.
|
|
1706
|
+
const resumable = status === 'failed' && !bridgeUnavailable && classifyFailure !== undefined
|
|
1645
1707
|
? await classifyFailureSafely(ctx, classifyFailure, {
|
|
1646
1708
|
runId,
|
|
1647
1709
|
agentId: def.id,
|
|
@@ -1650,21 +1712,14 @@ function wireChildLifecycle(ctx) {
|
|
|
1650
1712
|
sideEffects: { userVisibleOutputSeen, toolCallSeen },
|
|
1651
1713
|
})
|
|
1652
1714
|
: false;
|
|
1653
|
-
await lifecycle.finish({
|
|
1654
|
-
runId,
|
|
1655
|
-
status,
|
|
1656
|
-
code,
|
|
1657
|
-
signal: signal ?? null,
|
|
1658
|
-
resumable,
|
|
1659
|
-
...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}),
|
|
1660
|
-
});
|
|
1715
|
+
await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
|
|
1661
1716
|
})();
|
|
1662
1717
|
});
|
|
1663
1718
|
return {
|
|
1664
1719
|
closeStdinOnce,
|
|
1665
1720
|
recordSentBytes(content) {
|
|
1666
1721
|
if (journal)
|
|
1667
|
-
enqueueEmit(() => journal.record(runId, sentJournalEntry(content)));
|
|
1722
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
|
|
1668
1723
|
},
|
|
1669
1724
|
};
|
|
1670
1725
|
}
|
|
@@ -1705,9 +1760,9 @@ function translateAcpError(payload) {
|
|
|
1705
1760
|
* @param sink - The driver-specific effects to apply.
|
|
1706
1761
|
* @complexity O(1).
|
|
1707
1762
|
*/
|
|
1708
|
-
export function applyAgentTranslationSideEffects(payload, sessionId, sink) {
|
|
1763
|
+
export function applyAgentTranslationSideEffects({ payload, sessionId, sink }) {
|
|
1709
1764
|
if (sessionId !== undefined)
|
|
1710
|
-
sink.onSessionId(sessionId);
|
|
1765
|
+
sink.onSessionId({ sessionId });
|
|
1711
1766
|
if (payload.type === 'tool_use') {
|
|
1712
1767
|
sink.onToolCall();
|
|
1713
1768
|
}
|
|
@@ -1747,23 +1802,23 @@ function wireAcpLifecycle(ctx) {
|
|
|
1747
1802
|
child.stdout?.on('data', (chunk) => {
|
|
1748
1803
|
const text = chunk.toString('utf8');
|
|
1749
1804
|
if (journal)
|
|
1750
|
-
enqueueEmit(() => journal.record(runId, receivedJournalEntry('stdout', text)));
|
|
1751
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
|
|
1805
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stdout', text) }));
|
|
1806
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
|
|
1752
1807
|
});
|
|
1753
1808
|
child.stderr?.on('data', (chunk) => {
|
|
1754
1809
|
const text = chunk.toString('utf8');
|
|
1755
1810
|
if (journal)
|
|
1756
|
-
enqueueEmit(() => journal.record(runId, receivedJournalEntry('stderr', text)));
|
|
1757
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'stderr', data: { chunk: text } }));
|
|
1811
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stderr', text) }));
|
|
1812
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stderr', data: { chunk: text } } }));
|
|
1758
1813
|
});
|
|
1759
1814
|
child.stdin?.on('error', () => { });
|
|
1760
1815
|
child.on('error', () => { });
|
|
1761
1816
|
let controller = null;
|
|
1762
|
-
const unsubscribeCancel = lifecycle.onCancelRequested(runId, () => {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1817
|
+
const unsubscribeCancel = lifecycle.onCancelRequested({ runId: runId, listener: () => {
|
|
1818
|
+
cancelRequested = true;
|
|
1819
|
+
controller?.abort();
|
|
1820
|
+
void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
|
|
1821
|
+
} });
|
|
1767
1822
|
child.on('close', (code, signal) => {
|
|
1768
1823
|
void (async () => {
|
|
1769
1824
|
await emitQueue;
|
|
@@ -1780,49 +1835,33 @@ function wireAcpLifecycle(ctx) {
|
|
|
1780
1835
|
sideEffects: { userVisibleOutputSeen, toolCallSeen },
|
|
1781
1836
|
})
|
|
1782
1837
|
: false;
|
|
1783
|
-
await lifecycle.finish({
|
|
1784
|
-
runId,
|
|
1785
|
-
status,
|
|
1786
|
-
code,
|
|
1787
|
-
signal: signal ?? null,
|
|
1788
|
-
resumable,
|
|
1789
|
-
...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}),
|
|
1790
|
-
});
|
|
1838
|
+
await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
|
|
1791
1839
|
})();
|
|
1792
1840
|
});
|
|
1793
|
-
controller = ctx.attachAcpSession({
|
|
1794
|
-
child,
|
|
1795
|
-
prompt: ctx.prompt,
|
|
1796
|
-
cwd: ctx.cwd,
|
|
1797
|
-
...(ctx.model !== undefined ? { model: ctx.model } : {}),
|
|
1798
|
-
...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}),
|
|
1799
|
-
...(ctx.envFormat !== undefined ? { envFormat: ctx.envFormat } : {}),
|
|
1800
|
-
// Spread-when-present rather than always: passing `mcpServers: []` is not the same as passing
|
|
1801
|
-
// nothing for every downstream ACP agent, and "no bridge configured" must stay byte-identical
|
|
1802
|
-
// to before this field existed.
|
|
1803
|
-
...(ctx.mcpServers !== undefined && ctx.mcpServers.length > 0 ? { mcpServers: [...ctx.mcpServers] } : {}),
|
|
1804
|
-
...(ctx.onPermissionRequest !== undefined ? { onPermissionRequest: ctx.onPermissionRequest } : {}),
|
|
1805
|
-
send(event, payload) {
|
|
1841
|
+
controller = ctx.attachAcpSession({ child, prompt: ctx.prompt, send({ event, payload }) {
|
|
1806
1842
|
if (event === 'agent') {
|
|
1807
|
-
const translation = translateAgentRuntimeEvent(payload);
|
|
1843
|
+
const translation = translateAgentRuntimeEvent({ rawEvent: payload });
|
|
1808
1844
|
if (translation.kind === 'agent') {
|
|
1809
|
-
applyAgentTranslationSideEffects(translation.payload, translation.sessionId, {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: translation.payload }));
|
|
1845
|
+
applyAgentTranslationSideEffects({ payload: translation.payload, sessionId: translation.sessionId, sink: {
|
|
1846
|
+
onSessionId: ({ sessionId }) => { capturedSessionId = sessionId; },
|
|
1847
|
+
onToolCall: () => { toolCallSeen = true; },
|
|
1848
|
+
onUserVisibleOutput: () => { userVisibleOutputSeen = true; },
|
|
1849
|
+
} });
|
|
1850
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
|
|
1815
1851
|
}
|
|
1816
1852
|
else if (translation.kind === 'error') {
|
|
1817
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'error', data: translation.payload }));
|
|
1853
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
|
|
1818
1854
|
}
|
|
1819
1855
|
return;
|
|
1820
1856
|
}
|
|
1821
1857
|
if (event === 'error') {
|
|
1822
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'error', data: translateAcpError(payload) }));
|
|
1858
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translateAcpError(payload) } }));
|
|
1823
1859
|
}
|
|
1824
|
-
},
|
|
1825
|
-
|
|
1860
|
+
} }, { cwd: ctx.cwd, ...(ctx.model !== undefined ? { model: ctx.model } : {}), ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}), ...(ctx.envFormat !== undefined ? { envFormat: ctx.envFormat } : {}),
|
|
1861
|
+
// Spread-when-present rather than always: passing `mcpServers: []` is not the same as passing
|
|
1862
|
+
// nothing for every downstream ACP agent, and "no bridge configured" must stay byte-identical
|
|
1863
|
+
// to before this field existed.
|
|
1864
|
+
...(ctx.mcpServers !== undefined && ctx.mcpServers.length > 0 ? { mcpServers: [...ctx.mcpServers] } : {}), ...(ctx.onPermissionRequest !== undefined ? { onPermissionRequest: ctx.onPermissionRequest } : {}) });
|
|
1826
1865
|
return controller;
|
|
1827
1866
|
}
|
|
1828
1867
|
/**
|
|
@@ -1867,23 +1906,23 @@ function wirePiRpcLifecycle(ctx) {
|
|
|
1867
1906
|
child.stdout?.on('data', (chunk) => {
|
|
1868
1907
|
const text = chunk.toString('utf8');
|
|
1869
1908
|
if (journal)
|
|
1870
|
-
enqueueEmit(() => journal.record(runId, receivedJournalEntry('stdout', text)));
|
|
1871
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'stdout', data: { chunk: text } }));
|
|
1909
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stdout', text) }));
|
|
1910
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
|
|
1872
1911
|
});
|
|
1873
1912
|
child.stderr?.on('data', (chunk) => {
|
|
1874
1913
|
const text = chunk.toString('utf8');
|
|
1875
1914
|
if (journal)
|
|
1876
|
-
enqueueEmit(() => journal.record(runId, receivedJournalEntry('stderr', text)));
|
|
1877
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'stderr', data: { chunk: text } }));
|
|
1915
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stderr', text) }));
|
|
1916
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stderr', data: { chunk: text } } }));
|
|
1878
1917
|
});
|
|
1879
1918
|
child.stdin?.on('error', () => { });
|
|
1880
1919
|
child.on('error', () => { });
|
|
1881
1920
|
let session = null;
|
|
1882
|
-
const unsubscribeCancel = lifecycle.onCancelRequested(runId, () => {
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1921
|
+
const unsubscribeCancel = lifecycle.onCancelRequested({ runId: runId, listener: () => {
|
|
1922
|
+
cancelRequested = true;
|
|
1923
|
+
session?.abort();
|
|
1924
|
+
void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
|
|
1925
|
+
} });
|
|
1887
1926
|
child.on('close', (code, signal) => {
|
|
1888
1927
|
void (async () => {
|
|
1889
1928
|
await emitQueue;
|
|
@@ -1900,25 +1939,11 @@ function wirePiRpcLifecycle(ctx) {
|
|
|
1900
1939
|
sideEffects: { userVisibleOutputSeen, toolCallSeen },
|
|
1901
1940
|
})
|
|
1902
1941
|
: false;
|
|
1903
|
-
await lifecycle.finish({
|
|
1904
|
-
runId,
|
|
1905
|
-
status,
|
|
1906
|
-
code,
|
|
1907
|
-
signal: signal ?? null,
|
|
1908
|
-
resumable,
|
|
1909
|
-
...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}),
|
|
1910
|
-
});
|
|
1942
|
+
await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
|
|
1911
1943
|
})();
|
|
1912
1944
|
});
|
|
1913
|
-
session = ctx.attachPiRpcSession({
|
|
1914
|
-
|
|
1915
|
-
prompt: ctx.prompt,
|
|
1916
|
-
cwd: ctx.cwd,
|
|
1917
|
-
...(ctx.model !== undefined ? { model: ctx.model } : {}),
|
|
1918
|
-
...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}),
|
|
1919
|
-
...(ctx.uploadRoot !== undefined ? { uploadRoot: ctx.uploadRoot } : {}),
|
|
1920
|
-
send(_channel, payload) {
|
|
1921
|
-
const translation = translateAgentRuntimeEvent(payload);
|
|
1945
|
+
session = ctx.attachPiRpcSession({ child: ctx.child, prompt: ctx.prompt, send({ payload }) {
|
|
1946
|
+
const translation = translateAgentRuntimeEvent({ rawEvent: payload });
|
|
1922
1947
|
if (translation.kind === 'agent') {
|
|
1923
1948
|
if (translation.sessionId !== undefined)
|
|
1924
1949
|
capturedSessionId = translation.sessionId;
|
|
@@ -1929,13 +1954,12 @@ function wirePiRpcLifecycle(ctx) {
|
|
|
1929
1954
|
translation.payload.delta.length > 0) {
|
|
1930
1955
|
userVisibleOutputSeen = true;
|
|
1931
1956
|
}
|
|
1932
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'agent', data: translation.payload }));
|
|
1957
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: translation.payload } }));
|
|
1933
1958
|
}
|
|
1934
1959
|
else if (translation.kind === 'error') {
|
|
1935
|
-
enqueueEmit(() => lifecycle.emit(runId, { event: 'error', data: translation.payload }));
|
|
1960
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
|
|
1936
1961
|
}
|
|
1937
|
-
},
|
|
1938
|
-
});
|
|
1962
|
+
} }, { cwd: ctx.cwd, ...(ctx.model !== undefined ? { model: ctx.model } : {}), ...(ctx.imagePaths.length > 0 ? { imagePaths: [...ctx.imagePaths] } : {}), ...(ctx.uploadRoot !== undefined ? { uploadRoot: ctx.uploadRoot } : {}) });
|
|
1939
1963
|
return session;
|
|
1940
1964
|
}
|
|
1941
1965
|
/**
|
|
@@ -1997,8 +2021,8 @@ function resolveProcessDeps(options) {
|
|
|
1997
2021
|
createCommandInvocation: options.createCommandInvocation ?? createCommandInvocation,
|
|
1998
2022
|
spawn: options.spawn ?? nodeSpawn,
|
|
1999
2023
|
listProcessSnapshots: options.listProcessSnapshots ?? listProcessSnapshots,
|
|
2000
|
-
collectProcessTreePids: options.collectProcessTreePids ?? collectProcessTreePids,
|
|
2001
|
-
stopProcesses: options.stopProcesses ?? stopProcesses,
|
|
2024
|
+
collectProcessTreePids: options.collectProcessTreePids ?? (({ processes, rootPids }) => collectProcessTreePids(processes, rootPids)),
|
|
2025
|
+
stopProcesses: options.stopProcesses ?? (({ pids }) => stopProcesses(pids)),
|
|
2002
2026
|
};
|
|
2003
2027
|
}
|
|
2004
2028
|
/** Resolves the two remaining defaultable options — see {@link resolveAgentRuntimeDeps}'s doc. Pure. */
|
|
@@ -2009,23 +2033,23 @@ function resolveMiscExecutorDeps(options) {
|
|
|
2009
2033
|
};
|
|
2010
2034
|
}
|
|
2011
2035
|
/** Phase 1: registry lookup + `assessAgentExecutorCompatibility` guard. */
|
|
2012
|
-
export async function resolveDefAndStreamFormat(input, deps) {
|
|
2013
|
-
const def = deps.getAgentDef(input.agentId);
|
|
2036
|
+
export async function resolveDefAndStreamFormat({ input, deps }) {
|
|
2037
|
+
const def = deps.getAgentDef({ id: input.agentId });
|
|
2014
2038
|
if (!def) {
|
|
2015
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_NOT_FOUND', `AgentExecutor: unknown agentId "${input.agentId}"`);
|
|
2039
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_NOT_FOUND', message: `AgentExecutor: unknown agentId "${input.agentId}"` });
|
|
2016
2040
|
}
|
|
2017
|
-
const compatibility = assessAgentExecutorCompatibility(def);
|
|
2041
|
+
const compatibility = assessAgentExecutorCompatibility({ def: def });
|
|
2018
2042
|
if (!compatibility.supported) {
|
|
2019
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_RUNTIME_UNSUPPORTED', compatibility.reason);
|
|
2043
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_RUNTIME_UNSUPPORTED', message: compatibility.reason });
|
|
2020
2044
|
}
|
|
2021
2045
|
return { def, streamFormat: compatibility.streamFormat };
|
|
2022
2046
|
}
|
|
2023
2047
|
/** Phase 2: image-prompt-delivery augmentation + argv-budget guard for argv-bound defs. */
|
|
2024
|
-
export async function resolveImageDeliveryAndArgvBudget(input, deps) {
|
|
2025
|
-
const imageDelivery = applyImagePromptDelivery(input.def.imageDelivery, input.prompt, input.imagePaths, input.extraAllowedDirs);
|
|
2026
|
-
const argvBudgetError = checkPromptArgvBudget(input.def, imageDelivery.prompt);
|
|
2048
|
+
export async function resolveImageDeliveryAndArgvBudget({ input, deps }) {
|
|
2049
|
+
const imageDelivery = applyImagePromptDelivery({ imageDelivery: input.def.imageDelivery, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs });
|
|
2050
|
+
const argvBudgetError = checkPromptArgvBudget({ def: input.def, composed: imageDelivery.prompt });
|
|
2027
2051
|
if (argvBudgetError) {
|
|
2028
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_PROMPT_TOO_LARGE', argvBudgetError.message);
|
|
2052
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: argvBudgetError.message });
|
|
2029
2053
|
}
|
|
2030
2054
|
return imageDelivery;
|
|
2031
2055
|
}
|
|
@@ -2034,44 +2058,44 @@ export async function resolveImageDeliveryAndArgvBudget(input, deps) {
|
|
|
2034
2058
|
* caller-supplied escape hatch verbatim, or the deny-by-default `BASELINE_AGENT_ENV_KEYS` allowlist.
|
|
2035
2059
|
* Pure.
|
|
2036
2060
|
*/
|
|
2037
|
-
export function resolveRunEnv(input, hostEnv) {
|
|
2061
|
+
export function resolveRunEnv({ input, hostEnv }) {
|
|
2038
2062
|
return input.env !== undefined ? toStringEnvRecord(input.env) : buildAgentEnv(hostEnv, input.credentialEnv);
|
|
2039
2063
|
}
|
|
2040
2064
|
/** Phase 3b: launch-path resolution + binary-not-resolved guard. */
|
|
2041
|
-
export async function resolveLaunch(input, deps) {
|
|
2042
|
-
const launch = deps.resolveAgentLaunch(input.def, input.resolvedEnv);
|
|
2065
|
+
export async function resolveLaunch({ input, deps }) {
|
|
2066
|
+
const launch = deps.resolveAgentLaunch({ def: input.def }, { configuredEnv: input.resolvedEnv });
|
|
2043
2067
|
if (!launch.launchPath) {
|
|
2044
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_BINARY_NOT_RESOLVED', `AgentExecutor: could not resolve an executable for agent "${input.def.id}" (bin "${input.def.bin}")`);
|
|
2068
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_BINARY_NOT_RESOLVED', message: `AgentExecutor: could not resolve an executable for agent "${input.def.id}" (bin "${input.def.bin}")` });
|
|
2045
2069
|
}
|
|
2046
2070
|
// Narrowed by the guard above; `resolveAgentLaunch`'s own return type still declares
|
|
2047
2071
|
// `launchPath: string | null` since it can't know this call site already checked.
|
|
2048
2072
|
return launch;
|
|
2049
2073
|
}
|
|
2050
2074
|
/** Phase 4a: stage a `promptViaFile` def's prompt to a temp file (a no-op for every other def). */
|
|
2051
|
-
export async function stagePromptFile(input, deps) {
|
|
2075
|
+
export async function stagePromptFile({ input, deps }) {
|
|
2052
2076
|
try {
|
|
2053
|
-
return await deps.preparePromptFileForAgent(input.def, input.prompt, input.runId);
|
|
2077
|
+
return await deps.preparePromptFileForAgent({ def: input.def, prompt: input.prompt, label: input.runId });
|
|
2054
2078
|
}
|
|
2055
2079
|
catch (err) {
|
|
2056
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a prompt file for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2080
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a prompt file for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2057
2081
|
}
|
|
2058
2082
|
}
|
|
2059
2083
|
/** Phase 4b: stage a `needsAgentLogFile` def's diagnostic-log path (a no-op for every other def). */
|
|
2060
|
-
export async function stageLogFile(input, deps) {
|
|
2084
|
+
export async function stageLogFile({ input, deps }) {
|
|
2061
2085
|
try {
|
|
2062
|
-
return await deps.prepareAgentLogFile(input.def, input.runId);
|
|
2086
|
+
return await deps.prepareAgentLogFile({ def: input.def, label: input.runId });
|
|
2063
2087
|
}
|
|
2064
2088
|
catch (err) {
|
|
2065
2089
|
await (input.preparedPromptFile ? input.preparedPromptFile.cleanup() : Promise.resolve());
|
|
2066
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a log file for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2090
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a log file for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2067
2091
|
}
|
|
2068
2092
|
}
|
|
2069
2093
|
/** Phase 5: resolves this run's MCP bridge delivery (credential resolution + {@link buildMcpBridgeDelivery}). */
|
|
2070
|
-
export async function resolveMcpBridgeForRun(input, deps) {
|
|
2094
|
+
export async function resolveMcpBridgeForRun({ input, deps }) {
|
|
2071
2095
|
try {
|
|
2072
2096
|
// Awaited here rather than inside `buildMcpBridgeDelivery` so that function stays pure and
|
|
2073
2097
|
// synchronous. `undefined` when the host supplied no resolver, which omits the token entirely.
|
|
2074
|
-
const credential = deps.mcpJsonInjection !== undefined ? await deps.mcpJsonInjection.credential?.(input.runId) : undefined;
|
|
2098
|
+
const credential = deps.mcpJsonInjection !== undefined ? await deps.mcpJsonInjection.credential?.({ runId: input.runId }) : undefined;
|
|
2075
2099
|
return buildMcpBridgeDelivery({
|
|
2076
2100
|
cwd: input.cwd,
|
|
2077
2101
|
runId: input.runId,
|
|
@@ -2084,7 +2108,7 @@ export async function resolveMcpBridgeForRun(input, deps) {
|
|
|
2084
2108
|
// Spawning a child that cannot authenticate would produce a run whose every bridged tool call
|
|
2085
2109
|
// 401s, so a rejecting credential resolver fails the run before spawn instead.
|
|
2086
2110
|
await deps.cleanupStagedFiles();
|
|
2087
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not resolve the MCP bridge credential for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2111
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not resolve the MCP bridge credential for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2088
2112
|
}
|
|
2089
2113
|
}
|
|
2090
2114
|
/**
|
|
@@ -2108,11 +2132,11 @@ export async function resolveMcpBridgeForRun(input, deps) {
|
|
|
2108
2132
|
* @complexity O(1) plus `mergeEnvContentMcpConfig`'s and `mergeEnvContentInstructions`'s own `JSON.parse`/`JSON.stringify` cost.
|
|
2109
2133
|
* @overallScore 100/100
|
|
2110
2134
|
*/
|
|
2111
|
-
export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile, claudeConfigDir) {
|
|
2135
|
+
export function computeChildEnv({ spawnEnv, mcpBridge }, { codexHomeDir, systemPromptEnvOverrides, stagedInstructionsFile, claudeConfigDir } = {}) {
|
|
2112
2136
|
const envContentApplied = mcpBridge?.kind === 'env-content'
|
|
2113
2137
|
? {
|
|
2114
2138
|
...spawnEnv,
|
|
2115
|
-
[mcpBridge.envVarName]: mergeEnvContentMcpConfig(spawnEnv[mcpBridge.envVarName], mcpBridge.serverEntry),
|
|
2139
|
+
[mcpBridge.envVarName]: mergeEnvContentMcpConfig({ existingRaw: spawnEnv[mcpBridge.envVarName], entry: mcpBridge.serverEntry }),
|
|
2116
2140
|
}
|
|
2117
2141
|
: spawnEnv;
|
|
2118
2142
|
// `'env-passthrough'` (antigravity): no document, no named carrier variable — the bridge
|
|
@@ -2125,7 +2149,7 @@ export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptE
|
|
|
2125
2149
|
? envPassthroughApplied
|
|
2126
2150
|
: {
|
|
2127
2151
|
...envPassthroughApplied,
|
|
2128
|
-
[stagedInstructionsFile.varName]: mergeEnvContentInstructions(envPassthroughApplied[stagedInstructionsFile.varName], stagedInstructionsFile.path),
|
|
2152
|
+
[stagedInstructionsFile.varName]: mergeEnvContentInstructions({ existingRaw: envPassthroughApplied[stagedInstructionsFile.varName], instructionsFilePath: stagedInstructionsFile.path }),
|
|
2129
2153
|
};
|
|
2130
2154
|
const codexHomeApplied = codexHomeDir === undefined ? instructionsApplied : { ...instructionsApplied, CODEX_HOME: codexHomeDir };
|
|
2131
2155
|
// Finding 1 of SEC-assistant-env-isolation-2026-09-07: same "only set when staged" shape as
|
|
@@ -2146,7 +2170,7 @@ export function computeChildEnv(spawnEnv, mcpBridge, codexHomeDir, systemPromptE
|
|
|
2146
2170
|
* session id, with no prompt/log file staged and no claude-mcp-json bridge, is exactly the common
|
|
2147
2171
|
* case for a resumed turn.
|
|
2148
2172
|
*/
|
|
2149
|
-
export function computeRuntimeContext(preparedPromptFile, preparedLogFile, mcpBridge, resumeSessionId, newSessionId) {
|
|
2173
|
+
export function computeRuntimeContext({ preparedPromptFile, preparedLogFile, mcpBridge }, { resumeSessionId, newSessionId } = {}) {
|
|
2150
2174
|
// Matches claude.ts buildArgs' own `typeof x === 'string' && x` truthiness check, so an empty
|
|
2151
2175
|
// string or explicit `null` (no resume target yet) is treated as absent here too, rather than
|
|
2152
2176
|
// manufacturing a context that carries a session field the def would ignore anyway.
|
|
@@ -2187,13 +2211,18 @@ function computeSystemPromptOverlay(promptAugmenter, agentId, runtimeContext) {
|
|
|
2187
2211
|
turnIndex: runtimeContext?.hasPriorAssistantTurn ? 1 : 0,
|
|
2188
2212
|
});
|
|
2189
2213
|
}
|
|
2190
|
-
/** Phase 9a: the def's `buildArgs` 4th argument
|
|
2191
|
-
|
|
2214
|
+
/** Phase 9a: the def's `buildArgs` 4th argument. Historically omitted when every override was absent;
|
|
2215
|
+
* that let defs silently enable auto-approval. Always supply the validated restricted default now. Pure. */
|
|
2216
|
+
export function buildAgentBuildArgsOptions({ input, systemPromptOverlay }) {
|
|
2217
|
+
const permissionMode = input.permissionMode ?? 'restricted';
|
|
2218
|
+
if (permissionMode !== 'restricted' && permissionMode !== 'bypass') {
|
|
2219
|
+
throw new AgentExecutorError({ code: 'AGENT_PERMISSION_MODE_INVALID', message: 'AgentExecutor: permissionMode must be restricted or bypass' });
|
|
2220
|
+
}
|
|
2192
2221
|
const hasOverlay = systemPromptOverlay !== undefined && systemPromptOverlay !== null;
|
|
2193
2222
|
const options = {
|
|
2194
2223
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
2195
2224
|
...(input.reasoning !== undefined ? { reasoning: input.reasoning } : {}),
|
|
2196
|
-
|
|
2225
|
+
permissionMode,
|
|
2197
2226
|
...(input.disallowedTools !== undefined ? { disallowedTools: input.disallowedTools } : {}),
|
|
2198
2227
|
...(input.allowedTools !== undefined ? { allowedTools: input.allowedTools } : {}),
|
|
2199
2228
|
...(input.settingSources !== undefined ? { settingSources: input.settingSources } : {}),
|
|
@@ -2314,10 +2343,15 @@ export function resolveSystemPromptOverlayDelivery(input) {
|
|
|
2314
2343
|
* therefore only ever reach the 7 defs whose `buildArgs` reads its first argument at all — the
|
|
2315
2344
|
* other 17 declare it `_prompt` and discard it, which is exactly how the overlay used to go missing.
|
|
2316
2345
|
*/
|
|
2317
|
-
export async function buildRunArgs(input, deps) {
|
|
2346
|
+
export async function buildRunArgs({ input, deps }) {
|
|
2318
2347
|
try {
|
|
2319
2348
|
const delivery = input.overlayDelivery;
|
|
2320
|
-
const
|
|
2349
|
+
const options = buildAgentBuildArgsOptions({ input: input.runInput, systemPromptOverlay: input.systemPromptOverlay });
|
|
2350
|
+
const args = input.def.buildArgs({ prompt: delivery.prompt, imagePaths: [...(input.imagePaths ?? [])] }, {
|
|
2351
|
+
...(input.imageDelivery.extraAllowedDirs === undefined ? {} : { extraAllowedDirs: [...input.imageDelivery.extraAllowedDirs] }),
|
|
2352
|
+
...(options === undefined ? {} : { options }),
|
|
2353
|
+
...(input.runtimeContext === undefined ? {} : { runtimeContext: input.runtimeContext }),
|
|
2354
|
+
});
|
|
2321
2355
|
// `'append-flag'` delivery's extra argv (empty for every other def/strategy) is appended after
|
|
2322
2356
|
// whatever the def's own `buildArgs` returned — safe because it is only ever non-empty for a
|
|
2323
2357
|
// `promptViaStdin` def with no trailing positional argv (`claude`/`pi` today; see
|
|
@@ -2329,11 +2363,11 @@ export async function buildRunArgs(input, deps) {
|
|
|
2329
2363
|
}
|
|
2330
2364
|
catch (err) {
|
|
2331
2365
|
await deps.releaseStagedResources();
|
|
2332
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not build launch arguments for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2366
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not build launch arguments for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2333
2367
|
}
|
|
2334
2368
|
}
|
|
2335
2369
|
/** 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). */
|
|
2336
|
-
export async function writeMcpJsonIfNeeded(input, deps) {
|
|
2370
|
+
export async function writeMcpJsonIfNeeded({ input, deps }) {
|
|
2337
2371
|
if (input.mcpBridge?.kind !== 'claude-mcp-json' || deps.mcpJsonInjection === undefined) {
|
|
2338
2372
|
return undefined;
|
|
2339
2373
|
}
|
|
@@ -2343,7 +2377,7 @@ export async function writeMcpJsonIfNeeded(input, deps) {
|
|
|
2343
2377
|
}
|
|
2344
2378
|
catch (err) {
|
|
2345
2379
|
await deps.releaseStagedResources();
|
|
2346
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not write .mcp.json for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2380
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not write .mcp.json for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2347
2381
|
}
|
|
2348
2382
|
}
|
|
2349
2383
|
/**
|
|
@@ -2364,7 +2398,7 @@ export async function writeMcpJsonIfNeeded(input, deps) {
|
|
|
2364
2398
|
* @complexity O(1) plus one directory creation and one file write.
|
|
2365
2399
|
* @overallScore 100/100
|
|
2366
2400
|
*/
|
|
2367
|
-
export async function prepareSystemPromptOverlayFileIfNeeded(input, deps) {
|
|
2401
|
+
export async function prepareSystemPromptOverlayFileIfNeeded({ input, deps }) {
|
|
2368
2402
|
if (input.def.systemPromptDelivery?.strategy !== 'config-instructions-file' ||
|
|
2369
2403
|
typeof input.overlay !== 'string' ||
|
|
2370
2404
|
input.overlay.length === 0) {
|
|
@@ -2384,7 +2418,7 @@ export async function prepareSystemPromptOverlayFileIfNeeded(input, deps) {
|
|
|
2384
2418
|
}
|
|
2385
2419
|
catch (err) {
|
|
2386
2420
|
await deps.releaseStagedResources();
|
|
2387
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a system-prompt overlay file for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2421
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a system-prompt overlay file for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2388
2422
|
}
|
|
2389
2423
|
}
|
|
2390
2424
|
/**
|
|
@@ -2397,16 +2431,16 @@ export async function prepareSystemPromptOverlayFileIfNeeded(input, deps) {
|
|
|
2397
2431
|
* @complexity O(1) plus {@link prepareCodexHomeForRun}'s own cost.
|
|
2398
2432
|
* @overallScore 100/100
|
|
2399
2433
|
*/
|
|
2400
|
-
export async function prepareCodexHomeIfNeeded(input, deps) {
|
|
2434
|
+
export async function prepareCodexHomeIfNeeded({ input, deps }) {
|
|
2401
2435
|
if (input.mcpBridge?.kind !== 'codex-toml' || deps.mcpJsonInjection === undefined) {
|
|
2402
2436
|
return null;
|
|
2403
2437
|
}
|
|
2404
2438
|
try {
|
|
2405
|
-
return await prepareCodexHomeForRun(input.runId, input.mcpBridge.serverEntry, resolveSourceCodexHomeDir(deps.hostEnv), resolveCodexHomeSeams(deps.mcpJsonInjection));
|
|
2439
|
+
return await prepareCodexHomeForRun(input.runId, input.mcpBridge.serverEntry, resolveSourceCodexHomeDir({ hostEnv: deps.hostEnv }), resolveCodexHomeSeams(deps.mcpJsonInjection));
|
|
2406
2440
|
}
|
|
2407
2441
|
catch (err) {
|
|
2408
2442
|
await deps.releaseStagedResources();
|
|
2409
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a CODEX_HOME for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2443
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a CODEX_HOME for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2410
2444
|
}
|
|
2411
2445
|
}
|
|
2412
2446
|
/**
|
|
@@ -2432,25 +2466,25 @@ export async function prepareCodexHomeIfNeeded(input, deps) {
|
|
|
2432
2466
|
* @param deps.hostEnv - The daemon's own environment, threaded through to {@link resolveSourceClaudeConfigDir} rather than read from a module-level `process.env`, matching {@link prepareCodexHomeIfNeeded}'s identical testability reasoning.
|
|
2433
2467
|
* @complexity O(1) plus {@link prepareClaudeConfigDirForRun}'s own cost.
|
|
2434
2468
|
*/
|
|
2435
|
-
export async function prepareClaudeConfigDirIfNeeded(input, deps) {
|
|
2469
|
+
export async function prepareClaudeConfigDirIfNeeded({ input, deps }) {
|
|
2436
2470
|
if (input.def.id !== 'claude' || !deps.enabled) {
|
|
2437
2471
|
return null;
|
|
2438
2472
|
}
|
|
2439
2473
|
try {
|
|
2440
|
-
return await prepareClaudeConfigDirForRun(input.runId, resolveSourceClaudeConfigDir(deps.hostEnv), resolveClaudeConfigDirSeams(deps.claudeConfigDirIsolation));
|
|
2474
|
+
return await prepareClaudeConfigDirForRun(input.runId, resolveSourceClaudeConfigDir({ hostEnv: deps.hostEnv }), resolveClaudeConfigDirSeams(deps.claudeConfigDirIsolation));
|
|
2441
2475
|
}
|
|
2442
2476
|
catch (err) {
|
|
2443
2477
|
await deps.releaseStagedResources();
|
|
2444
|
-
return deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not stage a CLAUDE_CONFIG_DIR for agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2478
|
+
return deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not stage a CLAUDE_CONFIG_DIR for agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2445
2479
|
}
|
|
2446
2480
|
}
|
|
2447
2481
|
/** 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. */
|
|
2448
|
-
export async function guardWindowsCommandLineBudget(input, deps) {
|
|
2449
|
-
const windowsBudgetError = checkWindowsCmdShimCommandLineBudget(input.def, input.launchPath, input.args) ??
|
|
2450
|
-
checkWindowsDirectExeCommandLineBudget(input.def, input.launchPath, input.args);
|
|
2482
|
+
export async function guardWindowsCommandLineBudget({ input, deps }) {
|
|
2483
|
+
const windowsBudgetError = checkWindowsCmdShimCommandLineBudget({ def: input.def, resolvedBin: input.launchPath, args: input.args }) ??
|
|
2484
|
+
checkWindowsDirectExeCommandLineBudget({ def: input.def, resolvedBin: input.launchPath, args: input.args });
|
|
2451
2485
|
if (windowsBudgetError) {
|
|
2452
2486
|
await deps.releaseStagedResources();
|
|
2453
|
-
await deps.failBeforeSpawn(input.runId, 'AGENT_PROMPT_TOO_LARGE', windowsBudgetError.message);
|
|
2487
|
+
await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_PROMPT_TOO_LARGE', message: windowsBudgetError.message });
|
|
2454
2488
|
}
|
|
2455
2489
|
}
|
|
2456
2490
|
/**
|
|
@@ -2472,7 +2506,7 @@ export async function guardWindowsCommandLineBudget(input, deps) {
|
|
|
2472
2506
|
* which are safe to make asynchronous since no child (and hence no listener race) exists yet.
|
|
2473
2507
|
* @complexity O(1) plus `spawn`'s own cost.
|
|
2474
2508
|
*/
|
|
2475
|
-
export function spawnAgentChildProcess(input, deps) {
|
|
2509
|
+
export function spawnAgentChildProcess({ input, deps }) {
|
|
2476
2510
|
try {
|
|
2477
2511
|
return {
|
|
2478
2512
|
kind: 'ok',
|
|
@@ -2489,27 +2523,28 @@ export function spawnAgentChildProcess(input, deps) {
|
|
|
2489
2523
|
}
|
|
2490
2524
|
}
|
|
2491
2525
|
/** 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`. */
|
|
2492
|
-
export function isStdinDrivenFormat(
|
|
2526
|
+
export function isStdinDrivenFormat(args) {
|
|
2527
|
+
const { streamFormat } = args;
|
|
2493
2528
|
return streamFormat !== 'acp-json-rpc' && streamFormat !== 'pi-rpc';
|
|
2494
2529
|
}
|
|
2495
2530
|
/** Phase 13: awaits spawn confirmation, routing a failure through the same `failBeforeSpawn` shape every earlier guard uses. */
|
|
2496
|
-
export async function confirmChildSpawned(input, deps) {
|
|
2531
|
+
export async function confirmChildSpawned({ input, deps }) {
|
|
2497
2532
|
try {
|
|
2498
2533
|
await waitForSpawnOrError(input.child);
|
|
2499
2534
|
}
|
|
2500
2535
|
catch (err) {
|
|
2501
2536
|
await deps.releaseStagedResources();
|
|
2502
|
-
await deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}`);
|
|
2537
|
+
await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: failed to spawn agent "${input.def.id}": ${errorMessage(err)}` });
|
|
2503
2538
|
}
|
|
2504
2539
|
}
|
|
2505
2540
|
/** 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. */
|
|
2506
|
-
export function armHandoffWatcher(runtimeLockHold, handoffInput, release) {
|
|
2541
|
+
export function armHandoffWatcher({ runtimeLockHold, handoffInput, release }) {
|
|
2507
2542
|
if (!runtimeLockHold?.waitForHandoff)
|
|
2508
2543
|
return;
|
|
2509
2544
|
void runtimeLockHold.waitForHandoff(handoffInput).then(release, release);
|
|
2510
2545
|
}
|
|
2511
2546
|
/** Phase 15 (ACP branch): attaches the ACP session, escalating process-tree teardown and failing the run through `failBeforeSpawn` on an attach-time throw. */
|
|
2512
|
-
export async function runAcpDispatch(input, deps) {
|
|
2547
|
+
export async function runAcpDispatch({ input, deps }) {
|
|
2513
2548
|
try {
|
|
2514
2549
|
wireAcpLifecycle({
|
|
2515
2550
|
runId: input.runId,
|
|
@@ -2541,11 +2576,11 @@ export async function runAcpDispatch(input, deps) {
|
|
|
2541
2576
|
// fired-and-forgotten (SEC-007: "await where lifecycle ordering allows it").
|
|
2542
2577
|
await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'acp-attach-failure', deps.onCleanupFailure);
|
|
2543
2578
|
await deps.releaseStagedResources();
|
|
2544
|
-
await deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}`);
|
|
2579
|
+
await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach ACP session for agent "${input.agentId}": ${errorMessage(err)}` });
|
|
2545
2580
|
}
|
|
2546
2581
|
}
|
|
2547
2582
|
/** Phase 15 (pi-rpc branch): same discipline as {@link runAcpDispatch}, for the one `'pi-rpc'` def. */
|
|
2548
|
-
export async function runPiRpcDispatch(input, deps) {
|
|
2583
|
+
export async function runPiRpcDispatch({ input, deps }) {
|
|
2549
2584
|
try {
|
|
2550
2585
|
wirePiRpcLifecycle({
|
|
2551
2586
|
runId: input.runId,
|
|
@@ -2572,10 +2607,11 @@ export async function runPiRpcDispatch(input, deps) {
|
|
|
2572
2607
|
// fire-and-forget (SEC-007).
|
|
2573
2608
|
await terminateChildTreeBestEffort({ listProcessSnapshots: deps.listProcessSnapshots, collectProcessTreePids: deps.collectProcessTreePids, stopProcesses: deps.stopProcesses }, input.child, input.runId, 'pi-rpc-attach-failure', deps.onCleanupFailure);
|
|
2574
2609
|
await deps.releaseStagedResources();
|
|
2575
|
-
await deps.failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}`);
|
|
2610
|
+
await deps.failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: could not attach pi-rpc session for agent "${input.agentId}": ${errorMessage(err)}` });
|
|
2576
2611
|
}
|
|
2577
2612
|
}
|
|
2578
|
-
export function createAgentExecutor(
|
|
2613
|
+
export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
2614
|
+
const options = { ...requiredArgs, ...optionalArgs };
|
|
2579
2615
|
const lifecycle = options.lifecycle;
|
|
2580
2616
|
const { getAgentDef: getAgentDefFn, resolveAgentLaunch: resolveAgentLaunchFn, ensureAgentCapabilities: ensureAgentCapabilitiesFn, applyAgentLaunchEnv: applyAgentLaunchEnvFn, attachAcpSession: attachAcpSessionFn, attachPiRpcSession: attachPiRpcSessionFn, preparePromptFileForAgent: preparePromptFileForAgentFn, prepareAgentLogFile: prepareAgentLogFileFn, } = resolveAgentRuntimeDeps(options);
|
|
2581
2617
|
const { createCommandInvocation: createCommandInvocationFn, spawn: spawnFn, listProcessSnapshots: listProcessSnapshotsFn, collectProcessTreePids: collectProcessTreePidsFn, stopProcesses: stopProcessesFn, } = resolveProcessDeps(options);
|
|
@@ -2599,16 +2635,16 @@ export function createAgentExecutor(options) {
|
|
|
2599
2635
|
* @complexity O(1) plus `lifecycle.finish()`'s own cost.
|
|
2600
2636
|
* @overallScore 100/100
|
|
2601
2637
|
*/
|
|
2602
|
-
async function failBeforeSpawn(runId, code, message) {
|
|
2638
|
+
async function failBeforeSpawn({ runId, code, message }) {
|
|
2603
2639
|
// The reason goes on the run's own stream before `end`, not only into the thrown error: the
|
|
2604
2640
|
// thrown error reaches the server log, while a chat client sees only this stream. Without it
|
|
2605
2641
|
// the user got a bare "Run failed". Best-effort: a stream that will not take the event must not
|
|
2606
2642
|
// stop the run from finishing.
|
|
2607
2643
|
await lifecycle
|
|
2608
|
-
.emit(runId, { event: 'error', data: { message: `The assistant could not start: ${message.replace(/^AgentExecutor:\s*/, '')}` } })
|
|
2644
|
+
.emit({ runId: runId, input: { event: 'error', data: { message: `The assistant could not start: ${message.replace(/^AgentExecutor:\s*/, '')}` } } })
|
|
2609
2645
|
.catch(() => undefined);
|
|
2610
2646
|
await lifecycle.finish({ runId, status: 'failed', code: null, signal: null, resumable: false });
|
|
2611
|
-
throw new AgentExecutorError(code, message);
|
|
2647
|
+
throw new AgentExecutorError({ code: code, message: message });
|
|
2612
2648
|
}
|
|
2613
2649
|
/**
|
|
2614
2650
|
* `AgentExecutor.run()` — see that interface method's own doc for the
|
|
@@ -2621,8 +2657,12 @@ export function createAgentExecutor(options) {
|
|
|
2621
2657
|
* @complexity O(1) setup (registry lookup, launch resolution, one spawn call); steady-state cost thereafter belongs to {@link wireChildLifecycle}.
|
|
2622
2658
|
* @overallScore 100/100
|
|
2623
2659
|
*/
|
|
2624
|
-
async function run(
|
|
2625
|
-
const
|
|
2660
|
+
async function run(requiredArgs, optionalArgs = {}) {
|
|
2661
|
+
const input = { ...requiredArgs, ...optionalArgs, permissionMode: optionalArgs.permissionMode ?? 'restricted' };
|
|
2662
|
+
if (input.permissionMode !== 'restricted' && input.permissionMode !== 'bypass') {
|
|
2663
|
+
return failBeforeSpawn({ runId: input.runId, code: 'AGENT_PERMISSION_MODE_INVALID', message: 'AgentExecutor: permissionMode must be restricted or bypass' });
|
|
2664
|
+
}
|
|
2665
|
+
const { def, streamFormat } = await resolveDefAndStreamFormat({ input: { runId: input.runId, agentId: input.agentId }, deps: { getAgentDef: getAgentDefFn, failBeforeSpawn } });
|
|
2626
2666
|
// Computed once, before anything downstream ever looks at "the prompt" or "the allowed
|
|
2627
2667
|
// dirs" — a no-op (`{prompt: input.prompt, extraAllowedDirs: input.extraAllowedDirs}`,
|
|
2628
2668
|
// literally unchanged) unless `def.imageDelivery === 'prompt-path'` AND `input.imagePaths`
|
|
@@ -2633,7 +2673,7 @@ export function createAgentExecutor(options) {
|
|
|
2633
2673
|
// calls further down deliberately keep reading `input.prompt` verbatim, since those two
|
|
2634
2674
|
// defs' own native protocol already delivers the image and must never also get this
|
|
2635
2675
|
// treatment (the double-delivery hazard this mechanism exists to avoid).
|
|
2636
|
-
const imageDelivery = await resolveImageDeliveryAndArgvBudget({ runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, { failBeforeSpawn });
|
|
2676
|
+
const imageDelivery = await resolveImageDeliveryAndArgvBudget({ input: { runId: input.runId, def, prompt: input.prompt, imagePaths: input.imagePaths, extraAllowedDirs: input.extraAllowedDirs }, deps: { failBeforeSpawn } });
|
|
2637
2677
|
// Phase 8, hoisted deliberately above EVERY step that writes or sends the prompt. Four
|
|
2638
2678
|
// different channels carry a prompt in this driver, and they do not all run at the same point:
|
|
2639
2679
|
// the `promptViaFile` staging below writes its file BEFORE `buildArgs`, `buildArgs` itself runs
|
|
@@ -2652,7 +2692,10 @@ export function createAgentExecutor(options) {
|
|
|
2652
2692
|
// because every `PromptAugmenter.systemOverlay()` implementation this seam has today wants the
|
|
2653
2693
|
// same overlay on every turn, not a first-turn-only one; a caller that needs finer-grained turn
|
|
2654
2694
|
// numbering can track it itself and ignore this arg.
|
|
2655
|
-
const preStagingRuntimeContext = computeRuntimeContext(null, null, null
|
|
2695
|
+
const preStagingRuntimeContext = computeRuntimeContext({ preparedPromptFile: null, preparedLogFile: null, mcpBridge: null }, {
|
|
2696
|
+
...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }),
|
|
2697
|
+
...(input.newSessionId === undefined ? {} : { newSessionId: input.newSessionId }),
|
|
2698
|
+
});
|
|
2656
2699
|
const systemPromptOverlay = computeSystemPromptOverlay(promptAugmenter, def.id, preStagingRuntimeContext);
|
|
2657
2700
|
const overlayDelivery = resolveSystemPromptOverlayDelivery({
|
|
2658
2701
|
defId: def.id,
|
|
@@ -2666,14 +2709,14 @@ export function createAgentExecutor(options) {
|
|
|
2666
2709
|
// The def's own declared env (e.g. claude's `ENABLE_TOOL_SEARCH=false`) is a default the run's
|
|
2667
2710
|
// env can override. Detection already spawns with it (`detection.ts`); without this the run
|
|
2668
2711
|
// spawn silently dropped it. Declared by the def itself, so it never widens SEC-001's allowlist.
|
|
2669
|
-
const resolvedEnv = { ...(def.env ?? {}), ...resolveRunEnv(input, process.env) };
|
|
2670
|
-
const launch = await resolveLaunch({ runId: input.runId, def, resolvedEnv }, { resolveAgentLaunch: resolveAgentLaunchFn, failBeforeSpawn });
|
|
2671
|
-
const spawnEnv = applyAgentLaunchEnvFn({ ...resolvedEnv }, launch);
|
|
2712
|
+
const resolvedEnv = { ...(def.env ?? {}), ...resolveRunEnv({ input: input, hostEnv: process.env }) };
|
|
2713
|
+
const launch = await resolveLaunch({ input: { runId: input.runId, def, resolvedEnv }, deps: { resolveAgentLaunch: resolveAgentLaunchFn, failBeforeSpawn } });
|
|
2714
|
+
const spawnEnv = applyAgentLaunchEnvFn({ env: { ...resolvedEnv }, launch });
|
|
2672
2715
|
// Fill the def's `--help` capability gate before `buildArgs` reads it. Without this, only a
|
|
2673
2716
|
// host that happened to call `detectAgents` in THIS process ever had the gate filled, so e.g.
|
|
2674
2717
|
// `claude` never got `--include-partial-messages` (no streamed text). Probes once per binary;
|
|
2675
2718
|
// never rejects (a failed probe keeps the safe no-optional-flags baseline).
|
|
2676
|
-
await ensureAgentCapabilitiesFn(def, launch.launchPath, spawnEnv);
|
|
2719
|
+
await ensureAgentCapabilitiesFn({ def, launchPath: launch.launchPath, env: spawnEnv });
|
|
2677
2720
|
// Stage a promptViaFile def's (grok-build) prompt to a temp file before buildArgs runs — its
|
|
2678
2721
|
// buildArgs throws without runtimeContext.promptFilePath. A no-op (returns null) for every
|
|
2679
2722
|
// def without promptViaFile: true (preparePromptFileForAgent's own guard).
|
|
@@ -2681,13 +2724,13 @@ export function createAgentExecutor(options) {
|
|
|
2681
2724
|
// `overlayDelivery.prompt`, not `imageDelivery.prompt`: for a `promptViaFile` def this file IS
|
|
2682
2725
|
// the prompt transport — its `buildArgs` declares `_prompt` and passes only the path — so the
|
|
2683
2726
|
// overlay has to be in the bytes written here or the CLI never sees it at all.
|
|
2684
|
-
const preparedPromptFile = await stagePromptFile({ runId: input.runId, def, prompt: overlayDelivery.prompt }, { preparePromptFileForAgent: preparePromptFileForAgentFn, failBeforeSpawn });
|
|
2727
|
+
const preparedPromptFile = await stagePromptFile({ input: { runId: input.runId, def, prompt: overlayDelivery.prompt }, deps: { preparePromptFileForAgent: preparePromptFileForAgentFn, failBeforeSpawn } });
|
|
2685
2728
|
// Stage a needsAgentLogFile def's (antigravity) diagnostic-log path, on the same terms and at
|
|
2686
2729
|
// the same point as the prompt file above: before buildArgs, since buildArgs is what turns the
|
|
2687
2730
|
// path into a `--log-file <path>` argument. A no-op (returns null) for every def without
|
|
2688
2731
|
// `needsAgentLogFile: true` (prepareAgentLogFile's own guard). Sequenced after the prompt file
|
|
2689
2732
|
// rather than concurrently so the failure path above has exactly one thing to clean up.
|
|
2690
|
-
const preparedLogFile = await stageLogFile({ runId: input.runId, def, preparedPromptFile }, { prepareAgentLogFile: prepareAgentLogFileFn, failBeforeSpawn });
|
|
2733
|
+
const preparedLogFile = await stageLogFile({ input: { runId: input.runId, def, preparedPromptFile }, deps: { prepareAgentLogFile: prepareAgentLogFileFn, failBeforeSpawn } });
|
|
2691
2734
|
// Cleaned up after the child exits (wireChildLifecycle/wireAcpLifecycle/wirePiRpcLifecycle's
|
|
2692
2735
|
// close handlers) and on every pre-spawn/spawn-failure path below — a leaked temp file
|
|
2693
2736
|
// containing the full prompt, or whatever the CLI chose to write into its log, is a
|
|
@@ -2732,7 +2775,7 @@ export function createAgentExecutor(options) {
|
|
|
2732
2775
|
if (writtenMcpJsonPath !== undefined) {
|
|
2733
2776
|
const mcpJsonFileToRemove = writtenMcpJsonPath;
|
|
2734
2777
|
writtenMcpJsonPath = undefined;
|
|
2735
|
-
await removeMcpJsonFileFn(mcpJsonFileToRemove);
|
|
2778
|
+
await removeMcpJsonFileFn({ path: mcpJsonFileToRemove });
|
|
2736
2779
|
}
|
|
2737
2780
|
if (preparedCodexHome) {
|
|
2738
2781
|
const codexHomeToRemove = preparedCodexHome;
|
|
@@ -2755,8 +2798,11 @@ export function createAgentExecutor(options) {
|
|
|
2755
2798
|
// resolving here means the per-run bearer credential is minted exactly once no matter which of
|
|
2756
2799
|
// the five mechanisms ends up carrying it. `null` for an unconfigured host or a def declaring
|
|
2757
2800
|
// no strategy — see `buildMcpBridgeDelivery`'s doc.
|
|
2758
|
-
const mcpBridge = await resolveMcpBridgeForRun({ runId: input.runId, cwd: input.cwd, def }, { mcpJsonInjection, cleanupStagedFiles, failBeforeSpawn });
|
|
2759
|
-
const runtimeContext = computeRuntimeContext(preparedPromptFile, preparedLogFile, mcpBridge
|
|
2801
|
+
const mcpBridge = await resolveMcpBridgeForRun({ input: { runId: input.runId, cwd: input.cwd, def }, deps: { mcpJsonInjection, cleanupStagedFiles, failBeforeSpawn } });
|
|
2802
|
+
const runtimeContext = computeRuntimeContext({ preparedPromptFile: preparedPromptFile, preparedLogFile: preparedLogFile, mcpBridge: mcpBridge }, {
|
|
2803
|
+
...(input.resumeSessionId === undefined ? {} : { resumeSessionId: input.resumeSessionId }),
|
|
2804
|
+
...(input.newSessionId === undefined ? {} : { newSessionId: input.newSessionId }),
|
|
2805
|
+
});
|
|
2760
2806
|
// A `runtimeLock` def's buildArgs mutates process-global state its own CLI reads back at
|
|
2761
2807
|
// startup, so the mutex must be held from before buildArgs until the spawned child has
|
|
2762
2808
|
// demonstrably consumed it — see `RuntimeLock`'s own doc for the concrete race. Undefined for
|
|
@@ -2787,60 +2833,65 @@ export function createAgentExecutor(options) {
|
|
|
2787
2833
|
// `Error` — breaking this driver's "never a bare throw, always an `AgentExecutorError`" contract
|
|
2788
2834
|
// — and left the run `'running'` forever while still holding the process-global mutex and both
|
|
2789
2835
|
// staged files, so no later run of that def could ever acquire the lock either.
|
|
2790
|
-
const { args, envOverrides: systemPromptEnvOverrides } = await buildRunArgs({ runId: input.runId, def, imageDelivery, imagePaths: input.imagePaths, runInput: input, systemPromptOverlay, overlayDelivery, runtimeContext }, { releaseStagedResources, failBeforeSpawn });
|
|
2836
|
+
const { args, envOverrides: systemPromptEnvOverrides } = await buildRunArgs({ input: { runId: input.runId, def, imageDelivery, imagePaths: input.imagePaths, runInput: input, systemPromptOverlay, overlayDelivery, runtimeContext }, deps: { releaseStagedResources, failBeforeSpawn } });
|
|
2791
2837
|
// Mechanism 1 of 5's one effect — stage this run's own MCP config file (run-scoped, see
|
|
2792
2838
|
// `mcpJsonPathForRun`) before spawn so the `--mcp-config <path>` argv buildArgs just produced
|
|
2793
2839
|
// points at a real file. Skipped entirely for the other four mechanisms and whenever no bridge
|
|
2794
2840
|
// was resolved at all. `writtenMcpJsonPath` is set only once the write actually happens, so
|
|
2795
2841
|
// `cleanupStagedFiles` knows there is a live-token file to remove afterward.
|
|
2796
|
-
writtenMcpJsonPath = await writeMcpJsonIfNeeded({ runId: input.runId, cwd: input.cwd, def, mcpBridge }, { mcpJsonInjection, releaseStagedResources, failBeforeSpawn });
|
|
2842
|
+
writtenMcpJsonPath = await writeMcpJsonIfNeeded({ input: { runId: input.runId, cwd: input.cwd, def, mcpBridge }, deps: { mcpJsonInjection, releaseStagedResources, failBeforeSpawn } });
|
|
2797
2843
|
// Mechanism 5 of 5's one effect — stage this run's scratch `CODEX_HOME` directory. Skipped
|
|
2798
2844
|
// entirely for the other four mechanisms and whenever no bridge was resolved at all.
|
|
2799
2845
|
// `codex.ts`'s `buildArgs` needs no argv change for this (CODEX_HOME is an env var, not a flag),
|
|
2800
2846
|
// so — unlike the `.mcp.json` staging above — this can run after `buildArgs` with no ordering
|
|
2801
2847
|
// constraint of its own; it is placed here only to keep the two staging steps adjacent.
|
|
2802
|
-
preparedCodexHome = await prepareCodexHomeIfNeeded({ runId: input.runId, def, mcpBridge }, { mcpJsonInjection, hostEnv: process.env, releaseStagedResources, failBeforeSpawn });
|
|
2848
|
+
preparedCodexHome = await prepareCodexHomeIfNeeded({ input: { runId: input.runId, def, mcpBridge }, deps: { mcpJsonInjection, hostEnv: process.env, releaseStagedResources, failBeforeSpawn } });
|
|
2803
2849
|
// Finding 1 of SEC-assistant-env-isolation-2026-09-07's one effect — stage this run's scratch
|
|
2804
2850
|
// `CLAUDE_CONFIG_DIR` directory. Unconditional for a `claude`-id run (unlike CODEX_HOME above,
|
|
2805
2851
|
// this does not depend on `mcpJsonInjection` being configured at all — see
|
|
2806
2852
|
// `prepareClaudeConfigDirIfNeeded`'s own doc for why). Placed here only to stay adjacent to the
|
|
2807
2853
|
// other pre-`computeChildEnv` staging steps; `claude.ts`'s `buildArgs` needs no argv change for
|
|
2808
2854
|
// this (CLAUDE_CONFIG_DIR is an env var, not a flag), same as CODEX_HOME.
|
|
2809
|
-
preparedClaudeConfigDir = await prepareClaudeConfigDirIfNeeded({ runId: input.runId, def }, {
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2855
|
+
preparedClaudeConfigDir = await prepareClaudeConfigDirIfNeeded({ input: { runId: input.runId, def }, deps: {
|
|
2856
|
+
enabled: claudeConfigDirIsolationEnabled,
|
|
2857
|
+
claudeConfigDirIsolation,
|
|
2858
|
+
hostEnv: process.env,
|
|
2859
|
+
releaseStagedResources,
|
|
2860
|
+
failBeforeSpawn,
|
|
2861
|
+
} });
|
|
2816
2862
|
// `'config-instructions-file'`'s one effect — stage the overlay to a temp file so
|
|
2817
2863
|
// `computeChildEnv` below has a real path to merge into that def's `instructions` array. A
|
|
2818
2864
|
// no-op (`null`) for every other def/strategy or a run with no overlay at all. Independent of
|
|
2819
2865
|
// `mcpBridge`/`preparedCodexHome` above (a different strategy field entirely), so placed here
|
|
2820
2866
|
// only to stay adjacent to the other pre-`computeChildEnv` staging steps, not for any ordering
|
|
2821
2867
|
// requirement between them.
|
|
2822
|
-
preparedSystemPromptOverlayFile = await prepareSystemPromptOverlayFileIfNeeded({ runId: input.runId, def, overlay: systemPromptOverlay }, { releaseStagedResources, failBeforeSpawn });
|
|
2868
|
+
preparedSystemPromptOverlayFile = await prepareSystemPromptOverlayFileIfNeeded({ input: { runId: input.runId, def, overlay: systemPromptOverlay }, deps: { releaseStagedResources, failBeforeSpawn } });
|
|
2823
2869
|
// Computed only now, not right after `mcpBridge` resolution: mechanism 5's directory path is
|
|
2824
2870
|
// not known until the staging step directly above actually runs `mkdtemp` (see
|
|
2825
2871
|
// `McpBridgeDelivery`'s `'codex-toml'` variant doc for why it cannot be pre-computed the way
|
|
2826
2872
|
// `'claude-mcp-json'`'s deterministic path is). Nothing between the old, earlier call site and
|
|
2827
2873
|
// here ever read `childEnv`, so moving the call cost nothing.
|
|
2828
|
-
const childEnv = computeChildEnv(spawnEnv, mcpBridge
|
|
2829
|
-
? {
|
|
2830
|
-
|
|
2874
|
+
const childEnv = computeChildEnv({ spawnEnv: spawnEnv, mcpBridge: mcpBridge }, {
|
|
2875
|
+
...(preparedCodexHome === null ? {} : { codexHomeDir: preparedCodexHome.path }),
|
|
2876
|
+
systemPromptEnvOverrides,
|
|
2877
|
+
...(preparedSystemPromptOverlayFile && def.systemPromptDelivery?.strategy === 'config-instructions-file'
|
|
2878
|
+
? { stagedInstructionsFile: { varName: def.systemPromptDelivery.varName, path: preparedSystemPromptOverlayFile.path } }
|
|
2879
|
+
: {}),
|
|
2880
|
+
...(preparedClaudeConfigDir === null ? {} : { claudeConfigDir: preparedClaudeConfigDir.path }),
|
|
2881
|
+
});
|
|
2831
2882
|
// Post-buildArgs guard for argv-bound defs whose resolved binary is a
|
|
2832
2883
|
// Windows .cmd/.bat shim or a direct .exe: a prompt under the raw byte
|
|
2833
2884
|
// budget can still expand past CreateProcess's command-line cap once
|
|
2834
2885
|
// quote-escaped. Both are no-ops off-Windows / for non-argv-bound defs.
|
|
2835
|
-
await guardWindowsCommandLineBudget({ runId: input.runId, def, launchPath: launch.launchPath, args }, { releaseStagedResources, failBeforeSpawn });
|
|
2886
|
+
await guardWindowsCommandLineBudget({ input: { runId: input.runId, def, launchPath: launch.launchPath, args }, deps: { releaseStagedResources, failBeforeSpawn } });
|
|
2836
2887
|
const invocation = createCommandInvocationFn({ command: launch.launchPath, args, env: childEnv });
|
|
2837
2888
|
// Kept a synchronous call (no `await`) on purpose — see `spawnAgentChildProcess`'s own doc for
|
|
2838
2889
|
// the microtask-timing race this avoids. The error branch's own cleanup/failBeforeSpawn calls are
|
|
2839
2890
|
// async, which is fine: no child exists yet on that path, so nothing is racing a listener.
|
|
2840
|
-
const spawnResult = spawnAgentChildProcess({ cwd: input.cwd, childEnv, invocation }, { spawn: spawnFn });
|
|
2891
|
+
const spawnResult = spawnAgentChildProcess({ input: { cwd: input.cwd, childEnv, invocation }, deps: { spawn: spawnFn } });
|
|
2841
2892
|
if (spawnResult.kind === 'error') {
|
|
2842
2893
|
await releaseStagedResources();
|
|
2843
|
-
return failBeforeSpawn(input.runId, 'AGENT_SPAWN_FAILED', `AgentExecutor: spawn threw synchronously for agent "${def.id}": ${errorMessage(spawnResult.error)}`);
|
|
2894
|
+
return failBeforeSpawn({ runId: input.runId, code: 'AGENT_SPAWN_FAILED', message: `AgentExecutor: spawn threw synchronously for agent "${def.id}": ${errorMessage(spawnResult.error)}` });
|
|
2844
2895
|
}
|
|
2845
2896
|
const child = spawnResult.child;
|
|
2846
2897
|
// Registered before the spawn-confirmation await below, for the same reason
|
|
@@ -2850,11 +2901,12 @@ export function createAgentExecutor(options) {
|
|
|
2850
2901
|
// grandchild inheriting them can delay arbitrarily). A spawn that never produced a process at
|
|
2851
2902
|
// all emits no `'exit'`, and is covered instead by `releaseStagedResources` on the reject path.
|
|
2852
2903
|
child.once('exit', releaseRuntimeLock);
|
|
2853
|
-
const
|
|
2904
|
+
const formatForStdin = { streamFormat };
|
|
2905
|
+
const stdinHandle = isStdinDrivenFormat(formatForStdin)
|
|
2854
2906
|
? wireChildLifecycle({
|
|
2855
2907
|
runId: input.runId,
|
|
2856
2908
|
def,
|
|
2857
|
-
streamFormat,
|
|
2909
|
+
streamFormat: formatForStdin.streamFormat,
|
|
2858
2910
|
child,
|
|
2859
2911
|
lifecycle,
|
|
2860
2912
|
listProcessSnapshots: listProcessSnapshotsFn,
|
|
@@ -2866,75 +2918,76 @@ export function createAgentExecutor(options) {
|
|
|
2866
2918
|
continuation,
|
|
2867
2919
|
classifyFailure,
|
|
2868
2920
|
bufferedStdoutMaxBytes,
|
|
2921
|
+
expectsJiniBridge: writtenMcpJsonPath !== undefined,
|
|
2869
2922
|
})
|
|
2870
2923
|
: null;
|
|
2871
|
-
await confirmChildSpawned({ runId: input.runId, def, child }, { releaseStagedResources, failBeforeSpawn });
|
|
2924
|
+
await confirmChildSpawned({ input: { runId: input.runId, def, child }, deps: { releaseStagedResources, failBeforeSpawn } });
|
|
2872
2925
|
// Now — and only now — is there a live process that could consume the locked side effect, so
|
|
2873
2926
|
// this is where a def's handoff watcher starts. Deliberately not awaited: the whole point is to
|
|
2874
2927
|
// release the lock as soon as the child confirms the handoff, in parallel with this run
|
|
2875
2928
|
// continuing. Rejection releases too — a lock stuck open because a watcher threw is strictly
|
|
2876
2929
|
// worse than releasing early (see `RuntimeLockHold.waitForHandoff`'s own doc).
|
|
2877
|
-
armHandoffWatcher(runtimeLockHold, { logFilePath: preparedLogFile?.path, model: selectedModel, processExited: processExitedController.signal }, releaseRuntimeLock);
|
|
2930
|
+
armHandoffWatcher({ runtimeLockHold: runtimeLockHold, handoffInput: { logFilePath: preparedLogFile?.path, model: selectedModel, processExited: processExitedController.signal }, release: releaseRuntimeLock });
|
|
2878
2931
|
if (streamFormat === 'acp-json-rpc') {
|
|
2879
|
-
await runAcpDispatch({
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2932
|
+
await runAcpDispatch({ input: {
|
|
2933
|
+
runId: input.runId,
|
|
2934
|
+
agentId: def.id,
|
|
2935
|
+
child,
|
|
2936
|
+
// `overlayDelivery.promptPrefix` applied to `input.prompt`, NOT `overlayDelivery.prompt`:
|
|
2937
|
+
// this call site deliberately sends the raw input prompt rather than the image-delivery
|
|
2938
|
+
// rewrite (see `resolveImageDeliveryAndArgvBudget`'s call site above — an ACP def delivers
|
|
2939
|
+
// images natively and must never also get `'prompt-path'`'s appended paths), and the prefix
|
|
2940
|
+
// is the part of the overlay decision that applies to whatever prompt text a transport was
|
|
2941
|
+
// already sending. `''` for `reasonix` (env-var) and any resumed ACP session, so those keep
|
|
2942
|
+
// sending byte-identical text and never receive the overlay twice.
|
|
2943
|
+
prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
|
|
2944
|
+
cwd: input.cwd,
|
|
2945
|
+
model: input.model,
|
|
2946
|
+
imagePaths: input.imagePaths ?? [],
|
|
2947
|
+
envFormat: def.acpMcpEnvFormat,
|
|
2948
|
+
mcpBridge,
|
|
2949
|
+
}, deps: {
|
|
2950
|
+
lifecycle,
|
|
2951
|
+
attachAcpSession: attachAcpSessionFn,
|
|
2952
|
+
onPermissionRequest: options.acpPermissionHandler,
|
|
2953
|
+
listProcessSnapshots: listProcessSnapshotsFn,
|
|
2954
|
+
collectProcessTreePids: collectProcessTreePidsFn,
|
|
2955
|
+
stopProcesses: stopProcessesFn,
|
|
2956
|
+
onCleanupFailure: onCleanupFailureFn,
|
|
2957
|
+
cleanupStagedFiles,
|
|
2958
|
+
journal,
|
|
2959
|
+
classifyFailure,
|
|
2960
|
+
releaseStagedResources,
|
|
2961
|
+
failBeforeSpawn,
|
|
2962
|
+
} });
|
|
2910
2963
|
return;
|
|
2911
2964
|
}
|
|
2912
2965
|
if (streamFormat === 'pi-rpc') {
|
|
2913
|
-
await runPiRpcDispatch({
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2966
|
+
await runPiRpcDispatch({ input: {
|
|
2967
|
+
runId: input.runId,
|
|
2968
|
+
agentId: def.id,
|
|
2969
|
+
child,
|
|
2970
|
+
// Same reasoning as the ACP call site above. `pi` declares an `'append-flag'` strategy,
|
|
2971
|
+
// so its prefix is `''` and this stays byte-identical to the raw prompt — the overlay
|
|
2972
|
+
// rides `--append-system-prompt` instead, exactly once.
|
|
2973
|
+
prompt: `${overlayDelivery.promptPrefix}${input.prompt}`,
|
|
2974
|
+
cwd: input.cwd,
|
|
2975
|
+
model: input.model,
|
|
2976
|
+
imagePaths: input.imagePaths ?? [],
|
|
2977
|
+
uploadRoot: input.uploadRoot,
|
|
2978
|
+
}, deps: {
|
|
2979
|
+
lifecycle,
|
|
2980
|
+
attachPiRpcSession: attachPiRpcSessionFn,
|
|
2981
|
+
listProcessSnapshots: listProcessSnapshotsFn,
|
|
2982
|
+
collectProcessTreePids: collectProcessTreePidsFn,
|
|
2983
|
+
stopProcesses: stopProcessesFn,
|
|
2984
|
+
onCleanupFailure: onCleanupFailureFn,
|
|
2985
|
+
cleanupStagedFiles,
|
|
2986
|
+
journal,
|
|
2987
|
+
classifyFailure,
|
|
2988
|
+
releaseStagedResources,
|
|
2989
|
+
failBeforeSpawn,
|
|
2990
|
+
} });
|
|
2938
2991
|
return;
|
|
2939
2992
|
}
|
|
2940
2993
|
// The overlay reaches stdin only for a def that declares stdin as its prompt transport. The
|