toolnexus 0.22.0 → 0.23.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 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, C#, Elixir and Clojure**. Built on
15
+ library, byte-identical, also in **Python, Go, Java, C#, Elixir, Clojure and Rust**. Built on
16
16
  `@modelcontextprotocol/sdk` (the MCP SDK opencode uses).
17
17
 
18
18
  ## Install
@@ -28,11 +28,11 @@ Built-in tools are on by default, so an empty toolkit can already act:
28
28
  ```ts
29
29
  import { createToolkit, createClient } from "toolnexus"
30
30
 
31
- const tk = await createToolkit() // 10 built-in tools, on by default
31
+ const tk = await createToolkit({}) // 10 built-in tools, on by default
32
32
  const agent = createClient({
33
33
  baseUrl: "https://openrouter.ai/api/v1", // any OpenAI- or Anthropic-style endpoint
34
34
  style: "openai", // or "anthropic"
35
- model: "openai/gpt-4o-mini",
35
+ model: "openai/gpt-4o-mini", // key: OPENROUTER_API_KEY (matches the host)
36
36
  })
37
37
 
38
38
  const { text } = await agent.run("What files are in this folder?", { toolkit: tk })
@@ -49,6 +49,52 @@ const tk = await createToolkit({ mcp: "mcp.json", skills: ["skills"] })
49
49
  - `skills/` is a folder of `**/SKILL.md` files, loaded on demand through one `skill` tool.
50
50
  - Remote MCP `headers` values expand `${ENV_VAR}` at call time and are never logged.
51
51
 
52
+ ## API key by host (BREAKING in 0.22)
53
+
54
+ A key is only ever sent to the host it belongs to. `apiKey` wins; else `apiKeyEnv` (the NAME of
55
+ an env var); else only the env var of the `baseUrl` host, over https — `api.openai.com` →
56
+ `OPENAI_API_KEY`, `api.anthropic.com` → `ANTHROPIC_API_KEY`, `openrouter.ai` →
57
+ `OPENROUTER_API_KEY`. **A local server, a proxy or any other host needs `apiKey` or `apiKeyEnv`**
58
+ (before 0.22 the first provider key in the environment was sent to any `baseUrl`):
59
+
60
+ ```ts
61
+ createClient({ baseUrl: "http://localhost:11434/v1", style: "openai", model: "llama3", apiKey: "YOUR_KEY_HERE" })
62
+ createClient({ baseUrl: "https://my-proxy.example/v1", style: "openai", model: "m", apiKeyEnv: "MY_PROXY_KEY" })
63
+ ```
64
+
65
+ ## Prompt cache and tool tiers
66
+
67
+ ```ts
68
+ const agent = createClient({
69
+ baseUrl, style, model,
70
+ promptCache: { key: "session-42" }, // anthropic cache breakpoints / openai prompt_cache_key
71
+ toolTiers: { core: ["read", "grep"] }, // the rest load on demand via tool_search / call_tool
72
+ })
73
+ const res = await agent.run(prompt, { toolkit: tk })
74
+ res.usage.cacheReadTokens // cache hits; anthropic context = prompt + cacheRead + cacheWrite
75
+ agent.toolTierPlan(tk) // { read: "core", bash: "listed", … }, frozen per toolkit
76
+ agent.resetToolTiers(tk) // rebuild at session start
77
+ ```
78
+
79
+ `toolTiers` keeps the tools array identical on every request, so the cached prefix survives a
80
+ large or growing toolkit. `call_tool` is unwrapped before hooks, so permission gates see the real
81
+ tool. Runnable: `npm run example:prompt-cache` ([`examples/prompt_cache.ts`](examples/prompt_cache.ts)).
82
+
83
+ ## Host additions (0.22)
84
+
85
+ - `loadMcp(cfg, { onServer })` — each MCP server reported the moment it connects or fails;
86
+ `mcpSource.resourceTools()` adds `list_mcp_resources` / `read_mcp_resource`.
87
+ - `waitFor(request, { signal })` — a pending question ends with its turn.
88
+ - Tool `hints` (`readOnly` / `destructive` / `openWorld`) on every builtin and MCP tool, passed to
89
+ `hooks.beforeTool` — allow, deny with an error result, or ask via `pending`.
90
+ - Opt-in builtins: `builtins: { background: new BackgroundShells(), webSearch, notebookEdit: true }`
91
+ (`bash_output` / `kill_shell`, `websearch`, `notebook_edit`); `grep` takes `context` / `before` /
92
+ `after`; `read` / `grep` report truncation in metadata.
93
+ - `agents.compactor({ onCompact })`, `agent.asToolWith(ctx => ({ hooks, waitFor }))`, runtime
94
+ `waitFor`, `ctx.callId` on tools, `listModels(opts)`.
95
+
96
+ ACP host hooks (permission choice, update stream, cancel, streaming) are Go-only for now.
97
+
52
98
  ## Simple judgments
53
99
 
54
100
  A thin layer over any `Classifier` (SPEC.md §8B); the wire request is byte-identical to
@@ -102,4 +148,4 @@ Everything else — the full surface, with runnable examples — lives on the do
102
148
  | **API reference** | [JavaScript](https://muthuishere.github.io/toolnexus/api/javascript/) |
103
149
  | **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/) |
104
150
 
105
- Contract across all seven ports: [`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md).
151
+ Contract across all eight ports: [`SPEC.md`](https://github.com/muthuishere/toolnexus/blob/main/SPEC.md).
package/dist/client.d.ts CHANGED
@@ -493,6 +493,9 @@ export declare class Client {
493
493
  private runToolDirect;
494
494
  /** ToolTiers `call_tool {name, arguments}` -> the real call; undefined when tiers are off. */
495
495
  private unwrapCall;
496
+ /** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
497
+ * inner arguments, the name hooks and stream events see (the transcript keeps the wrapper). */
498
+ private toolCallRecord;
496
499
  /** The stream's tool_call event, naming the real tool for a `call_tool` call. */
497
500
  private callEvent;
498
501
  private resultEvent;
package/dist/client.js CHANGED
@@ -722,6 +722,12 @@ export class Client {
722
722
  unwrapCall(name, args) {
723
723
  return this.opts.toolTiers ? unwrapCallTool(name, args) : undefined;
724
724
  }
725
+ /** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
726
+ * inner arguments, the name hooks and stream events see (the transcript keeps the wrapper). */
727
+ toolCallRecord(name, args, result) {
728
+ const inner = this.unwrapCall(name, args);
729
+ return { name: inner ? inner.name : name, args: inner ? inner.args : args, output: result.output, isError: result.isError, metadata: result.metadata };
730
+ }
725
731
  /** The stream's tool_call event, naming the real tool for a `call_tool` call. */
726
732
  callEvent(id, name, args) {
727
733
  const inner = this.unwrapCall(name, args);
@@ -897,7 +903,7 @@ export class Client {
897
903
  // and the later suspensions' placeholder results never enter the transcript (they re-suspend
898
904
  // on resume). Mirrors the streaming path.
899
905
  for (const s of settled) {
900
- toolCalls.push({ name: s.call.function.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata });
906
+ toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result));
901
907
  messages.push(toolMessage(s.call.id, s.call.function.name, s.result));
902
908
  if (s.halted)
903
909
  return this.pendingRun(runStart, lastModel, s.halted, messages, toolCalls, turns, usage);
@@ -1021,7 +1027,7 @@ export class Client {
1021
1027
  const content = [];
1022
1028
  let halted;
1023
1029
  for (const s of settled) {
1024
- toolCalls.push({ name: s.use.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata });
1030
+ toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result));
1025
1031
  content.push(toolResultBlock(s.use.id, s.result));
1026
1032
  if (s.halted) {
1027
1033
  halted = s.halted;
@@ -1145,13 +1151,13 @@ export class Client {
1145
1151
  const r = await this.resolvePending(toolkit, c.name, args, req, c.id, turn, signal);
1146
1152
  result = r.result;
1147
1153
  if (r.halted) {
1148
- toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
1154
+ toolCalls.push(this.toolCallRecord(c.name, args, result));
1149
1155
  messages.push(toolMessage(c.id, c.name, result));
1150
1156
  yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
1151
1157
  return;
1152
1158
  }
1153
1159
  }
1154
- toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
1160
+ toolCalls.push(this.toolCallRecord(c.name, args, result));
1155
1161
  messages.push(toolMessage(c.id, c.name, result));
1156
1162
  yield this.resultEvent(c.id, c.name, safeJson(c.args), result);
1157
1163
  }
@@ -1266,14 +1272,14 @@ export class Client {
1266
1272
  const r = await this.resolvePending(toolkit, u.name, args, req, u.id, turn, signal);
1267
1273
  result = r.result;
1268
1274
  if (r.halted) {
1269
- toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
1275
+ toolCalls.push(this.toolCallRecord(u.name, args, result));
1270
1276
  results.push(toolResultBlock(u.id, result));
1271
1277
  messages.push({ role: "user", content: results });
1272
1278
  yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) };
1273
1279
  return;
1274
1280
  }
1275
1281
  }
1276
- toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata });
1282
+ toolCalls.push(this.toolCallRecord(u.name, args, result));
1277
1283
  results.push(toolResultBlock(u.id, result));
