@4fu/arbor-tools 2026.1106.1 → 2026.1107.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
@@ -56,7 +56,9 @@ npm install @4fu/arbor-core @4fu/arbor-journal-sqlite @4fu/arbor-notes-sqlite \
56
56
  ```
57
57
 
58
58
  `@4fu/arbor-plugin-http` is optional; it is the example plugin below.
59
- `@4fu/arbor-segmenter-jieba` is an optional peer of the journal.
59
+ `@4fu/arbor-segmenter-jieba` is an optional peer of the lexical package, and
60
+ `@4fu/arbor-search-zvec` an optional peer of the journal: absent either one, the
61
+ journal searches lexically and nothing else changes.
60
62
 
61
63
  ## A minimal host
62
64
 
@@ -120,6 +122,8 @@ window gets, and how a continuation keeps working in it, are in
120
122
  | [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
121
123
  | [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
122
124
  | [`@4fu/arbor-segmenter-jieba`](packages/segmenter-jieba) | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
125
+ | [`@4fu/arbor-lexical`](packages/lexical) | the lexical search the journal and notes share: word segmentation, the FTS5 helpers and the merge order |
126
+ | [`@4fu/arbor-search-zvec`](packages/search-zvec) | the optional semantic channel: event text embedded and searched by meaning with zvec |
123
127
  | [`@4fu/arbor-workspace-dir`](packages/workspace-dir) | the fallback `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place |
124
128
  | [`@4fu/arbor-workspace-ovnfs`](packages/workspace-ovnfs) | the default `Workspace` when ovnfs is installed: one shared folder, per-window copy-on-write checkouts the model opens and applies back |
