@jini-ai/core 0.6.15 → 0.6.16

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,17 @@
1
1
  # @jini-ai/core
2
2
 
3
+ ## 0.6.16
4
+
5
+ ### Patch Changes
6
+
7
+ - Remove the GitHub Copilot CLI agent: its def, `createCopilotStreamHandler`/`CopilotStreamEvent`,
8
+ account model discovery and the `@github/copilot-sdk` optional peer, the MCP installer's `copilot`
9
+ slug, and its icon. A saved `copilot` agent id no longer resolves (`getAgentDef` returns `null`).
10
+ CopilotKit/AG-UI is unrelated and unchanged.
11
+
12
+ Drop peers no runtime entry loads: `express` from core and http-kit (type-only imports; type
13
+ consumers keep `@types/express`), `vue` from ui-kit, `react-dom` from vibecoding.
14
+
3
15
  ## 0.6.15
4
16
 
5
17
  ### 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 top-level `required` fields {@link batchDescriptorFor} moved into its `items` description.
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(n) in the items description length; 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,
@@ -16,6 +16,22 @@ 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
+ /** The top-level `required` fields {@link batchDescriptorFor} moved into its `items` description.
22
+ * A batch schema declares no top-level `required`, so a discovery signature built from it alone
23
+ * makes a single call look argument-free and the model sends `{}` — this recovers them.
24
+ * @returns The moved field names in order; [] for a non-batch schema or a tool that required none.
25
+ * @complexity O(n) in the items description length; pure. */
26
+ export function batchItemRequiredFields({ inputSchema }, _optional = {}) {
27
+ const items = isPlainObject(inputSchema) && isPlainObject(inputSchema.properties) ? inputSchema.properties.items : undefined;
28
+ const text = isPlainObject(items) && typeof items.description === "string" ? items.description : "";
29
+ const start = text.indexOf(ITEM_NEEDS);
30
+ const end = text.indexOf(ITEM_NEEDS_END, start);
31
+ if (start === -1 || end === -1)
32
+ return [];
33
+ return text.slice(start + ITEM_NEEDS.length, end).split(", ");
34
+ }
19
35
  /** Used when a {@link BatchableTool} names no `maxItems`. */
20
36
  export const DEFAULT_MAX_BATCH_ITEMS = 25;
21
37
  /** Classify a batch result, including legacy envelopes, without dropping per-item evidence.
@@ -62,7 +78,7 @@ export function batchDescriptorFor({ descriptor, tool }, _optional = {}) {
62
78
  const requiredFields = schema.required ?? [];
63
79
  const rest = { ...schema };
64
80
  delete rest.required;
65
- const needs = requiredFields.length > 0 ? ` Each item needs ${requiredFields.join(", ")} (inside each item).` : "";
81
+ const needs = requiredFields.length > 0 ? `${ITEM_NEEDS}${requiredFields.join(", ")}${ITEM_NEEDS_END}` : "";
66
82
  const items = {
67
83
  type: "array",
68
84
  minItems: 1,
@@ -111,6 +127,38 @@ async function normalizeBatchItem({ registration, defaults, shared, item }) {
111
127
  const normalized = await normalizeToolInput({ input, registration });
112
128
  return { input: normalized.input, notes: [...layers.flatMap(layer => layer.notes), ...normalized.notes] };
113
129
  }
130
+ /** Restore the single-call requirement the published batch schema intentionally omits, from the
131
+ * wrapped tool's own schema. Normalize first so repaired spellings and domain aliases satisfy it.
132
+ * @throws ToolInputError with every required name/type when any required value is missing.
133
+ * @complexity O(r) for r required fields, plus the existing normalization; no effects.
134
+ */
135
+ async function normalizeSingleBatchInput({ input, registration }) {
136
+ const normalized = await normalizeToolInput({ input, registration });
137
+ const value = normalized.input;
138
+ const schema = registration.descriptor.inputSchema;
139
+ const required = schema.required ?? [];
140
+ if (!isPlainObject(value) || required.every(name => value[name] !== undefined))
141
+ return normalized;
142
+ const missing = required.filter(name => value[name] === undefined);
143
+ const fields = required.map(name => `${name} (${schemaTypeLabel(schema.properties?.[name])})`);
144
+ throw new ToolInputError({ message: `${registration.descriptor.id}: missing required inputs: ${missing.join(', ')}. Required inputs: ${fields.join(', ')}.` });
145
+ }
146
+ /** A property's declared JSON type, `a | b` for a type list, `unknown` when undeclared. */
147
+ function schemaTypeLabel(property) {
148
+ const type = isPlainObject(property) ? property.type : undefined;
149
+ if (Array.isArray(type))
150
+ return type.join(' | ');
151
+ return typeof type === 'string' ? type : 'unknown';
152
+ }
153
+ /** A batch result is one JSON value, so an item's MCP `image` block would reach the model and the
154
+ * chat as base64 text (a 20-item import batch, 20 previews). Drop images from item envelopes; the
155
+ * item's text blocks still carry its result. Anything that is not an envelope is returned as is. */
156
+ function withoutItemImages(output) {
157
+ if (!isPlainObject(output) || !Array.isArray(output.content))
158
+ return output;
159
+ const content = output.content.filter((block) => !isPlainObject(block) || block.type !== 'image');
160
+ return content.length === output.content.length ? output : { ...output, content };
161
+ }
114
162
  /** Classify one returned output without dropping error payloads or normalization notes.
115
163
  * @complexity O(n) for a nested batch's n results, plus formatting its notes; no effects.
116
164
  */
@@ -158,7 +206,7 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
158
206
  // its policy and planner receive the same targets; never execute items to preflight them.
159
207
  normalizeInput: async ({ input }) => {
160
208
  if (!Object.hasOwn(input, 'items'))
161
- return normalizeToolInput({ input, registration });
209
+ return normalizeSingleBatchInput({ input, registration });
162
210
  const { items: rawItems, ...shared } = input;
163
211
  const items = readItems({ items: rawItems, toolId, maxItems });
164
212
  const normalized = [];
@@ -189,7 +237,7 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
189
237
  if (!options?.invokeTool)
190
238
  throw new Error(`${toolId}: batch items require the gated ToolExecutor invocation port`);
191
239
  const output = await options.invokeTool({ toolId, input: normalized.input }, { registry: itemRegistry });
192
- results.push(batchItemResult({ index, output, notes: normalized.notes }));
240
+ results.push(batchItemResult({ index, output: withoutItemImages(output), notes: normalized.notes }));
193
241
  }
194
242
  catch (error) {
195
243
  results.push({ index, ok: false, error: withNormalizationNotes({ output: itemErrorMessage({ error, toolId }), notes }) });
@@ -199,9 +247,9 @@ export function withBatchInput({ registration, tool }, { mode = 'items' } = {})
199
247
  };
200
248
  return { ...registration, descriptor, handler,
201
249
  // Domain normalization belongs to each merged item, not the incomplete shared layer.
202
- normalizeInput: async ({ input }, options) => Object.hasOwn(input, 'items') || !registration.normalizeInput
250
+ normalizeInput: async ({ input }) => Object.hasOwn(input, 'items')
203
251
  ? { input, notes: [] }
204
- : registration.normalizeInput({ input }, options),
252
+ : normalizeSingleBatchInput({ input, registration }),
205
253
  };
206
254
  }
207
255
  /**
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.16",
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",