1278
1284
  yield this.resultEvent(u.id, u.name, u.input, result);
1279
1285
  }
package/dist/toolkit.d.ts CHANGED
@@ -52,7 +52,7 @@ export declare class Toolkit {
52
52
  private readonly mcpServerConfig;
53
53
  private byName;
54
54
  private constructor();
55
- static create(opts: ToolkitOptions): Promise<Toolkit>;
55
+ static create(opts?: ToolkitOptions): Promise<Toolkit>;
56
56
  tools(): Tool[];
57
57
  get(name: string): Tool | undefined;
58
58
  /** Add a tool at runtime (native, http, or any custom Tool). First name wins. */
@@ -104,4 +104,4 @@ export declare class Toolkit {
104
104
  }[];
105
105
  close(): Promise<void>;
106
106
  }
107
- export declare function createToolkit(opts: ToolkitOptions): Promise<Toolkit>;
107
+ export declare function createToolkit(opts?: ToolkitOptions): Promise<Toolkit>;
package/dist/toolkit.js CHANGED
@@ -38,7 +38,7 @@ export class Toolkit {
38
38
  this.byName.set(t.name, t);
39
39
  }
40
40
  }
41
- static async create(opts) {
41
+ static async create(opts = {}) {
42
42
  const mcp = opts.mcpConfig !== undefined ? await loadMcp(opts.mcpConfig, { waitFor: opts.waitFor, signal: opts.signal }) : undefined;
43
43
  // Skills come from directories, in-memory data, and/or a lazy provider (§3,
44
44
  // S1). The provider is resolved here (create() is async) and merged with the
@@ -184,6 +184,6 @@ export class Toolkit {
184
184
  await this.mcp?.close();
185
185
  }
186
186
  }
187
- export function createToolkit(opts) {
187
+ export function createToolkit(opts = {}) {
188
188
  return Toolkit.create(opts);
189
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolnexus",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Dynamic MCP servers + agent skills as uniform tools for any LLM (opencode-style).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,10 +17,11 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "tsc -p tsconfig.json",
20
- "test": "npm run build && node --experimental-strip-types --test test/unit.test.ts test/agents.test.ts test/agent-hooks.test.ts test/agent-inprocess.test.ts test/home.test.ts test/compaction.test.ts test/loop.test.ts test/in-process.test.ts test/translate.test.ts test/multimodal.test.ts test/classifier.test.ts test/acp.test.ts test/issues.test.ts test/host-boundary.test.ts test/judge.test.ts test/batteries.test.ts test/host-engine.test.ts test/host-turn-durability.test.ts test/host-engine-asks.test.ts",
20
+ "test": "npm run build && node --experimental-strip-types --test test/unit.test.ts test/agents.test.ts test/agent-hooks.test.ts test/agent-inprocess.test.ts test/home.test.ts test/compaction.test.ts test/loop.test.ts test/in-process.test.ts test/translate.test.ts test/multimodal.test.ts test/classifier.test.ts test/acp.test.ts test/issues.test.ts test/host-boundary.test.ts test/judge.test.ts test/batteries.test.ts test/host-engine.test.ts test/host-turn-durability.test.ts test/host-engine-asks.test.ts test/examples.test.ts",
21
21
  "example": "node --experimental-strip-types examples/basic.ts",
22
22
  "example:judge": "node --experimental-strip-types examples/judge.ts",
23
- "example:multimodal": "node --experimental-strip-types examples/multimodal.ts"
23
+ "example:multimodal": "node --experimental-strip-types examples/multimodal.ts",
24
+ "example:prompt-cache": "node --experimental-strip-types examples/prompt_cache.ts"
24
25
  },
25
26
  "keywords": [
26
27
  "mcp",
package/src/client.ts CHANGED
@@ -995,6 +995,13 @@ export class Client {
995
995
  return this.opts.toolTiers ? unwrapCallTool(name, args) : undefined
996
996
  }
997
997
 
998
+ /** The RunResult.toolCalls entry: a `call_tool` call is recorded as the real tool with its
999
+ * inner arguments, the name hooks and stream events see (the transcript keeps the wrapper). */
1000
+ private toolCallRecord(name: string, args: Record<string, unknown>, result: ToolResult): ToolCallRecord {
1001
+ const inner = this.unwrapCall(name, args)
1002
+ return { name: inner ? inner.name : name, args: inner ? inner.args : args, output: result.output, isError: result.isError, metadata: result.metadata }
1003
+ }
1004
+
998
1005
  /** The stream's tool_call event, naming the real tool for a `call_tool` call. */
999
1006
  private callEvent(id: string, name: string, args: Record<string, unknown>): StreamEvent {
1000
1007
  const inner = this.unwrapCall(name, args)
@@ -1168,7 +1175,7 @@ export class Client {
1168
1175
  // and the later suspensions' placeholder results never enter the transcript (they re-suspend
1169
1176
  // on resume). Mirrors the streaming path.
1170
1177
  for (const s of settled) {
1171
- toolCalls.push({ name: s.call.function.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata })
1178
+ toolCalls.push(this.toolCallRecord(s.call.function.name, s.args, s.result))
1172
1179
  messages.push(toolMessage(s.call.id, s.call.function.name, s.result))
1173
1180
  if (s.halted) return this.pendingRun(runStart, lastModel, s.halted, messages, toolCalls, turns, usage)
1174
1181
  }
@@ -1288,7 +1295,7 @@ export class Client {
1288
1295
  const content: any[] = []
1289
1296
  let halted: Request | undefined
1290
1297
  for (const s of settled) {
1291
- toolCalls.push({ name: s.use.name, args: s.args, output: s.result.output, isError: s.result.isError, metadata: s.result.metadata })
1298
+ toolCalls.push(this.toolCallRecord(s.use.name, s.args, s.result))
1292
1299
  content.push(toolResultBlock(s.use.id, s.result))
1293
1300
  if (s.halted) { halted = s.halted; break }
1294
1301
  }
@@ -1392,13 +1399,13 @@ export class Client {
1392
1399
  const r = await this.resolvePending(toolkit, c.name, args, req, c.id, turn, signal)
1393
1400
  result = r.result
1394
1401
  if (r.halted) {
1395
- toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata })
1402
+ toolCalls.push(this.toolCallRecord(c.name, args, result))
1396
1403
  messages.push(toolMessage(c.id, c.name, result))
1397
1404
  yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) }
1398
1405
  return
1399
1406
  }
1400
1407
  }
1401
- toolCalls.push({ name: c.name, args, output: result.output, isError: result.isError, metadata: result.metadata })
1408
+ toolCalls.push(this.toolCallRecord(c.name, args, result))
1402
1409
  messages.push(toolMessage(c.id, c.name, result))
1403
1410
  yield this.resultEvent(c.id, c.name, safeJson(c.args), result)
1404
1411
  }
@@ -1495,14 +1502,14 @@ export class Client {
1495
1502
  const r = await this.resolvePending(toolkit, u.name, args, req, u.id, turn, signal)
1496
1503
  result = r.result
1497
1504
  if (r.halted) {
1498
- toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata })
1505
+ toolCalls.push(this.toolCallRecord(u.name, args, result))
1499
1506
  results.push(toolResultBlock(u.id, result))
1500
1507
  messages.push({ role: "user", content: results })
1501
1508
  yield { type: "done", result: this.pendingRun(runStart, lastModel, r.halted, messages, toolCalls, turns, usage) }
1502
1509
  return
1503
1510
  }
