@jini-ai/daemon 0.6.8 → 0.6.10

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 (79) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
  3. package/dist/agent-executor/antigravity-permissions.js +64 -0
  4. package/dist/agent-executor/contracts.d.ts +37 -6
  5. package/dist/agent-executor/event-translation.d.ts +6 -1
  6. package/dist/agent-executor/event-translation.js +3 -1
  7. package/dist/agent-executor/events.d.ts +5 -0
  8. package/dist/agent-executor/events.js +143 -87
  9. package/dist/agent-executor/launch.d.ts +5 -3
  10. package/dist/agent-executor/launch.js +315 -217
  11. package/dist/agent-executor/mcp-bridge-probe.js +51 -35
  12. package/dist/agent-executor/mcp-bridge.d.ts +18 -8
  13. package/dist/agent-executor/mcp-bridge.js +105 -65
  14. package/dist/agent-executor/run-output.d.ts +20 -0
  15. package/dist/agent-executor/run-output.js +80 -0
  16. package/dist/attachment-content.js +18 -15
  17. package/dist/core/read-only-tools.d.ts +15 -0
  18. package/dist/core/read-only-tools.js +29 -1
  19. package/dist/core/tool-audit.d.ts +3 -1
  20. package/dist/core/tool-audit.js +35 -22
  21. package/dist/core/tool-gaps.d.ts +60 -0
  22. package/dist/core/tool-gaps.js +199 -0
  23. package/dist/delegated-result-correlation.d.ts +26 -0
  24. package/dist/delegated-result-correlation.js +68 -0
  25. package/dist/delegated-tool-bridge.d.ts +6 -14
  26. package/dist/delegated-tool-bridge.js +155 -155
  27. package/dist/event-log.d.ts +45 -2
  28. package/dist/event-log.js +83 -4
  29. package/dist/http/attachments.d.ts +26 -1
  30. package/dist/http/attachments.js +129 -65
  31. package/dist/http/connectors.d.ts +1 -32
  32. package/dist/http/connectors.js +13 -68
  33. package/dist/http/delegated-tools.d.ts +8 -1
  34. package/dist/http/delegated-tools.js +30 -30
  35. package/dist/http/host-tools.d.ts +1 -0
  36. package/dist/http/host-tools.js +64 -59
  37. package/dist/http/index.d.ts +2 -2
  38. package/dist/http/index.js +1 -1
  39. package/dist/http/media.js +48 -69
  40. package/dist/http/memory.js +45 -36
  41. package/dist/http/model-proxy-fields.d.ts +23 -0
  42. package/dist/http/model-proxy-fields.js +29 -0
  43. package/dist/http/model-proxy.js +193 -395
  44. package/dist/http/research-tavily.adapter.js +72 -49
  45. package/dist/http/research.js +33 -23
  46. package/dist/http/resource-request.d.ts +37 -0
  47. package/dist/http/resource-request.js +59 -0
  48. package/dist/http/routines.js +49 -90
  49. package/dist/http/runs.d.ts +7 -5
  50. package/dist/http/runs.js +21 -7
  51. package/dist/http/terminals.js +23 -48
  52. package/dist/http/tool-catalog.d.ts +5 -0
  53. package/dist/http/xai.d.ts +11 -1
  54. package/dist/http/xai.js +125 -124
  55. package/dist/index.d.ts +2 -0
  56. package/dist/index.js +1 -0
  57. package/dist/legacy-data-migration.js +41 -24
  58. package/dist/run/core/retry.js +51 -35
  59. package/dist/run/diagnostics/diagnostics.js +5 -17
  60. package/dist/run-credentials.js +49 -27
  61. package/dist/run-lifecycle.d.ts +31 -2
  62. package/dist/run-lifecycle.js +49 -42
  63. package/dist/store/agent-sessions/sqlite.d.ts +6 -0
  64. package/dist/store/agent-sessions/sqlite.js +6 -4
  65. package/dist/surface-exchanges.js +2 -1
  66. package/dist/terminal-session.d.ts +1 -1
  67. package/dist/terminal-session.js +26 -33
  68. package/dist/tool-audit.d.ts +1 -0
  69. package/dist/tool-audit.js +1 -0
  70. package/dist/tool-executor.d.ts +24 -3
  71. package/dist/tool-executor.js +276 -139
  72. package/dist/tool-recovery/recovery.js +64 -35
  73. package/dist/tool-recovery/stack.d.ts +2 -1
  74. package/dist/tool-recovery/stack.js +1 -1
  75. package/dist/tool-result-diagnostics.d.ts +27 -0
  76. package/dist/tool-result-diagnostics.js +77 -0
  77. package/dist/tool-result-media.d.ts +27 -14
  78. package/dist/tool-result-media.js +51 -38
  79. package/package.json +14 -14
