@net-mesh/core 0.25.2 → 0.27.0-beta.1

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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@net-mesh/core",
3
- "version": "0.25.2",
3
+ "version": "0.27.0-beta.1",
4
4
  "description": "High-performance, schema-agnostic event bus for AI runtime workloads",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -97,13 +97,13 @@
97
97
  "node": ">=20"
98
98
  },
99
99
  "optionalDependencies": {
100
- "@net-mesh/core-win32-x64-msvc": "0.25.2",
101
- "@net-mesh/core-win32-arm64-msvc": "0.25.2",
102
- "@net-mesh/core-darwin-x64": "0.25.2",
103
- "@net-mesh/core-darwin-arm64": "0.25.2",
104
- "@net-mesh/core-linux-x64-gnu": "0.25.2",
105
- "@net-mesh/core-linux-x64-musl": "0.25.2",
106
- "@net-mesh/core-linux-arm64-gnu": "0.25.2",
107
- "@net-mesh/core-linux-arm64-musl": "0.25.2"
100
+ "@net-mesh/core-win32-x64-msvc": "0.27.0-beta.1",
101
+ "@net-mesh/core-win32-arm64-msvc": "0.27.0-beta.1",
102
+ "@net-mesh/core-darwin-x64": "0.27.0-beta.1",
103
+ "@net-mesh/core-darwin-arm64": "0.27.0-beta.1",
104
+ "@net-mesh/core-linux-x64-gnu": "0.27.0-beta.1",
105
+ "@net-mesh/core-linux-x64-musl": "0.27.0-beta.1",
106
+ "@net-mesh/core-linux-arm64-gnu": "0.27.0-beta.1",
107
+ "@net-mesh/core-linux-arm64-musl": "0.27.0-beta.1"
108
108
  }
109
109
  }
