toolnexus 0.3.1 → 0.5.0
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/README.md +86 -4
- package/dist/client.d.ts +49 -1
- package/dist/client.js +410 -206
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp.js +3 -3
- package/dist/mcpserve.d.ts +47 -0
- package/dist/mcpserve.js +69 -0
- package/dist/serve.d.ts +8 -0
- package/dist/serve.js +42 -2
- package/dist/toolkit.d.ts +6 -1
- package/dist/toolkit.js +17 -2
- package/package.json +1 -1
- package/src/client.ts +401 -177
- package/src/index.ts +1 -0
- package/src/mcp.ts +3 -3
- package/src/mcpserve.ts +95 -0
- package/src/serve.ts +51 -3
- package/src/toolkit.ts +22 -4
package/README.md
CHANGED
|
@@ -105,6 +105,64 @@ automatically (`.messages`, `.reset()`).
|
|
|
105
105
|
The same `store` powers inbound A2A: `toolkit.serve` keys each peer's turns by the A2A
|
|
106
106
|
`contextId` through `client.ask`, so a remote agent's conversation is remembered too (see below).
|
|
107
107
|
|
|
108
|
+
**Streaming with memory.** The `id` also works while streaming. Pass `on_text` to `ask` to stream
|
|
109
|
+
text deltas as they arrive — `ask` still returns the final `RunResult` — or iterate `stream()`
|
|
110
|
+
directly. With an `id`, the thread is loaded before the stream and saved on the `done` event.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// block-style: stream deltas to stdout, still get the RunResult back — remembered under `id`
|
|
114
|
+
const res = await agent.ask("Draft a reply.", {
|
|
115
|
+
toolkit: tk,
|
|
116
|
+
id: "user-42",
|
|
117
|
+
on_text: (delta) => process.stdout.write(delta),
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// iterator: consume text + tool events; `id` makes it stateful (load before, save on `done`)
|
|
121
|
+
for await (const ev of agent.stream("And summarise it.", { toolkit: tk, id: "user-42" })) {
|
|
122
|
+
if (ev.type === "text") process.stdout.write(ev.delta)
|
|
123
|
+
else if (ev.type === "done") console.log("\n", ev.result.usage)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Observability / metrics
|
|
128
|
+
|
|
129
|
+
Zero-dependency, two outputs from one internal instrumentation — both opt-in, no cost when unused.
|
|
130
|
+
|
|
131
|
+
**`onMetric` — a semantic event feed.** Pass it to `createClient` and it receives a readable
|
|
132
|
+
`MetricEvent` at each significant point: `{ event: "llm", model, status, ms, promptTokens,
|
|
133
|
+
completionTokens }` per model call, `{ event: "tool", tool, source, isError, ms }` per tool call,
|
|
134
|
+
and a terminal `{ event: "run", model, turns, toolCalls, totalTokens, ms, error? }` per `run`/`ask`.
|
|
135
|
+
Forward it anywhere (statsd, logs, OpenTelemetry) — the library holds no opinion.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const agent = createClient({
|
|
139
|
+
baseUrl, style: "openai", model,
|
|
140
|
+
onMetric: (ev) => console.log("[metric]", ev.event, JSON.stringify(ev)),
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**`client.metrics()` — built-in Prometheus text.** The same events feed a tiny in-memory registry
|
|
145
|
+
that renders the Prometheus text exposition format (no third-party dep). Mount it at `GET /metrics`:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { createServer } from "node:http"
|
|
149
|
+
|
|
150
|
+
createServer((req, res) => {
|
|
151
|
+
if (req.url === "/metrics") {
|
|
152
|
+
res.setHeader("Content-Type", "text/plain; version=0.0.4")
|
|
153
|
+
res.end(agent.metrics())
|
|
154
|
+
} else {
|
|
155
|
+
res.statusCode = 404
|
|
156
|
+
res.end()
|
|
157
|
+
}
|
|
158
|
+
}).listen(9090)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Series: `toolnexus_llm_requests_total{model,status}`, `toolnexus_llm_tokens_total{type}`,
|
|
162
|
+
`toolnexus_tool_calls_total{tool,source,is_error}`, `toolnexus_run_errors_total{model}`, plus the
|
|
163
|
+
`toolnexus_llm_request_duration_seconds` and `toolnexus_tool_duration_seconds` histograms. The
|
|
164
|
+
rendered text is byte-identical across all five ports; OTLP push is a planned future companion.
|
|
165
|
+
|
|
108
166
|
## Add your own tools
|
|
109
167
|
|
|
110
168
|
```ts
|
|
@@ -210,6 +268,29 @@ Task persistence is a pluggable `TaskStore` — in-memory default, `"file:<dir>"
|
|
|
210
268
|
it **remembers**: a message's A2A `contextId` keys the conversation through `client.ask`, so a
|
|
211
269
|
peer's turns in the same context carry history via the client's `ConversationStore`.
|
|
212
270
|
|
|
271
|
+
## Serve as an MCP server (be a gateway)
|
|
272
|
+
|
|
273
|
+
The inbound mirror of A2A: expose your **whole toolkit as an MCP server** so any MCP client — an IDE,
|
|
274
|
+
another agent, a remote host — can call its tools. Point toolnexus at N MCP servers + skills + your
|
|
275
|
+
own functions, then re-expose the union as **one** MCP server. Unlike A2A (which advertises skills and
|
|
276
|
+
runs the client loop), the MCP client *is* the LLM host, so each `tools/call` dispatches straight to
|
|
277
|
+
`Tool.execute` — no `client`, no tasks, no store.
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
// streamable-HTTP — an embeddable MCP server mounted at POST /mcp, beside any A2A routes:
|
|
281
|
+
const srv = await tk.serve("127.0.0.1:0", {
|
|
282
|
+
mcp: { name: "my-gateway" /*, tools: ["echo"] // subset; omit ⇒ every toolkit tool */ },
|
|
283
|
+
onCall: (ev) => console.error(ev.name, ev.ms, ev.isError),
|
|
284
|
+
})
|
|
285
|
+
console.log(srv.url + "/mcp") // connect any MCP client here
|
|
286
|
+
await srv.stop()
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`tools/list` advertises every toolkit tool (name **verbatim**, `inputSchema` = the tool's parameters);
|
|
290
|
+
`mcp.tools` narrows the surface. The `mcp` profile can also live in the config file as a top-level
|
|
291
|
+
**`mcpServer`** block (singular — distinct from the client-side `mcpServers`). (Transport is
|
|
292
|
+
streamable-HTTP; a stdio transport for local clients like Claude Desktop is a planned follow-up.)
|
|
293
|
+
|
|
213
294
|
## Bring your own loop
|
|
214
295
|
|
|
215
296
|
Don't want the host loop? Use the schema adapters and execute calls yourself:
|
|
@@ -238,11 +319,12 @@ a sixth.
|
|
|
238
319
|
| Member | Description |
|
|
239
320
|
|--------|-------------|
|
|
240
321
|
| `createToolkit(opts)` | async factory → `Toolkit` (`mcpConfig`, `skillsDir`, `builtins`, `agents`, `extraTools`) |
|
|
241
|
-
| `createClient(opts)` | the unified host loop (`baseUrl`, `style`, `model`, `store?`, `hooks?`, `maxTurns?`, `retries?`, `timeoutMs?`) |
|
|
322
|
+
| `createClient(opts)` | the unified host loop (`baseUrl`, `style`, `model`, `store?`, `hooks?`, `maxTurns?`, `retries?`, `timeoutMs?`, `onMetric?`) |
|
|
242
323
|
| `client.run(prompt, { toolkit, signal?, history? })` | stateless run → `RunResult` |
|
|
243
|
-
| `client.ask(prompt, { toolkit, id?, signal? })` | stateful with `id` (loads → runs → saves via `store`); one-shot without `id` |
|
|
324
|
+
| `client.ask(prompt, { toolkit, id?, on_text?, signal? })` | stateful with `id` (loads → runs → saves via `store`); one-shot without `id`; `on_text` streams text deltas |
|
|
244
325
|
| `client.conversation({ toolkit })` | in-process multi-turn object — `.send(prompt)`, `.messages`, `.reset()` |
|
|
245
|
-
| `client.stream(prompt, { toolkit })` | async-iterate `StreamEvent`s (text deltas, tool calls/results, usage, done) |
|
|
326
|
+
| `client.stream(prompt, { toolkit, id? })` | async-iterate `StreamEvent`s (text deltas, tool calls/results, usage, done); `id` ⇒ stateful |
|
|
327
|
+
| `client.metrics()` | Prometheus text exposition of cumulative metrics — mount at `GET /metrics` |
|
|
246
328
|
| `ConversationStore` | `get(id)` / `save(id, messages)` — implement for file/db; default `InMemoryConversationStore` |
|
|
247
329
|
| `tk.tools()` / `tk.get(name)` | the uniform tools |
|
|
248
330
|
| `tk.register(...tools)` | add native/http/custom tools at runtime |
|
|
@@ -250,7 +332,7 @@ a sixth.
|
|
|
250
332
|
| `tk.skillsPrompt()` | system-prompt skill catalog |
|
|
251
333
|
| `tk.mcpStatus()` | per-server connection status |
|
|
252
334
|
| `tk.toOpenAI()` / `toAnthropic()` / `toGemini()` | provider tool schemas |
|
|
253
|
-
| `tk.serve(addr, { client
|
|
335
|
+
| `tk.serve(addr, { client?, a2a?, onTask?, mcp?, onCall? })` | serve the toolkit over HTTP — A2A agent and/or streamable-HTTP MCP server (`/mcp`) → `ServeHandle` |
|
|
254
336
|
| `tk.addAgent(cardUrl \| Agent)` | fetch a remote agent's card and register its skills as tools |
|
|
255
337
|
| `tk.close()` | disconnect MCP servers |
|
|
256
338
|
| `defineTool` / `httpTool` / `agent` | build native / HTTP / A2A tools |
|
package/dist/client.d.ts
CHANGED
|
@@ -24,7 +24,37 @@ export interface ClientOptions {
|
|
|
24
24
|
/** Conversation provider for `ask(prompt, { id })`. Default: in-memory (process lifetime).
|
|
25
25
|
* Supply a file/db store to persist conversations across processes. */
|
|
26
26
|
store?: ConversationStore;
|
|
27
|
+
/** Observability sink — receives semantic metric events ({event:"llm"|"tool"|"run"…}) as the
|
|
28
|
+
* loop runs (§8 Observability). Forward to statsd/logs/OTel/anything. No cost when unset. */
|
|
29
|
+
onMetric?: (ev: MetricEvent) => void;
|
|
27
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Semantic observability events (§8). NOT counter/histogram primitives — readable records the
|
|
33
|
+
* host can forward anywhere. The same events also feed the built-in Prometheus registry
|
|
34
|
+
* (`client.metrics()`).
|
|
35
|
+
*/
|
|
36
|
+
export type MetricEvent = {
|
|
37
|
+
event: "llm";
|
|
38
|
+
model: string;
|
|
39
|
+
status: "ok" | "error";
|
|
40
|
+
ms: number;
|
|
41
|
+
promptTokens: number;
|
|
42
|
+
completionTokens: number;
|
|
43
|
+
} | {
|
|
44
|
+
event: "tool";
|
|
45
|
+
tool: string;
|
|
46
|
+
source: string;
|
|
47
|
+
isError: boolean;
|
|
48
|
+
ms: number;
|
|
49
|
+
} | {
|
|
50
|
+
event: "run";
|
|
51
|
+
model: string;
|
|
52
|
+
turns: number;
|
|
53
|
+
toolCalls: number;
|
|
54
|
+
totalTokens: number;
|
|
55
|
+
ms: number;
|
|
56
|
+
error?: string;
|
|
57
|
+
};
|
|
28
58
|
/**
|
|
29
59
|
* Where `ask()` conversations are remembered — two methods. Ship the in-memory
|
|
30
60
|
* default; implement this for a file/db/redis provider to persist across processes.
|
|
@@ -145,7 +175,17 @@ export declare class Client {
|
|
|
145
175
|
private readonly opts;
|
|
146
176
|
/** Conversation provider for `ask()` — from opts.store, else in-memory. */
|
|
147
177
|
private readonly store;
|
|
178
|
+
/** Cumulative Prometheus registry fed by the same events `onMetric` sees. */
|
|
179
|
+
private readonly registry;
|
|
148
180
|
constructor(opts: ClientOptions);
|
|
181
|
+
/** Prometheus text exposition of cumulative metrics (§8). Always valid, empty-but-valid before activity. */
|
|
182
|
+
metrics(): string;
|
|
183
|
+
/** Feed a semantic metric event to the built-in registry and the optional onMetric sink. */
|
|
184
|
+
private emit;
|
|
185
|
+
/** Emit the terminal `run` metric event and build the RunResult. */
|
|
186
|
+
private endRun;
|
|
187
|
+
/** Emit a `run` error metric event (once, on a thrown run). */
|
|
188
|
+
private emitRunError;
|
|
149
189
|
run(prompt: string, ctx: {
|
|
150
190
|
toolkit: Toolkit;
|
|
151
191
|
signal?: AbortSignal;
|
|
@@ -160,6 +200,7 @@ export declare class Client {
|
|
|
160
200
|
ask(prompt: string, ctx: {
|
|
161
201
|
toolkit: Toolkit;
|
|
162
202
|
id?: string;
|
|
203
|
+
on_text?: (delta: string) => void;
|
|
163
204
|
signal?: AbortSignal;
|
|
164
205
|
}): Promise<RunResult>;
|
|
165
206
|
/** A stateful multi-turn conversation that retains history across sends. */
|
|
@@ -167,9 +208,14 @@ export declare class Client {
|
|
|
167
208
|
toolkit: Toolkit;
|
|
168
209
|
signal?: AbortSignal;
|
|
169
210
|
}): Conversation;
|
|
170
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* Streaming variant: async-iterate live events (text deltas, tool calls/results, usage, done).
|
|
213
|
+
* With an `id`, it is stateful (like `ask`): the thread's transcript is loaded as history before
|
|
214
|
+
* streaming, and saved back to the ConversationStore on the terminal `done` event. No `id` ⇒ stateless.
|
|
215
|
+
*/
|
|
171
216
|
stream(prompt: string, ctx: {
|
|
172
217
|
toolkit: Toolkit;
|
|
218
|
+
id?: string;
|
|
173
219
|
signal?: AbortSignal;
|
|
174
220
|
}): AsyncGenerator<StreamEvent, void, unknown>;
|
|
175
221
|
private system;
|
|
@@ -179,6 +225,8 @@ export declare class Client {
|
|
|
179
225
|
private llmFetch;
|
|
180
226
|
/** Run one tool through beforeTool/afterTool hooks (mutate args, short-circuit, transform result). */
|
|
181
227
|
private runTool;
|
|
228
|
+
/** One non-streaming LLM call, with an `llm` metric event (ok/error + per-call tokens + ms). */
|
|
229
|
+
private llmCallJson;
|
|
182
230
|
private runOpenAI;
|
|
183
231
|
private result;
|
|
184
232
|
private runAnthropic;
|