@4fu/arbor-prompt 2026.1106.0 → 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 +14 -6
- package/blocks.js +63 -3
- package/build.d.ts +47 -2
- package/build.js +83 -2
- package/diff.d.ts +23 -0
- package/diff.js +93 -0
- package/first-input.d.ts +5 -0
- package/first-input.js +4 -1
- package/index.d.ts +3 -2
- package/index.js +3 -2
- package/package.json +5 -3
- package/reminder.d.ts +10 -8
- package/reminder.js +14 -12
- package/render.d.ts +10 -2
- package/render.js +27 -7
- package/system.d.ts +48 -5
- package/system.js +86 -32
- package/text.js +124 -28
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.
|
|
@@ -54,7 +56,9 @@ npm install @4fu/arbor-core @4fu/arbor-journal-sqlite @4fu/arbor-notes-sqlite \
|
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
`@4fu/arbor-plugin-http` is optional; it is the example plugin below.
|
|
57
|
-
`@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.
|
|
58
62
|
|
|
59
63
|
## A minimal host
|
|
60
64
|
|
|
@@ -118,7 +122,10 @@ window gets, and how a continuation keeps working in it, are in
|
|
|
118
122
|
| [`@4fu/arbor-journal-sqlite`](packages/journal-sqlite) | the default `Journal` and `BlobStore` on SQLite |
|
|
119
123
|
| [`@4fu/arbor-notes-sqlite`](packages/notes-sqlite) | the default `NoteStore` on SQLite, beside the journal |
|
|
120
124
|
| [`@4fu/arbor-segmenter-jieba`](packages/segmenter-jieba) | the default `Segmenter`: jieba's dictionary segmentation for the word index, optional |
|
|
121
|
-
| [`@4fu/arbor-
|
|
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 |
|
|
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 |
|
|
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 |
|
|
122
129
|
| [`@4fu/arbor-environment-dir`](packages/environment-dir) | the default `Environment`: one `/env` directory, a revision per item, validation and deltas |
|
|
123
130
|
| [`@4fu/arbor-supervisor-process`](packages/supervisor-process) | the default `Supervisor`: one host process per service, restart with backoff, logs |
|
|
124
131
|
| [`@4fu/arbor-judge-jev`](packages/judge-jev) | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
|
|
@@ -151,7 +158,7 @@ deployment can see what it would be taking on before it adopts one.
|
|
|
151
158
|
| [docs/coordination.md](docs/coordination.md) | the board, expectations, deliveries and broadcasts, staying current, effects |
|
|
152
159
|
| [docs/environment.md](docs/environment.md) | skills, MCP servers and services: using, changing, reload, built-in skills, deltas |
|
|
153
160
|
| [docs/memory.md](docs/memory.md) | notes, the active set, anchors, links |
|
|
154
|
-
| [docs/workspaces.md](docs/workspaces.md) | per-window
|
|
161
|
+
| [docs/workspaces.md](docs/workspaces.md) | the shared folder and per-window checkouts: isolation, continuity, what the agent does |
|
|
155
162
|
| [docs/tools.md](docs/tools.md) | direct tools and code mode modules |
|
|
156
163
|
| [docs/prompting.md](docs/prompting.md) | how every model-facing text is written: layers, notes, deliveries, errors, judge questions, terms |
|
|
157
164
|
| [docs/release.md](docs/release.md) | calendar versioning, the manual release steps, how a package is published |
|
|
@@ -168,6 +175,7 @@ tarball would contain ([docs/release.md](docs/release.md)).
|
|
|
168
175
|
| `packages/errors` | the error shape every refusal uses: the rule and the next action |
|
|
169
176
|
| `packages/journal-sqlite` | the default `Journal` and `BlobStore` on SQLite |
|
|
170
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 |
|
|
171
179
|
| `packages/notes-sqlite` | the default `NoteStore` on SQLite, beside the journal |
|
|
172
180
|
| `packages/judge-jev` | the default `Judge`: Jev, TypeSafe AI's System One classifier, answering the dispatch decisions |
|
|
173
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/blocks.js
CHANGED
|
@@ -29,12 +29,13 @@ export const BUILTIN_BLOCK_KINDS = [
|
|
|
29
29
|
{
|
|
30
30
|
kind: "board_line",
|
|
31
31
|
tag: "board_line",
|
|
32
|
-
lines: ["board.line", "board.targets", "board.usage", "board.expects"],
|
|
32
|
+
lines: ["board.line", "board.targets", "board.open", "board.usage", "board.expects"],
|
|
33
33
|
fields: [
|
|
34
34
|
{ name: "window", type: "ref" },
|
|
35
35
|
{ name: "state", type: "string" },
|
|
36
36
|
{ name: "doing", type: "string" },
|
|
37
37
|
{ name: "targets", type: "strings" },
|
|
38
|
+
{ name: "openWork", type: "strings" },
|
|
38
39
|
{ name: "used", type: "number" },
|
|
39
40
|
{ name: "limit", type: "number" },
|
|
40
41
|
{ name: "expects", type: "strings" },
|
|
@@ -81,7 +82,7 @@ export const BUILTIN_BLOCK_KINDS = [
|
|
|
81
82
|
{
|
|
82
83
|
kind: "reminder",
|
|
83
84
|
tag: "reminder",
|
|
84
|
-
lines: ["reminder.numbers", "reminder.series", "reminder.tier.{tier}", "reminder.cost", "delivery.time"],
|
|
85
|
+
lines: ["reminder.head", "reminder.numbers", "reminder.series", "reminder.tier.{tier}", "reminder.open", "reminder.cost.{tier}", "delivery.time"],
|
|
85
86
|
fields: [
|
|
86
87
|
{ name: "tier", type: "string" },
|
|
87
88
|
{ name: "index", type: "number" },
|
|
@@ -89,6 +90,7 @@ export const BUILTIN_BLOCK_KINDS = [
|
|
|
89
90
|
{ name: "used", type: "number" },
|
|
90
91
|
{ name: "limit", type: "number" },
|
|
91
92
|
{ name: "percent", type: "number" },
|
|
93
|
+
{ name: "openWork", type: "strings" },
|
|
92
94
|
{ name: "time", type: "string" },
|
|
93
95
|
],
|
|
94
96
|
},
|
|
@@ -96,10 +98,11 @@ export const BUILTIN_BLOCK_KINDS = [
|
|
|
96
98
|
kind: "tool_result",
|
|
97
99
|
tag: "tool_result",
|
|
98
100
|
data: true,
|
|
99
|
-
lines: ["tool_result.call"],
|
|
101
|
+
lines: ["tool_result.call", "tool_result.{status}"],
|
|
100
102
|
fields: [
|
|
101
103
|
{ name: "callId", type: "string" },
|
|
102
104
|
{ name: "name", type: "string" },
|
|
105
|
+
{ name: "status", type: "string" },
|
|
103
106
|
{ name: "content", type: "string" },
|
|
104
107
|
],
|
|
105
108
|
},
|
|
@@ -203,6 +206,57 @@ export const BUILTIN_BLOCK_KINDS = [
|
|
|
203
206
|
{ name: "description", type: "string" },
|
|
204
207
|
],
|
|
205
208
|
},
|
|
209
|
+
{
|
|
210
|
+
kind: "plugin_section",
|
|
211
|
+
data: true,
|
|
212
|
+
lines: ["system.plugin.outside"],
|
|
213
|
+
fields: [{ name: "content", type: "blocks" }],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
kind: "prompt_delta",
|
|
217
|
+
tag: "prompt_delta",
|
|
218
|
+
lines: ["delivery.prompt_delta", "delivery.time"],
|
|
219
|
+
fields: [
|
|
220
|
+
{ name: "time", type: "string" },
|
|
221
|
+
{ name: "items", type: "blocks" },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
kind: "tools_delta",
|
|
226
|
+
tag: "tools_delta",
|
|
227
|
+
lines: ["delivery.tools_delta", "delivery.time"],
|
|
228
|
+
fields: [
|
|
229
|
+
{ name: "time", type: "string" },
|
|
230
|
+
{ name: "items", type: "blocks" },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
kind: "tool_change",
|
|
235
|
+
lines: ["delivery.tool_change"],
|
|
236
|
+
fields: [
|
|
237
|
+
{ name: "change", type: "string" },
|
|
238
|
+
{ name: "name", type: "string" },
|
|
239
|
+
{ name: "source", type: "string" },
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
kind: "prompt_section",
|
|
244
|
+
// The tag is the plugin's own choice, so it rides on the block
|
|
245
|
+
// (docs/prompting.md#plugin-sections-that-change).
|
|
246
|
+
tagField: "tag",
|
|
247
|
+
fields: [
|
|
248
|
+
{ name: "tag", type: "string", tag: true },
|
|
249
|
+
{ name: "items", type: "blocks" },
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
kind: "prompt_line",
|
|
254
|
+
lines: ["delivery.prompt_line"],
|
|
255
|
+
fields: [
|
|
256
|
+
{ name: "change", type: "string" },
|
|
257
|
+
{ name: "line", type: "string" },
|
|
258
|
+
],
|
|
259
|
+
},
|
|
206
260
|
{
|
|
207
261
|
kind: "continuation_note",
|
|
208
262
|
tag: "continuation_note",
|
|
@@ -232,4 +286,10 @@ export const BUILTIN_BLOCK_KINDS = [
|
|
|
232
286
|
lines: ["continuation.handoff.none"],
|
|
233
287
|
fields: [{ name: "previous", type: "ref" }],
|
|
234
288
|
},
|
|
289
|
+
{
|
|
290
|
+
kind: "continuation_interrupted",
|
|
291
|
+
tag: "continuation_interrupted",
|
|
292
|
+
lines: ["continuation.interrupted"],
|
|
293
|
+
fields: [{ name: "previous", type: "ref" }],
|
|
294
|
+
},
|
|
235
295
|
];
|
package/build.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* builders are the shapes the core, the tools and the plugins use, so a block
|
|
7
7
|
* of one kind is always built the same way.
|
|
8
8
|
*/
|
|
9
|
-
import type { Block, Delivery, EnvDelta, EnvItem, Handoff, Note, NoteSummary, Ref, TaskRef, WindowRef } from "@4fu/arbor-interfaces";
|
|
9
|
+
import type { Block, Delivery, DirectToolSpec, EnvDelta, EnvItem, ExternalToolChange, Handoff, Note, NoteSummary, Ref, TaskRef, TextTable, WindowRef } from "@4fu/arbor-interfaces";
|
|
10
10
|
import type { Renderer } from "./render.ts";
|
|
11
11
|
/** One task, as it would open a window (docs/prompting.md#deliveries). */
|
|
12
12
|
export declare function taskBlock(task: {
|
|
@@ -27,6 +27,8 @@ export declare function boardLineBlock(line: {
|
|
|
27
27
|
used: number;
|
|
28
28
|
limit: number;
|
|
29
29
|
expects?: readonly string[];
|
|
30
|
+
/** The window's open work in its workspace, each item as the model reads it. */
|
|
31
|
+
openWork?: readonly string[];
|
|
30
32
|
}): Block;
|
|
31
33
|
/** The cost of ending a window with unfinished work. */
|
|
32
34
|
export declare function handoffBlock(handoff: Handoff): Block;
|
|
@@ -43,12 +45,14 @@ export declare function reminderBlock(reminder: {
|
|
|
43
45
|
limit: number;
|
|
44
46
|
percent: number;
|
|
45
47
|
time: number;
|
|
48
|
+
/** The window's open work in its workspace, each item as the model reads it. */
|
|
49
|
+
openWork?: readonly string[];
|
|
46
50
|
}): Block;
|
|
47
51
|
/** A tool's result, as data (docs/prompting.md#layers). */
|
|
48
52
|
export declare function toolResultBlock(call: {
|
|
49
53
|
callId: string;
|
|
50
54
|
name: string;
|
|
51
|
-
}, content: string): Block;
|
|
55
|
+
}, content: string, status?: "ok" | "failed"): Block;
|
|
52
56
|
/** A refusal: the rule that refused the call and the next action. */
|
|
53
57
|
export declare function errorBlock(error: {
|
|
54
58
|
tool: string;
|
|
@@ -100,6 +104,13 @@ export declare function conversationUpdateBlock(update: {
|
|
|
100
104
|
}): Block;
|
|
101
105
|
/** One environment delta: one line per item, `+`, `~` or `-`. */
|
|
102
106
|
export declare function environmentDeltaBlock(delta: EnvDelta, time: number): Block;
|
|
107
|
+
/**
|
|
108
|
+
* One tools delta: the direct tools declared from outside the core that
|
|
109
|
+
* appeared, and the ones that were removed, one line each
|
|
110
|
+
* (docs/tools.md#external-tools-that-change). The window's own prefix is not
|
|
111
|
+
* rebuilt: it reads the change and calls what is new from its next tool call.
|
|
112
|
+
*/
|
|
113
|
+
export declare function toolDeltaBlock(changes: readonly ExternalToolChange[], time: number): Block;
|
|
103
114
|
/** The fixed notes of a continuation (docs/prompting.md#fixed-notes). */
|
|
104
115
|
export declare function continuationBlocks(continuation: {
|
|
105
116
|
previous: WindowRef;
|
|
@@ -113,7 +124,41 @@ export declare function midTaskResetBlock(previous: WindowRef): Block;
|
|
|
113
124
|
export declare function continuationHandoffBlock(previous: WindowRef, handoff: Handoff): Block;
|
|
114
125
|
/** No handoff: the order to recover from. */
|
|
115
126
|
export declare function continuationRecoveryBlock(previous: WindowRef): Block;
|
|
127
|
+
/** The previous window's run was cut short by a model failure. */
|
|
128
|
+
export declare function continuationInterruptedBlock(previous: WindowRef): Block;
|
|
129
|
+
/**
|
|
130
|
+
* One plugin prompt section's change, for a window that opened before it
|
|
131
|
+
* (docs/prompting.md#plugin-sections-that-change).
|
|
132
|
+
*
|
|
133
|
+
* Only the lines that changed are carried: a line both texts hold is left out,
|
|
134
|
+
* so the delta never repeats unchanged text and never rebuilds the prompt.
|
|
135
|
+
* `undefined` when nothing moved, so a section whose content did not change
|
|
136
|
+
* emits no delta at all.
|
|
137
|
+
*/
|
|
138
|
+
export declare function promptSectionDeltaBlock(section: {
|
|
139
|
+
/** The tag the section renders in, which is the plugin's own choice. */
|
|
140
|
+
tag: string;
|
|
141
|
+
/** What the window's prefix holds of the section. */
|
|
142
|
+
from: string;
|
|
143
|
+
/** What the section says now. */
|
|
144
|
+
to: string;
|
|
145
|
+
/** True when the section is material from outside the framework. */
|
|
146
|
+
outside?: boolean;
|
|
147
|
+
}, time: number): Block | undefined;
|
|
116
148
|
/** One environment item's line, for `<env>` and `tool_search`. */
|
|
117
149
|
export declare function envItemLine(item: EnvItem, renderer: Renderer): string;
|
|
150
|
+
/**
|
|
151
|
+
* One direct tool's line, the same one the `<tools>` section writes and the one
|
|
152
|
+
* `tool_search` answers with (docs/tools.md#external-tools).
|
|
153
|
+
*
|
|
154
|
+
* A tool declared from outside the core says where it comes from, so the model
|
|
155
|
+
* knows whose it is wherever it meets the name. A tool that carries the JSON
|
|
156
|
+
* Schema its own harness declared shows it on the line too, so the model reads
|
|
157
|
+
* the parameters it will actually call with - `enum`, `items`, a nested
|
|
158
|
+
* object's own properties - and not a summary that dropped them. The schema is
|
|
159
|
+
* escaped like every other string from outside the framework: it cannot close
|
|
160
|
+
* the section it renders in.
|
|
161
|
+
*/
|
|
162
|
+
export declare function toolLine(table: TextTable, tool: DirectToolSpec): string;
|
|
118
163
|
/** The text one delivery is steered in as. */
|
|
119
164
|
export declare function renderDelivery(delivery: Delivery, renderer: Renderer): string;
|
package/build.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* builders are the shapes the core, the tools and the plugins use, so a block
|
|
7
7
|
* of one kind is always built the same way.
|
|
8
8
|
*/
|
|
9
|
+
import { changedLines } from "./diff.js";
|
|
10
|
+
import { escapeText } from "./render.js";
|
|
9
11
|
import { textOf } from "./text.js";
|
|
10
12
|
import { formatTime } from "./time.js";
|
|
11
13
|
/** One task, as it would open a window (docs/prompting.md#deliveries). */
|
|
@@ -35,6 +37,7 @@ export function boardLineBlock(line) {
|
|
|
35
37
|
used: line.used,
|
|
36
38
|
limit: line.limit,
|
|
37
39
|
...(line.expects === undefined || line.expects.length === 0 ? {} : { expects: [...line.expects] }),
|
|
40
|
+
...(line.openWork === undefined || line.openWork.length === 0 ? {} : { openWork: [...line.openWork] }),
|
|
38
41
|
},
|
|
39
42
|
};
|
|
40
43
|
}
|
|
@@ -90,14 +93,17 @@ export function reminderBlock(reminder) {
|
|
|
90
93
|
limit: reminder.limit,
|
|
91
94
|
percent: reminder.percent,
|
|
92
95
|
time: formatTime(reminder.time),
|
|
96
|
+
...(reminder.openWork === undefined || reminder.openWork.length === 0
|
|
97
|
+
? {}
|
|
98
|
+
: { openWork: [...reminder.openWork] }),
|
|
93
99
|
},
|
|
94
100
|
};
|
|
95
101
|
}
|
|
96
102
|
/** A tool's result, as data (docs/prompting.md#layers). */
|
|
97
|
-
export function toolResultBlock(call, content) {
|
|
103
|
+
export function toolResultBlock(call, content, status = "ok") {
|
|
98
104
|
return {
|
|
99
105
|
kind: "tool_result",
|
|
100
|
-
fields: { callId: call.callId, name: call.name, content },
|
|
106
|
+
fields: { callId: call.callId, name: call.name, status, content },
|
|
101
107
|
};
|
|
102
108
|
}
|
|
103
109
|
/** A refusal: the rule that refused the call and the next action. */
|
|
@@ -196,6 +202,22 @@ export function environmentDeltaBlock(delta, time) {
|
|
|
196
202
|
fields: { revision: delta.revision, time: formatTime(time), items },
|
|
197
203
|
};
|
|
198
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* One tools delta: the direct tools declared from outside the core that
|
|
207
|
+
* appeared, and the ones that were removed, one line each
|
|
208
|
+
* (docs/tools.md#external-tools-that-change). The window's own prefix is not
|
|
209
|
+
* rebuilt: it reads the change and calls what is new from its next tool call.
|
|
210
|
+
*/
|
|
211
|
+
export function toolDeltaBlock(changes, time) {
|
|
212
|
+
const items = changes.map((change) => ({
|
|
213
|
+
kind: "tool_change",
|
|
214
|
+
fields: { change: DELTA_MARK[change.change], name: change.name, source: change.source },
|
|
215
|
+
}));
|
|
216
|
+
return {
|
|
217
|
+
kind: "tools_delta",
|
|
218
|
+
fields: { time: formatTime(time), items },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
199
221
|
/** The fixed notes of a continuation (docs/prompting.md#fixed-notes). */
|
|
200
222
|
export function continuationBlocks(continuation) {
|
|
201
223
|
return [
|
|
@@ -231,6 +253,36 @@ export function continuationHandoffBlock(previous, handoff) {
|
|
|
231
253
|
export function continuationRecoveryBlock(previous) {
|
|
232
254
|
return { kind: "continuation_recovery", fields: { previous } };
|
|
233
255
|
}
|
|
256
|
+
/** The previous window's run was cut short by a model failure. */
|
|
257
|
+
export function continuationInterruptedBlock(previous) {
|
|
258
|
+
return { kind: "continuation_interrupted", fields: { previous } };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* One plugin prompt section's change, for a window that opened before it
|
|
262
|
+
* (docs/prompting.md#plugin-sections-that-change).
|
|
263
|
+
*
|
|
264
|
+
* Only the lines that changed are carried: a line both texts hold is left out,
|
|
265
|
+
* so the delta never repeats unchanged text and never rebuilds the prompt.
|
|
266
|
+
* `undefined` when nothing moved, so a section whose content did not change
|
|
267
|
+
* emits no delta at all.
|
|
268
|
+
*/
|
|
269
|
+
export function promptSectionDeltaBlock(section, time) {
|
|
270
|
+
const changes = changedLines(section.from, section.to);
|
|
271
|
+
if (changes.length === 0)
|
|
272
|
+
return undefined;
|
|
273
|
+
const lines = changes.map((change) => ({
|
|
274
|
+
kind: "prompt_line",
|
|
275
|
+
fields: { change: change.change, line: change.line },
|
|
276
|
+
}));
|
|
277
|
+
const tagged = { kind: "prompt_section", fields: { tag: section.tag, items: lines } };
|
|
278
|
+
return {
|
|
279
|
+
kind: "prompt_delta",
|
|
280
|
+
fields: {
|
|
281
|
+
time: formatTime(time),
|
|
282
|
+
items: [section.outside === true ? { kind: "plugin_section", fields: { content: [tagged] } } : tagged],
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
234
286
|
/** One environment item's line, for `<env>` and `tool_search`. */
|
|
235
287
|
export function envItemLine(item, renderer) {
|
|
236
288
|
return item.path === undefined
|
|
@@ -241,6 +293,35 @@ export function envItemLine(item, renderer) {
|
|
|
241
293
|
path: item.path,
|
|
242
294
|
});
|
|
243
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* One direct tool's line, the same one the `<tools>` section writes and the one
|
|
298
|
+
* `tool_search` answers with (docs/tools.md#external-tools).
|
|
299
|
+
*
|
|
300
|
+
* A tool declared from outside the core says where it comes from, so the model
|
|
301
|
+
* knows whose it is wherever it meets the name. A tool that carries the JSON
|
|
302
|
+
* Schema its own harness declared shows it on the line too, so the model reads
|
|
303
|
+
* the parameters it will actually call with - `enum`, `items`, a nested
|
|
304
|
+
* object's own properties - and not a summary that dropped them. The schema is
|
|
305
|
+
* escaped like every other string from outside the framework: it cannot close
|
|
306
|
+
* the section it renders in.
|
|
307
|
+
*/
|
|
308
|
+
export function toolLine(table, tool) {
|
|
309
|
+
const schema = tool.schema === undefined ? undefined : escapeText(JSON.stringify(tool.schema));
|
|
310
|
+
const withSource = tool.source !== undefined;
|
|
311
|
+
const key = withSource
|
|
312
|
+
? schema === undefined
|
|
313
|
+
? "system.tool.source"
|
|
314
|
+
: "system.tool.source.schema"
|
|
315
|
+
: schema === undefined
|
|
316
|
+
? "system.tool"
|
|
317
|
+
: "system.tool.schema";
|
|
318
|
+
return textOf(table, key, {
|
|
319
|
+
name: tool.name,
|
|
320
|
+
description: tool.description,
|
|
321
|
+
...(tool.source === undefined ? {} : { source: tool.source }),
|
|
322
|
+
...(schema === undefined ? {} : { schema }),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
244
325
|
/** The text one delivery is steered in as. */
|
|
245
326
|
export function renderDelivery(delivery, renderer) {
|
|
246
327
|
return renderer.render([...delivery.content]);
|
package/diff.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The line diff a prompt-section delta is built from
|
|
3
|
+
* (docs/prompting.md#plugin-sections-that-change).
|
|
4
|
+
*
|
|
5
|
+
* A plugin's prompt section can change while a window runs. The window learns
|
|
6
|
+
* the change the way it learns an environment change - as a delta at the next
|
|
7
|
+
* tool-call boundary - and the delta carries only the lines that changed, so
|
|
8
|
+
* unchanged text is never repeated and the whole system prompt is never
|
|
9
|
+
* rebuilt. The diff is by longest common subsequence: a line both texts hold is
|
|
10
|
+
* left out, and a line that moved is reported where it now stands.
|
|
11
|
+
*/
|
|
12
|
+
/** One line that changed between two texts. */
|
|
13
|
+
export interface LineChange {
|
|
14
|
+
/** `+` for a line the new text has and the old one did not, `-` for one it lost. */
|
|
15
|
+
change: "+" | "-";
|
|
16
|
+
line: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The lines that changed between two texts, in the order the new text stands
|
|
20
|
+
* in. Two texts with the same lines give nothing, so a section whose content
|
|
21
|
+
* did not move emits no delta at all.
|
|
22
|
+
*/
|
|
23
|
+
export declare function changedLines(from: string, to: string): readonly LineChange[];
|
package/diff.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The line diff a prompt-section delta is built from
|
|
3
|
+
* (docs/prompting.md#plugin-sections-that-change).
|
|
4
|
+
*
|
|
5
|
+
* A plugin's prompt section can change while a window runs. The window learns
|
|
6
|
+
* the change the way it learns an environment change - as a delta at the next
|
|
7
|
+
* tool-call boundary - and the delta carries only the lines that changed, so
|
|
8
|
+
* unchanged text is never repeated and the whole system prompt is never
|
|
9
|
+
* rebuilt. The diff is by longest common subsequence: a line both texts hold is
|
|
10
|
+
* left out, and a line that moved is reported where it now stands.
|
|
11
|
+
*/
|
|
12
|
+
/** The lines one text carries. A blank line carries nothing a diff can say. */
|
|
13
|
+
function linesOf(text) {
|
|
14
|
+
return text.split("\n").filter((line) => line !== "");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The index pairs the two texts share, longest first: the lines that are in
|
|
18
|
+
* both, in order. The table is filled from the end, so one pass walks it from
|
|
19
|
+
* the start and prefers the side that keeps more lines in common.
|
|
20
|
+
*/
|
|
21
|
+
function commonLines(before, after) {
|
|
22
|
+
const rows = before.length;
|
|
23
|
+
const columns = after.length;
|
|
24
|
+
const table = Array.from({ length: rows + 1 }, () => new Array(columns + 1).fill(0));
|
|
25
|
+
for (let row = rows - 1; row >= 0; row -= 1) {
|
|
26
|
+
const current = table[row];
|
|
27
|
+
if (current === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
for (let column = columns - 1; column >= 0; column -= 1) {
|
|
30
|
+
const down = table[row + 1]?.[column] ?? 0;
|
|
31
|
+
const right = current[column + 1] ?? 0;
|
|
32
|
+
const diagonal = table[row + 1]?.[column + 1] ?? 0;
|
|
33
|
+
current[column] = before[row] === after[column] ? diagonal + 1 : Math.max(down, right);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const pairs = [];
|
|
37
|
+
let row = 0;
|
|
38
|
+
let column = 0;
|
|
39
|
+
while (row < rows && column < columns) {
|
|
40
|
+
if (before[row] === after[column]) {
|
|
41
|
+
pairs.push([row, column]);
|
|
42
|
+
row += 1;
|
|
43
|
+
column += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if ((table[row + 1]?.[column] ?? 0) >= (table[row]?.[column + 1] ?? 0))
|
|
47
|
+
row += 1;
|
|
48
|
+
else
|
|
49
|
+
column += 1;
|
|
50
|
+
}
|
|
51
|
+
return pairs;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The lines that changed between two texts, in the order the new text stands
|
|
55
|
+
* in. Two texts with the same lines give nothing, so a section whose content
|
|
56
|
+
* did not move emits no delta at all.
|
|
57
|
+
*/
|
|
58
|
+
export function changedLines(from, to) {
|
|
59
|
+
const before = linesOf(from);
|
|
60
|
+
const after = linesOf(to);
|
|
61
|
+
const changes = [];
|
|
62
|
+
let row = 0;
|
|
63
|
+
let column = 0;
|
|
64
|
+
for (const [sameRow, sameColumn] of commonLines(before, after)) {
|
|
65
|
+
while (row < sameRow) {
|
|
66
|
+
const line = before[row];
|
|
67
|
+
if (line !== undefined)
|
|
68
|
+
changes.push({ change: "-", line });
|
|
69
|
+
row += 1;
|
|
70
|
+
}
|
|
71
|
+
while (column < sameColumn) {
|
|
72
|
+
const line = after[column];
|
|
73
|
+
if (line !== undefined)
|
|
74
|
+
changes.push({ change: "+", line });
|
|
75
|
+
column += 1;
|
|
76
|
+
}
|
|
77
|
+
row = sameRow + 1;
|
|
78
|
+
column = sameColumn + 1;
|
|
79
|
+
}
|
|
80
|
+
while (row < before.length) {
|
|
81
|
+
const line = before[row];
|
|
82
|
+
if (line !== undefined)
|
|
83
|
+
changes.push({ change: "-", line });
|
|
84
|
+
row += 1;
|
|
85
|
+
}
|
|
86
|
+
while (column < after.length) {
|
|
87
|
+
const line = after[column];
|
|
88
|
+
if (line !== undefined)
|
|
89
|
+
changes.push({ change: "+", line });
|
|
90
|
+
column += 1;
|
|
91
|
+
}
|
|
92
|
+
return changes;
|
|
93
|
+
}
|
package/first-input.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export interface FirstInputInput {
|
|
|
26
26
|
handoff?: Handoff;
|
|
27
27
|
/** Set after the hard limit: the window was reset mid-task. */
|
|
28
28
|
midTaskReset?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Set when the previous window's run was cut short by a model failure:
|
|
31
|
+
* the steps it had taken stand, and the ones it had sent are not repeated.
|
|
32
|
+
*/
|
|
33
|
+
interrupted?: boolean;
|
|
29
34
|
/** The tasks the previous window still had open. */
|
|
30
35
|
openTasks?: readonly Ref[];
|
|
31
36
|
/** The effects the previous window already made. */
|
package/first-input.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* It is this window's own layer, so it carries the per-input facts a frozen
|
|
7
7
|
* prefix must not: the time, the targets, the numbers.
|
|
8
8
|
*/
|
|
9
|
-
import { boardLineBlock, continuationBlocks, continuationHandoffBlock, continuationRecoveryBlock, handoffBlock, midTaskResetBlock, taskBlock, } from "./build.js";
|
|
9
|
+
import { boardLineBlock, continuationBlocks, continuationHandoffBlock, continuationInterruptedBlock, continuationRecoveryBlock, handoffBlock, midTaskResetBlock, taskBlock, } from "./build.js";
|
|
10
10
|
/** The blocks of one window's first input, in the layout's order. */
|
|
11
11
|
export function firstInputBlocks(input) {
|
|
12
12
|
const blocks = [taskBlock(input.task)];
|
|
@@ -19,6 +19,7 @@ export function firstInputBlocks(input) {
|
|
|
19
19
|
used: line.usage.used,
|
|
20
20
|
limit: line.usage.limit,
|
|
21
21
|
expects: line.expects.map((expectation) => expectation.description ?? expectation.ref),
|
|
22
|
+
...(line.openWork === undefined ? {} : { openWork: line.openWork }),
|
|
22
23
|
}));
|
|
23
24
|
}
|
|
24
25
|
const continuation = input.continuation;
|
|
@@ -31,6 +32,8 @@ export function firstInputBlocks(input) {
|
|
|
31
32
|
}));
|
|
32
33
|
if (continuation.midTaskReset === true)
|
|
33
34
|
blocks.push(midTaskResetBlock(continuation.previous));
|
|
35
|
+
if (continuation.interrupted === true)
|
|
36
|
+
blocks.push(continuationInterruptedBlock(continuation.previous));
|
|
34
37
|
if (continuation.handoff === undefined) {
|
|
35
38
|
blocks.push(continuationRecoveryBlock(continuation.previous));
|
|
36
39
|
}
|
package/index.d.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* finished text.
|
|
10
10
|
*/
|
|
11
11
|
export { BUILTIN_BLOCK_KINDS } from "./blocks.ts";
|
|
12
|
+
export { changedLines, type LineChange } from "./diff.ts";
|
|
12
13
|
export { Renderer, escapeText, type RendererOptions } from "./render.ts";
|
|
13
|
-
export { DEFAULT_GENERAL_RULES, renderSystemPrompt, type SystemPromptInput, } from "./system.ts";
|
|
14
|
+
export { DEFAULT_GENERAL_RULES, renderPluginSection, renderPluginSections, renderSystemPrompt, sectionRevision, type RenderedPluginSection, type RenderedSystemPrompt, type SystemPromptInput, } from "./system.ts";
|
|
14
15
|
export { memoryBlocks, noteSortKey, renderMemorySnapshot, } from "./memory.ts";
|
|
15
|
-
export { boardLineBlock, broadcastBlock, continuationBlocks, continuationHandoffBlock, continuationRecoveryBlock, conversationUpdateBlock, dataBlock, envItemLine, environmentDeltaBlock, errorBlock, handoffBlock, midTaskResetBlock, noteBlock, noteSummaryBlock, noticeBlock, outsideMessageBlock, reminderBlock, renderDelivery, taskBlock, toolResultBlock, windowMessageBlock, } from "./build.ts";
|
|
16
|
+
export { boardLineBlock, broadcastBlock, continuationBlocks, continuationHandoffBlock, continuationInterruptedBlock, continuationRecoveryBlock, conversationUpdateBlock, dataBlock, envItemLine, environmentDeltaBlock, errorBlock, handoffBlock, midTaskResetBlock, noteBlock, noteSummaryBlock, noticeBlock, outsideMessageBlock, promptSectionDeltaBlock, reminderBlock, renderDelivery, taskBlock, toolDeltaBlock, toolLine, toolResultBlock, windowMessageBlock, } from "./build.ts";
|
|
16
17
|
export { firstInputBlocks, type FirstInputInput } from "./first-input.ts";
|
|
17
18
|
export { REMINDER_SERIES, REMINDER_TIERS, atHardLimit, hardLimitAt, hardLimitReserve, nextReminderTier, usedPercent, type ReminderTier, } from "./reminder.ts";
|
|
18
19
|
export { formatText, textOf, zhTextTable, type TextArgs } from "./text.ts";
|
package/index.js
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* finished text.
|
|
10
10
|
*/
|
|
11
11
|
export { BUILTIN_BLOCK_KINDS } from "./blocks.js";
|
|
12
|
+
export { changedLines } from "./diff.js";
|
|
12
13
|
export { Renderer, escapeText } from "./render.js";
|
|
13
|
-
export { DEFAULT_GENERAL_RULES, renderSystemPrompt, } from "./system.js";
|
|
14
|
+
export { DEFAULT_GENERAL_RULES, renderPluginSection, renderPluginSections, renderSystemPrompt, sectionRevision, } from "./system.js";
|
|
14
15
|
export { memoryBlocks, noteSortKey, renderMemorySnapshot, } from "./memory.js";
|
|
15
|
-
export { boardLineBlock, broadcastBlock, continuationBlocks, continuationHandoffBlock, continuationRecoveryBlock, conversationUpdateBlock, dataBlock, envItemLine, environmentDeltaBlock, errorBlock, handoffBlock, midTaskResetBlock, noteBlock, noteSummaryBlock, noticeBlock, outsideMessageBlock, reminderBlock, renderDelivery, taskBlock, toolResultBlock, windowMessageBlock, } from "./build.js";
|
|
16
|
+
export { boardLineBlock, broadcastBlock, continuationBlocks, continuationHandoffBlock, continuationInterruptedBlock, continuationRecoveryBlock, conversationUpdateBlock, dataBlock, envItemLine, environmentDeltaBlock, errorBlock, handoffBlock, midTaskResetBlock, noteBlock, noteSummaryBlock, noticeBlock, outsideMessageBlock, promptSectionDeltaBlock, reminderBlock, renderDelivery, taskBlock, toolDeltaBlock, toolLine, toolResultBlock, windowMessageBlock, } from "./build.js";
|
|
16
17
|
export { firstInputBlocks } from "./first-input.js";
|
|
17
18
|
export { REMINDER_SERIES, REMINDER_TIERS, atHardLimit, hardLimitAt, hardLimitReserve, nextReminderTier, usedPercent, } from "./reminder.js";
|
|
18
19
|
export { formatText, textOf, zhTextTable } from "./text.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4fu/arbor-prompt",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.1107.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed prompt blocks, the one deterministic renderer, the replaceable Chinese text table and the system prompt sections.",
|
|
6
6
|
"exports": {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"blocks.js",
|
|
17
17
|
"build.d.ts",
|
|
18
18
|
"build.js",
|
|
19
|
+
"diff.d.ts",
|
|
20
|
+
"diff.js",
|
|
19
21
|
"first-input.d.ts",
|
|
20
22
|
"first-input.js",
|
|
21
23
|
"index.d.ts",
|
|
@@ -46,7 +48,7 @@
|
|
|
46
48
|
"access": "public"
|
|
47
49
|
},
|
|
48
50
|
"dependencies": {
|
|
49
|
-
"@4fu/arbor-errors": "2026.
|
|
50
|
-
"@4fu/arbor-interfaces": "2026.
|
|
51
|
+
"@4fu/arbor-errors": "2026.1107.0",
|
|
52
|
+
"@4fu/arbor-interfaces": "2026.1107.0"
|
|
51
53
|
}
|
|
52
54
|
}
|
package/reminder.d.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Usage reminders (docs/windows.md#usage-reminders).
|
|
3
3
|
*
|
|
4
|
-
* Host reminders are appended to an input, never to the prefix
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
4
|
+
* Host reminders are appended to an input, never to the prefix. The series has
|
|
5
|
+
* seven tiers, each with its own instruction: at 15, 30 and 50 % write down
|
|
6
|
+
* what a later window would need; at 60 % say what the boundary is; at 70 %
|
|
7
|
+
* open no new large task; at 80 % rotate before new work; a final one at
|
|
8
|
+
* `window - 2 x reserve` rotates now. Each carries the real numbers, its place
|
|
9
|
+
* in the series and, from 80 % on, the cost of ignoring it. Only the highest
|
|
10
|
+
* crossed tier is sent; the lower ones it jumped over are recorded as sent
|
|
11
|
+
* with it.
|
|
10
12
|
*/
|
|
11
13
|
import type { TokenUsage } from "@4fu/arbor-interfaces";
|
|
12
14
|
/** One tier of the series. `tier` names the instruction in the text table. */
|
|
13
15
|
export interface ReminderTier {
|
|
14
16
|
/** The share of the window, in percent. */
|
|
15
17
|
percent: number;
|
|
16
|
-
/**
|
|
17
|
-
tier: "
|
|
18
|
+
/** The tier's key in the text table: `p15` … `p80`, or `final`. */
|
|
19
|
+
tier: "p15" | "p30" | "p50" | "p60" | "p70" | "p80" | "final";
|
|
18
20
|
/** The share of the window's tokens at which this tier fires. */
|
|
19
21
|
at: (usage: TokenUsage) => number;
|
|
20
22
|
}
|