125
129
  | [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
@@ -171,6 +175,7 @@ tarball would contain ([docs/release.md](docs/release.md)).
171
175
  | `packages/errors` | the error shape every refusal uses: the rule and the next action |
172
176
  | `packages/journal-sqlite` | the default `Journal` and `BlobStore` on SQLite |
173
177
  | `packages/segmenter-jieba` | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
178
+ | `packages/lexical` | the lexical search the journal and notes share: word segmentation, the FTS5 helpers and the merge order |
174
179
  | `packages/notes-sqlite` | the default `NoteStore` on SQLite, beside the journal |
175
180
  | `packages/judge-jev` | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
176
181
  | `packages/workspace-dir` | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place, and the lookup of another window's directory it contributes to the catalog |
package/catalog.d.ts CHANGED
@@ -49,9 +49,22 @@ export declare class ToolCatalog implements ToolGroup, ScriptCatalog {
49
49
  private readonly services;
50
50
  /** The groups the core declares itself, which no external tool may shadow. */
51
51
  private readonly own;
52
- /** Every group, the core's own first, then what a deployment registered. */
53
- private readonly groups;
52
+ /**
53
+ * The groups a deployment registered - the core's own, what the workspace
54
+ * implementation contributes, and a plugin's own groups - which no external
55
+ * tool may shadow either.
56
+ */
57
+ private readonly base;
58
+ /**
59
+ * One group per source that declared external tools, the source's name as the
60
+ * key. A refresh replaces the group its source declared before, so a tool the
61
+ * source dropped is no longer declared and no longer callable
62
+ * (docs/tools.md#external-tools-that-change).
63
+ */
64
+ private readonly externalGroups;
54
65
  constructor(options: ToolCatalogOptions);
66
+ /** Every group: the base, then one per source that declared external tools. */
67
+ private get groups();
55
68
  /**
56
69
  * Register direct tools declared from outside the core - a plugin's own, or a
57
70
  * backend's injection of the tools an outside harness registered
@@ -60,7 +73,12 @@ export declare class ToolCatalog implements ToolGroup, ScriptCatalog {
60
73
  * A name the model already has is not registered: a core tool always wins,
61
74
  * and so does the external tool that arrived first. The collisions come back
62
75
  * with the rule that refused them, so the host records them for the operator
63
- * instead of throwing.
76
+ * instead of throwing. The rule runs on every call, so every refresh of the
77
+ * set is checked the same way.
78
+ *
79
+ * What this source declared before is replaced, so a refresh that drops a
80
+ * tool leaves it undeclared and uncallable - and a tool the source declares
81
+ * again is not a collision with itself.
64
82
  */
65
83
  external(tools: readonly ExternalTool[], source: string): readonly ExternalToolRefusal[];
66
84
  /** Every direct tool, one line each in the prompt. */
package/catalog.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * moment it is an ordinary direct tool: same declaration, same routing, same
14
14
  * result cut (docs/tools.md#external-tools).
15
15
  */
16
- import { textOf } from "@4fu/arbor-prompt";
16
+ import { textOf, toolLine } from "@4fu/arbor-prompt";
17
17
  import { directToolIn, runScript } from "./code-exec.js";
18
18
  import { boundResult } from "./bound.js";
19
19
  import { CoreToolGroup } from "./groups/core.js";
@@ -29,8 +29,19 @@ export class ToolCatalog {
29
29
  services;
30
30
  /** The groups the core declares itself, which no external tool may shadow. */
31
31
  own;
32
- /** Every group, the core's own first, then what a deployment registered. */
33
- groups;
32
+ /**
33
+ * The groups a deployment registered - the core's own, what the workspace
34
+ * implementation contributes, and a plugin's own groups - which no external
35
+ * tool may shadow either.
36
+ */
37
+ base;
38
+ /**
39
+ * One group per source that declared external tools, the source's name as the
40
+ * key. A refresh replaces the group its source declared before, so a tool the
41
+ * source dropped is no longer declared and no longer callable
42
+ * (docs/tools.md#external-tools-that-change).
43
+ */
44
+ externalGroups = new Map();
34
45
  constructor(options) {
35
46
  this.services = options.services;
36
47
  this.own = [
@@ -48,7 +59,7 @@ export class ToolCatalog {
48
59
  new EnvToolGroup(options.services),
49
60
  new EnvModuleGroup(options.services),
50
61
  ];
51
- this.groups = [
62
+ this.base = [
52
63
  ...this.own,
53
64
  // What the workspace implementation contributes: a capability the core
54
65
  // does not name, such as showing another window's directory.
@@ -56,6 +67,10 @@ export class ToolCatalog {
56
67
  ...(options.groups ?? []),
57
68
  ];
58
69
  }
70
+ /** Every group: the base, then one per source that declared external tools. */
71
+ get groups() {
72
+ return [...this.base, ...this.externalGroups.values()];
73
+ }
59
74
  /**
60
75
  * Register direct tools declared from outside the core - a plugin's own, or a
61
76
  * backend's injection of the tools an outside harness registered
@@ -64,10 +79,19 @@ export class ToolCatalog {
64
79
  * A name the model already has is not registered: a core tool always wins,
65
80
  * and so does the external tool that arrived first. The collisions come back
66
81
  * with the rule that refused them, so the host records them for the operator
67
- * instead of throwing.
82
+ * instead of throwing. The rule runs on every call, so every refresh of the
83
+ * set is checked the same way.
84
+ *
85
+ * What this source declared before is replaced, so a refresh that drops a
86
+ * tool leaves it undeclared and uncallable - and a tool the source declares
87
+ * again is not a collision with itself.
68
88
  */
69
89
  external(tools, source) {
70
- const taken = new Set([...this.tools().map((tool) => tool.name), ...this.modules().map((module) => module.name)]);
90
+ const others = [...this.base, ...[...this.externalGroups].filter(([name]) => name !== source).map(([, group]) => group)];
91
+ const taken = new Set([
92
+ ...others.flatMap((group) => group.tools()).map((tool) => tool.name),
93
+ ...others.flatMap((group) => group.modules()).map((module) => module.name),
94
+ ]);
71
95
  const refused = [];
72
96
  const declared = [];
73
97
  for (const tool of tools) {
@@ -78,8 +102,7 @@ export class ToolCatalog {
78
102
  taken.add(tool.name);
79
103
  declared.push(tool);
80
104
  }
81
- if (declared.length > 0)
82
- this.groups.push(new ExternalToolGroup(declared, source, this.services));
105
+ this.externalGroups.set(source, new ExternalToolGroup(declared, source, this.services));
83
106
  return refused;
84
107
  }
85
108
  /** Every direct tool, one line each in the prompt. */
@@ -290,6 +313,7 @@ export class ToolCatalog {
290
313
  lines.push(textOf(table, "result.tool_search.modules", { names: found.modules.join(", ") }));
291
314
  }
292
315
  for (const tool of found.tools) {
316
+ // The renderer's own rule, so the lookup and the prompt never disagree.
293
317
  lines.push(toolLine(table, tool));
294
318
  }
295
319
  for (const name of found.functions) {
@@ -327,15 +351,6 @@ function stringArg(args, name) {
327
351
  const value = args[name];
328
352
  return typeof value === "string" ? value : "";
329
353
  }
330
- /**
331
- * One tool's line, the same one the tools list writes: a tool declared from
332
- * outside the core says where it comes from, so the model knows whose it is
333
- * wherever it meets the name (docs/tools.md#external-tools).
334
- */
335
- function toolLine(table, tool) {
336
- const key = tool.source === undefined ? "system.tool" : "system.tool.source";
337
- return lookup(table, key, { name: tool.name, description: tool.description, source: tool.source ?? "" });
338
- }
339
354
  /**
340
355
  * The matches, strongest first: an exact name, then a name that starts with
341
356
  * the query, then a description that holds it. Ties keep the order the catalog
@@ -9,7 +9,9 @@
9
9
  * every direct tool.
10
10
  *
11
11
  * The `source` rides on each spec, so the tools list the model reads says where
12
- * a tool comes from and `tool_search` answers with the same line.
12
+ * a tool comes from and `tool_search` answers with the same line. So does the
13
+ * JSON Schema the outside harness declared, carried through untouched
14
+ * (docs/tools.md#external-tools).
13
15
  */
14
16
  import type { CallContext, DirectToolSpec, ExternalTool, ToolCall, ToolGroup, ToolResult } from "@4fu/arbor-interfaces";
15
17
  import { type ToolServices } from "../host.ts";
@@ -9,7 +9,9 @@
9
9
  * every direct tool.
10
10
  *
11
11
  * The `source` rides on each spec, so the tools list the model reads says where
12
- * a tool comes from and `tool_search` answers with the same line.
12
+ * a tool comes from and `tool_search` answers with the same line. So does the
13
+ * JSON Schema the outside harness declared, carried through untouched
14
+ * (docs/tools.md#external-tools).
13
15
  */
14
16
  import { refused } from "../host.js";
15
17
  export class ExternalToolGroup {
@@ -27,7 +29,11 @@ export class ExternalToolGroup {
27
29
  return this.declared.map((tool) => ({
28
30
  name: tool.name,
29
31
  description: tool.description,
30
- parameters: tool.parameters,
32
+ // A tool that declares a JSON Schema of its own needs no restatement of
33
+ // it in arbor's shape: the schema is the declaration
34
+ // (docs/tools.md#external-tools).
35
+ parameters: tool.parameters ?? {},
36
+ ...(tool.schema === undefined ? {} : { schema: tool.schema }),
31
37
  source: this.source,
32
38
  }));
33
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4fu/arbor-tools",
3
- "version": "2026.1106.1",
3
+ "version": "2026.1107.0",
4
4
  "type": "module",
5
5
  "description": "The entry points: direct tools, code mode modules and the one tool catalog that routes both through the service layer.",
6
6
  "exports": {
@@ -54,8 +54,8 @@
54
54
  "access": "public"
55
55
  },
56
56
  "dependencies": {
57
- "@4fu/arbor-errors": "2026.1106.1",
58
- "@4fu/arbor-interfaces": "2026.1106.1",
59
- "@4fu/arbor-prompt": "2026.1106.1"
57
+ "@4fu/arbor-errors": "2026.1107.0",
58
+ "@4fu/arbor-interfaces": "2026.1107.0",
59
+ "@4fu/arbor-prompt": "2026.1107.0"
60
60
  }
61
61
  }