@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
package/dist/http/runs.js
CHANGED
|
@@ -154,8 +154,8 @@ export const runCancelRoute = defineJsonRoute({
|
|
|
154
154
|
/**
|
|
155
155
|
* `POST /api/runs/:runId/messages` — a message the human sent while the run is going. `202` when it
|
|
156
156
|
* reached the live agent; `409 CONFLICT` with `details.delivery` (`'unsupported'` or
|
|
157
|
-
* `'not-running'`) when it did not, which tells the client
|
|
158
|
-
*
|
|
157
|
+
* `'not-running'`) when it did not, which tells the client to retain the next-turn queue without canceling live work.
|
|
158
|
+
* Acceptance is distinct from the UUID-attributed consumption receipt in the run stream.
|
|
159
159
|
*/
|
|
160
160
|
export const runMessageRoute = defineJsonRoute({
|
|
161
161
|
method: 'post', path: '/api/runs/:runId/messages', parse: parseRunMessage, handle: async ({ input, deps }) => {
|
|
@@ -163,9 +163,10 @@ export const runMessageRoute = defineJsonRoute({
|
|
|
163
163
|
if (run === undefined)
|
|
164
164
|
return err({ error: createApiError({ code: 'NOT_FOUND', message: `run "${input.runId}" was not found` }) });
|
|
165
165
|
const deliver = input.attachmentIds?.length ? deps.deliverUserMessageWithAttachments : deps.deliverUserMessage;
|
|
166
|
-
const
|
|
166
|
+
const id = randomUUID();
|
|
167
|
+
const delivery = deliver ? await deliver({ ...input, id }) : 'unsupported';
|
|
167
168
|
if (delivery === 'delivered')
|
|
168
|
-
return ok({ value: { delivery } });
|
|
169
|
+
return ok({ value: { delivery, id } });
|
|
169
170
|
return err({ error: createApiError({ code: 'CONFLICT', message: `run "${input.runId}" did not take the message` }, { details: { delivery } }) });
|
|
170
171
|
}
|
|
171
172
|
}, { requireSameOrigin: true, successStatus: 202 });
|
|
@@ -208,7 +209,12 @@ export async function handleRunEventStreamRequest({ res, runId, afterCursor, dep
|
|
|
208
209
|
}
|
|
209
210
|
});
|
|
210
211
|
try {
|
|
211
|
-
const subscribed = await deps.lifecycle.stream({ runId, onEvent: (event) =>
|
|
212
|
+
const subscribed = await deps.lifecycle.stream({ runId, onEvent: (event) => {
|
|
213
|
+
// RunLifecycle validates the run/cursor before replay callbacks. Open before enqueueing
|
|
214
|
+
// history so a healthy response drains replay instead of overflowing the unopened queue.
|
|
215
|
+
channel.open();
|
|
216
|
+
channel.enqueue({ event });
|
|
217
|
+
} }, { afterCursor });
|
|
212
218
|
if (subscribed.kind !== 'ok') {
|
|
213
219
|
// Nothing was ever subscribed, so `abandon()` has nothing to unsubscribe — it only marks
|
|
214
220
|
// the channel closed. `res` itself is untouched, leaving `sendStreamFailure` free to send
|
package/dist/index.d.ts
CHANGED
|
@@ -93,4 +93,6 @@ export * from './run/index.js';
|
|
|
93
93
|
export * from './continuation/index.js';
|
|
94
94
|
export * from './routines/index.js';
|
|
95
95
|
export * from './attachment-content.js';
|
|
96
|
+
export * from './delegated-result-correlation.js';
|
|
97
|
+
export type { StdoutJournalBlobPort } from './tool-result-diagnostics.js';
|
|
96
98
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
package/dist/run-lifecycle.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RunLifecycle` — start/stream/cancel/resume a run keyed on an opaque
|
|
3
|
+
* `contextRef` (extraction-plan §2.1: runs key on an opaque `contextRef`,
|
|
4
|
+
* never a product-record identifier), generalized from OD's `design.runs` service
|
|
5
|
+
* (`apps/daemon/src/runtimes/runs.ts` + the terminal-decision half of
|
|
6
|
+
* `apps/daemon/src/runtimes/start-chat-run.ts`, `arch/server-startserver-endgame`
|
|
7
|
+
* branch — see `archived provenance ledger` for exact line citations).
|
|
8
|
+
*
|
|
9
|
+
* Scope boundary: this module owns the run *state machine* and its event
|
|
10
|
+
* log — create, transition, emit, replay, cancellation-intent propagation,
|
|
11
|
+
* idempotent start/finish, resume. It does not spawn or signal a subprocess;
|
|
12
|
+
* that is `@jini-ai/agent-runtime`'s job (extraction-plan task 7). A driver
|
|
13
|
+
* (today: this package's tests; later: agent-runtime) calls `emit()` for
|
|
14
|
+
* agent/stdout/stderr/error events, observes cancellation via
|
|
15
|
+
* `onCancelRequested`, and calls `finish()` once it knows the real outcome.
|
|
16
|
+
*/
|
|
17
|
+
import type { DelegatedResultCorrelation } from './delegated-result-correlation.js';
|
|
1
18
|
import type { RunAgentPayload, RunCancelRequest, RunChunkPayload, RunErrorPayload, RunProtocolEvent, RunStartPayload, RunStatus } from '@jini-ai/protocol';
|
|
2
19
|
import type { EventLog, EventLogEntry, EventLogReplayResult } from './event-log.js';
|
|
3
20
|
import type { InactivityWatchdog, TerminalRunOutcome } from './close-status.js';
|
|
@@ -276,6 +293,8 @@ export declare function finishStreamSubscription({ record, subscriber, terminal
|
|
|
276
293
|
readonly terminal: boolean;
|
|
277
294
|
}): StreamSubscribeResult;
|
|
278
295
|
export interface CreateRunLifecycleInput {
|
|
296
|
+
/** Share the negotiated bridge/executor digest store so run eviction also releases proofs. */
|
|
297
|
+
readonly delegatedResultCorrelation?: DelegatedResultCorrelation;
|
|
279
298
|
readonly eventLog: EventLog;
|
|
280
299
|
/** Host-owned sink for asynchronous lifecycle failures that cannot be returned to a caller. Defaults to `console.error`. */
|
|
281
300
|
readonly onInternalError?: (context: RunLifecycleInternalErrorContext) => void;
|
|
@@ -319,5 +338,5 @@ export interface RunLifecycleInternalErrorContext {
|
|
|
319
338
|
* @complexity Per-call complexities documented on each method; the registry itself is a `Map` keyed by `runId` (O(1) lookup). Memory is bounded: non-terminal runs are O(n) in concurrently-live runs, and terminal runs are capped at `maxTerminalRuns` (each additionally bounded in retention time by `terminalRetentionMs`) rather than growing for the life of the process.
|
|
320
339
|
* @overallScore 100/100
|
|
321
340
|
*/
|
|
322
|
-
export declare function createRunLifecycle(requiredArgs: Pick<CreateRunLifecycleInput, "eventLog">, optionalArgs?: Pick<CreateRunLifecycleInput, "onInternalError" | "terminalRetentionMs" | "maxTerminalRuns" | "slowRunThresholdMs" | "dropEventsOnEviction">): RunLifecycle;
|
|
341
|
+
export declare function createRunLifecycle(requiredArgs: Pick<CreateRunLifecycleInput, "eventLog">, optionalArgs?: Pick<CreateRunLifecycleInput, "onInternalError" | "terminalRetentionMs" | "maxTerminalRuns" | "slowRunThresholdMs" | "dropEventsOnEviction" | "delegatedResultCorrelation">): RunLifecycle;
|
|
323
342
|
//# sourceMappingURL=run-lifecycle.d.ts.map
|
package/dist/run-lifecycle.js
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `RunLifecycle` — start/stream/cancel/resume a run keyed on an opaque
|
|
3
|
-
* `contextRef` (extraction-plan §2.1: runs key on an opaque `contextRef`,
|
|
4
|
-
* never a product-record identifier), generalized from OD's `design.runs` service
|
|
5
|
-
* (`apps/daemon/src/runtimes/runs.ts` + the terminal-decision half of
|
|
6
|
-
* `apps/daemon/src/runtimes/start-chat-run.ts`, `arch/server-startserver-endgame`
|
|
7
|
-
* branch — see `archived provenance ledger` for exact line citations).
|
|
8
|
-
*
|
|
9
|
-
* Scope boundary: this module owns the run *state machine* and its event
|
|
10
|
-
* log — create, transition, emit, replay, cancellation-intent propagation,
|
|
11
|
-
* idempotent start/finish, resume. It does not spawn or signal a subprocess;
|
|
12
|
-
* that is `@jini-ai/agent-runtime`'s job (extraction-plan task 7). A driver
|
|
13
|
-
* (today: this package's tests; later: agent-runtime) calls `emit()` for
|
|
14
|
-
* agent/stdout/stderr/error events, observes cancellation via
|
|
15
|
-
* `onCancelRequested`, and calls `finish()` once it knows the real outcome.
|
|
16
|
-
*/
|
|
17
1
|
import { randomUUID } from 'node:crypto';
|
|
18
2
|
import { RUN_PROTOCOL_VERSION, isTerminalRunState } from '@jini-ai/protocol';
|
|
19
3
|
import { createInactivityWatchdog } from './close-status.js';
|
|
@@ -367,6 +351,7 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
367
351
|
}
|
|
368
352
|
catch (error) {
|
|
369
353
|
runs.delete(runId);
|
|
354
|
+
input.delegatedResultCorrelation?.clearRun({ runId });
|
|
370
355
|
clearIdempotencyIndexEntryIfMatching({ idempotencyIndex: idempotencyIndex, idempotencyKey: startInput.idempotencyKey, runId: runId });
|
|
371
356
|
throw error;
|
|
372
357
|
}
|
|
@@ -418,6 +403,7 @@ export function createRunLifecycle(requiredArgs, optionalArgs = {}) {
|
|
|
418
403
|
return;
|
|
419
404
|
record.retentionTimer = undefined;
|
|
420
405
|
runs.delete(runId);
|
|
406
|
+
input.delegatedResultCorrelation?.clearRun({ runId });
|
|
421
407
|
clearIdempotencyIndexEntryIfMatching({ idempotencyIndex: idempotencyIndex, idempotencyKey: record.idempotencyKey, runId: runId });
|
|
422
408
|
if (input.dropEventsOnEviction === true)
|
|
423
409
|
void dropEvictedRunEvents(runId);
|
package/dist/tool-executor.js
CHANGED
|
@@ -212,11 +212,12 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
212
212
|
const error = guidance ? `${message}\n\n${guidance}` : message;
|
|
213
213
|
return { executionId, status: 'failed', error, errorKind, ...(normalizationNotes.length ? { normalizationNotes } : {}) };
|
|
214
214
|
}
|
|
215
|
-
/** Throws the typed unknown-tool input error, naming up to eight
|
|
216
|
-
|
|
215
|
+
/** Throws the typed unknown-tool input error, naming up to eight similarity-ranked registrations.
|
|
216
|
+
* Input is used only for a diagnostic resource hint; this never dispatches an alternate tool. */
|
|
217
|
+
function assertKnownTool({ registry: invocationRegistry, toolId, input }) {
|
|
217
218
|
if (invocationRegistry.has({ toolId }))
|
|
218
219
|
return;
|
|
219
|
-
const candidates = toolCandidates({ toolId, tools: invocationRegistry.list({}) });
|
|
220
|
+
const candidates = toolCandidates({ toolId, tools: invocationRegistry.list({}), input });
|
|
220
221
|
const hint = candidates.length > 0 ? `; did you mean ${candidates.join(', ')}?` : '';
|
|
221
222
|
throw new ToolInputError({ message: `ToolExecutor: unknown tool "${toolId}"${hint}` });
|
|
222
223
|
}
|
|
@@ -395,7 +396,7 @@ export function createToolExecutor(requiredArgs, optionalArgs = {}) {
|
|
|
395
396
|
// policy that hangs or throws used to still leave a record behind. `has()` only confirms
|
|
396
397
|
// existence, never returns a handler, so this doesn't reopen the leak authorizeToolInvocation
|
|
397
398
|
// closed — that gate still runs, still unconditionally, inside the call below.
|
|
398
|
-
assertKnownTool({ registry: invocationRegistry, toolId });
|
|
399
|
+
assertKnownTool({ registry: invocationRegistry, toolId, input });
|
|
399
400
|
const executionId = openAudit(principal, run, toolId);
|
|
400
401
|
// Observes `signal` for this execution's ENTIRE remaining lifetime, not just the handler-run
|
|
401
402
|
// phase. `requestConfirmation()`'s wait below is unbounded — it settles only when a human
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Audit writers must store exact chunks in blobs and retain resolvable references separately.
|
|
2
|
+
* This port deliberately cannot masquerade as the inline RunByteJournal contract. */
|
|
3
|
+
export interface StdoutJournalBlobPort {
|
|
4
|
+
/** Buffer chunks remain bytes, including UTF-8 characters split at OS read boundaries. */
|
|
5
|
+
record(input: {
|
|
6
|
+
runId: string;
|
|
7
|
+
chunk: string | Uint8Array;
|
|
8
|
+
}, options?: Record<string, never>): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
/** Projects one complete diagnostic frame; ordinary non-JSON output remains byte-identical. */
|
|
11
|
+
export declare function projectDiagnosticFrame({ line }: {
|
|
12
|
+
line: string;
|
|
13
|
+
}, { maxFrameChars }?: {
|
|
14
|
+
maxFrameChars?: number;
|
|
15
|
+
}): string;
|
|
16
|
+
/** Bounded JSONL framing: split chunks are held until complete, tails flush at close.
|
|
17
|
+
* Oversized frames produce an explicit notice and are discarded through their newline.
|
|
18
|
+
* @complexity O(n) total input characters; O(maxFrameChars) retained carry, no per-chunk joins. */
|
|
19
|
+
export declare function createToolResultDiagnosticProjection(_input: Record<string, never>, { maxFrameChars }?: {
|
|
20
|
+
maxFrameChars?: number;
|
|
21
|
+
}): {
|
|
22
|
+
feed({ chunk }: {
|
|
23
|
+
chunk: string;
|
|
24
|
+
}, _options?: {}): string;
|
|
25
|
+
flush(_input: Record<string, never>, _options?: {}): string;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=tool-result-diagnostics.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
function imageSummary(value) {
|
|
2
|
+
if (value.type !== 'image')
|
|
3
|
+
return undefined;
|
|
4
|
+
if (typeof value.mimeType === 'string' && typeof value.data === 'string') {
|
|
5
|
+
return { ...value, data: `[image ${value.mimeType}; ${value.data.length} base64 characters]` };
|
|
6
|
+
}
|
|
7
|
+
const source = value.source;
|
|
8
|
+
if (typeof source !== 'object' || source === null)
|
|
9
|
+
return undefined;
|
|
10
|
+
const image = source;
|
|
11
|
+
if (image.type !== 'base64' || typeof image.data !== 'string' || typeof image.media_type !== 'string')
|
|
12
|
+
return undefined;
|
|
13
|
+
return { ...value, source: { ...image, data: `[image ${image.media_type}; ${image.data.length} base64 characters]` } };
|
|
14
|
+
}
|
|
15
|
+
/** Redacts only recognized image fields, including JSON-encoded tool-result content.
|
|
16
|
+
* @complexity O(n) frame bytes, bounded by the framing limit before parsing. */
|
|
17
|
+
function summarizeImages(_key, value) {
|
|
18
|
+
if (_key === 'content' && typeof value === 'string')
|
|
19
|
+
return projectDiagnosticFrame({ line: value });
|
|
20
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
21
|
+
return value;
|
|
22
|
+
return imageSummary(value) ?? value;
|
|
23
|
+
}
|
|
24
|
+
/** Projects one complete diagnostic frame; ordinary non-JSON output remains byte-identical. */
|
|
25
|
+
export function projectDiagnosticFrame({ line }, { maxFrameChars = 16 * 1024 * 1024 } = {}) {
|
|
26
|
+
if (line.length > maxFrameChars)
|
|
27
|
+
return '[diagnostic frame exceeded image projection limit]';
|
|
28
|
+
try {
|
|
29
|
+
return JSON.stringify(JSON.parse(line), summarizeImages);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return line;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Bounded JSONL framing: split chunks are held until complete, tails flush at close.
|
|
36
|
+
* Oversized frames produce an explicit notice and are discarded through their newline.
|
|
37
|
+
* @complexity O(n) total input characters; O(maxFrameChars) retained carry, no per-chunk joins. */
|
|
38
|
+
export function createToolResultDiagnosticProjection(_input, { maxFrameChars = 16 * 1024 * 1024 } = {}) {
|
|
39
|
+
if (!Number.isSafeInteger(maxFrameChars) || maxFrameChars < 1)
|
|
40
|
+
throw new Error('diagnostic frame limit must be a positive integer');
|
|
41
|
+
let parts = [];
|
|
42
|
+
let size = 0;
|
|
43
|
+
let dropping = false;
|
|
44
|
+
function append(part) {
|
|
45
|
+
if (dropping || part.length === 0)
|
|
46
|
+
return;
|
|
47
|
+
size += part.length;
|
|
48
|
+
if (size > maxFrameChars) {
|
|
49
|
+
parts = [];
|
|
50
|
+
dropping = true;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
parts.push(part);
|
|
54
|
+
}
|
|
55
|
+
function finish() {
|
|
56
|
+
const line = dropping ? '[diagnostic frame exceeded image projection limit]' : projectDiagnosticFrame({ line: parts.join('') }, { maxFrameChars });
|
|
57
|
+
parts = [];
|
|
58
|
+
size = 0;
|
|
59
|
+
dropping = false;
|
|
60
|
+
return line;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
feed({ chunk }, _options = {}) {
|
|
64
|
+
const completed = [];
|
|
65
|
+
let start = 0;
|
|
66
|
+
for (let end = chunk.indexOf('\n'); end !== -1; end = chunk.indexOf('\n', start)) {
|
|
67
|
+
append(chunk.slice(start, end));
|
|
68
|
+
completed.push(finish(), '\n');
|
|
69
|
+
start = end + 1;
|
|
70
|
+
}
|
|
71
|
+
append(chunk.slice(start));
|
|
72
|
+
return completed.join('');
|
|
73
|
+
},
|
|
74
|
+
flush(_input, _options = {}) { return size > 0 || dropping ? finish() : ''; },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=tool-result-diagnostics.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolExecutionResult } from './tool-executor.js';
|
|
1
2
|
/**
|
|
2
3
|
* Extracts UI-renderable media blocks from a tool handler's raw MCP-style content envelope
|
|
3
4
|
* (`{ content: [...] }`), for `delegated-tool-bridge.ts` and `agent-executor.ts` to attach to the
|
|
@@ -11,18 +12,9 @@
|
|
|
11
12
|
* and is deliberately narrower: it recognizes exactly one block type today (`image`), and has no
|
|
12
13
|
* withhold-from-model concept of its own.
|
|
13
14
|
*
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* blocks from `content` before the surfaces pass ran, because `splitToolResultSurfaces`'s whitelist
|
|
18
|
-
* used to recognize only `text`, and an image block left in place would have been swept into
|
|
19
|
-
* `surfaces` and withheld from the model. Now that `image` is itself one of
|
|
20
|
-
* `tool-result-surfaces.ts`'s `MODEL_VISIBLE_BLOCK_TYPES` (a deliberate addition — see that
|
|
21
|
-
* constant's own doc), the surfaces pass classifies an image block correctly on its own, so there is
|
|
22
|
-
* no misclassification left to prevent by removing it first. This module's `remainder` field is
|
|
23
|
-
* therefore no longer read by that caller at all (kept for this module's own tests and any future
|
|
24
|
-
* caller that wants a de-imaged copy); only `media` is used, to populate the `tool_result` event's
|
|
25
|
-
* sibling `media` field for a UI that wants to render inline without re-parsing `content`.
|
|
15
|
+
* The surface whitelist runs on the original output first. Its model-visible result still
|
|
16
|
+
* contains images for MCP/model delivery. Only the event projection below serializes this
|
|
17
|
+
* module's de-imaged remainder, alongside one `media` array for rendering.
|
|
26
18
|
*
|
|
27
19
|
* ## Why only `image`, and why blocks are silently skipped rather than reported
|
|
28
20
|
*
|
|
@@ -67,8 +59,7 @@ export interface ExtractResultMedia {
|
|
|
67
59
|
}
|
|
68
60
|
/**
|
|
69
61
|
* Splits a tool handler's raw return value into its recognized media blocks and the remainder,
|
|
70
|
-
*
|
|
71
|
-
* matters).
|
|
62
|
+
* without modifying the model-facing output. Run the surface whitelist on the original first.
|
|
72
63
|
*
|
|
73
64
|
* @param output - A tool handler's raw return value (`ToolExecutionResult.output` when
|
|
74
65
|
* `status === 'completed'`). Non-envelope values (the overwhelming majority of tools, which return
|
|
@@ -78,4 +69,26 @@ export interface ExtractResultMedia {
|
|
|
78
69
|
export declare function extractResultMedia({ output }: {
|
|
79
70
|
readonly output: unknown;
|
|
80
71
|
}): ExtractResultMedia;
|
|
72
|
+
/** Converts arbitrary registered-tool output into the string-bearing run protocol. */
|
|
73
|
+
export declare function serializeDelegatedToolOutput({ output }: {
|
|
74
|
+
readonly output: unknown;
|
|
75
|
+
}): string;
|
|
76
|
+
/**
|
|
77
|
+
* Maps a `ToolExecutionResult`'s status to the string a delegated-tool caller (this bridge, or
|
|
78
|
+
* gap 3's stdin-tool-result injector in `agent-executor.ts`) reports back as the tool's visible
|
|
79
|
+
* output/failure reason. Exported so both real callers share one mapping rather than drifting.
|
|
80
|
+
*/
|
|
81
|
+
export declare function resultContent({ result }: {
|
|
82
|
+
readonly result: ToolExecutionResult;
|
|
83
|
+
}): string;
|
|
84
|
+
/** Projects a model-safe result into the saved event: pixels occur only in `media`.
|
|
85
|
+
* Uses the existing serializer and status mapping; callers keep the original for the model.
|
|
86
|
+
* @complexity O(n) output bytes, with O(n) transient serialization space. */
|
|
87
|
+
export declare function projectToolResultEvent({ result }: {
|
|
88
|
+
result: ToolExecutionResult;
|
|
89
|
+
}, _options?: {}): {
|
|
90
|
+
content: string;
|
|
91
|
+
isError?: true;
|
|
92
|
+
media?: readonly ToolResultMediaBlock[];
|
|
93
|
+
};
|
|
81
94
|
//# sourceMappingURL=tool-result-media.d.ts.map
|
|
@@ -1,39 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Extracts UI-renderable media blocks from a tool handler's raw MCP-style content envelope
|
|
3
|
-
* (`{ content: [...] }`), for `delegated-tool-bridge.ts` and `agent-executor.ts` to attach to the
|
|
4
|
-
* `tool_result` wire event's new `media` field (`@jini-ai/protocol`'s `events.ts`).
|
|
5
|
-
*
|
|
6
|
-
* ## Why this is not `tool-result-surfaces.ts`
|
|
7
|
-
*
|
|
8
|
-
* That module's whitelist governs a SECURITY property: which blocks may reach the model versus
|
|
9
|
-
* which are withheld for a human-only UI resource (MCP-UI, still deferred). This module answers a
|
|
10
|
-
* different question — "does this block carry displayable media a chat pane can render inline" —
|
|
11
|
-
* and is deliberately narrower: it recognizes exactly one block type today (`image`), and has no
|
|
12
|
-
* withhold-from-model concept of its own.
|
|
13
|
-
*
|
|
14
|
-
* The two DO interact, though not by pipelining: `delegated-tool-bridge.ts`'s `execute()` calls both
|
|
15
|
-
* of them against the SAME raw `executed.output`, not this module's `remainder` into
|
|
16
|
-
* `splitToolResultSurfaces`. That used to be pipelined — this module removed recognized image
|
|
17
|
-
* blocks from `content` before the surfaces pass ran, because `splitToolResultSurfaces`'s whitelist
|
|
18
|
-
* used to recognize only `text`, and an image block left in place would have been swept into
|
|
19
|
-
* `surfaces` and withheld from the model. Now that `image` is itself one of
|
|
20
|
-
* `tool-result-surfaces.ts`'s `MODEL_VISIBLE_BLOCK_TYPES` (a deliberate addition — see that
|
|
21
|
-
* constant's own doc), the surfaces pass classifies an image block correctly on its own, so there is
|
|
22
|
-
* no misclassification left to prevent by removing it first. This module's `remainder` field is
|
|
23
|
-
* therefore no longer read by that caller at all (kept for this module's own tests and any future
|
|
24
|
-
* caller that wants a de-imaged copy); only `media` is used, to populate the `tool_result` event's
|
|
25
|
-
* sibling `media` field for a UI that wants to render inline without re-parsing `content`.
|
|
26
|
-
*
|
|
27
|
-
* ## Why only `image`, and why blocks are silently skipped rather than reported
|
|
28
|
-
*
|
|
29
|
-
* `text` blocks already reach the model (and the UI, via `content`) through the existing flattened
|
|
30
|
-
* string — duplicating them into `media` would be redundant, not additive, so this module leaves
|
|
31
|
-
* them in `remainder` and never collects them. A block claiming `type: 'image'` without a string
|
|
32
|
-
* `mimeType`/`data` is dropped rather than surfaced as a malformed-block error: a handler's return
|
|
33
|
-
* value is untrusted input to this module (it may come from an external MCP server), and the
|
|
34
|
-
* contract is "render what is well-formed, ignore what isn't" — the same fail-quiet posture
|
|
35
|
-
* `serializeDelegatedToolOutput` already takes on a non-serializable output.
|
|
36
|
-
*/
|
|
1
|
+
import { batchOutcomeFor } from '@jini-ai/core';
|
|
37
2
|
function isRecord(value) {
|
|
38
3
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
39
4
|
}
|
|
@@ -50,8 +15,7 @@ function asImageBlock(block) {
|
|
|
50
15
|
}
|
|
51
16
|
/**
|
|
52
17
|
* Splits a tool handler's raw return value into its recognized media blocks and the remainder,
|
|
53
|
-
*
|
|
54
|
-
* matters).
|
|
18
|
+
* without modifying the model-facing output. Run the surface whitelist on the original first.
|
|
55
19
|
*
|
|
56
20
|
* @param output - A tool handler's raw return value (`ToolExecutionResult.output` when
|
|
57
21
|
* `status === 'completed'`). Non-envelope values (the overwhelming majority of tools, which return
|
|
@@ -77,4 +41,53 @@ export function extractResultMedia({ output }) {
|
|
|
77
41
|
return { remainder: output, media: [] };
|
|
78
42
|
return { remainder: { ...output, content: kept }, media };
|
|
79
43
|
}
|
|
44
|
+
/** Converts arbitrary registered-tool output into the string-bearing run protocol. */
|
|
45
|
+
export function serializeDelegatedToolOutput({ output }) {
|
|
46
|
+
if (typeof output === 'string')
|
|
47
|
+
return output;
|
|
48
|
+
if (output === undefined)
|
|
49
|
+
return '';
|
|
50
|
+
try {
|
|
51
|
+
const serialized = JSON.stringify(output);
|
|
52
|
+
return serialized === undefined ? String(output) : serialized;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return String(output);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Maps a `ToolExecutionResult`'s status to the string a delegated-tool caller (this bridge, or
|
|
60
|
+
* gap 3's stdin-tool-result injector in `agent-executor.ts`) reports back as the tool's visible
|
|
61
|
+
* output/failure reason. Exported so both real callers share one mapping rather than drifting.
|
|
62
|
+
*/
|
|
63
|
+
export function resultContent({ result }) {
|
|
64
|
+
switch (result.status) {
|
|
65
|
+
case 'completed':
|
|
66
|
+
return serializeDelegatedToolOutput({ output: result.output });
|
|
67
|
+
case 'denied':
|
|
68
|
+
return 'Tool execution denied by policy.';
|
|
69
|
+
case 'confirmation-denied':
|
|
70
|
+
return 'Tool execution denied during confirmation.';
|
|
71
|
+
case 'timed-out':
|
|
72
|
+
return 'Tool execution timed out.';
|
|
73
|
+
case 'cancelled':
|
|
74
|
+
return 'Tool execution cancelled.';
|
|
75
|
+
case 'failed':
|
|
76
|
+
return result.error ?? 'Tool execution failed.';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Projects a model-safe result into the saved event: pixels occur only in `media`.
|
|
80
|
+
* Uses the existing serializer and status mapping; callers keep the original for the model.
|
|
81
|
+
* @complexity O(n) output bytes, with O(n) transient serialization space. */
|
|
82
|
+
export function projectToolResultEvent({ result }, _options = {}) {
|
|
83
|
+
const { remainder, media } = extractResultMedia({ output: result.output });
|
|
84
|
+
const isError = result.status !== 'completed'
|
|
85
|
+
|| (batchOutcomeFor({ output: result.output }) ?? 'completed') !== 'completed'
|
|
86
|
+
|| (isRecord(result.output) && result.output.isError === true);
|
|
87
|
+
return {
|
|
88
|
+
content: resultContent({ result: { ...result, output: remainder } }),
|
|
89
|
+
...(isError ? { isError: true } : {}),
|
|
90
|
+
...(media.length > 0 ? { media } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
80
93
|
//# sourceMappingURL=tool-result-media.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/daemon",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
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,29 +139,29 @@
|
|
|
139
139
|
"@types/pg": "8.23.1",
|
|
140
140
|
"express": "^4.21.0",
|
|
141
141
|
"@types/express": "^4.17.21",
|
|
142
|
-
"@jini-ai/db": "0.
|
|
143
|
-
"@jini-ai/
|
|
144
|
-
"@jini-ai/
|
|
145
|
-
"@jini-ai/
|
|
146
|
-
"@jini-ai/
|
|
147
|
-
"@jini-ai/
|
|
148
|
-
"@jini-ai/
|
|
142
|
+
"@jini-ai/db": "0.6.11",
|
|
143
|
+
"@jini-ai/core": "^0.6.11",
|
|
144
|
+
"@jini-ai/platform": "^0.6.11",
|
|
145
|
+
"@jini-ai/agent-runtime": "^0.6.11",
|
|
146
|
+
"@jini-ai/chat": "0.6.11",
|
|
147
|
+
"@jini-ai/protocol": "^0.6.11",
|
|
148
|
+
"@jini-ai/http-kit": "0.6.11"
|
|
149
149
|
},
|
|
150
150
|
"dependencies": {
|
|
151
|
-
"@jini-ai/
|
|
152
|
-
"@jini-ai/
|
|
151
|
+
"@jini-ai/protocol": "^0.6.11",
|
|
152
|
+
"@jini-ai/core": "^0.6.11"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
155
|
"node-pty": "^1.1.0",
|
|
156
|
-
"@jini-ai/db": "^0.
|
|
156
|
+
"@jini-ai/db": "^0.6.10",
|
|
157
157
|
"kysely": "^0.29.6",
|
|
158
158
|
"better-sqlite3": "^13.0.0",
|
|
159
159
|
"@electric-sql/pglite": "0.5.8",
|
|
160
160
|
"pg": "^8.23.0",
|
|
161
|
-
"@jini-ai/http-kit": "^0.
|
|
161
|
+
"@jini-ai/http-kit": "^0.6.10",
|
|
162
162
|
"express": "^4.21.0 || ^5.0.0",
|
|
163
|
-
"@jini-ai/agent-runtime": "^0.
|
|
164
|
-
"@jini-ai/platform": "^0.
|
|
163
|
+
"@jini-ai/agent-runtime": "^0.6.10",
|
|
164
|
+
"@jini-ai/platform": "^0.6.10"
|
|
165
165
|
},
|
|
166
166
|
"peerDependenciesMeta": {
|
|
167
167
|
"node-pty": {
|