@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 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. `express` is an optional
19
- peer used only by `@jini-ai/core/settings/express`; install it in hosts mounting those routes.
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
 
@@ -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
- }, _optional?: {}): {
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
- }, _optional?: {}): Promise<unknown>;
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
- return {
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 }, _optional = {}) {
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
- signal.addEventListener('abort', clear, { once: true });
178
- detach = () => signal.removeEventListener('abort', clear);
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 }, _optional = {}) {
183
- if (!action || action.approvalId !== approvalId)
184
- throw new Error(messages.missing);
185
- const prepared = action;
186
- clear();
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 moved into the `items` description (a batch call sets those per item or once at
57
- * the top level through the declared legacy normalization), and batch guidance appended to its description.
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) in the schema's top-level property count.
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 moved into the `items` description (a batch call sets those per item or once at
44
- * the top level through the declared legacy normalization), and batch guidance appended to its description.
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) in the schema's top-level property count.
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 ? ` Each item needs ${requiredFields.join(", ")} (inside each item).` : "";
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: { ...schema.properties, items } },
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 normalizeToolInput({ input, registration });
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 }, options) => Object.hasOwn(input, 'items') || !registration.normalizeInput
260
+ normalizeInput: async ({ input }) => Object.hasOwn(input, 'items')
203
261
  ? { input, notes: [] }
204
- : registration.normalizeInput({ input }, options),
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
@@ -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 supplies a confirmation decision or grants authority. */
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.15",
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",