@@ -261,26 +261,37 @@ function planRemedyCall(input) {
261
261
  return undefined;
262
262
  return { remedyToolId, carryForward, askFor };
263
263
  }
264
+ /** Prefill keys that name a secret (or a carrier of one) — never accepted as card metadata. */
265
+ const SECRET_SHAPED_PREFILL_KEY = /token|secret|password|authorization|env|headers|body|key/i;
266
+ /** Whether one prefill entry is plain card metadata: a key the setup schema declares that does not
267
+ * name a secret, holding a JSON primitive, and — for a string — a value neither the secret detector
268
+ * nor the credential-free assertion rejects. */
269
+ function isMetadataPrefillEntry({ key, value, properties, deps }) {
270
+ if (!(key in properties) || SECRET_SHAPED_PREFILL_KEY.test(key))
271
+ return false;
272
+ if (!isJsonPrimitive(value))
273
+ return false;
274
+ if (typeof value !== "string")
275
+ return true;
276
+ if (deps.containsSecret({ text: value }))
277
+ return false;
278
+ try {
279
+ deps.assertCredentialFreeField({ value, field: key }, {});
280
+ }
281
+ catch {
282
+ return false;
283
+ }
284
+ return true;
285
+ }
264
286
  /** Card schemas accept metadata only. Reject secret-shaped prefills before even asking. */
