@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 +6 -1
- package/backend.js +9 -1
- package/package.json +4 -4
- package/session.d.ts +21 -1
- package/session.js +19 -0
- package/tools.d.ts +13 -3
- package/tools.js +14 -3
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
|
|
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
|
|
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.
|
|
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.
|
|
46
|
-
"@4fu/arbor-interfaces": "2026.
|
|
47
|
-
"@4fu/arbor-prompt": "2026.
|
|
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
|
-
/**
|
|
18
|
-
|
|
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
|
-
/**
|
|
48
|
-
|
|
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);
|