@jini-ai/daemon 0.6.9 → 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 +157 -0
- package/dist/agent-executor/contracts.d.ts +34 -11
- package/dist/agent-executor/event-translation.js +2 -0
- package/dist/agent-executor/events.d.ts +7 -15
- package/dist/agent-executor/events.js +142 -194
- package/dist/agent-executor/launch.d.ts +9 -3
- package/dist/agent-executor/launch.js +104 -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/delegated-result-correlation.d.ts +26 -0
- package/dist/delegated-result-correlation.js +68 -0
- package/dist/delegated-tool-bridge.d.ts +5 -13
- package/dist/delegated-tool-bridge.js +19 -58
- package/dist/http/attachments.d.ts +13 -0
- package/dist/http/attachments.js +27 -1
- package/dist/http/delegated-tools.d.ts +3 -0
- package/dist/http/delegated-tools.js +2 -2
- 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/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/run-lifecycle.d.ts +20 -1
- package/dist/run-lifecycle.js +2 -16
- package/dist/tool-executor.js +5 -4
- package/dist/tool-result-diagnostics.d.ts +27 -0
- package/dist/tool-result-diagnostics.js +77 -0
- package/dist/tool-result-media.d.ts +27 -14
- package/dist/tool-result-media.js +51 -38
- package/package.json +14 -14
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { createToolResultDiagnosticProjection, projectDiagnosticFrame } from '../tool-result-diagnostics.js';
|
|
1
2
|
import { appendSerialTask } from "@jini-ai/core/primitives";
|
|
3
|
+
import { createLiveSessionState } from './live-session-state.js';
|
|
4
|
+
import { acceptsMidRunUserMessages, createMessageRelay } from './message-relay.js';
|
|
2
5
|
import { randomUUID } from 'node:crypto';
|
|
3
6
|
import { createModelReceiptTracker } from './model-receipts.js';
|
|
4
7
|
import { observeRunOutput } from './run-output.js';
|
|
@@ -9,7 +12,7 @@ import { collectProcessTreePids, listProcessSnapshots, stopProcesses, } from '@j
|
|
|
9
12
|
import { classifyRunCloseStatus, } from '../close-status.js';
|
|
10
13
|
import { resolveContinuationTransport, } from '../continuation/continuation-transport.js';
|
|
11
14
|
import { resultContent, } from '../delegated-tool-bridge.js';
|
|
12
|
-
import {
|
|
15
|
+
import { projectToolResultEvent, } from '../tool-result-media.js';
|
|
13
16
|
import { translateAgentRuntimeEvent, translateAcpError, applyAgentTranslationSideEffects, } from './event-translation.js';
|
|
14
17
|
import { errorMessage, isRecord, } from './values.js';
|
|
15
18
|
import { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, bridgeUnavailableMessage, } from './mcp-bridge.js';
|
|
@@ -202,6 +205,45 @@ export const DEFAULT_BUFFERED_STDOUT_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
202
205
|
function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
|
|
203
206
|
return `\n[jini] agent stdout truncated: ${droppedBytes} byte(s) dropped after the ${maxBytes}-byte buffer limit was reached.\n`;
|
|
204
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
|
+
}
|
|
218
|
+
/** Keeps exact-byte auditing separate from retained diagnostics on the run's existing FIFO.
|
|
219
|
+
* Projection retains at most one bounded frame; ordinary stdout keeps its legacy byte journal. */
|
|
220
|
+
function createStdoutDiagnostics({ ctx, enqueueEmit }, _options = {}) {
|
|
221
|
+
const { runId, lifecycle, journal, stdoutJournalBlobs } = ctx;
|
|
222
|
+
const projection = ctx.delegatedResultCorrelation && ctx.streamFormat === 'claude-stream-json'
|
|
223
|
+
? createToolResultDiagnosticProjection({}) : undefined;
|
|
224
|
+
function emit({ text }) {
|
|
225
|
+
if (text)
|
|
226
|
+
enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'stdout', data: { chunk: text } } }));
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
record({ chunk, text }) {
|
|
230
|
+
if (!journal)
|
|
231
|
+
return;
|
|
232
|
+
if (projection && stdoutJournalBlobs) {
|
|
233
|
+
enqueueEmit(() => stdoutJournalBlobs.record({ runId, chunk }));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
enqueueEmit(() => journal.record({ runId, entry: receivedJournalEntry('stdout', text) }));
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
feed({ text }) {
|
|
240
|
+
emit({ text: projection ? projection.feed({ chunk: text }) : text });
|
|
241
|
+
},
|
|
242
|
+
flush() {
|
|
243
|
+
emit({ text: projection?.flush({}) ?? '' });
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
205
247
|
/**
|
|
206
248
|
* Wires one spawned child's full observable lifecycle: raw stdout/stderr
|
|
207
249
|
* forwarding, structured stream-parser dispatch (translated via
|
|
@@ -264,6 +306,7 @@ function bufferedStdoutTruncationNotice(droppedBytes, maxBytes) {
|
|
|
264
306
|
*/
|
|
265
307
|
export function wireChildLifecycle(ctx) {
|
|
266
308
|
const { runId, def, streamFormat, child, lifecycle, journal, continuation, classifyFailure } = ctx;
|
|
309
|
+
const stdoutDiagnostics = createStdoutDiagnostics({ ctx, enqueueEmit });
|
|
267
310
|
const output = observeRunOutput({ lifecycle, runId, agentName: def.name }, { plainStdout: streamFormat === 'plain' });
|
|
268
311
|
const modelReceipt = createModelReceiptTracker(ctx.startingModel, ctx.modelCatalog?.models);
|
|
269
312
|
let stdinClosed = false;
|
|
@@ -283,9 +326,8 @@ export function wireChildLifecycle(ctx) {
|
|
|
283
326
|
// only ever acted on when a host has explicitly allowlisted the tool's name.
|
|
284
327
|
let pendingToolUse;
|
|
285
328
|
// Observe the canonical lifecycle, including delegated calls that bypass the CLI parser.
|
|
286
|
-
// A single latest tool_use cannot represent parallel
|
|
329
|
+
// A single latest tool_use cannot represent all parallel calls at explicit cancellation.
|
|
287
330
|
const unresolvedToolCalls = new Set();
|
|
288
|
-
const interruptedToolCalls = new Set();
|
|
289
331
|
const toolSubscription = lifecycle.stream({ runId, onEvent: (event) => {
|
|
290
332
|
if (event.kind !== 'agent')
|
|
291
333
|
return;
|
|
@@ -301,9 +343,9 @@ export function wireChildLifecycle(ctx) {
|
|
|
301
343
|
});
|
|
302
344
|
/** Run on the same FIFO as parsed results, after the aborted segment has drained.
|
|
303
345
|
* O(t) in unresolved calls; completed results are never replaced with cancellation. */
|
|
304
|
-
async function cancelUnresolvedToolCalls(
|
|
346
|
+
async function cancelUnresolvedToolCalls() {
|
|
305
347
|
await toolSubscription;
|
|
306
|
-
for (const toolUseId of
|
|
348
|
+
for (const toolUseId of [...unresolvedToolCalls]) {
|
|
307
349
|
if (!unresolvedToolCalls.has(toolUseId))
|
|
308
350
|
continue;
|
|
309
351
|
await lifecycle.emit({ runId, input: { event: 'agent', data: {
|
|
@@ -342,71 +384,46 @@ export function wireChildLifecycle(ctx) {
|
|
|
342
384
|
stdinClosed = true;
|
|
343
385
|
child.stdin?.end();
|
|
344
386
|
}
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
// runs, an `end_turn` is not the run's end: the CLI keeps going until they finish and reports on
|
|
355
|
-
// them, so stdin must stay open for mid-run messages. Closing it there (2026-10-10, plannie chat)
|
|
356
|
-
// made every message sent while two subagents worked come back 'not-running' and wait for the run
|
|
357
|
-
// to end — and at EOF the CLI stops background tasks at its own wait ceiling (10 minutes).
|
|
358
|
-
let backgroundTaskCount = 0;
|
|
359
|
-
// Set when a turn ended and stdin stayed open only for background tasks: no turn is running, so a
|
|
360
|
-
// 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.
|
|
361
396
|
let heldForBackgroundTasks = false;
|
|
362
|
-
|
|
397
|
+
let heldForQueuedInput = false;
|
|
363
398
|
let controlScanBuffer = '';
|
|
364
|
-
/**
|
|
365
|
-
|
|
366
|
-
const requestId = controlResponseRequestId({ line: line });
|
|
367
|
-
const settle = requestId === undefined ? undefined : pendingInterruptAcks.get(requestId);
|
|
368
|
-
if (!settle)
|
|
369
|
-
return;
|
|
370
|
-
pendingInterruptAcks.delete(requestId);
|
|
371
|
-
settle(true);
|
|
372
|
-
}
|
|
373
|
-
/**
|
|
374
|
-
* Tracks a `background_tasks_changed` line. The last background task finishing while stdin is
|
|
375
|
-
* held only for them closes it, exactly as the turn end would have: the CLI then answers what it
|
|
376
|
-
* still owes (the tasks' report) and exits. @complexity O(line length).
|
|
377
|
-
*/
|
|
378
|
-
function updateBackgroundTasks(line) {
|
|
379
|
-
const count = backgroundTaskListLength({ line: line });
|
|
380
|
-
if (count === undefined)
|
|
381
|
-
return;
|
|
382
|
-
backgroundTaskCount = count;
|
|
383
|
-
if (count === 0 && heldForBackgroundTasks) {
|
|
384
|
-
heldForBackgroundTasks = false;
|
|
385
|
-
closeStdinOnce();
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Reads the stdout lines a mid-run message depends on: interrupt acks and background-task
|
|
390
|
-
* changes. Only agents that take mid-run messages are scanned. A chunk with no newline is only
|
|
391
|
-
* appended, so a long line split across many chunks is split once, not once per chunk.
|
|
392
|
-
* @complexity O(chunk length) amortized.
|
|
393
|
-
*/
|
|
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. */
|
|
394
401
|
function scanControlLines(text) {
|
|
395
|
-
if (!acceptsMidRunUserMessages({ def: def }))
|
|
396
|
-
return;
|
|
397
402
|
controlScanBuffer += text;
|
|
398
403
|
if (!text.includes('\n'))
|
|
399
404
|
return;
|
|
400
|
-
// `text` holds a newline, so the buffer does: split off the complete lines, keep the partial tail.
|
|
401
405
|
const lastNewline = controlScanBuffer.lastIndexOf('\n');
|
|
402
406
|
const lines = controlScanBuffer.slice(0, lastNewline).split('\n');
|
|
403
407
|
controlScanBuffer = controlScanBuffer.slice(lastNewline + 1);
|
|
404
|
-
for (const line of lines)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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();
|
|
410
427
|
}
|
|
411
428
|
}
|
|
412
429
|
/**
|
|
@@ -428,25 +445,15 @@ export function wireChildLifecycle(ctx) {
|
|
|
428
445
|
if (journal)
|
|
429
446
|
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
|
|
430
447
|
}
|
|
431
|
-
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* message or task report), `'turn-continues'` for the reasons below, `'close'` otherwise. Open
|
|
435
|
-
* because the run is not over: a `tool_use` stop (the CLI runs the tool itself and carries on, and a
|
|
436
|
-
* message sent while the tool runs must still reach it), any end while an interrupt still waits
|
|
437
|
-
* for its ack (the message is about to be written), and the end of a turn an interrupt aborted
|
|
438
|
-
* (the message written after it starts the next turn), and any end while background tasks still
|
|
439
|
-
* run (see `backgroundTaskCount`). Every other end closes stdin, so the CLI exits once it has
|
|
440
|
-
* answered everything written before EOF.
|
|
441
|
-
*/
|
|
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. */
|
|
442
451
|
function stdinHoldAfterTurnEnd(stopReason) {
|
|
443
|
-
if (!acceptsMidRunUserMessages({ def
|
|
452
|
+
if (!acceptsMidRunUserMessages({ def }))
|
|
444
453
|
return 'close';
|
|
445
|
-
if (stopReason === 'tool_use' ||
|
|
446
|
-
return 'turn-continues';
|
|
447
|
-
if (interruptSent && stopReason !== undefined && stopReason.startsWith('aborted'))
|
|
454
|
+
if (stopReason === 'tool_use' || session.hasPendingInput())
|
|
448
455
|
return 'turn-continues';
|
|
449
|
-
return backgroundTaskCount > 0 ? 'background-tasks' : 'close';
|
|
456
|
+
return session.backgroundTaskCount() > 0 ? 'background-tasks' : 'close';
|
|
450
457
|
}
|
|
451
458
|
/**
|
|
452
459
|
* Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
|
|
@@ -455,11 +462,6 @@ export function wireChildLifecycle(ctx) {
|
|
|
455
462
|
* unconfigured or the pending tool isn't allowlisted).
|
|
456
463
|
*/
|
|
457
464
|
function handleTurnEnd(stopReason) {
|
|
458
|
-
if (stopReason?.startsWith('aborted'))
|
|
459
|
-
enqueueEmit(async () => {
|
|
460
|
-
await cancelUnresolvedToolCalls([...interruptedToolCalls]);
|
|
461
|
-
interruptedToolCalls.clear();
|
|
462
|
-
});
|
|
463
465
|
const toolUse = pendingToolUse;
|
|
464
466
|
const shouldInject = stopReason === 'tool_use' &&
|
|
465
467
|
toolUse !== undefined &&
|
|
@@ -469,6 +471,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
469
471
|
if (!shouldInject) {
|
|
470
472
|
const hold = stdinHoldAfterTurnEnd(stopReason);
|
|
471
473
|
heldForBackgroundTasks = hold === 'background-tasks';
|
|
474
|
+
heldForQueuedInput = hold === 'turn-continues' && stopReason !== 'tool_use';
|
|
472
475
|
if (hold === 'close')
|
|
473
476
|
closeStdinOnce();
|
|
474
477
|
return;
|
|
@@ -478,11 +481,8 @@ export function wireChildLifecycle(ctx) {
|
|
|
478
481
|
const run = { id: runId };
|
|
479
482
|
let content;
|
|
480
483
|
let isError;
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
// `splitToolResultSurfaces` (it has no `mcp-ui` withhold-from-model concept — the flattened
|
|
484
|
-
// `content` below already carries the whole raw output, a pre-existing, unrelated gap), so
|
|
485
|
-
// there is no `remainder` to thread back in — only the extracted blocks are used here.
|
|
484
|
+
// Event projection removes pixels only from saved content; stdin keeps the original.
|
|
485
|
+
let eventContent;
|
|
486
486
|
let media = [];
|
|
487
487
|
// Suspend the slow-run watchdog for the duration of this daemon-awaited execution: the daemon
|
|
488
488
|
// knows exactly why the run is quiet here (it dispatched the tool itself and is waiting on it),
|
|
@@ -494,11 +494,14 @@ export function wireChildLifecycle(ctx) {
|
|
|
494
494
|
try {
|
|
495
495
|
const result = await continuation.toolExecutor.execute({ principal: continuation.principal, run: run, toolId: toolUse.name, input: toolUse.input });
|
|
496
496
|
content = resultContent({ result: result });
|
|
497
|
-
|
|
498
|
-
|
|
497
|
+
const projection = projectToolResultEvent({ result });
|
|
498
|
+
eventContent = projection.content;
|
|
499
|
+
isError = projection.isError === true;
|
|
500
|
+
media = projection.media ?? [];
|
|
499
501
|
}
|
|
500
502
|
catch (error) {
|
|
501
503
|
content = errorMessage(error);
|
|
504
|
+
eventContent = content;
|
|
502
505
|
isError = true;
|
|
503
506
|
}
|
|
504
507
|
finally {
|
|
@@ -509,7 +512,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
509
512
|
data: {
|
|
510
513
|
type: 'tool_result',
|
|
511
514
|
toolUseId: toolUse.id,
|
|
512
|
-
content,
|
|
515
|
+
content: eventContent,
|
|
513
516
|
...(isError ? { isError: true } : {}),
|
|
514
517
|
...(media.length > 0 ? { media } : {}),
|
|
515
518
|
},
|
|
@@ -539,7 +542,10 @@ export function wireChildLifecycle(ctx) {
|
|
|
539
542
|
} });
|
|
540
543
|
if (rejectUnavailableBridge({ payload: translation.payload, rawEvent }, {}))
|
|
541
544
|
return;
|
|
542
|
-
enqueueEmit(() =>
|
|
545
|
+
enqueueEmit(() => {
|
|
546
|
+
const payload = projectEcho({ payload: translation.payload, rawEvent }, {});
|
|
547
|
+
return lifecycle.emit({ runId, input: { event: 'agent', data: statusWithChildIdentity({ payload, childPid: ctx.child.pid }, {}) } });
|
|
548
|
+
});
|
|
543
549
|
}
|
|
544
550
|
else if (translation.kind === 'error') {
|
|
545
551
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'error', data: translation.payload } }));
|
|
@@ -548,6 +554,17 @@ export function wireChildLifecycle(ctx) {
|
|
|
548
554
|
handleTurnEnd(translation.stopReason);
|
|
549
555
|
}
|
|
550
556
|
});
|
|
557
|
+
/** Verify against an already-saved call in this run; unverified metadata preserves all output. */
|
|
558
|
+
function projectEcho({ payload, rawEvent }, _options = {}) {
|
|
559
|
+
if (!ctx.delegatedResultCorrelation)
|
|
560
|
+
return payload;
|
|
561
|
+
if (payload.type === 'raw')
|
|
562
|
+
return { ...payload, line: projectDiagnosticFrame({ line: payload.line }) };
|
|
563
|
+
if (payload.type !== 'tool_result' || !payload.delegatedToolUseId || !Array.isArray(rawEvent.delegatedResultContent))
|
|
564
|
+
return payload;
|
|
565
|
+
const matches = ctx.delegatedResultCorrelation.matches({ runId, toolUseId: payload.delegatedToolUseId, content: rawEvent.delegatedResultContent, isError: payload.isError === true });
|
|
566
|
+
return matches ? { ...payload, content: `Result shown by delegated call ${payload.delegatedToolUseId}.` } : payload;
|
|
567
|
+
}
|
|
551
568
|
function rejectUnavailableBridge({ payload, rawEvent }, _options = {}) {
|
|
552
569
|
if (!bridgeChecked && payload.type === 'status' && payload.label === 'initializing') {
|
|
553
570
|
bridgeChecked = true;
|
|
@@ -616,8 +633,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
616
633
|
}
|
|
617
634
|
child.stdout?.on('data', (chunk) => {
|
|
618
635
|
const text = chunk.toString('utf8');
|
|
619
|
-
|
|
620
|
-
enqueueEmit(() => journal.record({ runId: runId, entry: receivedJournalEntry('stdout', text) }));
|
|
636
|
+
stdoutDiagnostics.record({ chunk, text });
|
|
621
637
|
if (streamFormat === 'plain') {
|
|
622
638
|
if (text.length > 0)
|
|
623
639
|
userVisibleOutputSeen = true;
|
|
@@ -643,14 +659,13 @@ export function wireChildLifecycle(ctx) {
|
|
|
643
659
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'text_delta', delta: text } } }));
|
|
644
660
|
return;
|
|
645
661
|
}
|
|
646
|
-
|
|
647
|
-
//
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
streamHandler.feed({ chunk: text });
|
|
662
|
+
stdoutDiagnostics.feed({ 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 });
|
|
654
669
|
});
|
|
655
670
|
child.stderr?.on('data', (chunk) => {
|
|
656
671
|
const text = chunk.toString('utf8');
|
|
@@ -673,9 +688,7 @@ export function wireChildLifecycle(ctx) {
|
|
|
673
688
|
void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
|
|
674
689
|
} });
|
|
675
690
|
child.on('close', (code, signal) => {
|
|
676
|
-
|
|
677
|
-
settle(false);
|
|
678
|
-
pendingInterruptAcks.clear();
|
|
691
|
+
stdinClosed = true;
|
|
679
692
|
void (async () => {
|
|
680
693
|
// Not wrapped in try/catch: all 4 supported parser factories'
|
|
681
694
|
// flush() implementations already internally guard their own
|
|
@@ -685,7 +698,13 @@ export function wireChildLifecycle(ctx) {
|
|
|
685
698
|
// here would be dead code for the fixed, closed set of parsers this
|
|
686
699
|
// driver dispatches to. `streamHandler` is null for `'plain'` (no
|
|
687
700
|
// parser, hence nothing to flush) — `?.` skips it cleanly.
|
|
701
|
+
if (controlScanBuffer) {
|
|
702
|
+
feedSessionLine(controlScanBuffer);
|
|
703
|
+
controlScanBuffer = '';
|
|
704
|
+
}
|
|
688
705
|
streamHandler?.flush();
|
|
706
|
+
session.endSession();
|
|
707
|
+
stdoutDiagnostics.flush();
|
|
689
708
|
// Queued before `await emitQueue` so the flushed text is durably appended ahead of
|
|
690
709
|
// `finish()`'s `'end'` event, exactly like every live-path emit already is.
|
|
691
710
|
flushBufferedStdout();
|
|
@@ -716,63 +735,27 @@ export function wireChildLifecycle(ctx) {
|
|
|
716
735
|
})
|
|
717
736
|
: false;
|
|
718
737
|
}
|
|
719
|
-
/**
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
* {@link INTERRUPT_ACK_TIMEOUT_MS}: the CLI then folds the line in at its next step. Once
|
|
727
|
-
* `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
|
|
728
|
-
* reach — `'not-running'`. Between turns, while stdin is held only for background tasks, there is
|
|
729
|
-
* no generation to stop: the line is written at once and starts the next turn.
|
|
730
|
-
* Host-prepared images use the same content blocks as the initial stdin turn; no file tools
|
|
731
|
-
* or new directory grants are needed to see these pixels.
|
|
732
|
-
*/
|
|
733
|
-
async function sendUserMessage(text, images = []) {
|
|
734
|
-
if (!acceptsMidRunUserMessages({ def: def }))
|
|
735
|
-
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';
|
|
736
745
|
const stdin = child.stdin;
|
|
737
746
|
if (stdinClosed || !stdin)
|
|
738
747
|
return 'not-running';
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
writeUserMessage(stdin, text, images);
|
|
742
|
-
return 'delivered';
|
|
743
|
-
}
|
|
744
|
-
const requestId = `jini-interrupt-${randomUUID()}`;
|
|
745
|
-
interruptSent = true;
|
|
746
|
-
// Capture before requesting the interrupt. Its result can precede the acknowledgement, or
|
|
747
|
-
// arrive after the next segment starts: neither ordering may cancel that segment's new tools.
|
|
748
|
-
enqueueEmit(async () => {
|
|
749
|
-
await toolSubscription;
|
|
750
|
-
for (const id of interruptedToolCalls)
|
|
751
|
-
if (!unresolvedToolCalls.has(id))
|
|
752
|
-
interruptedToolCalls.delete(id);
|
|
753
|
-
for (const id of unresolvedToolCalls)
|
|
754
|
-
interruptedToolCalls.add(id);
|
|
755
|
-
});
|
|
756
|
-
const acked = new Promise((resolve) => pendingInterruptAcks.set(requestId, resolve));
|
|
757
|
-
stdin.write(`${JSON.stringify({ type: 'control_request', request_id: requestId, request: { subtype: 'interrupt' } })}\n`, 'utf8');
|
|
758
|
-
let timer;
|
|
759
|
-
const timedOut = new Promise((resolve) => {
|
|
760
|
-
timer = setTimeout(() => resolve(true), INTERRUPT_ACK_TIMEOUT_MS);
|
|
761
|
-
});
|
|
762
|
-
const processAlive = await Promise.race([acked, timedOut]);
|
|
763
|
-
clearTimeout(timer);
|
|
764
|
-
pendingInterruptAcks.delete(requestId);
|
|
765
|
-
if (!processAlive || stdinClosed)
|
|
766
|
-
return 'not-running';
|
|
767
|
-
writeUserMessage(stdin, text, images);
|
|
748
|
+
heldForBackgroundTasks = false;
|
|
749
|
+
writeUserMessage(stdin, text, images, id);
|
|
768
750
|
return 'delivered';
|
|
769
751
|
}
|
|
770
|
-
/** Writes a complete mid-run turn using the idle image encoder; journals text without base64.
|
|
771
|
-
|
|
772
|
-
|
|
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');
|
|
773
757
|
if (journal)
|
|
774
|
-
enqueueEmit(() => journal.record({ runId
|
|
775
|
-
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
|
|
758
|
+
enqueueEmit(() => journal.record({ runId, entry: sentJournalEntry(text) }));
|
|
776
759
|
}
|
|
777
760
|
return {
|
|
778
761
|
closeStdinOnce,
|
|
@@ -783,45 +766,6 @@ export function wireChildLifecycle(ctx) {
|
|
|
783
766
|
sendUserMessage,
|
|
784
767
|
};
|
|
785
768
|
}
|
|
786
|
-
/**
|
|
787
|
-
* Whether a def's live process can take another user message mid-run: only a stream-json stdin
|
|
788
|
-
* transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
|
|
789
|
-
* Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
|
|
790
|
-
* reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
|
|
791
|
-
*/
|
|
792
|
-
export function acceptsMidRunUserMessages({ def }) {
|
|
793
|
-
return def.promptViaStdin === true && def.promptInputFormat === 'stream-json';
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
|
|
797
|
-
* anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
|
|
798
|
-
* the ceiling only bounds an agent that ignores control requests.
|
|
799
|
-
*/
|
|
800
|
-
export const INTERRUPT_ACK_TIMEOUT_MS = 5_000;
|
|
801
|
-
/** The `request_id` of a stream-json `control_response` line, or `undefined` for anything else. */
|
|
802
|
-
function controlResponseRequestId({ line }) {
|
|
803
|
-
try {
|
|
804
|
-
const parsed = JSON.parse(line);
|
|
805
|
-
if (!isRecord(parsed) || parsed.type !== 'control_response' || !isRecord(parsed.response))
|
|
806
|
-
return undefined;
|
|
807
|
-
return typeof parsed.response.request_id === 'string' ? parsed.response.request_id : undefined;
|
|
808
|
-
}
|
|
809
|
-
catch {
|
|
810
|
-
return undefined;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
/** How many tasks a `background_tasks_changed` line lists, or `undefined` for any other line. */
|
|
814
|
-
function backgroundTaskListLength({ line }) {
|
|
815
|
-
try {
|
|
816
|
-
const parsed = JSON.parse(line);
|
|
817
|
-
if (!isRecord(parsed) || parsed.type !== 'system' || parsed.subtype !== 'background_tasks_changed')
|
|
818
|
-
return undefined;
|
|
819
|
-
return Array.isArray(parsed.tasks) ? parsed.tasks.length : undefined;
|
|
820
|
-
}
|
|
821
|
-
catch {
|
|
822
|
-
return undefined;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
769
|
/** The stream-json stdin envelope for one user message — the initial prompt and every mid-run one. */
|
|
826
770
|
function streamJsonUserMessage({ content }) {
|
|
827
771
|
return { type: 'user', message: { role: 'user', content: content } };
|
|
@@ -865,6 +809,9 @@ function wireAcpLifecycle(ctx) {
|
|
|
865
809
|
function enqueueEmit(task) {
|
|
866
810
|
emitQueue = appendSerialTask({ previous: emitQueue, task, onError: reportDroppedEmit });
|
|
867
811
|
}
|
|
812
|
+
attachMessageRelay({ register: ctx.registerMessageRelay, child,
|
|
813
|
+
emit: data => enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data } })),
|
|
814
|
+
});
|
|
868
815
|
if (ctx.model)
|
|
869
816
|
enqueueEmit(() => lifecycle.emit({ runId, input: { event: 'agent', data: { type: 'status', label: 'starting_model', model: ctx.model } } }));
|
|
870
817
|
child.stdout?.on('data', (chunk) => {
|
|
@@ -1064,6 +1011,7 @@ export function writePromptToStdin(def, child, prompt, handle) {
|
|
|
1064
1011
|
export async function runAcpDispatch({ input, deps }) {
|
|
1065
1012
|
try {
|
|
1066
1013
|
wireAcpLifecycle({
|
|
1014
|
+
...(deps.registerMessageRelay ? { registerMessageRelay: deps.registerMessageRelay } : {}),
|
|
1067
1015
|
runId: input.runId,
|
|
1068
1016
|
agentId: input.agentId,
|
|
1069
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" | "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
|