@4fu/arbor-tools 0.1.0 → 2026.1106.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 +8 -5
- package/catalog.d.ts +71 -11
- package/catalog.js +177 -31
- package/child-env.d.ts +24 -0
- package/child-env.js +32 -0
- package/code-exec.d.ts +23 -7
- package/code-exec.js +43 -15
- package/groups/core.d.ts +1 -1
- package/groups/core.js +21 -12
- package/groups/external.d.ts +25 -0
- package/groups/external.js +45 -0
- package/groups/files.d.ts +8 -0
- package/groups/files.js +45 -24
- package/groups/notes.d.ts +6 -0
- package/groups/notes.js +52 -25
- package/host.d.ts +0 -2
- package/index.d.ts +4 -1
- package/index.js +3 -0
- package/modules/modules.d.ts +8 -1
- package/modules/modules.js +69 -13
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -27,9 +27,11 @@ everything that takes judgment, such as git, merges and conflicts, to the agent.
|
|
|
27
27
|
guarantees what an agent cannot guarantee for itself - the record, isolation,
|
|
28
28
|
exactly-once effects, the checks, dispatch. Everything that takes judgment,
|
|
29
29
|
such as git, merges and conflicts, is the agent's.
|
|
30
|
-
- **A
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
- **A shared workspace with per-window checkouts.** The default workspace is
|
|
31
|
+
ovnfs: one shared folder every window reads, and a window opens its own
|
|
32
|
+
copy-on-write checkout of the project it needs, works there and merges back
|
|
33
|
+
with `apply`. It falls back to a plain directory per window when ovnfs is not
|
|
34
|
+
installed. It is not a sandbox: ovnfs isolates files, not processes.
|
|
33
35
|
- **A self-changing environment.** The model adds and changes skills, MCP
|
|
34
36
|
servers and resident services itself; a `reload` validates the change and
|
|
35
37
|
broadcasts it to every live window as a delta of names and descriptions.
|
|
@@ -118,7 +120,8 @@ window gets, and how a continuation keeps working in it, are in
|
|
|
118
120
|
| [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
|
|
119
121
|
| [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
|
|
120
122
|
| [`@4fu/arbor-segmenter-jieba`](packages/segmenter-jieba) | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
|
|
121
|
-
| [`@4fu/arbor-workspace-dir`](packages/workspace-dir) | the
|
|
123
|
+
| [`@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
|
+
| [`@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 |
|
|
122
125
|
| [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
|
|
123
126
|
| [`@4fu/arbor-supervisor-process`](packages/supervisor-process) | the default `Supervisor`: one host process per service, restart with backoff, logs |
|
|
124
127
|
| [`@4fu/arbor-judge-jev`](packages/judge-jev) | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
|
|
@@ -151,7 +154,7 @@ deployment can see what it would be taking on before it adopts one.
|
|
|
151
154
|
| [docs/coordination.md](docs/coordination.md) | the board, expectations, deliveries and broadcasts, staying current, effects |
|
|
152
155
|
| [docs/environment.md](docs/environment.md) | skills, MCP servers and services: using, changing, reload, built-in skills, deltas |
|
|
153
156
|
| [docs/memory.md](docs/memory.md) | notes, the active set, anchors, links |
|
|
154
|
-
| [docs/workspaces.md](docs/workspaces.md) | per-window
|
|
157
|
+
| [docs/workspaces.md](docs/workspaces.md) | the shared folder and per-window checkouts: isolation, continuity, what the agent does |
|
|
155
158
|
| [docs/tools.md](docs/tools.md) | direct tools and code mode modules |
|
|
156
159
|
| [docs/prompting.md](docs/prompting.md) | how every model-facing text is written: layers, notes, deliveries, errors, judge questions, terms |
|
|
157
160
|
| [docs/release.md](docs/release.md) | calendar versioning, the manual release steps, how a package is published |
|
package/catalog.d.ts
CHANGED
|
@@ -5,9 +5,15 @@
|
|
|
5
5
|
* A direct tool and a code mode function are thin wrappers of one service call:
|
|
6
6
|
* same arguments, same behaviour, same errors. `tool_search` and `tool_describe`
|
|
7
7
|
* read this catalog, so the lookup and the prompt never disagree. `code_exec`
|
|
8
|
-
* runs a script against it and refuses a script that names
|
|
8
|
+
* runs a script against it and refuses a script that names one of arbor's own
|
|
9
|
+
* direct tools.
|
|
10
|
+
*
|
|
11
|
+
* A tool declared from outside the core - a plugin's own, or a backend's
|
|
12
|
+
* injection of an outside harness's - is registered here too, and from that
|
|
13
|
+
* moment it is an ordinary direct tool: same declaration, same routing, same
|
|
14
|
+
* result cut (docs/tools.md#external-tools).
|
|
9
15
|
*/
|
|
10
|
-
import type { CallContext, DirectToolSpec, EnvItem, FunctionSpec, ModuleSpec, ToolCall, ToolGroup, ToolResult } from "@4fu/arbor-interfaces";
|
|
16
|
+
import type { CallContext, DirectToolSpec, EnvItem, ExternalTool, FunctionSpec, ModuleSpec, ToolCall, ToolGroup, ToolResult } from "@4fu/arbor-interfaces";
|
|
11
17
|
import { type CodeExecContext, type ScriptCatalog } from "./code-exec.ts";
|
|
12
18
|
import type { ToolServices } from "./host.ts";
|
|
13
19
|
export interface ToolCatalogOptions {
|
|
@@ -15,13 +21,52 @@ export interface ToolCatalogOptions {
|
|
|
15
21
|
/** Extra groups, from a plugin. */
|
|
16
22
|
groups?: readonly ToolGroup[];
|
|
17
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* One external tool the catalog did not register, because its name was already
|
|
26
|
+
* declared. A core tool always wins, and so does the external tool that arrived
|
|
27
|
+
* first; the collision is handed back rather than thrown, so the host records
|
|
28
|
+
* it with its rule and the operator sees it (docs/tools.md#external-tools).
|
|
29
|
+
*/
|
|
30
|
+
export interface ExternalToolRefusal {
|
|
31
|
+
/** The tool that was not registered. */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Who declared it: the plugin's name, or the backend's. */
|
|
34
|
+
source: string;
|
|
35
|
+
/** The rule that refused it: `tool.name_taken`. */
|
|
36
|
+
rule: string;
|
|
37
|
+
}
|
|
38
|
+
/** What one `tool_search` answers: names, and the items a name belongs to. */
|
|
39
|
+
export interface SearchAnswer {
|
|
40
|
+
modules: readonly string[];
|
|
41
|
+
functions: readonly string[];
|
|
42
|
+
skills: readonly EnvItem[];
|
|
43
|
+
tools: readonly DirectToolSpec[];
|
|
44
|
+
}
|
|
45
|
+
/** How many names one `tool_search` answer lists before it says there are more. */
|
|
46
|
+
export declare const SEARCH_CAP = 20;
|
|
18
47
|
export declare class ToolCatalog implements ToolGroup, ScriptCatalog {
|
|
19
48
|
readonly id = "catalog";
|
|
20
49
|
private readonly services;
|
|
50
|
+
/** The groups the core declares itself, which no external tool may shadow. */
|
|
51
|
+
private readonly own;
|
|
52
|
+
/** Every group, the core's own first, then what a deployment registered. */
|
|
21
53
|
private readonly groups;
|
|
22
54
|
constructor(options: ToolCatalogOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Register direct tools declared from outside the core - a plugin's own, or a
|
|
57
|
+
* backend's injection of the tools an outside harness registered
|
|
58
|
+
* (docs/tools.md#external-tools).
|
|
59
|
+
*
|
|
60
|
+
* A name the model already has is not registered: a core tool always wins,
|
|
61
|
+
* and so does the external tool that arrived first. The collisions come back
|
|
62
|
+
* with the rule that refused them, so the host records them for the operator
|
|
63
|
+
* instead of throwing.
|
|
64
|
+
*/
|
|
65
|
+
external(tools: readonly ExternalTool[], source: string): readonly ExternalToolRefusal[];
|
|
23
66
|
/** Every direct tool, one line each in the prompt. */
|
|
24
67
|
tools(): readonly DirectToolSpec[];
|
|
68
|
+
/** The two entry points to the modules, which every window has. */
|
|
69
|
+
private entryPoints;
|
|
25
70
|
/**
|
|
26
71
|
* Every module, one line each in the prompt. A name is declared once: every
|
|
27
72
|
* group that declares it lists its functions together, so a group that adds a
|
|
@@ -34,21 +79,36 @@ export declare class ToolCatalog implements ToolGroup, ScriptCatalog {
|
|
|
34
79
|
functions(): readonly FunctionSpec[];
|
|
35
80
|
/** The direct tool names, which a script may not call. */
|
|
36
81
|
directTools(): readonly string[];
|
|
82
|
+
/**
|
|
83
|
+
* The direct tools a script may call: every one the core does not declare
|
|
84
|
+
* itself (docs/tools.md#rules). A plugin's or a backend's tool is a
|
|
85
|
+
* capability of its own, like a module function, so a script reaches it the
|
|
86
|
+
* same way; arbor's own decisions and commands stay in the turn.
|
|
87
|
+
*/
|
|
88
|
+
scriptTools(): readonly DirectToolSpec[];
|
|
89
|
+
/** The direct tools arbor declares itself: its decisions and its commands. */
|
|
90
|
+
private ownTools;
|
|
37
91
|
/** One call, through the group that owns it. */
|
|
38
92
|
call(request: ToolCall, context: CallContext): Promise<ToolResult>;
|
|
39
93
|
/** One `module.function` call from a script: the same path as a direct call. */
|
|
40
94
|
callModule(name: string, args: Record<string, unknown>, context: CodeExecContext): Promise<ToolResult>;
|
|
41
95
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
96
|
+
* The call itself: the removal check, the two entry points, then the group
|
|
97
|
+
* that owns the name. A name no group owns is refused, because the prompt
|
|
98
|
+
* promises `tool_search` answers it (docs/tools.md#rules).
|
|
99
|
+
*/
|
|
100
|
+
private route;
|
|
101
|
+
/** The result as it leaves: the tool that was called, and the call's own id. */
|
|
102
|
+
private named;
|
|
103
|
+
/**
|
|
104
|
+
* `tool_search`: keywords, an exact `module.function` or tool name, or
|
|
105
|
+
* nothing. An exact module name answers with that module's functions, a
|
|
106
|
+
* direct tool answers with its description, and a skill's name or keywords
|
|
107
|
+
* answer with its path and how to read it
|
|
108
|
+
* (docs/environment.md#using-a-skill-or-a-server). A name is matched exactly
|
|
109
|
+
* first, then by prefix, then by description.
|
|
46
110
|
*/
|
|
47
|
-
search(keywords?: string):
|
|
48
|
-
modules: readonly string[];
|
|
49
|
-
functions: readonly string[];
|
|
50
|
-
skills: readonly EnvItem[];
|
|
51
|
-
};
|
|
111
|
+
search(keywords?: string): SearchAnswer;
|
|
52
112
|
/** `tool_describe`: one function's signature and description. */
|
|
53
113
|
describe(name: string): string | undefined;
|
|
54
114
|
/** `code_exec`: run one script against this catalog. */
|
package/catalog.js
CHANGED
|
@@ -5,22 +5,35 @@
|
|
|
5
5
|
* A direct tool and a code mode function are thin wrappers of one service call:
|
|
6
6
|
* same arguments, same behaviour, same errors. `tool_search` and `tool_describe`
|
|
7
7
|
* read this catalog, so the lookup and the prompt never disagree. `code_exec`
|
|
8
|
-
* runs a script against it and refuses a script that names
|
|
8
|
+
* runs a script against it and refuses a script that names one of arbor's own
|
|
9
|
+
* direct tools.
|
|
10
|
+
*
|
|
11
|
+
* A tool declared from outside the core - a plugin's own, or a backend's
|
|
12
|
+
* injection of an outside harness's - is registered here too, and from that
|
|
13
|
+
* moment it is an ordinary direct tool: same declaration, same routing, same
|
|
14
|
+
* result cut (docs/tools.md#external-tools).
|
|
9
15
|
*/
|
|
10
16
|
import { textOf } from "@4fu/arbor-prompt";
|
|
11
17
|
import { directToolIn, runScript } from "./code-exec.js";
|
|
18
|
+
import { boundResult } from "./bound.js";
|
|
12
19
|
import { CoreToolGroup } from "./groups/core.js";
|
|
13
20
|
import { EnvToolGroup } from "./groups/env.js";
|
|
21
|
+
import { ExternalToolGroup } from "./groups/external.js";
|
|
14
22
|
import { FileToolGroup } from "./groups/files.js";
|
|
15
23
|
import { NoteToolGroup } from "./groups/notes.js";
|
|
16
24
|
import { BoardModuleGroup, ChannelsModuleGroup, EnvModuleGroup, JournalModuleGroup, NotesModuleGroup, ScheduleModuleGroup, TasksModuleGroup, WindowsModuleGroup, WorkspaceModuleGroup, } from "./modules/modules.js";
|
|
25
|
+
/** How many names one `tool_search` answer lists before it says there are more. */
|
|
26
|
+
export const SEARCH_CAP = 20;
|
|
17
27
|
export class ToolCatalog {
|
|
18
28
|
id = "catalog";
|
|
19
29
|
services;
|
|
30
|
+
/** The groups the core declares itself, which no external tool may shadow. */
|
|
31
|
+
own;
|
|
32
|
+
/** Every group, the core's own first, then what a deployment registered. */
|
|
20
33
|
groups;
|
|
21
34
|
constructor(options) {
|
|
22
35
|
this.services = options.services;
|
|
23
|
-
this.
|
|
36
|
+
this.own = [
|
|
24
37
|
new CoreToolGroup(options.services),
|
|
25
38
|
new FileToolGroup(options.services),
|
|
26
39
|
new NoteToolGroup(options.services),
|
|
@@ -34,16 +47,49 @@ export class ToolCatalog {
|
|
|
34
47
|
new WorkspaceModuleGroup(options.services),
|
|
35
48
|
new EnvToolGroup(options.services),
|
|
36
49
|
new EnvModuleGroup(options.services),
|
|
50
|
+
];
|
|
51
|
+
this.groups = [
|
|
52
|
+
...this.own,
|
|
37
53
|
// What the workspace implementation contributes: a capability the core
|
|
38
54
|
// does not name, such as showing another window's directory.
|
|
39
55
|
...(options.services.workspace?.groups?.(options.services.table) ?? []),
|
|
40
56
|
...(options.groups ?? []),
|
|
41
57
|
];
|
|
42
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Register direct tools declared from outside the core - a plugin's own, or a
|
|
61
|
+
* backend's injection of the tools an outside harness registered
|
|
62
|
+
* (docs/tools.md#external-tools).
|
|
63
|
+
*
|
|
64
|
+
* A name the model already has is not registered: a core tool always wins,
|
|
65
|
+
* and so does the external tool that arrived first. The collisions come back
|
|
66
|
+
* with the rule that refused them, so the host records them for the operator
|
|
67
|
+
* instead of throwing.
|
|
68
|
+
*/
|
|
69
|
+
external(tools, source) {
|
|
70
|
+
const taken = new Set([...this.tools().map((tool) => tool.name), ...this.modules().map((module) => module.name)]);
|
|
71
|
+
const refused = [];
|
|
72
|
+
const declared = [];
|
|
73
|
+
for (const tool of tools) {
|
|
74
|
+
if (taken.has(tool.name)) {
|
|
75
|
+
refused.push({ name: tool.name, source, rule: "tool.name_taken" });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
taken.add(tool.name);
|
|
79
|
+
declared.push(tool);
|
|
80
|
+
}
|
|
81
|
+
if (declared.length > 0)
|
|
82
|
+
this.groups.push(new ExternalToolGroup(declared, source, this.services));
|
|
83
|
+
return refused;
|
|
84
|
+
}
|
|
43
85
|
/** Every direct tool, one line each in the prompt. */
|
|
44
86
|
tools() {
|
|
87
|
+
return [...this.entryPoints(), ...this.groups.flatMap((group) => group.tools())];
|
|
88
|
+
}
|
|
89
|
+
/** The two entry points to the modules, which every window has. */
|
|
90
|
+
entryPoints() {
|
|
45
91
|
const table = this.services.table;
|
|
46
|
-
|
|
92
|
+
return [
|
|
47
93
|
{
|
|
48
94
|
name: "code_exec",
|
|
49
95
|
description: lookup(table, "tool.code_exec", {}),
|
|
@@ -55,7 +101,6 @@ export class ToolCatalog {
|
|
|
55
101
|
parameters: { keywords: { type: "string", description: lookup(table, "param.keywords", {}) } },
|
|
56
102
|
},
|
|
57
103
|
];
|
|
58
|
-
return [...entryPoints, ...this.groups.flatMap((group) => group.tools())];
|
|
59
104
|
}
|
|
60
105
|
/**
|
|
61
106
|
* Every module, one line each in the prompt. A name is declared once: every
|
|
@@ -86,10 +131,54 @@ export class ToolCatalog {
|
|
|
86
131
|
}
|
|
87
132
|
/** The direct tool names, which a script may not call. */
|
|
88
133
|
directTools() {
|
|
89
|
-
return this.
|
|
134
|
+
return this.ownTools().map((tool) => tool.name);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The direct tools a script may call: every one the core does not declare
|
|
138
|
+
* itself (docs/tools.md#rules). A plugin's or a backend's tool is a
|
|
139
|
+
* capability of its own, like a module function, so a script reaches it the
|
|
140
|
+
* same way; arbor's own decisions and commands stay in the turn.
|
|
141
|
+
*/
|
|
142
|
+
scriptTools() {
|
|
143
|
+
const own = new Set(this.directTools());
|
|
144
|
+
return this.tools().filter((tool) => !own.has(tool.name));
|
|
145
|
+
}
|
|
146
|
+
/** The direct tools arbor declares itself: its decisions and its commands. */
|
|
147
|
+
ownTools() {
|
|
148
|
+
return [...this.entryPoints(), ...this.own.flatMap((group) => group.tools())];
|
|
90
149
|
}
|
|
91
150
|
/** One call, through the group that owns it. */
|
|
92
151
|
async call(request, context) {
|
|
152
|
+
const result = this.named(request, await this.route(request, context));
|
|
153
|
+
// The one exit every direct call leaves by: one head-and-tail cut, for
|
|
154
|
+
// every tool, with the whole kept in the workspace
|
|
155
|
+
// (docs/prompting.md#information-density).
|
|
156
|
+
if (!result.ok)
|
|
157
|
+
return result;
|
|
158
|
+
return {
|
|
159
|
+
...result,
|
|
160
|
+
content: await boundResult(result.content, {
|
|
161
|
+
...(context.workspace === undefined ? {} : { workspace: context.workspace.path }),
|
|
162
|
+
...(context.call === undefined ? {} : { callId: context.call }),
|
|
163
|
+
table: this.services.table,
|
|
164
|
+
}),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/** One `module.function` call from a script: the same path as a direct call. */
|
|
168
|
+
async callModule(name, args, context) {
|
|
169
|
+
// No cut here: the script reads the whole answer and cuts what it returns.
|
|
170
|
+
return this.named({ name, args, callId: context.call ?? "script" }, await this.route({
|
|
171
|
+
name,
|
|
172
|
+
args,
|
|
173
|
+
callId: context.call ?? "script",
|
|
174
|
+
}, { window: context.window }));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The call itself: the removal check, the two entry points, then the group
|
|
178
|
+
* that owns the name. A name no group owns is refused, because the prompt
|
|
179
|
+
* promises `tool_search` answers it (docs/tools.md#rules).
|
|
180
|
+
*/
|
|
181
|
+
async route(request, context) {
|
|
93
182
|
// A removal never changes a prompt, but a call into what it removed is
|
|
94
183
|
// refused with the ref of the change that removed it
|
|
95
184
|
// (docs/environment.md#deltas-in-a-window).
|
|
@@ -135,40 +224,51 @@ export class ToolCatalog {
|
|
|
135
224
|
}
|
|
136
225
|
return owner.call(request, context);
|
|
137
226
|
}
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
return
|
|
227
|
+
/** The result as it leaves: the tool that was called, and the call's own id. */
|
|
228
|
+
named(request, result) {
|
|
229
|
+
return {
|
|
230
|
+
...result,
|
|
231
|
+
callId: result.callId === "" ? request.callId : result.callId,
|
|
232
|
+
tool: result.tool ?? request.name,
|
|
233
|
+
};
|
|
141
234
|
}
|
|
142
235
|
/**
|
|
143
|
-
* `tool_search`: keywords, an exact `module.function
|
|
144
|
-
* module name answers with that module's functions,
|
|
145
|
-
*
|
|
146
|
-
*
|
|
236
|
+
* `tool_search`: keywords, an exact `module.function` or tool name, or
|
|
237
|
+
* nothing. An exact module name answers with that module's functions, a
|
|
238
|
+
* direct tool answers with its description, and a skill's name or keywords
|
|
239
|
+
* answer with its path and how to read it
|
|
240
|
+
* (docs/environment.md#using-a-skill-or-a-server). A name is matched exactly
|
|
241
|
+
* first, then by prefix, then by description.
|
|
147
242
|
*/
|
|
148
243
|
search(keywords) {
|
|
149
244
|
const modules = this.modules();
|
|
150
245
|
const functions = this.functions();
|
|
246
|
+
const tools = this.tools();
|
|
151
247
|
const skills = this.services.environment === undefined ? [] : this.services.envCatalog().items.filter((item) => item.kind === "skill");
|
|
152
248
|
if (keywords === undefined || keywords.trim() === "") {
|
|
153
|
-
return { modules: modules.map((module) => module.name), functions: [], skills };
|
|
249
|
+
return { modules: modules.map((module) => module.name), functions: [], skills, tools: [] };
|
|
154
250
|
}
|
|
155
251
|
const wanted = keywords.trim();
|
|
156
252
|
const named = modules.filter((module) => module.name === wanted);
|
|
157
253
|
if (wanted.includes(".")) {
|
|
158
|
-
|
|
159
|
-
|
|
254
|
+
return {
|
|
255
|
+
modules: named.map((module) => module.name),
|
|
256
|
+
functions: functions.filter((fn) => fn.name === wanted).map((fn) => fn.name),
|
|
257
|
+
skills: [],
|
|
258
|
+
tools: tools.filter((tool) => tool.name === wanted),
|
|
259
|
+
};
|
|
160
260
|
}
|
|
261
|
+
const rankedFunctions = ranked(wanted, functions, (fn) => [shortName(fn.name), fn.description]);
|
|
161
262
|
return {
|
|
162
|
-
modules: modules
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
functions:
|
|
167
|
-
|
|
168
|
-
fn.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
skills: skills.filter((skill) => skill.name.includes(wanted) || skill.description.includes(wanted)),
|
|
263
|
+
modules: ranked(wanted, modules, (module) => [module.name, module.description]).map((module) => module.name),
|
|
264
|
+
// A server's name answers with its functions, not only the module's line;
|
|
265
|
+
// any other keyword answers with the functions whose own name or
|
|
266
|
+
// description holds it.
|
|
267
|
+
functions: (named.length === 0
|
|
268
|
+
? rankedFunctions
|
|
269
|
+
: functions.filter((fn) => named.some((module) => module.functions.includes(fn)))).map((fn) => fn.name),
|
|
270
|
+
skills: ranked(wanted, skills, (skill) => [skill.name, skill.description]),
|
|
271
|
+
tools: ranked(wanted, tools, (tool) => [tool.name, tool.description]),
|
|
172
272
|
};
|
|
173
273
|
}
|
|
174
274
|
/** `tool_describe`: one function's signature and description. */
|
|
@@ -185,10 +285,13 @@ export class ToolCatalog {
|
|
|
185
285
|
const table = this.services.table;
|
|
186
286
|
const keywords = stringArg(request.args, "keywords");
|
|
187
287
|
const found = this.search(keywords === "" ? undefined : keywords);
|
|
188
|
-
const lines = [];
|
|
288
|
+
const lines = [textOf(table, "result.tool_search.lead")];
|
|
189
289
|
if (found.modules.length > 0) {
|
|
190
290
|
lines.push(textOf(table, "result.tool_search.modules", { names: found.modules.join(", ") }));
|
|
191
291
|
}
|
|
292
|
+
for (const tool of found.tools) {
|
|
293
|
+
lines.push(toolLine(table, tool));
|
|
294
|
+
}
|
|
192
295
|
for (const name of found.functions) {
|
|
193
296
|
lines.push(this.describe(name) ?? name);
|
|
194
297
|
}
|
|
@@ -198,11 +301,22 @@ export class ToolCatalog {
|
|
|
198
301
|
continue;
|
|
199
302
|
lines.push(textOf(table, "result.tool_search.skill", { name: skill.name, description: skill.description, path: skill.path }));
|
|
200
303
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
304
|
+
const listed = found.modules.length + found.tools.length + found.functions.length + found.skills.length;
|
|
305
|
+
if (listed === 0) {
|
|
306
|
+
lines.push(textOf(table, "result.tool_search.none"));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
// A module the answer names with no function to call is said out loud,
|
|
310
|
+
// so the model does not go looking for one.
|
|
311
|
+
if (found.modules.length > 0 && found.functions.length === 0 && found.tools.length === 0 && found.skills.length === 0) {
|
|
312
|
+
lines.push(textOf(table, "result.tool_search.none_functions"));
|
|
313
|
+
}
|
|
314
|
+
if (listed > SEARCH_CAP) {
|
|
315
|
+
lines.push(textOf(table, "result.tool_search.more", { count: listed - SEARCH_CAP }));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
lines.push(textOf(table, "result.tool_search.hint"));
|
|
319
|
+
return { callId: request.callId, ok: true, content: lines.join("\n") };
|
|
206
320
|
}
|
|
207
321
|
/** The guard a script goes through, exposed for tests. */
|
|
208
322
|
decisionIn(script) {
|
|
@@ -213,6 +327,38 @@ function stringArg(args, name) {
|
|
|
213
327
|
const value = args[name];
|
|
214
328
|
return typeof value === "string" ? value : "";
|
|
215
329
|
}
|
|
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
|
+
/**
|
|
340
|
+
* The matches, strongest first: an exact name, then a name that starts with
|
|
341
|
+
* the query, then a description that holds it. Ties keep the order the catalog
|
|
342
|
+
* declares them in, so the answer is deterministic.
|
|
343
|
+
*/
|
|
344
|
+
function ranked(wanted, items, keys) {
|
|
345
|
+
const found = [];
|
|
346
|
+
for (const item of items) {
|
|
347
|
+
const [name, description] = keys(item);
|
|
348
|
+
if (name === wanted)
|
|
349
|
+
found.push({ rank: 0, item });
|
|
350
|
+
else if (name.startsWith(wanted))
|
|
351
|
+
found.push({ rank: 1, item });
|
|
352
|
+
else if (description.includes(wanted))
|
|
353
|
+
found.push({ rank: 2, item });
|
|
354
|
+
}
|
|
355
|
+
return found.sort((left, right) => left.rank - right.rank).map((one) => one.item);
|
|
356
|
+
}
|
|
357
|
+
/** A function's own name, without the `module.` the catalog declares it with. */
|
|
358
|
+
function shortName(name) {
|
|
359
|
+
const dot = name.indexOf(".");
|
|
360
|
+
return dot === -1 ? name : name.slice(dot + 1);
|
|
361
|
+
}
|
|
216
362
|
function lookup(table, key, args) {
|
|
217
363
|
const template = table[key];
|
|
218
364
|
if (template === undefined)
|
package/child-env.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment a child process gets
|
|
3
|
+
* (docs/environment.md#what-a-subprocess-gets).
|
|
4
|
+
*
|
|
5
|
+
* Every MCP server, resident service and command a window runs inherits the
|
|
6
|
+
* host's whole environment, plus what its own configuration adds: the
|
|
7
|
+
* directory it works in, the item's own `env`, and the secrets it names by
|
|
8
|
+
* name. Nothing is withheld here on purpose: protection is one layer, the
|
|
9
|
+
* secret scrub, which replaces every configured secret value in whatever
|
|
10
|
+
* reaches the model or the journal (docs/environment.md#secrets).
|
|
11
|
+
*/
|
|
12
|
+
/** The variable that carries the directory a child process works in. */
|
|
13
|
+
export declare const WORKSPACE_ENV = "ARBOR_WORKSPACE";
|
|
14
|
+
/** What one child process is given beyond the host's own environment. */
|
|
15
|
+
export interface ChildEnvInput {
|
|
16
|
+
/** The directory it works in, when it has one. */
|
|
17
|
+
workspace?: string;
|
|
18
|
+
/** What its own configuration declares. */
|
|
19
|
+
env?: Record<string, string | undefined>;
|
|
20
|
+
/** The secrets its configuration names; their values are resolved by the caller. */
|
|
21
|
+
secrets?: Record<string, string | undefined>;
|
|
22
|
+
}
|
|
23
|
+
/** One child process's environment: the host's, then what its configuration adds. */
|
|
24
|
+
export declare function childEnvironment(input: ChildEnvInput): Record<string, string>;
|
package/child-env.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment a child process gets
|
|
3
|
+
* (docs/environment.md#what-a-subprocess-gets).
|
|
4
|
+
*
|
|
5
|
+
* Every MCP server, resident service and command a window runs inherits the
|
|
6
|
+
* host's whole environment, plus what its own configuration adds: the
|
|
7
|
+
* directory it works in, the item's own `env`, and the secrets it names by
|
|
8
|
+
* name. Nothing is withheld here on purpose: protection is one layer, the
|
|
9
|
+
* secret scrub, which replaces every configured secret value in whatever
|
|
10
|
+
* reaches the model or the journal (docs/environment.md#secrets).
|
|
11
|
+
*/
|
|
12
|
+
/** The variable that carries the directory a child process works in. */
|
|
13
|
+
export const WORKSPACE_ENV = "ARBOR_WORKSPACE";
|
|
14
|
+
/** One child process's environment: the host's, then what its configuration adds. */
|
|
15
|
+
export function childEnvironment(input) {
|
|
16
|
+
const env = {};
|
|
17
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
18
|
+
if (value !== undefined)
|
|
19
|
+
env[key] = value;
|
|
20
|
+
}
|
|
21
|
+
if (input.workspace !== undefined && input.workspace !== "")
|
|
22
|
+
env[WORKSPACE_ENV] = input.workspace;
|
|
23
|
+
for (const [key, value] of Object.entries(input.env ?? {})) {
|
|
24
|
+
if (value !== undefined)
|
|
25
|
+
env[key] = value;
|
|
26
|
+
}
|
|
27
|
+
for (const [key, value] of Object.entries(input.secrets ?? {})) {
|
|
28
|
+
if (value !== undefined && value !== "")
|
|
29
|
+
env[key] = value;
|
|
30
|
+
}
|
|
31
|
+
return env;
|
|
32
|
+
}
|
package/code-exec.d.ts
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
* `code_exec`: everything that is not a decision runs from a script
|
|
3
3
|
* (docs/tools.md#rules).
|
|
4
4
|
*
|
|
5
|
-
* A script that names
|
|
6
|
-
* the host. Calls from a script go through the same hooks as
|
|
7
|
-
* anchors, watermarks, quotas, validation and logging behave
|
|
8
|
-
* module function is the same service call the direct tool
|
|
5
|
+
* A script that names one of arbor's own direct tools is refused with the rule
|
|
6
|
+
* and never reaches the host. Calls from a script go through the same hooks as
|
|
7
|
+
* direct calls, so anchors, watermarks, quotas, validation and logging behave
|
|
8
|
+
* the same: every module function is the same service call the direct tool
|
|
9
|
+
* makes, and so is every tool a plugin or a backend declared.
|
|
9
10
|
*
|
|
10
11
|
* Two lookups are script globals: `tool_search` takes keywords, an exact
|
|
11
12
|
* `module.function` name, or nothing for every module; `tool_describe` returns
|
|
12
13
|
* one signature. Both read the catalog frozen with the window plus the
|
|
13
14
|
* environment deltas since.
|
|
14
15
|
*/
|
|
15
|
-
import type { ModuleSpec, ToolResult, WindowRef } from "@4fu/arbor-interfaces";
|
|
16
|
+
import type { DirectToolSpec, ModuleSpec, ToolResult, WindowRef } from "@4fu/arbor-interfaces";
|
|
16
17
|
import type { ToolServices } from "./host.ts";
|
|
17
18
|
/** How long one script may run before it is stopped. */
|
|
18
19
|
declare const SCRIPT_TIMEOUT_MS = 60000;
|
|
@@ -38,17 +39,32 @@ export interface ScriptCatalog {
|
|
|
38
39
|
description: string;
|
|
39
40
|
path?: string;
|
|
40
41
|
}[];
|
|
42
|
+
tools: readonly {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
}[];
|
|
41
46
|
};
|
|
42
47
|
/** `tool_describe`: one function's signature. */
|
|
43
48
|
describe(name: string): string | undefined;
|
|
44
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* The direct tool names a script may not call: arbor's own decisions and
|
|
51
|
+
* commands, which belong in the turn (docs/tools.md#rules).
|
|
52
|
+
*/
|
|
45
53
|
directTools(): readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* The direct tools a script may call: a plugin's or a backend's, which are
|
|
56
|
+
* capabilities of their own like a module function (docs/tools.md#rules).
|
|
57
|
+
*/
|
|
58
|
+
scriptTools(): readonly DirectToolSpec[];
|
|
46
59
|
}
|
|
47
60
|
/** Run one script in its own context, with the modules as globals. */
|
|
48
61
|
export declare function runScript(script: string, catalog: ScriptCatalog, services: ToolServices, context: CodeExecContext): Promise<ToolResult>;
|
|
49
62
|
/**
|
|
50
63
|
* The direct tool a script names, if any. This is a guard, not a sandbox: it
|
|
51
|
-
* keeps
|
|
64
|
+
* keeps one of arbor's own decisions or commands out of a script, and the host
|
|
65
|
+
* still enforces every rule. A tool a plugin or a backend declared is not in
|
|
66
|
+
* the list - a script may call it, the way it calls a module function
|
|
67
|
+
* (docs/tools.md#rules).
|
|
52
68
|
*/
|
|
53
69
|
export declare function directToolIn(script: string, tools: readonly string[]): string | undefined;
|
|
54
70
|
export { SCRIPT_TIMEOUT_MS };
|