toolnexus 0.3.0 → 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 +201 -63
- 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
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
and you have a working agent — MCP servers, agent skills, your own functions, and HTTP endpoints
|
|
8
8
|
unified as one tool set, driving any LLM.
|
|
9
9
|
|
|
10
|
-
> **Right-sized.** Not a framework (no builders, advisors, runnables, config), not a toy
|
|
11
|
-
> falls over the moment you need streaming or a retry. Everything a real agent needs — the
|
|
12
|
-
> hooks, streaming, retries, memory — and nothing it doesn't.
|
|
10
|
+
> **Right-sized.** Not a framework (no builders, advisors, runnables, config graphs), not a toy
|
|
11
|
+
> that falls over the moment you need streaming or a retry. Everything a real agent needs — the
|
|
12
|
+
> loop, hooks, streaming, retries, memory — and nothing it doesn't.
|
|
13
13
|
|
|
14
14
|
The JS/TypeScript port of [toolnexus](https://github.com/muthuishere/toolnexus) — the same
|
|
15
15
|
library, byte-identical, also in **Python, Go, Java, and C#**. Built on
|
|
@@ -21,30 +21,147 @@ library, byte-identical, also in **Python, Go, Java, and C#**. Built on
|
|
|
21
21
|
npm install toolnexus
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
##
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
Built-in tools are on by default, so an empty toolkit can already act:
|
|
25
27
|
|
|
26
28
|
```ts
|
|
27
29
|
import { createToolkit, createClient } from "toolnexus"
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
const tk = await createToolkit({ mcpConfig: "./mcp.json", skillsDir: "./skills" })
|
|
31
|
-
|
|
32
|
-
// 2. point at any OpenAI- or Anthropic-style endpoint
|
|
31
|
+
const tk = await createToolkit() // 10 built-in tools, on by default
|
|
33
32
|
const agent = createClient({
|
|
34
|
-
baseUrl: "https://openrouter.ai/api/v1",
|
|
35
|
-
style: "openai",
|
|
33
|
+
baseUrl: "https://openrouter.ai/api/v1", // any OpenAI- or Anthropic-style endpoint
|
|
34
|
+
style: "openai", // or "anthropic"
|
|
36
35
|
model: "openai/gpt-4o-mini",
|
|
37
36
|
})
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
const { text } = await agent.run("Refund order 1234 for the customer.", { toolkit: tk })
|
|
38
|
+
const { text } = await agent.run("What files are in this folder?", { toolkit: tk })
|
|
41
39
|
console.log(text)
|
|
42
|
-
await tk.close()
|
|
43
40
|
```
|
|
44
41
|
|
|
45
42
|
`createClient` reads the key from `OPENROUTER_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY`
|
|
46
|
-
|
|
47
|
-
with hooks, streaming (`agent.stream(...)`), retries/backoff, and
|
|
43
|
+
(or pass `apiKey`). The loop runs call → execute tools → feed back → repeat (up to `maxTurns`,
|
|
44
|
+
default 10), with hooks, streaming (`agent.stream(...)`), retries/backoff, timeout, and
|
|
45
|
+
conversation memory available.
|
|
46
|
+
|
|
47
|
+
## With MCP + skills
|
|
48
|
+
|
|
49
|
+
Point the toolkit at an `mcp.json` and a `skills/` folder — every MCP server tool and every
|
|
50
|
+
`SKILL.md` becomes part of the same tool set. Skills are injected into the system prompt; a
|
|
51
|
+
single `skill` tool loads each on demand (progressive disclosure).
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const tk = await createToolkit({
|
|
55
|
+
mcpConfig: "./mcp.json", // path or a config object; local stdio + remote streamable-HTTP
|
|
56
|
+
skillsDir: "./skills", // a folder of <name>/SKILL.md (string or string[])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const { text } = await agent.run("Refund order 1234 for the customer.", { toolkit: tk })
|
|
60
|
+
console.log(text)
|
|
61
|
+
await tk.close() // disconnect MCP servers
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Conversations / memory
|
|
65
|
+
|
|
66
|
+
`run()` is stateless — each call starts fresh. To keep a thread's history, use `ask()` with an
|
|
67
|
+
`id`, or a `conversation()` object.
|
|
68
|
+
|
|
69
|
+
**`ask(prompt, { toolkit, id? })`** — with an `id`, the client's `ConversationStore` remembers
|
|
70
|
+
the thread: it *loads* that id's transcript, *runs*, *saves* the updated transcript, and returns
|
|
71
|
+
the answer. The next `ask` with the same `id` continues it. Without an `id` it's a stateless
|
|
72
|
+
one-shot (identical to `run`).
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
const agent = createClient({
|
|
76
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
77
|
+
style: "openai",
|
|
78
|
+
model: "openai/gpt-4o-mini",
|
|
79
|
+
// store defaults to InMemoryConversationStore (process lifetime).
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
await agent.ask("My name is Muthu.", { toolkit: tk, id: "user-42" })
|
|
83
|
+
const { text } = await agent.ask("What's my name?", { toolkit: tk, id: "user-42" })
|
|
84
|
+
console.log(text) // → "Your name is Muthu."
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Persist across processes.** A `ConversationStore` is two methods — implement it for a file, DB,
|
|
88
|
+
or Redis and pass it as `store`:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import type { ConversationStore } from "toolnexus"
|
|
92
|
+
|
|
93
|
+
interface ConversationStore {
|
|
94
|
+
get(id: string): Promise<any[] | undefined> // stored transcript for id, or undefined
|
|
95
|
+
save(id: string, messages: any[]): Promise<void>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const agent = createClient({ baseUrl, style: "openai", model, store: myFileStore })
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The default is `InMemoryConversationStore` (also exported). For a quick in-process thread without
|
|
102
|
+
ids, `agent.conversation({ toolkit })` returns an object whose `.send(prompt)` retains history
|
|
103
|
+
automatically (`.messages`, `.reset()`).
|
|
104
|
+
|
|
105
|
+
The same `store` powers inbound A2A: `toolkit.serve` keys each peer's turns by the A2A
|
|
106
|
+
`contextId` through `client.ask`, so a remote agent's conversation is remembered too (see below).
|
|
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.
|
|
48
165
|
|
|
49
166
|
## Add your own tools
|
|
50
167
|
|
|
@@ -52,64 +169,53 @@ with hooks, streaming (`agent.stream(...)`), retries/backoff, and conversation m
|
|
|
52
169
|
import { defineTool, httpTool } from "toolnexus"
|
|
53
170
|
|
|
54
171
|
tk.register(
|
|
55
|
-
// a plain function → a tool
|
|
172
|
+
// a plain function → a tool (run returns a string or a full ToolResult)
|
|
56
173
|
defineTool({
|
|
57
|
-
name: "add",
|
|
58
|
-
|
|
59
|
-
|
|
174
|
+
name: "add",
|
|
175
|
+
description: "Add two numbers",
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: { a: { type: "number" }, b: { type: "number" } },
|
|
179
|
+
required: ["a", "b"],
|
|
180
|
+
},
|
|
181
|
+
run: (args) => `${(args.a as number) + (args.b as number)}`,
|
|
60
182
|
}),
|
|
61
183
|
// a REST endpoint → a tool
|
|
62
184
|
httpTool({
|
|
63
|
-
name: "create_ticket",
|
|
185
|
+
name: "create_ticket",
|
|
186
|
+
description: "Create a ticket",
|
|
187
|
+
method: "POST",
|
|
64
188
|
url: "https://api.example.com/tickets",
|
|
65
189
|
headers: { Authorization: "Bearer ${API_TOKEN}" }, // ${ENV} expands from process.env, never logged
|
|
66
|
-
inputSchema: {
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: { title: { type: "string" } },
|
|
193
|
+
required: ["title"],
|
|
194
|
+
},
|
|
67
195
|
}),
|
|
68
196
|
)
|
|
69
197
|
```
|
|
70
198
|
|
|
71
|
-
URL `{placeholders}` are filled from args; `query`
|
|
72
|
-
the JSON body
|
|
73
|
-
|
|
74
|
-
## Bring your own loop
|
|
75
|
-
|
|
76
|
-
Don't want the host loop? Use the schema adapters and execute calls yourself:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
const tools = tk.toOpenAI() // or tk.toAnthropic() / tk.toGemini()
|
|
80
|
-
const system = tk.skillsPrompt() // skills catalog for your system prompt (opens with a preamble telling the model to use the skill tool)
|
|
81
|
-
// when the model returns a tool call { name, arguments }:
|
|
82
|
-
const res = await tk.execute(name, args) // -> { output, isError, metadata }
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## The four sources
|
|
86
|
-
|
|
87
|
-
| Source | How |
|
|
88
|
-
|--------|-----|
|
|
89
|
-
| **MCP servers** | an `mcp.json` (`mcpServers`/`servers`/`mcp`); local stdio + remote streamable-HTTP, `headers` for auth |
|
|
90
|
-
| **Agent skills** | a folder of `<name>/SKILL.md`; a `skill` tool loads each on demand + a system-prompt catalog |
|
|
91
|
-
| **Native tools** | `defineTool({...})` — a function becomes a tool |
|
|
92
|
-
| **HTTP / REST** | `httpTool({...})` — an endpoint becomes a tool, `${ENV}` headers |
|
|
93
|
-
|
|
94
|
-
All four appear as one uniform `Tool` in `tk.tools()`. Each is à la carte — use just `loadMcp(config)`
|
|
95
|
-
or `loadSkills(dir)` if that's all you need.
|
|
199
|
+
For `httpTool`: URL `{placeholders}` are filled from args; `query` arg names go to the
|
|
200
|
+
querystring (all args on GET); the rest become the JSON body (`body: "json" | "form" | "raw"`).
|
|
201
|
+
Non-2xx → `{ isError: true, output: "HTTP <status>: <body>" }`.
|
|
96
202
|
|
|
97
203
|
## Built-in tools
|
|
98
204
|
|
|
99
205
|
A fifth source ships **10 built-in tools** — `bash`, `read`, `write`, `edit`, `grep`, `glob`,
|
|
100
|
-
`webfetch`, `question`, `apply_patch`, `todowrite` (names + input schemas match
|
|
101
|
-
|
|
206
|
+
`webfetch`, `question`, `apply_patch`, `todowrite` (names + input schemas match opencode) — so an
|
|
207
|
+
agent can act with zero wiring. They appear in the tool schema
|
|
102
208
|
(`toOpenAI()`/`toAnthropic()`/`toGemini()`), like MCP tools — not the system prompt.
|
|
103
209
|
|
|
104
210
|
**On by default.** One global toggle turns the whole source off, or a per-tool `tools` map
|
|
105
211
|
disables individual builtins on the all-on baseline:
|
|
106
212
|
|
|
107
213
|
```ts
|
|
108
|
-
const tk = await createToolkit({
|
|
214
|
+
const tk = await createToolkit({ builtins: false })
|
|
109
215
|
// also accepts { disabled: true } or { enabled: false }
|
|
110
216
|
|
|
111
217
|
// per-tool: drop bash, keep the other nine (unknown names ignored; whole-source-off still wins)
|
|
112
|
-
const tk2 = await createToolkit({
|
|
218
|
+
const tk2 = await createToolkit({ builtins: { tools: { bash: false } } })
|
|
113
219
|
```
|
|
114
220
|
|
|
115
221
|
`bash`/`write`/`edit`/`apply_patch` run commands and mutate the filesystem — the toggle is the
|
|
@@ -136,7 +242,7 @@ await tk.addAgent("https://writer.example.com/.well-known/agent-card.json")
|
|
|
136
242
|
```
|
|
137
243
|
|
|
138
244
|
`agent({ card, headers?, timeout?, pollEvery? })` — `headers` support `${ENV}` expansion (never
|
|
139
|
-
logged); `timeout` defaults to 300000ms, `pollEvery` to 1000ms. A config
|
|
245
|
+
logged); `timeout` defaults to 300000ms, `pollEvery` to 1000ms. A config object can also carry an
|
|
140
246
|
`agents` block (mirrors `mcpServers`). A failing agent is isolated — logged, contributes no tools,
|
|
141
247
|
never fatal.
|
|
142
248
|
|
|
@@ -144,12 +250,8 @@ never fatal.
|
|
|
144
250
|
built from your **SKILL.md skills** (never raw tools):
|
|
145
251
|
|
|
146
252
|
```ts
|
|
147
|
-
import { createClient } from "toolnexus"
|
|
148
|
-
|
|
149
|
-
const agent = createClient({ baseUrl: "https://openrouter.ai/api/v1", style: "openai", model: "openai/gpt-4o-mini" })
|
|
150
|
-
|
|
151
253
|
const handle = await tk.serve("127.0.0.1:0", {
|
|
152
|
-
client: agent,
|
|
254
|
+
client: agent, // a createClient(...) instance fulfils each task
|
|
153
255
|
a2a: {
|
|
154
256
|
name: "research-agent",
|
|
155
257
|
description: "Answers research questions.",
|
|
@@ -161,26 +263,62 @@ console.log(handle.url) // GET /.well-known/agent-card.json ; POST / (SendMess
|
|
|
161
263
|
await handle.stop()
|
|
162
264
|
```
|
|
163
265
|
|
|
164
|
-
`serve(addr, { client, a2a?, onTask? })` fulfils each inbound `SendMessage` task via
|
|
165
|
-
Task persistence is a pluggable `TaskStore` — in-memory default, `"file:<dir>"`, or your own.
|
|
266
|
+
`serve(addr, { client, a2a?, onTask? })` fulfils each inbound `SendMessage` task via the client.
|
|
267
|
+
Task persistence is a pluggable `TaskStore` — in-memory default, `"file:<dir>"`, or your own. And
|
|
268
|
+
it **remembers**: a message's A2A `contextId` keys the conversation through `client.ask`, so a
|
|
269
|
+
peer's turns in the same context carry history via the client's `ConversationStore`.
|
|
270
|
+
|
|
271
|
+
## Bring your own loop
|
|
272
|
+
|
|
273
|
+
Don't want the host loop? Use the schema adapters and execute calls yourself:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
const tools = tk.toOpenAI() // or tk.toAnthropic() / tk.toGemini()
|
|
277
|
+
const system = tk.skillsPrompt() // skills catalog for your system prompt
|
|
278
|
+
// when the model returns a tool call { name, arguments }:
|
|
279
|
+
const res = await tk.execute(name, args) // → { output, isError, metadata }
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## The four sources
|
|
283
|
+
|
|
284
|
+
| Source | How |
|
|
285
|
+
|--------|-----|
|
|
286
|
+
| **MCP servers** | an `mcp.json` (`mcpServers`/`servers`/`mcp`); local stdio + remote streamable-HTTP, `headers` for auth |
|
|
287
|
+
| **Agent skills** | a folder of `<name>/SKILL.md`; a `skill` tool loads each on demand + a system-prompt catalog |
|
|
288
|
+
| **Native tools** | `defineTool({...})` — a function becomes a tool |
|
|
289
|
+
| **HTTP / REST** | `httpTool({...})` — an endpoint becomes a tool, `${ENV}` headers |
|
|
290
|
+
|
|
291
|
+
All appear as one uniform `Tool` in `tk.tools()`. Built-ins are a fifth source; remote A2A agents
|
|
292
|
+
a sixth.
|
|
166
293
|
|
|
167
294
|
## API
|
|
168
295
|
|
|
169
296
|
| Member | Description |
|
|
170
297
|
|--------|-------------|
|
|
171
|
-
| `createToolkit(opts)` | async factory → `Toolkit` |
|
|
172
|
-
| `createClient(opts)` | the unified host loop (`
|
|
298
|
+
| `createToolkit(opts)` | async factory → `Toolkit` (`mcpConfig`, `skillsDir`, `builtins`, `agents`, `extraTools`) |
|
|
299
|
+
| `createClient(opts)` | the unified host loop (`baseUrl`, `style`, `model`, `store?`, `hooks?`, `maxTurns?`, `retries?`, `timeoutMs?`, `onMetric?`) |
|
|
300
|
+
| `client.run(prompt, { toolkit, signal?, history? })` | stateless run → `RunResult` |
|
|
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 |
|
|
302
|
+
| `client.conversation({ toolkit })` | in-process multi-turn object — `.send(prompt)`, `.messages`, `.reset()` |
|
|
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` |
|
|
305
|
+
| `ConversationStore` | `get(id)` / `save(id, messages)` — implement for file/db; default `InMemoryConversationStore` |
|
|
173
306
|
| `tk.tools()` / `tk.get(name)` | the uniform tools |
|
|
307
|
+
| `tk.register(...tools)` | add native/http/custom tools at runtime |
|
|
174
308
|
| `tk.execute(name, args, ctx?)` | run a tool → `ToolResult` |
|
|
175
309
|
| `tk.skillsPrompt()` | system-prompt skill catalog |
|
|
176
310
|
| `tk.mcpStatus()` | per-server connection status |
|
|
177
311
|
| `tk.toOpenAI()` / `toAnthropic()` / `toGemini()` | provider tool schemas |
|
|
178
|
-
| `tk.
|
|
312
|
+
| `tk.serve(addr, { client, a2a?, onTask? })` | serve the toolkit as an A2A agent → `ServeHandle` |
|
|
313
|
+
| `tk.addAgent(cardUrl \| Agent)` | fetch a remote agent's card and register its skills as tools |
|
|
179
314
|
| `tk.close()` | disconnect MCP servers |
|
|
315
|
+
| `defineTool` / `httpTool` / `agent` | build native / HTTP / A2A tools |
|
|
316
|
+
| `InMemoryConversationStore` · `InMemoryTaskStore` / `FileTaskStore` | default stores (memory / A2A tasks) |
|
|
180
317
|
|
|
181
318
|
## More
|
|
182
319
|
|
|
183
|
-
Full docs, the other four language ports, the shared behavior
|
|
184
|
-
|
|
320
|
+
Full docs, the other four language ports, and the shared cross-language behavior contract
|
|
321
|
+
([`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md)) — plus runnable
|
|
322
|
+
`examples/`: **https://github.com/muthuishere/toolnexus**
|
|
185
323
|
|
|
186
324
|
MIT licensed.
|
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;
|