@jini-ai/core 0.6.15 → 0.6.17
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 +18 -0
- package/README.md +3 -2
- package/dist/approval.d.ts +15 -2
- package/dist/approval.js +47 -11
- package/dist/batch-tool-inputs.d.ts +12 -3
- package/dist/batch-tool-inputs.js +67 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/optional-peer.d.ts +17 -0
- package/dist/optional-peer.js +40 -0
- package/dist/tool-registry.d.ts +5 -1
- package/package.json +8 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @jini-ai/core
|
|
2
2
|
|
|
3
|
+
## 0.6.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Mark required single-call fields in published batched-tool schemas and descriptions while preserving batching and clear missing-input errors.
|
|
8
|
+
|
|
9
|
+
## 0.6.16
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Remove the GitHub Copilot CLI agent: its def, `createCopilotStreamHandler`/`CopilotStreamEvent`,
|
|
14
|
+
account model discovery and the `@github/copilot-sdk` optional peer, the MCP installer's `copilot`
|
|
15
|
+
slug, and its icon. A saved `copilot` agent id no longer resolves (`getAgentDef` returns `null`).
|
|
16
|
+
CopilotKit/AG-UI is unrelated and unchanged.
|
|
17
|
+
|
|
18
|
+
Drop peers no runtime entry loads: `express` from core and http-kit (type-only imports; type
|
|
19
|
+
consumers keep `@types/express`), `vue` from ui-kit, `react-dom` from vibecoding.
|
|
20
|
+
|
|
3
21
|
## 0.6.15
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -15,8 +15,9 @@ set of tokens, and fails to typecheck if any pack's declared dependency is left
|
|
|
15
15
|
npm install @jini-ai/core
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
The root has no runtime dependencies and never loads settings or Express.
|
|
19
|
-
|
|
18
|
+
The root has no runtime dependencies and never loads settings or Express. The
|
|
19
|
+
`@jini-ai/core/settings/express` adapter uses Express types and the host's injected app; it never
|
|
20
|
+
loads the Express runtime, so this package declares no Express peer.
|
|
20
21
|
|
|
21
22
|
## What you get
|
|
22
23
|
|
package/dist/approval.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type HumanConfirmer } from './registration-kit.js';
|
|
2
2
|
import type { ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolRegistration } from './tool-registry.js';
|
|
3
|
+
/** Maximum human approval wait for one invocation, independent of execution time. */
|
|
4
|
+
export declare const APPROVAL_WAIT_TIMEOUT_MS = 120000;
|
|
3
5
|
/** A host describes the action; core owns the decision lifecycle, never the card format. */
|
|
4
6
|
export interface ApprovalPlan<TDescription> {
|
|
5
7
|
readonly ask: boolean;
|
|
@@ -103,6 +105,12 @@ export declare function createApprovalProposalStore<TDescription>({ responseTool
|
|
|
103
105
|
}, { createId }?: {
|
|
104
106
|
createId?: () => string;
|
|
105
107
|
}): {
|
|
108
|
+
request: ({ description, signal }: {
|
|
109
|
+
description: TDescription;
|
|
110
|
+
signal: AbortSignal;
|
|
111
|
+
}, { timeoutMs }?: {
|
|
112
|
+
timeoutMs?: number;
|
|
113
|
+
}) => Promise<ApprovalAnswer>;
|
|
106
114
|
getSnapshot: (_required?: {}, _optional?: {}) => {
|
|
107
115
|
pending: {
|
|
108
116
|
approvalId: string;
|
|
@@ -117,7 +125,10 @@ export declare function createApprovalProposalStore<TDescription>({ responseTool
|
|
|
117
125
|
description: TDescription;
|
|
118
126
|
execute: () => Promise<unknown>;
|
|
119
127
|
signal: AbortSignal;
|
|
120
|
-
},
|
|
128
|
+
}, { humanOnly, decide }?: {
|
|
129
|
+
humanOnly?: boolean;
|
|
130
|
+
decide?: (answer: ApprovalAnswer) => void;
|
|
131
|
+
}): {
|
|
121
132
|
status: "approval_required";
|
|
122
133
|
approvalId: string;
|
|
123
134
|
description: TDescription;
|
|
@@ -126,7 +137,9 @@ export declare function createApprovalProposalStore<TDescription>({ responseTool
|
|
|
126
137
|
respond({ approvalId, approved }: {
|
|
127
138
|
approvalId: string;
|
|
128
139
|
approved: boolean;
|
|
129
|
-
},
|
|
140
|
+
}, { source }?: {
|
|
141
|
+
source?: "human";
|
|
142
|
+
}): Promise<unknown>;
|
|
130
143
|
};
|
|
131
144
|
/** Apply a host classification through the same handler owner used by domain plans.
|
|
132
145
|
* A host description's string `rememberKey` opts into reusable grants; descriptions remain opaque.
|
package/dist/approval.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { humanConfirmedHandler, requireInputRecord } from './registration-kit.js';
|
|
2
2
|
import { ToolInputError } from './tool-registry.js';
|
|
3
3
|
import { isNonArrayRecord } from './records.js';
|
|
4
|
+
/** Maximum human approval wait for one invocation, independent of execution time. */
|
|
5
|
+
export const APPROVAL_WAIT_TIMEOUT_MS = 120_000;
|
|
4
6
|
/** The decision reducer shared by parked tool calls and the browser's explicit reply transport.
|
|
5
7
|
* An answer never revives an aborted operation. Transport failures propagate without an effect.
|
|
6
8
|
* @param optional.onApprovalStateChange Reports only the human wait, released on every outcome.
|
|
@@ -157,33 +159,45 @@ export function createApprovalProposalStore({ responseTool, messages }, { create
|
|
|
157
159
|
const listeners = new Set();
|
|
158
160
|
const publish = (next) => { snapshot = next; for (const listener of listeners)
|
|
159
161
|
listener(); };
|
|
160
|
-
const clear = () => {
|
|
162
|
+
const clear = (answer = { confirmed: false, reason: 'abandoned' }) => {
|
|
163
|
+
const decide = action?.decide;
|
|
161
164
|
detach?.();
|
|
162
165
|
detach = null;
|
|
163
166
|
action = null;
|
|
164
167
|
publish({ pending: null, error: null });
|
|
168
|
+
decide?.(answer);
|
|
165
169
|
};
|
|
166
|
-
|
|
170
|
+
/** Resolve authority before consuming the single-use action; model input cannot set source. */
|
|
171
|
+
const actionForReply = ({ approvalId, source }) => {
|
|
172
|
+
if (!action || action.approvalId !== approvalId)
|
|
173
|
+
throw new Error(messages.missing);
|
|
174
|
+
if (action.humanOnly && source !== 'human')
|
|
175
|
+
throw new Error('This decision requires the app dialog');
|
|
176
|
+
return action;
|
|
177
|
+
};
|
|
178
|
+
const store = {
|
|
167
179
|
getSnapshot: (_required = {}, _optional = {}) => snapshot,
|
|
168
180
|
subscribe: ({ listener }, _optional = {}) => { listeners.add(listener); return () => { listeners.delete(listener); }; },
|
|
169
|
-
propose({ description, execute, signal },
|
|
181
|
+
propose({ description, execute, signal }, { humanOnly = false, decide } = {}) {
|
|
170
182
|
if (signal.aborted)
|
|
171
183
|
throw new Error(messages.closed);
|
|
172
184
|
if (snapshot.pending)
|
|
173
185
|
throw new Error(messages.pending);
|
|
174
186
|
const frozenDescription = structuredClone(description);
|
|
175
187
|
const approvalId = createId();
|
|
176
|
-
action = { approvalId, execute, signal };
|
|
177
|
-
|
|
178
|
-
|
|
188
|
+
action = { approvalId, execute, signal, humanOnly, ...(decide ? { decide } : {}) };
|
|
189
|
+
const abort = () => clear();
|
|
190
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
191
|
+
detach = () => signal.removeEventListener('abort', abort);
|
|
179
192
|
publish({ pending: { approvalId, description: structuredClone(frozenDescription) }, error: null });
|
|
180
193
|
return { status: 'approval_required', approvalId, description: frozenDescription, responseTool };
|
|
181
194
|
},
|
|
182
|
-
async respond({ approvalId, approved },
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
async respond({ approvalId, approved }, { source } = {}) {
|
|
196
|
+
const prepared = actionForReply({ approvalId, source });
|
|
197
|
+
clear(approved === true ? { confirmed: true } : { confirmed: false, reason: 'declined' });
|
|
198
|
+
// An awaiting caller owns execution after this decision; do not start a second lifecycle.
|
|
199
|
+
if (prepared.decide)
|
|
200
|
+
return { status: approved === true ? 'approved' : 'declined' };
|
|
187
201
|
const answer = await resolveApproval({ signal: prepared.signal, askHuman: async () => approved === true ? { confirmed: true } : { confirmed: false, reason: 'declined' } });
|
|
188
202
|
if (!answer.confirmed)
|
|
189
203
|
return { status: 'declined' };
|
|
@@ -196,6 +210,28 @@ export function createApprovalProposalStore({ responseTool, messages }, { create
|
|
|
196
210
|
}
|
|
197
211
|
},
|
|
198
212
|
};
|
|
213
|
+
/** Park a call on this same store without publishing any assistant reply capability. The app
|
|
214
|
+
* dialog alone may decide it. Expiry/abort consume the proposal before resolving the waiter. */
|
|
215
|
+
async function request({ description, signal }, { timeoutMs = APPROVAL_WAIT_TIMEOUT_MS } = {}) {
|
|
216
|
+
const lifetime = new AbortController();
|
|
217
|
+
const abort = () => lifetime.abort();
|
|
218
|
+
if (signal.aborted)
|
|
219
|
+
return { confirmed: false, reason: 'abandoned' };
|
|
220
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
221
|
+
let expired = false;
|
|
222
|
+
const timer = setTimeout(() => { expired = true; lifetime.abort(); }, timeoutMs);
|
|
223
|
+
try {
|
|
224
|
+
return await resolveApproval({ signal: lifetime.signal, askHuman: () => new Promise(resolve => {
|
|
225
|
+
store.propose({ description, signal: lifetime.signal, execute: async () => undefined }, { humanOnly: true, decide: resolve });
|
|
226
|
+
}) }).then(answer => expired ? { confirmed: false, reason: 'expired' } : answer);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
clearTimeout(timer);
|
|
230
|
+
signal.removeEventListener('abort', abort);
|
|
231
|
+
lifetime.abort();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { ...store, request };
|
|
199
235
|
}
|
|
200
236
|
/** Apply a host classification through the same handler owner used by domain plans.
|
|
201
237
|
* A host description's string `rememberKey` opts into reusable grants; descriptions remain opaque.
|
|
@@ -20,6 +20,14 @@ export declare const BATCH_GUIDANCE_MARKER = " Batch: ";
|
|
|
20
20
|
export declare function withoutBatchGuidance({ description }: {
|
|
21
21
|
description: string;
|
|
22
22
|
}, _optional?: {}): string;
|
|
23
|
+
/** The original top-level `required` fields retained by {@link batchDescriptorFor} as schema metadata.
|
|
24
|
+
* A batch schema declares no top-level `required`, so a discovery signature built from it alone
|
|
25
|
+
* makes a single call look argument-free and the model sends `{}` — this recovers them.
|
|
26
|
+
* @returns The moved field names in order; [] for a non-batch schema or a tool that required none.
|
|
27
|
+
* @complexity O(r) time and space for r required fields; pure. */
|
|
28
|
+
export declare function batchItemRequiredFields({ inputSchema }: {
|
|
29
|
+
inputSchema: unknown;
|
|
30
|
+
}, _optional?: {}): string[];
|
|
23
31
|
/** Used when a {@link BatchableTool} names no `maxItems`. */
|
|
24
32
|
export declare const DEFAULT_MAX_BATCH_ITEMS = 25;
|
|
25
33
|
/** One item's outcome. `error` is the domain's own message without the appended input schema,
|
|
@@ -53,15 +61,16 @@ export declare function batchOutcomeFor({ output }: {
|
|
|
53
61
|
}, _options?: {}): BatchResult['outcome'] | undefined;
|
|
54
62
|
/**
|
|
55
63
|
* The descriptor a batchable tool publishes: its own schema plus an `items` array, its top-level
|
|
56
|
-
* `required` list
|
|
57
|
-
* the top level through the declared legacy normalization), and batch
|
|
64
|
+
* `required` list retained as metadata and described on required properties (a batch call sets
|
|
65
|
+
* those per item or once at the top level through the declared legacy normalization), and batch
|
|
66
|
+
* guidance appended to its description.
|
|
58
67
|
*
|
|
59
68
|
* @param required.descriptor - The tool's registered descriptor.
|
|
60
69
|
* @param required.tool - Its batch settings.
|
|
61
70
|
* @returns A new descriptor; the input is not mutated.
|
|
62
71
|
* @throws {Error} If the tool publishes no object schema, or already has an `items` property
|
|
63
72
|
* (the batch field would shadow it) — both developer errors caught at composition time.
|
|
64
|
-
* @complexity O(p)
|
|
73
|
+
* @complexity O(p + r) time and space for p top-level properties and r required fields.
|
|
65
74
|
*/
|
|
66
75
|
export declare function batchDescriptorFor({ descriptor, tool }: {
|
|
67
76
|
descriptor: ToolRegistration["descriptor"];
|
|
@@ -16,6 +16,21 @@ export function withoutBatchGuidance({ description }, _optional = {}) {
|
|
|
16
16
|
return end === -1 ? `${BATCH_GUIDANCE_MARKER}to do several at once,${part}` : part.slice(end + BATCH_GUIDANCE_END.length);
|
|
17
17
|
}).join("");
|
|
18
18
|
}
|
|
19
|
+
const ITEM_NEEDS = " Each item needs ";
|
|
20
|
+
const ITEM_NEEDS_END = " (inside each item).";
|
|
21
|
+
// Keep discovery requirements through descriptor/schema spreads without adding wire-schema keys.
|
|
22
|
+
const BATCH_ITEM_REQUIRED_FIELDS = Symbol('batchItemRequiredFields');
|
|
23
|
+
/** The original top-level `required` fields retained by {@link batchDescriptorFor} as schema metadata.
|
|
24
|
+
* A batch schema declares no top-level `required`, so a discovery signature built from it alone
|
|
25
|
+
* makes a single call look argument-free and the model sends `{}` — this recovers them.
|
|
26
|
+
* @returns The moved field names in order; [] for a non-batch schema or a tool that required none.
|
|
27
|
+
* @complexity O(r) time and space for r required fields; pure. */
|
|
28
|
+
export function batchItemRequiredFields({ inputSchema }, _optional = {}) {
|
|
29
|
+
if (!isPlainObject(inputSchema))
|
|
30
|
+
return [];
|
|
31
|
+
const schema = inputSchema;
|
|
32
|
+
return [...(schema[BATCH_ITEM_REQUIRED_FIELDS] ?? [])];
|
|
33
|
+
}
|
|
19
34
|
/** Used when a {@link BatchableTool} names no `maxItems`. */
|
|
20
35
|
export const DEFAULT_MAX_BATCH_ITEMS = 25;
|
|
21
36
|
/** Classify a batch result, including legacy envelopes, without dropping per-item evidence.
|
|
@@ -40,15 +55,16 @@ function isPlainObject(value) {
|
|
|
40
55
|
}
|
|
41
56
|
/**
|
|
42
57
|
* The descriptor a batchable tool publishes: its own schema plus an `items` array, its top-level
|
|
43
|
-
* `required` list
|
|
44
|
-
* the top level through the declared legacy normalization), and batch
|
|
58
|
+
* `required` list retained as metadata and described on required properties (a batch call sets
|
|
59
|
+
* those per item or once at the top level through the declared legacy normalization), and batch
|
|
60
|
+
* guidance appended to its description.
|
|
45
61
|
*
|
|
46
62
|
* @param required.descriptor - The tool's registered descriptor.
|
|
47
63
|
* @param required.tool - Its batch settings.
|
|
48
64
|
* @returns A new descriptor; the input is not mutated.
|
|
49
65
|
* @throws {Error} If the tool publishes no object schema, or already has an `items` property
|
|
50
66
|
* (the batch field would shadow it) — both developer errors caught at composition time.
|
|
51
|
-
* @complexity O(p)
|
|
67
|
+
* @complexity O(p + r) time and space for p top-level properties and r required fields.
|
|
52
68
|
*/
|
|
53
69
|
export function batchDescriptorFor({ descriptor, tool }, _optional = {}) {
|
|
54
70
|
const schema = descriptor.inputSchema;
|
|
@@ -60,9 +76,18 @@ export function batchDescriptorFor({ descriptor, tool }, _optional = {}) {
|
|
|
60
76
|
}
|
|
61
77
|
const maxItems = tool.maxItems ?? DEFAULT_MAX_BATCH_ITEMS;
|
|
62
78
|
const requiredFields = schema.required ?? [];
|
|
79
|
+
const requiredNames = new Set(requiredFields);
|
|
80
|
+
const properties = Object.fromEntries(Object.entries(schema.properties).map(([name, property]) => [
|
|
81
|
+
name,
|
|
82
|
+
requiredNames.has(name) && isPlainObject(property)
|
|
83
|
+
? { ...property, description: `Required for a single call (or set it in each item). ${property.description ?? ""}` }
|
|
84
|
+
: property,
|
|
85
|
+
]));
|
|
63
86
|
const rest = { ...schema };
|
|
64
87
|
delete rest.required;
|
|
65
|
-
const needs = requiredFields.length > 0 ?
|
|
88
|
+
const needs = requiredFields.length > 0 ? `${ITEM_NEEDS}${requiredFields.join(", ")}${ITEM_NEEDS_END}` : "";
|
|
89
|
+
// Inside the removable guidance so search keeps only the authored description and signature.
|
|
90
|
+
const singleNeeds = requiredFields.length > 0 ? `Single call requires: ${requiredFields.join(", ")}. ` : "";
|
|
66
91
|
const items = {
|
|
67
92
|
type: "array",
|
|
68
93
|
minItems: 1,
|
|
@@ -75,8 +100,9 @@ export function batchDescriptorFor({ descriptor, tool }, _optional = {}) {
|
|
|
75
100
|
...descriptor,
|
|
76
101
|
description: `${descriptor.description ?? ""}${BATCH_GUIDANCE_MARKER}to do several at once, pass items: [{...}, ...] (up to ${maxItems}) in ONE call instead of one call per item. ` +
|
|
77
102
|
"Canonical shape: {items:[{...}, ...]} only. Legacy shared top-level fields are normalized into each item; item values win. " +
|
|
103
|
+
singleNeeds +
|
|
78
104
|
"Returns { batch: true, outcome: completed | partial_failure | error, total, succeeded, failed, results: [{ index, ok, result | error }] }; one item's failure does not stop the others.",
|
|
79
|
-
inputSchema: { ...rest, properties: { ...
|
|
105
|
+
inputSchema: { ...rest, [BATCH_ITEM_REQUIRED_FIELDS]: [...requiredFields], properties: { ...properties, items } },
|
|
80
106
|
};
|
|
81
107
|
}
|
|
82
108
|
/** The domain message without `withSchemaOnRejection`'s appended schema: the text before the
|
|
@@ -111,6 +137,38 @@ async function normalizeBatchItem({ registration, defaults, shared, item }) {
|
|
|
111
137
|
const normalized = await normalizeToolInput({ input, registration });
|
|
112
138
|
return { input: normalized.input, notes: [...layers.flatMap(layer => layer.notes), ...normalized.notes] };
|
|
113
139
|
}
|
|
140
|
+
/** Restore the single-call requirement the published batch schema intentionally omits, from the
|
|
141
|
+
* wrapped tool's own schema. Normalize first so repaired spellings and domain aliases satisfy it.
|
|
142
|
+
* @throws ToolInputError with every required name/type when any required value is missing.
|
|
143
|
+
* @complexity O(r) for r required fields, plus the existing normalization; no effects.
|
|
144
|
+
*/
|
|
145
|
+
async function normalizeSingleBatchInput({ input, registration }) {
|
|
146
|
+
const normalized = await normalizeToolInput({ input, registration });
|
|
147
|
+
const value = normalized.input;
|
|
148
|
+
const schema = registration.descriptor.inputSchema;
|
|
149
|
+
const required = schema.required ?? [];
|
|
150
|
+
if (!isPlainObject(value) || required.every(name => value[name] !== undefined))
|
|
151
|
+
return normalized;
|
|
152
|
+
const missing = required.filter(name => value[name] === undefined);
|
|
153
|
+
const fields = required.map(name => `${name} (${schemaTypeLabel(schema.properties?.[name])})`);
|
|
154
|
+
throw new ToolInputError({ message: `${registration.descriptor.id}: missing required inputs: ${missing.join(', ')}. Required inputs: ${fields.join(', ')}.` });
|
|
155
|
+
}
|
|
156
|
+
/** A property's declared JSON type, `a | b` for a type list, `unknown` when undeclared. */
|
|
157
|
+
function schemaTypeLabel(property) {
|
|
158
|
+
const type = isPlainObject(property) ? property.type : undefined;
|
|
159
|
+
if (Array.isArray(type))
|
|
160
|
+
return type.join(' | ');
|
|
161
|
+
return typeof type === 'string' ? type : 'unknown';
|
|
162
|
+
}
|
|
163
|
+
/** A batch result is one JSON value, so an item's MCP `image` block would reach the model and the
|
|
164
|
+
* chat as base64 text (a 20-item import batch, 20 previews). Drop images from item envelopes; the
|
|
165
|
+
* item's text blocks still carry its result. Anything that is not an envelope is returned as is. */
|
|
166
|
+
function withoutItemImages(output) {
|
|
167
|
+
if (!isPlainObject(output) || !Array.isArray(output.content))
|
|
168
|
+
return output;
|
|
169
|
+
const content = output.content.filter((block) => !isPlainObject(block) || block.type !== 'image');
|
|
170
|
+
return content.length === output.content.length ? output : { ...output, content };
|
|
171
|
+
}
|
|
114
172
|
/** Classify one returned output without dropping error payloads or normalization notes.
|
|
115
173
|
* @complexity O(n) for a nested batch's n results, plus formatting its notes; no effects.
|
|
116
174
|
*/
|
|
@@ -158,7 +216,7 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
|
|
|
158
216
|
// its policy and planner receive the same targets; never execute items to preflight them.
|
|
159
217
|
normalizeInput: async ({ input }) => {
|
|
160
218
|
if (!Object.hasOwn(input, 'items'))
|
|
161
|
-
return
|
|
219
|
+
return normalizeSingleBatchInput({ input, registration });
|
|
162
220
|
const { items: rawItems, ...shared } = input;
|
|
163
221
|
const items = readItems({ items: rawItems, toolId, maxItems });
|
|
164
222
|
const normalized = [];
|
|
@@ -189,7 +247,7 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
|
|
|
189
247
|
if (!options?.invokeTool)
|
|
190
248
|
throw new Error(`${toolId}: batch items require the gated ToolExecutor invocation port`);
|
|
191
249
|
const output = await options.invokeTool({ toolId, input: normalized.input }, { registry: itemRegistry });
|
|
192
|
-
results.push(batchItemResult({ index, output, notes: normalized.notes }));
|
|
250
|
+
results.push(batchItemResult({ index, output: withoutItemImages(output), notes: normalized.notes }));
|
|
193
251
|
}
|
|
194
252
|
catch (error) {
|
|
195
253
|
results.push({ index, ok: false, error: withNormalizationNotes({ output: itemErrorMessage({ error, toolId }), notes }) });
|
|
@@ -199,9 +257,9 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
|
|
|
199
257
|
};
|
|
200
258
|
return { ...registration, descriptor, handler,
|
|
201
259
|
// Domain normalization belongs to each merged item, not the incomplete shared layer.
|
|
202
|
-
normalizeInput: async ({ input }
|
|
260
|
+
normalizeInput: async ({ input }) => Object.hasOwn(input, 'items')
|
|
203
261
|
? { input, notes: [] }
|
|
204
|
-
:
|
|
262
|
+
: normalizeSingleBatchInput({ input, registration }),
|
|
205
263
|
};
|
|
206
264
|
}
|
|
207
265
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export * from './api-token-auth.js';
|
|
|
11
11
|
export * from './origin-validation.js';
|
|
12
12
|
export * from './principal.js';
|
|
13
13
|
export * from './surface-exchanges.js';
|
|
14
|
-
export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolExecutionOptions, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
|
|
14
|
+
export type { AuthorizationDecision, RunRef, SurfaceEmission, SurfaceEmitter, ToolAuthorizationContext, ToolDescriptor, ToolExecutionContext, ToolExecutionOptions, ToolApprovalDecision, ToolHandler, ToolPolicy, ToolRegistration, ToolRegistry, } from './tool-registry.js';
|
|
15
15
|
export { createToolRegistrationScope, toolRegistryRevision, createToolRegistry, isReadOnlyTool, toolDiscoveryMetadata, ToolInputError } from './tool-registry.js';
|
|
16
16
|
export * from './tool-tokens.js';
|
|
17
17
|
export * from './tool-exposure.js';
|
|
@@ -30,6 +30,6 @@ export * from "./native-regexp.js";
|
|
|
30
30
|
export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, type LegacyToolAuthorizeFn, type AuthorizationPort as ToolPermissionAuthorizationPort, type RequireToolPermissionRequired, type RequireToolPermissionOptional, } from "./tool-permissions.js";
|
|
31
31
|
export { evaluateFeatureFlag, type FeatureFlagDecision } from './feature-flags.js';
|
|
32
32
|
export { normalizeToolInput, withNormalizationNotes, ToolInputNormalizationError, type NormalizedToolInput, type ToolInputNormalizer } from './tool-input-normalization.js';
|
|
33
|
-
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS, type BatchableTool, type BatchEligibility, type BatchOptOut, type BatchResult, type BatchItemResult } from './batch-tool-inputs.js';
|
|
33
|
+
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchItemRequiredFields, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS, type BatchableTool, type BatchEligibility, type BatchOptOut, type BatchResult, type BatchItemResult } from './batch-tool-inputs.js';
|
|
34
34
|
export type { MediaProviderCatalogPolicy, MediaProviderOption, ProviderPreset, ProviderPresetKind } from './provider-catalog.js';
|
|
35
35
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -25,5 +25,5 @@ export * from "./native-regexp.js";
|
|
|
25
25
|
export { ForbiddenError as ToolPermissionDeniedError, requireToolPermission, adaptLegacyAuthorize, } from "./tool-permissions.js";
|
|
26
26
|
export { evaluateFeatureFlag } from './feature-flags.js';
|
|
27
27
|
export { normalizeToolInput, withNormalizationNotes, ToolInputNormalizationError } from './tool-input-normalization.js';
|
|
28
|
-
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS } from './batch-tool-inputs.js';
|
|
28
|
+
export { applyBatchInputs, BATCH_GUIDANCE_MARKER, withoutBatchGuidance, batchOutcomeFor, batchDescriptorFor, batchItemRequiredFields, batchEligibilityFor, defaultBatchableTools, withBatchInput, DEFAULT_MAX_BATCH_ITEMS } from './batch-tool-inputs.js';
|
|
29
29
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a host-selected feature's optional library, synchronously or asynchronously.
|
|
3
|
+
* Keep the import/require literal in the caller so bundlers and closure guards can see it.
|
|
4
|
+
* @throws A feature-specific load error with the original resolution/ABI error as `cause`.
|
|
5
|
+
* @complexity O(1) wrapper work; loading cost belongs to the supplied loader.
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadOptionalPeer<T>({ feature, dependency, load }: {
|
|
8
|
+
feature: string;
|
|
9
|
+
dependency: string;
|
|
10
|
+
load: () => T;
|
|
11
|
+
}, { createError }?: {
|
|
12
|
+
createError?: (input: {
|
|
13
|
+
message: string;
|
|
14
|
+
cause: unknown;
|
|
15
|
+
}) => Error;
|
|
16
|
+
}): T;
|
|
17
|
+
//# sourceMappingURL=optional-peer.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a host-selected feature's optional library, synchronously or asynchronously.
|
|
3
|
+
* Keep the import/require literal in the caller so bundlers and closure guards can see it.
|
|
4
|
+
* @throws A feature-specific load error with the original resolution/ABI error as `cause`.
|
|
5
|
+
* @complexity O(1) wrapper work; loading cost belongs to the supplied loader.
|
|
6
|
+
*/
|
|
7
|
+
export function loadOptionalPeer({ feature, dependency, load }, { createError = ({ message, cause }) => new Error(message, { cause }) } = {}) {
|
|
8
|
+
const failed = (cause) => {
|
|
9
|
+
throw createError({ message: optionalPeerLoadMessage({ feature, dependency, cause }), cause });
|
|
10
|
+
};
|
|
11
|
+
try {
|
|
12
|
+
const library = load();
|
|
13
|
+
// Native import() returns a Promise. Synchronous factories must keep their return contract.
|
|
14
|
+
if (library instanceof Promise)
|
|
15
|
+
return library.catch(failed);
|
|
16
|
+
return library;
|
|
17
|
+
}
|
|
18
|
+
catch (cause) {
|
|
19
|
+
return failed(cause);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Classify loader failures without echoing paths or other untrusted driver diagnostics.
|
|
23
|
+
* Resolution of an internal dependency is not evidence the selected peer is absent.
|
|
24
|
+
* Pure; O(1) branches and O(message length) matching; no errors or side effects. */
|
|
25
|
+
function optionalPeerLoadMessage({ feature, dependency, cause }, _options = {}) {
|
|
26
|
+
const prefix = `${feature} could not load ${dependency}.`;
|
|
27
|
+
const { message: detail = '', code } = cause instanceof Error ? cause : {};
|
|
28
|
+
if (/NODE_MODULE_VERSION|compiled against a different Node\.js version/.test(detail)) {
|
|
29
|
+
return `${prefix} Its native binary was built for a different Node.js/Electron ABI. Rebuild ${dependency} for the host runtime.`;
|
|
30
|
+
}
|
|
31
|
+
if (code === 'ERR_DLOPEN_FAILED') {
|
|
32
|
+
return `${prefix} Its native binary is incompatible with this runtime or architecture. Rebuild ${dependency} for the host runtime.`;
|
|
33
|
+
}
|
|
34
|
+
const missing = code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
|
|
35
|
+
if (missing && (detail.includes(`Cannot find module '${dependency}'`) || detail.includes(`Cannot find package '${dependency}'`))) {
|
|
36
|
+
return `${prefix} It is not installed. Install ${dependency} to enable this feature.`;
|
|
37
|
+
}
|
|
38
|
+
return `${prefix} Check its installed dependencies and native binaries.`;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=optional-peer.js.map
|
package/dist/tool-registry.d.ts
CHANGED
|
@@ -160,13 +160,17 @@ export interface ToolExecutionContext {
|
|
|
160
160
|
readonly input: unknown;
|
|
161
161
|
readonly signal: AbortSignal;
|
|
162
162
|
}
|
|
163
|
+
/** Value-free human-dialog outcomes recorded by the executor; never authorization grants. */
|
|
164
|
+
export type ToolApprovalDecision = 'approved' | 'declined' | 'expired' | 'abandoned';
|
|
163
165
|
/** Optional handler ports supplied by the invoking transport. */
|
|
164
166
|
export interface ToolExecutionOptions {
|
|
165
167
|
readonly emitSurface?: SurfaceEmitter;
|
|
166
168
|
/** Reports the human wait to the executor so concurrent writes can be refused. This is
|
|
167
|
-
* notification only; it never
|
|
169
|
+
* notification only; it never grants authority or substitutes for the human dialog. */
|
|
168
170
|
readonly onApprovalStateChange?: (required: {
|
|
169
171
|
pending: boolean;
|
|
172
|
+
/** Bound frontend's human-dialog outcome; observation only, never an authorization grant. */
|
|
173
|
+
decision?: ToolApprovalDecision;
|
|
170
174
|
}, optional?: Record<string, never>) => void;
|
|
171
175
|
/** Invoke through the same executor, retaining this call's principal, run, cancellation and
|
|
172
176
|
* transport gates. Rejects on denial, failed confirmation, validation or execution failure.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.17",
|
|
4
4
|
"description": "The Jini kernel: typed DI tokens, pack composition and lifecycle, the tool registry, redaction, and origin/API-token auth primitives. Zero dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"./settings": "universal",
|
|
29
29
|
"./settings/express": "node",
|
|
30
30
|
"./plugin-directory": "universal",
|
|
31
|
-
"./filesystem": "universal"
|
|
31
|
+
"./filesystem": "universal",
|
|
32
|
+
"./optional-peer": "universal"
|
|
32
33
|
},
|
|
33
34
|
"isolation": {
|
|
34
35
|
"entries": {
|
|
@@ -161,6 +162,11 @@
|
|
|
161
162
|
"types": "./dist/filesystem.d.ts",
|
|
162
163
|
"import": "./dist/filesystem.js",
|
|
163
164
|
"default": "./dist/filesystem.js"
|
|
165
|
+
},
|
|
166
|
+
"./optional-peer": {
|
|
167
|
+
"types": "./dist/optional-peer.d.ts",
|
|
168
|
+
"import": "./dist/optional-peer.js",
|
|
169
|
+
"default": "./dist/optional-peer.js"
|
|
164
170
|
}
|
|
165
171
|
},
|
|
166
172
|
"files": [
|
|
@@ -187,14 +193,6 @@
|
|
|
187
193
|
"express": "^4.21.0",
|
|
188
194
|
"@types/express": "^4.17.21"
|
|
189
195
|
},
|
|
190
|
-
"peerDependencies": {
|
|
191
|
-
"express": "^4.21.0 || ^5.0.0"
|
|
192
|
-
},
|
|
193
|
-
"peerDependenciesMeta": {
|
|
194
|
-
"express": {
|
|
195
|
-
"optional": true
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
196
|
"scripts": {
|
|
199
197
|
"build": "tsc -p tsconfig.json",
|
|
200
198
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|