@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/agent-executor/contracts.d.ts +34 -11
  3. package/dist/agent-executor/event-translation.js +2 -0
  4. package/dist/agent-executor/events.d.ts +7 -15
  5. package/dist/agent-executor/events.js +142 -194
  6. package/dist/agent-executor/launch.d.ts +9 -3
  7. package/dist/agent-executor/launch.js +104 -8
  8. package/dist/agent-executor/live-session-state.d.ts +23 -0
  9. package/dist/agent-executor/live-session-state.js +155 -0
  10. package/dist/agent-executor/mcp-bridge.d.ts +3 -1
  11. package/dist/agent-executor/mcp-bridge.js +9 -2
  12. package/dist/agent-executor/message-relay.d.ts +39 -0
  13. package/dist/agent-executor/message-relay.js +56 -0
  14. package/dist/core/read-only-tools.d.ts +9 -4
  15. package/dist/core/read-only-tools.js +10 -7
  16. package/dist/core/tool-candidates.d.ts +8 -3
  17. package/dist/core/tool-candidates.js +39 -4
  18. package/dist/delegated-result-correlation.d.ts +26 -0
  19. package/dist/delegated-result-correlation.js +68 -0
  20. package/dist/delegated-tool-bridge.d.ts +5 -13
  21. package/dist/delegated-tool-bridge.js +19 -58
  22. package/dist/http/attachments.d.ts +13 -0
  23. package/dist/http/attachments.js +27 -1
  24. package/dist/http/delegated-tools.d.ts +3 -0
  25. package/dist/http/delegated-tools.js +2 -2
  26. package/dist/http/index.d.ts +2 -1
  27. package/dist/http/index.js +3 -1
  28. package/dist/http/message-relay.d.ts +21 -0
  29. package/dist/http/message-relay.js +44 -0
  30. package/dist/http/runs.d.ts +5 -2
  31. package/dist/http/runs.js +11 -5
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +1 -0
  34. package/dist/run-lifecycle.d.ts +20 -1
  35. package/dist/run-lifecycle.js +2 -16
  36. package/dist/tool-executor.js +5 -4
  37. package/dist/tool-result-diagnostics.d.ts +27 -0
  38. package/dist/tool-result-diagnostics.js +77 -0
  39. package/dist/tool-result-media.d.ts +27 -14
  40. package/dist/tool-result-media.js +51 -38
  41. 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 whether to interrupt the run and send the
158
- * text as the next turn, or simply send it next because the run is already ending.
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 delivery = deliver ? await deliver(input) : 'unsupported';
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) => channel.enqueue({ event }) }, { afterCursor });
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
@@ -93,4 +93,5 @@ 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';
96
97
  //# sourceMappingURL=index.js.map
@@ -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
@@ -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);
@@ -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 registered prefix matches. */
216
- function assertKnownTool({ registry: invocationRegistry, toolId }) {
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 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`.
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
- * ready to feed onward into `splitToolResultSurfaces` (see this module's doc for why that order
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
- * ready to feed onward into `splitToolResultSurfaces` (see this module's doc for why that order
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.9",
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.3.6",
143
- "@jini-ai/http-kit": "0.4.3",
144
- "@jini-ai/agent-runtime": "^0.4.12",
145
- "@jini-ai/core": "^0.4.9",
146
- "@jini-ai/protocol": "^0.4.5",
147
- "@jini-ai/platform": "^0.5.10",
148
- "@jini-ai/chat": "0.6.7"
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/core": "^0.4.9",
152
- "@jini-ai/protocol": "^0.4.5"
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.3.2",
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.4.0",
161
+ "@jini-ai/http-kit": "^0.6.10",
162
162
  "express": "^4.21.0 || ^5.0.0",
163
- "@jini-ai/agent-runtime": "^0.4.10",
164
- "@jini-ai/platform": "^0.5.5"
163
+ "@jini-ai/agent-runtime": "^0.6.10",
164
+ "@jini-ai/platform": "^0.6.10"
165
165
  },
166
166
  "peerDependenciesMeta": {
167
167
  "node-pty": {