@4fu/arbor-agent-pi 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/backend.d.ts +26 -0
- package/backend.js +51 -1
- package/package.json +4 -4
- package/session.d.ts +21 -0
- package/session.js +78 -1
- package/tools.d.ts +5 -2
- package/tools.js +20 -15
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/backend.d.ts
CHANGED
|
@@ -34,12 +34,25 @@ export interface PiBackendOptions {
|
|
|
34
34
|
control?: WindowControl;
|
|
35
35
|
/** The name this backend reports. */
|
|
36
36
|
name?: string;
|
|
37
|
+
/**
|
|
38
|
+
* How long to wait before each retry of a transient model error. A run that
|
|
39
|
+
* fails with one pi classifies as transient is sent again, byte for byte, so
|
|
40
|
+
* the provider's cached prefix still holds
|
|
41
|
+
* (docs/windows.md#model-failures).
|
|
42
|
+
*/
|
|
43
|
+
retryDelaysMs?: readonly number[];
|
|
44
|
+
/** How the backend waits between retries. Tests pass an immediate one. */
|
|
45
|
+
sleep?: (ms: number) => Promise<void>;
|
|
37
46
|
}
|
|
47
|
+
/** How long to wait before each retry of a transient model error. */
|
|
48
|
+
export declare const DEFAULT_RETRY_DELAYS_MS: readonly number[];
|
|
38
49
|
export declare class PiAgentBackend implements AgentBackend {
|
|
39
50
|
readonly name: string;
|
|
40
51
|
private readonly runtime;
|
|
41
52
|
private readonly renderer;
|
|
42
53
|
private control;
|
|
54
|
+
private readonly retryDelaysMs;
|
|
55
|
+
private readonly sleep;
|
|
43
56
|
constructor(options?: PiBackendOptions);
|
|
44
57
|
/**
|
|
45
58
|
* Take the host's window control. A backend that runs windows in process
|
|
@@ -57,6 +70,19 @@ export declare class PiAgentBackend implements AgentBackend {
|
|
|
57
70
|
login(request: LoginRequest): Promise<LoginResult>;
|
|
58
71
|
/** Open one window and start its first turn. */
|
|
59
72
|
open(request: OpenWindowRequest): Promise<WindowSession>;
|
|
73
|
+
/**
|
|
74
|
+
* The window's first turn, retried while the model service fails transiently.
|
|
75
|
+
*
|
|
76
|
+
* pi settles a failed run rather than throwing: the agent records an assistant
|
|
77
|
+
* message whose `stopReason` is `error`, and `isRetryableAssistantError` says
|
|
78
|
+
* whether that error is the transient kind. When it is, the failed message is
|
|
79
|
+
* dropped and the same request is sent again - the same bytes, in the same
|
|
80
|
+
* order, so the provider's cached prefix still holds and nothing the model
|
|
81
|
+
* already did is repeated. A run that fails any other way, or one that runs
|
|
82
|
+
* out of retries, rejects, which is how the session settles the window as
|
|
83
|
+
* failed for the host (docs/windows.md#model-failures).
|
|
84
|
+
*/
|
|
85
|
+
private runWithRetry;
|
|
60
86
|
/**
|
|
61
87
|
* One direct tool call. The call is recorded at its event before it runs, and
|
|
62
88
|
* the call itself goes through the host's tool groups - the same path a direct
|
package/backend.js
CHANGED
|
@@ -16,20 +16,27 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { Agent } from "@earendil-works/pi-agent-core";
|
|
18
18
|
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
|
|
19
|
+
import { isRetryableAssistantError } from "@earendil-works/pi-ai";
|
|
19
20
|
import { arborError } from "@4fu/arbor-errors";
|
|
20
21
|
import { Renderer } from "@4fu/arbor-prompt";
|
|
21
22
|
import { PiWindowSession } from "./session.js";
|
|
22
23
|
import { piTool } from "./tools.js";
|
|
24
|
+
/** How long to wait before each retry of a transient model error. */
|
|
25
|
+
export const DEFAULT_RETRY_DELAYS_MS = [2000, 8000, 30000];
|
|
23
26
|
export class PiAgentBackend {
|
|
24
27
|
name;
|
|
25
28
|
runtime;
|
|
26
29
|
renderer;
|
|
27
30
|
control;
|
|
31
|
+
retryDelaysMs;
|
|
32
|
+
sleep;
|
|
28
33
|
constructor(options = {}) {
|
|
29
34
|
this.name = options.name ?? "pi";
|
|
30
35
|
this.runtime = options.models ?? builtinModels();
|
|
31
36
|
this.renderer = options.renderer ?? new Renderer();
|
|
32
37
|
this.control = options.control;
|
|
38
|
+
this.retryDelaysMs = options.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
39
|
+
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* Take the host's window control. A backend that runs windows in process
|
|
@@ -107,7 +114,7 @@ export class PiAgentBackend {
|
|
|
107
114
|
const first = request.firstInput ?? "";
|
|
108
115
|
const run = first === ""
|
|
109
116
|
? Promise.resolve()
|
|
110
|
-
:
|
|
117
|
+
: this.runWithRetry(agent, first);
|
|
111
118
|
return new PiWindowSession({
|
|
112
119
|
agent,
|
|
113
120
|
window: request.window,
|
|
@@ -117,6 +124,38 @@ export class PiAgentBackend {
|
|
|
117
124
|
run,
|
|
118
125
|
});
|
|
119
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* The window's first turn, retried while the model service fails transiently.
|
|
129
|
+
*
|
|
130
|
+
* pi settles a failed run rather than throwing: the agent records an assistant
|
|
131
|
+
* message whose `stopReason` is `error`, and `isRetryableAssistantError` says
|
|
132
|
+
* whether that error is the transient kind. When it is, the failed message is
|
|
133
|
+
* dropped and the same request is sent again - the same bytes, in the same
|
|
134
|
+
* order, so the provider's cached prefix still holds and nothing the model
|
|
135
|
+
* already did is repeated. A run that fails any other way, or one that runs
|
|
136
|
+
* out of retries, rejects, which is how the session settles the window as
|
|
137
|
+
* failed for the host (docs/windows.md#model-failures).
|
|
138
|
+
*/
|
|
139
|
+
async runWithRetry(agent, first) {
|
|
140
|
+
await agent.prompt(first);
|
|
141
|
+
for (let attempt = 0;; attempt += 1) {
|
|
142
|
+
const failed = failedMessageOf(agent);
|
|
143
|
+
if (failed === null)
|
|
144
|
+
return;
|
|
145
|
+
if (attempt >= this.retryDelaysMs.length || !isRetryableAssistantError(failed)) {
|
|
146
|
+
throw new Error(failed.errorMessage ?? "the model call failed");
|
|
147
|
+
}
|
|
148
|
+
const delay = this.retryDelaysMs[attempt];
|
|
149
|
+
if (delay !== undefined && delay > 0)
|
|
150
|
+
await this.sleep(delay);
|
|
151
|
+
// The failed turn is not an answer: dropping it leaves the transcript as
|
|
152
|
+
// it was, so the retry sends exactly what the first attempt sent.
|
|
153
|
+
agent.state.messages = agent.state.messages.filter((message) => message !== failed);
|
|
154
|
+
await agent.continue().catch(() => {
|
|
155
|
+
// The window was ended while the retry waited.
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
120
159
|
/**
|
|
121
160
|
* One direct tool call. The call is recorded at its event before it runs, and
|
|
122
161
|
* the call itself goes through the host's tool groups - the same path a direct
|
|
@@ -164,3 +203,14 @@ function systemPromptOf(request) {
|
|
|
164
203
|
const memory = request.prompt.memory.trim();
|
|
165
204
|
return memory === "" ? request.prompt.system : `${request.prompt.system}\n\n${memory}`;
|
|
166
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* The assistant message a failed run left on the transcript, or null when the
|
|
208
|
+
* last message is not one. pi settles a provider failure by recording it, so
|
|
209
|
+
* this is where a run's failure is read from.
|
|
210
|
+
*/
|
|
211
|
+
function failedMessageOf(agent) {
|
|
212
|
+
const last = agent.state.messages.at(-1);
|
|
213
|
+
if (last === undefined || last.role !== "assistant")
|
|
214
|
+
return null;
|
|
215
|
+
return last.stopReason === "error" ? last : null;
|
|
216
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4fu/arbor-agent-pi",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2026.1106.1",
|
|
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": "
|
|
46
|
-
"@4fu/arbor-interfaces": "
|
|
47
|
-
"@4fu/arbor-prompt": "
|
|
45
|
+
"@4fu/arbor-errors": "2026.1106.1",
|
|
46
|
+
"@4fu/arbor-interfaces": "2026.1106.1",
|
|
47
|
+
"@4fu/arbor-prompt": "2026.1106.1",
|
|
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
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* boundary through pi's steering, queues follow-ups for after the run, ends the
|
|
6
6
|
* run, and reads tool calls, results and usage as they happen. Nothing pi owns
|
|
7
7
|
* crosses the interface.
|
|
8
|
+
*
|
|
9
|
+
* What the model is writing while it runs travels the same way: pi's
|
|
10
|
+
* `message_update` carries its own text and tool-call argument deltas, and this
|
|
11
|
+
* session reports them as the core's `StreamEvent`, so a host surface can show a
|
|
12
|
+
* window's output - above all what it is about to `send` - before the call is
|
|
13
|
+
* even complete (docs/windows.md#streaming).
|
|
8
14
|
*/
|
|
9
15
|
import type { Agent } from "@earendil-works/pi-agent-core";
|
|
10
16
|
import type { Delivery, Handoff, TokenUsage, ToolResult, WindowEndReason, WindowEvent, WindowOutcome, WindowRef, WindowSession, WindowState } from "@4fu/arbor-interfaces";
|
|
@@ -34,6 +40,12 @@ export declare class PiWindowSession implements WindowSession {
|
|
|
34
40
|
private paused;
|
|
35
41
|
private current;
|
|
36
42
|
private readonly results;
|
|
43
|
+
/**
|
|
44
|
+
* The arguments of the tool call being written, by content index. pi reports
|
|
45
|
+
* them as JSON text in pieces, and a call still being written is not valid
|
|
46
|
+
* JSON yet, so they are read with pi's own tolerant parser.
|
|
47
|
+
*/
|
|
48
|
+
private readonly writing;
|
|
37
49
|
constructor(options: PiSessionOptions);
|
|
38
50
|
get state(): WindowState;
|
|
39
51
|
steer(delivery: Delivery): Promise<void>;
|
|
@@ -48,6 +60,15 @@ export declare class PiWindowSession implements WindowSession {
|
|
|
48
60
|
/** Continue an idle window, so a queued delivery is picked up. */
|
|
49
61
|
private continue_;
|
|
50
62
|
private onAgentEvent;
|
|
63
|
+
/**
|
|
64
|
+
* One delta of what the model is writing, or nothing when this event is not
|
|
65
|
+
* one. pi's `message_update` carries the provider's own event: `text_delta`
|
|
66
|
+
* for assistant text, `toolcall_delta` for the arguments of a tool call as
|
|
67
|
+
* they are written. What stands so far is what the delta reports, so a
|
|
68
|
+
* surface that shows a window's output never reassembles the pieces itself
|
|
69
|
+
* (docs/windows.md#streaming).
|
|
70
|
+
*/
|
|
71
|
+
private deltaOf;
|
|
51
72
|
/**
|
|
52
73
|
* The window's footprint: the last request's input, cache read and cache
|
|
53
74
|
* write. A running total would resend the context on every turn and cross
|
package/session.js
CHANGED
|
@@ -5,7 +5,14 @@
|
|
|
5
5
|
* boundary through pi's steering, queues follow-ups for after the run, ends the
|
|
6
6
|
* run, and reads tool calls, results and usage as they happen. Nothing pi owns
|
|
7
7
|
* crosses the interface.
|
|
8
|
+
*
|
|
9
|
+
* What the model is writing while it runs travels the same way: pi's
|
|
10
|
+
* `message_update` carries its own text and tool-call argument deltas, and this
|
|
11
|
+
* session reports them as the core's `StreamEvent`, so a host surface can show a
|
|
12
|
+
* window's output - above all what it is about to `send` - before the call is
|
|
13
|
+
* even complete (docs/windows.md#streaming).
|
|
8
14
|
*/
|
|
15
|
+
import { ToolCallArgumentsStream } from "@earendil-works/pi-ai";
|
|
9
16
|
import { arborError } from "@4fu/arbor-errors";
|
|
10
17
|
import { renderDelivery } from "@4fu/arbor-prompt";
|
|
11
18
|
/** A queue of window events the host drains as they happen. */
|
|
@@ -51,6 +58,12 @@ export class PiWindowSession {
|
|
|
51
58
|
paused = false;
|
|
52
59
|
current;
|
|
53
60
|
results;
|
|
61
|
+
/**
|
|
62
|
+
* The arguments of the tool call being written, by content index. pi reports
|
|
63
|
+
* them as JSON text in pieces, and a call still being written is not valid
|
|
64
|
+
* JSON yet, so they are read with pi's own tolerant parser.
|
|
65
|
+
*/
|
|
66
|
+
writing = new Map();
|
|
54
67
|
constructor(options) {
|
|
55
68
|
this.window = options.window;
|
|
56
69
|
this.agent = options.agent;
|
|
@@ -131,6 +144,16 @@ export class PiWindowSession {
|
|
|
131
144
|
}
|
|
132
145
|
onAgentEvent(event) {
|
|
133
146
|
switch (event.type) {
|
|
147
|
+
case "message_start":
|
|
148
|
+
// A new response numbers its content blocks from zero again.
|
|
149
|
+
this.writing.clear();
|
|
150
|
+
break;
|
|
151
|
+
case "message_update": {
|
|
152
|
+
const delta = this.deltaOf(event);
|
|
153
|
+
if (delta !== undefined)
|
|
154
|
+
this.stream.push(delta);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
134
157
|
case "tool_execution_start":
|
|
135
158
|
this.stream.push({
|
|
136
159
|
kind: "tool_call",
|
|
@@ -145,8 +168,16 @@ export class PiWindowSession {
|
|
|
145
168
|
});
|
|
146
169
|
break;
|
|
147
170
|
case "message_end":
|
|
148
|
-
if (event.message.role === "assistant")
|
|
171
|
+
if (event.message.role === "assistant") {
|
|
149
172
|
this.trackUsage(event.message.usage);
|
|
173
|
+
// What the model said in this turn, as one event on the window's
|
|
174
|
+
// record: the host journals it, so `windows.show` replays it and a
|
|
175
|
+
// continuation reads what its predecessor said. A message that only
|
|
176
|
+
// carries tool calls says nothing and records nothing.
|
|
177
|
+
const text = assistantText(event.message);
|
|
178
|
+
if (text !== "")
|
|
179
|
+
this.stream.push({ kind: "appended", event: text });
|
|
180
|
+
}
|
|
150
181
|
break;
|
|
151
182
|
case "agent_end":
|
|
152
183
|
this.stream.push({ kind: "run_ended", outcome: this.outcomeOf("model_ended") });
|
|
@@ -155,6 +186,35 @@ export class PiWindowSession {
|
|
|
155
186
|
break;
|
|
156
187
|
}
|
|
157
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* One delta of what the model is writing, or nothing when this event is not
|
|
191
|
+
* one. pi's `message_update` carries the provider's own event: `text_delta`
|
|
192
|
+
* for assistant text, `toolcall_delta` for the arguments of a tool call as
|
|
193
|
+
* they are written. What stands so far is what the delta reports, so a
|
|
194
|
+
* surface that shows a window's output never reassembles the pieces itself
|
|
195
|
+
* (docs/windows.md#streaming).
|
|
196
|
+
*/
|
|
197
|
+
deltaOf(event) {
|
|
198
|
+
const inner = event.assistantMessageEvent;
|
|
199
|
+
if (inner.type === "text_delta")
|
|
200
|
+
return { kind: "text_delta", delta: inner.delta };
|
|
201
|
+
// A provider reports the tool call's name and id on the chunk that opens it,
|
|
202
|
+
// which need not carry any argument text yet.
|
|
203
|
+
if (inner.type !== "toolcall_delta" || inner.delta === "")
|
|
204
|
+
return undefined;
|
|
205
|
+
const block = writingCall(event.message, inner.contentIndex);
|
|
206
|
+
if (block === undefined)
|
|
207
|
+
return undefined;
|
|
208
|
+
let arguments_ = this.writing.get(inner.contentIndex);
|
|
209
|
+
if (arguments_ === undefined) {
|
|
210
|
+
arguments_ = new ToolCallArgumentsStream();
|
|
211
|
+
this.writing.set(inner.contentIndex, arguments_);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
kind: "tool_call_delta",
|
|
215
|
+
call: { name: block.name, args: arguments_.append(inner.delta), callId: block.id },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
158
218
|
/**
|
|
159
219
|
* The window's footprint: the last request's input, cache read and cache
|
|
160
220
|
* write. A running total would resend the context on every turn and cross
|
|
@@ -215,3 +275,20 @@ export class PiWindowSession {
|
|
|
215
275
|
function callOf(name, args, callId) {
|
|
216
276
|
return { name, args: (args ?? {}), callId };
|
|
217
277
|
}
|
|
278
|
+
/** The text one assistant message carries, without its tool calls. */
|
|
279
|
+
function assistantText(message) {
|
|
280
|
+
return message.content
|
|
281
|
+
.flatMap((block) => (block.type === "text" ? [block.text] : []))
|
|
282
|
+
.join("\n");
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* The tool call block at one content index of a message still being written.
|
|
286
|
+
* pi keeps the partial message's blocks in place, so the block a delta belongs
|
|
287
|
+
* to is the one at its own index; its name and id may still be filling in.
|
|
288
|
+
*/
|
|
289
|
+
function writingCall(message, index) {
|
|
290
|
+
if (message.role !== "assistant")
|
|
291
|
+
return undefined;
|
|
292
|
+
const block = message.content[index];
|
|
293
|
+
return block !== undefined && block.type === "toolCall" ? block : undefined;
|
|
294
|
+
}
|
package/tools.d.ts
CHANGED
|
@@ -17,8 +17,11 @@ export declare function parameterSchema(parameter: ParameterSpec): Record<string
|
|
|
17
17
|
/** The JSON Schema one tool declares to pi. */
|
|
18
18
|
export declare function toolParameters(parameters: Record<string, ParameterSpec>): Record<string, unknown>;
|
|
19
19
|
/**
|
|
20
|
-
* What the model reads of one result. A
|
|
21
|
-
*
|
|
20
|
+
* What the model reads of one result. A successful result is rendered as the
|
|
21
|
+
* `tool_result` block, so it is escaped and marked as data like every other
|
|
22
|
+
* block. A refusal is rendered as the error block, so the model always gets
|
|
23
|
+
* the rule and the next action, never an empty result; a failure that carries
|
|
24
|
+
* no error shape is marked as a failed result instead.
|
|
22
25
|
*/
|
|
23
26
|
export declare function resultText(result: ToolResult, renderer: Renderer): string;
|
|
24
27
|
/** The core's result as pi's. */
|
package/tools.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* core's own `ToolResult`, and a refusal reaches the model as the renderer's
|
|
10
10
|
* error block - the rule and the next action, one line.
|
|
11
11
|
*/
|
|
12
|
-
import { errorBlock } from "@4fu/arbor-prompt";
|
|
12
|
+
import { errorBlock, toolResultBlock } from "@4fu/arbor-prompt";
|
|
13
13
|
/** One parameter's JSON Schema, from its `ParameterSpec`. */
|
|
14
14
|
export function parameterSchema(parameter) {
|
|
15
15
|
const schema = {
|
|
@@ -56,23 +56,28 @@ export function toolParameters(parameters) {
|
|
|
56
56
|
return { type: "object", properties, ...(required.length === 0 ? {} : { required }) };
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* What the model reads of one result. A
|
|
60
|
-
*
|
|
59
|
+
* What the model reads of one result. A successful result is rendered as the
|
|
60
|
+
* `tool_result` block, so it is escaped and marked as data like every other
|
|
61
|
+
* block. A refusal is rendered as the error block, so the model always gets
|
|
62
|
+
* the rule and the next action, never an empty result; a failure that carries
|
|
63
|
+
* no error shape is marked as a failed result instead.
|
|
61
64
|
*/
|
|
62
65
|
export function resultText(result, renderer) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const name = result.tool ?? result.callId;
|
|
67
|
+
const error = result.ok ? undefined : result.error;
|
|
68
|
+
if (error !== undefined) {
|
|
69
|
+
return renderer.render([
|
|
70
|
+
errorBlock({
|
|
71
|
+
tool: name,
|
|
72
|
+
rule: error.rule,
|
|
73
|
+
message: error.message,
|
|
74
|
+
...(error.nextAction === undefined ? {} : { nextAction: error.nextAction }),
|
|
75
|
+
...(error.refs === undefined ? {} : { refs: error.refs }),
|
|
76
|
+
}),
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
68
79
|
return renderer.render([
|
|
69
|
-
|
|
70
|
-
tool: result.callId,
|
|
71
|
-
rule: error.rule,
|
|
72
|
-
message: error.message,
|
|
73
|
-
...(error.nextAction === undefined ? {} : { nextAction: error.nextAction }),
|
|
74
|
-
...(error.refs === undefined ? {} : { refs: error.refs }),
|
|
75
|
-
}),
|
|
80
|
+
toolResultBlock({ callId: result.callId, name }, result.content, result.ok ? "ok" : "failed"),
|
|
76
81
|
]);
|
|
77
82
|
}
|
|
78
83
|
/** The core's result as pi's. */
|