@4fu/arbor-prompt 2026.1107.0 → 2026.1109.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4fu/arbor-prompt",
3
- "version": "2026.1107.0",
3
+ "version": "2026.1109.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": {
@@ -49,6 +49,6 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@4fu/arbor-errors": "2026.1107.0",
52
- "@4fu/arbor-interfaces": "2026.1107.0"
52
+ "@4fu/arbor-interfaces": "2026.1109.0"
53
53
  }
54
54
  }
package/render.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * - the output is deterministic, so the same blocks always give the same bytes.
11
11
  */
12
12
  import { BUILTIN_BLOCK_KINDS } from "./blocks.js";
13
- import { formatText, zhTextTable } from "./text.js";
13
+ import { formatText, zhTextTable, PLACEHOLDER } from "./text.js";
14
14
  /** Joins refs, which are identifiers. */
15
15
  const REF_SEPARATOR = ", ";
16
16
  /** Joins a list of words inside one sentence. */
@@ -169,15 +169,20 @@ export class Renderer {
169
169
  const template = this.table[resolved];
170
170
  if (template === undefined)
171
171
  return undefined;
172
- for (const [, name] of `${key} ${template}`.matchAll(/\{(\w+)\}/g)) {
172
+ for (const [, name] of `${key} ${template}`.matchAll(PLACEHOLDER)) {
173
173
  if (name === undefined)
174
174
  continue;
175
175
  if (args[name] === undefined || args[name] === "")
176
176
  return undefined;
177
177
  carried.add(name);
178
178
  }
179
+ // The line is complete when every placeholder its own template names has a
180
+ // value - never by scanning what was filled: a value that carries a `{` of
181
+ // its own, JSON in a message or a snippet in a note, is data the line must
182
+ // keep, not a placeholder it must drop for
183
+ // (docs/prompting.md#blocks-and-the-renderer).
179
184
  const filled = formatText(template, args);
180
- if (filled.includes("{") || filled.trim() === "")
185
+ if (filled.trim() === "")
181
186
  return undefined;
182
187
  return filled;
183
188
  }
package/system.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * with the snapshot epoch, and rendering is deterministic: no timestamps or
7
7
  * counters reach it.
8
8
  */
9
- import type { DirectToolSpec, EnvItem, ModuleSpec, PromptSection, Settings } from "@4fu/arbor-interfaces";
9
+ import type { DirectToolSpec, EnvItem, ModuleSpec, PromptSection, Settings, WindowRef } from "@4fu/arbor-interfaces";
10
10
  import { Renderer } from "./render.ts";
11
11
  /** What one epoch's system prompt is built from. */
12
12
  export interface SystemPromptInput {
@@ -55,17 +55,30 @@ export declare function sectionRevision(section: PromptSection): Promise<string>
55
55
  * One plugin's prompt section, rendered in its tag, with what it produced. The
56
56
  * revision it reports is what a later call compares against, so an unchanged
57
57
  * section is never rendered twice.
58
+ *
59
+ * `window` is the window this render is for: a section that serves several
60
+ * sessions answers for that one, and a section whose content is the same
61
+ * everywhere renders the same text for each
62
+ * (docs/prompting.md#plugin-sections-that-change). `label` is the opaque label
63
+ * the embedding attached to what that window serves, carried through
64
+ * (docs/windows.md#what-a-window-serves).
58
65
  */
59
- export declare function renderPluginSection(section: PromptSection, renderer: Renderer): Promise<RenderedPluginSection>;
66
+ export declare function renderPluginSection(section: PromptSection, renderer: Renderer, window?: WindowRef, label?: string): Promise<RenderedPluginSection>;
60
67
  /**
61
- * Every plugin section, rendered once, in the order they registered. A section
62
- * whose content renders to nothing is still listed - with empty text - so a
63
- * later call can tell a section that stayed empty from one that appeared.
68
+ * Every plugin section, rendered once, in the order they registered, for the
69
+ * window they are rendered for. A section whose content renders to nothing is
70
+ * still listed - with empty text - so a later call can tell a section that
71
+ * stayed empty from one that appeared.
64
72
  */
65
- export declare function renderPluginSections(plugins: readonly PromptSection[], renderer: Renderer): Promise<readonly RenderedPluginSection[]>;
73
+ export declare function renderPluginSections(plugins: readonly PromptSection[], renderer: Renderer, window?: WindowRef, label?: string): Promise<readonly RenderedPluginSection[]>;
66
74
  /**
67
75
  * Build the frozen prefix. `plugins` are rendered first because their content
68
76
  * is produced per epoch, their sections render in a tag named after the plugin,
69
77
  * and one of them being outside is what puts the precedence rule into `<agent>`.
78
+ *
79
+ * `window` is the window the prefix is built for, when it is built for one: a
80
+ * section that renders per window makes this prefix that window's own. `label`
81
+ * is the opaque label the embedding attached to what that window serves, which
82
+ * a per-window section reads (docs/windows.md#what-a-window-serves).
70
83
  */
71
- export declare function renderSystemPrompt(input: SystemPromptInput, renderer: Renderer): Promise<RenderedSystemPrompt>;
84
+ export declare function renderSystemPrompt(input: SystemPromptInput, renderer: Renderer, window?: WindowRef, label?: string): Promise<RenderedSystemPrompt>;
package/system.js CHANGED
@@ -93,9 +93,16 @@ function outsideBlock(content) {
93
93
  * One plugin's prompt section, rendered in its tag, with what it produced. The
94
94
  * revision it reports is what a later call compares against, so an unchanged
95
95
  * section is never rendered twice.
96
+ *
97
+ * `window` is the window this render is for: a section that serves several
98
+ * sessions answers for that one, and a section whose content is the same
99
+ * everywhere renders the same text for each
100
+ * (docs/prompting.md#plugin-sections-that-change). `label` is the opaque label
101
+ * the embedding attached to what that window serves, carried through
102
+ * (docs/windows.md#what-a-window-serves).
96
103
  */
97
- export async function renderPluginSection(section, renderer) {
98
- const content = await section.content();
104
+ export async function renderPluginSection(section, renderer, window, label) {
105
+ const content = await section.content(window, label);
99
106
  return {
100
107
  tag: section.tag,
101
108
  text: renderer.render(section.outside === true ? [outsideBlock(content)] : content),
@@ -104,24 +111,30 @@ export async function renderPluginSection(section, renderer) {
104
111
  };
105
112
  }
106
113
  /**
107
- * Every plugin section, rendered once, in the order they registered. A section
108
- * whose content renders to nothing is still listed - with empty text - so a
109
- * later call can tell a section that stayed empty from one that appeared.
114
+ * Every plugin section, rendered once, in the order they registered, for the
115
+ * window they are rendered for. A section whose content renders to nothing is
116
+ * still listed - with empty text - so a later call can tell a section that
117
+ * stayed empty from one that appeared.
110
118
  */
111
- export async function renderPluginSections(plugins, renderer) {
119
+ export async function renderPluginSections(plugins, renderer, window, label) {
112
120
  const rendered = [];
113
121
  for (const plugin of plugins)
114
- rendered.push(await renderPluginSection(plugin, renderer));
122
+ rendered.push(await renderPluginSection(plugin, renderer, window, label));
115
123
  return rendered;
116
124
  }
117
125
  /**
118
126
  * Build the frozen prefix. `plugins` are rendered first because their content
119
127
  * is produced per epoch, their sections render in a tag named after the plugin,
120
128
  * and one of them being outside is what puts the precedence rule into `<agent>`.
129
+ *
130
+ * `window` is the window the prefix is built for, when it is built for one: a
131
+ * section that renders per window makes this prefix that window's own. `label`
132
+ * is the opaque label the embedding attached to what that window serves, which
133
+ * a per-window section reads (docs/windows.md#what-a-window-serves).
121
134
  */
122
- export async function renderSystemPrompt(input, renderer) {
135
+ export async function renderSystemPrompt(input, renderer, window, label) {
123
136
  const table = renderer.table;
124
- const sections = await renderPluginSections(input.plugins ?? [], renderer);
137
+ const sections = await renderPluginSections(input.plugins ?? [], renderer, window, label);
125
138
  const parts = sectionsOf(input, table, sections.some((section) => section.outside)).map((section) => [`<${section.tag}>`, ...section.lines, `</${section.tag}>`].join("\n"));
126
139
  for (const section of sections) {
127
140
  if (section.text === "")
package/text.d.ts CHANGED
@@ -11,6 +11,13 @@ import type { TextTable } from "@4fu/arbor-interfaces";
11
11
  export type TextArgs = Record<string, string | number>;
12
12
  /** The framework's own table: Chinese, one entry per fixed string. */
13
13
  export declare const zhTextTable: TextTable;
14
+ /**
15
+ * One placeholder in a table entry: `{name}`. It is the whole vocabulary of
16
+ * what a renderer fills, so anything else inside braces - `{ everyMs }` written
17
+ * with spaces, a `{` a filled value carries - is literal text and stays as it
18
+ * was written.
19
+ */
20
+ export declare const PLACEHOLDER: RegExp;
14
21
  /** Fill one table entry. Unknown placeholders are left as they are. */
15
22
  export declare function formatText(template: string, args?: TextArgs): string;
16
23
  /**
package/text.js CHANGED
@@ -94,7 +94,7 @@ export const zhTextTable = {
94
94
  "continuation.open_work": "上个窗口链未完成的工作:{openWork};按需继续或收尾,状态写进笔记和交接内容。",
95
95
  // ---- results and refusals the tools report -----------------------------
96
96
  "secret.hidden": "[hidden:{name}]",
97
- "result.sent": "已记录 {ref},效果已入队。",
97
+ "result.sent": "已记录 {ref},效果已入队;执行结果以效果结果事件为准。",
98
98
  "result.effect.ok": "效果 {ref} 已执行。",
99
99
  "result.effect.failed": "效果 {ref} 没有执行({reason})。",
100
100
  "result.window_message": "消息 {ref} 已发送给 {window}。",
@@ -168,6 +168,7 @@ export const zhTextTable = {
168
168
  "refused.send.target": "发送目标不能为空。",
169
169
  "refused.send.content": "发送内容不能为空。",
170
170
  "refused.send.secret": "发送内容包含环境变量 {name} 的原始值,本次没有发送。",
171
+ "refused.send.unowned": "没有效果执行器负责目标 {target},本次没有发送。",
171
172
  "refused.bash.notfound": "没有作业 {job}。",
172
173
  "refused.note.write.journal": "笔记 {ref} 没有写入:记录拒绝了这次事件({detail})。",
173
174
  "refused.note.write.title": "笔记需要一个标题。",
@@ -234,6 +235,7 @@ export const zhTextTable = {
234
235
  "next.send.target": "目标写成一个引用或一个外部 id。",
235
236
  "next.send.content": "补上要发送的内容。",
236
237
  "next.send.secret": "请删去其中涉及该值的内容,或只写变量名 {name} 指代,然后重发。",
238
+ "next.send.unowned": "用 channels.list 查看已接入的频道,把内容发到有频道负责的目标。",
237
239
  "next.bash.notfound": "用 bash 重新执行该命令;本窗口不会收到后台作业的完成通知,需要结果时用 bash_output 查询。",
238
240
  "next.note.write.journal": "修正记录拒绝的原因,然后重新写入。",
239
241
  "next.note.write.title": "补上一个简短标题。",
@@ -377,11 +379,18 @@ export const zhTextTable = {
377
379
  "function.services.status": "查看一个服务的状态:版本、端口、重启次数和最近错误。",
378
380
  "function.services.logs": "读取一个服务的日志,可按行数取尾部。",
379
381
  };
382
+ /**
383
+ * One placeholder in a table entry: `{name}`. It is the whole vocabulary of
384
+ * what a renderer fills, so anything else inside braces - `{ everyMs }` written
385
+ * with spaces, a `{` a filled value carries - is literal text and stays as it
386
+ * was written.
387
+ */
388
+ export const PLACEHOLDER = /\{(\w+)\}/g;
380
389
  /** Fill one table entry. Unknown placeholders are left as they are. */
381
390
  export function formatText(template, args) {
382
391
  if (args === undefined)
383
392
  return template;
384
- return template.replace(/\{(\w+)\}/g, (match, key) => {
393
+ return template.replace(PLACEHOLDER, (match, key) => {
385
394
  const value = args[key];
386
395
  return value === undefined ? match : String(value);
387
396
  });