@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.
- package/CHANGELOG.md +148 -0
- package/dist/agent-executor/antigravity-permissions.d.ts +11 -0
- package/dist/agent-executor/antigravity-permissions.js +64 -0
- package/dist/agent-executor/contracts.d.ts +37 -6
- package/dist/agent-executor/event-translation.d.ts +6 -1
- package/dist/agent-executor/event-translation.js +3 -1
- package/dist/agent-executor/events.d.ts +5 -0
- package/dist/agent-executor/events.js +143 -87
- package/dist/agent-executor/launch.d.ts +5 -3
- package/dist/agent-executor/launch.js +315 -217
- package/dist/agent-executor/mcp-bridge-probe.js +51 -35
- package/dist/agent-executor/mcp-bridge.d.ts +18 -8
- package/dist/agent-executor/mcp-bridge.js +105 -65
- package/dist/agent-executor/run-output.d.ts +20 -0
- package/dist/agent-executor/run-output.js +80 -0
- package/dist/attachment-content.js +18 -15
- package/dist/core/read-only-tools.d.ts +15 -0
- package/dist/core/read-only-tools.js +29 -1
- package/dist/core/tool-audit.d.ts +3 -1
- package/dist/core/tool-audit.js +35 -22
- package/dist/core/tool-gaps.d.ts +60 -0
- package/dist/core/tool-gaps.js +199 -0
- package/dist/delegated-result-correlation.d.ts +26 -0
- package/dist/delegated-result-correlation.js +68 -0
- package/dist/delegated-tool-bridge.d.ts +6 -14
- package/dist/delegated-tool-bridge.js +155 -155
- package/dist/event-log.d.ts +45 -2
- package/dist/event-log.js +83 -4
- package/dist/http/attachments.d.ts +26 -1
- package/dist/http/attachments.js +129 -65
- package/dist/http/connectors.d.ts +1 -32
- package/dist/http/connectors.js +13 -68
- package/dist/http/delegated-tools.d.ts +8 -1
- package/dist/http/delegated-tools.js +30 -30
- package/dist/http/host-tools.d.ts +1 -0
- package/dist/http/host-tools.js +64 -59
- package/dist/http/index.d.ts +2 -2
- package/dist/http/index.js +1 -1
- package/dist/http/media.js +48 -69
- package/dist/http/memory.js +45 -36
- package/dist/http/model-proxy-fields.d.ts +23 -0
- package/dist/http/model-proxy-fields.js +29 -0
- package/dist/http/model-proxy.js +193 -395
- package/dist/http/research-tavily.adapter.js +72 -49
- package/dist/http/research.js +33 -23
- package/dist/http/resource-request.d.ts +37 -0
- package/dist/http/resource-request.js +59 -0
- package/dist/http/routines.js +49 -90
- package/dist/http/runs.d.ts +7 -5
- package/dist/http/runs.js +21 -7
- package/dist/http/terminals.js +23 -48
- package/dist/http/tool-catalog.d.ts +5 -0
- package/dist/http/xai.d.ts +11 -1
- package/dist/http/xai.js +125 -124
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/legacy-data-migration.js +41 -24
- package/dist/run/core/retry.js +51 -35
- package/dist/run/diagnostics/diagnostics.js +5 -17
- package/dist/run-credentials.js +49 -27
- package/dist/run-lifecycle.d.ts +31 -2
- package/dist/run-lifecycle.js +49 -42
- package/dist/store/agent-sessions/sqlite.d.ts +6 -0
- package/dist/store/agent-sessions/sqlite.js +6 -4
- package/dist/surface-exchanges.js +2 -1
- package/dist/terminal-session.d.ts +1 -1
- package/dist/terminal-session.js +26 -33
- package/dist/tool-audit.d.ts +1 -0
- package/dist/tool-audit.js +1 -0
- package/dist/tool-executor.d.ts +24 -3
- package/dist/tool-executor.js +276 -139
- package/dist/tool-recovery/recovery.js +64 -35
- package/dist/tool-recovery/stack.d.ts +2 -1
- package/dist/tool-recovery/stack.js +1 -1
- 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
|
@@ -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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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,
|
|
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,
|
|
423
|
+
const gate = resolveRecoveryGate({ diagnostic, principal, originalInput: input, result, deps });
|
|
382
424
|
if (!gate.attempt)
|
|
383
425
|
return gate.outcome;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
402
|
-
|
|
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
|
|
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.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/
|
|
143
|
-
"@jini-ai/
|
|
144
|
-
"@jini-ai/
|
|
145
|
-
"@jini-ai/
|
|
146
|
-
"@jini-ai/
|
|
147
|
-
"@jini-ai/
|
|
148
|
-
"@jini-ai/
|
|
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/
|
|
152
|
-
"@jini-ai/
|
|
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.
|
|
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": {
|