@4fu/arbor-prompt 2026.1107.1 → 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.1",
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.1"
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
@@ -59,22 +59,26 @@ export declare function sectionRevision(section: PromptSection): Promise<string>
59
59
  * `window` is the window this render is for: a section that serves several
60
60
  * sessions answers for that one, and a section whose content is the same
61
61
  * everywhere renders the same text for each
62
- * (docs/prompting.md#plugin-sections-that-change).
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).
63
65
  */
64
- export declare function renderPluginSection(section: PromptSection, renderer: Renderer, window?: WindowRef): Promise<RenderedPluginSection>;
66
+ export declare function renderPluginSection(section: PromptSection, renderer: Renderer, window?: WindowRef, label?: string): Promise<RenderedPluginSection>;
65
67
  /**
66
68
  * Every plugin section, rendered once, in the order they registered, for the
67
69
  * window they are rendered for. A section whose content renders to nothing is
68
70
  * still listed - with empty text - so a later call can tell a section that
69
71
  * stayed empty from one that appeared.
70
72
  */
71
- export declare function renderPluginSections(plugins: readonly PromptSection[], renderer: Renderer, window?: WindowRef): Promise<readonly RenderedPluginSection[]>;
73
+ export declare function renderPluginSections(plugins: readonly PromptSection[], renderer: Renderer, window?: WindowRef, label?: string): Promise<readonly RenderedPluginSection[]>;
72
74
  /**
73
75
  * Build the frozen prefix. `plugins` are rendered first because their content
74
76
  * is produced per epoch, their sections render in a tag named after the plugin,
75
77
  * and one of them being outside is what puts the precedence rule into `<agent>`.
76
78
  *
77
79
  * `window` is the window the prefix is built for, when it is built for one: a
78
- * section that renders per window makes this prefix that window's own.
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).
79
83
  */
80
- export declare function renderSystemPrompt(input: SystemPromptInput, renderer: Renderer, window?: WindowRef): Promise<RenderedSystemPrompt>;
84
+ export declare function renderSystemPrompt(input: SystemPromptInput, renderer: Renderer, window?: WindowRef, label?: string): Promise<RenderedSystemPrompt>;
package/system.js CHANGED
@@ -97,10 +97,12 @@ function outsideBlock(content) {
97
97
  * `window` is the window this render is for: a section that serves several
98
98
  * sessions answers for that one, and a section whose content is the same
99
99
  * everywhere renders the same text for each
100
- * (docs/prompting.md#plugin-sections-that-change).
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).
101
103
  */
102
- export async function renderPluginSection(section, renderer, window) {
103
- const content = await section.content(window);
104
+ export async function renderPluginSection(section, renderer, window, label) {
105
+ const content = await section.content(window, label);
104
106
  return {
105
107
  tag: section.tag,
106
108
  text: renderer.render(section.outside === true ? [outsideBlock(content)] : content),
@@ -114,10 +116,10 @@ export async function renderPluginSection(section, renderer, window) {
114
116
  * still listed - with empty text - so a later call can tell a section that
115
117
  * stayed empty from one that appeared.
116
118
  */
117
- export async function renderPluginSections(plugins, renderer, window) {
119
+ export async function renderPluginSections(plugins, renderer, window, label) {
118
120
  const rendered = [];
119
121
  for (const plugin of plugins)
120
- rendered.push(await renderPluginSection(plugin, renderer, window));
122
+ rendered.push(await renderPluginSection(plugin, renderer, window, label));
121
123
  return rendered;
122
124
  }
123
125
  /**
@@ -126,11 +128,13 @@ export async function renderPluginSections(plugins, renderer, window) {
126
128
  * and one of them being outside is what puts the precedence rule into `<agent>`.
127
129
  *
128
130
  * `window` is the window the prefix is built for, when it is built for one: a
129
- * section that renders per window makes this prefix that window's own.
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).
130
134
  */
131
- export async function renderSystemPrompt(input, renderer, window) {
135
+ export async function renderSystemPrompt(input, renderer, window, label) {
132
136
  const table = renderer.table;
133
- const sections = await renderPluginSections(input.plugins ?? [], renderer, window);
137
+ const sections = await renderPluginSections(input.plugins ?? [], renderer, window, label);
134
138
  const parts = sectionsOf(input, table, sections.some((section) => section.outside)).map((section) => [`<${section.tag}>`, ...section.lines, `</${section.tag}>`].join("\n"));
135
139
  for (const section of sections) {
136
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
  });