package/tool.d.ts ADDED
@@ -0,0 +1,476 @@
1
+ import type { CallOptions, TypedMeshRpc } from './mesh_rpc';
2
+ /**
3
+ * Structural shape of the napi `NetMesh.listTools()` return value
4
+ * — field-for-field the same as [`ToolDescriptor`]. Declared here
5
+ * (instead of imported from `./index`) so this file compiles
6
+ * cleanly before `napi build` regenerates `index.d.ts` with the
7
+ * new `ToolDescriptorJs` export.
8
+ */
9
+ interface MeshWithListTools {
10
+ listTools(): ToolDescriptor[];
11
+ }
12
+ /**
13
+ * Structural shape of the napi `ToolWatchIter` — an async iterator
14
+ * over the substrate `watch_tools` stream. `next()` resolves to one
15
+ * JSON-encoded `ToolListChange`, or `null` when the stream ends /
16
+ * is closed. Declared here so this file compiles before `napi build`
17
+ * regenerates `index.d.ts` with the `ToolWatchIter` export.
18
+ */
19
+ interface NativeToolWatchIter {
20
+ next(): Promise<string | null>;
21
+ close(): void;
22
+ }
23
+ /**
24
+ * Structural shape of the napi `NetMesh.watchTools(intervalMs)`
25
+ * surface consumed by [`watchTools`].
26
+ */
27
+ interface MeshWithWatchTools {
28
+ watchTools(intervalMs?: number | null): Promise<NativeToolWatchIter>;
29
+ }
30
+ /**
31
+ * Structural mirror of the napi `ToolJs` capability-fold entry —
32
+ * declared here, like the shapes above, so this file type-checks
33
+ * before `napi build` regenerates `index.d.ts`. Field-for-field
34
+ * identical to `index.d.ts`'s `ToolJs`.
35
+ */
36
+ interface ToolJs {
37
+ toolId: string;
38
+ name?: string;
39
+ version?: string;
40
+ inputSchema?: string;
41
+ outputSchema?: string;
42
+ requires?: Array<string>;
43
+ estimatedTimeMs?: number;
44
+ stateless?: boolean;
45
+ }
46
+ /**
47
+ * Structural mirror of the napi `CapabilitySetJs`, narrowed to the
48
+ * two fields `addToolCapabilitiesToAnnounce` reads/writes. Every napi
49
+ * `CapabilitySetJs` field is optional, so this narrower shape stays
50
+ * assignable in both directions — a full capability set passes in,
51
+ * and the returned object still flows into `announceCapabilities(...)`.
52
+ */
53
+ interface CapabilitySetJs {
54
+ tags?: Array<string>;
55
+ tools?: Array<ToolJs>;
56
+ }
57
+ /**
58
+ * Discovery shape for an AI tool, as advertised on the capability
59
+ * fold. One row per `(tool_id, version)`; `nodeCount` is filled by
60
+ * the aggregating walk (`list_tools` once it lands).
61
+ *
62
+ * Schemas are stored as JSON-encoded strings (matching the Rust
63
+ * substrate's wire shape). Use `JSON.parse(desc.inputSchema)` to
64
+ * get the parsed JSON Schema object — most consumers want this
65
+ * for lowering into a provider tool definition.
66
+ *
67
+ * Wire-compatible 1:1 with `net::adapter::net::cortex::tool::ToolDescriptor`.
68
+ */
69
+ export interface ToolDescriptor {
70
+ /** nRPC service name. Same string `callTool(...)` takes. */
71
+ toolId: string;
72
+ /** Human-readable name. Defaults to `toolId` if unset. */
73
+ name: string;
74
+ /** Tool version (semver-ish). Defaults to `"1.0.0"`. */
75
+ version: string;
76
+ /** Human-readable description; LLMs read this to decide when to call. */
77
+ description?: string;
78
+ /** JSON-encoded JSON Schema (draft 2020-12) for the request body. */
79
+ inputSchema?: string;
80
+ /** JSON-encoded JSON Schema (draft 2020-12) for the response body. */
81
+ outputSchema?: string;
82
+ /** Required capabilities / dependencies (free-form strings). */
83
+ requires: string[];
84
+ /** Soft latency hint (ms); `0` = no estimate. */
85
+ estimatedTimeMs: number;
86
+ /** True if the tool is a pure function (no session state). */
87
+ stateless: boolean;
88
+ /** True if the tool is server-streaming (uses `serveToolStreaming`). */
89
+ streaming: boolean;
90
+ /** Free-form host-attached tags (e.g. `["web", "research"]`). */
91
+ tags: string[];
92
+ /** How many nodes currently serve this `(toolId, version)`. */
93
+ nodeCount: number;
94
+ }
95
+ /**
96
+ * One envelope on a streaming tool. Discriminated by `type`.
97
+ *
98
+ * Wire-compatible 1:1 with `net::adapter::net::cortex::tool::ToolEvent`
99
+ * (JSON tag form, `{"type": "start", …}` shape).
100
+ *
101
+ * Every stream ends with exactly one terminal event
102
+ * (`type: "result"` or `type: "error"`). Handlers that forget emit
103
+ * a synthesized `{type: "error", code: "missing_terminal", …}` from
104
+ * the Rust SDK's streaming wrapper.
105
+ */
106
+ export type ToolEvent = ToolEventStart | ToolEventProgress | ToolEventDelta | ToolEventResult | ToolEventError;
107
+ export interface ToolEventStart {
108
+ type: 'start';
109
+ toolId: string;
110
+ callId?: number;
111
+ metadata?: unknown;
112
+ }
113
+ export interface ToolEventProgress {
114
+ type: 'progress';
115
+ pct?: number;
116
+ message?: string;
117
+ }
118
+ export interface ToolEventDelta {
119
+ type: 'delta';
120
+ data: unknown;
121
+ }
122
+ export interface ToolEventResult {
123
+ type: 'result';
124
+ data: unknown;
125
+ }
126
+ export interface ToolEventError {
127
+ type: 'error';
128
+ code: string;
129
+ message: string;
130
+ details?: unknown;
131
+ }
132
+ /** True if `event` is a terminal envelope (`result` or `error`). */
133
+ export declare function isTerminalEvent(event: ToolEvent): boolean;
134
+ /**
135
+ * Options for `tool({...})` and `serveTool(rpc, options, handler)`.
136
+ * Mirror of the Rust `ToolMetadataBuilder` shape — caller supplies
137
+ * the fields that don't derive from a type signature in JS (no
138
+ * compile-time type system to introspect, unlike `schemars` in Rust).
139
+ *
140
+ * `inputSchema` / `outputSchema` are JSON-Schema-as-object (caller
141
+ * uses `zod-to-json-schema`, `pydantic`, or hand-rolls); we serialize
142
+ * to a string before stashing on the descriptor.
143
+ */
144
+ export interface ToolOptions {
145
+ /** nRPC service name + tool identifier. Required. */
146
+ name: string;
147
+ /** Human-readable description. Strongly recommended. */
148
+ description?: string;
149
+ /** Version. Defaults to `"1.0.0"`. */
150
+ version?: string;
151
+ /** JSON Schema object for the request. */
152
+ inputSchema?: object;
153
+ /** JSON Schema object for the response. */
154
+ outputSchema?: object;
155
+ /** Required capabilities / dependencies. */
156
+ requires?: string[];
157
+ /** Soft latency hint (ms). */
158
+ estimatedTimeMs?: number;
159
+ /** Pure-function flag. Default `true`. */
160
+ stateless?: boolean;
161
+ /** Free-form tags. */
162
+ tags?: string[];
163
+ }
164
+ /** Construct a [`ToolDescriptor`] from a `ToolOptions` literal. */
165
+ export declare function descriptorFrom(options: ToolOptions): ToolDescriptor;
166
+ /**
167
+ * Handler signature for `serveTool` — receives a decoded request,
168
+ * returns a decoded response (or a Promise of one).
169
+ */
170
+ export type ToolHandler<Req = unknown, Resp = unknown> = (req: Req) => Resp | Promise<Resp>;
171
+ /**
172
+ * Handle returned by `serveTool`. Calling `.close()` deregisters the
173
+ * underlying nRPC handler (mirror of the Rust `ToolServeHandle`'s
174
+ * Drop semantics). Calling `.close()` twice is idempotent; the
175
+ * second call is a no-op.
176
+ *
177
+ * NOTE: v1 does NOT yet integrate with the substrate-side
178
+ * `tool_registry`, so the `ai-tool:<toolId>` capability tag must be
179
+ * added to the caller's announce explicitly. See
180
+ * [`addToolCapabilitiesToAnnounce`] for the convention. Once the
181
+ * napi surface exposes `tool_registry()` insert/remove (a Wave 3
182
+ * follow-up), this handle will atomically reverse both the
183
+ * registry insert and the handler registration on `.close()`.
184
+ */
185
+ export interface ToolServeHandle {
186
+ /** The descriptor under which the tool was registered. */
187
+ readonly descriptor: ToolDescriptor;
188
+ /** Deregister the handler. Idempotent. */
189
+ close(): void;
190
+ }
191
+ /**
192
+ * Register an AI tool against `rpc`. The handler is registered as
193
+ * an nRPC service at `descriptor.toolId` with JSON codec (same as
194
+ * the Rust SDK's `Mesh::serve_tool`).
195
+ *
196
+ * Atomically:
197
+ * 1. Inserts the descriptor into a per-rpc local registry keyed on
198
+ * `toolId`. The next [`fetchToolMetadata`] call against this
199
+ * host can resolve the descriptor by name.
200
+ * 2. Registers the typed handler at `toolId` with JSON codec.
201
+ * 3. On the FIRST `serveTool` call against this rpc, lazy-
202
+ * installs the `tool.metadata.fetch` nRPC service handler so
203
+ * remote agents can pull the full descriptor for any
204
+ * registered tool. Subsequent `serveTool` calls reuse the
205
+ * same fetch handler. Mirrors the Rust SDK's
206
+ * `ensure_tool_metadata_fetch_installed` pattern.
207
+ *
208
+ * The caller is still responsible for announcing the tool to
209
+ * peers — use [`addToolCapabilitiesToAnnounce`] on the
210
+ * `CapabilitySetJs` you pass to `mesh.announceCapabilities(...)`.
211
+ *
212
+ * On `handle.close()`: removes the descriptor from the per-rpc
213
+ * registry and unregisters the handler. The lazy `tool.metadata.fetch`
214
+ * service stays installed for the lifetime of the rpc — harmless
215
+ * when empty (returns NotFound for every request).
216
+ */
217
+ export declare function serveTool<Req = unknown, Resp = unknown>(rpc: TypedMeshRpc, options: ToolOptions, handler: ToolHandler<Req, Resp>): ToolServeHandle;
218
+ /**
219
+ * Streaming-handler shape for `serveToolStreaming`. The handler
220
+ * is an async generator that yields `ToolEvent` envelopes — each
221
+ * yielded event is JSON-encoded and pushed onto the wire via the
222
+ * substrate's response sink. The generator returning normally is
223
+ * the "handler done" signal; if the generator never yields a
224
+ * terminal `result` / `error` envelope, callers see the
225
+ * synthesized `missing_terminal` error (the T-2 contract).
226
+ *
227
+ * Throwing inside the generator maps to a terminal error frame on
228
+ * the wire; the caller's `callToolStreaming` sees it as a normal
229
+ * `ToolEvent` with `type: "error"`.
230
+ */
231
+ export type StreamingToolHandler<Req = unknown> = (req: Req) => AsyncIterable<ToolEvent>;
232
+ /**
233
+ * Register a streaming tool handler. The handler is an async
234
+ * generator that yields ToolEvents — each yielded event is
235
+ * forwarded to the caller via `callToolStreaming` (or any other
236
+ * client that drains a `tool.metadata.fetch`-discoverable
237
+ * streaming service).
238
+ *
239
+ * Atomic register + lazy auto-install of `tool.metadata.fetch` —
240
+ * same pattern as `serveTool` for unary handlers. Stamps
241
+ * `streaming: true` on the descriptor so peers can discover the
242
+ * streaming variant explicitly.
243
+ */
244
+ export declare function serveToolStreaming<Req = unknown>(rpc: TypedMeshRpc, options: ToolOptions, handler: StreamingToolHandler<Req>): ToolServeHandle;
245
+ /**
246
+ * Capability-routed unary tool invocation. Encodes `req` as JSON
247
+ * (the codec every AI provider consumes for tool input/output),
248
+ * dispatches via `rpc.callService(toolId, req, opts)`.
249
+ *
250
+ * Throws `NoRouteError` if no host advertises `nrpc:<toolId>` in
251
+ * the local capability fold; bubbles handler errors as
252
+ * `RpcServerError` with the typed-handler status code.
253
+ */
254
+ export declare function callTool<Req = unknown, Resp = unknown>(rpc: TypedMeshRpc, toolId: string, req: Req, opts?: CallOptions): Promise<Resp>;
255
+ /**
256
+ * Capability-routed streaming tool invocation. Returns an
257
+ * `AsyncIterable<ToolEvent>` — drain via `for await (...)` until
258
+ * the stream terminates. The substrate routes the call via the
259
+ * cap-auth gate just like `callService`; the iterator yields each
260
+ * JSON-decoded `ToolEvent` envelope.
261
+ *
262
+ * Synthesizes a terminal `error` event with code
263
+ * `missing_terminal` if the stream ends without a `result` /
264
+ * `error` envelope — matches the Rust SDK's `serve_tool_streaming`
265
+ * contract and the T-2 cross-language fixture.
266
+ *
267
+ * Cancel mid-stream by aborting `opts.signal` (wired through the
268
+ * substrate's cancel-registry on the underlying RpcStream).
269
+ */
270
+ export declare function callToolStreaming<Req = unknown>(rpc: TypedMeshRpc, toolId: string, req: Req, opts?: CallOptions): AsyncGenerator<ToolEvent, void, void>;
271
+ /**
272
+ * Merge tool descriptors into a `CapabilitySetJs` so the next
273
+ * `mesh.announceCapabilities(caps)` carries:
274
+ *
275
+ * - `ai-tool:<toolId>` tag — peer fold's tag-prefix lookup hits.
276
+ * - A `ToolJs` entry — peer's `list_tools` walk sees the
277
+ * tool's tag-encoded fields.
278
+ *
279
+ * Caller still owns the `caps` object — pass it through
280
+ * `mesh.announceCapabilities(caps)` to publish. Returns the same
281
+ * object for chaining.
282
+ *
283
+ * This is a v1 convenience; once the napi surface exposes
284
+ * `tool_registry()`, the announce-time merge happens
285
+ * automatically and this helper becomes optional.
286
+ */
287
+ export declare function addToolCapabilitiesToAnnounce(caps: CapabilitySetJs, descriptors: ToolDescriptor[]): CapabilitySetJs;
288
+ /**
289
+ * Walk the local capability fold for every published AI tool.
290
+ * Returns one [`ToolDescriptor`] per `(toolId, version)` slot,
291
+ * with `nodeCount` filled in by the aggregating walk.
292
+ *
293
+ * Pure delegation to the napi binding's `NetMesh.listTools()` (B-3
294
+ * of the plan). Requires the napi binding's `tool` Cargo feature
295
+ * (default-on); throws if the underlying mesh wasn't built with it.
296
+ *
297
+ * Schemas come back as JSON-encoded strings on
298
+ * `descriptor.inputSchema` / `descriptor.outputSchema` — call
299
+ * `JSON.parse(...)` for the parsed shape that adapter packages
300
+ * consume when lowering into provider-specific tool definitions.
301
+ */
302
+ export declare function listTools(mesh: MeshWithListTools): ToolDescriptor[];
303
+ /**
304
+ * One change in the set of tools visible to the local capability
305
+ * fold. Discriminated union on `type`; identity for diffing is
306
+ * `(toolId, version)`.
307
+ *
308
+ * Wire-compatible 1:1 with the Rust SDK's `ToolListChange` enum
309
+ * (JSON tag-form `{ "type": "added", "descriptor": {...} }` /
310
+ * `"removed"` / `"node_count_changed"`).
311
+ */
312
+ export type ToolListChange = {
313
+ type: 'added';
314
+ descriptor: ToolDescriptor;
315
+ } | {
316
+ type: 'removed';
317
+ descriptor: ToolDescriptor;
318
+ } | {
319
+ type: 'node_count_changed';
320
+ descriptor: ToolDescriptor;
321
+ prevNodeCount: number;
322
+ };
323
+ /** Options for [`watchTools`]. */
324
+ export interface WatchToolsOptions {
325
+ /**
326
+ * Debounce ceiling in milliseconds — NOT a poll cadence.
327
+ *
328
+ * Omitted / `0` is pure event-driven: a change is delivered the
329
+ * moment the capability fold mutates, and an idle fold does zero
330
+ * periodic work. A positive value additionally guarantees a
331
+ * re-diff at least every `intervalMs` as a safety net.
332
+ */
333
+ intervalMs?: number;
334
+ /**
335
+ * `AbortSignal` to end the watch. Aborting closes the underlying
336
+ * native iterator, which ends the stream promptly.
337
+ */
338
+ signal?: AbortSignal;
339
+ }
340
+ /**
341
+ * Subscribe to a stream of [`ToolListChange`] events for every
342
+ * dynamic addition / removal / publisher-count change in the
343
+ * local capability fold's tool view.
344
+ *
345
+ * Event-driven: consumes the substrate's `MeshNode::watch_tools`
346
+ * stream via the napi `ToolWatchIter` — a change is delivered the
347
+ * moment the fold mutates (latency is bounded by fold-apply, not a
348
+ * timer), and an idle fold does zero periodic work. The diff happens
349
+ * substrate-side; this just `JSON.parse`s each emitted change. No
350
+ * client-side `setTimeout` / `listTools` re-diff loop.
351
+ *
352
+ * The native subscription is kicked off eagerly — when `watchTools`
353
+ * is *called*, not on the first iteration — so a change published
354
+ * between the call and the first `for await` is not lost (the prior
355
+ * version subscribed lazily and could drop that first event). Because
356
+ * the subscription is started at call time, the returned iterable
357
+ * holds a live substrate watch: consume it (or abort via `signal`) so
358
+ * it is closed. Call `listTools(mesh)` once for the starting shape.
359
+ *
360
+ * Mirror of the Rust SDK's `Mesh::watch_tools(matcher, interval)`
361
+ * and the Python `watch_tools` — all three are event-driven off the
362
+ * same substrate change signal, and all three subscribe eagerly.
363
+ *
364
+ * Returns an `AsyncIterable<ToolListChange>` suitable for
365
+ * `for await (const change of watchTools(mesh)) { ... }`. The
366
+ * iterator ends when `options.signal` aborts, when the stream is
367
+ * closed, or on an unrecoverable error.
368
+ */
369
+ export declare function watchTools(mesh: MeshWithWatchTools, options?: WatchToolsOptions): AsyncIterable<ToolListChange>;
370
+ /** nRPC service name for the on-demand tool-descriptor pull. */
371
+ export declare const TOOL_METADATA_FETCH_SERVICE = "tool.metadata.fetch";
372
+ /** Wire-shape variants of `ToolMetadataResponse` (JSON-tagged on
373
+ * `type`, snake_case). Pinned by the substrate's
374
+ * `cortex::tool::ToolMetadataResponse` enum.
375
+ */
376
+ export type ToolMetadataResponse = {
377
+ type: 'found';
378
+ descriptor: ToolDescriptor;
379
+ } | {
380
+ type: 'not_found';
381
+ name: string;
382
+ };
383
+ /**
384
+ * Pull a tool's full descriptor from a specific host by calling
385
+ * the auto-installed `tool.metadata.fetch` nRPC service. Useful
386
+ * when the local fold's capability-fold entry dropped the schema
387
+ * (size-budget-exceeded) and the agent needs the full
388
+ * input/output schemas for strict-mode provider lowering.
389
+ *
390
+ * Mirror of calling `mesh.call_typed(host, TOOL_METADATA_FETCH_SERVICE,
391
+ * { name: tool_id })` in the Rust SDK. The
392
+ * `tool.metadata.fetch` server-side handler is auto-installed on
393
+ * the host's first `serveTool` call.
394
+ */
395
+ export declare function fetchToolMetadata(rpc: TypedMeshRpc, hostNodeId: bigint, toolId: string, opts?: CallOptions): Promise<ToolMetadataResponse>;
396
+ /** Canonical hand-off between a provider adapter and `callTool`. */
397
+ export interface ToolCallSpec {
398
+ /** nRPC tool_id to invoke. */
399
+ name: string;
400
+ /** JSON-encoded arguments to pass to `callTool` (caller parses). */
401
+ argumentsJson: string;
402
+ /** Provider-supplied call id when present (for reply correlation). */
403
+ providerCallId?: string;
404
+ }
405
+ /** Thrown when a provider's tool-call reply doesn't match its spec. */
406
+ export declare class ToolCallParseError extends Error {
407
+ constructor(message: string);
408
+ }
409
+ /** OpenAI Chat Completions / Responses API `tools` array. */
410
+ export declare const openai: {
411
+ /**
412
+ * Lower a descriptor to an OpenAI tool definition. Shape:
413
+ * ```
414
+ * { type: "function", function: { name, description, parameters, strict } }
415
+ * ```
416
+ * `strict` is true when the descriptor carried an `inputSchema`.
417
+ */
418
+ toOpenaiTool(desc: ToolDescriptor): object;
419
+ /**
420
+ * Parse one OpenAI `tool_calls[]` entry into a `ToolCallSpec`.
421
+ * OpenAI's `function.arguments` is a JSON-encoded STRING; this
422
+ * helper validates it parses up front so malformed payloads fail
423
+ * fast instead of riding through `callTool`.
424
+ */
425
+ lowerOpenaiToolCall(call: Record<string, unknown>): ToolCallSpec;
426
+ };
427
+ /** Anthropic Messages API `tools` array + `tool_use` content blocks. */
428
+ export declare const anthropic: {
429
+ /**
430
+ * Lower a descriptor to an Anthropic tool definition. Shape:
431
+ * ```
432
+ * { name, description, input_schema }
433
+ * ```
434
+ * No tool-level `strict` flag — Anthropic relies on schema-
435
+ * validated tool input as the default.
436
+ */
437
+ toAnthropicTool(desc: ToolDescriptor): object;
438
+ /**
439
+ * Parse one Anthropic `tool_use` content block into a
440
+ * `ToolCallSpec`. `input` is already a parsed object (not a
441
+ * string like OpenAI); re-serializes once to preserve the
442
+ * `argumentsJson: string` invariant.
443
+ */
444
+ lowerAnthropicToolUse(block: Record<string, unknown>): ToolCallSpec;
445
+ };
446
+ /** Model Context Protocol `tools/list` + `tools/call`. */
447
+ export declare const mcp: {
448
+ /** Lower a descriptor to an MCP tool definition. Shape: `{ name, description, inputSchema }` (camelCase). */
449
+ toMcpTool(desc: ToolDescriptor): object;
450
+ /**
451
+ * Parse an MCP `tools/call` request's `params` into a
452
+ * `ToolCallSpec`. `providerCallId` is left `undefined` — MCP's
453
+ * JSON-RPC `id` lives one envelope layer up, threaded
454
+ * independently.
455
+ */
456
+ lowerMcpToolsCall(params: Record<string, unknown>): ToolCallSpec;
457
+ };
458
+ /** Gemini `generateContent` function-calling shape. */
459
+ export declare const gemini: {
460
+ /**
461
+ * Lower a descriptor to one Gemini `FunctionDeclaration`. Shape:
462
+ * ```
463
+ * { name, description, parameters }
464
+ * ```
465
+ * Caller wraps these into the outer
466
+ * `tools: [{ function_declarations: [ … ] }]` array.
467
+ */
468
+ toGeminiFunctionDeclaration(desc: ToolDescriptor): object;
469
+ /**
470
+ * Parse one Gemini `functionCall` part into a `ToolCallSpec`.
471
+ * Gemini has no per-call id; the spec leaves `providerCallId`
472
+ * `undefined` (multi-call sequences are positional).
473
+ */
474
+ lowerGeminiFunctionCall(call: Record<string, unknown>): ToolCallSpec;
475
+ };
476
+ export {};
package/tool.js ADDED
@@ -0,0 +1,554 @@
1
+ "use strict";
2
+ // TypeScript layer for AI tool calling on net.
3
+ //
4
+ // Wraps the existing `TypedMeshRpc` napi surface with the `tool()` /
5
+ // `callTool()` ergonomic helpers + format translators that lower
6
+ // `ToolDescriptor`s to OpenAI / Anthropic / MCP / Gemini tool shapes
7
+ // and parse provider tool-call replies back into nRPC dispatches.
8
+ //
9
+ // This is the Wave 3 / B-1 + B-4 starting point. v1 covers unary
10
+ // register + invoke + format conversion. Streaming (B-2) and
11
+ // discovery (B-3 list_tools / watch_tools) follow once the
12
+ // underlying napi surface exposes them; today the only available
13
+ // streaming primitive is direct-addressed (`callStreaming(nodeId,
14
+ // ...)`), so capability-routed streaming has to wait on a
15
+ // `callServiceStreaming` TS wrapper or a `findServiceNodes` +
16
+ // direct-call composition (TODO).
17
+ //
18
+ // Plan: see
19
+ // `crates/net/docs/plans/NRPC_AI_TOOL_CALLING_AND_AGENT_DX.md`,
20
+ // slices B-1 / B-2 / B-4. Mirror of the Rust SDK's
21
+ // `net_sdk::tool` + `net_sdk::tool::formats` modules — cross-
22
+ // language tests (T-1) will pin byte equality across both.
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.gemini = exports.mcp = exports.anthropic = exports.openai = exports.ToolCallParseError = exports.TOOL_METADATA_FETCH_SERVICE = void 0;
25
+ exports.isTerminalEvent = isTerminalEvent;
26
+ exports.descriptorFrom = descriptorFrom;
27
+ exports.serveTool = serveTool;
28
+ exports.serveToolStreaming = serveToolStreaming;
29
+ exports.callTool = callTool;
30
+ exports.callToolStreaming = callToolStreaming;
31
+ exports.addToolCapabilitiesToAnnounce = addToolCapabilitiesToAnnounce;
32
+ exports.listTools = listTools;
33
+ exports.watchTools = watchTools;
34
+ exports.fetchToolMetadata = fetchToolMetadata;
35
+ /** True if `event` is a terminal envelope (`result` or `error`). */
36
+ function isTerminalEvent(event) {
37
+ return event.type === 'result' || event.type === 'error';
38
+ }
39
+ /** Construct a [`ToolDescriptor`] from a `ToolOptions` literal. */
40
+ function descriptorFrom(options) {
41
+ return {
42
+ toolId: options.name,
43
+ name: options.name,
44
+ version: options.version ?? '1.0.0',
45
+ description: options.description,
46
+ inputSchema: options.inputSchema ? JSON.stringify(options.inputSchema) : undefined,
47
+ outputSchema: options.outputSchema ? JSON.stringify(options.outputSchema) : undefined,
48
+ requires: options.requires ?? [],
49
+ estimatedTimeMs: options.estimatedTimeMs ?? 0,
50
+ stateless: options.stateless ?? true,
51
+ streaming: false,
52
+ tags: options.tags ?? [],
53
+ nodeCount: 0,
54
+ };
55
+ }
56
+ const _toolRegistries = new WeakMap();
57
+ function _ensureFetchInstalled(rpc) {
58
+ let entry = _toolRegistries.get(rpc);
59
+ if (entry)
60
+ return entry;
61
+ entry = { registry: new Map(), fetchHandle: null };
62
+ _toolRegistries.set(rpc, entry);
63
+ // Register the fetch handler. The handler queries `entry.registry`
64
+ // for the name; falls back to NotFound. Mirrors the Rust SDK's
65
+ // `tool.metadata.fetch` handler shape.
66
+ try {
67
+ entry.fetchHandle = rpc.serve(exports.TOOL_METADATA_FETCH_SERVICE, (req) => {
68
+ const d = entry.registry.get(req.name);
69
+ return d
70
+ ? { type: 'found', descriptor: d }
71
+ : { type: 'not_found', name: req.name };
72
+ });
73
+ }
74
+ catch {
75
+ // If install fails (e.g. another caller already registered the
76
+ // service manually), leave fetchHandle null. Subsequent serveTool
77
+ // calls retry. Silent because the failure is recoverable +
78
+ // observable via `fetchToolMetadata` returning NoRoute / NotFound
79
+ // on the agent side.
80
+ }
81
+ return entry;
82
+ }
83
+ /**
84
+ * Register an AI tool against `rpc`. The handler is registered as
85
+ * an nRPC service at `descriptor.toolId` with JSON codec (same as
86
+ * the Rust SDK's `Mesh::serve_tool`).
87
+ *
88
+ * Atomically:
89
+ * 1. Inserts the descriptor into a per-rpc local registry keyed on
90
+ * `toolId`. The next [`fetchToolMetadata`] call against this
91
+ * host can resolve the descriptor by name.
92
+ * 2. Registers the typed handler at `toolId` with JSON codec.
93
+ * 3. On the FIRST `serveTool` call against this rpc, lazy-
94
+ * installs the `tool.metadata.fetch` nRPC service handler so
95
+ * remote agents can pull the full descriptor for any
96
+ * registered tool. Subsequent `serveTool` calls reuse the
97
+ * same fetch handler. Mirrors the Rust SDK's
98
+ * `ensure_tool_metadata_fetch_installed` pattern.
99
+ *
100
+ * The caller is still responsible for announcing the tool to
101
+ * peers — use [`addToolCapabilitiesToAnnounce`] on the
102
+ * `CapabilitySetJs` you pass to `mesh.announceCapabilities(...)`.
103
+ *
104
+ * On `handle.close()`: removes the descriptor from the per-rpc
105
+ * registry and unregisters the handler. The lazy `tool.metadata.fetch`
106
+ * service stays installed for the lifetime of the rpc — harmless
107
+ * when empty (returns NotFound for every request).
108
+ */
109
+ function serveTool(rpc, options, handler) {
110
+ const descriptor = descriptorFrom(options);
111
+ const entry = _ensureFetchInstalled(rpc);
112
+ entry.registry.set(descriptor.toolId, descriptor);
113
+ const inner = rpc.serve(descriptor.toolId, handler);
114
+ let closed = false;
115
+ return {
116
+ descriptor,
117
+ close() {
118
+ if (closed)
119
+ return;
120
+ closed = true;
121
+ entry.registry.delete(descriptor.toolId);
122
+ inner.close();
123
+ },
124
+ };
125
+ }
126
+ /**
127
+ * Register a streaming tool handler. The handler is an async
128
+ * generator that yields ToolEvents — each yielded event is
129
+ * forwarded to the caller via `callToolStreaming` (or any other
130
+ * client that drains a `tool.metadata.fetch`-discoverable
131
+ * streaming service).
132
+ *
133
+ * Atomic register + lazy auto-install of `tool.metadata.fetch` —
134
+ * same pattern as `serveTool` for unary handlers. Stamps
135
+ * `streaming: true` on the descriptor so peers can discover the
136
+ * streaming variant explicitly.
137
+ */
138
+ function serveToolStreaming(rpc, options, handler) {
139
+ const baseDescriptor = descriptorFrom(options);
140
+ const descriptor = { ...baseDescriptor, streaming: true };
141
+ const entry = _ensureFetchInstalled(rpc);
142
+ entry.registry.set(descriptor.toolId, descriptor);
143
+ const inner = rpc.serveStreaming(descriptor.toolId, async (req, sink) => {
144
+ let sawTerminal = false;
145
+ try {
146
+ for await (const event of handler(req)) {
147
+ sink.send(event);
148
+ if (isTerminalEvent(event))
149
+ sawTerminal = true;
150
+ }
151
+ if (!sawTerminal) {
152
+ sink.send({
153
+ type: 'error',
154
+ code: 'missing_terminal',
155
+ message: 'tool stream ended without a terminal result or error envelope',
156
+ });
157
+ }
158
+ }
159
+ catch (err) {
160
+ // Convert handler exceptions into a terminal error envelope
161
+ // so the caller sees a typed error rather than relying on
162
+ // the client-side missing_terminal fallback.
163
+ const message = err instanceof Error ? err.message : String(err);
164
+ const errEvent = {
165
+ type: 'error',
166
+ code: 'handler_error',
167
+ message,
168
+ };
169
+ sink.send(errEvent);
170
+ }
171
+ });
172
+ let closed = false;
173
+ return {
174
+ descriptor,
175
+ close() {
176
+ if (closed)
177
+ return;
178
+ closed = true;
179
+ entry.registry.delete(descriptor.toolId);
180
+ inner.close();
181
+ },
182
+ };
183
+ }
184
+ /**
185
+ * Capability-routed unary tool invocation. Encodes `req` as JSON
186
+ * (the codec every AI provider consumes for tool input/output),
187
+ * dispatches via `rpc.callService(toolId, req, opts)`.
188
+ *
189
+ * Throws `NoRouteError` if no host advertises `nrpc:<toolId>` in
190
+ * the local capability fold; bubbles handler errors as
191
+ * `RpcServerError` with the typed-handler status code.
192
+ */
193
+ async function callTool(rpc, toolId, req, opts) {
194
+ return rpc.callService(toolId, req, opts);
195
+ }
196
+ /**
197
+ * Capability-routed streaming tool invocation. Returns an
198
+ * `AsyncIterable<ToolEvent>` — drain via `for await (...)` until
199
+ * the stream terminates. The substrate routes the call via the
200
+ * cap-auth gate just like `callService`; the iterator yields each
201
+ * JSON-decoded `ToolEvent` envelope.
202
+ *
203
+ * Synthesizes a terminal `error` event with code
204
+ * `missing_terminal` if the stream ends without a `result` /
205
+ * `error` envelope — matches the Rust SDK's `serve_tool_streaming`
206
+ * contract and the T-2 cross-language fixture.
207
+ *
208
+ * Cancel mid-stream by aborting `opts.signal` (wired through the
209
+ * substrate's cancel-registry on the underlying RpcStream).
210
+ */
211
+ async function* callToolStreaming(rpc, toolId, req, opts) {
212
+ const stream = await rpc.callServiceStreaming(toolId, req, opts);
213
+ let sawTerminal = false;
214
+ try {
215
+ for await (const event of stream) {
216
+ yield event;
217
+ if (isTerminalEvent(event)) {
218
+ sawTerminal = true;
219
+ }
220
+ }
221
+ }
222
+ finally {
223
+ await stream.close().catch(() => { });
224
+ }
225
+ if (!sawTerminal) {
226
+ const synthesized = {
227
+ type: 'error',
228
+ code: 'missing_terminal',
229
+ message: 'tool stream ended without a terminal result or error envelope',
230
+ };
231
+ yield synthesized;
232
+ }
233
+ }
234
+ /**
235
+ * Merge tool descriptors into a `CapabilitySetJs` so the next
236
+ * `mesh.announceCapabilities(caps)` carries:
237
+ *
238
+ * - `ai-tool:<toolId>` tag — peer fold's tag-prefix lookup hits.
239
+ * - A `ToolJs` entry — peer's `list_tools` walk sees the
240
+ * tool's tag-encoded fields.
241
+ *
242
+ * Caller still owns the `caps` object — pass it through
243
+ * `mesh.announceCapabilities(caps)` to publish. Returns the same
244
+ * object for chaining.
245
+ *
246
+ * This is a v1 convenience; once the napi surface exposes
247
+ * `tool_registry()`, the announce-time merge happens
248
+ * automatically and this helper becomes optional.
249
+ */
250
+ function addToolCapabilitiesToAnnounce(caps, descriptors) {
251
+ if (descriptors.length === 0)
252
+ return caps;
253
+ const tags = new Set(caps.tags ?? []);
254
+ const tools = [...(caps.tools ?? [])];
255
+ for (const desc of descriptors) {
256
+ tags.add(`ai-tool:${desc.toolId}`);
257
+ tools.push({
258
+ toolId: desc.toolId,
259
+ name: desc.name,
260
+ version: desc.version,
261
+ inputSchema: desc.inputSchema,
262
+ outputSchema: desc.outputSchema,
263
+ requires: desc.requires,
264
+ estimatedTimeMs: desc.estimatedTimeMs,
265
+ stateless: desc.stateless,
266
+ });
267
+ }
268
+ caps.tags = Array.from(tags);
269
+ caps.tools = tools;
270
+ return caps;
271
+ }
272
+ /**
273
+ * Walk the local capability fold for every published AI tool.
274
+ * Returns one [`ToolDescriptor`] per `(toolId, version)` slot,
275
+ * with `nodeCount` filled in by the aggregating walk.
276
+ *
277
+ * Pure delegation to the napi binding's `NetMesh.listTools()` (B-3
278
+ * of the plan). Requires the napi binding's `tool` Cargo feature
279
+ * (default-on); throws if the underlying mesh wasn't built with it.
280
+ *
281
+ * Schemas come back as JSON-encoded strings on
282
+ * `descriptor.inputSchema` / `descriptor.outputSchema` — call
283
+ * `JSON.parse(...)` for the parsed shape that adapter packages
284
+ * consume when lowering into provider-specific tool definitions.
285
+ */
286
+ function listTools(mesh) {
287
+ return mesh.listTools();
288
+ }
289
+ /**
290
+ * Subscribe to a stream of [`ToolListChange`] events for every
291
+ * dynamic addition / removal / publisher-count change in the
292
+ * local capability fold's tool view.
293
+ *
294
+ * Event-driven: consumes the substrate's `MeshNode::watch_tools`
295
+ * stream via the napi `ToolWatchIter` — a change is delivered the
296
+ * moment the fold mutates (latency is bounded by fold-apply, not a
297
+ * timer), and an idle fold does zero periodic work. The diff happens
298
+ * substrate-side; this just `JSON.parse`s each emitted change. No
299
+ * client-side `setTimeout` / `listTools` re-diff loop.
300
+ *
301
+ * The native subscription is kicked off eagerly — when `watchTools`
302
+ * is *called*, not on the first iteration — so a change published
303
+ * between the call and the first `for await` is not lost (the prior
304
+ * version subscribed lazily and could drop that first event). Because
305
+ * the subscription is started at call time, the returned iterable
306
+ * holds a live substrate watch: consume it (or abort via `signal`) so
307
+ * it is closed. Call `listTools(mesh)` once for the starting shape.
308
+ *
309
+ * Mirror of the Rust SDK's `Mesh::watch_tools(matcher, interval)`
310
+ * and the Python `watch_tools` — all three are event-driven off the
311
+ * same substrate change signal, and all three subscribe eagerly.
312
+ *
313
+ * Returns an `AsyncIterable<ToolListChange>` suitable for
314
+ * `for await (const change of watchTools(mesh)) { ... }`. The
315
+ * iterator ends when `options.signal` aborts, when the stream is
316
+ * closed, or on an unrecoverable error.
317
+ */
318
+ function watchTools(mesh, options = {}) {
319
+ const intervalMs = options.intervalMs;
320
+ const signal = options.signal;
321
+ // Subscribe eagerly — at call time, not on the first iteration — so a
322
+ // change published before iteration begins is still observed. The
323
+ // generator below awaits this same promise; the extra no-op `.catch`
324
+ // keeps an unhandled-rejection warning from firing if the returned
325
+ // iterable is created but never consumed (the generator's own `await`
326
+ // still surfaces the real rejection to a consumer that does iterate).
327
+ const nativePromise = mesh.watchTools(intervalMs ?? null);
328
+ void nativePromise.catch(() => { });
329
+ async function* iterator() {
330
+ const native = await nativePromise;
331
+ const onAbort = () => native.close();
332
+ if (signal) {
333
+ if (signal.aborted) {
334
+ native.close();
335
+ return;
336
+ }
337
+ signal.addEventListener('abort', onAbort, { once: true });
338
+ }
339
+ try {
340
+ while (true) {
341
+ const raw = await native.next();
342
+ if (raw == null) {
343
+ return;
344
+ }
345
+ yield JSON.parse(raw);
346
+ }
347
+ }
348
+ finally {
349
+ if (signal) {
350
+ signal.removeEventListener('abort', onAbort);
351
+ }
352
+ native.close();
353
+ }
354
+ }
355
+ return { [Symbol.asyncIterator]: iterator };
356
+ }
357
+ /** nRPC service name for the on-demand tool-descriptor pull. */
358
+ exports.TOOL_METADATA_FETCH_SERVICE = 'tool.metadata.fetch';
359
+ /**
360
+ * Pull a tool's full descriptor from a specific host by calling
361
+ * the auto-installed `tool.metadata.fetch` nRPC service. Useful
362
+ * when the local fold's capability-fold entry dropped the schema
363
+ * (size-budget-exceeded) and the agent needs the full
364
+ * input/output schemas for strict-mode provider lowering.
365
+ *
366
+ * Mirror of calling `mesh.call_typed(host, TOOL_METADATA_FETCH_SERVICE,
367
+ * { name: tool_id })` in the Rust SDK. The
368
+ * `tool.metadata.fetch` server-side handler is auto-installed on
369
+ * the host's first `serveTool` call.
370
+ */
371
+ async function fetchToolMetadata(rpc, hostNodeId, toolId, opts) {
372
+ return rpc.call(hostNodeId, exports.TOOL_METADATA_FETCH_SERVICE, { name: toolId }, opts);
373
+ }
374
+ /** Thrown when a provider's tool-call reply doesn't match its spec. */
375
+ class ToolCallParseError extends Error {
376
+ constructor(message) {
377
+ super(message);
378
+ this.name = 'ToolCallParseError';
379
+ }
380
+ }
381
+ exports.ToolCallParseError = ToolCallParseError;
382
+ function inputSchemaValue(desc) {
383
+ if (!desc.inputSchema)
384
+ return { type: 'object', properties: {} };
385
+ try {
386
+ return JSON.parse(desc.inputSchema);
387
+ }
388
+ catch {
389
+ // Schema string was malformed (shouldn't happen for descriptors
390
+ // built via `descriptorFrom`). Empty-object fallback keeps
391
+ // provider validators happy.
392
+ return { type: 'object', properties: {} };
393
+ }
394
+ }
395
+ /** OpenAI Chat Completions / Responses API `tools` array. */
396
+ exports.openai = {
397
+ /**
398
+ * Lower a descriptor to an OpenAI tool definition. Shape:
399
+ * ```
400
+ * { type: "function", function: { name, description, parameters, strict } }
401
+ * ```
402
+ * `strict` is true when the descriptor carried an `inputSchema`.
403
+ */
404
+ toOpenaiTool(desc) {
405
+ return {
406
+ type: 'function',
407
+ function: {
408
+ name: desc.toolId,
409
+ description: desc.description ?? '',
410
+ parameters: inputSchemaValue(desc),
411
+ strict: desc.inputSchema !== undefined,
412
+ },
413
+ };
414
+ },
415
+ /**
416
+ * Parse one OpenAI `tool_calls[]` entry into a `ToolCallSpec`.
417
+ * OpenAI's `function.arguments` is a JSON-encoded STRING; this
418
+ * helper validates it parses up front so malformed payloads fail
419
+ * fast instead of riding through `callTool`.
420
+ */
421
+ lowerOpenaiToolCall(call) {
422
+ const fn = call['function'];
423
+ if (!fn)
424
+ throw new ToolCallParseError('tool-call reply missing field `function`');
425
+ const name = fn['name'];
426
+ if (typeof name !== 'string') {
427
+ throw new ToolCallParseError('tool-call reply field `function.name` must be a string');
428
+ }
429
+ const argumentsField = fn['arguments'];
430
+ if (typeof argumentsField !== 'string') {
431
+ throw new ToolCallParseError('tool-call reply field `function.arguments` must be a JSON-encoded string');
432
+ }
433
+ try {
434
+ JSON.parse(argumentsField);
435
+ }
436
+ catch (e) {
437
+ throw new ToolCallParseError(`tool-call arguments were not valid JSON: ${e.message}`);
438
+ }
439
+ const id = call['id'];
440
+ return {
441
+ name,
442
+ argumentsJson: argumentsField,
443
+ providerCallId: typeof id === 'string' ? id : undefined,
444
+ };
445
+ },
446
+ };
447
+ /** Anthropic Messages API `tools` array + `tool_use` content blocks. */
448
+ exports.anthropic = {
449
+ /**
450
+ * Lower a descriptor to an Anthropic tool definition. Shape:
451
+ * ```
452
+ * { name, description, input_schema }
453
+ * ```
454
+ * No tool-level `strict` flag — Anthropic relies on schema-
455
+ * validated tool input as the default.
456
+ */
457
+ toAnthropicTool(desc) {
458
+ return {
459
+ name: desc.toolId,
460
+ description: desc.description ?? '',
461
+ input_schema: inputSchemaValue(desc),
462
+ };
463
+ },
464
+ /**
465
+ * Parse one Anthropic `tool_use` content block into a
466
+ * `ToolCallSpec`. `input` is already a parsed object (not a
467
+ * string like OpenAI); re-serializes once to preserve the
468
+ * `argumentsJson: string` invariant.
469
+ */
470
+ lowerAnthropicToolUse(block) {
471
+ const name = block['name'];
472
+ if (typeof name !== 'string') {
473
+ throw new ToolCallParseError('tool_use block field `name` must be a string');
474
+ }
475
+ if (!('input' in block)) {
476
+ throw new ToolCallParseError('tool_use block missing field `input`');
477
+ }
478
+ const argumentsJson = JSON.stringify(block['input']);
479
+ const id = block['id'];
480
+ return {
481
+ name,
482
+ argumentsJson,
483
+ providerCallId: typeof id === 'string' ? id : undefined,
484
+ };
485
+ },
486
+ };
487
+ /** Model Context Protocol `tools/list` + `tools/call`. */
488
+ exports.mcp = {
489
+ /** Lower a descriptor to an MCP tool definition. Shape: `{ name, description, inputSchema }` (camelCase). */
490
+ toMcpTool(desc) {
491
+ return {
492
+ name: desc.toolId,
493
+ description: desc.description ?? '',
494
+ inputSchema: inputSchemaValue(desc),
495
+ };
496
+ },
497
+ /**
498
+ * Parse an MCP `tools/call` request's `params` into a
499
+ * `ToolCallSpec`. `providerCallId` is left `undefined` — MCP's
500
+ * JSON-RPC `id` lives one envelope layer up, threaded
501
+ * independently.
502
+ */
503
+ lowerMcpToolsCall(params) {
504
+ const name = params['name'];
505
+ if (typeof name !== 'string') {
506
+ throw new ToolCallParseError('tools/call params field `name` must be a string');
507
+ }
508
+ if (!('arguments' in params)) {
509
+ throw new ToolCallParseError('tools/call params missing field `arguments`');
510
+ }
511
+ return {
512
+ name,
513
+ argumentsJson: JSON.stringify(params['arguments']),
514
+ providerCallId: undefined,
515
+ };
516
+ },
517
+ };
518
+ /** Gemini `generateContent` function-calling shape. */
519
+ exports.gemini = {
520
+ /**
521
+ * Lower a descriptor to one Gemini `FunctionDeclaration`. Shape:
522
+ * ```
523
+ * { name, description, parameters }
524
+ * ```
525
+ * Caller wraps these into the outer
526
+ * `tools: [{ function_declarations: [ … ] }]` array.
527
+ */
528
+ toGeminiFunctionDeclaration(desc) {
529
+ return {
530
+ name: desc.toolId,
531
+ description: desc.description ?? '',
532
+ parameters: inputSchemaValue(desc),
533
+ };
534
+ },
535
+ /**
536
+ * Parse one Gemini `functionCall` part into a `ToolCallSpec`.
537
+ * Gemini has no per-call id; the spec leaves `providerCallId`
538
+ * `undefined` (multi-call sequences are positional).
539
+ */
540
+ lowerGeminiFunctionCall(call) {
541
+ const name = call['name'];
542
+ if (typeof name !== 'string') {
543
+ throw new ToolCallParseError('functionCall field `name` must be a string');
544
+ }
545
+ if (!('args' in call)) {
546
+ throw new ToolCallParseError('functionCall missing field `args`');
547
+ }
548
+ return {
549
+ name,
550
+ argumentsJson: JSON.stringify(call['args']),
551
+ providerCallId: undefined,
552
+ };
553
+ },
554
+ };