toolnexus 0.18.0 → 0.18.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/README.md +17 -410
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ unified as one tool set, driving any LLM.
|
|
|
12
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
|
-
library, byte-identical, also in **Python, Go, Java, and
|
|
15
|
+
library, byte-identical, also in **Python, Go, Java, C#, Elixir and Clojure**. Built on
|
|
16
16
|
`@modelcontextprotocol/sdk` (the MCP SDK opencode uses).
|
|
17
17
|
|
|
18
18
|
## Install
|
|
@@ -39,420 +39,27 @@ const { text } = await agent.run("What files are in this folder?", { toolkit: tk
|
|
|
39
39
|
console.log(text)
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
|
|
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
|
-
## Suspension — login / approval / input (`Pending` / `waitFor`)
|
|
65
|
-
|
|
66
|
-
Some tools can't finish in one shot: they need an **out-of-band, async resolution** — a human
|
|
67
|
-
logs in, approves a trade, types a value. That's one idea, not one feature: *login* is just the
|
|
68
|
-
`kind:"authorization"` case. A tool returns **`Pending(request)`**; the client calls your
|
|
69
|
-
**`waitFor(request) → answer`**, then **retries the tool** with `ctx.answer`, and the model gets
|
|
70
|
-
the real answer. Everything crossing a boundary is plain data, so it works the same across
|
|
71
|
-
processes, agents, and restarts — and identically in all five language ports.
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
import { authRequired } from "toolnexus"
|
|
75
|
-
|
|
76
|
-
// a tool that needs login the first time:
|
|
77
|
-
defineTool({
|
|
78
|
-
name: "get_balance", description: "…", inputSchema: { type: "object", properties: {} },
|
|
79
|
-
run: (_args, ctx) =>
|
|
80
|
-
authed ? `balance: ₹67,417` : authRequired("https://example.com/login", "Log in to continue"),
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// the host supplies ONE function — how the link reaches the human is your call:
|
|
84
|
-
const agent = createClient({
|
|
85
|
-
baseUrl, style: "openai", model,
|
|
86
|
-
waitFor: async (request) => { // interactive: open a browser + poll
|
|
87
|
-
open(request.url); await pollUntilLoggedIn(); return { id: request.id, ok: true }
|
|
88
|
-
},
|
|
89
|
-
// headless desk: waitFor = send request.url to your channel + poll
|
|
90
|
-
// another agent: waitFor = forward the request over A2A, await its answer
|
|
91
|
-
})
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Omit `waitFor` for a **durable** host: `run()` doesn't hang — it returns
|
|
95
|
-
`{ status: "pending", pending: request }`, so you can deliver the link out-of-band (file, HTTP,
|
|
96
|
-
channel) and resume later, even in another process. `stream()` emits a `{ type: "pending", request }`
|
|
97
|
-
event so a channel can push the link in real time. Runnable demo: `examples/pending.ts`.
|
|
98
|
-
|
|
99
|
-
## Conversations / memory
|
|
100
|
-
|
|
101
|
-
`run()` is stateless — each call starts fresh. To keep a thread's history, use `ask()` with an
|
|
102
|
-
`id`, or a `conversation()` object.
|
|
103
|
-
|
|
104
|
-
**`ask(prompt, { toolkit, id? })`** — with an `id`, the client's `ConversationStore` remembers
|
|
105
|
-
the thread: it *loads* that id's transcript, *runs*, *saves* the updated transcript, and returns
|
|
106
|
-
the answer. The next `ask` with the same `id` continues it. Without an `id` it's a stateless
|
|
107
|
-
one-shot (identical to `run`).
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
const agent = createClient({
|
|
111
|
-
baseUrl: "https://openrouter.ai/api/v1",
|
|
112
|
-
style: "openai",
|
|
113
|
-
model: "openai/gpt-4o-mini",
|
|
114
|
-
// store defaults to InMemoryConversationStore (process lifetime).
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
await agent.ask("My name is Muthu.", { toolkit: tk, id: "user-42" })
|
|
118
|
-
const { text } = await agent.ask("What's my name?", { toolkit: tk, id: "user-42" })
|
|
119
|
-
console.log(text) // → "Your name is Muthu."
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
**Persist across processes.** A `ConversationStore` is two methods — implement it for a file, DB,
|
|
123
|
-
or Redis and pass it as `store`:
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
import type { ConversationStore } from "toolnexus"
|
|
127
|
-
|
|
128
|
-
interface ConversationStore {
|
|
129
|
-
get(id: string): Promise<any[] | undefined> // stored transcript for id, or undefined
|
|
130
|
-
save(id: string, messages: any[]): Promise<void>
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const agent = createClient({ baseUrl, style: "openai", model, store: myFileStore })
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
The default is `InMemoryConversationStore` (also exported). For a quick in-process thread without
|
|
137
|
-
ids, `agent.conversation({ toolkit })` returns an object whose `.send(prompt)` retains history
|
|
138
|
-
automatically (`.messages`, `.reset()`).
|
|
139
|
-
|
|
140
|
-
The same `store` powers inbound A2A: `toolkit.serve` keys each peer's turns by the A2A
|
|
141
|
-
`contextId` through `client.ask`, so a remote agent's conversation is remembered too (see below).
|
|
142
|
-
|
|
143
|
-
**Streaming with memory.** The `id` also works while streaming. Pass `on_text` to `ask` to stream
|
|
144
|
-
text deltas as they arrive — `ask` still returns the final `RunResult` — or iterate `stream()`
|
|
145
|
-
directly. With an `id`, the thread is loaded before the stream and saved on the `done` event.
|
|
146
|
-
|
|
147
|
-
```ts
|
|
148
|
-
// block-style: stream deltas to stdout, still get the RunResult back — remembered under `id`
|
|
149
|
-
const res = await agent.ask("Draft a reply.", {
|
|
150
|
-
toolkit: tk,
|
|
151
|
-
id: "user-42",
|
|
152
|
-
on_text: (delta) => process.stdout.write(delta),
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
// iterator: consume text + tool events; `id` makes it stateful (load before, save on `done`)
|
|
156
|
-
for await (const ev of agent.stream("And summarise it.", { toolkit: tk, id: "user-42" })) {
|
|
157
|
-
if (ev.type === "text") process.stdout.write(ev.delta)
|
|
158
|
-
else if (ev.type === "done") console.log("\n", ev.result.usage)
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Observability / metrics
|
|
163
|
-
|
|
164
|
-
Zero-dependency, two outputs from one internal instrumentation — both opt-in, no cost when unused.
|
|
165
|
-
|
|
166
|
-
**`onMetric` — a semantic event feed.** Pass it to `createClient` and it receives a readable
|
|
167
|
-
`MetricEvent` at each significant point: `{ event: "llm", model, status, ms, promptTokens,
|
|
168
|
-
completionTokens }` per model call, `{ event: "tool", tool, source, isError, ms }` per tool call,
|
|
169
|
-
and a terminal `{ event: "run", model, turns, toolCalls, totalTokens, ms, error? }` per `run`/`ask`.
|
|
170
|
-
Forward it anywhere (statsd, logs, OpenTelemetry) — the library holds no opinion.
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
const agent = createClient({
|
|
174
|
-
baseUrl, style: "openai", model,
|
|
175
|
-
onMetric: (ev) => console.log("[metric]", ev.event, JSON.stringify(ev)),
|
|
176
|
-
})
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**`client.metrics()` — built-in Prometheus text.** The same events feed a tiny in-memory registry
|
|
180
|
-
that renders the Prometheus text exposition format (no third-party dep). Mount it at `GET /metrics`:
|
|
181
|
-
|
|
182
|
-
```ts
|
|
183
|
-
import { createServer } from "node:http"
|
|
184
|
-
|
|
185
|
-
createServer((req, res) => {
|
|
186
|
-
if (req.url === "/metrics") {
|
|
187
|
-
res.setHeader("Content-Type", "text/plain; version=0.0.4")
|
|
188
|
-
res.end(agent.metrics())
|
|
189
|
-
} else {
|
|
190
|
-
res.statusCode = 404
|
|
191
|
-
res.end()
|
|
192
|
-
}
|
|
193
|
-
}).listen(9090)
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Series: `toolnexus_llm_requests_total{model,status}`, `toolnexus_llm_tokens_total{type}`,
|
|
197
|
-
`toolnexus_tool_calls_total{tool,source,is_error}`, `toolnexus_run_errors_total{model}`, plus the
|
|
198
|
-
`toolnexus_llm_request_duration_seconds` and `toolnexus_tool_duration_seconds` histograms. The
|
|
199
|
-
rendered text is byte-identical across all five ports; OTLP push is a planned future companion.
|
|
200
|
-
|
|
201
|
-
## Add your own tools
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import { defineTool, httpTool } from "toolnexus"
|
|
205
|
-
|
|
206
|
-
tk.register(
|
|
207
|
-
// a plain function → a tool (run returns a string or a full ToolResult)
|
|
208
|
-
defineTool({
|
|
209
|
-
name: "add",
|
|
210
|
-
description: "Add two numbers",
|
|
211
|
-
inputSchema: {
|
|
212
|
-
type: "object",
|
|
213
|
-
properties: { a: { type: "number" }, b: { type: "number" } },
|
|
214
|
-
required: ["a", "b"],
|
|
215
|
-
},
|
|
216
|
-
run: (args) => `${(args.a as number) + (args.b as number)}`,
|
|
217
|
-
}),
|
|
218
|
-
// a REST endpoint → a tool
|
|
219
|
-
httpTool({
|
|
220
|
-
name: "create_ticket",
|
|
221
|
-
description: "Create a ticket",
|
|
222
|
-
method: "POST",
|
|
223
|
-
url: "https://api.example.com/tickets",
|
|
224
|
-
headers: { Authorization: "Bearer ${API_TOKEN}" }, // ${ENV} expands from process.env, never logged
|
|
225
|
-
inputSchema: {
|
|
226
|
-
type: "object",
|
|
227
|
-
properties: { title: { type: "string" } },
|
|
228
|
-
required: ["title"],
|
|
229
|
-
},
|
|
230
|
-
}),
|
|
231
|
-
)
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
For `httpTool`: URL `{placeholders}` are filled from args; `query` arg names go to the
|
|
235
|
-
querystring (all args on GET); the rest become the JSON body (`body: "json" | "form" | "raw"`).
|
|
236
|
-
Non-2xx → `{ isError: true, output: "HTTP <status>: <body>" }`.
|
|
237
|
-
|
|
238
|
-
## Images, PDFs and audio (content parts)
|
|
239
|
-
|
|
240
|
-
Parts go in the **first** argument, next to your text, because the order of text and image is
|
|
241
|
-
semantic to a model:
|
|
242
|
-
|
|
243
|
-
```ts
|
|
244
|
-
import { attach, text } from "toolnexus"
|
|
245
|
-
|
|
246
|
-
const { text: answer } = await agent.run(
|
|
247
|
-
["What is broken in this screenshot?", await attach("./shot.png")],
|
|
248
|
-
{ toolkit: tk },
|
|
249
|
-
)
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
`attach(source, opts?)` takes what you already hold: a filesystem path, a `data:` URL, an
|
|
253
|
-
`https:` URL, native bytes (`Uint8Array`, `Buffer`, `ArrayBuffer`, any `ArrayBufferView` —
|
|
254
|
-
`mimeType` required, bytes carry no extension), or a `Blob`/`File` straight off an upload.
|
|
255
|
-
`fromPath`, `fromBytes`, `fromBlob`, `fromDataUrl`, `fromUrl` are the same thing spelled
|
|
256
|
-
explicitly. Mime types come from a fixed extension table (`png jpg jpeg gif webp pdf mp3 wav`) —
|
|
257
|
-
never sniffed, never read from the machine's mime database, so every port agrees. A plain string
|
|
258
|
-
prompt is untouched and byte-identical to before.
|
|
259
|
-
|
|
260
|
-
A tool can answer with media too — `parts` alongside the still-required `output`, which is what
|
|
261
|
-
the transcript, compaction and any text-only provider keep seeing:
|
|
262
|
-
|
|
263
|
-
```ts
|
|
264
|
-
defineTool({
|
|
265
|
-
name: "screenshot",
|
|
266
|
-
description: "Grab the screen",
|
|
267
|
-
inputSchema: { type: "object", properties: {} },
|
|
268
|
-
run: async () => ({
|
|
269
|
-
output: "screenshot, 1280x720 png",
|
|
270
|
-
parts: [await attach("./shot.png")],
|
|
271
|
-
}),
|
|
272
|
-
})
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
**MCP tools' images now survive.** Non-text content from an MCP server — image, audio, embedded
|
|
276
|
-
resource, `resource_link` — used to be filtered out silently; it is preserved as `parts` and
|
|
277
|
-
emitted per provider style (Anthropic inside `tool_result`; OpenAI, whose `tool` message rejects
|
|
278
|
-
an image, in a synthetic user message that never enters your transcript).
|
|
279
|
-
|
|
280
|
-
Whatever you pass in, a part stores **bytes plus a `mimeType`** — never a handle, never a path —
|
|
281
|
-
so a persisted transcript replays without the original file. A `Blob` is read **eagerly** at
|
|
282
|
-
construction, and a handle you supply is read, not closed.
|
|
283
|
-
|
|
284
|
-
## Built-in tools
|
|
285
|
-
|
|
286
|
-
A fifth source ships **10 built-in tools** — `bash`, `read`, `write`, `edit`, `grep`, `glob`,
|
|
287
|
-
`webfetch`, `question`, `apply_patch`, `todowrite` (names + input schemas match opencode) — so an
|
|
288
|
-
agent can act with zero wiring. They appear in the tool schema
|
|
289
|
-
(`toOpenAI()`/`toAnthropic()`/`toGemini()`), like MCP tools — not the system prompt.
|
|
290
|
-
|
|
291
|
-
**On by default.** One global toggle turns the whole source off, or a per-tool `tools` map
|
|
292
|
-
disables individual builtins on the all-on baseline:
|
|
293
|
-
|
|
294
|
-
```ts
|
|
295
|
-
const tk = await createToolkit({ builtins: false })
|
|
296
|
-
// also accepts { disabled: true } or { enabled: false }
|
|
297
|
-
|
|
298
|
-
// per-tool: drop bash, keep the other nine (unknown names ignored; whole-source-off still wins)
|
|
299
|
-
const tk2 = await createToolkit({ builtins: { tools: { bash: false } } })
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
`bash`/`write`/`edit`/`apply_patch` run commands and mutate the filesystem — the toggle is the
|
|
303
|
-
off-switch for locked-down hosts.
|
|
304
|
-
|
|
305
|
-
## A2A agents (agent-to-agent)
|
|
306
|
-
|
|
307
|
-
Call **remote A2A agents** (each of their skills becomes a tool) and serve your own toolkit as an
|
|
308
|
-
agent other A2A peers can call. A genuine, minimal subset of real A2A (JSON-RPC 2.0; Agent Card at
|
|
309
|
-
`/.well-known/agent-card.json`; `SendMessage` → poll `GetTask`). No streaming / push / auth in v1.
|
|
310
|
-
|
|
311
|
-
**Outbound — call a remote agent.** Each advertised skill becomes a tool named `<agent>_<skill>`
|
|
312
|
-
(`source: "a2a"`):
|
|
313
|
-
|
|
314
|
-
```ts
|
|
315
|
-
import { createToolkit, agent } from "toolnexus"
|
|
316
|
-
|
|
317
|
-
const tk = await createToolkit({
|
|
318
|
-
agents: [agent({ card: "https://researcher.example.com/.well-known/agent-card.json" })],
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
// or add one at runtime (an Agent descriptor or a bare card URL):
|
|
322
|
-
await tk.addAgent("https://writer.example.com/.well-known/agent-card.json")
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
`agent({ card, headers?, timeout?, pollEvery? })` — `headers` support `${ENV}` expansion (never
|
|
326
|
-
logged); `timeout` defaults to 300000ms, `pollEvery` to 1000ms. A config object can also carry an
|
|
327
|
-
`agents` block (mirrors `mcpServers`). A failing agent is isolated — logged, contributes no tools,
|
|
328
|
-
never fatal.
|
|
329
|
-
|
|
330
|
-
**Inbound — serve your toolkit as an agent.** Opt in with the `a2a` profile; the Agent Card is
|
|
331
|
-
built from your **SKILL.md skills** (never raw tools):
|
|
332
|
-
|
|
333
|
-
```ts
|
|
334
|
-
const handle = await tk.serve("127.0.0.1:0", {
|
|
335
|
-
client: agent, // a createClient(...) instance fulfils each task
|
|
336
|
-
a2a: {
|
|
337
|
-
name: "research-agent",
|
|
338
|
-
description: "Answers research questions.",
|
|
339
|
-
// skills: ["hello-world"], // subset of skills to advertise; omit ⇒ all
|
|
340
|
-
store: "memory", // "memory" (default) | "file:<dir>" | a custom TaskStore
|
|
341
|
-
},
|
|
342
|
-
})
|
|
343
|
-
console.log(handle.url) // GET /.well-known/agent-card.json ; POST / (SendMessage / GetTask)
|
|
344
|
-
await handle.stop()
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
`serve(addr, { client, a2a?, onTask? })` fulfils each inbound `SendMessage` task via the client.
|
|
348
|
-
Task persistence is a pluggable `TaskStore` — in-memory default, `"file:<dir>"`, or your own. And
|
|
349
|
-
it **remembers**: a message's A2A `contextId` keys the conversation through `client.ask`, so a
|
|
350
|
-
peer's turns in the same context carry history via the client's `ConversationStore`.
|
|
351
|
-
|
|
352
|
-
## Serve as an MCP server (be a gateway)
|
|
353
|
-
|
|
354
|
-
The inbound mirror of A2A: expose your **whole toolkit as an MCP server** so any MCP client — an IDE,
|
|
355
|
-
another agent, a remote host — can call its tools. Point toolnexus at N MCP servers + skills + your
|
|
356
|
-
own functions, then re-expose the union as **one** MCP server. Unlike A2A (which advertises skills and
|
|
357
|
-
runs the client loop), the MCP client *is* the LLM host, so each `tools/call` dispatches straight to
|
|
358
|
-
`Tool.execute` — no `client`, no tasks, no store.
|
|
42
|
+
Add real tool sources by pointing at them:
|
|
359
43
|
|
|
360
44
|
```ts
|
|
361
|
-
|
|
362
|
-
const srv = await tk.serve("127.0.0.1:0", {
|
|
363
|
-
mcp: { name: "my-gateway" /*, tools: ["echo"] // subset; omit ⇒ every toolkit tool */ },
|
|
364
|
-
onCall: (ev) => console.error(ev.name, ev.ms, ev.isError),
|
|
365
|
-
})
|
|
366
|
-
console.log(srv.url + "/mcp") // connect any MCP client here
|
|
367
|
-
await srv.stop()
|
|
45
|
+
const tk = await createToolkit({ mcp: "mcp.json", skills: ["skills"] })
|
|
368
46
|
```
|
|
369
47
|
|
|
370
|
-
`
|
|
371
|
-
`
|
|
372
|
-
|
|
373
|
-
streamable-HTTP; a stdio transport for local clients like Claude Desktop is a planned follow-up.)
|
|
374
|
-
|
|
375
|
-
## Bring your own loop
|
|
376
|
-
|
|
377
|
-
Don't want the host loop? Use the schema adapters and execute calls yourself:
|
|
378
|
-
|
|
379
|
-
```ts
|
|
380
|
-
const tools = tk.toOpenAI() // or tk.toAnthropic() / tk.toGemini()
|
|
381
|
-
const system = tk.skillsPrompt() // skills catalog for your system prompt
|
|
382
|
-
// when the model returns a tool call { name, arguments }:
|
|
383
|
-
const res = await tk.execute(name, args) // → { output, isError, metadata }
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
## The four sources
|
|
387
|
-
|
|
388
|
-
| Source | How |
|
|
389
|
-
|--------|-----|
|
|
390
|
-
| **MCP servers** | an `mcp.json` (`mcpServers`/`servers`/`mcp`); local stdio + remote streamable-HTTP, `headers` for auth |
|
|
391
|
-
| **Agent skills** | a folder of `<name>/SKILL.md`; a `skill` tool loads each on demand + a system-prompt catalog |
|
|
392
|
-
| **Native tools** | `defineTool({...})` — a function becomes a tool |
|
|
393
|
-
| **HTTP / REST** | `httpTool({...})` — an endpoint becomes a tool, `${ENV}` headers |
|
|
394
|
-
|
|
395
|
-
All appear as one uniform `Tool` in `tk.tools()`. Built-ins are a fifth source; remote A2A agents
|
|
396
|
-
a sixth; in-process **sub-agents** a seventh (below).
|
|
397
|
-
|
|
398
|
-
## Sub-agents & teams
|
|
399
|
-
|
|
400
|
-
An **Agent is a Tool**: a system prompt × a scoped toolkit view × the client loop. One agent
|
|
401
|
-
delegates to another **in-process** via one `task` tool — isolated context, one result back,
|
|
402
|
-
tokens rolled up, hierarchical budgets, durable suspension (`SPEC.md §7D`). The surface lives in
|
|
403
|
-
the **`agents`** namespace (never the A2A `agent` above):
|
|
404
|
-
|
|
405
|
-
```ts
|
|
406
|
-
import { agents } from "toolnexus"
|
|
407
|
-
|
|
408
|
-
const explore = agents.agent("explore", {
|
|
409
|
-
does: "read-only research",
|
|
410
|
-
uses: { tools: [lookup] }, // this child sees ONLY these tools
|
|
411
|
-
})
|
|
412
|
-
const coder = agents.agent("coder", {
|
|
413
|
-
does: "implements changes",
|
|
414
|
-
soulFile: "./AGENTS.md", // identity file → system prompt
|
|
415
|
-
team: [explore], // team = the task tool's only targets; no team ⇒ no task tool
|
|
416
|
-
budget: { maxTokens: 10_000 },
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
const r = await coder.run("fix the failing test", {
|
|
420
|
-
llm: { baseUrl: "https://openrouter.ai/api/v1", style: "openai", model: "openai/gpt-4o-mini" },
|
|
421
|
-
})
|
|
422
|
-
console.log(r.status, r.text, r.totalTokens) // "done" | "incomplete" | "pending", final text, tree-wide tokens
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
`agents.asTool(...)` drops an agent back into `extraTools` for the classic API. Full guide:
|
|
426
|
-
[Sub-agents & teams](https://muthuishere.github.io/toolnexus/subagents/).
|
|
427
|
-
|
|
428
|
-
## API
|
|
48
|
+
- `mcp.json` is the standard Claude-desktop-style config (`mcpServers` / `servers` / `mcp` keys all accepted).
|
|
49
|
+
- `skills/` is a folder of `**/SKILL.md` files, loaded on demand through one `skill` tool.
|
|
50
|
+
- Remote MCP `headers` values expand `${ENV_VAR}` at call time and are never logged.
|
|
429
51
|
|
|
430
|
-
|
|
431
|
-
|--------|-------------|
|
|
432
|
-
| `createToolkit(opts)` | async factory → `Toolkit` (`mcpConfig`, `skillsDir`, `builtins`, `agents`, `extraTools`) |
|
|
433
|
-
| `createClient(opts)` | the unified host loop (`baseUrl`, `style`, `model`, `store?`, `hooks?`, `maxTurns?`, `retries?`, `timeoutMs?`, `onMetric?`) |
|
|
434
|
-
| `client.run(prompt, { toolkit, signal?, history? })` | stateless run → `RunResult` |
|
|
435
|
-
| `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 |
|
|
436
|
-
| `client.conversation({ toolkit })` | in-process multi-turn object — `.send(prompt)`, `.messages`, `.reset()` |
|
|
437
|
-
| `client.stream(prompt, { toolkit, id? })` | async-iterate `StreamEvent`s (text deltas, tool calls/results, usage, done); `id` ⇒ stateful |
|
|
438
|
-
| `client.metrics()` | Prometheus text exposition of cumulative metrics — mount at `GET /metrics` |
|
|
439
|
-
| `ConversationStore` | `get(id)` / `save(id, messages)` — implement for file/db; default `InMemoryConversationStore` |
|
|
440
|
-
| `tk.tools()` / `tk.get(name)` | the uniform tools |
|
|
441
|
-
| `tk.register(...tools)` | add native/http/custom tools at runtime |
|
|
442
|
-
| `tk.execute(name, args, ctx?)` | run a tool → `ToolResult` |
|
|
443
|
-
| `tk.skillsPrompt()` | system-prompt skill catalog |
|
|
444
|
-
| `tk.mcpStatus()` | per-server connection status |
|
|
445
|
-
| `tk.toOpenAI()` / `toAnthropic()` / `toGemini()` | provider tool schemas |
|
|
446
|
-
| `tk.serve(addr, { client?, a2a?, onTask?, mcp?, onCall? })` | serve the toolkit over HTTP — A2A agent and/or streamable-HTTP MCP server (`/mcp`) → `ServeHandle` |
|
|
447
|
-
| `tk.addAgent(cardUrl \| Agent)` | fetch a remote agent's card and register its skills as tools |
|
|
448
|
-
| `tk.close()` | disconnect MCP servers |
|
|
449
|
-
| `defineTool` / `httpTool` / `agent` | build native / HTTP / A2A tools |
|
|
450
|
-
| `InMemoryConversationStore` · `InMemoryTaskStore` / `FileTaskStore` | default stores (memory / A2A tasks) |
|
|
52
|
+
## Documentation
|
|
451
53
|
|
|
452
|
-
|
|
54
|
+
Everything else — the full surface, with runnable examples — lives on the docs site:
|
|
453
55
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
56
|
+
| | |
|
|
57
|
+
|---|---|
|
|
58
|
+
| **Start here** | [Quickstart](https://muthuishere.github.io/toolnexus/quickstart/) · [Concepts](https://muthuishere.github.io/toolnexus/concepts/) · [Install](https://muthuishere.github.io/toolnexus/install/) |
|
|
59
|
+
| **Tool sources** | [MCP](https://muthuishere.github.io/toolnexus/mcp/) · [Skills](https://muthuishere.github.io/toolnexus/skills/) · [Native](https://muthuishere.github.io/toolnexus/native/) · [HTTP](https://muthuishere.github.io/toolnexus/http/) · [Built-ins](https://muthuishere.github.io/toolnexus/builtins/) · [A2A](https://muthuishere.github.io/toolnexus/a2a/) |
|
|
60
|
+
| **The loop** | [Streaming](https://muthuishere.github.io/toolnexus/streaming/) · [Memory](https://muthuishere.github.io/toolnexus/memory/) · [Suspension](https://muthuishere.github.io/toolnexus/suspension/) · [Resilience](https://muthuishere.github.io/toolnexus/resilience/) · [Observability](https://muthuishere.github.io/toolnexus/observability/) |
|
|
61
|
+
| **Agents** | [Sub-agents & teams](https://muthuishere.github.io/toolnexus/subagents/) · [Personas](https://muthuishere.github.io/toolnexus/persona-agents/) · [Typed decisions](https://muthuishere.github.io/toolnexus/judge/) |
|
|
62
|
+
| **API reference** | [JavaScript](https://muthuishere.github.io/toolnexus/api/javascript/) |
|
|
63
|
+
| **Cookbook** | [Zero to agent](https://muthuishere.github.io/toolnexus/cookbook/zero-to-agent/) · [MCP servers](https://muthuishere.github.io/toolnexus/cookbook/mcp-servers/) · [Agent skills](https://muthuishere.github.io/toolnexus/cookbook/agent-skills/) · [Judge](https://muthuishere.github.io/toolnexus/cookbook/judge/) |
|
|
457
64
|
|
|
458
|
-
|
|
65
|
+
Contract across all seven ports: [`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md).
|