@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 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 directory per window.** Each window gets its own directory, and a
31
- continuation keeps working in its predecessor's directory in place. It is not
32
- bound to a repository; the agent clones and pushes what it needs.
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 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.
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-workspace-dir`](packages/workspace-dir) | the default `Workspace`: one directory per window, with a continuation working in its predecessor's directory in place |
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 directories: isolation, continuity, what the agent does |
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.1106.0",
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.1106.0",
50
- "@4fu/arbor-interfaces": "2026.1106.0"
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: at 15, 30 and
5
- * 50 % of the window, write down what a later window would need; at 60, 70 and
6
- * 80 %, also end the window with a handoff; a final one at
7
- * `window - 2 x reserve`. Each carries the real numbers, its place in the
8
- * series and the cost of ignoring it. Only the highest crossed tier is sent;
9
- * the lower ones it jumped over are recorded as sent with it.
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
- /** `note`, `handoff` or `final`. */
17
- tier: "note" | "handoff" | "final";
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
  }