@4fu/arbor-agent-pi 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/backend.js CHANGED
@@ -96,6 +96,11 @@ export class PiAgentBackend {
96
96
  });
97
97
  }
98
98
  const results = new Map();
99
+ // The tools the window declares, built once here: the backend owns the call
100
+ // path, so it is the one that wires a spec to the host's catalog. A refresh
101
+ // while the window runs builds them again and replaces them on the agent's
102
+ // own state (docs/tools.md#external-tools-that-change).
103
+ const declareTools = (tools) => tools.map((tool) => piTool(tool, (args, callId) => this.callTool(request.window, tool.name, args, callId, results), this.renderer));
99
104
  const agent = new Agent({
100
105
  streamFn: (piModel, context, options) => this.runtime.streamSimple(piModel, context, options),
101
106
  getApiKey: async (provider) => {
@@ -105,7 +110,7 @@ export class PiAgentBackend {
105
110
  initialState: {
106
111
  systemPrompt: systemPromptOf(request),
107
112
  model,
108
- tools: request.tools.map((tool) => piTool(tool, (args, callId) => this.callTool(request.window, tool.name, args, callId, results), this.renderer)),
113
+ tools: declareTools(request.tools),
109
114
  },
110
115
  steeringMode: "all",
111
116
  followUpMode: "all",
@@ -122,6 +127,9 @@ export class PiAgentBackend {
122
127
  limits: { limit: request.limits.contextTokens, reserve: request.limits.reserveTokens },
123
128
  results,
124
129
  run,
130
+ refreshTools: (tools) => {
131
+ agent.state.tools = declareTools(tools);
132
+ },
125
133
  });
126
134
  }
127
135
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4fu/arbor-agent-pi",
3
- "version": "2026.1106.1",
3
+ "version": "2026.1107.0",
4
4
  "type": "module",
5
5
  "description": "The default AgentBackend: pi-agent-core in process, with its own system prompt and no pi type crossing the interface.",
6
6
  "exports": {
@@ -42,9 +42,9 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dependencies": {
45
- "@4fu/arbor-errors": "2026.1106.1",
46
- "@4fu/arbor-interfaces": "2026.1106.1",
47
- "@4fu/arbor-prompt": "2026.1106.1",
45
+ "@4fu/arbor-errors": "2026.1107.0",
46
+ "@4fu/arbor-interfaces": "2026.1107.0",
47
+ "@4fu/arbor-prompt": "2026.1107.0",
48
48
  "@earendil-works/pi-agent-core": "1.5.0",
49
49
  "@earendil-works/pi-ai": "1.5.0"
50
50
  }
package/session.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * even complete (docs/windows.md#streaming).
14
14
  */
15
15
  import type { Agent } from "@earendil-works/pi-agent-core";
16
- import type { Delivery, Handoff, TokenUsage, ToolResult, WindowEndReason, WindowEvent, WindowOutcome, WindowRef, WindowSession, WindowState } from "@4fu/arbor-interfaces";
16
+ import type { Delivery, DirectToolSpec, Handoff, TokenUsage, ToolResult, WindowEndReason, WindowEvent, WindowOutcome, WindowRef, WindowSession, WindowState } from "@4fu/arbor-interfaces";
17
17
  import type { Renderer } from "@4fu/arbor-prompt";
18
18
  export interface PiSessionOptions {
19
19
  agent: Agent;
@@ -27,6 +27,12 @@ export interface PiSessionOptions {
27
27
  results?: Map<string, ToolResult>;
28
28
  /** The first turn, already started by the backend. */
29
29
  run: Promise<void>;
30
+ /**
31
+ * Replace the tools this window declares, when the host's set changed while
32
+ * the window runs. The backend owns the call path, so it builds them
33
+ * (docs/tools.md#external-tools-that-change).
34
+ */
35
+ refreshTools?: (tools: readonly DirectToolSpec[]) => void;
30
36
  }
31
37
  export declare class PiWindowSession implements WindowSession {
32
38
  readonly window: WindowRef;
@@ -46,10 +52,24 @@ export declare class PiWindowSession implements WindowSession {
46
52
  * JSON yet, so they are read with pi's own tolerant parser.
47
53
  */
48
54
  private readonly writing;
55
+ /**
56
+ * How this window's declared tools are replaced when the host's set changes.
57
+ * The backend owns the call path, so it builds them
58
+ * (docs/tools.md#external-tools-that-change).
59
+ */
60
+ private readonly refresh;
49
61
  constructor(options: PiSessionOptions);
50
62
  get state(): WindowState;
51
63
  steer(delivery: Delivery): Promise<void>;
52
64
  followUp(delivery: Delivery): Promise<void>;
65
+ /**
66
+ * The tools this window may call changed. pi keeps its declaration on the
67
+ * agent's own state, which may be replaced while the window runs, so a tool
68
+ * that appeared is callable from the next model request and one that was
69
+ * removed is not - and the window's prefix is never rebuilt for it
70
+ * (docs/tools.md#external-tools-that-change).
71
+ */
72
+ refreshTools(tools: readonly DirectToolSpec[]): void;
53
73
  end(handoff?: Handoff, reason?: WindowEndReason): Promise<void>;
54
74
  pause(): Promise<void>;
55
75
  resume(): Promise<void>;
package/session.js CHANGED
@@ -64,9 +64,16 @@ export class PiWindowSession {
64
64
  * JSON yet, so they are read with pi's own tolerant parser.
65
65
  */
66
66
  writing = new Map();
67
+ /**
68
+ * How this window's declared tools are replaced when the host's set changes.
69
+ * The backend owns the call path, so it builds them
70
+ * (docs/tools.md#external-tools-that-change).
71
+ */
72
+ refresh;
67
73
  constructor(options) {
68
74
  this.window = options.window;
69
75
  this.agent = options.agent;
76
+ this.refresh = options.refreshTools;
70
77
  this.renderer = options.renderer;
71
78
  this.results = options.results ?? new Map();
72
79
  this.current = { used: 0, limit: options.limits.limit, reserve: options.limits.reserve };
@@ -104,6 +111,18 @@ export class PiWindowSession {
104
111
  return;
105
112
  this.enqueue(delivery, "followUp");
106
113
  }
114
+ /**
115
+ * The tools this window may call changed. pi keeps its declaration on the
116
+ * agent's own state, which may be replaced while the window runs, so a tool
117
+ * that appeared is callable from the next model request and one that was
118
+ * removed is not - and the window's prefix is never rebuilt for it
119
+ * (docs/tools.md#external-tools-that-change).
120
+ */
121
+ refreshTools(tools) {
122
+ if (this.ended)
123
+ return;
124
+ this.refresh?.(tools);
125
+ }
107
126
  async end(handoff, reason = "model_ended") {
108
127
  if (this.ended)
109
128
  return;
package/tools.d.ts CHANGED
@@ -10,12 +10,21 @@
10
10
  * error block - the rule and the next action, one line.
11
11
  */
12
12
  import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
13
- import type { ParameterSpec, ToolResult } from "@4fu/arbor-interfaces";
13
+ import type { JSONSchema, ParameterSpec, ToolResult } from "@4fu/arbor-interfaces";
14
14
  import type { Renderer } from "@4fu/arbor-prompt";
15
15
  /** One parameter's JSON Schema, from its `ParameterSpec`. */
16
16
  export declare function parameterSchema(parameter: ParameterSpec): Record<string, unknown>;
17
- /** The JSON Schema one tool declares to pi. */
18
- export declare function toolParameters(parameters: Record<string, ParameterSpec>): Record<string, unknown>;
17
+ /**
18
+ * The JSON Schema one tool declares to pi.
19
+ *
20
+ * A tool declared from outside the core hands over the JSON Schema its own
21
+ * harness declared, and that is what pi sends: `enum`, `items`, `oneOf`, a
22
+ * nested object's own properties, all of it, because the model calls the tool
23
+ * with the schema it was given and a summary of it would only drop what it says
24
+ * (docs/tools.md#external-tools). A tool that declares none - arbor's own - is
25
+ * built from its `ParameterSpec`s, which is the declaration for those.
26
+ */
27
+ export declare function toolParameters(parameters: Record<string, ParameterSpec>, schema?: JSONSchema): Record<string, unknown>;
19
28
  /**
20
29
  * What the model reads of one result. A successful result is rendered as the
21
30
  * `tool_result` block, so it is escaped and marked as data like every other
@@ -31,4 +40,5 @@ export declare function piTool(spec: {
31
40
  name: string;
32
41
  description: string;
33
42
  parameters: Record<string, ParameterSpec>;
43
+ schema?: JSONSchema;
34
44
  }, call: (args: Record<string, unknown>, callId: string) => Promise<ToolResult>, renderer: Renderer): AgentTool<Record<string, unknown>>;
package/tools.js CHANGED
@@ -44,8 +44,19 @@ function jsonType(type) {
44
44
  return "string";
45
45
  }
46
46
  }
47
- /** The JSON Schema one tool declares to pi. */
48
- export function toolParameters(parameters) {
47
+ /**
48
+ * The JSON Schema one tool declares to pi.
49
+ *
50
+ * A tool declared from outside the core hands over the JSON Schema its own
51
+ * harness declared, and that is what pi sends: `enum`, `items`, `oneOf`, a
52
+ * nested object's own properties, all of it, because the model calls the tool
53
+ * with the schema it was given and a summary of it would only drop what it says
54
+ * (docs/tools.md#external-tools). A tool that declares none - arbor's own - is
55
+ * built from its `ParameterSpec`s, which is the declaration for those.
56
+ */
57
+ export function toolParameters(parameters, schema) {
58
+ if (schema !== undefined)
59
+ return schema;
49
60
  const properties = {};
50
61
  const required = [];
51
62
  for (const [name, parameter] of Object.entries(parameters)) {
@@ -94,7 +105,7 @@ export function piTool(spec, call, renderer) {
94
105
  name: spec.name,
95
106
  label: spec.name,
96
107
  description: spec.description,
97
- parameters: toolParameters(spec.parameters),
108
+ parameters: toolParameters(spec.parameters, spec.schema),
98
109
  execute: async (callId, params) => {
99
110
  const result = await call((params ?? {}), callId);
100
111
  return toolResultOf(result, renderer);