@jini-ai/daemon 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +157 -0
- package/dist/agent-executor/contracts.d.ts +34 -11
- package/dist/agent-executor/event-translation.js +2 -0
- package/dist/agent-executor/events.d.ts +7 -15
- package/dist/agent-executor/events.js +142 -194
- package/dist/agent-executor/launch.d.ts +9 -3
- package/dist/agent-executor/launch.js +104 -8
- package/dist/agent-executor/live-session-state.d.ts +23 -0
- package/dist/agent-executor/live-session-state.js +155 -0
- package/dist/agent-executor/mcp-bridge.d.ts +3 -1
- package/dist/agent-executor/mcp-bridge.js +9 -2
- package/dist/agent-executor/message-relay.d.ts +39 -0
- package/dist/agent-executor/message-relay.js +56 -0
- package/dist/core/read-only-tools.d.ts +9 -4
- package/dist/core/read-only-tools.js +10 -7
- package/dist/core/tool-candidates.d.ts +8 -3
- package/dist/core/tool-candidates.js +39 -4
- package/dist/delegated-result-correlation.d.ts +26 -0
- package/dist/delegated-result-correlation.js +68 -0
- package/dist/delegated-tool-bridge.d.ts +5 -13
- package/dist/delegated-tool-bridge.js +19 -58
- package/dist/http/attachments.d.ts +13 -0
- package/dist/http/attachments.js +27 -1
- package/dist/http/delegated-tools.d.ts +3 -0
- package/dist/http/delegated-tools.js +2 -2
- package/dist/http/index.d.ts +2 -1
- package/dist/http/index.js +3 -1
- package/dist/http/message-relay.d.ts +21 -0
- package/dist/http/message-relay.js +44 -0
- package/dist/http/runs.d.ts +5 -2
- package/dist/http/runs.js +11 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/run-lifecycle.d.ts +20 -1
- package/dist/run-lifecycle.js +2 -16
- package/dist/tool-executor.js +5 -4
- package/dist/tool-result-diagnostics.d.ts +27 -0
- package/dist/tool-result-diagnostics.js +77 -0
- package/dist/tool-result-media.d.ts +27 -14
- package/dist/tool-result-media.js +51 -38
- package/package.json +14 -14
|
@@ -60,14 +60,15 @@ export function readOnlyToolRefusalMessage({ toolId, messages }, _optional = {})
|
|
|
60
60
|
/**
|
|
61
61
|
* Single read-only registration decision, shared by routes and inner dispatch.
|
|
62
62
|
* @returns Null for an explicitly read-only registration, otherwise host refusal text.
|
|
63
|
+
* @complexity O(n) for known tools; unknown-tool diagnostics add bounded name scoring and O(n log n) sorting.
|
|
63
64
|
*/
|
|
64
|
-
export function checkReadOnlyTool({ toolId, registry, messages }, _optional = {}) {
|
|
65
|
+
export function checkReadOnlyTool({ toolId, input, registry, messages }, _optional = {}) {
|
|
65
66
|
if (registry === undefined)
|
|
66
67
|
return messages.unverifiableMessage;
|
|
67
68
|
const tools = registry.list({});
|
|
68
69
|
const descriptor = tools.find((candidate) => candidate.id === toolId);
|
|
69
70
|
if (!descriptor)
|
|
70
|
-
return messages.unknownToolMessage({ toolId, candidates: toolCandidates({ toolId, tools }) });
|
|
71
|
+
return messages.unknownToolMessage({ toolId, candidates: toolCandidates({ toolId, tools, input }) });
|
|
71
72
|
if (isReadOnlyTool({ descriptor }))
|
|
72
73
|
return null;
|
|
73
74
|
return readOnlyToolRefusalMessage({ toolId, messages });
|
|
@@ -86,8 +87,9 @@ export function checkReadOnlyTool({ toolId, registry, messages }, _optional = {}
|
|
|
86
87
|
* @param required.registry - Descriptors to resolve `toolId` against; `undefined` ⇒ refuse (an
|
|
87
88
|
* unverifiable constrained dispatch is refused, never waived, matching `checkReadOnlyTool`).
|
|
88
89
|
* @returns `null` when the dispatch may proceed, otherwise the refusal text to report.
|
|
89
|
-
* @complexity O(1) for an unconstrained principal; O(n) in registered tool count for a
|
|
90
|
-
*
|
|
90
|
+
* @complexity O(1) for an unconstrained principal; O(n) in registered tool count for a known tool,
|
|
91
|
+
* since `ToolRegistry` exposes enumeration rather than lookup by id. Unknown ids also incur
|
|
92
|
+
* the bounded similarity scoring and sorting cost documented by `toolCandidates`.
|
|
91
93
|
*/
|
|
92
94
|
export function refuseNonReadOnlyDispatch(required, _optional = {}) {
|
|
93
95
|
if (!principalIsReadOnlyConstrained({ principal: required.principal }))
|
|
@@ -127,13 +129,14 @@ export function readOnlyRemedyRefusalMessage({ refusal, formatMessage }, _option
|
|
|
127
129
|
*
|
|
128
130
|
* @returns A drop-in `ToolExecutor`; `resumeConfirmation`/`cancel`/`getAuditRecord` delegate
|
|
129
131
|
* straight through.
|
|
130
|
-
* @complexity One extra property read per unconstrained call; one registry scan per constrained one
|
|
132
|
+
* @complexity One extra property read per unconstrained call; one registry scan per constrained one,
|
|
133
|
+
* plus bounded similarity scoring and sorting for an unknown id.
|
|
131
134
|
*/
|
|
132
135
|
export function withReadOnlyToolConstraint({ inner, registry, idGenerator, messages }, _optional = {}) {
|
|
133
136
|
return {
|
|
134
137
|
execute: async (requiredArgs, optionalArgs = {}) => {
|
|
135
|
-
const { principal, toolId } = requiredArgs;
|
|
136
|
-
const refusal = refuseNonReadOnlyDispatch({ principal, toolId, registry, messages });
|
|
138
|
+
const { principal, toolId, input } = requiredArgs;
|
|
139
|
+
const refusal = refuseNonReadOnlyDispatch({ principal, toolId, input, registry, messages });
|
|
137
140
|
// Deny before inner: no handler or kernel audit record exists. Use the authorization result
|
|
138
141
|
// union instead of throwing, and mint a denial ID because there is no inner execution ID.
|
|
139
142
|
if (refusal !== null)
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { ToolDescriptor } from '@jini-ai/core';
|
|
2
|
-
/** Registered prefix
|
|
3
|
-
*
|
|
4
|
-
|
|
2
|
+
/** Registered prefix/near-name suggestions, closest first and capped at eight. Equal scores keep
|
|
3
|
+
* catalog order; an exact content_read.<resource> takes priority even beyond the old cutoff.
|
|
4
|
+
* Suggestions grant no execution authority; the caller must retry through the existing gates.
|
|
5
|
+
* @example toolCandidates({ toolId: 'content_read', input: { resource: 'theme' }, tools });
|
|
6
|
+
* @complexity O(n*l*r + n log n) time, O(n + r) space; scoring lengths capped at 128.
|
|
7
|
+
*/
|
|
8
|
+
export declare function toolCandidates({ toolId, tools, input }: {
|
|
5
9
|
toolId: string;
|
|
6
10
|
tools: readonly Pick<ToolDescriptor, 'id'>[];
|
|
11
|
+
input?: unknown;
|
|
7
12
|
}, _optional?: Record<string, never>): string[];
|
|
8
13
|
//# sourceMappingURL=tool-candidates.d.ts.map
|
|
@@ -1,6 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/** Bounded Levenshtein distance for diagnostic ranking; oversized names retain prefix discovery.
|
|
2
|
+
* @returns Edit count, or Infinity above the 128-character scoring limit (no effects).
|
|
3
|
+
* @complexity O(l*r) time and O(r) space, with both name lengths capped at 128.
|
|
4
|
+
*/
|
|
5
|
+
function nameDistance({ left, right }, _options = {}) {
|
|
6
|
+
if (left.length > 128 || right.length > 128)
|
|
7
|
+
return Infinity;
|
|
8
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
9
|
+
for (let row = 1; row <= left.length; row++) {
|
|
10
|
+
const current = [row];
|
|
11
|
+
for (let column = 1; column <= right.length; column++) {
|
|
12
|
+
current[column] = Math.min(current[column - 1] + 1, previous[column] + 1, previous[column - 1] + Number(left[row - 1] !== right[column - 1]));
|
|
13
|
+
}
|
|
14
|
+
previous = current;
|
|
15
|
+
}
|
|
16
|
+
return previous[right.length];
|
|
17
|
+
}
|
|
18
|
+
/** Use content_read's explicit resource selector only as a diagnostic hint, never a dispatch.
|
|
19
|
+
* @returns The target spelling to rank against; malformed selectors use the original tool id.
|
|
20
|
+
* @complexity O(r) to join an r-character selector; no mutation or effects. Name scoring is bounded separately.
|
|
21
|
+
*/
|
|
22
|
+
function candidateTarget({ toolId, input }, _options = {}) {
|
|
23
|
+
if (toolId !== 'content_read' || typeof input !== 'object' || input === null || Array.isArray(input))
|
|
24
|
+
return toolId;
|
|
25
|
+
const resource = input.resource;
|
|
26
|
+
return typeof resource === 'string' && resource.length > 0 ? `${toolId}.${resource}` : toolId;
|
|
27
|
+
}
|
|
28
|
+
/** Registered prefix/near-name suggestions, closest first and capped at eight. Equal scores keep
|
|
29
|
+
* catalog order; an exact content_read.<resource> takes priority even beyond the old cutoff.
|
|
30
|
+
* Suggestions grant no execution authority; the caller must retry through the existing gates.
|
|
31
|
+
* @example toolCandidates({ toolId: 'content_read', input: { resource: 'theme' }, tools });
|
|
32
|
+
* @complexity O(n*l*r + n log n) time, O(n + r) space; scoring lengths capped at 128.
|
|
33
|
+
*/
|
|
34
|
+
export function toolCandidates({ toolId, tools, input }, _optional = {}) {
|
|
35
|
+
const target = candidateTarget({ toolId, input });
|
|
36
|
+
return tools.map(({ id }) => ({ id, distance: nameDistance({ left: target, right: id }) }))
|
|
37
|
+
.filter(({ id, distance }) => id === target || id.startsWith(toolId) || distance <= 2)
|
|
38
|
+
.sort((left, right) => Number(right.id === target) - Number(left.id === target) || left.distance - right.distance)
|
|
39
|
+
.slice(0, 8).map(({ id }) => id);
|
|
5
40
|
}
|
|
6
41
|
//# sourceMappingURL=tool-candidates.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CredentialCryptoPort } from './run-credentials.js';
|
|
2
|
+
interface ResultIdentity {
|
|
3
|
+
readonly runId: string;
|
|
4
|
+
readonly toolUseId: string;
|
|
5
|
+
readonly content: readonly unknown[];
|
|
6
|
+
readonly isError: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Run-scoped proof of a saved bridge result. Implementations retain digests, never pixels. */
|
|
9
|
+
export interface DelegatedResultCorrelation {
|
|
10
|
+
record(input: ResultIdentity, options?: Record<string, never>): void;
|
|
11
|
+
matches(input: ResultIdentity, options?: Record<string, never>): boolean;
|
|
12
|
+
clearRun(input: {
|
|
13
|
+
runId: string;
|
|
14
|
+
}, options?: Record<string, never>): void;
|
|
15
|
+
}
|
|
16
|
+
/** Bounded digest store. Eviction only disables summarization, so legacy output remains safe.
|
|
17
|
+
* Share this instance with bridge/executor/lifecycle; lifecycle eviction clears each run.
|
|
18
|
+
* @complexity O(n) hashing plus JSON key sorting; O(r*c) retained fixed-size digests. */
|
|
19
|
+
export declare function createDelegatedResultCorrelation({ crypto }: {
|
|
20
|
+
crypto: Pick<CredentialCryptoPort, 'digest'>;
|
|
21
|
+
}, { maxRuns, maxCallsPerRun }?: {
|
|
22
|
+
maxRuns?: number;
|
|
23
|
+
maxCallsPerRun?: number;
|
|
24
|
+
}): DelegatedResultCorrelation;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=delegated-result-correlation.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Accept only the complete base64 source shape; extra fields must remain part of the digest. */
|
|
2
|
+
function base64ImageSource({ source }, _options = {}) {
|
|
3
|
+
if (typeof source !== 'object' || source === null)
|
|
4
|
+
return undefined;
|
|
5
|
+
const image = source;
|
|
6
|
+
if (image.type !== 'base64' || typeof image.media_type !== 'string' || typeof image.data !== 'string' || Object.keys(image).length !== 3)
|
|
7
|
+
return undefined;
|
|
8
|
+
return { data: image.data, media_type: image.media_type };
|
|
9
|
+
}
|
|
10
|
+
/** Convert only source-only images; dual representations and extra source fields stay in the proof. */
|
|
11
|
+
function normalizeSourceImage({ record }, _options = {}) {
|
|
12
|
+
if (record.type !== 'image' || 'data' in record || 'mimeType' in record)
|
|
13
|
+
return record;
|
|
14
|
+
const image = base64ImageSource({ source: record.source });
|
|
15
|
+
if (!image)
|
|
16
|
+
return record;
|
|
17
|
+
const { source: _source, ...rest } = record;
|
|
18
|
+
return { ...rest, data: image.data, mimeType: image.media_type };
|
|
19
|
+
}
|
|
20
|
+
/** Normalize unambiguous CLI images and JSON object key order.
|
|
21
|
+
* @complexity O(n + k log k) for n bytes and k keys per object; inputs are wire JSON. */
|
|
22
|
+
function normalizedJson(_key, value) {
|
|
23
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
24
|
+
return value;
|
|
25
|
+
const record = normalizeSourceImage({ record: value });
|
|
26
|
+
return Object.fromEntries(Object.keys(record).sort().map(key => [key, record[key]]));
|
|
27
|
+
}
|
|
28
|
+
/** Bounded digest store. Eviction only disables summarization, so legacy output remains safe.
|
|
29
|
+
* Share this instance with bridge/executor/lifecycle; lifecycle eviction clears each run.
|
|
30
|
+
* @complexity O(n) hashing plus JSON key sorting; O(r*c) retained fixed-size digests. */
|
|
31
|
+
export function createDelegatedResultCorrelation({ crypto }, { maxRuns = 256, maxCallsPerRun = 2048 } = {}) {
|
|
32
|
+
if (!Number.isSafeInteger(maxRuns) || maxRuns < 1 || !Number.isSafeInteger(maxCallsPerRun) || maxCallsPerRun < 1)
|
|
33
|
+
throw new Error('correlation limits must be positive integers');
|
|
34
|
+
const runs = new Map();
|
|
35
|
+
// Optional correlation must not turn an otherwise serializable legacy fallback into a failure
|
|
36
|
+
// (for example, a cyclic in-process tool result handled by serializeDelegatedToolOutput).
|
|
37
|
+
const digest = (input) => {
|
|
38
|
+
try {
|
|
39
|
+
return crypto.digest({ token: JSON.stringify({ isError: input.isError, content: input.content }, normalizedJson) });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
record(input) {
|
|
47
|
+
const fingerprint = digest(input);
|
|
48
|
+
if (fingerprint === undefined)
|
|
49
|
+
return;
|
|
50
|
+
let calls = runs.get(input.runId);
|
|
51
|
+
if (!calls) {
|
|
52
|
+
if (runs.size >= maxRuns)
|
|
53
|
+
runs.delete(runs.keys().next().value);
|
|
54
|
+
calls = new Map();
|
|
55
|
+
runs.set(input.runId, calls);
|
|
56
|
+
}
|
|
57
|
+
if (calls.size >= maxCallsPerRun)
|
|
58
|
+
calls.delete(calls.keys().next().value);
|
|
59
|
+
calls.set(input.toolUseId, fingerprint);
|
|
60
|
+
},
|
|
61
|
+
matches(input) {
|
|
62
|
+
const expected = runs.get(input.runId)?.get(input.toolUseId);
|
|
63
|
+
return expected !== undefined && expected === digest(input);
|
|
64
|
+
},
|
|
65
|
+
clearRun({ runId }) { runs.delete(runId); },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=delegated-result-correlation.js.map
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type Principal } from '@jini-ai/core';
|
|
10
10
|
import type { RunLifecycle } from './run-lifecycle.js';
|
|
11
|
+
import type { DelegatedResultCorrelation } from './delegated-result-correlation.js';
|
|
11
12
|
import type { ToolExecutionResult, ToolExecutor } from './tool-executor.js';
|
|
12
13
|
/**
|
|
13
14
|
* Run-protocol `type` for a human-only surface withheld from a tool result.
|
|
@@ -42,24 +43,15 @@ export interface DelegatedToolBridge {
|
|
|
42
43
|
export interface CreateDelegatedToolBridgeOptions {
|
|
43
44
|
readonly lifecycle: RunLifecycle;
|
|
44
45
|
readonly toolExecutor: ToolExecutor;
|
|
46
|
+
/** Share with the executor and lifecycle only after the host negotiates marker v1. */
|
|
47
|
+
readonly delegatedResultCorrelation?: DelegatedResultCorrelation;
|
|
45
48
|
}
|
|
46
|
-
|
|
47
|
-
export declare function serializeDelegatedToolOutput({ output }: {
|
|
48
|
-
readonly output: unknown;
|
|
49
|
-
}): string;
|
|
50
|
-
/**
|
|
51
|
-
* Maps a `ToolExecutionResult`'s status to the string a delegated-tool caller (this bridge, or
|
|
52
|
-
* gap 3's stdin-tool-result injector in `agent-executor.ts`) reports back as the tool's visible
|
|
53
|
-
* output/failure reason. Exported so both real callers share one mapping rather than drifting.
|
|
54
|
-
*/
|
|
55
|
-
export declare function resultContent({ result }: {
|
|
56
|
-
readonly result: ToolExecutionResult;
|
|
57
|
-
}): string;
|
|
49
|
+
export { serializeDelegatedToolOutput, resultContent } from './tool-result-media.js';
|
|
58
50
|
/**
|
|
59
51
|
* Creates the transport-neutral bridge used by future ACP-delegate, MCP, or
|
|
60
52
|
* other host protocols. This module does not invent a server transport: a
|
|
61
53
|
* concrete protocol calls this bridge after it has decoded and authenticated a
|
|
62
54
|
* delegated tool request.
|
|
63
55
|
*/
|
|
64
|
-
export declare function createDelegatedToolBridge(
|
|
56
|
+
export declare function createDelegatedToolBridge(requiredArgs: Pick<CreateDelegatedToolBridgeOptions, 'lifecycle' | 'toolExecutor'>, optionalArgs?: Pick<CreateDelegatedToolBridgeOptions, 'delegatedResultCorrelation'>): DelegatedToolBridge;
|
|
65
57
|
//# sourceMappingURL=delegated-tool-bridge.d.ts.map
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* enforces Jini's registry policy, confirmation, timeout, cancellation, and
|
|
7
7
|
* audit trail before any registered handler runs.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import {} from '@jini-ai/core';
|
|
10
|
+
import { projectToolResultEvent } from './tool-result-media.js';
|
|
11
11
|
import { splitToolResultSurfaces } from './tool-result-surfaces.js';
|
|
12
12
|
/**
|
|
13
13
|
* Run-protocol `type` for a human-only surface withheld from a tool result.
|
|
@@ -38,41 +38,7 @@ function correlationFor(channel, toolUseId) {
|
|
|
38
38
|
function errorMessage(error) {
|
|
39
39
|
return error instanceof Error ? error.message : String(error);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
export function serializeDelegatedToolOutput({ output }) {
|
|
43
|
-
if (typeof output === 'string')
|
|
44
|
-
return output;
|
|
45
|
-
if (output === undefined)
|
|
46
|
-
return '';
|
|
47
|
-
try {
|
|
48
|
-
const serialized = JSON.stringify(output);
|
|
49
|
-
return serialized === undefined ? String(output) : serialized;
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return String(output);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Maps a `ToolExecutionResult`'s status to the string a delegated-tool caller (this bridge, or
|
|
57
|
-
* gap 3's stdin-tool-result injector in `agent-executor.ts`) reports back as the tool's visible
|
|
58
|
-
* output/failure reason. Exported so both real callers share one mapping rather than drifting.
|
|
59
|
-
*/
|
|
60
|
-
export function resultContent({ result }) {
|
|
61
|
-
switch (result.status) {
|
|
62
|
-
case 'completed':
|
|
63
|
-
return serializeDelegatedToolOutput({ output: result.output });
|
|
64
|
-
case 'denied':
|
|
65
|
-
return 'Tool execution denied by policy.';
|
|
66
|
-
case 'confirmation-denied':
|
|
67
|
-
return 'Tool execution denied during confirmation.';
|
|
68
|
-
case 'timed-out':
|
|
69
|
-
return 'Tool execution timed out.';
|
|
70
|
-
case 'cancelled':
|
|
71
|
-
return 'Tool execution cancelled.';
|
|
72
|
-
case 'failed':
|
|
73
|
-
return result.error ?? 'Tool execution failed.';
|
|
74
|
-
}
|
|
75
|
-
}
|
|
41
|
+
export { serializeDelegatedToolOutput, resultContent } from './tool-result-media.js';
|
|
76
42
|
/**
|
|
77
43
|
* Builds the abort controller handed to `ToolExecutor`, aborted by either a run cancellation or the
|
|
78
44
|
* invocation's own transport signal. `dispose()` detaches both listeners once the call settles.
|
|
@@ -168,21 +134,7 @@ function createSurfaceSeam({ lifecycle, runId, toolUseId }) {
|
|
|
168
134
|
return { emitSurface, settle, release };
|
|
169
135
|
}
|
|
170
136
|
/** Splits human-only surfaces out of an executed result, emits them, then the matching `tool_result`. */
|
|
171
|
-
async function emitExecutedResult({ lifecycle, runId, toolUseId }, { executed }) {
|
|
172
|
-
// Recognized media blocks (images) are collected here purely to populate the sibling
|
|
173
|
-
// `media` field the `tool_result` event carries below, for a UI that wants to render one
|
|
174
|
-
// inline without re-parsing `content` (`ToolCard`'s `<img>`, per `assistant_demo_image`'s
|
|
175
|
-
// own doc). This does NOT remove the image block from `executed.output` — `image` is one
|
|
176
|
-
// of `tool-result-surfaces.ts`'s own `MODEL_VISIBLE_BLOCK_TYPES`, so the split below already
|
|
177
|
-
// keeps it in `modelOutput` on its own. An image reaching the model IS the point
|
|
178
|
-
// (`assistant_demo_image`/`admin.capture_screenshot` exist to prove exactly that round
|
|
179
|
-
// trip) — unlike an `mcp-ui` resource, which is genuinely human-only and must never survive
|
|
180
|
-
// into `modelOutput`. (Earlier this extraction fed its own de-imaged `remainder` into the
|
|
181
|
-
// surfaces split below and discarded the image from `output` entirely, on the assumption
|
|
182
|
-
// that the sibling `media` field was the only channel a caller needed — true for a UI
|
|
183
|
-
// reading the run's event stream, false for `@jini-ai/mcp`'s `execute_delegated_tool`, which
|
|
184
|
-
// hands the bridge's RETURN VALUE to the model with no access to that event stream at all.)
|
|
185
|
-
const { media } = extractResultMedia({ output: executed.output });
|
|
137
|
+
async function emitExecutedResult({ lifecycle, runId, toolUseId, delegatedResultCorrelation }, { executed }) {
|
|
186
138
|
// THE MODEL/HUMAN FORK. A tool call's return value is definitionally what the model
|
|
187
139
|
// receives, so a UI resource left inside it is model-visible context no matter what any
|
|
188
140
|
// downstream layer does — `@jini-ai/mcp`'s `okResult()` JSON.stringifies the whole result
|
|
@@ -202,32 +154,41 @@ async function emitExecutedResult({ lifecycle, runId, toolUseId }, { executed })
|
|
|
202
154
|
data: { type: MCP_UI_EVENT_TYPE, toolUseId, resource: surface },
|
|
203
155
|
} });
|
|
204
156
|
}
|
|
157
|
+
// The return keeps model-visible images; only the stored event removes their duplicate.
|
|
158
|
+
const projection = projectToolResultEvent({ result });
|
|
205
159
|
await lifecycle.emit({ runId: runId, input: {
|
|
206
160
|
event: 'agent',
|
|
207
161
|
data: {
|
|
208
162
|
type: 'tool_result',
|
|
209
163
|
toolUseId,
|
|
210
|
-
|
|
211
|
-
...(result.status === 'completed' && (batchOutcomeFor({ output: result.output }) ?? 'completed') === 'completed' ? {} : { isError: true }),
|
|
212
|
-
...(media.length > 0 ? { media } : {}),
|
|
164
|
+
...projection,
|
|
213
165
|
},
|
|
214
166
|
} });
|
|
167
|
+
// Record only after the canonical result is saved; failed emission cannot hide an echo.
|
|
168
|
+
if (result.status === 'completed' && isContentEnvelope(result.output)) {
|
|
169
|
+
delegatedResultCorrelation?.record({ runId, toolUseId, content: result.output.content, isError: projection.isError === true });
|
|
170
|
+
}
|
|
215
171
|
return result;
|
|
216
172
|
}
|
|
173
|
+
function isContentEnvelope(output) {
|
|
174
|
+
return typeof output === 'object' && output !== null && 'content' in output && Array.isArray(output.content);
|
|
175
|
+
}
|
|
217
176
|
/**
|
|
218
177
|
* Creates the transport-neutral bridge used by future ACP-delegate, MCP, or
|
|
219
178
|
* other host protocols. This module does not invent a server transport: a
|
|
220
179
|
* concrete protocol calls this bridge after it has decoded and authenticated a
|
|
221
180
|
* delegated tool request.
|
|
222
181
|
*/
|
|
223
|
-
export function createDelegatedToolBridge(
|
|
224
|
-
const {
|
|
182
|
+
export function createDelegatedToolBridge(requiredArgs, optionalArgs = {}) {
|
|
183
|
+
const options = { ...requiredArgs, ...optionalArgs };
|
|
184
|
+
// A route may pass its broader deps as options; its wrapped executor remains authoritative.
|
|
185
|
+
const { lifecycle, toolExecutor } = requiredArgs;
|
|
225
186
|
async function execute(requiredArgs, optionalArgs = {}) {
|
|
226
187
|
// Merged rather than read from `optionalArgs` alone: a caller may hand `signal` in the first
|
|
227
188
|
// object (the delegated-tools HTTP route does), and it must still be honoured.
|
|
228
189
|
const invocation = { ...requiredArgs, ...optionalArgs };
|
|
229
190
|
const { runId, toolUseId, toolId, principal, input } = invocation;
|
|
230
|
-
const call = { lifecycle, runId, toolUseId };
|
|
191
|
+
const call = { lifecycle, runId, toolUseId, ...(options.delegatedResultCorrelation ? { delegatedResultCorrelation: options.delegatedResultCorrelation } : {}) };
|
|
231
192
|
await lifecycle.emit({ runId: runId, input: {
|
|
232
193
|
event: 'agent',
|
|
233
194
|
data: { type: 'tool_use', id: toolUseId, name: toolId, input },
|
|
@@ -33,6 +33,19 @@ export interface AttachmentClaim {
|
|
|
33
33
|
/** Absent only for an empty claim — there is no directory to grant when nothing was claimed. */
|
|
34
34
|
batchDirectory?: string;
|
|
35
35
|
}
|
|
36
|
+
/** Copy an already authenticated claim into the host's filesystem boundary. Keep the original
|
|
37
|
+
* store records intact for promotion/read-back; the caller owns cleanup after the run ends.
|
|
38
|
+
* Only claimed files are copied, never batch sidecars or another upload's capability ids.
|
|
39
|
+
* O(b) I/O for b claimed bytes, bounded by the attachment store's batch limits. Partial copies
|
|
40
|
+
* are removed before rejection. The temporary directory is private and directly under real root.
|
|
41
|
+
*/
|
|
42
|
+
export declare function stageAttachmentClaim(required: {
|
|
43
|
+
claim: AttachmentClaim;
|
|
44
|
+
filesystemRoot: string;
|
|
45
|
+
}, _optional?: {}): Promise<{
|
|
46
|
+
claim: AttachmentClaim;
|
|
47
|
+
cleanup: () => Promise<void>;
|
|
48
|
+
}>;
|
|
36
49
|
/** Why an attachment operation was refused. Maps to an HTTP status in `statusForRejection`. */
|
|
37
50
|
export type AttachmentRejectionReason =
|
|
38
51
|
/** The batch id is missing or not of the accepted shape. */
|
package/dist/http/attachments.js
CHANGED
|
@@ -99,13 +99,39 @@
|
|
|
99
99
|
* correlation-id-bearing generic `INTERNAL_ERROR`, with the real error reaching a host-owned sink —
|
|
100
100
|
* the same `reportInternalError` shape `media.ts`/`delegated-tools.ts` use.
|
|
101
101
|
*/
|
|
102
|
-
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
102
|
+
import { chmod, copyFile, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
103
103
|
import { randomUUID } from 'node:crypto';
|
|
104
104
|
import { basename, dirname, extname, resolve } from 'node:path';
|
|
105
105
|
import { createApiError } from '@jini-ai/protocol';
|
|
106
106
|
import { guardSameOrigin } from '@jini-ai/http-kit';
|
|
107
107
|
import { sendApiError, sendJson } from '@jini-ai/http-kit';
|
|
108
108
|
import { writeJsonFileAtomicAsync } from '@jini-ai/platform/fs';
|
|
109
|
+
/** Copy an already authenticated claim into the host's filesystem boundary. Keep the original
|
|
110
|
+
* store records intact for promotion/read-back; the caller owns cleanup after the run ends.
|
|
111
|
+
* Only claimed files are copied, never batch sidecars or another upload's capability ids.
|
|
112
|
+
* O(b) I/O for b claimed bytes, bounded by the attachment store's batch limits. Partial copies
|
|
113
|
+
* are removed before rejection. The temporary directory is private and directly under real root.
|
|
114
|
+
*/
|
|
115
|
+
export async function stageAttachmentClaim(required, _optional = {}) {
|
|
116
|
+
const root = await realpath(required.filesystemRoot);
|
|
117
|
+
const directory = await mkdtemp(resolve(root, '.agent-attachments-'));
|
|
118
|
+
const cleanup = () => rm(directory, { recursive: true, force: true });
|
|
119
|
+
try {
|
|
120
|
+
await chmod(directory, 0o700);
|
|
121
|
+
const attachments = [];
|
|
122
|
+
for (const attachment of required.claim.attachments) {
|
|
123
|
+
const target = resolve(directory, basename(attachment.path));
|
|
124
|
+
await copyFile(attachment.path, target);
|
|
125
|
+
await chmod(target, 0o600);
|
|
126
|
+
attachments.push({ ...attachment, path: target });
|
|
127
|
+
}
|
|
128
|
+
return { claim: { attachments, batchDirectory: directory }, cleanup };
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
await cleanup();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
109
135
|
/**
|
|
110
136
|
* A refusal this module classified itself, as opposed to an unexpected filesystem/programming
|
|
111
137
|
* error. The route pack turns the `reason` into a status code and lets the `message` through only
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DelegatedResultCorrelation } from '../delegated-result-correlation.js';
|
|
1
2
|
import type { Express } from 'express';
|
|
2
3
|
import { type Principal, type ToolRegistry } from '@jini-ai/core';
|
|
3
4
|
import { type RunLifecycle, type ToolExecutionResult, type ToolExecutor } from '../index.js';
|
|
@@ -49,6 +50,8 @@ export interface DelegatedToolsInternalErrorContext {
|
|
|
49
50
|
readonly status?: 'timed-out' | 'cancelled' | 'failed';
|
|
50
51
|
}
|
|
51
52
|
export interface DelegatedToolsHttpDeps {
|
|
53
|
+
/** The same negotiated digest store supplied to the run lifecycle and agent executor. */
|
|
54
|
+
readonly delegatedResultCorrelation?: DelegatedResultCorrelation;
|
|
52
55
|
readonly lifecycle: RunLifecycle;
|
|
53
56
|
readonly toolExecutor: ToolExecutor;
|
|
54
57
|
/** Host-owned: resolves the `Principal` a given delegated-tool-call request executes as. Mandatory — see module doc; there is no safe default identity this package could assume on a host's behalf. */
|
|
@@ -36,7 +36,7 @@ function checkReadOnlyConstraint(deps, input) {
|
|
|
36
36
|
if (input.requireReadOnly !== true)
|
|
37
37
|
return null;
|
|
38
38
|
const refusal = readOnlyTools.checkReadOnlyTool({
|
|
39
|
-
toolId: input.toolId, registry: deps.toolRegistry, messages: readOnlyTools.defaultDaemonMessages.readOnly,
|
|
39
|
+
toolId: input.toolId, input: input.input, registry: deps.toolRegistry, messages: readOnlyTools.defaultDaemonMessages.readOnly,
|
|
40
40
|
});
|
|
41
41
|
if (refusal === null)
|
|
42
42
|
return null;
|
|
@@ -203,7 +203,7 @@ export const delegatedToolExecuteRoute = defineJsonRoute({
|
|
|
203
203
|
const toolExecutor = input.requireReadOnly === true
|
|
204
204
|
? readOnlyTools.withReadOnlyToolConstraint({ inner: deps.toolExecutor, registry: deps.toolRegistry, messages: readOnlyTools.defaultDaemonMessages.readOnly, idGenerator: { newId: randomUUID } })
|
|
205
205
|
: deps.toolExecutor;
|
|
206
|
-
const bridge = createDelegatedToolBridge({ lifecycle: deps.lifecycle, toolExecutor });
|
|
206
|
+
const bridge = createDelegatedToolBridge({ lifecycle: deps.lifecycle, toolExecutor }, deps);
|
|
207
207
|
try {
|
|
208
208
|
// `signal` carries the caller's HTTP connection dropping — see `mountJsonRoute`
|
|
209
209
|
// (`adapter.ts`). The bridge already combines it with the run's own cancellation
|
package/dist/http/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Daemon route packs; generic transport primitives live in @jini-ai/http-kit. */
|
|
2
|
+
export { createMessageRelayClaimHandler, registerMessageRelayRoutes, type MessageRelayHttpDeps } from './message-relay.js';
|
|
2
3
|
export type { ResolveWorkspaceRootOptions, WorkspaceRootRequest, WorkspaceRootResolver, } from './workspace-root.js';
|
|
3
4
|
export { denyAllWorkspaceRoots, resolveWorkspaceRoot, WorkspaceRootDeniedError } from './workspace-root.js';
|
|
4
5
|
export type { RunCancellationService } from './cancel-owned-runs.js';
|
|
@@ -45,7 +46,7 @@ export { createResearchSearchRoute, registerResearchRoutes, researchSearchRoute
|
|
|
45
46
|
export type { MediaGenerateResponse, MediaHttpDeps, MediaInternalErrorContext, MediaTaskDeleteResponse, MediaTaskListResponse, MediaTaskResponse, } from './media.js';
|
|
46
47
|
export { mediaGenerateRoute, mediaTaskDeleteRoute, mediaTaskGetRoute, mediaTaskListRoute, registerMediaRoutes, } from './media.js';
|
|
47
48
|
export type { AttachmentClaim, AttachmentRejectionReason, AttachmentsHttpDeps, AttachmentsInternalErrorContext, AttachmentStore, AttachmentUploadResponse, CreateDiskAttachmentStoreOptions, ObservedAttachmentIdentity, PendingAttachmentSummary, RecordedAttachmentIdentity, StoredAttachment, } from './attachments.js';
|
|
48
|
-
export { ATTACHMENTS_ROUTE_PATH, AttachmentRejectedError, createDiskAttachmentStore, detectAttachmentKind, handleAttachmentCleanup, handleAttachmentUpload, isUnchangedAttachment, registerAttachmentRoutes, sanitizeAttachmentName, writeBoundedAttachmentBody, } from './attachments.js';
|
|
49
|
+
export { ATTACHMENTS_ROUTE_PATH, AttachmentRejectedError, createDiskAttachmentStore, detectAttachmentKind, handleAttachmentCleanup, handleAttachmentUpload, isUnchangedAttachment, registerAttachmentRoutes, sanitizeAttachmentName, stageAttachmentClaim, writeBoundedAttachmentBody, } from './attachments.js';
|
|
49
50
|
export type { XaiAuthStatusResponse, XaiHttpDeps, XaiInternalErrorContext, XaiOauthStartResponse, XaiOkResponse, XaiSearchResponse, } from './xai.js';
|
|
50
51
|
export { registerXaiRoutes, xaiAuthStatusRoute, xaiOauthCancelRoute, xaiOauthCompleteRoute, xaiOauthDisconnectRoute, xaiOauthStartRoute, xaiSearchRoute, } from './xai.js';
|
|
51
52
|
export * from './run-credentials.js';
|
package/dist/http/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** Daemon route packs; generic transport primitives live in @jini-ai/http-kit. */
|
|
2
|
+
export { createMessageRelayClaimHandler, registerMessageRelayRoutes } from './message-relay.js';
|
|
1
3
|
export { denyAllWorkspaceRoots, resolveWorkspaceRoot, WorkspaceRootDeniedError } from './workspace-root.js';
|
|
2
4
|
export { cancelRunsOwnedBy } from './cancel-owned-runs.js';
|
|
3
5
|
export { ACTIVE_CONTEXT_TTL_MS, getActiveRoute, registerActiveContextRoutes, setActiveRoute, } from './active-context.js';
|
|
@@ -21,7 +23,7 @@ export { apiHealthRoute, apiReadyRoute, apiVersionInfoRoute, healthRoute, readyR
|
|
|
21
23
|
export { connectorsAuthSessionRoute, connectorsAuthSignInRoute, connectorsAuthSignOutRoute, connectorsAuthSignUpRoute, connectorsDbDeleteRoute, connectorsDbGetRoute, connectorsDbInsertRoute, connectorsDbQueryRoute, connectorsDbUpdateRoute, connectorsRealtimePublishRoute, connectorsStorageDeleteRoute, connectorsStorageGetRoute, connectorsStorageListRoute, connectorsStoragePutRoute, registerConnectorsRoutes, } from './connectors.js';
|
|
22
24
|
export { createResearchSearchRoute, registerResearchRoutes, researchSearchRoute } from './research.js';
|
|
23
25
|
export { mediaGenerateRoute, mediaTaskDeleteRoute, mediaTaskGetRoute, mediaTaskListRoute, registerMediaRoutes, } from './media.js';
|
|
24
|
-
export { ATTACHMENTS_ROUTE_PATH, AttachmentRejectedError, createDiskAttachmentStore, detectAttachmentKind, handleAttachmentCleanup, handleAttachmentUpload, isUnchangedAttachment, registerAttachmentRoutes, sanitizeAttachmentName, writeBoundedAttachmentBody, } from './attachments.js';
|
|
26
|
+
export { ATTACHMENTS_ROUTE_PATH, AttachmentRejectedError, createDiskAttachmentStore, detectAttachmentKind, handleAttachmentCleanup, handleAttachmentUpload, isUnchangedAttachment, registerAttachmentRoutes, sanitizeAttachmentName, stageAttachmentClaim, writeBoundedAttachmentBody, } from './attachments.js';
|
|
25
27
|
export { registerXaiRoutes, xaiAuthStatusRoute, xaiOauthCancelRoute, xaiOauthCompleteRoute, xaiOauthDisconnectRoute, xaiOauthStartRoute, xaiSearchRoute, } from './xai.js';
|
|
26
28
|
export * from './run-credentials.js';
|
|
27
29
|
export { createTavilyResearchProvider } from './research-tavily.adapter.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Express, RequestHandler } from 'express';
|
|
2
|
+
import type { RunScopedCallerResolver } from '../run-credentials.js';
|
|
3
|
+
import type { AgentExecutor } from '../agent-executor/contracts.js';
|
|
4
|
+
export interface MessageRelayHttpDeps {
|
|
5
|
+
readonly agentExecutor: Pick<AgentExecutor, 'claimUserMessages'>;
|
|
6
|
+
/** The same live credential registry used to issue JINI_DAEMON_TOKEN at spawn.
|
|
7
|
+
* Explicit undefined opts into the host's existing unauthenticated loopback trust: any local
|
|
8
|
+
* process can claim messages. Authenticated hosts must supply their resolver, never proxy identity. */
|
|
9
|
+
readonly runScopedCallers: RunScopedCallerResolver | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** Claim a FIFO prefix without I/O. A run credential determines scope; the URL only checks it.
|
|
12
|
+
* Body-supplied scope is refused. A proxy credential or another run's token cannot claim.
|
|
13
|
+
* @returns Express handler; errors go to the host's existing error boundary.
|
|
14
|
+
* @complexity O(claimed text bytes); no storage or network work outside the existing event FIFO.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createMessageRelayClaimHandler({ agentExecutor, runScopedCallers }: MessageRelayHttpDeps, _options?: {}): RequestHandler;
|
|
17
|
+
/** Mount behind the host's existing daemon gate; allow this POST for live run credentials there. */
|
|
18
|
+
export declare function registerMessageRelayRoutes({ app, ...deps }: {
|
|
19
|
+
app: Express;
|
|
20
|
+
} & MessageRelayHttpDeps, _options?: {}): void;
|
|
21
|
+
//# sourceMappingURL=message-relay.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { bearerTokenFromHeader } from '@jini-ai/http-kit';
|
|
2
|
+
/** Claim a FIFO prefix without I/O. A run credential determines scope; the URL only checks it.
|
|
3
|
+
* Body-supplied scope is refused. A proxy credential or another run's token cannot claim.
|
|
4
|
+
* @returns Express handler; errors go to the host's existing error boundary.
|
|
5
|
+
* @complexity O(claimed text bytes); no storage or network work outside the existing event FIFO.
|
|
6
|
+
*/
|
|
7
|
+
export function createMessageRelayClaimHandler({ agentExecutor, runScopedCallers }, _options = {}) {
|
|
8
|
+
return (req, res, next) => {
|
|
9
|
+
try {
|
|
10
|
+
const token = bearerTokenFromHeader({ header: req.get('authorization') });
|
|
11
|
+
const caller = token ? runScopedCallers?.resolveCaller({ token }) : undefined;
|
|
12
|
+
if (runScopedCallers && !caller) {
|
|
13
|
+
res.status(401).json({ error: { code: 'UNAUTHENTICATED', message: 'a live run credential is required' } });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const runId = caller?.runId ?? req.params.runId;
|
|
17
|
+
if (!runId || runId !== req.params.runId) {
|
|
18
|
+
res.status(404).json({ error: { code: 'NOT_FOUND', message: 'run was not found' } });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const body = req.body;
|
|
22
|
+
if (!validClaim(body)) {
|
|
23
|
+
res.status(400).json({ error: { code: 'BAD_REQUEST', message: 'body must contain only via: tool-result or check_messages' } });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
res.json({ messages: agentExecutor.claimUserMessages({ runId, via: body.via }) });
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
next(error);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Strict receipt-channel validation; no caller-selected run or principal is accepted. */
|
|
34
|
+
function validClaim(body) {
|
|
35
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body))
|
|
36
|
+
return false;
|
|
37
|
+
const value = body;
|
|
38
|
+
return Object.keys(value).length === 1 && (value.via === 'tool-result' || value.via === 'check_messages');
|
|
39
|
+
}
|
|
40
|
+
/** Mount behind the host's existing daemon gate; allow this POST for live run credentials there. */
|
|
41
|
+
export function registerMessageRelayRoutes({ app, ...deps }, _options = {}) {
|
|
42
|
+
app.post('/api/runs/:runId/messages/claim', createMessageRelayClaimHandler(deps));
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=message-relay.js.map
|
package/dist/http/runs.d.ts
CHANGED
|
@@ -55,12 +55,15 @@ export interface RunCancelResponse {
|
|
|
55
55
|
readonly run: RunStatus;
|
|
56
56
|
}
|
|
57
57
|
export interface RunMessageRequest {
|
|
58
|
+
/** Generated at intake and passed unchanged to the executor's queue/consumption events. */
|
|
59
|
+
readonly id?: string;
|
|
58
60
|
readonly runId: string;
|
|
59
61
|
readonly text: string;
|
|
60
62
|
/** Opaque upload capabilities; the host must authorize and prepare every ref before delivery. */
|
|
61
63
|
readonly attachmentIds?: readonly string[];
|
|
62
64
|
}
|
|
63
65
|
export interface RunMessageResponse {
|
|
66
|
+
readonly id: string;
|
|
64
67
|
readonly delivery: 'delivered';
|
|
65
68
|
}
|
|
66
69
|
/** Ceiling on one mid-run message, matching what a person can reasonably type or paste into a composer. */
|
|
@@ -81,8 +84,8 @@ export declare const runCancelRoute: import("@jini-ai/http-kit").JsonRouteSpec<{
|
|
|
81
84
|
/**
|
|
82
85
|
* `POST /api/runs/:runId/messages` — a message the human sent while the run is going. `202` when it
|
|
83
86
|
* reached the live agent; `409 CONFLICT` with `details.delivery` (`'unsupported'` or
|
|
84
|
-
* `'not-running'`) when it did not, which tells the client
|
|
85
|
-
*
|
|
87
|
+
* `'not-running'`) when it did not, which tells the client to retain the next-turn queue without canceling live work.
|
|
88
|
+
* Acceptance is distinct from the UUID-attributed consumption receipt in the run stream.
|
|
86
89
|
*/
|
|
87
90
|
export declare const runMessageRoute: import("@jini-ai/http-kit").JsonRouteSpec<RunMessageRequest, RunMessageResponse, RunHttpDeps, undefined>;
|
|
88
91
|
/**
|