@jini-ai/daemon 0.5.4 → 0.6.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/dist/agent-executor/contracts.d.ts +33 -4
- package/dist/agent-executor/events.d.ts +17 -2
- package/dist/agent-executor/events.js +177 -6
- package/dist/agent-executor/index.d.ts +1 -1
- package/dist/agent-executor/launch.js +25 -2
- package/dist/agent-executor/mcp-bridge.js +19 -6
- package/dist/agent-executor.d.ts +1 -1
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +1 -1
- package/dist/http/route-manifest.js +2 -2
- package/dist/http/runs.d.ts +27 -2
- package/dist/http/runs.js +36 -1
- package/package.json +8 -8
|
@@ -194,7 +194,27 @@ export interface AgentExecutor {
|
|
|
194
194
|
* `lifecycle.finish()` before this rejects (see index.ts module doc's Invariant).
|
|
195
195
|
*/
|
|
196
196
|
run(requiredArgs: Pick<AgentExecutorRunInput, "runId" | "agentId" | "prompt" | "cwd">, optionalArgs?: Pick<AgentExecutorRunInput, "model" | "reasoning" | "permissionMode" | "imagePaths" | "imageContents" | "extraAllowedDirs" | "uploadRoot" | "credentialEnv" | "env" | "resumeSessionId" | "newSessionId" | "disallowedTools" | "allowedTools" | "settingSources" | "settings">): Promise<void>;
|
|
197
|
+
/**
|
|
198
|
+
* Delivers a message the human sent while `runId` is going straight into its live agent process
|
|
199
|
+
* (see {@link UserMessageDelivery} for each outcome). A stream-json agent is interrupted first and
|
|
200
|
+
* gets the message once the CLI acknowledges the interrupt, so this settles in milliseconds (at
|
|
201
|
+
* most `INTERRUPT_ACK_TIMEOUT_MS`); it never waits on the agent's reply.
|
|
202
|
+
*/
|
|
203
|
+
sendUserMessage(requiredArgs: {
|
|
204
|
+
readonly runId: string;
|
|
205
|
+
readonly text: string;
|
|
206
|
+
}): Promise<UserMessageDelivery>;
|
|
197
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* What {@link AgentExecutor.sendUserMessage} did with a mid-run message:
|
|
210
|
+
* - `'delivered'` — the running turn was interrupted and the message written into the live
|
|
211
|
+
* session; the agent answers it next, in the same process and session.
|
|
212
|
+
* - `'unsupported'` — the run is live, but its agent has no way to take input mid-run (its prompt
|
|
213
|
+
* is plain text or an RPC request). The host should interrupt and send it as the next turn.
|
|
214
|
+
* - `'not-running'` — no live process will accept it: an unknown run, a finished one, or one whose
|
|
215
|
+
* last turn already ended and closed stdin. The run is ending; the host should send it next.
|
|
216
|
+
*/
|
|
217
|
+
export type UserMessageDelivery = 'delivered' | 'unsupported' | 'not-running';
|
|
198
218
|
/** Process discovery is a getter; process collection and stopping accept explicit host inputs. */
|
|
199
219
|
export type CollectProcessTreePidsPort = (args: {
|
|
200
220
|
readonly processes: ProcessSnapshot[];
|
|
@@ -212,11 +232,11 @@ export interface TerminateChildTreeDeps {
|
|
|
212
232
|
* Which step reported a contained failure through `onCleanupFailure` (SEC-007).
|
|
213
233
|
*
|
|
214
234
|
* The first three come from {@link terminateChildTreeBestEffort} (process-tree teardown). The last
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* transition, and
|
|
235
|
+
* three cover tool cancellation, staged-file removal and the host's own `classifyFailure`.
|
|
236
|
+
* These fallible steps between a child's `'close'` and `finish()` must never prevent the terminal
|
|
237
|
+
* transition, and none may fail silently either. See each close handler.
|
|
218
238
|
*/
|
|
219
|
-
export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure' | 'staged-file-cleanup' | 'failure-classification';
|
|
239
|
+
export type AgentCleanupFailurePhase = 'cancel' | 'acp-attach-failure' | 'pi-rpc-attach-failure' | 'tool-cancellation' | 'staged-file-cleanup' | 'failure-classification';
|
|
220
240
|
export interface AgentCleanupFailureContext {
|
|
221
241
|
readonly runId: string;
|
|
222
242
|
readonly phase: AgentCleanupFailurePhase;
|
|
@@ -237,6 +257,8 @@ export interface StdinCloseHandle {
|
|
|
237
257
|
closeStdinOnce(): void;
|
|
238
258
|
/** 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`). */
|
|
239
259
|
recordSentBytes(content: string): void;
|
|
260
|
+
/** Interrupts this child's turn, then writes a mid-run user message into its open stdin — see {@link AgentExecutor.sendUserMessage}. */
|
|
261
|
+
sendUserMessage(text: string): Promise<UserMessageDelivery>;
|
|
240
262
|
}
|
|
241
263
|
/**
|
|
242
264
|
* Gap 3 (capability-routed continuation transport) — host-owned config for the
|
|
@@ -374,6 +396,13 @@ export interface McpJsonInjectionOptions {
|
|
|
374
396
|
readonly removeDir?: ({ path }: {
|
|
375
397
|
readonly path: string;
|
|
376
398
|
}) => Promise<void>;
|
|
399
|
+
/** `'codex-toml'` only. Links the run home's sessions/archived_sessions to the operator's
|
|
400
|
+
* persistent rollout store. Config and auth remain isolated and are removed after the run.
|
|
401
|
+
* @default Native mkdir (0700) and directory symlinks; errors fail staging before spawn. */
|
|
402
|
+
readonly linkSessionStore?: (required: {
|
|
403
|
+
runHome: string;
|
|
404
|
+
sourceHome: string;
|
|
405
|
+
}, optional?: {}) => Promise<void>;
|
|
377
406
|
}
|
|
378
407
|
/** One `.mcp.json` `mcpServers` entry — the shape Claude Code's own config schema expects. */
|
|
379
408
|
export interface McpJsonServerEntry {
|
|
@@ -118,6 +118,21 @@ interface WireChildLifecycleContext extends TerminateChildTreeDeps {
|
|
|
118
118
|
* @overallScore 100/100
|
|
119
119
|
*/
|
|
120
120
|
export declare function wireChildLifecycle(ctx: WireChildLifecycleContext): StdinCloseHandle;
|
|
121
|
+
/**
|
|
122
|
+
* Whether a def's live process can take another user message mid-run: only a stream-json stdin
|
|
123
|
+
* transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
|
|
124
|
+
* Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
|
|
125
|
+
* reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
|
|
126
|
+
*/
|
|
127
|
+
export declare function acceptsMidRunUserMessages({ def }: {
|
|
128
|
+
readonly def: RuntimeAgentDef;
|
|
129
|
+
}): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
|
|
132
|
+
* anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
|
|
133
|
+
* the ceiling only bounds an agent that ignores control requests.
|
|
134
|
+
*/
|
|
135
|
+
export declare const INTERRUPT_ACK_TIMEOUT_MS = 5000;
|
|
121
136
|
/**
|
|
122
137
|
* Writes the initial user turn to the child's stdin per `def.promptInputFormat`
|
|
123
138
|
* (both branches only run for `promptViaStdin: true` defs — the only shape
|
|
@@ -125,8 +140,8 @@ export declare function wireChildLifecycle(ctx: WireChildLifecycleContext): Stdi
|
|
|
125
140
|
* - `'text'` (default): the raw prompt buffer, then stdin is closed —
|
|
126
141
|
* matches `RuntimeAgentDef.promptInputFormat`'s own doc.
|
|
127
142
|
* - `'stream-json'`: one JSONL line wrapping the prompt as an Anthropic
|
|
128
|
-
* user message; stdin is deliberately left open
|
|
129
|
-
*
|
|
143
|
+
* user message; stdin is deliberately left open so a message the human
|
|
144
|
+
* sends mid-run can follow it (`StdinCloseHandle.sendUserMessage`), and
|
|
130
145
|
* {@link wireChildLifecycle}'s `turn_end` handling closes it once the
|
|
131
146
|
* agent's own stream reports the turn ended.
|
|
132
147
|
* @param def - The resolved agent def (only `.promptInputFormat` is read).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { createModelReceiptTracker } from './model-receipts.js';
|
|
2
3
|
import { messageContentWithImages } from '../attachment-content.js';
|
|
3
4
|
import { redactSecrets, } from '@jini-ai/core';
|
|
@@ -8,7 +9,7 @@ import { resolveContinuationTransport, } from '../continuation/continuation-tran
|
|
|
8
9
|
import { resultContent, } from '../delegated-tool-bridge.js';
|
|
9
10
|
import { extractResultMedia, } from '../tool-result-media.js';
|
|
10
11
|
import { translateAgentRuntimeEvent, translateAcpError, applyAgentTranslationSideEffects, } from './event-translation.js';
|
|
11
|
-
import { errorMessage, } from './values.js';
|
|
12
|
+
import { errorMessage, isRecord, } from './values.js';
|
|
12
13
|
import { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, bridgeUnavailableMessage, } from './mcp-bridge.js';
|
|
13
14
|
/** Carry PID only on structured status so the host can record its OS start identity early.
|
|
14
15
|
* A PID alone is never proof of death; hosts verify PID reuse before native continuation. */
|
|
@@ -68,7 +69,7 @@ export function defaultCleanupFailureSink(context) {
|
|
|
68
69
|
/**
|
|
69
70
|
* Reports a contained post-close failure through the host's sink, absorbing a throwing sink.
|
|
70
71
|
*
|
|
71
|
-
* A diagnostic sink is host code too, and the whole point of
|
|
72
|
+
* A diagnostic sink is host code too, and the whole point of these callers is that nothing
|
|
72
73
|
* between `'close'` and `finish()` can strand the run — a sink that throws must not reintroduce
|
|
73
74
|
* exactly that. Same reasoning `run-lifecycle.ts`'s `handleInactivityTimeout` already applies to its
|
|
74
75
|
* own `onInternalError`.
|
|
@@ -273,6 +274,37 @@ export function wireChildLifecycle(ctx) {
|
|
|
273
274
|
// consumed by a turn-end injection decision. See `ContinuationOptions`'s doc for why this is
|
|
274
275
|
// only ever acted on when a host has explicitly allowlisted the tool's name.
|
|
275
276
|
let pendingToolUse;
|
|
277
|
+
// Observe the canonical lifecycle, including delegated calls that bypass the CLI parser.
|
|
278
|
+
// A single latest tool_use cannot represent parallel tools at an interrupt boundary.
|
|
279
|
+
const unresolvedToolCalls = new Set();
|
|
280
|
+
const interruptedToolCalls = new Set();
|
|
281
|
+
const toolSubscription = lifecycle.stream({ runId, onEvent: (event) => {
|
|
282
|
+
if (event.kind !== 'agent')
|
|
283
|
+
return;
|
|
284
|
+
if (event.payload.type === 'tool_use')
|
|
285
|
+
unresolvedToolCalls.add(event.payload.id);
|
|
286
|
+
if (event.payload.type === 'tool_result')
|
|
287
|
+
unresolvedToolCalls.delete(event.payload.toolUseId);
|
|
288
|
+
} }).catch(error => {
|
|
289
|
+
// Observing tools is fallible I/O too: report it, but never strand close/finish on a replay
|
|
290
|
+
// failure or leave an unhandled rejection while the child is still running.
|
|
291
|
+
reportPostCloseFailure(ctx.onCleanupFailure, { runId, phase: 'tool-cancellation', pid: child.pid, error });
|
|
292
|
+
return null;
|
|
293
|
+
});
|
|
294
|
+
/** Run on the same FIFO as parsed results, after the aborted segment has drained.
|
|
295
|
+
* O(t) in unresolved calls; completed results are never replaced with cancellation. */
|
|
296
|
+
async function cancelUnresolvedToolCalls(ids) {
|
|
297
|
+
await toolSubscription;
|
|
298
|
+
for (const toolUseId of ids ?? [...unresolvedToolCalls]) {
|
|
299
|
+
if (!unresolvedToolCalls.has(toolUseId))
|
|
300
|
+
continue;
|
|
301
|
+
await lifecycle.emit({ runId, input: { event: 'agent', data: {
|
|
302
|
+
type: 'tool_result', toolUseId, content: 'Tool execution cancelled.', isError: true,
|
|
303
|
+
} } }).catch(error => {
|
|
304
|
+
reportPostCloseFailure(ctx.onCleanupFailure, { runId, phase: 'tool-cancellation', pid: child.pid, error });
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
276
308
|
// Bridge guard (see `expectsJiniBridge`): checked on the first init frame only. Once the bridge is
|
|
277
309
|
// found unavailable, the child is being stopped and nothing it still prints is forwarded.
|
|
278
310
|
let bridgeChecked = !ctx.expectsJiniBridge;
|
|
@@ -311,6 +343,35 @@ export function wireChildLifecycle(ctx) {
|
|
|
311
343
|
stdinClosed = true;
|
|
312
344
|
child.stdin?.end();
|
|
313
345
|
}
|
|
346
|
+
// Mid-run interrupts (see `sendUserMessage` below): each sent `interrupt` control request waits
|
|
347
|
+
// here, by request id, for the CLI's `control_response`. Settled with `true` on the ack, `false`
|
|
348
|
+
// when the process exits first. Bounded by the mid-run messages in flight on this one run.
|
|
349
|
+
const pendingInterruptAcks = new Map();
|
|
350
|
+
// Set once this run has interrupted its agent: from then on a turn the interrupt aborted
|
|
351
|
+
// (`aborted_*` stop reason) is not the run's last — the message that follows starts a new one.
|
|
352
|
+
let interruptSent = false;
|
|
353
|
+
// stdout carry-over for spotting acks; only filled while an interrupt waits for one.
|
|
354
|
+
let ackScanBuffer = '';
|
|
355
|
+
/** Resolves every waiting interrupt whose `control_response` is in this stdout chunk. @complexity O(chunk length). */
|
|
356
|
+
function scanForInterruptAcks(text) {
|
|
357
|
+
if (pendingInterruptAcks.size === 0)
|
|
358
|
+
return;
|
|
359
|
+
ackScanBuffer += text;
|
|
360
|
+
const lines = ackScanBuffer.split('\n');
|
|
361
|
+
ackScanBuffer = lines.pop() ?? '';
|
|
362
|
+
for (const line of lines) {
|
|
363
|
+
if (!line.includes('"control_response"'))
|
|
364
|
+
continue;
|
|
365
|
+
const requestId = controlResponseRequestId({ line: line });
|
|
366
|
+
const settle = requestId === undefined ? undefined : pendingInterruptAcks.get(requestId);
|
|
367
|
+
if (!settle)
|
|
368
|
+
continue;
|
|
369
|
+
pendingInterruptAcks.delete(requestId);
|
|
370
|
+
settle(true);
|
|
371
|
+
}
|
|
372
|
+
if (pendingInterruptAcks.size === 0)
|
|
373
|
+
ackScanBuffer = '';
|
|
374
|
+
}
|
|
314
375
|
/**
|
|
315
376
|
* Writes a structured (never string-concatenated — see `ContinuationOptions`'s doc on the
|
|
316
377
|
* prompt-injection stakes here) tool_result JSONL line, mirroring the shape
|
|
@@ -330,6 +391,21 @@ export function wireChildLifecycle(ctx) {
|
|
|
330
391
|
if (journal)
|
|
331
392
|
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
|
|
332
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Whether a `turn_end` leaves stdin open for a live agent that takes mid-run messages, because
|
|
396
|
+
* the run is not over: a `tool_use` stop (the CLI runs the tool itself and carries on, and a
|
|
397
|
+
* message sent while the tool runs must still reach it), any end while an interrupt still waits
|
|
398
|
+
* for its ack (the message is about to be written), and the end of a turn an interrupt aborted
|
|
399
|
+
* (the message written after it starts the next turn). Every other end closes stdin, so the CLI
|
|
400
|
+
* exits once it has answered everything written before EOF.
|
|
401
|
+
*/
|
|
402
|
+
function keepsStdinOpenAfterTurnEnd(stopReason) {
|
|
403
|
+
if (!acceptsMidRunUserMessages({ def: def }))
|
|
404
|
+
return false;
|
|
405
|
+
if (stopReason === 'tool_use' || pendingInterruptAcks.size > 0)
|
|
406
|
+
return true;
|
|
407
|
+
return interruptSent && stopReason !== undefined && stopReason.startsWith('aborted');
|
|
408
|
+
}
|
|
333
409
|
/**
|
|
334
410
|
* Decides, per `turn_end`, whether to auto-resolve a pending tool_use through the injected
|
|
335
411
|
* `ToolExecutor` and keep stdin open (gap 3), or close stdin exactly as every version of this
|
|
@@ -337,6 +413,11 @@ export function wireChildLifecycle(ctx) {
|
|
|
337
413
|
* unconfigured or the pending tool isn't allowlisted).
|
|
338
414
|
*/
|
|
339
415
|
function handleTurnEnd(stopReason) {
|
|
416
|
+
if (stopReason?.startsWith('aborted'))
|
|
417
|
+
enqueueEmit(async () => {
|
|
418
|
+
await cancelUnresolvedToolCalls([...interruptedToolCalls]);
|
|
419
|
+
interruptedToolCalls.clear();
|
|
420
|
+
});
|
|
340
421
|
const toolUse = pendingToolUse;
|
|
341
422
|
const shouldInject = stopReason === 'tool_use' &&
|
|
342
423
|
toolUse !== undefined &&
|
|
@@ -344,7 +425,8 @@ export function wireChildLifecycle(ctx) {
|
|
|
344
425
|
resolveContinuationTransport({ def: def }) === 'stdin-injection' &&
|
|
345
426
|
continuation.autonomousToolNames.has(toolUse.name);
|
|
346
427
|
if (!shouldInject) {
|
|
347
|
-
|
|
428
|
+
if (!keepsStdinOpenAfterTurnEnd(stopReason))
|
|
429
|
+
closeStdinOnce();
|
|
348
430
|
return;
|
|
349
431
|
}
|
|
350
432
|
pendingToolUse = undefined;
|
|
@@ -497,6 +579,9 @@ export function wireChildLifecycle(ctx) {
|
|
|
497
579
|
return;
|
|
498
580
|
}
|
|
499
581
|
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'stdout', data: { chunk: text } } }));
|
|
582
|
+
// Before the parser: an ack and the aborted turn's end can share a chunk, and the turn end must
|
|
583
|
+
// see that no interrupt is still waiting (see `keepsStdinOpenAfterTurnEnd`).
|
|
584
|
+
scanForInterruptAcks(text);
|
|
500
585
|
// Non-null: `streamHandler` is only ever null when `streamFormat === 'plain'` (see its
|
|
501
586
|
// construction above), the branch this statement is provably unreachable from.
|
|
502
587
|
streamHandler.feed({ chunk: text });
|
|
@@ -522,6 +607,9 @@ export function wireChildLifecycle(ctx) {
|
|
|
522
607
|
void terminateChildTreeBestEffort(ctx, child, runId, 'cancel', ctx.onCleanupFailure);
|
|
523
608
|
} });
|
|
524
609
|
child.on('close', (code, signal) => {
|
|
610
|
+
for (const settle of pendingInterruptAcks.values())
|
|
611
|
+
settle(false);
|
|
612
|
+
pendingInterruptAcks.clear();
|
|
525
613
|
void (async () => {
|
|
526
614
|
// Not wrapped in try/catch: all 4 supported parser factories'
|
|
527
615
|
// flush() implementations already internally guard their own
|
|
@@ -536,6 +624,11 @@ export function wireChildLifecycle(ctx) {
|
|
|
536
624
|
// `finish()`'s `'end'` event, exactly like every live-path emit already is.
|
|
537
625
|
flushBufferedStdout();
|
|
538
626
|
await emitQueue;
|
|
627
|
+
if (cancelRequested)
|
|
628
|
+
await cancelUnresolvedToolCalls();
|
|
629
|
+
const subscribedTools = await toolSubscription;
|
|
630
|
+
if (subscribedTools?.kind === 'ok')
|
|
631
|
+
subscribedTools.unsubscribe();
|
|
539
632
|
unsubscribeCancel();
|
|
540
633
|
// Both of the next two steps are guarded: neither a failed cleanup nor a rejecting host
|
|
541
634
|
// classifier may prevent the terminal transition below — see each helper's own doc.
|
|
@@ -554,14 +647,92 @@ export function wireChildLifecycle(ctx) {
|
|
|
554
647
|
await lifecycle.finish({ runId, status, code, signal: signal ?? null, resumable }, { ...(capturedSessionId !== undefined ? { sessionRef: capturedSessionId } : {}) });
|
|
555
648
|
})();
|
|
556
649
|
});
|
|
650
|
+
/**
|
|
651
|
+
* A mid-run message: first an `interrupt` control request on the still-open stdin, so the agent
|
|
652
|
+
* stops generating (or abandons the tool it is running) at once instead of finishing its whole
|
|
653
|
+
* reply; then, once the CLI acknowledges, the message as the next user line, which the CLI
|
|
654
|
+
* answers straight away in the same session; then the run's own `user_message` event through the
|
|
655
|
+
* same FIFO as the agent's output, so the message sits in the stream where it took effect.
|
|
656
|
+
* An ack that never comes (an agent that ignores control requests) only delays the write by
|
|
657
|
+
* {@link INTERRUPT_ACK_TIMEOUT_MS}: the CLI then folds the line in at its next step. Once
|
|
658
|
+
* `turn_end` has closed stdin, or the process exits before the ack, there is no session left to
|
|
659
|
+
* reach — `'not-running'`.
|
|
660
|
+
*/
|
|
661
|
+
async function sendUserMessage(text) {
|
|
662
|
+
if (!acceptsMidRunUserMessages({ def: def }))
|
|
663
|
+
return 'unsupported';
|
|
664
|
+
const stdin = child.stdin;
|
|
665
|
+
if (stdinClosed || !stdin)
|
|
666
|
+
return 'not-running';
|
|
667
|
+
const requestId = `jini-interrupt-${randomUUID()}`;
|
|
668
|
+
interruptSent = true;
|
|
669
|
+
// Capture before requesting the interrupt. Its result can precede the acknowledgement, or
|
|
670
|
+
// arrive after the next segment starts: neither ordering may cancel that segment's new tools.
|
|
671
|
+
enqueueEmit(async () => {
|
|
672
|
+
await toolSubscription;
|
|
673
|
+
for (const id of interruptedToolCalls)
|
|
674
|
+
if (!unresolvedToolCalls.has(id))
|
|
675
|
+
interruptedToolCalls.delete(id);
|
|
676
|
+
for (const id of unresolvedToolCalls)
|
|
677
|
+
interruptedToolCalls.add(id);
|
|
678
|
+
});
|
|
679
|
+
const acked = new Promise((resolve) => pendingInterruptAcks.set(requestId, resolve));
|
|
680
|
+
stdin.write(`${JSON.stringify({ type: 'control_request', request_id: requestId, request: { subtype: 'interrupt' } })}\n`, 'utf8');
|
|
681
|
+
let timer;
|
|
682
|
+
const timedOut = new Promise((resolve) => {
|
|
683
|
+
timer = setTimeout(() => resolve(true), INTERRUPT_ACK_TIMEOUT_MS);
|
|
684
|
+
});
|
|
685
|
+
const processAlive = await Promise.race([acked, timedOut]);
|
|
686
|
+
clearTimeout(timer);
|
|
687
|
+
pendingInterruptAcks.delete(requestId);
|
|
688
|
+
if (!processAlive || stdinClosed)
|
|
689
|
+
return 'not-running';
|
|
690
|
+
stdin.write(`${JSON.stringify(streamJsonUserMessage({ content: [{ type: 'text', text: text }] }))}\n`, 'utf8');
|
|
691
|
+
if (journal)
|
|
692
|
+
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(text) }));
|
|
693
|
+
enqueueEmit(() => lifecycle.emit({ runId: runId, input: { event: 'agent', data: { type: 'user_message', id: randomUUID(), text: text } } }));
|
|
694
|
+
return 'delivered';
|
|
695
|
+
}
|
|
557
696
|
return {
|
|
558
697
|
closeStdinOnce,
|
|
559
698
|
recordSentBytes(content) {
|
|
560
699
|
if (journal)
|
|
561
700
|
enqueueEmit(() => journal.record({ runId: runId, entry: sentJournalEntry(content) }));
|
|
562
701
|
},
|
|
702
|
+
sendUserMessage,
|
|
563
703
|
};
|
|
564
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Whether a def's live process can take another user message mid-run: only a stream-json stdin
|
|
707
|
+
* transport keeps stdin open for more JSONL user lines (see `RuntimeAgentDef.promptInputFormat`).
|
|
708
|
+
* Deliberately not `resolveContinuationTransport`: that answers how a tool result gets back, and
|
|
709
|
+
* reports `'mcp-callback'` for Claude even though its stdin takes user lines all the same.
|
|
710
|
+
*/
|
|
711
|
+
export function acceptsMidRunUserMessages({ def }) {
|
|
712
|
+
return def.promptViaStdin === true && def.promptInputFormat === 'stream-json';
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* How long a mid-run message waits for the CLI to acknowledge its interrupt before being written
|
|
716
|
+
* anyway. Claude Code acks within milliseconds (it answers before the aborted turn's own result);
|
|
717
|
+
* the ceiling only bounds an agent that ignores control requests.
|
|
718
|
+
*/
|
|
719
|
+
export const INTERRUPT_ACK_TIMEOUT_MS = 5_000;
|
|
720
|
+
/** The `request_id` of a stream-json `control_response` line, or `undefined` for anything else. */
|
|
721
|
+
function controlResponseRequestId({ line }) {
|
|
722
|
+
try {
|
|
723
|
+
const parsed = JSON.parse(line);
|
|
724
|
+
if (!isRecord(parsed) || parsed.type !== 'control_response' || !isRecord(parsed.response))
|
|
725
|
+
return undefined;
|
|
726
|
+
return typeof parsed.response.request_id === 'string' ? parsed.response.request_id : undefined;
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/** The stream-json stdin envelope for one user message — the initial prompt and every mid-run one. */
|
|
733
|
+
function streamJsonUserMessage({ content }) {
|
|
734
|
+
return { type: 'user', message: { role: 'user', content: content } };
|
|
735
|
+
}
|
|
565
736
|
/**
|
|
566
737
|
* Wires an ACP child to a run. Unlike the JSON-stream path, ACP owns the
|
|
567
738
|
* prompt protocol and reports its parsed events through `attachAcpSession`'s
|
|
@@ -773,8 +944,8 @@ function wirePiRpcLifecycle(ctx) {
|
|
|
773
944
|
* - `'text'` (default): the raw prompt buffer, then stdin is closed —
|
|
774
945
|
* matches `RuntimeAgentDef.promptInputFormat`'s own doc.
|
|
775
946
|
* - `'stream-json'`: one JSONL line wrapping the prompt as an Anthropic
|
|
776
|
-
* user message; stdin is deliberately left open
|
|
777
|
-
*
|
|
947
|
+
* user message; stdin is deliberately left open so a message the human
|
|
948
|
+
* sends mid-run can follow it (`StdinCloseHandle.sendUserMessage`), and
|
|
778
949
|
* {@link wireChildLifecycle}'s `turn_end` handling closes it once the
|
|
779
950
|
* agent's own stream reports the turn ended.
|
|
780
951
|
* @param def - The resolved agent def (only `.promptInputFormat` is read).
|
|
@@ -789,7 +960,7 @@ export function writePromptToStdin(def, child, prompt, handle) {
|
|
|
789
960
|
if (!stdin)
|
|
790
961
|
return;
|
|
791
962
|
if (def.promptInputFormat === 'stream-json') {
|
|
792
|
-
const line = JSON.stringify({
|
|
963
|
+
const line = JSON.stringify(streamJsonUserMessage({ content: messageContentWithImages({ prompt, images: handle.imageContents ?? [] }, {}) }));
|
|
793
964
|
stdin.write(`${line}\n`, 'utf8');
|
|
794
965
|
handle.recordSentBytes(prompt);
|
|
795
966
|
return;
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
* a second, independently-built (and rejected) classifier that once lived in
|
|
90
90
|
* this module.
|
|
91
91
|
*/
|
|
92
|
-
export { type SupportedStreamFormat, type AgentExecutorCompatibility, type AgentRuntimeEventTranslation, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, type McpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, type FailureClassificationContext, type ClassifyFailure, type CreateAgentExecutorOptions, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult, } from './contracts.js';
|
|
92
|
+
export { type SupportedStreamFormat, type AgentExecutorCompatibility, type AgentRuntimeEventTranslation, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, type McpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, type FailureClassificationContext, type ClassifyFailure, type CreateAgentExecutorOptions, type SystemPromptOverlayDelivery, type PreparedSystemPromptOverlayFile, type SpawnAgentChildProcessResult, } from './contracts.js';
|
|
93
93
|
export { isSupportedStreamFormat, assessAgentExecutorCompatibility, isAgentExecutorSupported, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, resolveSystemPromptOverlayDelivery, buildRunArgs, prepareSystemPromptOverlayFileIfNeeded, guardWindowsCommandLineBudget, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, createAgentExecutor, } from './launch.js';
|
|
94
94
|
export { extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, applyAgentTranslationSideEffects, } from './event-translation.js';
|
|
95
95
|
export { MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, buildMcpBridgeDelivery, resolveSourceClaudeConfigDir, resolveMcpBridgeForRun, writeMcpJsonIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, } from './mcp-bridge.js';
|
|
@@ -640,6 +640,18 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
640
640
|
await lifecycle.finish({ runId, status: 'failed', code: null, signal: null, resumable: false });
|
|
641
641
|
throw new AgentExecutorError({ code: code, message: message });
|
|
642
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Every run with a live child, keyed by run id, mapped to how a mid-run message reaches it — the
|
|
645
|
+
* stdin handle's `sendUserMessage` for a stdin-driven run, a constant `'unsupported'` for an
|
|
646
|
+
* ACP/pi run (whose RPC takes one prompt per turn). Removed on the child's `'close'`, so a
|
|
647
|
+
* finished or unknown run reads as `'not-running'`. Bounded by the number of live children.
|
|
648
|
+
*/
|
|
649
|
+
const liveRunInputs = new Map();
|
|
650
|
+
/** `AgentExecutor.sendUserMessage()` — see that interface method's own doc. @complexity O(1). */
|
|
651
|
+
async function sendUserMessage({ runId, text }) {
|
|
652
|
+
const deliver = liveRunInputs.get(runId);
|
|
653
|
+
return deliver ? deliver(text) : 'not-running';
|
|
654
|
+
}
|
|
643
655
|
/**
|
|
644
656
|
* `AgentExecutor.run()` — see that interface method's own doc for the
|
|
645
657
|
* public contract. Implementation note on shape: every guard below
|
|
@@ -934,7 +946,18 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
934
946
|
expectsJiniBridge: writtenMcpJsonPath !== undefined,
|
|
935
947
|
})
|
|
936
948
|
: null;
|
|
937
|
-
|
|
949
|
+
// Registered with the lifecycle wiring above, before the spawn-confirmation await, so a child
|
|
950
|
+
// that closes immediately never leaves a stale entry behind.
|
|
951
|
+
const runId = input.runId;
|
|
952
|
+
liveRunInputs.set(runId, stdinHandle ? stdinHandle.sendUserMessage : async () => 'unsupported');
|
|
953
|
+
child.once('close', () => liveRunInputs.delete(runId));
|
|
954
|
+
try {
|
|
955
|
+
await confirmChildSpawned({ input: { runId: input.runId, def, child }, deps: { releaseStagedResources, failBeforeSpawn } });
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
liveRunInputs.delete(runId);
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
938
961
|
// Now — and only now — is there a live process that could consume the locked side effect, so
|
|
939
962
|
// this is where a def's handoff watcher starts. Deliberately not awaited: the whole point is to
|
|
940
963
|
// release the lock as soon as the child confirms the handoff, in parallel with this run
|
|
@@ -1019,6 +1042,6 @@ export function createAgentExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
1019
1042
|
stdinHandle.imageContents = input.imageContents;
|
|
1020
1043
|
writePromptToStdin(def, child, stdinPrompt, stdinHandle);
|
|
1021
1044
|
}
|
|
1022
|
-
return { run };
|
|
1045
|
+
return { run, sendUserMessage };
|
|
1023
1046
|
}
|
|
1024
1047
|
//# sourceMappingURL=launch.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fsPromises, } from 'node:fs';
|
|
2
2
|
import { homedir, tmpdir, } from 'node:os';
|
|
3
|
-
import { join, } from 'node:path';
|
|
3
|
+
import { join, resolve, } from 'node:path';
|
|
4
4
|
import { isRecord, errorMessage, } from './values.js';
|
|
5
5
|
const JINI_MCP_SERVER_KEY = 'jini';
|
|
6
6
|
/** The run-error code a run gets when its CLI reports the injected `jini` bridge as anything but connected. */
|
|
@@ -552,12 +552,23 @@ function defaultMkdtempCodexHome({ prefix }) {
|
|
|
552
552
|
function defaultRemoveCodexHomeDir({ path }) {
|
|
553
553
|
return fsPromises.rm(path, { recursive: true, force: true });
|
|
554
554
|
}
|
|
555
|
+
/** Config credentials stay run-scoped, but rollouts must survive both completion and cancellation.
|
|
556
|
+
* Native directory links keep Codex as the sole writer of its own persisted session format.
|
|
557
|
+
* Cleanup removes the links themselves, never their targets. Two fixed directories, O(1) I/O. */
|
|
558
|
+
async function defaultLinkCodexSessionStore({ runHome, sourceHome }, _optional = {}) {
|
|
559
|
+
for (const name of ['sessions', 'archived_sessions']) {
|
|
560
|
+
const target = resolve(sourceHome, name);
|
|
561
|
+
await fsPromises.mkdir(target, { recursive: true, mode: 0o700 });
|
|
562
|
+
await fsPromises.symlink(target, join(runHome, name), 'junction');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
555
565
|
function resolveCodexHomeSeams(options) {
|
|
556
566
|
return {
|
|
557
567
|
mkdtemp: options.mkdtemp ?? defaultMkdtempCodexHome,
|
|
558
568
|
readFile: options.readFile ?? defaultReadMcpJsonFile,
|
|
559
569
|
writeFile: options.writeFile ?? defaultWriteMcpJsonFile,
|
|
560
570
|
removeDir: options.removeDir ?? defaultRemoveCodexHomeDir,
|
|
571
|
+
linkSessionStore: options.linkSessionStore ?? defaultLinkCodexSessionStore,
|
|
561
572
|
};
|
|
562
573
|
}
|
|
563
574
|
/**
|
|
@@ -573,8 +584,9 @@ function resolveCodexHomeSeams(options) {
|
|
|
573
584
|
* 0.151.0) to fail fast with a structured `401 Unauthorized` stream event, never an interactive
|
|
574
585
|
* login prompt or a hang — see `defs/codex.ts`'s module doc for the full transcript summary.
|
|
575
586
|
*
|
|
576
|
-
* **Never
|
|
577
|
-
*
|
|
587
|
+
* **Never changes the real config or credentials.** Those source files stay read-only. Only the
|
|
588
|
+
* native rollout directories are shared: deleting a run's scratch home must not erase the thread
|
|
589
|
+
* whose id the host already persisted. No copied login or run-bound MCP credential is retained.
|
|
578
590
|
*
|
|
579
591
|
* A failure after the directory is created (a rejecting `writeFile`, most plausibly) does not leak
|
|
580
592
|
* it: the directory may already hold a partial `config.toml` or a copied credential, so the
|
|
@@ -584,9 +596,9 @@ function resolveCodexHomeSeams(options) {
|
|
|
584
596
|
* `@jini-ai/agent-runtime`'s `prepareAgentLogFile`'s `label` is.
|
|
585
597
|
* @param entry - The shared bridge entry.
|
|
586
598
|
* @param sourceCodexHomeDir - Where to read the real install's `config.toml`/`auth.json` from — see {@link resolveSourceCodexHomeDir}.
|
|
587
|
-
* @param seams - Injectable mkdtemp/readFile/writeFile/removeDir, real filesystem by default.
|
|
588
|
-
* @throws Whatever
|
|
589
|
-
* @complexity O(1)
|
|
599
|
+
* @param seams - Injectable mkdtemp/readFile/writeFile/removeDir/linkSessionStore, real filesystem by default.
|
|
600
|
+
* @throws Whatever staging rejects with — the caller ({@link prepareCodexHomeIfNeeded}) turns that into a pre-spawn `AGENT_SPAWN_FAILED` failure, matching {@link writeMcpJsonForRun}'s own contract.
|
|
601
|
+
* @complexity O(1): fixed directory links and up to two best-effort file read/write round trips.
|
|
590
602
|
* @overallScore 100/100
|
|
591
603
|
*/
|
|
592
604
|
async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
@@ -616,6 +628,7 @@ async function prepareCodexHomeForRun(runId, entry, sourceCodexHomeDir, seams) {
|
|
|
616
628
|
// No stored login (or unreadable) — the spawned CLI runs unauthenticated. Confirmed above:
|
|
617
629
|
// this fails the run fast and observably, never as a hang.
|
|
618
630
|
}
|
|
631
|
+
await seams.linkSessionStore({ runHome: dir, sourceHome: sourceCodexHomeDir }, {});
|
|
619
632
|
}
|
|
620
633
|
catch (err) {
|
|
621
634
|
await seams.removeDir({ path: dir }).catch(() => {
|
package/dist/agent-executor.d.ts
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* wrappers or a second implementation would let tested and shipped behavior drift.
|
|
7
7
|
* See agent-executor/index.ts for the driver contracts and invariants.
|
|
8
8
|
*/
|
|
9
|
-
export { type SupportedStreamFormat, isSupportedStreamFormat, type AgentExecutorCompatibility, assessAgentExecutorCompatibility, isAgentExecutorSupported, type AgentRuntimeEventTranslation, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, type McpBridgeDelivery, buildMcpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, resolveSourceClaudeConfigDir, type FailureClassificationContext, type ClassifyFailure, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, type CreateAgentExecutorOptions, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, type SystemPromptOverlayDelivery, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, type PreparedSystemPromptOverlayFile, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, type SpawnAgentChildProcessResult, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
|
|
9
|
+
export { type SupportedStreamFormat, isSupportedStreamFormat, type AgentExecutorCompatibility, assessAgentExecutorCompatibility, isAgentExecutorSupported, type AgentRuntimeEventTranslation, extractUsageTokens, translateStatusEvent, translateToolResultEvent, translateErrorEvent, translateTurnEndEvent, translateAgentRuntimeEvent, type AgentExecutorErrorCode, AgentExecutorError, type AgentExecutorRunInput, type AgentExecutor, type UserMessageDelivery, type CollectProcessTreePidsPort, type StopProcessesPort, type AgentCleanupFailurePhase, type AgentCleanupFailureContext, type ContinuationOptions, type McpJsonInjectionOptions, MCP_BRIDGE_UNAVAILABLE, unavailableJiniBridgeStatus, buildMcpJsonServerEntry, mergeMcpJsonContent, buildAcpMcpBridgeServers, mergeEnvContentMcpConfig, mergeEnvContentInstructions, buildCodexMcpServerToml, buildCodexHomeConfigToml, resolveSourceCodexHomeDir, type McpBridgeDelivery, buildMcpBridgeDelivery, type PreparedCodexHome, type PreparedClaudeConfigDir, type ClaudeConfigDirSeams, type ClaudeConfigDirIsolationOptions, resolveSourceClaudeConfigDir, type FailureClassificationContext, type ClassifyFailure, DEFAULT_BUFFERED_STDOUT_MAX_BYTES, applyAgentTranslationSideEffects, type CreateAgentExecutorOptions, resolveDefAndStreamFormat, resolveImageDeliveryAndArgvBudget, resolveRunEnv, resolveLaunch, stagePromptFile, stageLogFile, resolveMcpBridgeForRun, computeChildEnv, computeRuntimeContext, buildAgentBuildArgsOptions, type SystemPromptOverlayDelivery, resolveSystemPromptOverlayDelivery, buildRunArgs, writeMcpJsonIfNeeded, type PreparedSystemPromptOverlayFile, prepareSystemPromptOverlayFileIfNeeded, prepareCodexHomeIfNeeded, prepareClaudeConfigDirIfNeeded, guardWindowsCommandLineBudget, type SpawnAgentChildProcessResult, spawnAgentChildProcess, isStdinDrivenFormat, confirmChildSpawned, armHandoffWatcher, runAcpDispatch, runPiRpcDispatch, createAgentExecutor, } from './agent-executor/index.js';
|
|
10
10
|
//# sourceMappingURL=agent-executor.d.ts.map
|
package/dist/http/index.d.ts
CHANGED
|
@@ -9,8 +9,8 @@ export type { CatalogueEntry, HostEditor, HostEditorsResponse, HostToolLaunchPla
|
|
|
9
9
|
export { applicableForPlatform, CATALOGUE, currentPlatform, defaultProbeEnv, hostEditorsRoute, launchHostTool, listAvailableEditors, pathDirs, probeCommandOnPath, probeMacBundle, registerHostToolsRoutes, resolveEntry, resolveHostToolLaunchPlan, } from './host-tools.js';
|
|
10
10
|
export type { DaemonShutdownResponse, DaemonStatusDeps, DaemonStatusResponse, } from './daemon-status.js';
|
|
11
11
|
export { daemonShutdownRoute, daemonStatusRoute, registerDaemonStatusRoutes } from './daemon-status.js';
|
|
12
|
-
export type { RunCancelResponse, RunCreateRequest, RunHttpDeps, RunInternalErrorContext, RunListResponse, RunStartContext, RunStartHandler, RunStartResponse, RunStatusResponse, } from './runs.js';
|
|
13
|
-
export { registerRunEventStream, registerRunRoutes, RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
|
|
12
|
+
export type { RunCancelResponse, RunCreateRequest, RunHttpDeps, RunInternalErrorContext, RunListResponse, RunStartContext, RunMessageRequest, RunMessageResponse, RunStartHandler, RunStartResponse, RunStatusResponse, } from './runs.js';
|
|
13
|
+
export { registerRunEventStream, registerRunRoutes, RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runMessageRoute, RUN_MESSAGE_MAX_CHARS, runStartRoute, runStatusRoute, } from './runs.js';
|
|
14
14
|
export { JINI_ROUTE_MANIFEST, manifestRoutesForFamilies, routeFamilyManifest } from './route-manifest.js';
|
|
15
15
|
export type { AgentListResponse, AgentModelSummary, AgentsHttpDeps, AgentSummary } from './agents.js';
|
|
16
16
|
export { agentListRoute, agentRescanRoute, registerAgentRoutes } from './agents.js';
|
package/dist/http/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { cancelRunsOwnedBy } from './cancel-owned-runs.js';
|
|
|
3
3
|
export { ACTIVE_CONTEXT_TTL_MS, getActiveRoute, registerActiveContextRoutes, setActiveRoute, } from './active-context.js';
|
|
4
4
|
export { applicableForPlatform, CATALOGUE, currentPlatform, defaultProbeEnv, hostEditorsRoute, launchHostTool, listAvailableEditors, pathDirs, probeCommandOnPath, probeMacBundle, registerHostToolsRoutes, resolveEntry, resolveHostToolLaunchPlan, } from './host-tools.js';
|
|
5
5
|
export { daemonShutdownRoute, daemonStatusRoute, registerDaemonStatusRoutes } from './daemon-status.js';
|
|
6
|
-
export { registerRunEventStream, registerRunRoutes, RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
|
|
6
|
+
export { registerRunEventStream, registerRunRoutes, RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runMessageRoute, RUN_MESSAGE_MAX_CHARS, runStartRoute, runStatusRoute, } from './runs.js';
|
|
7
7
|
export { JINI_ROUTE_MANIFEST, manifestRoutesForFamilies, routeFamilyManifest } from './route-manifest.js';
|
|
8
8
|
export { agentListRoute, agentRescanRoute, registerAgentRoutes } from './agents.js';
|
|
9
9
|
export { memoryClearExtractionsRoute, memoryClearVerificationsRoute, memoryCreateEntryRoute, memoryDeleteEntryRoute, memoryListExtractionsRoute, memoryListVerificationsRoute, memoryOverviewRoute, memoryReadEntryRoute, memoryRemoveExtractionRoute, memoryRemoveVerificationRoute, memoryTreeRoute, memoryUpdateEntryRoute, memoryUpdateTreeNodeRoute, memoryWriteConfigRoute, memoryWriteIndexRoute, registerMemoryEventStream, registerMemoryRoutes, } from './memory.js';
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
import { agentListRoute, agentRescanRoute } from './agents.js';
|
|
32
32
|
import { delegatedToolExecuteRoute } from './delegated-tools.js';
|
|
33
33
|
import { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyRoute, versionInfoRoute, } from './health.js';
|
|
34
|
-
import { RUN_EVENTS_ROUTE_PATH, runCancelRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
|
|
34
|
+
import { RUN_EVENTS_ROUTE_PATH, runCancelRoute, runMessageRoute, runListRoute, runStartRoute, runStatusRoute, } from './runs.js';
|
|
35
35
|
import { toolCatalogDescribeRoute, toolCatalogSearchRoute } from './tool-catalog.js';
|
|
36
36
|
/**
|
|
37
37
|
* Projects route specs onto `RouteRegistration`s, upper-casing the method so entries compare directly
|
|
@@ -52,7 +52,7 @@ function fromSpecs(...specs) {
|
|
|
52
52
|
export const JINI_ROUTE_MANIFEST = {
|
|
53
53
|
health: fromSpecs(healthRoute, apiHealthRoute, readyRoute, apiReadyRoute, versionInfoRoute, apiVersionInfoRoute),
|
|
54
54
|
runs: [
|
|
55
|
-
...fromSpecs(runStartRoute, runListRoute, runStatusRoute, runCancelRoute),
|
|
55
|
+
...fromSpecs(runStartRoute, runListRoute, runStatusRoute, runCancelRoute, runMessageRoute),
|
|
56
56
|
// Registered with a bare `app.get` rather than a route spec (by `registerRunRoutes` itself), so
|
|
57
57
|
// it contributes its exported path constant instead of a copied literal.
|
|
58
58
|
{ method: 'GET', path: RUN_EVENTS_ROUTE_PATH },
|
package/dist/http/runs.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Express } from 'express';
|
|
2
2
|
import { type RunStatus } from '@jini-ai/protocol';
|
|
3
|
-
import type { RunLifecycle } from '../index.js';
|
|
3
|
+
import type { RunLifecycle, UserMessageDelivery } from '../index.js';
|
|
4
4
|
import { type AdapterContext } from '@jini-ai/http-kit';
|
|
5
5
|
import type { ServerResponse } from 'node:http';
|
|
6
6
|
/**
|
|
@@ -34,6 +34,15 @@ export interface RunHttpDeps {
|
|
|
34
34
|
readonly onStarted?: RunStartHandler;
|
|
35
35
|
/** Host-owned sink for the real exception behind a generic `INTERNAL_ERROR` response (SEC-005). Defaults to `console.error`. */
|
|
36
36
|
readonly onInternalError?: (context: RunInternalErrorContext) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Delivers a message the human sent mid-run into that run's live agent — a host passes its
|
|
39
|
+
* `AgentExecutor.sendUserMessage`. Omitted, every such message reports `'unsupported'`, so the
|
|
40
|
+
* client falls back to interrupting the run and sending the text as the next turn.
|
|
41
|
+
*/
|
|
42
|
+
readonly deliverUserMessage?: (input: {
|
|
43
|
+
readonly runId: string;
|
|
44
|
+
readonly text: string;
|
|
45
|
+
}) => UserMessageDelivery | Promise<UserMessageDelivery>;
|
|
37
46
|
}
|
|
38
47
|
export interface RunStartResponse {
|
|
39
48
|
readonly run: RunStatus;
|
|
@@ -45,6 +54,15 @@ export interface RunStatusResponse {
|
|
|
45
54
|
export interface RunCancelResponse {
|
|
46
55
|
readonly run: RunStatus;
|
|
47
56
|
}
|
|
57
|
+
export interface RunMessageRequest {
|
|
58
|
+
readonly runId: string;
|
|
59
|
+
readonly text: string;
|
|
60
|
+
}
|
|
61
|
+
export interface RunMessageResponse {
|
|
62
|
+
readonly delivery: 'delivered';
|
|
63
|
+
}
|
|
64
|
+
/** Ceiling on one mid-run message, matching what a person can reasonably type or paste into a composer. */
|
|
65
|
+
export declare const RUN_MESSAGE_MAX_CHARS = 100000;
|
|
48
66
|
export interface RunListResponse {
|
|
49
67
|
readonly runs: readonly RunStatus[];
|
|
50
68
|
}
|
|
@@ -58,6 +76,13 @@ export declare const runCancelRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
|
|
|
58
76
|
runId: string;
|
|
59
77
|
reason?: string;
|
|
60
78
|
}, RunCancelResponse, RunHttpDeps, undefined>;
|
|
79
|
+
/**
|
|
80
|
+
* `POST /api/runs/:runId/messages` — a message the human sent while the run is going. `202` when it
|
|
81
|
+
* reached the live agent; `409 CONFLICT` with `details.delivery` (`'unsupported'` or
|
|
82
|
+
* `'not-running'`) when it did not, which tells the client whether to interrupt the run and send the
|
|
83
|
+
* text as the next turn, or simply send it next because the run is already ending.
|
|
84
|
+
*/
|
|
85
|
+
export declare const runMessageRoute: import("@jini-ai/http-kit").JsonRouteSpec<RunMessageRequest, RunMessageResponse, RunHttpDeps, undefined>;
|
|
61
86
|
/**
|
|
62
87
|
* The core of `GET /api/runs/:runId/events` — canonical events as SSE, with Last-Event-ID
|
|
63
88
|
* reconnect support. Takes `runId`/`afterCursor` already resolved by the caller. `res` is typed
|
|
@@ -82,7 +107,7 @@ export declare function registerRunEventStream({ app, deps }: {
|
|
|
82
107
|
readonly app: Express;
|
|
83
108
|
readonly deps: RunHttpDeps;
|
|
84
109
|
}, _optional?: Record<string, never>): void;
|
|
85
|
-
/** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
|
|
110
|
+
/** Mounts create/status/cancel/message JSON endpoints and the SSE event stream as one run transport. */
|
|
86
111
|
export declare function registerRunRoutes({ app, deps, adapter }: {
|
|
87
112
|
readonly app: Express;
|
|
88
113
|
readonly deps: RunHttpDeps;
|
package/dist/http/runs.js
CHANGED
|
@@ -57,6 +57,23 @@ function parseRunCreate(input) {
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
+
/** Ceiling on one mid-run message, matching what a person can reasonably type or paste into a composer. */
|
|
61
|
+
export const RUN_MESSAGE_MAX_CHARS = 100_000;
|
|
62
|
+
function parseRunMessage(input) {
|
|
63
|
+
const parsedRunId = parseRunId(input);
|
|
64
|
+
if (!parsedRunId.ok)
|
|
65
|
+
return parsedRunId;
|
|
66
|
+
if (!isRecord(input.body))
|
|
67
|
+
return err({ error: validationError({ message: 'body must be a JSON object' }) });
|
|
68
|
+
const text = optionalString(input.body, 'text');
|
|
69
|
+
if (text === undefined || text === null) {
|
|
70
|
+
return err({ error: validationError({ message: 'text must be a non-empty string' }, { issues: [{ path: 'text', message: 'required non-empty string' }] }) });
|
|
71
|
+
}
|
|
72
|
+
if (text.length > RUN_MESSAGE_MAX_CHARS) {
|
|
73
|
+
return err({ error: validationError({ message: `text must be at most ${RUN_MESSAGE_MAX_CHARS} characters` }, { issues: [{ path: 'text', message: 'too long' }] }) });
|
|
74
|
+
}
|
|
75
|
+
return ok({ value: { runId: parsedRunId.value, text } });
|
|
76
|
+
}
|
|
60
77
|
function parseRunList(input) {
|
|
61
78
|
const value = input.query.contextRef;
|
|
62
79
|
if (value === undefined)
|
|
@@ -121,6 +138,23 @@ export const runCancelRoute = defineJsonRoute({
|
|
|
121
138
|
return ok({ value: { run: await deps.lifecycle.cancel(input) } });
|
|
122
139
|
}
|
|
123
140
|
}, { requireSameOrigin: true });
|
|
141
|
+
/**
|
|
142
|
+
* `POST /api/runs/:runId/messages` — a message the human sent while the run is going. `202` when it
|
|
143
|
+
* reached the live agent; `409 CONFLICT` with `details.delivery` (`'unsupported'` or
|
|
144
|
+
* `'not-running'`) when it did not, which tells the client whether to interrupt the run and send the
|
|
145
|
+
* text as the next turn, or simply send it next because the run is already ending.
|
|
146
|
+
*/
|
|
147
|
+
export const runMessageRoute = defineJsonRoute({
|
|
148
|
+
method: 'post', path: '/api/runs/:runId/messages', parse: parseRunMessage, handle: async ({ input, deps }) => {
|
|
149
|
+
const run = await deps.lifecycle.get({ runId: input.runId });
|
|
150
|
+
if (run === undefined)
|
|
151
|
+
return err({ error: createApiError({ code: 'NOT_FOUND', message: `run "${input.runId}" was not found` }) });
|
|
152
|
+
const delivery = deps.deliverUserMessage ? await deps.deliverUserMessage({ runId: input.runId, text: input.text }) : 'unsupported';
|
|
153
|
+
if (delivery === 'delivered')
|
|
154
|
+
return ok({ value: { delivery } });
|
|
155
|
+
return err({ error: createApiError({ code: 'CONFLICT', message: `run "${input.runId}" did not take the message` }, { details: { delivery } }) });
|
|
156
|
+
}
|
|
157
|
+
}, { requireSameOrigin: true, successStatus: 202 });
|
|
124
158
|
function sendStreamFailure(res, kind) {
|
|
125
159
|
if (kind.kind === 'unknown-run') {
|
|
126
160
|
sendRawApiError({ res, status: 404, error: createApiError({ code: 'NOT_FOUND', message: 'run was not found' }) });
|
|
@@ -210,12 +244,13 @@ export function registerRunEventStream({ app, deps }, _optional = {}) {
|
|
|
210
244
|
await handleRunEventStreamRequest({ res, runId, afterCursor: requestedAfterCursor(req), deps });
|
|
211
245
|
});
|
|
212
246
|
}
|
|
213
|
-
/** Mounts create/status/cancel JSON endpoints and the SSE event stream as one run transport. */
|
|
247
|
+
/** Mounts create/status/cancel/message JSON endpoints and the SSE event stream as one run transport. */
|
|
214
248
|
export function registerRunRoutes({ app, deps, adapter }, _optional = {}) {
|
|
215
249
|
mountJsonRoute({ app, spec: runStartRoute, deps, adapter });
|
|
216
250
|
mountJsonRoute({ app, spec: runListRoute, deps, adapter });
|
|
217
251
|
mountJsonRoute({ app, spec: runStatusRoute, deps, adapter });
|
|
218
252
|
mountJsonRoute({ app, spec: runCancelRoute, deps, adapter });
|
|
253
|
+
mountJsonRoute({ app, spec: runMessageRoute, deps, adapter });
|
|
219
254
|
registerRunEventStream({ app, deps });
|
|
220
255
|
}
|
|
221
256
|
//# sourceMappingURL=runs.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The stateful agent daemon runtime: run lifecycle, durable event-log port, the tool-execution boundary, agent executor, terminal sessions, and routines.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -139,17 +139,17 @@
|
|
|
139
139
|
"@types/pg": "8.23.1",
|
|
140
140
|
"express": "^4.21.0",
|
|
141
141
|
"@types/express": "^4.17.21",
|
|
142
|
-
"@jini-ai/chat": "0.5.5",
|
|
143
|
-
"@jini-ai/http-kit": "0.4.1",
|
|
144
142
|
"@jini-ai/db": "0.3.1",
|
|
145
|
-
"@jini-ai/
|
|
143
|
+
"@jini-ai/http-kit": "0.4.1",
|
|
144
|
+
"@jini-ai/agent-runtime": "^0.4.4",
|
|
145
|
+
"@jini-ai/chat": "0.5.7",
|
|
146
146
|
"@jini-ai/core": "^0.4.2",
|
|
147
|
-
"@jini-ai/
|
|
148
|
-
"@jini-ai/
|
|
147
|
+
"@jini-ai/platform": "^0.5.2",
|
|
148
|
+
"@jini-ai/protocol": "^0.4.3"
|
|
149
149
|
},
|
|
150
150
|
"dependencies": {
|
|
151
151
|
"@jini-ai/core": "^0.4.2",
|
|
152
|
-
"@jini-ai/protocol": "^0.4.
|
|
152
|
+
"@jini-ai/protocol": "^0.4.3"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
155
|
"node-pty": "^1.1.0",
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
"pg": "^8.23.0",
|
|
161
161
|
"@jini-ai/http-kit": "^0.4.0",
|
|
162
162
|
"express": "^4.21.0",
|
|
163
|
-
"@jini-ai/agent-runtime": "^0.4.
|
|
163
|
+
"@jini-ai/agent-runtime": "^0.4.4",
|
|
164
164
|
"@jini-ai/platform": "^0.5.1"
|
|
165
165
|
},
|
|
166
166
|
"peerDependenciesMeta": {
|