@4fu/arbor-prompt 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +187 -2
- package/blocks.d.ts +14 -0
- package/blocks.js +235 -0
- package/build.d.ts +119 -0
- package/build.js +247 -0
- package/first-input.d.ts +40 -0
- package/first-input.js +47 -0
- package/index.d.ts +19 -0
- package/index.js +19 -0
- package/memory.d.ts +15 -0
- package/memory.js +31 -0
- package/package.json +50 -4
- package/reminder.d.ts +34 -0
- package/reminder.js +63 -0
- package/render.d.ts +66 -0
- package/render.js +220 -0
- package/system.d.ts +28 -0
- package/system.js +78 -0
- package/text.d.ts +20 -0
- package/text.js +300 -0
- package/time.d.ts +9 -0
- package/time.js +17 -0
package/build.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block builders: the one place a block is constructed
|
|
3
|
+
* (docs/prompting.md#blocks-and-the-renderer).
|
|
4
|
+
*
|
|
5
|
+
* No component writes prompt text; it hands the renderer blocks. These
|
|
6
|
+
* builders are the shapes the core, the tools and the plugins use, so a block
|
|
7
|
+
* of one kind is always built the same way.
|
|
8
|
+
*/
|
|
9
|
+
import { textOf } from "./text.js";
|
|
10
|
+
import { formatTime } from "./time.js";
|
|
11
|
+
/** One task, as it would open a window (docs/prompting.md#deliveries). */
|
|
12
|
+
export function taskBlock(task) {
|
|
13
|
+
return {
|
|
14
|
+
kind: "task",
|
|
15
|
+
fields: {
|
|
16
|
+
ref: task.ref,
|
|
17
|
+
kind: task.kind,
|
|
18
|
+
reason: task.reason,
|
|
19
|
+
time: formatTime(task.time),
|
|
20
|
+
targets: [...task.targets],
|
|
21
|
+
...(task.source === undefined ? {} : { source: task.source }),
|
|
22
|
+
...(task.content === undefined ? {} : { content: [...task.content] }),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** One line on the board (docs/coordination.md#the-board). */
|
|
27
|
+
export function boardLineBlock(line) {
|
|
28
|
+
return {
|
|
29
|
+
kind: "board_line",
|
|
30
|
+
fields: {
|
|
31
|
+
window: line.window,
|
|
32
|
+
state: line.state,
|
|
33
|
+
doing: line.doing,
|
|
34
|
+
targets: [...line.targets],
|
|
35
|
+
used: line.used,
|
|
36
|
+
limit: line.limit,
|
|
37
|
+
...(line.expects === undefined || line.expects.length === 0 ? {} : { expects: [...line.expects] }),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The cost of ending a window with unfinished work. */
|
|
42
|
+
export function handoffBlock(handoff) {
|
|
43
|
+
return {
|
|
44
|
+
kind: "handoff",
|
|
45
|
+
fields: {
|
|
46
|
+
topics: [...handoff.topics],
|
|
47
|
+
progress: [...handoff.progress],
|
|
48
|
+
pending: [...handoff.pending],
|
|
49
|
+
agreements: [...handoff.agreements],
|
|
50
|
+
refs: [...handoff.refs],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** A note's body, as data (docs/memory.md#notes). */
|
|
55
|
+
export function noteBlock(note) {
|
|
56
|
+
return {
|
|
57
|
+
kind: "note",
|
|
58
|
+
fields: {
|
|
59
|
+
ref: note.ref,
|
|
60
|
+
title: note.title,
|
|
61
|
+
summary: note.summary,
|
|
62
|
+
revision: note.revision,
|
|
63
|
+
status: note.status,
|
|
64
|
+
...(note.body === null ? {} : { body: [dataBlock(note.body)] }),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** A note's summary, for lists and the memory snapshot. */
|
|
69
|
+
export function noteSummaryBlock(note) {
|
|
70
|
+
return {
|
|
71
|
+
kind: "note_summary",
|
|
72
|
+
fields: {
|
|
73
|
+
ref: note.ref,
|
|
74
|
+
title: note.title,
|
|
75
|
+
summary: note.summary,
|
|
76
|
+
revision: note.revision,
|
|
77
|
+
status: note.status,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** A host usage reminder (docs/windows.md#usage-reminders). */
|
|
82
|
+
export function reminderBlock(reminder) {
|
|
83
|
+
return {
|
|
84
|
+
kind: "reminder",
|
|
85
|
+
fields: {
|
|
86
|
+
tier: reminder.tier,
|
|
87
|
+
index: reminder.index,
|
|
88
|
+
total: reminder.total,
|
|
89
|
+
used: reminder.used,
|
|
90
|
+
limit: reminder.limit,
|
|
91
|
+
percent: reminder.percent,
|
|
92
|
+
time: formatTime(reminder.time),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** A tool's result, as data (docs/prompting.md#layers). */
|
|
97
|
+
export function toolResultBlock(call, content) {
|
|
98
|
+
return {
|
|
99
|
+
kind: "tool_result",
|
|
100
|
+
fields: { callId: call.callId, name: call.name, content },
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** A refusal: the rule that refused the call and the next action. */
|
|
104
|
+
export function errorBlock(error) {
|
|
105
|
+
return {
|
|
106
|
+
kind: "error",
|
|
107
|
+
fields: {
|
|
108
|
+
tool: error.tool,
|
|
109
|
+
rule: error.rule,
|
|
110
|
+
message: error.message,
|
|
111
|
+
...(error.nextAction === undefined ? {} : { next: error.nextAction }),
|
|
112
|
+
...(error.refs === undefined || error.refs.length === 0 ? {} : { refs: [...error.refs] }),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Anything outside the framework is data, never instructions. */
|
|
117
|
+
export function dataBlock(text) {
|
|
118
|
+
return { kind: "data", fields: { content: text } };
|
|
119
|
+
}
|
|
120
|
+
/** One outside message, by its native id (docs/journal.md#external-ids). */
|
|
121
|
+
export function outsideMessageBlock(message) {
|
|
122
|
+
return {
|
|
123
|
+
kind: "outside_message",
|
|
124
|
+
fields: {
|
|
125
|
+
...(message.external === undefined ? {} : { external: message.external }),
|
|
126
|
+
source: message.source,
|
|
127
|
+
time: formatTime(message.time),
|
|
128
|
+
...(message.replyTo === undefined ? {} : { replyTo: message.replyTo }),
|
|
129
|
+
...(message.content === undefined ? {} : { content: [...message.content] }),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** A notice: the event's opening words and its ref, from which window. */
|
|
134
|
+
export function noticeBlock(notice) {
|
|
135
|
+
return {
|
|
136
|
+
kind: "notice",
|
|
137
|
+
fields: {
|
|
138
|
+
...(notice.from === undefined ? {} : { from: notice.from }),
|
|
139
|
+
refs: [...notice.refs],
|
|
140
|
+
time: formatTime(notice.time),
|
|
141
|
+
...(notice.content === undefined || notice.content === "" ? {} : { content: notice.content }),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** A broadcast: it needs no reply, and a reply goes to the sender. */
|
|
146
|
+
export function broadcastBlock(broadcast) {
|
|
147
|
+
return {
|
|
148
|
+
kind: "broadcast",
|
|
149
|
+
fields: {
|
|
150
|
+
...(broadcast.from === undefined ? {} : { from: broadcast.from }),
|
|
151
|
+
time: formatTime(broadcast.time),
|
|
152
|
+
...(broadcast.content === undefined ? {} : { content: [...broadcast.content] }),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** A window message, with its `reply_to` when it has one. */
|
|
157
|
+
export function windowMessageBlock(message) {
|
|
158
|
+
return {
|
|
159
|
+
kind: "window_message",
|
|
160
|
+
fields: {
|
|
161
|
+
from: message.from,
|
|
162
|
+
...(message.replyTo === undefined ? {} : { replyTo: message.replyTo }),
|
|
163
|
+
...(message.ref === undefined ? {} : { ref: message.ref }),
|
|
164
|
+
time: formatTime(message.time),
|
|
165
|
+
...(message.content === undefined ? {} : { content: [...message.content] }),
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** New events on a conversation a window is in. */
|
|
170
|
+
export function conversationUpdateBlock(update) {
|
|
171
|
+
return {
|
|
172
|
+
kind: "conversation_update",
|
|
173
|
+
fields: {
|
|
174
|
+
conversation: update.conversation,
|
|
175
|
+
refs: [...update.refs],
|
|
176
|
+
time: formatTime(update.time),
|
|
177
|
+
...(update.content === undefined ? {} : { content: [...update.content] }),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** What a delta says arrived, changed or left: one character per item. */
|
|
182
|
+
const DELTA_MARK = { added: "+", changed: "~", removed: "-" };
|
|
183
|
+
/** One environment delta: one line per item, `+`, `~` or `-`. */
|
|
184
|
+
export function environmentDeltaBlock(delta, time) {
|
|
185
|
+
const items = delta.items.map((change) => ({
|
|
186
|
+
kind: "env_item",
|
|
187
|
+
fields: {
|
|
188
|
+
change: DELTA_MARK[change.change],
|
|
189
|
+
kind: change.item.kind,
|
|
190
|
+
name: change.item.name,
|
|
191
|
+
description: change.item.description,
|
|
192
|
+
},
|
|
193
|
+
}));
|
|
194
|
+
return {
|
|
195
|
+
kind: "environment_delta",
|
|
196
|
+
fields: { revision: delta.revision, time: formatTime(time), items },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** The fixed notes of a continuation (docs/prompting.md#fixed-notes). */
|
|
200
|
+
export function continuationBlocks(continuation) {
|
|
201
|
+
return [
|
|
202
|
+
{
|
|
203
|
+
kind: "continuation_note",
|
|
204
|
+
fields: {
|
|
205
|
+
previous: continuation.previous,
|
|
206
|
+
...(continuation.openTasks === undefined || continuation.openTasks.length === 0
|
|
207
|
+
? {}
|
|
208
|
+
: { openTasks: [...continuation.openTasks] }),
|
|
209
|
+
...(continuation.effects === undefined || continuation.effects.length === 0
|
|
210
|
+
? {}
|
|
211
|
+
: { effects: [...continuation.effects] }),
|
|
212
|
+
...(continuation.changes === undefined || continuation.changes.length === 0
|
|
213
|
+
? {}
|
|
214
|
+
: { changes: [...continuation.changes] }),
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
/** The one host line after the hard limit. */
|
|
220
|
+
export function midTaskResetBlock(previous) {
|
|
221
|
+
return { kind: "continuation_midtask", fields: { previous } };
|
|
222
|
+
}
|
|
223
|
+
/** The handoff, labelled as the agent's own earlier note. */
|
|
224
|
+
export function continuationHandoffBlock(previous, handoff) {
|
|
225
|
+
return {
|
|
226
|
+
kind: "continuation_handoff",
|
|
227
|
+
fields: { previous, handoff: [handoffBlock(handoff)] },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** No handoff: the order to recover from. */
|
|
231
|
+
export function continuationRecoveryBlock(previous) {
|
|
232
|
+
return { kind: "continuation_recovery", fields: { previous } };
|
|
233
|
+
}
|
|
234
|
+
/** One environment item's line, for `<env>` and `tool_search`. */
|
|
235
|
+
export function envItemLine(item, renderer) {
|
|
236
|
+
return item.path === undefined
|
|
237
|
+
? textOf(renderer.table, "system.env.item", { name: item.name, description: item.description })
|
|
238
|
+
: textOf(renderer.table, "system.env.skill", {
|
|
239
|
+
name: item.name,
|
|
240
|
+
description: item.description,
|
|
241
|
+
path: item.path,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/** The text one delivery is steered in as. */
|
|
245
|
+
export function renderDelivery(delivery, renderer) {
|
|
246
|
+
return renderer.render([...delivery.content]);
|
|
247
|
+
}
|
package/first-input.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first input: the task, the board lines related to it, and for a
|
|
3
|
+
* continuation the previous window's ref and handoff, plus what changed since
|
|
4
|
+
* the epoch (docs/prompting.md#layers, docs/windows.md#prompt-layout).
|
|
5
|
+
*
|
|
6
|
+
* It is this window's own layer, so it carries the per-input facts a frozen
|
|
7
|
+
* prefix must not: the time, the targets, the numbers.
|
|
8
|
+
*/
|
|
9
|
+
import type { Block, BoardLine, Handoff, Ref, TaskRef, WindowRef } from "@4fu/arbor-interfaces";
|
|
10
|
+
import { handoffBlock } from "./build.ts";
|
|
11
|
+
/** What the first input is built from. */
|
|
12
|
+
export interface FirstInputInput {
|
|
13
|
+
task: {
|
|
14
|
+
ref: TaskRef;
|
|
15
|
+
kind: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
time: number;
|
|
18
|
+
targets: readonly string[];
|
|
19
|
+
source?: string;
|
|
20
|
+
content?: readonly Block[];
|
|
21
|
+
};
|
|
22
|
+
/** The live windows related to the task. */
|
|
23
|
+
boardLines?: readonly BoardLine[];
|
|
24
|
+
continuation?: {
|
|
25
|
+
previous: WindowRef;
|
|
26
|
+
handoff?: Handoff;
|
|
27
|
+
/** Set after the hard limit: the window was reset mid-task. */
|
|
28
|
+
midTaskReset?: boolean;
|
|
29
|
+
/** The tasks the previous window still had open. */
|
|
30
|
+
openTasks?: readonly Ref[];
|
|
31
|
+
/** The effects the previous window already made. */
|
|
32
|
+
effects?: readonly Ref[];
|
|
33
|
+
};
|
|
34
|
+
/** Notes and environment changes since the snapshot epoch. */
|
|
35
|
+
changes?: readonly Block[];
|
|
36
|
+
}
|
|
37
|
+
/** The blocks of one window's first input, in the layout's order. */
|
|
38
|
+
export declare function firstInputBlocks(input: FirstInputInput): Block[];
|
|
39
|
+
/** A handoff's blocks, for a continuation's first input. */
|
|
40
|
+
export { handoffBlock };
|
package/first-input.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first input: the task, the board lines related to it, and for a
|
|
3
|
+
* continuation the previous window's ref and handoff, plus what changed since
|
|
4
|
+
* the epoch (docs/prompting.md#layers, docs/windows.md#prompt-layout).
|
|
5
|
+
*
|
|
6
|
+
* It is this window's own layer, so it carries the per-input facts a frozen
|
|
7
|
+
* prefix must not: the time, the targets, the numbers.
|
|
8
|
+
*/
|
|
9
|
+
import { boardLineBlock, continuationBlocks, continuationHandoffBlock, continuationRecoveryBlock, handoffBlock, midTaskResetBlock, taskBlock, } from "./build.js";
|
|
10
|
+
/** The blocks of one window's first input, in the layout's order. */
|
|
11
|
+
export function firstInputBlocks(input) {
|
|
12
|
+
const blocks = [taskBlock(input.task)];
|
|
13
|
+
for (const line of input.boardLines ?? []) {
|
|
14
|
+
blocks.push(boardLineBlock({
|
|
15
|
+
window: line.window,
|
|
16
|
+
state: line.state,
|
|
17
|
+
doing: line.doing,
|
|
18
|
+
targets: line.targets,
|
|
19
|
+
used: line.usage.used,
|
|
20
|
+
limit: line.usage.limit,
|
|
21
|
+
expects: line.expects.map((expectation) => expectation.description ?? expectation.ref),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
const continuation = input.continuation;
|
|
25
|
+
if (continuation !== undefined) {
|
|
26
|
+
blocks.push(...continuationBlocks({
|
|
27
|
+
previous: continuation.previous,
|
|
28
|
+
...(continuation.openTasks === undefined ? {} : { openTasks: continuation.openTasks }),
|
|
29
|
+
...(continuation.effects === undefined ? {} : { effects: continuation.effects }),
|
|
30
|
+
...(input.changes === undefined ? {} : { changes: input.changes }),
|
|
31
|
+
}));
|
|
32
|
+
if (continuation.midTaskReset === true)
|
|
33
|
+
blocks.push(midTaskResetBlock(continuation.previous));
|
|
34
|
+
if (continuation.handoff === undefined) {
|
|
35
|
+
blocks.push(continuationRecoveryBlock(continuation.previous));
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
blocks.push(continuationHandoffBlock(continuation.previous, continuation.handoff));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
blocks.push(...(input.changes ?? []));
|
|
43
|
+
}
|
|
44
|
+
return blocks;
|
|
45
|
+
}
|
|
46
|
+
/** A handoff's blocks, for a continuation's first input. */
|
|
47
|
+
export { handoffBlock };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @4fu/arbor-prompt - typed blocks, the one deterministic renderer and the
|
|
3
|
+
* framework's own text (docs/prompting.md).
|
|
4
|
+
*
|
|
5
|
+
* No component writes prompt text. Everything that reaches a model is a typed
|
|
6
|
+
* block: a kind and its fields. One renderer turns blocks into text when a
|
|
7
|
+
* request is built, by fixed rules, so the same blocks always give the same
|
|
8
|
+
* bytes. A plugin registers kinds of its own and hands the host blocks, never
|
|
9
|
+
* finished text.
|
|
10
|
+
*/
|
|
11
|
+
export { BUILTIN_BLOCK_KINDS } from "./blocks.ts";
|
|
12
|
+
export { Renderer, escapeText, type RendererOptions } from "./render.ts";
|
|
13
|
+
export { DEFAULT_GENERAL_RULES, renderSystemPrompt, type SystemPromptInput, } from "./system.ts";
|
|
14
|
+
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 { firstInputBlocks, type FirstInputInput } from "./first-input.ts";
|
|
17
|
+
export { REMINDER_SERIES, REMINDER_TIERS, atHardLimit, hardLimitAt, hardLimitReserve, nextReminderTier, usedPercent, type ReminderTier, } from "./reminder.ts";
|
|
18
|
+
export { formatText, textOf, zhTextTable, type TextArgs } from "./text.ts";
|
|
19
|
+
export { formatTime } from "./time.ts";
|
package/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @4fu/arbor-prompt - typed blocks, the one deterministic renderer and the
|
|
3
|
+
* framework's own text (docs/prompting.md).
|
|
4
|
+
*
|
|
5
|
+
* No component writes prompt text. Everything that reaches a model is a typed
|
|
6
|
+
* block: a kind and its fields. One renderer turns blocks into text when a
|
|
7
|
+
* request is built, by fixed rules, so the same blocks always give the same
|
|
8
|
+
* bytes. A plugin registers kinds of its own and hands the host blocks, never
|
|
9
|
+
* finished text.
|
|
10
|
+
*/
|
|
11
|
+
export { BUILTIN_BLOCK_KINDS } from "./blocks.js";
|
|
12
|
+
export { Renderer, escapeText } from "./render.js";
|
|
13
|
+
export { DEFAULT_GENERAL_RULES, renderSystemPrompt, } from "./system.js";
|
|
14
|
+
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 { firstInputBlocks } from "./first-input.js";
|
|
17
|
+
export { REMINDER_SERIES, REMINDER_TIERS, atHardLimit, hardLimitAt, hardLimitReserve, nextReminderTier, usedPercent, } from "./reminder.js";
|
|
18
|
+
export { formatText, textOf, zhTextTable } from "./text.js";
|
|
19
|
+
export { formatTime } from "./time.js";
|
package/memory.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory snapshot: the active notes, by summary
|
|
3
|
+
* (docs/prompting.md#layers, docs/windows.md#prompt-layout).
|
|
4
|
+
*
|
|
5
|
+
* It is part of the frozen prefix, so rendering is deterministic: notes sorted
|
|
6
|
+
* by id, no timestamps and no counters. Lists show summaries, never bodies.
|
|
7
|
+
*/
|
|
8
|
+
import type { Block, NoteSummary } from "@4fu/arbor-interfaces";
|
|
9
|
+
import type { Renderer } from "./render.ts";
|
|
10
|
+
/** `n012` sorts by its number, so `n9` comes before `n10`. */
|
|
11
|
+
export declare function noteSortKey(ref: string): number;
|
|
12
|
+
/** The active set as blocks, sorted by id. */
|
|
13
|
+
export declare function memoryBlocks(notes: readonly NoteSummary[]): Block[];
|
|
14
|
+
/** The snapshot's text. Empty when the active set is empty. */
|
|
15
|
+
export declare function renderMemorySnapshot(notes: readonly NoteSummary[], renderer: Renderer): string;
|
package/memory.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory snapshot: the active notes, by summary
|
|
3
|
+
* (docs/prompting.md#layers, docs/windows.md#prompt-layout).
|
|
4
|
+
*
|
|
5
|
+
* It is part of the frozen prefix, so rendering is deterministic: notes sorted
|
|
6
|
+
* by id, no timestamps and no counters. Lists show summaries, never bodies.
|
|
7
|
+
*/
|
|
8
|
+
/** `n012` sorts by its number, so `n9` comes before `n10`. */
|
|
9
|
+
export function noteSortKey(ref) {
|
|
10
|
+
const match = /^n(\d+)/.exec(ref);
|
|
11
|
+
return match === null ? Number.MAX_SAFE_INTEGER : Number(match[1]);
|
|
12
|
+
}
|
|
13
|
+
/** The active set as blocks, sorted by id. */
|
|
14
|
+
export function memoryBlocks(notes) {
|
|
15
|
+
return [...notes]
|
|
16
|
+
.sort((left, right) => noteSortKey(left.id) - noteSortKey(right.id))
|
|
17
|
+
.map((note) => ({
|
|
18
|
+
kind: "note_summary",
|
|
19
|
+
fields: {
|
|
20
|
+
ref: note.ref,
|
|
21
|
+
title: note.title,
|
|
22
|
+
summary: note.summary,
|
|
23
|
+
revision: note.revision,
|
|
24
|
+
status: note.status,
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
/** The snapshot's text. Empty when the active set is empty. */
|
|
29
|
+
export function renderMemorySnapshot(notes, renderer) {
|
|
30
|
+
return renderer.render(memoryBlocks(notes));
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4fu/arbor-prompt",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Typed prompt blocks, the one deterministic renderer, the replaceable Chinese text table and the system prompt sections.",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"default": "./index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"main": "./index.js",
|
|
14
|
+
"files": [
|
|
15
|
+
"blocks.d.ts",
|
|
16
|
+
"blocks.js",
|
|
17
|
+
"build.d.ts",
|
|
18
|
+
"build.js",
|
|
19
|
+
"first-input.d.ts",
|
|
20
|
+
"first-input.js",
|
|
21
|
+
"index.d.ts",
|
|
22
|
+
"index.js",
|
|
23
|
+
"memory.d.ts",
|
|
24
|
+
"memory.js",
|
|
25
|
+
"reminder.d.ts",
|
|
26
|
+
"reminder.js",
|
|
27
|
+
"render.d.ts",
|
|
28
|
+
"render.js",
|
|
29
|
+
"system.d.ts",
|
|
30
|
+
"system.js",
|
|
31
|
+
"text.d.ts",
|
|
32
|
+
"text.js",
|
|
33
|
+
"time.d.ts",
|
|
34
|
+
"time.js"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=24"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/4fuu/arbor.git",
|
|
43
|
+
"directory": "packages/prompt"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@4fu/arbor-errors": "0.1.0",
|
|
50
|
+
"@4fu/arbor-interfaces": "0.1.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/reminder.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage reminders (docs/windows.md#usage-reminders).
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
import type { TokenUsage } from "@4fu/arbor-interfaces";
|
|
12
|
+
/** One tier of the series. `tier` names the instruction in the text table. */
|
|
13
|
+
export interface ReminderTier {
|
|
14
|
+
/** The share of the window, in percent. */
|
|
15
|
+
percent: number;
|
|
16
|
+
/** `note`, `handoff` or `final`. */
|
|
17
|
+
tier: "note" | "handoff" | "final";
|
|
18
|
+
/** The share of the window's tokens at which this tier fires. */
|
|
19
|
+
at: (usage: TokenUsage) => number;
|
|
20
|
+
}
|
|
21
|
+
/** The tiers in their fixed order. */
|
|
22
|
+
export declare const REMINDER_TIERS: readonly ReminderTier[];
|
|
23
|
+
/** How many reminders the series holds. */
|
|
24
|
+
export declare const REMINDER_SERIES: number;
|
|
25
|
+
/** The share of the window the host holds back at the hard limit. */
|
|
26
|
+
export declare function hardLimitReserve(limit: number): number;
|
|
27
|
+
/** The token count at which the host ends the window by itself. */
|
|
28
|
+
export declare function hardLimitAt(usage: TokenUsage): number;
|
|
29
|
+
/** True once the window has reached its hard limit. */
|
|
30
|
+
export declare function atHardLimit(usage: TokenUsage): boolean;
|
|
31
|
+
/** The share of the window used, in whole percent. */
|
|
32
|
+
export declare function usedPercent(usage: TokenUsage): number;
|
|
33
|
+
/** The tier that fires next, or `null` when none has been crossed. */
|
|
34
|
+
export declare function nextReminderTier(usage: TokenUsage, sent: number): ReminderTier | null;
|
package/reminder.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage reminders (docs/windows.md#usage-reminders).
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
const HARD_LIMIT_TOKENS = 16000;
|
|
12
|
+
function atPercent(percent) {
|
|
13
|
+
return (usage) => Math.floor((usage.limit * percent) / 100);
|
|
14
|
+
}
|
|
15
|
+
/** The tiers in their fixed order. */
|
|
16
|
+
export const REMINDER_TIERS = [
|
|
17
|
+
{ percent: 15, tier: "note", at: atPercent(15) },
|
|
18
|
+
{ percent: 30, tier: "note", at: atPercent(30) },
|
|
19
|
+
{ percent: 50, tier: "note", at: atPercent(50) },
|
|
20
|
+
{ percent: 60, tier: "handoff", at: atPercent(60) },
|
|
21
|
+
{ percent: 70, tier: "handoff", at: atPercent(70) },
|
|
22
|
+
{ percent: 80, tier: "handoff", at: atPercent(80) },
|
|
23
|
+
{
|
|
24
|
+
percent: 100,
|
|
25
|
+
tier: "final",
|
|
26
|
+
at: (usage) => usage.limit - 2 * usage.reserve,
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
/** How many reminders the series holds. */
|
|
30
|
+
export const REMINDER_SERIES = REMINDER_TIERS.length;
|
|
31
|
+
/** The share of the window the host holds back at the hard limit. */
|
|
32
|
+
export function hardLimitReserve(limit) {
|
|
33
|
+
return Math.min(HARD_LIMIT_TOKENS, Math.floor(limit / 4));
|
|
34
|
+
}
|
|
35
|
+
/** The token count at which the host ends the window by itself. */
|
|
36
|
+
export function hardLimitAt(usage) {
|
|
37
|
+
return usage.limit - hardLimitReserve(usage.limit);
|
|
38
|
+
}
|
|
39
|
+
/** True once the window has reached its hard limit. */
|
|
40
|
+
export function atHardLimit(usage) {
|
|
41
|
+
return usage.used >= hardLimitAt(usage);
|
|
42
|
+
}
|
|
43
|
+
/** The share of the window used, in whole percent. */
|
|
44
|
+
export function usedPercent(usage) {
|
|
45
|
+
if (usage.limit <= 0)
|
|
46
|
+
return 0;
|
|
47
|
+
return Math.min(100, Math.floor((usage.used / usage.limit) * 100));
|
|
48
|
+
}
|
|
49
|
+
/** The tier that fires next, or `null` when none has been crossed. */
|
|
50
|
+
export function nextReminderTier(usage, sent) {
|
|
51
|
+
let crossed = null;
|
|
52
|
+
for (let index = 0; index < REMINDER_TIERS.length; index += 1) {
|
|
53
|
+
const tier = REMINDER_TIERS[index];
|
|
54
|
+
if (tier === undefined)
|
|
55
|
+
continue;
|
|
56
|
+
if (usage.used >= tier.at(usage))
|
|
57
|
+
crossed = tier;
|
|
58
|
+
}
|
|
59
|
+
if (crossed === null)
|
|
60
|
+
return null;
|
|
61
|
+
const index = REMINDER_TIERS.indexOf(crossed);
|
|
62
|
+
return index < sent ? null : crossed;
|
|
63
|
+
}
|
package/render.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one renderer: blocks in, text out, by fixed rules
|
|
3
|
+
* (docs/prompting.md#blocks-and-the-renderer).
|
|
4
|
+
*
|
|
5
|
+
* - each kind maps to one shape, with its tag, its field order and its fixed
|
|
6
|
+
* lines taken from the text table;
|
|
7
|
+
* - content inside a block is escaped, so outside text cannot close a tag or
|
|
8
|
+
* pose as host text;
|
|
9
|
+
* - data blocks are marked as data;
|
|
10
|
+
* - the output is deterministic, so the same blocks always give the same bytes.
|
|
11
|
+
*/
|
|
12
|
+
import type { Block, BlockKindSpec, TextTable } from "@4fu/arbor-interfaces";
|
|
13
|
+
/** Escape content so outside text cannot close a tag or pose as host text. */
|
|
14
|
+
export declare function escapeText(value: string): string;
|
|
15
|
+
export interface RendererOptions {
|
|
16
|
+
/** The fixed strings. Defaults to the framework's Chinese table. */
|
|
17
|
+
table?: TextTable;
|
|
18
|
+
/** Extra kinds, from a plugin (`Plugin.promptSection`). */
|
|
19
|
+
kinds?: readonly BlockKindSpec[];
|
|
20
|
+
/** Skip the built-in kinds. A deployment that restates them passes its own. */
|
|
21
|
+
builtin?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare class Renderer {
|
|
24
|
+
/** The fixed strings this renderer writes. */
|
|
25
|
+
readonly table: TextTable;
|
|
26
|
+
private readonly kinds;
|
|
27
|
+
constructor(options?: RendererOptions);
|
|
28
|
+
/** Register a plugin's kind. Its mapping is declarative, like a built-in one. */
|
|
29
|
+
register(spec: BlockKindSpec): void;
|
|
30
|
+
/**
|
|
31
|
+
* A plugin's fixed strings, merged into this renderer's table. A deployment
|
|
32
|
+
* that replaces the table passes its own object, so a plugin's entries never
|
|
33
|
+
* reach another host's.
|
|
34
|
+
*/
|
|
35
|
+
extend(entries: TextTable): void;
|
|
36
|
+
/**
|
|
37
|
+
* One spec against the kind it extends: the extended kind's tag, fields,
|
|
38
|
+
* lines and data mark are kept, and this spec's are added. A field named
|
|
39
|
+
* twice keeps this spec's declaration, so a plugin may restate one.
|
|
40
|
+
*/
|
|
41
|
+
private merge;
|
|
42
|
+
/** The kinds this renderer knows, in registration order. */
|
|
43
|
+
knownKinds(): readonly string[];
|
|
44
|
+
/** One block kind's spec, when the renderer knows it. */
|
|
45
|
+
specOf(kind: string): BlockKindSpec | undefined;
|
|
46
|
+
/** Blocks to text. Top-level blocks are separated by a blank line. */
|
|
47
|
+
render(blocks: readonly Block[]): string;
|
|
48
|
+
/**
|
|
49
|
+
* Blocks nested inside a data block. The outer mark already says they are
|
|
50
|
+
* data, so a nested block is not wrapped again.
|
|
51
|
+
*/
|
|
52
|
+
private renderInside;
|
|
53
|
+
/** One block to text, in its kind's shape. */
|
|
54
|
+
renderBlock(block: Block, insideData?: boolean): string;
|
|
55
|
+
/**
|
|
56
|
+
* One fixed line: its key may name a field (`reminder.tier.{tier}`), and a
|
|
57
|
+
* line whose placeholders have no value is dropped rather than rendered
|
|
58
|
+
* half-empty. `carried` collects the field names the line already states, so
|
|
59
|
+
* the fields are not repeated after it.
|
|
60
|
+
*/
|
|
61
|
+
private lineOf;
|
|
62
|
+
/** The placeholder values one block carries, escaped and joined. */
|
|
63
|
+
private argsOf;
|
|
64
|
+
/** A kind nobody declared still renders, as data, so nothing is lost. */
|
|
65
|
+
private fallbackLines;
|
|
66
|
+
}
|