@jini-ai/daemon 0.6.10 → 0.6.11
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 +13 -0
- package/dist/agent-executor/contracts.d.ts +28 -11
- package/dist/agent-executor/events.d.ts +3 -15
- package/dist/agent-executor/events.js +84 -181
- package/dist/agent-executor/launch.d.ts +9 -3
- package/dist/agent-executor/launch.js +95 -8
- package/dist/agent-executor/live-session-state.d.ts +23 -0
- package/dist/agent-executor/live-session-state.js +155 -0
- package/dist/agent-executor/mcp-bridge.d.ts +3 -1
- package/dist/agent-executor/mcp-bridge.js +9 -2
- package/dist/agent-executor/message-relay.d.ts +39 -0
- package/dist/agent-executor/message-relay.js +56 -0
- package/dist/core/read-only-tools.d.ts +9 -4
- package/dist/core/read-only-tools.js +10 -7
- package/dist/core/tool-candidates.d.ts +8 -3
- package/dist/core/tool-candidates.js +39 -4
- package/dist/http/attachments.d.ts +13 -0
- package/dist/http/attachments.js +27 -1
- package/dist/http/delegated-tools.js +1 -1
- package/dist/http/index.d.ts +2 -1
- package/dist/http/index.js +3 -1
- package/dist/http/message-relay.d.ts +21 -0
- package/dist/http/message-relay.js +44 -0
- package/dist/http/runs.d.ts +5 -2
- package/dist/http/runs.js +11 -5
- package/dist/tool-executor.js +5 -4
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 0.6.11
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- Mid-run message relay for MCP-bridged engines (Codex, ACP, Antigravity): `registerMessageRelayRoutes` on `@jini-ai/daemon/http`; mid-run messages queue instead of interrupting, and recovery waits on an uncertain probe.
|
|
6
|
+
- Site agents are confined to the site folder: Claude file tools are bounded to the site root and private temp dir, and attachments are staged inside the site (`stageAttachmentClaim`).
|
|
7
|
+
- Tool inputs apply a sole near-miss and declared input aliases; plugin-load outcomes are reported; `web_fetch` reads SVG/JS as text; `content_read` suggests its resource first; the browser falls back to system Chrome.
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- Updated dependencies
|
|
10
|
+
- Updated dependencies
|
|
11
|
+
- @jini-ai/core@0.6.11
|
|
12
|
+
- @jini-ai/protocol@0.6.11
|
|
13
|
+
|
|
1
14
|
## 0.6.10
|
|
2
15
|
|
|
3
16
|
### Patch Changes
|
|
@@ -4,8 +4,8 @@ import type { MessageAttachmentImage } from '../attachment-content.js';
|
|
|
4
4
|
import type { probeMcpBridge } from './mcp-bridge-probe.js';
|
|
5
5
|
import type { spawn as nodeSpawn, ChildProcess } from 'node:child_process';
|
|
6
6
|
import type { Principal } from '@jini-ai/core';
|
|
7
|
-
import type { RunAgentPayload, RunErrorPayload } from '@jini-ai/protocol';
|
|
8
|
-
import type { AgentRuntimeState, applyAgentLaunchEnv, ensureAgentCapabilities, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, AcpMcpServerInput, AcpPermissionHandler, PromptAugmenter } from '@jini-ai/agent-runtime';
|
|
7
|
+
import type { RunAgentPayload, RunErrorPayload, MessageRelayVia, RelayedUserMessage } from '@jini-ai/protocol';
|
|
8
|
+
import type { AgentRuntimeState, applyAgentLaunchEnv, ensureAgentCapabilities, assertClaudeFilesystemSupport, resolveModelForLaunch, getAgentDef, resolveAgentLaunch, attachAcpSession, attachPiRpcSession, prepareAgentLogFile, preparePromptFileForAgent, AcpMcpServerInput, AcpPermissionHandler, PromptAugmenter } from '@jini-ai/agent-runtime';
|
|
9
9
|
import type { createCommandInvocation } from '@jini-ai/platform/command';
|
|
10
10
|
import type { listProcessSnapshots, ProcessSnapshot, StopProcessesResult } from '@jini-ai/platform/process';
|
|
11
11
|
import type { RunByteJournal } from '../continuation/journal.js';
|
|
@@ -94,7 +94,7 @@ export type AgentRuntimeEventTranslation = {
|
|
|
94
94
|
readonly kind: 'ignored';
|
|
95
95
|
};
|
|
96
96
|
/** Machine-readable failure reasons `run()` can reject with — every one is preceded by a `lifecycle.finish({status:'failed'})` call (see index.ts module doc's Invariant section). */
|
|
97
|
-
export type AgentExecutorErrorCode = 'NATIVE_TOOL_POLICY_UNSUPPORTED' | 'AGENT_MODEL_UNRESOLVED' | 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
|
|
97
|
+
export type AgentExecutorErrorCode = 'FILESYSTEM_POLICY_UNSUPPORTED' | 'FILESYSTEM_POLICY_INVALID' | 'NATIVE_TOOL_POLICY_UNSUPPORTED' | 'AGENT_MODEL_UNRESOLVED' | 'AGENT_PERMISSION_MODE_INVALID' | 'AGENT_NOT_FOUND' | 'AGENT_RUNTIME_UNSUPPORTED' | 'AGENT_BINARY_NOT_RESOLVED' | 'AGENT_SPAWN_FAILED' | 'AGENT_PROMPT_TOO_LARGE';
|
|
98
98
|
/** Thrown by `AgentExecutor.run()` on every failure path — never a bare `Error`, so callers can branch on `.code` instead of parsing `.message`. */
|
|
99
99
|
export declare class AgentExecutorError extends Error {
|
|
100
100
|
readonly code: AgentExecutorErrorCode;
|
|
@@ -104,6 +104,8 @@ export declare class AgentExecutorError extends Error {
|
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
106
|
export interface AgentExecutorRunInput {
|
|
107
|
+
/** Host-selected mandatory filesystem boundary; no user grant may widen it. */
|
|
108
|
+
readonly filesystemRoot?: string;
|
|
107
109
|
readonly runId: string;
|
|
108
110
|
readonly agentId: string;
|
|
109
111
|
readonly prompt: string;
|
|
@@ -205,28 +207,35 @@ export interface AgentExecutor {
|
|
|
205
207
|
* underlying run is always already transitioned to `'failed'` via
|
|
206
208
|
* `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
|
|
207
209
|
*/
|
|
208
|
-
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "nativeToolAccess" | "nativeCapabilities" | "cliParity" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings" | "chromeIntegration">): Promise<void>;
|
|
210
|
+
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "filesystemRoot" | "model" | "reasoning" | "nativeToolAccess" | "nativeCapabilities" | "cliParity" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings" | "chromeIntegration">): Promise<void>;
|
|
209
211
|
/**
|
|
210
212
|
* Delivers a message the human sent while `runId` is going straight into its live agent process
|
|
211
|
-
* (see {@link UserMessageDelivery} for each outcome). A stream-json agent
|
|
212
|
-
*
|
|
213
|
-
*
|
|
213
|
+
* (see {@link UserMessageDelivery} for each outcome). A stream-json agent queues the message
|
|
214
|
+
* directly without interrupting tools. This settles on write acceptance; consumption is reported
|
|
215
|
+
* separately by UUID receipts. It never waits on the agent's reply.
|
|
214
216
|
* Optional `imageContents` contains host-authorized pixels, encoded exactly as in an idle send.
|
|
215
217
|
*/
|
|
216
218
|
sendUserMessage(requiredArgs: {
|
|
217
219
|
readonly runId: string;
|
|
218
220
|
readonly text: string;
|
|
221
|
+
readonly id?: string;
|
|
222
|
+
readonly attachmentIds?: readonly string[];
|
|
219
223
|
}, optionalArgs?: {
|
|
220
224
|
readonly imageContents?: readonly MessageAttachmentImage[];
|
|
221
225
|
}): Promise<UserMessageDelivery>;
|
|
226
|
+
/** Trusted transport supplies the credential-bound run, never model arguments. No I/O or waiting. */
|
|
227
|
+
claimUserMessages(input: {
|
|
228
|
+
readonly runId: string;
|
|
229
|
+
readonly via: MessageRelayVia;
|
|
230
|
+
}, options?: Record<string, never>): readonly RelayedUserMessage[];
|
|
222
231
|
}
|
|
223
232
|
/**
|
|
224
233
|
* What {@link AgentExecutor.sendUserMessage} did with a mid-run message:
|
|
225
|
-
* - `'delivered'` — the
|
|
234
|
+
* - `'delivered'` — the message was queued without interruption into the live
|
|
226
235
|
* session; the agent answers it next, in the same process and session.
|
|
227
236
|
* - `'unsupported'` — the run is live, but its agent has no way to take input mid-run (its prompt
|
|
228
237
|
* is plain text or an RPC request). Queue attachment turns intact after the reply; text-only
|
|
229
|
-
* turns
|
|
238
|
+
* turns retain the next-turn queue fallback.
|
|
230
239
|
* - `'not-running'` — no live process will accept it: an unknown run, a finished one, or one whose
|
|
231
240
|
* last turn already ended and closed stdin. The run is ending; the host should send it next.
|
|
232
241
|
*/
|
|
@@ -273,8 +282,14 @@ export interface StdinCloseHandle {
|
|
|
273
282
|
closeStdinOnce(): void;
|
|
274
283
|
/** Journals a sent-to-stdin byte chunk, queued through the same FIFO {@link wireChildLifecycle} already uses for emitted events. No-op when no journal was configured (see `CreateAgentExecutorOptions.journal`). */
|
|
275
284
|
recordSentBytes(content: string): void;
|
|
276
|
-
/**
|
|
277
|
-
sendUserMessage(
|
|
285
|
+
/** Queues a mid-run user message into this child's open stdin — see {@link AgentExecutor.sendUserMessage}. */
|
|
286
|
+
sendUserMessage(input: {
|
|
287
|
+
text: string;
|
|
288
|
+
id?: string;
|
|
289
|
+
attachmentIds?: readonly string[];
|
|
290
|
+
}, options?: {
|
|
291
|
+
imageContents?: readonly MessageAttachmentImage[];
|
|
292
|
+
}): Promise<UserMessageDelivery>;
|
|
278
293
|
}
|
|
279
294
|
/**
|
|
280
295
|
* Gap 3 (capability-routed continuation transport) — host-owned config for the
|
|
@@ -622,6 +637,8 @@ export interface CreateAgentExecutorOptions {
|
|
|
622
637
|
readonly resolveAgentLaunch?: typeof resolveAgentLaunch;
|
|
623
638
|
/** @default the real `@jini-ai/agent-runtime` once-per-binary `--help` capability probe */
|
|
624
639
|
readonly ensureAgentCapabilities?: typeof ensureAgentCapabilities;
|
|
640
|
+
/** Bounded version/enforcement probe; replaceable for offline host tests. */
|
|
641
|
+
readonly assertClaudeFilesystemSupport?: typeof assertClaudeFilesystemSupport;
|
|
625
642
|
/** @default the real `@jini-ai/agent-runtime` PATH-env composer */
|
|
626
643
|
readonly applyAgentLaunchEnv?: typeof applyAgentLaunchEnv;
|
|
627
644
|
/** @default the real `@jini-ai/platform` cross-platform invocation builder */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DelegatedResultCorrelation } from '../delegated-result-correlation.js';
|
|
2
2
|
import type { StdoutJournalBlobPort } from '../tool-result-diagnostics.js';
|
|
3
|
+
import { type RunMessageRelay } from './message-relay.js';
|
|
3
4
|
import type { probeMcpBridge } from './mcp-bridge-probe.js';
|
|
4
5
|
import type { ModelCatalogSnapshot } from '@jini-ai/agent-runtime';
|
|
5
6
|
import type { ChildProcess } from 'node:child_process';
|
|
@@ -26,6 +27,7 @@ export declare function defaultCleanupFailureSink(context: AgentCleanupFailureCo
|
|
|
26
27
|
*/
|
|
27
28
|
export declare const DEFAULT_BUFFERED_STDOUT_MAX_BYTES: number;
|
|
28
29
|
interface WireChildLifecycleContext extends TerminateChildTreeDeps {
|
|
30
|
+
readonly registerMessageRelay?: (relay: RunMessageRelay) => void;
|
|
29
31
|
readonly delegatedResultCorrelation?: DelegatedResultCorrelation;
|
|
30
32
|
readonly stdoutJournalBlobs?: StdoutJournalBlobPort;
|
|
31
33
|
readonly startingModel?: string;
|
|
@@ -125,21 +127,6 @@ interface WireChildLifecycleContext extends TerminateChildTreeDeps {
|
|
|
125
127
|
* @overallScore 100/100
|
|
126
128
|
*/
|
|
127
129
|
export declare function wireChildLifecycle(ctx: WireChildLifecycleContext): StdinCloseHandle;
|
|
128
|
-
/**
|
|
129
|
-
* Whether a def's live process can take another user message mid-run: only a stream-json stdin
|
|
130
|
-
* transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
|
|
131
|
-
* Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
|
|
132
|
-
* reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
|
|
133
|
-
*/
|
|
134
|
-
export declare function acceptsMidRunUserMessages({ def }: {
|
|
135
|
-
readonly def: RuntimeAgentDef;
|
|
136
|
-
}): boolean;
|
|
137
|
-
/**
|
|
138
|
-
* How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
|
|
139
|
-
* anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
|
|
140
|
-
* the ceiling only bounds an agent that ignores control requests.
|
|
141
|
-
*/
|
|
142
|
-
export declare const INTERRUPT_ACK_TIMEOUT_MS = 5000;
|
|
143
130
|
/**
|
|
144
131
|
* Writes the initial user turn to the child's stdin per `def.promptInputFormat`
|
|
145
132
|
* (both branches only run for `promptViaStdin: true` defs — the only shape
|
|
@@ -174,6 +161,7 @@ interface RunAcpDispatchInput {
|
|
|
174
161
|
readonly mcpBridge: McpBridgeDelivery | null;
|
|
175
162
|
}
|
|
176
163
|
interface RunAcpDispatchDeps extends TerminateChildTreeDeps {
|
|
164
|
+
readonly registerMessageRelay?: (relay: RunMessageRelay) => void;
|
|
177
165
|
readonly lifecycle: RunLifecycle;
|
|
178
166
|
readonly attachAcpSession: typeof attachAcpSession;
|
|
179
167
|
readonly onPermissionRequest: AcpPermissionHandler | undefined;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createToolResultDiagnosticProjection, projectDiagnosticFrame } from '../tool-result-diagnostics.js';
|
|
2
2
|
import { appendSerialTask } from "@jini-ai/core/primitives";
|
|
3
|
+
import { createLiveSessionState } from './live-session-state.js';
|
|
4
|
+
import { acceptsMidRunUserMessages, createMessageRelay } from './message-relay.js';
|
|
3
5
|
import { randomUUID } from 'node:crypto';
|
|
4
6
|
import { createModelReceiptTracker } from './model-receipts.js';
|
|
5
7
|
import { observeRunOutput } from './run-output.js';
|
|
@@ -203,6 +205,16 @@ export const DEFAULT_BUFFERED_STDOUT_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
203
205
|
function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
|
|
204
206
|
return `\n[jini] agent stdout truncated: ${droppedBytes} byte(s) dropped after the ${maxBytes}-byte buffer limit was reached.\n`;
|
|
205
207
|
}
|
|
208
|
+
/** Install once on a driver's FIFO; close synchronously before any terminal handler yields.
|
|
209
|
+
* Native runs omit registration, so they allocate no relay and emit only CLI receipts. */
|
|
210
|
+
function attachMessageRelay({ register, emit, child }) {
|
|
211
|
+
if (!register)
|
|
212
|
+
return undefined;
|
|
213
|
+
const relay = createMessageRelay({ emit });
|
|
214
|
+
register(relay);
|
|
215
|
+
child.once('close', () => relay.close());
|
|
216
|
+
return relay;
|
|
217
|
+
}
|
|
206
218
|
/** Keeps exact-byte auditing separate from retained diagnostics on the run's existing FIFO.
|
|
207
219
|
* Projection retains at most one bounded frame; ordinary stdout keeps its legacy byte journal. */
|
|
208
220
|
function createStdoutDiagnostics({ ctx, enqueueEmit }, _options = {}) {
|
|
@@ -314,9 +326,8 @@ export function wireChildLifecycle(ctx) {
|
|
|
314
326
|
// only ever acted on when a host has explicitly allowlisted the tool's name.
|
|
315
327
|
let pendingToolUse;
|
|
316
328
|
// Observe the canonical lifecycle, including delegated calls that bypass the CLI parser.
|
|
317
|
-
// A single latest tool_use cannot represent parallel
|
|
329
|
+
// A single latest tool_use cannot represent all parallel calls at explicit cancellation.
|
|
318
330
|
const unresolvedToolCalls = new Set();
|
|
319
|
-
const interruptedToolCalls = new Set();
|
|
320
331
|
const toolSubscription = lifecycle.stream({ runId, onEvent: (event) => {
|
|
321
332
|
if (event.kind !== 'agent')
|
|
322
333
|
return;
|
|
@@ -332,9 +343,9 @@ export function wireChildLifecycle(ctx) {
|
|
|
332
343
|
});
|
|
333
344
|
/** Run on the same FIFO as parsed results, after the aborted segment has drained.
|
|
334
345
|
* O(t) in unresolved calls; completed results are never replaced with cancellation. */
|
|
335
|
-
async function cancelUnresolvedToolCalls(
|
|
346
|
+
async function cancelUnresolvedToolCalls() {
|
|
336
347
|
await toolSubscription;
|
|
337
|
-
for (const toolUseId of
|
|
348
|
+
for (const toolUseId of [...unresolvedToolCalls]) {
|
|
338
349
|
if (!unresolvedToolCalls.has(toolUseId))
|
|
339
350
|
continue;
|
|
340
351
|
await lifecycle.emit({ runId, input: { event: 'agent', data: {
|
|
@@ -373,71 +384,46 @@ export function wireChildLifecycle(ctx) {
|
|
|
373
384
|
stdinClosed = true;
|
|
374
385
|
child.stdin?.end();
|
|
375
386
|
}
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
// runs, an `end_turn` is not the run's end: the CLI keeps going until they finish and reports on
|
|
386
|
-
// them, so stdin must stay open for mid-run messages. Closing it there (2026-10-10, plannie chat)
|
|
387
|
-
// made every message sent while two subagents worked come back 'not-running' and wait for the run
|
|
388
|
-
// to end — and at EOF the CLI stops background tasks at its own wait ceiling (10 minutes).
|
|
389
|
-
let backgroundTaskCount = 0;
|
|
390
|
-
// Set when a turn ended and stdin stayed open only for background tasks: no turn is running, so a
|
|
391
|
-
// mid-run message has nothing to interrupt, and the last task finishing closes stdin (below).
|
|
387
|
+
// Unanswered UUIDs and the CLI's queued_turn_count keep stdin open across intermediate turn ends.
|
|
388
|
+
// Consumption is confirmed by CLI reply/result attribution, never by HTTP delivery or an echo.
|
|
389
|
+
const emitInput = (data) => enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data } }));
|
|
390
|
+
const session = createLiveSessionState({ emit: emitInput });
|
|
391
|
+
const relay = attachMessageRelay({ register: ctx.registerMessageRelay, emit: emitInput, child });
|
|
392
|
+
// Claude's background tasks can outlive a parent turn. Keep the existing hold until the last
|
|
393
|
+
// task finishes; closing stdin early stops them at the CLI's background-wait ceiling.
|
|
394
|
+
// 2026-10-10 (plannie chat): closing at the parent's end_turn made messages return
|
|
395
|
+
// 'not-running' for ten minutes, then EOF killed the still-working subagents at that ceiling.
|
|
392
396
|
let heldForBackgroundTasks = false;
|
|
393
|
-
|
|
397
|
+
let heldForQueuedInput = false;
|
|
394
398
|
let controlScanBuffer = '';
|
|
395
|
-
/**
|
|
396
|
-
|
|
397
|
-
const requestId = controlResponseRequestId({ line: line });
|
|
398
|
-
const settle = requestId === undefined ? undefined : pendingInterruptAcks.get(requestId);
|
|
399
|
-
if (!settle)
|
|
400
|
-
return;
|
|
401
|
-
pendingInterruptAcks.delete(requestId);
|
|
402
|
-
settle(true);
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Tracks a `background_tasks_changed` line. The last background task finishing while stdin is
|
|
406
|
-
* held only for them closes it, exactly as the turn end would have: the CLI then answers what it
|
|
407
|
-
* still owes (the tasks' report) and exits. @complexity O(line length).
|
|
408
|
-
*/
|
|
409
|
-
function updateBackgroundTasks(line) {
|
|
410
|
-
const count = backgroundTaskListLength({ line: line });
|
|
411
|
-
if (count === undefined)
|
|
412
|
-
return;
|
|
413
|
-
backgroundTaskCount = count;
|
|
414
|
-
if (count === 0 && heldForBackgroundTasks) {
|
|
415
|
-
heldForBackgroundTasks = false;
|
|
416
|
-
closeStdinOnce();
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Reads the stdout lines a mid-run message depends on: interrupt acks and background-task
|
|
421
|
-
* changes. Only agents that take mid-run messages are scanned. A chunk with no newline is only
|
|
422
|
-
* appended, so a long line split across many chunks is split once, not once per chunk.
|
|
423
|
-
* @complexity O(chunk length) amortized.
|
|
424
|
-
*/
|
|
399
|
+
/** Scan and parse in wire order: a receipt later in a chunk cannot affect an earlier turn end.
|
|
400
|
+
* O(chunk length) amortized; only the partial line remains buffered. */
|
|
425
401
|
function scanControlLines(text) {
|
|
426
|
-
if (!acceptsMidRunUserMessages({ def: def }))
|
|
427
|
-
return;
|
|
428
402
|
controlScanBuffer += text;
|
|
429
403
|
if (!text.includes('\n'))
|
|
430
404
|
return;
|
|
431
|
-
// `text` holds a newline, so the buffer does: split off the complete lines, keep the partial tail.
|
|
432
405
|
const lastNewline = controlScanBuffer.lastIndexOf('\n');
|
|
433
406
|
const lines = controlScanBuffer.slice(0, lastNewline).split('\n');
|
|
434
407
|
controlScanBuffer = controlScanBuffer.slice(lastNewline + 1);
|
|
435
|
-
for (const line of lines)
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
408
|
+
for (const line of lines)
|
|
409
|
+
feedSessionLine(line);
|
|
410
|
+
}
|
|
411
|
+
function feedSessionLine(line) {
|
|
412
|
+
const observed = session.observe({ line });
|
|
413
|
+
if (observed.startedInput)
|
|
414
|
+
heldForQueuedInput = false;
|
|
415
|
+
streamHandler.feed({ chunk: `${line}\n` });
|
|
416
|
+
if (session.hasPendingInput())
|
|
417
|
+
return;
|
|
418
|
+
if (heldForBackgroundTasks && session.backgroundTaskCount() === 0) {
|
|
419
|
+
heldForBackgroundTasks = false;
|
|
420
|
+
closeStdinOnce();
|
|
421
|
+
}
|
|
422
|
+
// A result can lower queued_turn_count after the assistant end already held stdin open.
|
|
423
|
+
// Re-evaluate that held end after parsing the result (the parser deduplicates turn ends).
|
|
424
|
+
if (observed.result && heldForQueuedInput && session.backgroundTaskCount() === 0) {
|
|
425
|
+
heldForQueuedInput = false;
|
|
426
|
+
closeStdinOnce();
|
|
441
427
|
}
|
|
442
428
|
}
|
|
443
429
|
/**
|
|
@@ -459,25 +445,15 @@ export function wireChildLifecycle(ctx) {
|
|
|
459
445
|
if (journal)
|
|
460
446
|
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
|
|
461
447
|
}
|
|
462
|
-
/**
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* message or task report), `'turn-continues'` for the reasons below, `'close'` otherwise. Open
|
|
466
|
-
* because the run is not over: a `tool_use` stop (the CLI runs the tool itself and carries on, and a
|
|
467
|
-
* message sent while the tool runs must still reach it), any end while an interrupt still waits
|
|
468
|
-
* for its ack (the message is about to be written), and the end of a turn an interrupt aborted
|
|
469
|
-
* (the message written after it starts the next turn), and any end while background tasks still
|
|
470
|
-
* run (see `backgroundTaskCount`). Every other end closes stdin, so the CLI exits once it has
|
|
471
|
-
* answered everything written before EOF.
|
|
472
|
-
*/
|
|
448
|
+
/** A tool_use end is intermediate: the CLI executes that batch itself and carries on.
|
|
449
|
+
* Pending input and background tasks also owe work. Every other end closes stdin so the CLI
|
|
450
|
+
* exits once it has answered everything written before EOF. */
|
|
473
451
|
function stdinHoldAfterTurnEnd(stopReason) {
|
|
474
|
-
if (!acceptsMidRunUserMessages({ def
|
|
452
|
+
if (!acceptsMidRunUserMessages({ def }))
|
|
475
453
|
return 'close';
|
|
476
|
-
if (stopReason === 'tool_use' ||
|
|
477
|
-
return 'turn-continues';
|
|
478
|
-
if (interruptSent && stopReason !== undefined && stopReason.startsWith('aborted'))
|
|
454
|
+
if (stopReason === 'tool_use' || session.hasPendingInput())
|
|
479
455
|
return 'turn-continues';
|
|
480
|
-
return backgroundTaskCount > 0 ? 'background-tasks' : 'close';
|
|
456
|
+
return session.backgroundTaskCount() > 0 ? 'background-tasks' : 'close';
|
|
481
457
|
}
|
|
482
458
|
/**
|
|
483
459
|
* Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
|
|
@@ -486,11 +462,6 @@ export function wireChildLifecycle(ctx) {
|
|
|
486
462
|
* unconfigured or the pending tool isn't allowlisted).
|
|
487
463
|
*/
|
|
488
464
|
function handleTurnEnd(stopReason) {
|
|
489
|
-
if (stopReason?.startsWith('aborted'))
|
|
490
|
-
enqueueEmit(async () => {
|
|
491
|
-
await cancelUnresolvedToolCalls([...interruptedToolCalls]);
|
|
492
|
-
interruptedToolCalls.clear();
|
|
493
|
-
});
|
|
494
465
|
const toolUse = pendingToolUse;
|
|
495
466
|
const shouldInject = stopReason === 'tool_use' &&
|
|
496
467
|
toolUse !== undefined &&
|
|
@@ -500,6 +471,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
500
471
|
if (!shouldInject) {
|
|
501
472
|
const hold = stdinHoldAfterTurnEnd(stopReason);
|
|
502
473
|
heldForBackgroundTasks = hold === 'background-tasks';
|
|
474
|
+
heldForQueuedInput = hold === 'turn-continues' && stopReason !== 'tool_use';
|
|
503
475
|
if (hold === 'close')
|
|
504
476
|
closeStdinOnce();
|
|
505
477
|
return;
|
|
@@ -688,13 +660,12 @@ export function wireChildLifecycle(ctx) {
|
|
|
688
660
|
return;
|
|
689
661
|
}
|
|
690
662
|
stdoutDiagnostics.feed({ text });
|
|
691
|
-
//
|
|
692
|
-
//
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
streamHandler.feed({ chunk: text });
|
|
663
|
+
// Receipts, background membership and turn ends must be processed in the same wire order.
|
|
664
|
+
// streamHandler is non-null here: the plain-format branch returned above.
|
|
665
|
+
if (acceptsMidRunUserMessages({ def }))
|
|
666
|
+
scanControlLines(text);
|
|
667
|
+
else
|
|
668
|
+
streamHandler.feed({ chunk: text });
|
|
698
669
|
});
|
|
699
670
|
child.stderr?.on('data', (chunk) => {
|
|
700
671
|
const text = chunk.toString('utf8');
|
|
@@ -717,9 +688,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
717
688
|
void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
|
|
718
689
|
} });
|
|
719
690
|
child.on('close', (code, signal) => {
|
|
720
|
-
|
|
721
|
-
settle(false);
|
|
722
|
-
pendingInterruptAcks.clear();
|
|
691
|
+
stdinClosed = true;
|
|
723
692
|
void (async () => {
|
|
724
693
|
// Not wrapped in try/catch: all 4 supported parser factories'
|
|
725
694
|
// flush() implementations already internally guard their own
|
|
@@ -729,7 +698,12 @@ export function wireChildLifecycle(ctx) {
|
|
|
729
698
|
// here would be dead code for the fixed, closed set of parsers this
|
|
730
699
|
// driver dispatches to. `streamHandler` is null for `'plain'` (no
|
|
731
700
|
// parser, hence nothing to flush) — `?.` skips it cleanly.
|
|
701
|
+
if (controlScanBuffer) {
|
|
702
|
+
feedSessionLine(controlScanBuffer);
|
|
703
|
+
controlScanBuffer = '';
|
|
704
|
+
}
|
|
732
705
|
streamHandler?.flush();
|
|
706
|
+
session.endSession();
|
|
733
707
|
stdoutDiagnostics.flush();
|
|
734
708
|
// Queued before `await emitQueue` so the flushed text is durably appended ahead of
|
|
735
709
|
// `finish()`'s `'end'` event, exactly like every live-path emit already is.
|
|
@@ -761,63 +735,27 @@ export function wireChildLifecycle(ctx) {
|
|
|
761
735
|
})
|
|
762
736
|
: false;
|
|
763
737
|
}
|
|
764
|
-
/**
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
* {@link INTERRUPT_ACK_TIMEOUT_MS}: the CLI then folds the line in at its next step. Once
|
|
772
|
-
* `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
|
|
773
|
-
* reach — `'not-running'`. Between turns, while stdin is held only for background tasks, there is
|
|
774
|
-
* no generation to stop: the line is written at once and starts the next turn.
|
|
775
|
-
* Host-prepared images use the same content blocks as the initial stdin turn; no file tools
|
|
776
|
-
* or new directory grants are needed to see these pixels.
|
|
777
|
-
*/
|
|
778
|
-
async function sendUserMessage(text, images = []) {
|
|
779
|
-
if (!acceptsMidRunUserMessages({ def: def }))
|
|
780
|
-
return 'unsupported';
|
|
738
|
+
/** Queue into the existing Claude session without interrupting parallel tools or subagents.
|
|
739
|
+
* Images use the idle-send encoder without file tools or new directory grants; journals keep
|
|
740
|
+
* text without base64. HTTP acceptance only
|
|
741
|
+
* confirms the write; UUID-attributed CLI receipts separately confirm consumption. */
|
|
742
|
+
async function sendUserMessage({ text, id = randomUUID(), attachmentIds }, { imageContents: images = [] } = {}) {
|
|
743
|
+
if (!acceptsMidRunUserMessages({ def }))
|
|
744
|
+
return relay?.enqueue({ id, text, hasAttachments: images.length > 0 || (attachmentIds?.length ?? 0) > 0 }) ?? 'unsupported';
|
|
781
745
|
const stdin = child.stdin;
|
|
782
746
|
if (stdinClosed || !stdin)
|
|
783
747
|
return 'not-running';
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
writeUserMessage(stdin, text, images);
|
|
787
|
-
return 'delivered';
|
|
788
|
-
}
|
|
789
|
-
const requestId = `jini-interrupt-${randomUUID()}`;
|
|
790
|
-
interruptSent = true;
|
|
791
|
-
// Capture before requesting the interrupt. Its result can precede the acknowledgement, or
|
|
792
|
-
// arrive after the next segment starts: neither ordering may cancel that segment's new tools.
|
|
793
|
-
enqueueEmit(async () => {
|
|
794
|
-
await toolSubscription;
|
|
795
|
-
for (const id of interruptedToolCalls)
|
|
796
|
-
if (!unresolvedToolCalls.has(id))
|
|
797
|
-
interruptedToolCalls.delete(id);
|
|
798
|
-
for (const id of unresolvedToolCalls)
|
|
799
|
-
interruptedToolCalls.add(id);
|
|
800
|
-
});
|
|
801
|
-
const acked = new Promise((resolve) => pendingInterruptAcks.set(requestId, resolve));
|
|
802
|
-
stdin.write(`${JSON.stringify({ type: 'control_request', request_id: requestId, request: { subtype: 'interrupt' } })}\n`, 'utf8');
|
|
803
|
-
let timer;
|
|
804
|
-
const timedOut = new Promise((resolve) => {
|
|
805
|
-
timer = setTimeout(() => resolve(true), INTERRUPT_ACK_TIMEOUT_MS);
|
|
806
|
-
});
|
|
807
|
-
const processAlive = await Promise.race([acked, timedOut]);
|
|
808
|
-
clearTimeout(timer);
|
|
809
|
-
pendingInterruptAcks.delete(requestId);
|
|
810
|
-
if (!processAlive || stdinClosed)
|
|
811
|
-
return 'not-running';
|
|
812
|
-
writeUserMessage(stdin, text, images);
|
|
748
|
+
heldForBackgroundTasks = false;
|
|
749
|
+
writeUserMessage(stdin, text, images, id);
|
|
813
750
|
return 'delivered';
|
|
814
751
|
}
|
|
815
|
-
/** Writes a complete mid-run turn using the idle image encoder; journals text without base64.
|
|
816
|
-
|
|
817
|
-
|
|
752
|
+
/** Writes a complete mid-run turn using the idle image encoder; journals text without base64.
|
|
753
|
+
* The same UUID identifies stdin, queue acceptance and subsequent consumption receipts. */
|
|
754
|
+
function writeUserMessage(stdin, text, images, id) {
|
|
755
|
+
session.queue({ id, text });
|
|
756
|
+
stdin.write(`${JSON.stringify({ ...streamJsonUserMessage({ content: messageContentWithImages({ prompt: text, images }, {}) }), uuid: id })}\n`, 'utf8');
|
|
818
757
|
if (journal)
|
|
819
|
-
enqueueEmit(() => journal.record({ runId
|
|
820
|
-
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
|
|
758
|
+
enqueueEmit(() => journal.record({ runId, entry: sentJournalEntry(text) }));
|
|
821
759
|
}
|
|
822
760
|
return {
|
|
823
761
|
closeStdinOnce,
|
|
@@ -828,45 +766,6 @@ export function wireChildLifecycle(ctx) {
|
|
|
828
766
|
sendUserMessage,
|
|
829
767
|
};
|
|
830
768
|
}
|
|
831
|
-
/**
|
|
832
|
-
* Whether a def's live process can take another user message mid-run: only a stream-json stdin
|
|
833
|
-
* transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
|
|
834
|
-
* Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
|
|
835
|
-
* reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
|
|
836
|
-
*/
|
|
837
|
-
export function acceptsMidRunUserMessages({ def }) {
|
|
838
|
-
return def.promptViaStdin === true && def.promptInputFormat === 'stream-json';
|
|
839
|
-
}
|
|
840
|
-
/**
|
|
841
|
-
* How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
|
|
842
|
-
* anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
|
|
843
|
-
* the ceiling only bounds an agent that ignores control requests.
|
|
844
|
-
*/
|
|
845
|
-
export const INTERRUPT_ACK_TIMEOUT_MS = 5_000;
|
|
846
|
-
/** The `request_id` of a stream-json `control_response` line, or `undefined` for anything else. */
|
|
847
|
-
function controlResponseRequestId({ line }) {
|
|
848
|
-
try {
|
|
849
|
-
const parsed = JSON.parse(line);
|
|
850
|
-
if (!isRecord(parsed) || parsed.type !== 'control_response' || !isRecord(parsed.response))
|
|
851
|
-
return undefined;
|
|
852
|
-
return typeof parsed.response.request_id === 'string' ? parsed.response.request_id : undefined;
|
|
853
|
-
}
|
|
854
|
-
catch {
|
|
855
|
-
return undefined;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
/** How many tasks a `background_tasks_changed` line lists, or `undefined` for any other line. */
|
|
859
|
-
function backgroundTaskListLength({ line }) {
|
|
860
|
-
try {
|
|
861
|
-
const parsed = JSON.parse(line);
|
|
862
|
-
if (!isRecord(parsed) || parsed.type !== 'system' || parsed.subtype !== 'background_tasks_changed')
|
|
863
|
-
return undefined;
|
|
864
|
-
return Array.isArray(parsed.tasks) ? parsed.tasks.length : undefined;
|
|
865
|
-
}
|
|
866
|
-
catch {
|
|
867
|
-
return undefined;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
769
|
/** The stream-json stdin envelope for one user message — the initial prompt and every mid-run one. */
|
|
871
770
|
function streamJsonUserMessage({ content }) {
|
|
872
771
|
return { type: 'user', message: { role: 'user', content: content } };
|
|
@@ -910,6 +809,9 @@ function wireAcpLifecycle(ctx) {
|
|
|
910
809
|
function enqueueEmit(task) {
|
|
911
810
|
emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
|
|
912
811
|
}
|
|
812
|
+
attachMessageRelay({ register: ctx.registerMessageRelay, child,
|
|
813
|
+
emit: data => enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data } })),
|
|
814
|
+
});
|
|
913
815
|
if (ctx.model)
|
|
914
816
|
enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.model } } }));
|
|
915
817
|
child.stdout?.on('data', (chunk) => {
|
|
@@ -1109,6 +1011,7 @@ export function writePromptToStdin(def, child, prompt, handle) {
|
|
|
1109
1011
|
export async function runAcpDispatch({ input, deps }) {
|
|
1110
1012
|
try {
|
|
1111
1013
|
wireAcpLifecycle({
|
|
1014
|
+
...(deps.registerMessageRelay ? { registerMessageRelay: deps.registerMessageRelay } : {}),
|
|
1112
1015
|
runId: input.runId,
|
|
1113
1016
|
agentId: input.agentId,
|
|
1114
1017
|
child: input.child,
|
|
@@ -186,10 +186,16 @@ export declare function computeRuntimeContext({ preparedPromptFile, preparedLogF
|
|
|
186
186
|
readonly resumeSessionId?: string | null;
|
|
187
187
|
readonly newSessionId?: string;
|
|
188
188
|
}): RuntimeContext | undefined;
|
|
189
|
+
type AgentBuildInput = Parameters<typeof buildAgentBuildArgsOptions>[0]['input'];
|
|
189
190
|
/** Phase 9a: the def's `buildArgs` 4th argument. Historically omitted when every override was absent;
|
|
190
191
|
* that let defs silently enable auto-approval. Always supply the validated restricted default now. Pure. */
|
|
191
192
|
export declare function buildAgentBuildArgsOptions({ input, systemPromptOverlay }: {
|
|
192
|
-
readonly input: Pick<AgentExecutorRunInput, 'model' | 'reasoning' | 'permissionMode' | 'nativeToolAccess' | 'nativeCapabilities' | 'cliParity' | 'disallowedTools' | 'allowedTools' | 'settingSources' | 'settings' | 'chromeIntegration'
|
|
193
|
+
readonly input: Pick<AgentExecutorRunInput, 'filesystemRoot' | 'model' | 'reasoning' | 'permissionMode' | 'nativeToolAccess' | 'nativeCapabilities' | 'cliParity' | 'disallowedTools' | 'allowedTools' | 'settingSources' | 'settings' | 'chromeIntegration'> & {
|
|
194
|
+
filesystemRuntimeReadPaths?: readonly string[];
|
|
195
|
+
filesystemTempDir?: string;
|
|
196
|
+
filesystemSharedTempRoots?: readonly string[];
|
|
197
|
+
filesystemNotice?: string;
|
|
198
|
+
};
|
|
193
199
|
readonly systemPromptOverlay: string | null | undefined;
|
|
194
200
|
}): RuntimeBuildOptions;
|
|
195
201
|
/**
|
|
@@ -262,7 +268,7 @@ export declare function buildRunArgs({ input, deps }: {
|
|
|
262
268
|
readonly def: RuntimeAgentDef;
|
|
263
269
|
readonly imageDelivery: ImagePromptDelivery;
|
|
264
270
|
readonly imagePaths: readonly string[] | undefined;
|
|
265
|
-
readonly runInput:
|
|
271
|
+
readonly runInput: AgentBuildInput;
|
|
266
272
|
readonly systemPromptOverlay: string | null | undefined;
|
|
267
273
|
readonly overlayDelivery: SystemPromptOverlayDelivery;
|
|
268
274
|
readonly runtimeContext: RuntimeContext | undefined;
|
|
@@ -381,6 +387,6 @@ export declare function confirmChildSpawned({ input, deps }: {
|
|
|
381
387
|
* @complexity `run()`'s own setup is O(1); steady-state cost is the chosen stream parser's.
|
|
382
388
|
* @overallScore 100/100
|
|
383
389
|
*/
|
|
384
|
-
export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "runtimeState" | "codexDisablePlugins" | "piGracefulShutdownMs" | "hostEnv" | "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "probeMcpBridge" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "delegatedResultCorrelation" | "stdoutJournalBlobs" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
|
|
390
|
+
export declare function createAgentExecutor(requiredArgs: Pick<CreateAgentExecutorOptions, "lifecycle">, optionalArgs?: Pick<CreateAgentExecutorOptions, "runtimeState" | "codexDisablePlugins" | "piGracefulShutdownMs" | "hostEnv" | "getAgentDef" | "resolveAgentLaunch" | "resolveModelForLaunch" | "ensureAgentCapabilities" | "assertClaudeFilesystemSupport" | "applyAgentLaunchEnv" | "createCommandInvocation" | "spawn" | "probeMcpBridge" | "attachAcpSession" | "acpPermissionHandler" | "attachPiRpcSession" | "preparePromptFileForAgent" | "prepareAgentLogFile" | "listProcessSnapshots" | "collectProcessTreePids" | "stopProcesses" | "onCleanupFailure" | "journal" | "delegatedResultCorrelation" | "stdoutJournalBlobs" | "continuation" | "classifyFailure" | "mcpJsonInjection" | "claudeConfigDirIsolation" | "claudeConfigDirIsolationEnabled" | "bufferedStdoutMaxBytes" | "promptAugmenter">): AgentExecutor;
|
|
385
391
|
export {};
|
|
386
392
|
//# sourceMappingURL=launch.d.ts.map
|