toolnexus 0.3.1 → 0.4.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 +62 -3
- package/dist/client.d.ts +49 -1
- package/dist/client.js +410 -206
- package/package.json +1 -1
- package/src/client.ts +401 -177
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
|
|
@@ -238,11 +296,12 @@ a sixth.
|
|
|
238
296
|
| Member | Description |
|
|
239
297
|
|--------|-------------|
|
|
240
298
|
| `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?`) |
|
|
299
|
+
| `createClient(opts)` | the unified host loop (`baseUrl`, `style`, `model`, `store?`, `hooks?`, `maxTurns?`, `retries?`, `timeoutMs?`, `onMetric?`) |
|
|
242
300
|
| `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` |
|
|
301
|
+
| `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
302
|
| `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) |
|
|
303
|
+
| `client.stream(prompt, { toolkit, id? })` | async-iterate `StreamEvent`s (text deltas, tool calls/results, usage, done); `id` ⇒ stateful |
|
|
304
|
+
| `client.metrics()` | Prometheus text exposition of cumulative metrics — mount at `GET /metrics` |
|
|
246
305
|
| `ConversationStore` | `get(id)` / `save(id, messages)` — implement for file/db; default `InMemoryConversationStore` |
|
|
247
306
|
| `tk.tools()` / `tk.get(name)` | the uniform tools |
|
|
248
307
|
| `tk.register(...tools)` | add native/http/custom tools at runtime |
|
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;
|