1504
1511
  }
1505
- toolCalls.push({ name: u.name, args, output: result.output, isError: result.isError, metadata: result.metadata })
1512
+ toolCalls.push(this.toolCallRecord(u.name, args, result))
1506
1513
  results.push(toolResultBlock(u.id, result))
1507
1514
  yield this.resultEvent(u.id, u.name, u.input, result)
1508
1515
  }
package/src/toolkit.ts CHANGED
@@ -79,7 +79,7 @@ export class Toolkit {
79
79
  }
80
80
  }
81
81
 
82
- static async create(opts: ToolkitOptions): Promise<Toolkit> {
82
+ static async create(opts: ToolkitOptions = {}): Promise<Toolkit> {
83
83
  const mcp = opts.mcpConfig !== undefined ? await loadMcp(opts.mcpConfig, { waitFor: opts.waitFor, signal: opts.signal }) : undefined
84
84
  // Skills come from directories, in-memory data, and/or a lazy provider (§3,
85
85
  // S1). The provider is resolved here (create() is async) and merged with the
@@ -250,6 +250,6 @@ export class Toolkit {
250
250
  }
251
251
  }
252
252
 
253
- export function createToolkit(opts: ToolkitOptions): Promise<Toolkit> {
253
+ export function createToolkit(opts: ToolkitOptions = {}): Promise<Toolkit> {
254
254
  return Toolkit.create(opts)
255
255
  }