@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.
- package/net.darwin-arm64.node +0 -0
- package/net.darwin-x64.node +0 -0
- package/net.linux-arm64-gnu.node +0 -0
- package/net.linux-arm64-musl.node +0 -0
- package/net.linux-x64-gnu.node +0 -0
- package/net.linux-x64-musl.node +0 -0
- package/net.win32-arm64-msvc.node +0 -0
- package/net.win32-x64-msvc.node +0 -0
- package/package.json +9 -9
- package/tool.d.ts +476 -0
- package/tool.js +554 -0
package/net.darwin-arm64.node
CHANGED
|
Binary file
|
package/net.darwin-x64.node
CHANGED
|
Binary file
|
package/net.linux-arm64-gnu.node
CHANGED
|
Binary file
|
|
Binary file
|
package/net.linux-x64-gnu.node
CHANGED
|
Binary file
|
package/net.linux-x64-musl.node
CHANGED
|
Binary file
|
|
Binary file
|
package/net.win32-x64-msvc.node
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@net-mesh/core",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
101
|
-
"@net-mesh/core-win32-arm64-msvc": "0.
|
|
102
|
-
"@net-mesh/core-darwin-x64": "0.
|
|
103
|
-
"@net-mesh/core-darwin-arm64": "0.
|
|
104
|
-
"@net-mesh/core-linux-x64-gnu": "0.
|
|
105
|
-
"@net-mesh/core-linux-x64-musl": "0.
|
|
106
|
-
"@net-mesh/core-linux-arm64-gnu": "0.
|
|
107
|
-
"@net-mesh/core-linux-arm64-musl": "0.
|
|
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
|
+
};
|