265
287
  function planCredentialSetup({ diagnostic, descriptor, deps }, _optional = {}) {
266
288
  const shape = resolveRemedySchemaShape(descriptor);
267
289
  if (!shape || diagnostic.setupToolId !== diagnostic.remedyToolId || !diagnostic.prefill)
268
290
  return undefined;
269
291
  const prefill = diagnostic.prefill;
270
- for (const [key, value] of Object.entries(prefill)) {
271
- if (!(key in shape.properties) || /token|secret|password|authorization|env|headers|body|key/i.test(key))
272
- return undefined;
273
- if (!isJsonPrimitive(value) || (typeof value === "string" && deps.containsSecret({ text: value })))
274
- return undefined;
275
- if (typeof value === "string") {
276
- try {
277
- deps.assertCredentialFreeField({ value, field: key }, {});
278
- }
279
- catch {
280
- return undefined;
281
- }
282
- }
283
- }
292
+ const allMetadata = Object.entries(prefill).every(([key, value]) => isMetadataPrefillEntry({ key, value, properties: shape.properties, deps }));
293
+ if (!allMetadata)
294
+ return undefined;
284
295
  if (shape.required.some(key => !(key in prefill)))
285
296
  return undefined;
286
297
  return { remedyToolId: diagnostic.remedyToolId, carryForward: prefill };
@@ -309,7 +320,8 @@ async function resolveRecoveryDecision(exchange, ui, askForKey) {
309
320
  return { proceed: true, answerValue };
310
321
  }
311
322
  function resolveRecoveryGate(input) {
312
- const { diagnostic, principal, originalInput, registry, result, deps } = input;
323
+ const { diagnostic, principal, originalInput, result, deps } = input;
324
+ const { registry } = deps;
313
325
  const readOnlyRefusal = refuseNonReadOnlyDispatch({ principal, toolId: diagnostic.remedyToolId, registry, messages: deps.readOnlyMessages }, {});
314
326
  if (readOnlyRefusal !== null)
315
327
  return { attempt: false, outcome: { ...result, error: deps.formatReadOnlyRemedyRefusal({ refusal: readOnlyRefusal }) } };
@@ -346,6 +358,36 @@ function buildRemedyInput(plan, decision) {
346
358
  remedyInput[plan.askFor.key] = decision.answerValue;
347
359
  return remedyInput;
348
360
  }
361
+ /**
362
+ * The remedy tool's input for an accepted gate, or `undefined` when the human did not give a usable
363
+ * go-ahead. A credential-setup diagnostic skips the recovery surface entirely: it goes directly to the
364
+ * masked card, which supplies its own human decision. Every other diagnostic asks first through the
365
+ * host-built recovery form.
366
+ */
367
+ async function resolveRemedyInput(input) {
368
+ const { plan, diagnostic, principal, emitSurface, signal, deps } = input;
369
+ // Credential setup goes directly to the masked card; it supplies its own human decision.
370
+ if (diagnostic.setupToolId)
371
+ return { ...plan.carryForward };
372
+ const exchange = deps.surfaceExchanges.open({ binding: { toolId: deps.recoveryToolId, principalId: principal.id }, emit: emitSurface });
373
+ const ui = deps.buildRecoveryForm({ exchangeId: exchange.id, hint: diagnostic.hint, remedyToolId: diagnostic.remedyToolId, ...(plan.askFor ? { askFor: plan.askFor } : {}) }, {});
374
+ const decision = await collectRecoveryDecision({ exchange, ui, askForKey: plan.askFor?.key, signal });
375
+ // Declined, dismissed, expired, or a blank answer — the ORIGINAL failure is still the truth.
376
+ if (!decision.proceed)
377
+ return undefined;
378
+ return buildRemedyInput(plan, decision);
379
+ }
380
+ /** Whether the remedy call actually applied its fix — see the two cases in the comments below. */
381
+ function remedyApplied(remedyResult) {
382
+ // The fix itself did not complete (denied, threw, timed out, ...) — nothing changed, so retrying
383
+ // the original would only reproduce the same failure.
384
+ if (remedyResult.status !== "completed")
385
+ return false;
386
+ // The remedy RAN, but its own result says the fix was never actually applied (a declined,
387
+ // expired, or invalid human-gated write) — see {@link remedyReportedFailure}. "Completed" is not
388
+ // "succeeded"; retrying here would only reproduce the same failure.
389
+ return !remedyReportedFailure(remedyResult.output);
390
+ }
349
391
  /**
350
392
  * Wraps `inner` with the generic ask -> apply -> retry-once recovery loop described in this file's
351
393
  * header. A drop-in `ToolExecutor` — every method other than `execute` delegates straight through,
@@ -378,34 +420,21 @@ export function withToolFailureRecovery({ inner, ...deps }, _optional = {}) {
378
420
  // `withReadOnlyToolConstraint` would refuse the dispatch below regardless — it is the
379
421
  // enforcement, and it does not depend on this check — but a refusal discovered THERE arrives
380
422
  // only after a human has already been asked to fill in a form whose answer is then thrown away.
381
- const gate = resolveRecoveryGate({ diagnostic, principal, originalInput: input, registry: deps.registry, result, deps });
423
+ const gate = resolveRecoveryGate({ diagnostic, principal, originalInput: input, result, deps });
382
424
  if (!gate.attempt)
383
425
  return gate.outcome;
384
- let remedyInput = { ...gate.plan.carryForward };
385
- if (!diagnostic.setupToolId) {
386
- const exchange = deps.surfaceExchanges.open({ binding: { toolId: deps.recoveryToolId, principalId: principal.id }, emit: emitSurface });
387
- const ui = deps.buildRecoveryForm({ exchangeId: exchange.id, hint: diagnostic.hint, remedyToolId: diagnostic.remedyToolId, ...(gate.plan.askFor ? { askFor: gate.plan.askFor } : {}) }, {});
388
- const decision = await collectRecoveryDecision({ exchange, ui, askForKey: gate.plan.askFor?.key, signal });
389
- // Declined, dismissed, expired, or a blank answer — the ORIGINAL failure is still the truth.
390
- if (!decision.proceed)
391
- return result;
392
- remedyInput = buildRemedyInput(gate.plan, decision);
393
- }
394
- // Credential setup goes directly to the masked card; it supplies its own human decision.
426
+ const remedyInput = await resolveRemedyInput({ plan: gate.plan, diagnostic, principal, emitSurface, signal, deps });
427
+ // Declined, dismissed, expired, or a blank answer — the ORIGINAL failure is still the truth.
428
+ if (!remedyInput)
429
+ return result;
395
430
  // ---- STRUCTURAL ONE-CYCLE GUARD ----
396
431
  // Both calls below go straight to `inner` — never back through this function or the object it
397
432
  // returns. Whatever either one returns, including a fresh hint+remedyToolId of its own, is never
398
433
  // fed back into the scan above: that scan is written exactly once, above this comment, and
399
434
  // nothing here loops or recurses into it again. See this file's header.
400
435
  const remedyResult = await inner.execute({ ...required, toolId: diagnostic.remedyToolId, input: remedyInput }, optional);
401
- // The fix itself did not complete (denied, threw, timed out, ...) — nothing changed, so retrying
402
- // the original would only reproduce the same failure. Return the ORIGINAL result, untouched.
403
- if (remedyResult.status !== "completed")
404
- return result;
405
- // The remedy RAN, but its own result says the fix was never actually applied (a declined,
406
- // expired, or invalid human-gated write) — see {@link remedyReportedFailure}. "Completed" is not
407
- // "succeeded"; retrying here would only reproduce the same failure.
408
- if (remedyReportedFailure(remedyResult.output))
436
+ // The fix was not applied (see `remedyApplied`) — return the ORIGINAL result, untouched.
437
+ if (!remedyApplied(remedyResult))
409
438
  return result;
410
439
  // Retry the ORIGINAL call exactly once, with its exact original input. Whatever this returns —
411
440
  // success or a fresh failure — is this wrapper's final answer.
@@ -41,7 +41,7 @@
41
41
  * Architectural role: `src/assistant` composition-layer adapter. Takes its collaborators as
42
42
  * arguments and constructs no policy, sink, or registry of its own.
43
43
  */
44
- import { type ToolExecutor } from "../tool-executor.js";
44
+ import { type ToolExecutor, type PrepareToolGuidance } from "../tool-executor.js";
45
45
  import type { ToolRegistry } from "@jini-ai/core";
46
46
  import type { ToolAttemptAuditSink } from "../core/tool-audit.js";
47
47
  import type { SurfaceExchangeStore } from "../surface-exchanges.js";
@@ -49,6 +49,7 @@ import { type ToolFailureRecoveryDeps } from "./recovery.js";
49
49
  import { type RedactedToolFailuresDeps } from "./redaction.js";
50
50
  export interface AssistantToolExecutorDeps extends Omit<ToolFailureRecoveryDeps, "registry"> {
51
51
  readonly registry: ToolRegistry;
52
+ readonly prepareGuidance?: PrepareToolGuidance;
52
53
  readonly surfaceExchanges: SurfaceExchangeStore;
53
54
  readonly newExecutionId: () => string;
54
55
  /** Optional sink is a real supported configuration; never require a dummy observer. */
@@ -51,7 +51,7 @@ import { withRedactedToolFailures, readToolErrorId } from "./redaction.js";
51
51
  * @complexity O(1) composition; each decorator owns its runtime cost.
52
52
  */
53
53
  export function createAssistantToolExecutor(deps, _optional = {}) {
54
- const guarded = withReadOnlyToolConstraint({ inner: createToolExecutor({ registry: deps.registry }),
54
+ const guarded = withReadOnlyToolConstraint({ inner: createToolExecutor({ registry: deps.registry }, { ...(deps.prepareGuidance ? { prepareGuidance: deps.prepareGuidance } : {}) }),
55
55
  registry: deps.registry, idGenerator: { newId: deps.newExecutionId }, messages: deps.readOnlyMessages }, {});
56
56
  const redacted = withRedactedToolFailures({ inner: guarded, ...deps.toolFailures }, {});
57
57
  const audited = deps.toolAttemptAudit
@@ -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.8",
3
+ "version": "0.6.10",
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.5",
143
- "@jini-ai/http-kit": "0.4.2",
144
- "@jini-ai/agent-runtime": "^0.4.11",
145
- "@jini-ai/chat": "0.6.6",
146
- "@jini-ai/platform": "^0.5.9",
147
- "@jini-ai/core": "^0.4.7",
148
- "@jini-ai/protocol": "^0.4.4"
142
+ "@jini-ai/http-kit": "0.6.10",
143
+ "@jini-ai/db": "0.6.10",
144
+ "@jini-ai/chat": "0.6.10",
145
+ "@jini-ai/core": "^0.6.10",
146
+ "@jini-ai/agent-runtime": "^0.6.10",
147
+ "@jini-ai/protocol": "^0.6.10",
148
+ "@jini-ai/platform": "^0.6.10"
149
149
  },
150
150
  "dependencies": {
151
- "@jini-ai/core": "^0.4.7",
152
- "@jini-ai/protocol": "^0.4.4"
151
+ "@jini-ai/protocol": "^0.6.10",
152
+ "@jini-ai/core": "^0.6.10"
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": {