@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/code-exec.js CHANGED
@@ -2,10 +2,11 @@
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 a direct tool is refused with the rule and never reaches
6
- * the host. Calls from a script go through the same hooks as direct calls, so
7
- * anchors, watermarks, quotas, validation and logging behave the same: every
8
- * module function is the same service call the direct tool makes.
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
@@ -14,7 +15,6 @@
14
15
  */
15
16
  import vm from "node:vm";
16
17
  import { textOf } from "@4fu/arbor-prompt";
17
- import { boundResult } from "./bound.js";
18
18
  /** How long one script may run before it is stopped. */
19
19
  const SCRIPT_TIMEOUT_MS = 60000;
20
20
  /** Run one script in its own context, with the modules as globals. */
@@ -41,6 +41,22 @@ export async function runScript(script, catalog, services, context) {
41
41
  for (const module of catalog.modules()) {
42
42
  globals[module.name] = moduleFunctions(module, catalog, context);
43
43
  }
44
+ // A tool a plugin or a backend declared is a capability of its own, not one
45
+ // of arbor's decisions, so a script may call it - through the same path a
46
+ // module function takes (docs/tools.md#rules). A module's name and the
47
+ // script's own globals keep theirs: the catalog has already refused a tool
48
+ // whose name a module declares, and nothing here shadows `tool_search` or
49
+ // `console`.
50
+ for (const tool of catalog.scriptTools()) {
51
+ if (globals[tool.name] !== undefined)
52
+ continue;
53
+ globals[tool.name] = async (args = {}) => {
54
+ const result = await catalog.callModule(tool.name, args, context);
55
+ if (!result.ok)
56
+ throw new Error(result.error?.message ?? result.content);
57
+ return result.content;
58
+ };
59
+ }
44
60
  const sandbox = vm.createContext(globals);
45
61
  const wrapped = `(async () => {\n${script}\n})()`;
46
62
  let value;
@@ -49,6 +65,21 @@ export async function runScript(script, catalog, services, context) {
49
65
  value = await compiled.runInContext(sandbox, { timeout: SCRIPT_TIMEOUT_MS });
50
66
  }
51
67
  catch (error) {
68
+ // A script that outran the timeout gets its own rule: the message is the
69
+ // vm's, and the next action says what to do about a long one.
70
+ const timedOut = typeof error === "object" && error !== null && error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT";
71
+ if (timedOut) {
72
+ return {
73
+ callId: "",
74
+ ok: false,
75
+ content: "",
76
+ error: {
77
+ rule: "script.timeout",
78
+ message: textOf(table, "refused.script_timeout", { ms: SCRIPT_TIMEOUT_MS }),
79
+ nextAction: textOf(table, "next.script_timeout"),
80
+ },
81
+ };
82
+ }
52
83
  const message = error instanceof Error ? error.message : String(error);
53
84
  return {
54
85
  callId: "",
@@ -62,15 +93,9 @@ export async function runScript(script, catalog, services, context) {
62
93
  };
63
94
  }
64
95
  const text = value === undefined ? "" : typeof value === "string" ? value : JSON.stringify(value);
65
- return {
66
- callId: "",
67
- ok: true,
68
- content: await boundResult(text ?? "", {
69
- workspace: context.workspace,
70
- callId: context.call,
71
- table,
72
- }),
73
- };
96
+ // The whole answer goes back to the script's caller; the cut happens once,
97
+ // at the one exit every direct call leaves by (docs/prompting.md).
98
+ return { callId: "", ok: true, content: text ?? "" };
74
99
  }
75
100
  /** One module's functions, as the globals a script calls. */
76
101
  function moduleFunctions(module, catalog, context) {
@@ -87,7 +112,10 @@ function moduleFunctions(module, catalog, context) {
87
112
  }
88
113
  /**
89
114
  * The direct tool a script names, if any. This is a guard, not a sandbox: it
90
- * keeps a decision out of a script, and the host still enforces every rule.
115
+ * keeps one of arbor's own decisions or commands out of a script, and the host
116
+ * still enforces every rule. A tool a plugin or a backend declared is not in
117
+ * the list - a script may call it, the way it calls a module function
118
+ * (docs/tools.md#rules).
91
119
  */
92
120
  export function directToolIn(script, tools) {
93
121
  for (const tool of tools) {
package/groups/core.d.ts CHANGED
@@ -19,7 +19,7 @@ export declare class CoreToolGroup implements ToolGroup {
19
19
  private send;
20
20
  /**
21
21
  * `end` with unfinished work needs a handoff: current topics, progress,
22
- * pending items and agreements. The host knows whether work is open; the
22
+ * pending items and agreements. The host knows what is open and names it; the
23
23
  * tool does not guess.
24
24
  */
25
25
  private end;
package/groups/core.js CHANGED
@@ -68,27 +68,23 @@ export class CoreToolGroup {
68
68
  }
69
69
  /**
70
70
  * `end` with unfinished work needs a handoff: current topics, progress,
71
- * pending items and agreements. The host knows whether work is open; the
71
+ * pending items and agreements. The host knows what is open and names it; the
72
72
  * tool does not guess.
73
73
  */
74
74
  async end(request, window) {
75
75
  const table = this.services.table;
76
76
  const handoff = handoffOf(request.args.handoff);
77
- if (handoff === undefined && (await this.services.hasOpenWork(window))) {
78
- return refused("end.handoff.required", table, "end.no_handoff");
79
- }
80
77
  const result = await this.services.endWindow(window, handoff);
81
- return {
82
- ...result,
83
- callId: request.callId,
84
- content: textOf(table, "result.ended", { window }),
85
- };
78
+ if (!result.ok)
79
+ return { ...result, callId: request.callId };
80
+ return { ...result, callId: request.callId, content: textOf(table, "result.ended", { window }) };
86
81
  }
87
82
  async search(request, window) {
88
83
  const table = this.services.table;
89
84
  const query = stringArg(request.args, "query");
90
85
  const scope = stringArg(request.args, "scope");
91
- const outcome = await this.services.search(window, query, scope === "" ? undefined : scope.split(",").map((part) => part.trim()));
86
+ const wanted = scope === "" ? undefined : scope.split(",").map((part) => part.trim());
87
+ const outcome = await this.services.search(window, query, wanted);
92
88
  const lines = [];
93
89
  for (const hit of outcome.hits) {
94
90
  lines.push(`${hit.ref} ${hit.kind} ${hit.summary}`);
@@ -100,8 +96,21 @@ export class CoreToolGroup {
100
96
  lines.push(`${live.window} ${live.doing}`);
101
97
  }
102
98
  const body = lines.length === 0 ? textOf(table, "result.search.empty") : lines.join("\n");
103
- const total = textOf(table, "result.search.total", { total: outcome.total });
104
- return { callId: request.callId, ok: true, content: `${body}\n${total}\n${outcome.nextCall}` };
99
+ // What was searched, how much there is, and how to reach the rest: a limit
100
+ // that shapes a result without refusing is reported every time
101
+ // (docs/prompting.md#rules-surface-where-they-bite).
102
+ const more = outcome.total - lines.length;
103
+ return {
104
+ callId: request.callId,
105
+ ok: true,
106
+ content: [
107
+ body,
108
+ textOf(table, "result.search.total", { total: outcome.total }),
109
+ textOf(table, "result.search.scope", { scope: outcome.scope.join("、") }),
110
+ ...(more > 0 ? [textOf(table, "result.search.more", { count: more })] : []),
111
+ outcome.nextCall,
112
+ ].join("\n"),
113
+ };
105
114
  }
106
115
  notOwned(request) {
107
116
  return {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The group a plugin's or a backend's direct tools are registered as
3
+ * (docs/tools.md#external-tools).
4
+ *
5
+ * One group per source, holding every tool that source declared. Nothing here
6
+ * is special: the group declares its tools the way any group does and runs a
7
+ * call the way any group does, so the hooks, the journaling, the scrub and the
8
+ * one result cut around a call are the ones the host and the catalog apply to
9
+ * every direct tool.
10
+ *
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.
13
+ */
14
+ import type { CallContext, DirectToolSpec, ExternalTool, ToolCall, ToolGroup, ToolResult } from "@4fu/arbor-interfaces";
15
+ import { type ToolServices } from "../host.ts";
16
+ export declare class ExternalToolGroup implements ToolGroup {
17
+ readonly id: string;
18
+ private readonly declared;
19
+ private readonly source;
20
+ private readonly services;
21
+ constructor(declared: readonly ExternalTool[], source: string, services: Pick<ToolServices, "table">);
22
+ tools(): readonly DirectToolSpec[];
23
+ modules(): readonly [];
24
+ call(request: ToolCall, context: CallContext): Promise<ToolResult>;
25
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The group a plugin's or a backend's direct tools are registered as
3
+ * (docs/tools.md#external-tools).
4
+ *
5
+ * One group per source, holding every tool that source declared. Nothing here
6
+ * is special: the group declares its tools the way any group does and runs a
7
+ * call the way any group does, so the hooks, the journaling, the scrub and the
8
+ * one result cut around a call are the ones the host and the catalog apply to
9
+ * every direct tool.
10
+ *
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.
13
+ */
14
+ import { refused } from "../host.js";
15
+ export class ExternalToolGroup {
16
+ id;
17
+ declared;
18
+ source;
19
+ services;
20
+ constructor(declared, source, services) {
21
+ this.id = `external:${source}`;
22
+ this.declared = declared;
23
+ this.source = source;
24
+ this.services = services;
25
+ }
26
+ tools() {
27
+ return this.declared.map((tool) => ({
28
+ name: tool.name,
29
+ description: tool.description,
30
+ parameters: tool.parameters,
31
+ source: this.source,
32
+ }));
33
+ }
34
+ modules() {
35
+ return [];
36
+ }
37
+ async call(request, context) {
38
+ const tool = this.declared.find((one) => one.name === request.name);
39
+ // The catalog routes by name, so a call reaches this group only for a name
40
+ // it declares; the refusal keeps the answer in the core's error shape.
41
+ if (tool === undefined)
42
+ return refused("tool.not_owned", this.services.table, "unknown_tool", { name: request.name });
43
+ return tool.execute(request, context);
44
+ }
45
+ }
package/groups/files.d.ts CHANGED
@@ -17,6 +17,14 @@
17
17
  * stopped with `bash_stop`, through the host's own process path.
18
18
  */
19
19
  import type { CallContext, DirectToolSpec, ToolCall, ToolGroup, ToolResult } from "@4fu/arbor-interfaces";
20
+ /**
21
+ * The environment one command runs with: the host's environment, the window's
22
+ * own directory, and the extra environment the workspace
23
+ * implementation asks for. This is the one place a command's environment is
24
+ * built, so everything a deployment adds to it is added here
25
+ * (docs/environment.md#what-a-subprocess-gets).
26
+ */
27
+ export declare function bashEnvironment(cwd: string, extraEnv?: Record<string, string>): Record<string, string>;
20
28
  export declare class FileToolGroup implements ToolGroup {
21
29
  readonly id = "files";
22
30
  private readonly services;
package/groups/files.js CHANGED
@@ -20,13 +20,23 @@ import { spawn } from "node:child_process";
20
20
  import { mkdir, readFile, writeFile } from "node:fs/promises";
21
21
  import { dirname, isAbsolute, join, normalize, resolve } from "node:path";
22
22
  import { textOf } from "@4fu/arbor-prompt";
23
- import { boundResult } from "../bound.js";
23
+ import { childEnvironment } from "../child-env.js";
24
24
  import { envCommand } from "../env-stage.js";
25
25
  import { toolSpec } from "../host.js";
26
26
  /** How long `bash` waits before it hands the command to `bash_output`. */
27
27
  const BASH_WAIT_MS = 20000;
28
28
  /** How much one background job keeps in memory. */
29
29
  const JOB_BUFFER = 200000;
30
+ /**
31
+ * The environment one command runs with: the host's environment, the window's
32
+ * own directory, and the extra environment the workspace
33
+ * implementation asks for. This is the one place a command's environment is
34
+ * built, so everything a deployment adds to it is added here
35
+ * (docs/environment.md#what-a-subprocess-gets).
36
+ */
37
+ export function bashEnvironment(cwd, extraEnv) {
38
+ return childEnvironment({ workspace: cwd, env: extraEnv });
39
+ }
30
40
  export class FileToolGroup {
31
41
  id = "files";
32
42
  services;
@@ -119,13 +129,16 @@ export class FileToolGroup {
119
129
  // there and `path` is not a host path.
120
130
  return this.ran(request, await runToEnd(workspace.exec, command));
121
131
  }
122
- const job = this.start(command, workspace.path);
123
- await sleep(numberArg(request.args, "timeout", BASH_WAIT_MS));
124
- return {
125
- callId: request.callId,
126
- ok: job.done ? job.code === 0 : true,
127
- content: `${job.output}\n${textOf(table, "result.bash.started", { job: job.id })}`,
128
- };
132
+ const job = this.start(command, workspace.path, this.services.workspace?.bashEnv?.(window));
133
+ const wait = numberArg(request.args, "timeout", BASH_WAIT_MS);
134
+ await Promise.race([job.ended, sleep(wait)]);
135
+ // A command that ran to its end inside the wait window is answered with
136
+ // what it wrote; one that outlived it becomes a background job, and the
137
+ // result line says the window is not woken when it ends.
138
+ const content = job.done
139
+ ? job.output
140
+ : `${job.output}\n${textOf(table, "result.bash.started", { job: job.id, ms: wait })}`;
141
+ return { callId: request.callId, ok: job.done ? job.code === 0 : true, content };
129
142
  }
130
143
  /** One command that ran to its end: what it wrote, and whether it succeeded. */
131
144
  ran(request, outcome) {
@@ -174,15 +187,12 @@ export class FileToolGroup {
174
187
  const lines = text.split("\n");
175
188
  const offset = Math.max(0, numberArg(request.args, "offset", 0));
176
189
  const limit = Math.min(500, Math.max(1, numberArg(request.args, "limit", 400)));
177
- const body = await boundResult(lines.slice(offset, offset + limit).join("\n"), {
178
- workspace: workspace?.path ?? path,
179
- callId: request.callId,
180
- table,
181
- });
190
+ // The page is bounded once, at the one exit every call leaves by, so this
191
+ // tool cuts nothing of its own.
182
192
  return {
183
193
  callId: request.callId,
184
194
  ok: true,
185
- content: `${textOf(table, "result.read.file", { path, lines: lines.length })}\n${body}`,
195
+ content: `${textOf(table, "result.read.file", { path, lines: lines.length })}\n${lines.slice(offset, offset + limit).join("\n")}`,
186
196
  };
187
197
  }
188
198
  async write(request, window) {
@@ -243,34 +253,45 @@ export class FileToolGroup {
243
253
  await mkdir(dirname(target), { recursive: true });
244
254
  await writeFile(target, text, "utf8");
245
255
  }
246
- start(command, cwd) {
256
+ start(command, cwd, extraEnv) {
247
257
  const id = `b${this.nextJob}`;
248
258
  this.nextJob += 1;
249
- const child = spawn("/bin/sh", ["-c", command], { cwd });
250
- let output = "";
251
- const append = (chunk) => {
252
- output = (output + chunk.toString("utf8")).slice(-JOB_BUFFER);
253
- };
254
- child.stdout.on("data", append);
255
- child.stderr.on("data", append);
259
+ // The one place a command's environment is built: the host's environment,
260
+ // the window's own directory, and the extra environment the workspace
261
+ // implementation asks for - the ovnfs workspace's `ARBOR_WINDOW` and git
262
+ // hook path. The extra variables reach no MCP server and no service
263
+ // (docs/environment.md#what-a-subprocess-gets).
264
+ const child = spawn("/bin/sh", ["-c", command], { cwd, env: bashEnvironment(cwd, extraEnv) });
265
+ let end = () => { };
256
266
  const job = {
257
267
  id,
258
268
  output: "",
259
269
  done: false,
260
270
  code: null,
271
+ ended: new Promise((resolve) => {
272
+ end = resolve;
273
+ }),
261
274
  stop: () => {
262
275
  child.kill("SIGTERM");
263
276
  },
264
277
  };
278
+ // Output lands on the job as it is written, so a running job's output can
279
+ // be read before it ends.
280
+ const append = (chunk) => {
281
+ job.output = (job.output + chunk.toString("utf8")).slice(-JOB_BUFFER);
282
+ };
283
+ child.stdout.on("data", append);
284
+ child.stderr.on("data", append);
265
285
  child.on("close", (code) => {
266
286
  job.done = true;
267
287
  job.code = code;
268
- job.output = output;
288
+ end();
269
289
  });
270
290
  child.on("error", (error) => {
271
291
  job.done = true;
272
292
  job.code = 1;
273
- job.output = output + error.message;
293
+ job.output += error.message;
294
+ end();
274
295
  });
275
296
  this.jobs.set(id, job);
276
297
  return job;
package/groups/notes.d.ts CHANGED
@@ -27,6 +27,12 @@ export declare class NoteToolGroup implements ToolGroup {
27
27
  * that changed since the one the writer read.
28
28
  */
29
29
  private record;
30
+ /**
31
+ * One write's answer, with the active set's budget beside it: the budget is
32
+ * part of every prefix, so a window that writes hears how full it is
33
+ * (docs/memory.md#the-active-set). A delete is its own line: it removed a
34
+ * note rather than writing one.
35
+ */
30
36
  private answer;
31
37
  /** One refusal the note store returned, in the core's error shape. */
32
38
  private refusalOf;
package/groups/notes.js CHANGED
@@ -24,6 +24,7 @@ export class NoteToolGroup {
24
24
  title: { type: "string", required: true },
25
25
  summary: { type: "string" },
26
26
  body: { type: "string", required: true },
27
+ refs: { type: "refs" },
27
28
  }),
28
29
  toolSpec(table, "note_replace", {
29
30
  id: { type: "string", required: true },
@@ -31,6 +32,7 @@ export class NoteToolGroup {
31
32
  title: { type: "string" },
32
33
  summary: { type: "string" },
33
34
  body: { type: "string", required: true },
35
+ refs: { type: "refs" },
34
36
  }),
35
37
  toolSpec(table, "note_delete", {
36
38
  id: { type: "string", required: true },
@@ -57,59 +59,76 @@ export class NoteToolGroup {
57
59
  }
58
60
  }
59
61
  async write(request, store, window) {
60
- const title = stringArg(request.args, "title");
61
- const body = stringArg(request.args, "body");
62
- if (title === "" || body === "")
63
- return this.refusal(request, "note.write.empty");
62
+ // Every rule a write has to meet lives in the store, which answers in the
63
+ // core's error shape; the group adds nothing of its own.
64
+ const refs = refsArg(request.args, "refs");
64
65
  return this.record(request, store, window, {
65
- title,
66
- body,
66
+ title: stringArg(request.args, "title"),
67
+ body: stringArg(request.args, "body"),
67
68
  ...(stringArg(request.args, "summary") === "" ? {} : { summary: stringArg(request.args, "summary") }),
69
+ ...(refs.length === 0 ? {} : { refs }),
68
70
  });
69
71
  }
70
72
  async replace(request, store, window) {
71
- const id = stringArg(request.args, "id");
72
- const revision = numberArg(request.args, "revision");
73
- const body = stringArg(request.args, "body");
74
- if (id === "" || body === "")
75
- return this.refusal(request, "note.write.empty");
73
+ const refs = refsArg(request.args, "refs");
76
74
  return this.record(request, store, window, {
77
- id: id,
75
+ id: stringArg(request.args, "id"),
78
76
  title: stringArg(request.args, "title"),
79
- body,
77
+ body: stringArg(request.args, "body"),
80
78
  replace: true,
81
- expectedRevision: revision,
79
+ expectedRevision: numberArg(request.args, "revision"),
82
80
  ...(stringArg(request.args, "summary") === "" ? {} : { summary: stringArg(request.args, "summary") }),
81
+ ...(refs.length === 0 ? {} : { refs }),
83
82
  });
84
83
  }
85
84
  async remove(request, store, window) {
86
- const id = stringArg(request.args, "id");
87
- if (id === "")
88
- return this.refusal(request, "note.write.empty");
89
85
  return this.record(request, store, window, {
90
- id: id,
86
+ id: stringArg(request.args, "id"),
91
87
  title: "",
92
88
  body: "",
93
89
  delete: true,
94
90
  expectedRevision: numberArg(request.args, "revision"),
95
- });
91
+ }, true);
96
92
  }
97
93
  /**
98
94
  * One write, with the anchor the host fills in. A refused write changes
99
95
  * nothing; the answer carries the rule, the next action and the revisions
100
96
  * that changed since the one the writer read.
101
97
  */
102
- async record(request, _store, window, input) {
98
+ async record(request, store, window, input, deleted = false) {
103
99
  try {
104
100
  const note = await this.services.writeNote(window, input);
105
- return this.answer(request, note.ref, this.services.table);
101
+ return this.answer(request, note.ref, store, deleted);
106
102
  }
107
103
  catch (error) {
108
104
  return this.refusalOf(request, error);
109
105
  }
110
106
  }
111
- answer(request, ref, table) {
112
- return { callId: request.callId, ok: true, content: textOf(table, "result.note_written", { ref }) };
107
+ /**
108
+ * One write's answer, with the active set's budget beside it: the budget is
109
+ * part of every prefix, so a window that writes hears how full it is
110
+ * (docs/memory.md#the-active-set). A delete is its own line: it removed a
111
+ * note rather than writing one.
112
+ */
113
+ async answer(request, ref, store, deleted = false) {
114
+ const table = this.services.table;
115
+ const snapshot = await store.snapshot().catch(() => null);
116
+ return {
117
+ callId: request.callId,
118
+ ok: true,
119
+ content: [
120
+ textOf(table, deleted ? "result.note_deleted" : "result.note_written", { ref }),
121
+ ...(snapshot === null
122
+ ? []
123
+ : [
124
+ textOf(table, "result.note.budget", {
125
+ used: snapshot.tokens,
126
+ hard: snapshot.budget.tokens,
127
+ left: Math.max(0, snapshot.budget.count - snapshot.total),
128
+ }),
129
+ ]),
130
+ ].join("\n"),
131
+ };
113
132
  }
114
133
  /** One refusal the note store returned, in the core's error shape. */
115
134
  refusalOf(request, error) {
@@ -117,8 +136,9 @@ export class NoteToolGroup {
117
136
  const info = error;
118
137
  const revisions = info.details?.revisions ?? [];
119
138
  const lines = [info.message ?? String(error)];
120
- if (info.nextAction !== undefined)
121
- lines.push(`${textOf(table, "error.next", {})}${info.nextAction}`.trim());
139
+ if (info.nextAction !== undefined) {
140
+ lines.push(textOf(table, "error.next", { next: info.nextAction }));
141
+ }
122
142
  for (const revision of revisions) {
123
143
  lines.push(`${revision.ref} ${revision.window ?? ""}`);
124
144
  }
@@ -174,3 +194,10 @@ function numberArg(args, name) {
174
194
  const value = args[name];
175
195
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
176
196
  }
197
+ /** The refs a write names, so the store can link them to the note. */
198
+ function refsArg(args, name) {
199
+ const value = args[name];
200
+ if (!Array.isArray(value))
201
+ return [];
202
+ return value.filter((item) => typeof item === "string");
203
+ }
package/host.d.ts CHANGED
@@ -76,8 +76,6 @@ export interface ToolServices {
76
76
  ensureWorkspace(window: WindowRef): Promise<WorkspaceMount | undefined>;
77
77
  /** End the run, with a handoff when there is unfinished work. */
78
78
  endWindow(window: WindowRef, handoff?: Handoff): Promise<ToolResult>;
79
- /** True when the window still has work the host can see. */
80
- hasOpenWork(window: WindowRef): Promise<boolean>;
81
79
  /**
82
80
  * One note write, with the anchor the host fills in: the window, the event
83
81
  * and the evidence. The model never declares an anchor.
package/index.d.ts CHANGED
@@ -7,11 +7,14 @@
7
7
  * in `code_exec`. There is no CLI, and there is no second path around the
8
8
  * service layer.
9
9
  */
10
- export { ToolCatalog, type ToolCatalogOptions } from "./catalog.ts";
10
+ export { ToolCatalog, type ExternalToolRefusal, type ToolCatalogOptions } from "./catalog.ts";
11
11
  export { SCRIPT_TIMEOUT_MS, directToolIn, runScript, type CodeExecContext, type ScriptCatalog, } from "./code-exec.ts";
12
12
  export { RESULT_LIMIT, boundResult, boundText, nextCall, type Bound } from "./bound.ts";
13
+ export { WORKSPACE_ENV, childEnvironment, type ChildEnvInput } from "./child-env.ts";
13
14
  export { callContext, moduleSpec, refused, toolSpec, type EnvCatalog, type LiveWindow, type ReloadOutcome, type SearchOutcome, type SendOutcome, type TaskRecord, type TaskSummary, type ToolServices, } from "./host.ts";
14
15
  export { CoreToolGroup, handoffOf } from "./groups/core.ts";
16
+ export { ExternalToolGroup } from "./groups/external.ts";
17
+ export { bashEnvironment } from "./groups/files.ts";
15
18
  export { EnvToolGroup } from "./groups/env.ts";
16
19
  export { NoteToolGroup } from "./groups/notes.ts";
17
20
  export { FileToolGroup } from "./groups/files.ts";
package/index.js CHANGED
@@ -10,8 +10,11 @@
10
10
  export { ToolCatalog } from "./catalog.js";
11
11
  export { SCRIPT_TIMEOUT_MS, directToolIn, runScript, } from "./code-exec.js";
12
12
  export { RESULT_LIMIT, boundResult, boundText, nextCall } from "./bound.js";
13
+ export { WORKSPACE_ENV, childEnvironment } from "./child-env.js";
13
14
  export { callContext, moduleSpec, refused, toolSpec, } from "./host.js";
14
15
  export { CoreToolGroup, handoffOf } from "./groups/core.js";
16
+ export { ExternalToolGroup } from "./groups/external.js";
17
+ export { bashEnvironment } from "./groups/files.js";
15
18
  export { EnvToolGroup } from "./groups/env.js";
16
19
  export { NoteToolGroup } from "./groups/notes.js";
17
20
  export { FileToolGroup } from "./groups/files.js";
@@ -17,7 +17,14 @@ export declare class JournalModuleGroup implements ToolGroup {
17
17
  tools(): readonly [];
18
18
  modules(): readonly ModuleSpec[];
19
19
  call(request: ToolCall, _context: CallContext): Promise<ToolResult>;
20
+ /**
21
+ * One answer: the items, then one 相关 line per returned item for the first
22
+ * three of them, then the total and the call that pages the rest
23
+ * (docs/prompting.md#information-density).
24
+ */
20
25
  private answer;
26
+ /** One 相关 line per item, for the first three: no line without neighbours. */
27
+ private related;
21
28
  private notOwned;
22
29
  }
23
30
  /** `windows`: list, show, send, broadcast, inbox. */
@@ -90,7 +97,7 @@ export declare class ScheduleModuleGroup implements ToolGroup {
90
97
  private create;
91
98
  /** `schedule.update`: only the fields the script named change. */
92
99
  private update;
93
- /** One repeat rule as one line: only once, a period, or a daily clock time. */
100
+ /** One repeat rule as one line: only once, a period, a daily or a weekly time. */
94
101
  private repeatText;
95
102
  /** One argument the script did not name in a shape this module can read. */
96
103
  private badInput;