soulbridge 0.0.26 → 0.0.98

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.
Files changed (44) hide show
  1. package/LICENSE +202 -202
  2. package/dist/packages/cli/src/cli.js +0 -0
  3. package/dist/packages/core/src/config/dimensions.d.ts +18 -0
  4. package/dist/packages/core/src/config/dimensions.js +33 -0
  5. package/dist/packages/core/src/config/domains.d.ts +70 -0
  6. package/dist/packages/core/src/config/domains.js +70 -0
  7. package/dist/packages/core/src/config/labels.d.ts +22 -0
  8. package/dist/packages/core/src/config/labels.js +21 -0
  9. package/dist/packages/core/src/config/status.d.ts +51 -0
  10. package/dist/packages/core/src/config/status.js +47 -0
  11. package/dist/packages/core/src/consolidate.d.ts +170 -0
  12. package/dist/packages/core/src/consolidate.js +311 -0
  13. package/dist/packages/core/src/context.d.ts +2 -0
  14. package/dist/packages/core/src/context.js +48 -17
  15. package/dist/packages/core/src/dimension-text.d.ts +6 -0
  16. package/dist/packages/core/src/dimension-text.js +24 -0
  17. package/dist/packages/core/src/dimensions.d.ts +28 -0
  18. package/dist/packages/core/src/dimensions.js +53 -0
  19. package/dist/packages/core/src/extract.d.ts +140 -0
  20. package/dist/packages/core/src/extract.js +611 -0
  21. package/dist/packages/core/src/index.d.ts +362 -10
  22. package/dist/packages/core/src/index.js +983 -127
  23. package/dist/packages/core/src/inject-text.d.ts +16 -0
  24. package/dist/packages/core/src/inject-text.js +24 -0
  25. package/dist/packages/core/src/profile-prompt.d.ts +18 -0
  26. package/dist/packages/core/src/profile-prompt.js +103 -0
  27. package/dist/packages/core/src/prompts.d.ts +122 -0
  28. package/dist/packages/core/src/prompts.generated.d.ts +9 -0
  29. package/dist/packages/core/src/prompts.generated.js +9 -0
  30. package/dist/packages/core/src/prompts.js +293 -0
  31. package/dist/packages/core/src/reflect.d.ts +7 -0
  32. package/dist/packages/core/src/reflect.js +25 -27
  33. package/dist/packages/core/src/retrieval.d.ts +39 -0
  34. package/dist/packages/core/src/retrieval.js +105 -0
  35. package/dist/packages/core/src/roles.d.ts +18 -0
  36. package/dist/packages/core/src/roles.js +17 -0
  37. package/dist/packages/core/src/rules.d.ts +69 -0
  38. package/dist/packages/core/src/rules.js +152 -0
  39. package/dist/packages/core/src/sqlite.d.ts +26 -5
  40. package/dist/packages/core/src/sqlite.js +944 -33
  41. package/dist/packages/core/src/store.d.ts +92 -3
  42. package/dist/packages/core/src/types.d.ts +442 -1
  43. package/dist/packages/core/src/types.js +1 -0
  44. package/package.json +5 -3
@@ -0,0 +1,16 @@
1
+ /**
2
+ * One escape point for text that goes inside an injection block.
3
+ *
4
+ * Blocks are plain `<tag>…</tag>` envelopes, so content containing `</tag>` ends the
5
+ * block early — a pasted message, a fact statement or an entity name can forge a
6
+ * `<rules>` block, and `<rules>` is read as a requirement, not as background. The
7
+ * defence is structural, not a prompt instruction: every dynamic value passes through
8
+ * here before it is concatenated.
9
+ *
10
+ * Newlines and ordinary punctuation stay readable; only the characters that can
11
+ * change structure are replaced.
12
+ */
13
+ /** Text inside a block: `<` and `>` cannot form a tag any more. */
14
+ export declare function escapeBlockText(text: string): string;
15
+ /** Attribute value: quotes and newlines would end the attribute early. */
16
+ export declare function escapeAttr(text: string): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * One escape point for text that goes inside an injection block.
3
+ *
4
+ * Blocks are plain `<tag>…</tag>` envelopes, so content containing `</tag>` ends the
5
+ * block early — a pasted message, a fact statement or an entity name can forge a
6
+ * `<rules>` block, and `<rules>` is read as a requirement, not as background. The
7
+ * defence is structural, not a prompt instruction: every dynamic value passes through
8
+ * here before it is concatenated.
9
+ *
10
+ * Newlines and ordinary punctuation stay readable; only the characters that can
11
+ * change structure are replaced.
12
+ */
13
+ /** Text inside a block: `<` and `>` cannot form a tag any more. */
14
+ export function escapeBlockText(text) {
15
+ return String(text ?? '')
16
+ // Control characters (except tab/newline) are invisible noise in a prompt.
17
+ .replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, '')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;');
20
+ }
21
+ /** Attribute value: quotes and newlines would end the attribute early. */
22
+ export function escapeAttr(text) {
23
+ return escapeBlockText(text).replace(/"/g, '&quot;').replace(/\r?\n/g, ' ');
24
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What each dimension admits — rendered from the declaration in dimensions.ts.
3
+ *
4
+ * Two model stages ask about dimensions (the merge and the classifier) and they must
5
+ * mean the same thing. Generating the block means a new dimension appears in both
6
+ * prompts automatically instead of drifting in whichever copy was not edited.
7
+ */
8
+ export declare const PROFILE_DIMENSIONS_BLOCK: string;
9
+ export interface ProfileMergePromptInput {
10
+ items: {
11
+ id: string;
12
+ statement: string;
13
+ }[];
14
+ /** When set, this batch is already one dimension and the output must stay in it. */
15
+ dimension?: string;
16
+ }
17
+ /** Compress a pile of judgments about the person into a few that hold. */
18
+ export declare function renderProfileMergePrompt(input: ProfileMergePromptInput): string;
@@ -0,0 +1,103 @@
1
+ /* ------------------------------------------------------------------ *
2
+ * Profile merging: many judgments → a few that hold up
3
+ * ------------------------------------------------------------------ */
4
+ import { PROFILE_DIMENSIONS } from './config/dimensions.js';
5
+ import { dimensionChoices, renderDimensionDefinitions } from './dimension-text.js';
6
+ const PROFILE_ROLE = `<role>
7
+ 你在整理一组"关于这个人的判断"。它们现在又碎又重复,你要把它们合并成更少、更准的句子。
8
+ 你不添加新判断,只压缩已有的。
9
+ </role>`;
10
+ /**
11
+ * The merge operations, named.
12
+ *
13
+ * "合并一下" is not an instruction a model can follow reliably; naming the six
14
+ * operations is. The two failure modes seen in a real run — an inverted direction
15
+ * (先→后) and a fragment ("每次因为…") — are called out explicitly because a model
16
+ * that is only told "merge similar items" produces both.
17
+ */
18
+ const PROFILE_METHOD = `<method>
19
+ 合并是把"同一件事的多种说法"收成一句,不是把不同的事拼成一句。可以做的六种操作:
20
+
21
+ 1. 合并同义:两条说同一件事 → 一条,用更具体的那个说法。
22
+ 2. 提升抽象:两条是同一原则的两个例子 → 提成原则,但必须仍被这两条支持。
23
+ 3. 保留方向:原文有"先/后""要/不要""多/少""该/不该",合并后方向必须不变。
24
+ 方向反了比漏掉更糟——那是把人写反了。
25
+ 4. 拆开无关:一句里有两个不相干的点 → 拆成两条,不要用"并且"硬连。
26
+ 5. 丢弃残句:没有主语、只有半个意思的("每次…""因为…")→ 丢弃,不要补全。
27
+ 6. 排序:${PROFILE_DIMENSIONS.join(' > ')}。
28
+ </method>`;
29
+ /**
30
+ * What each dimension admits — rendered from the declaration in dimensions.ts.
31
+ *
32
+ * Two model stages ask about dimensions (the merge and the classifier) and they must
33
+ * mean the same thing. Generating the block means a new dimension appears in both
34
+ * prompts automatically instead of drifting in whichever copy was not edited.
35
+ */
36
+ export const PROFILE_DIMENSIONS_BLOCK = renderDimensionDefinitions();
37
+ const PROFILE_RULES = `<rules>
38
+ - 每条 6–32 字,一句一个判断,不编号。
39
+ - 一条只讲一件事。禁止用分号把几件事连成一条——那等于没拆。
40
+ - 不许出现"有时候/有时/通常/大部分时候"——含糊说明没有真的合并。
41
+ - 不许出现"一切/所有/总是/从不",除非来源原文里就有。
42
+ - 每行给一个维度:${dimensionChoices()}
43
+ - 一行只讲一个维度。不同维度必须分行——跨维度拼成一句就是没拆。
44
+ - 每条必须给来源:来源=fc_xxx,fc_yyy,只能引用 <input> 里出现过的 id;没有来源的整行不要。
45
+ - 合并后 5–8 条;宁少不凑。
46
+ - 输入里没有的新判断,不许出现。
47
+ </rules>`;
48
+ const PROFILE_EXAMPLES = `<examples>
49
+ <bad>做事后再验证 ← 来源是"做事之前先验证再动手",方向被写反了</bad>
50
+ <good>动手前先验证,不靠推测</good>
51
+
52
+ <bad>每次因为换工具,换模型而要重复去讲 ← 残句,不是判断</bad>
53
+ <good>讨厌为换工具、换模型重复交代</good>
54
+
55
+ <bad>有时候希望先讨论 ← "有时候"等于没合并</bad>
56
+ <good>先讨论清楚再动手</good>
57
+
58
+ <bad>设指标促成长;记忆衰减想起概率;教训按作用域衰减 ← 分号硬拼三件事</bad>
59
+ <good>拆成三条:设指标促成长 / 记忆按想起概率衰减 / 教训按作用域和复现衰减</good>
60
+
61
+ <bad>要机制并且讨厌重复并且喜欢爬山 ← 三件事硬拼</bad>
62
+ <good>拆成三条:要机制不靠自觉 / 讨厌重复交代 / 喜欢爬山</good>
63
+ </examples>`;
64
+ const PROFILE_EXAMPLE = `<example>
65
+ <input>
66
+ - fc_a | 用户认为改 prompt 不能有效让模型输出 JSON,主张用工具解析
67
+ - fc_b | 结构化输出优先工具解析/模板填充,不靠 prompt 直出 JSON
68
+ - fc_c | 用户很讨厌一件事反复讲,换工具又要重新交代
69
+ </input>
70
+ <output>
71
+ 画像: 要机制,不要靠 prompt 的自觉 | 维度=${PROFILE_DIMENSIONS[1]} | 来源=fc_a,fc_b
72
+ 画像: 讨厌重复交代,工具换了也不该重讲 | 维度=${PROFILE_DIMENSIONS[2]} | 来源=fc_c
73
+ </output>
74
+ </example>`;
75
+ /**
76
+ * One dimension per batch.
77
+ *
78
+ * Splitting the input is what actually stops cross-dimension welding; this block
79
+ * only stops the model from re-widening the batch it was handed. The parser rejects
80
+ * a line that leaves the dimension anyway, so this is the explanation, not the guard.
81
+ */
82
+ function scopeBlock(dimension) {
83
+ return `<scope>
84
+ 这一批输入已经按维度分好:全部是「${dimension}」维度的判断。
85
+ 输出每行都必须是 维度=${dimension}。不要输出别的维度,也不要把别的维度的内容并进来。
86
+ 如果某条判断其实不属于「${dimension}」,直接丢掉它,不要改写它的维度。
87
+ </scope>`;
88
+ }
89
+ /** Compress a pile of judgments about the person into a few that hold. */
90
+ export function renderProfileMergePrompt(input) {
91
+ const items = input.items.map((item) => `- ${item.id} | ${item.statement.slice(0, 120)}`);
92
+ return [
93
+ PROFILE_ROLE,
94
+ PROFILE_METHOD,
95
+ PROFILE_DIMENSIONS_BLOCK,
96
+ PROFILE_RULES,
97
+ ...(input.dimension ? [scopeBlock(input.dimension)] : []),
98
+ PROFILE_EXAMPLES,
99
+ PROFILE_EXAMPLE,
100
+ ['<input>', ...items, '</input>'].join('\n'),
101
+ '<output>',
102
+ ].join('\n\n');
103
+ }
@@ -0,0 +1,122 @@
1
+ export interface KnownEntityPrompt {
2
+ id: string;
3
+ name: string;
4
+ aliases?: string[];
5
+ relation?: string;
6
+ }
7
+ export interface KnownFactPrompt {
8
+ id: string;
9
+ statement: string;
10
+ atText?: string;
11
+ }
12
+ export interface KnownThreadPrompt {
13
+ id: string;
14
+ topic: string;
15
+ /** How many turns ago this thread was last seen. */
16
+ ago?: number;
17
+ /**
18
+ * The thread's most recent lines, oldest first.
19
+ *
20
+ * A topic label alone cannot answer "does this turn continue that conversation" —
21
+ * the model would be keyword-matching the label. The words are the evidence, so a
22
+ * candidate carries its last turns (ADR 0034).
23
+ */
24
+ recent?: Array<{
25
+ role: 'user' | 'assistant';
26
+ text: string;
27
+ }>;
28
+ }
29
+ export interface ExtractionPromptInput {
30
+ /** The turn's user text. */
31
+ text: string;
32
+ /** Date the turn happened; relative times resolve against it. */
33
+ now: number;
34
+ entities?: KnownEntityPrompt[];
35
+ facts?: KnownFactPrompt[];
36
+ previousUserText?: string;
37
+ /**
38
+ * My reply in the same turn (ADR 0035).
39
+ *
40
+ * The unit of memory is the exchange, not the user's message: without the reply, the
41
+ * prompt's own fourth category ("my promises") can never fire, and "did this turn
42
+ * decide anything" is unanswerable.
43
+ */
44
+ assistantText?: string;
45
+ /** Open topic threads: the candidate list for attribution. */
46
+ threads?: KnownThreadPrompt[];
47
+ /** Which thread the previous turn belonged to, when known. */
48
+ previousThreadId?: string;
49
+ }
50
+ /**
51
+ * The extraction prompt. Every piece comes from a named block above, so a rule
52
+ * is edited in one place and shows up in every host's extraction call.
53
+ */
54
+ export declare function renderExtractionPrompt(input: ExtractionPromptInput): string;
55
+ export interface ConsolidationPromptInput {
56
+ facts: {
57
+ id: string;
58
+ statement: string;
59
+ atText?: string;
60
+ }[];
61
+ existing: {
62
+ id: string;
63
+ statement: string;
64
+ }[];
65
+ /**
66
+ * The thread being closed: its topic and its turns in order.
67
+ *
68
+ * Facts alone lose a resume (A B A) — the turns are what make this a chapter
69
+ * instead of a batch, and the rule below tells the model to read to the end.
70
+ */
71
+ thread?: {
72
+ topic: string;
73
+ turns: string[];
74
+ };
75
+ }
76
+ export interface VerificationPromptInput {
77
+ conclusion: string;
78
+ facts: {
79
+ id: string;
80
+ statement: string;
81
+ }[];
82
+ }
83
+ /**
84
+ * Stage one: write conclusions.
85
+ *
86
+ * The model sees only facts and existing conclusions, never the conversation.
87
+ */
88
+ export declare function renderConsolidationPrompt(input: ConsolidationPromptInput): string;
89
+ /**
90
+ * Stage two: check the draft against its evidence.
91
+ *
92
+ * Deliberately does not carry the draft's reasoning: an independent check is the
93
+ * whole point (Chain-of-Verification), and a check that can see the excuse tends
94
+ * to accept it.
95
+ */
96
+ export declare function renderVerificationPrompt(input: VerificationPromptInput): string;
97
+ export interface RelationPromptInput {
98
+ conclusion: string;
99
+ existing: {
100
+ id: string;
101
+ statement: string;
102
+ evidence: number;
103
+ }[];
104
+ }
105
+ /**
106
+ * Stage three: the duplicate check.
107
+ *
108
+ * String matching can only catch the same text; a restatement like "用户要机制
109
+ * 不要 hope" versus "用户倾向方法论依据而不是 prompt 的 hope" needs meaning. The
110
+ * output is a fixed vocabulary so the caller can gate it.
111
+ */
112
+ export declare function renderRelationPrompt(input: RelationPromptInput): string;
113
+ export interface RuleClassifyPromptInput {
114
+ items: {
115
+ id: string;
116
+ statement: string;
117
+ }[];
118
+ }
119
+ /** Classify a batch of lessons into rules (with a trigger) or profile items. */
120
+ export declare function renderRuleClassifyPrompt(input: RuleClassifyPromptInput): string;
121
+ export { renderProfileMergePrompt } from './profile-prompt.ts';
122
+ export type { ProfileMergePromptInput } from './profile-prompt.ts';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * GENERATED by scripts/sync-prompts.mjs — do not edit.
3
+ * Source of truth: packages/core/prompts/*.md
4
+ */
5
+ export declare const extractionExamples = "<examples>\n<example>\n<source>\u6211\u540C\u4E8B\u5C0F\u6797\u4E0A\u5468\u88AB\u88C1\u5458\u4E86\uFF0C\u633A\u96BE\u53D7\u7684</source>\n<output>\u5B9E\u4F53: \u5C0F\u6797 | person | \u540C\u4E8B\n\u4E8B\u5B9E: \u5C0F\u6797\u5728 2026-10-24 \u90A3\u5468\u88AB\u88C1\u5458 | \u88C1\u5458 | 2026-10-24 | \u4E0A\u5468 | told | 0.8 | \u6301\u6709=world | \u7CBE\u5EA6=day | \u88AB\u88C1\u5458\u8005=@\u5C0F\u6797</output>\n</example>\n<example>\n<source>\u8FD9\u4E2A bug \u6211\u4E0A\u5468\u5C31\u4FEE\u597D\u4E86</source>\n<output>\u4E8B\u5B9E: \u7528\u6237\u8BF4\u4ED6\u5728 2026-10-24 \u90A3\u5468\u4FEE\u597D\u4E86\u8FD9\u4E2A bug | \u4FEE\u590D | 2026-10-24 | \u4E0A\u5468 | experienced | 0.7 | \u6301\u6709=world | \u7CBE\u5EA6=day | \u4FEE\u590D\u8005=\u7528\u6237</output>\n</example>\n<example>\n<source>\u597D\u7684\uFF0C\u8C22\u8C22</source>\n<output>\u65E0</output>\n</example>\n<example>\n<source>\u4F60\u5E2E\u6211\u628A\u90A3\u4E2A\u8868\u91CD\u65B0\u8DD1\u4E00\u904D</source>\n<output>\u65E0</output>\n</example>\n<example>\n<source>\u6211\u8BA8\u538C\u540C\u4E00\u4EF6\u4E8B\u53CD\u590D\u8BB2\uFF0C\u6362\u4E2A\u5DE5\u5177\u53C8\u8981\u91CD\u65B0\u8BF4\u4E00\u904D</source>\n<output>\u4E8B\u5B9E: \u7528\u6237\u8BA8\u538C\u540C\u4E00\u4EF6\u4E8B\u53CD\u590D\u8BB2\uFF0C\u6362\u5DE5\u5177\u4E5F\u4E0D\u60F3\u91CD\u590D\u4EA4\u4EE3 | \u504F\u597D | | | experienced | 0.9 | \u6301\u6709=\u7528\u6237 | \u7CBE\u5EA6=unknown | \u504F\u597D\u4E3B\u4F53=\u7528\u6237</output>\n</example>\n</examples>";
6
+ export declare const extractionRole = "<role>\n\u4F60\u662F\u8BB0\u5FC6\u62BD\u53D6\u5668\u3002\u4F60\u53EA\u4ECE\u5BF9\u8BDD\u91CC\u62BD\u53D6\u53EF\u9A8C\u8BC1\u7684\u4E8B\u5B9E\uFF0C\u4E0D\u505A\u5224\u65AD\u3001\u4E0D\u7ED9\u5EFA\u8BAE\u3001\u4E0D\u5199\u603B\u7ED3\u3002\n\u4F60\u7684\u8F93\u51FA\u7531\u4E00\u4E2A\u786E\u5B9A\u6027\u7684\u7A0B\u5E8F\u6D88\u8D39\uFF0C\u5B83\u4E0D\u4F1A\u8BFB\u4F60\u7684\u89E3\u91CA\uFF1B\u683C\u5F0F\u9519\u4E00\u4E2A\u5B57\u6BB5\uFF0C\u8FD9\u6761\u8BB0\u5FC6\u5C31\u4E22\u4E86\u3002\n</role>";
7
+ export declare const extractionRules = "<rules>\n\u672F\u8BED\u7684\u542B\u4E49\u89C1 <terms>\uFF1B\u8FD9\u91CC\u53EA\u8BF4\u64CD\u4F5C\u548C\u53D6\u820D\u3002\n- \u5408\u5E76\uFF1A\u4E0E <known-facts> \u8BED\u4E49\u76F8\u540C \u2192 noop\uFF1B\u4E92\u76F8\u77DB\u76FE \u2192 supersede \u5E76\u7ED9 target\uFF1B\u662F\u8865\u5145 \u2192 update\u3002\n \u76F8\u5173\u4F46\u72EC\u7ACB \u2192 add\u3002\u4E0D\u8981\u7528 <known-facts> \u91CC\u6CA1\u6709\u7684\u65B0\u4E8B\u5B9E\u53BB supersede\u3002\n- \u8981\u53D6\u4EE3\u65E7\u4E8B\u5B9E\u65F6\uFF0C\u5728\u4E8B\u5B9E\u884C\u5C3E\u90E8\u5199\u300C\u64CD\u4F5C=\u53D6\u4EE3 <id>\u300D\uFF08id \u53EA\u80FD\u6765\u81EA <known-facts>\uFF09\u3002\n \u4E0D\u5199\u5C31\u662F\u65B0\u589E\u3002\u4EE3\u7801\u4F1A\u5148\u786E\u8BA4\u90A3\u4E2A id \u771F\u5B9E\u5B58\u5728\uFF1B\u7F16\u9020\u7684 id \u4F1A\u9000\u5316\u6210\u65B0\u589E\u5E76\u8BB0\u539F\u56E0\u3002\n- \u69FD\u4F4D\u53EA\u653E\u53C2\u4E0E\u8005\uFF0C\u4E0D\u653E\u8BDD\u9898\uFF1A\u5B81\u53EF\u5C11\u5199\u4E00\u4E2A\u69FD\u4F4D\uFF0C\u4E5F\u4E0D\u8981\u628A\u4E00\u53E5\u8BDD\u585E\u8FDB\u69FD\u4F4D\u3002\n- \u4E0D\u786E\u5B9A\u5C31\u7701\u7565\u5B57\u6BB5\uFF0C\u7EDD\u4E0D\u7F16\u9020\u6570\u5B57\u3001\u65E5\u671F\u3001\u5173\u7CFB\u3002\n- \u4E0D\u8981\u91CD\u590D\u7528\u6237\u521A\u843D\u7684\u8BDD\uFF08\u56DE\u58F0\u4E0D\u7B97\u65B0\u4E8B\u5B9E\uFF09\u3002\n- \u6765\u6E90\u6309\u300C\u8C01\u8BF4\u7684\u300D\u5B9A\uFF08<source> \u91CC\u6807\u4E86\u53CC\u65B9\uFF09\uFF1A\u7528\u6237\u90A3\u4E00\u4FA7\u5199 told \u6216 experienced\uFF1B\n **\u6211\u90A3\u4E00\u4FA7\u5199 inferred**\u3002\u6211\u7684\u627F\u8BFA/\u51B3\u5B9A\u5199 \u7C7B\u578B\u540D=\u627F\u8BFA/\u51B3\u5B9A\uFF1B\n \u6211\u7684\u5B8C\u6210\u58F0\u660E\uFF08\"\u6211\u6539\u597D\u4E86\"\"\u5DF2\u7ECF\u4FEE\u4E86\"\uFF09\u5982\u679C\u6CA1\u6709\u5DE5\u5177\u6216\u6D4B\u8BD5\u7ED3\u679C\u4F5C\u4E3A\u4F9D\u636E\uFF0C**\u4E0D\u8981\u5199**\u3002\n \u81EA\u8FF0\uFF08\"\u6211\u662F\u5C0F\u7075\uFF0C\u4E00\u4E2A\u6301\u7EED\u7684\u4F19\u4F34\"\uFF09\u6C38\u8FDC\u4E0D\u5199\u3002\n- \u7EBF\u7D22\uFF1A\u5019\u9009\u89C1 <open-threads>\uFF0C\u6BCF\u6761\u5E26\u5B83\u6700\u8FD1\u51E0\u8F6E\u7684\u539F\u6587\u3002**\u9ED8\u8BA4\u7EE7\u7EED**\uFF1A\u8FD9\u4E00\u8F6E\u5728\u804A\u5019\u9009\u91CC\u7684\u67D0\u4E00\u6761\uFF0C\n \u5C31\u5199\u300C\u7EE7\u7EED <id>\u300D\u3002\u53EA\u6709\u51FA\u73B0\u660E\u786E\u8F6C\u6298\uFF08\"\u53E6\u5916 / \u6362\u4E2A\u8BDD\u9898 / \u8BF4\u56DE / \u4E0D\u804A\u8FD9\u4E2A\u4E86\"\uFF09\uFF0C\n \u6216\u8FD9\u4E00\u8F6E\u4E0E\u6240\u6709\u5019\u9009\u7684\u4E3B\u9898\u3001\u5B9E\u4F53\u90FD\u4E0D\u76F8\u5E72\u65F6\uFF0C\u624D\u5199\u300C\u7EBF\u7D22: \u65B0 | \u4E00\u53E5\u8BDD\u4E3B\u9898\u300D\u3002\n \u62FF\u4E0D\u51C6\u5C31\u7EE7\u7EED\u6700\u63A5\u8FD1\u7684\u90A3\u4E00\u6761\u2014\u2014\u7EBF\u7D22\u788E\u6389\u4F1A\u8BA9\u540E\u9762\u7684\u5F52\u7EB3\u65E0\u6CD5\u8FDB\u884C\uFF0C\u63A5\u9519\u53EF\u4EE5\u4E8B\u540E\u62C6\u5F00\u3002\n \u7EE7\u7EED\u65F6\uFF0C\u5982\u679C\u8FD9\u4E00\u8F6E\u5DF2\u7ECF\u4E0D\u662F\u5728\u804A\u5019\u9009\u539F\u6765\u7684\u4E3B\u9898\uFF08\u8BDD\u9898\u5728\u540C\u4E00\u7EBF\u7D22\u5185\u5F80\u524D\u8D70\u4E86\uFF09\uFF0C\n \u8865\u4E00\u53E5\u300C| \u4E3B\u9898=\u73B0\u5728\u804A\u7684\u662F\u4EC0\u4E48\u300D\u2014\u2014\u7EBF\u7D22\u7684\u4E3B\u9898\u662F\u7ED9\u540E\u7EED\u8BFB\u7684\u4EBA\u770B\u7684\uFF0C\u4E0D\u66F4\u65B0\u5C31\u4F1A\u4E00\u76F4\u505C\u5728\u7B2C\u4E00\u8F6E\u7684\u8BF4\u6CD5\u3002\n \u6CA1\u6709\u4EFB\u4F55\u5019\u9009\u65F6\u8FD9\u4E00\u884C\u53EA\u53EF\u80FD\u662F\"\u65B0\"\u3002\n</rules>\n\n<when-to-write>\n\u53EA\u5728\u8FD9\u56DB\u79CD\u60C5\u51B5\u4E0B\u5199\u4E8B\u5B9E\uFF1A\n1. \u67D0\u4E2A\u4EBA\u6216\u67D0\u4EF6\u4E8B\u7684\u72B6\u6001\u3001\u7ECF\u5386\uFF08\u201C\u5C0F\u6797\u6362\u4E86\u5DE5\u4F5C\u201D\u201C\u90A3\u4E2A\u9879\u76EE\u9EC4\u4E86\u201D\uFF09\n2. \u4E00\u4E2A\u51B3\u5B9A\uFF0C\u4EE5\u53CA\u505A\u8FD9\u4E2A\u51B3\u5B9A\u7684\u7406\u7531\n3. \u7528\u6237\u8868\u8FBE\u7684\u504F\u597D\u3001\u539F\u5219\u6216\u4E60\u60EF\uFF08\u201C\u6211\u8BA8\u538C\u53CD\u590D\u8BB2\u540C\u4E00\u4EF6\u4E8B\u201D\uFF09\n4. \u6211\u7ED9\u51FA\u7684\u627F\u8BFA\uFF08\u201C\u6211\u660E\u5929\u628A\u8868\u8865\u4E0A\u201D\uFF09\n\n**\u4E0D\u8981\u5199**\uFF08\u8FD9\u662F\u6700\u5E38\u89C1\u7684\u566A\u58F0\uFF09\uFF1A\n- \u7528\u6237\u5BF9\u6211\u7684\u8981\u6C42\u3001\u63D0\u95EE\u3001\u50AC\u4FC3\u3001\u540C\u610F\u3001\u5426\u5B9A\u2014\u2014\u8FD9\u4E9B\u662F\u5BF9\u8BDD\u52A8\u4F5C\uFF0C\u5BF9\u8BDD\u539F\u6587\u5DF2\u7ECF\u4FDD\u5B58\uFF0C\u518D\u5199\u4E00\u4EFD\u53EA\u4F1A\u53D8\u6210\u566A\u58F0\n- \u6211\u95EE\u7528\u6237\u7684\u6F84\u6E05\u95EE\u9898\n- \u5BD2\u6684\u3001\u6211\u7684\u81EA\u6211\u4ECB\u7ECD\u3001\u6211\u5BF9\u80FD\u529B\u7684\u8BF4\u660E\n- \u7528\u6237\u6B63\u5728\u95EE\u6211\u7684\u4E8B\uFF0C\u4E0D\u662F\u201C\u5173\u4E8E\u7528\u6237\u7684\u4E8B\u5B9E\u201D\n</when-to-write>\n\n<gaps>\n\u53EA\u6709\u4E24\u79CD\u60C5\u51B5\u5EFA\u7F3A\u53E3\uFF1A\n1. \u7528\u6237\u8BF4\u81EA\u5DF1\u4E0D\u77E5\u9053\u3001\u6CA1\u6D88\u606F\u3001\u60F3\u77E5\u9053\u67D0\u4EF6\u4E8B\uFF08\u201C\u5979\u540E\u6765\u600E\u4E48\u6837\u4E86\u201D\u201C\u4E0D\u77E5\u9053\u8FD8\u5728\u4E0D\u5728\u201D\uFF09\n2. \u6211\u8BF4\u8FC7\u8981\u505A\u3001\u4F46\u72B6\u6001\u4ECD\u672A\u5B8C\u6210\u7684\u4E8B\n\n\u7528\u6237\u53EA\u662F\u5728\u95EE\u6211\u4E00\u4E2A\u95EE\u9898\u65F6**\u4E0D\u8981**\u5EFA\u7F3A\u53E3\u2014\u2014\u90A3\u662F\u4ED6\u5728\u63D0\u95EE\uFF0C\u4E0D\u662F\u4ED6\u7F3A\u4FE1\u606F\u3002\n</gaps>";
8
+ export declare const extractionSchema = "<output-format>\n\u4E0D\u8981\u8F93\u51FA JSON\u3002\u6309\u4E0B\u9762\u7684\u8868\u683C\u4E00\u884C\u4E00\u6761\u586B\u5199\uFF0C\u5B57\u6BB5\u4E4B\u95F4\u7528 | \u5206\u9694\uFF0C\u6CA1\u6709\u5185\u5BB9\u7684\u5B57\u6BB5\u7559\u7A7A\u3002\n\u53EF\u4EE5\u53EA\u5199\u5176\u4E2D\u51E0\u884C\uFF1B\u4EC0\u4E48\u90FD\u8BB0\u4E0D\u4F4F\u5C31\u53EA\u5199\uFF1A\u65E0\n\n\u5B9E\u4F53: \u540D\u5B57 | \u7C7B\u578B | \u5173\u7CFB\n\u4E8B\u5B9E: \u9648\u8FF0 | \u7C7B\u578B\u540D | \u65E5\u671F(YYYY-MM-DD) | \u65F6\u95F4\u539F\u6587 | \u6765\u6E90 | \u7F6E\u4FE1(0-1) | \u6301\u6709=world\u6216\u4EBA\u540D | \u7CBE\u5EA6=day/range/relative/unknown | \u539F\u5219\u5019\u9009=\u662F | \u69FD\u4F4D | \u64CD\u4F5C=\u53D6\u4EE3 <id>\n\u7F3A\u53E3: \u4E3B\u9898 | \u5F20\u529B(0-1) | \u76F8\u5173\u5B9E\u4F53\n\u7EBF\u7D22: \u7EE7\u7EED <id> \u6216 \u7EBF\u7D22: \u65B0 | \u4E00\u53E5\u8BDD\u4E3B\u9898\n \u8BDD\u9898\u5728\u540C\u4E00\u7EBF\u7D22\u5185\u53D8\u4E86\uFF0C\u5C31\u5199\uFF1A\u7EBF\u7D22: \u7EE7\u7EED <id> | \u4E3B\u9898=\u65B0\u7684\u4E00\u53E5\u8BDD\u4E3B\u9898\n\n\u6BCF\u4E2A\u5B57\u6BB5\u7684\u8FB9\u754C\u89C1 <terms>\uFF0C\u8FD9\u91CC\u53EA\u8BF4\u683C\u5F0F\uFF1A\u65E5\u671F\u5FC5\u987B\u662F YYYY-MM-DD\uFF1B\u69FD\u4F4D\u5199\u6210 \u540D\u79F0=\u503C\uFF0C\u591A\u4E2A\u7528 ; \u9694\u5F00\uFF1B\n\"\u6301\u6709/\u7CBE\u5EA6/\u539F\u5219\u5019\u9009\"\u662F\u952E\u503C\u5BF9\uFF0C\u5199\u5728\u4E8B\u5B9E\u884C\u7684\u5C3E\u90E8\uFF1B\u7EBF\u7D22\u884C\u4E8C\u9009\u4E00\uFF08\u7EE7\u7EED\u65F6\u53EF\u9644 \u4E3B\u9898=\uFF09\u3002\n\u793A\u4F8B\uFF1A\n\u5B9E\u4F53: \u5C0F\u6797 | person | \u540C\u4E8B\n\u4E8B\u5B9E: \u5C0F\u6797\u5728 2026-10-24 \u90A3\u5468\u88AB\u88C1\u5458 | \u88C1\u5458 | 2026-10-24 | \u4E0A\u5468 | told | 0.8 | \u6301\u6709=world | \u7CBE\u5EA6=day | \u88AB\u88C1\u5458\u8005=@\u5C0F\u6797\n\u4E8B\u5B9E: \u7528\u6237\u8981\u6C42\u6539\u52A8\u524D\u5148\u8BF4\u660E\u4E3A\u4EC0\u4E48\u3001\u600E\u4E48\u505A\u3001\u6709\u4EC0\u4E48\u5F71\u54CD | \u8981\u6C42 | | | experienced | 0.9 | \u6301\u6709=\u7528\u6237 | \u7CBE\u5EA6=unknown | \u539F\u5219\u5019\u9009=\u662F | \u5BF9\u8C61=@\u7528\u6237\n\u7F3A\u53E3: \u5C0F\u6797\u540E\u6765\u6709\u7740\u843D\u5417 | 0.6 | \u5C0F\u6797\n\u7EBF\u7D22: \u7EE7\u7EED th_ab12\n</output-format>";
9
+ export declare const extractionTerms = "<terms>\n\u6BCF\u4E2A\u672F\u8BED\u7684\u542B\u4E49\u548C\u8FB9\u754C\u3002\u5199\u4E4B\u524D\u5148\u5224\u65AD\u5B83\u5C5E\u4E8E\u54EA\u4E2A\u672F\u8BED\u2014\u2014\u653E\u9519\u4F4D\u7F6E\u6BD4\u6F0F\u5199\u66F4\u7CDF\u3002\n\n**\u5B9E\u4F53**\uFF1A\u4E00\u4E2A\u5177\u4F53\u7684\u4EBA\u3001\u7269\u3001\u5730\u70B9\u6216\u5E73\u53F0\u3002\u5B83\u662F\"\u8C01/\u4EC0\u4E48\"\uFF0C\u4E0D\u662F\"\u505A\u4E86\u4EC0\u4E48\"\u3002\n \u7B97\uFF1A\u5C0F\u6797\u3001\u7528\u6237\u3001TypeScript\u3001\u6296\u97F3\u3001PowerShell\u3001config \u6587\u4EF6\u5939\n \u4E0D\u7B97\uFF1A\u4E0D\u5220\u9664\u539F\u59CB\u5BF9\u8BDD\u3001\u4ECE\u6839\u4E0A\u89E3\u51B3\u3001\u5FC5\u8981\u3001\u566A\u58F0\u8BB0\u5FC6\u3001\u60C5\u7EEA\u6CE8\u5165\u3001\u9879\u76EE\u8FDB\u5C55\n \u8FB9\u754C\uFF1A\u80FD\u7528\"\u8FD9\u8BF4\u7684\u662F\u8C01\"\u95EE\u51FA\u6765\u7684\u624D\u7B97\u3002\u4E00\u53E5\u8BDD\u3001\u4E00\u4E2A\u89C2\u70B9\u3001\u4E00\u4E2A\u505A\u6CD5\u90FD\u4E0D\u662F\u5B9E\u4F53\u3002\n \u5224\u636E\uFF1A\u8D85\u8FC7 12 \u4E2A\u5B57\uFF0C\u6216\u542B\"\u7684/\u548C/\u4E0E\"\uFF0C\u6216\u80FD\u52A0\u4E0A\"\u8FD9\u4EF6\u4E8B\"\u7684\uFF0C\u57FA\u672C\u90FD\u4E0D\u662F\u5B9E\u4F53\u3002\n \u540C\u4E00\u4E2A\u4EBA\u6216\u7269\u53EA\u51FA\u73B0\u4E00\u6B21\uFF1Btype \u53EA\u80FD\u662F person / thing / place / self\u3002\n\n**\u4E8B\u5B9E**\uFF1A\u4E00\u53E5\u81EA\u5305\u542B\u7684\u9648\u8FF0\uFF0C\u56DE\u7B54\"\u53D1\u751F\u4E86\u4EC0\u4E48\"\u6216\"\u67D0\u4EBA\u662F\u4EC0\u4E48\u6837\"\u3002\n \u7B97\uFF1A\u5C0F\u6797\u5728 2026-10-24 \u90A3\u5468\u88AB\u88C1\u5458\uFF1B\u7528\u6237\u8981\u6C42\u6539\u52A8\u524D\u5148\u8BF4\u660E\u5F71\u54CD\n \u4E0D\u7B97\uFF1A\u7528\u6237\u7684\u63D0\u95EE\u3001\u50AC\u4FC3\u3001\u540C\u610F\u3001\u5426\u5B9A\uFF1B\u5BD2\u6684\uFF1B\u6211\u5BF9\u81EA\u5DF1\u80FD\u529B\u7684\u8BF4\u660E\n \u8FB9\u754C\uFF1A\u628A\u4EE3\u8BCD\u6362\u6210\u540D\u5B57\u540E\u4ECD\u7136\u6210\u7ACB\uFF0C\u4E14\u6362\u4E2A\u4EBA\u590D\u8FF0\u4E0D\u9700\u8981\u52A0\u95EE\u53F7\u300215\u201380 \u5B57\uFF0C\u4FDD\u7559\u6570\u5B57\u548C\u4E13\u6709\u540D\u8BCD\u3002\n\n**\u6301\u6709**\uFF1A\u8FD9\u6761\u4E8B\u5B9E\u5728\u8C01\u7684\u89C6\u89D2\u91CC\u6210\u7ACB\u3002\u53EA\u6709\u4E24\u4E2A\u5408\u6CD5\u503C\u3002\n world\uFF1A\u4E16\u754C\u4E0A\u53D1\u751F\u7684\u4E8B\uFF0C\u7B2C\u4E09\u4E2A\u4EBA\u4E5F\u80FD\u6838\u5B9E\uFF08\u6362\u5DE5\u4F5C\u3001\u5403\u996D\u3001\u4E0B\u96E8\u3001\u7EA6\u5B9A\u6697\u53F7\uFF09\n \u4EBA\u540D\uFF1A\u67D0\u4EBA\u7684\u770B\u6CD5\u3001\u504F\u597D\u3001\u8981\u6C42\u3001\u62C5\u5FC3\uFF08\"\u7528\u6237\u8BA4\u4E3A\u2026\"\uFF09\uFF0C\u53EA\u6709\u90A3\u4E2A\u4EBA\u80FD\u786E\u8BA4\n \u8FB9\u754C\uFF1A\u80FD\u5199\u6210\u65B0\u95FB\u62A5\u9053\u7684 \u2192 world\uFF1B\u53EA\u80FD\u5199\u6210\"X \u8BA4\u4E3A\"\u7684 \u2192 \u4EBA\u540D\u3002\u4EBA\u7269\u5C42\u4E0D\u5199\u65E5\u671F\u3002\n\n**\u69FD\u4F4D**\uFF1A\u8FD9\u6761\u4E8B\u5B9E\u7684\u53C2\u4E0E\u8005\u4E0E\u53C2\u6570\uFF0C\u5199\u6210 \u540D\u79F0=\u503C\uFF0C\u591A\u4E2A\u7528 ; \u9694\u5F00\u3002\n \u53EA\u653E\"\u8C01\u3001\u5BF9\u8C01\u3001\u5728\u54EA\u91CC\u3001\u591A\u5C11\"\u8FD9\u7C7B\u7ED3\u6784\u4F4D\u7F6E\u3002\n \u7B97\uFF1A\u88AB\u88C1\u5458\u8005=@\u5C0F\u6797\uFF1B\u4EBA\u6570=300\uFF1B\u5730\u70B9=\u676D\u5DDE\n \u4E0D\u7B97\uFF1A\u4E8B\u9879=\u4E0D\u5220\u9664\u539F\u59CB\u5BF9\u8BDD\uFF1B\u505A\u6CD5=\u4ECE\u6839\u4E0A\u89E3\u51B3\uFF1B\u7406\u7531=\u5FC5\u8981\n \u8FB9\u754C\uFF1A\u503C\u8981\u4EE5\u5B9E\u4F53\u8EAB\u4EFD\u53C2\u4E0E\u65F6\uFF0C\u5199\u6210 **@\u540D\u5B57**\uFF1B\u4E0D\u5199 @ \u7684\u4E00\u5F8B\u5F53\u6587\u5B57\u3002\n @\u540D\u5B57 \u5FC5\u987B\u5728\u672C\u8F6E\u5B9E\u4F53\u884C\u58F0\u660E\uFF08\u6216\u5DF2\u5728 <known-entities> \u91CC\uFF09\uFF0C\u5426\u5219\u4ECD\u6309\u6587\u5B57\u4FDD\u5B58\u3001\u4E0D\u5EFA\u5B9E\u4F53\u3002\n\n**\u65E5\u671F\u4E0E\u7CBE\u5EA6**\uFF1A\u5148\u6309 <observation-date> \u628A\"\u6628\u5929/\u4E0A\u5468/\u4E0A\u4E2A\u6708\"\u6362\u7B97\u6210 YYYY-MM-DD \u586B\u5165\u65E5\u671F\u3002\n \u7CBE\u5EA6\u56DB\u503C\uFF1Aday \u786E\u5207\u65E5\u671F / range \u53EA\u77E5\u9053\u4E00\u5468\u4E00\u6708 / relative \u53EA\u6709\u76F8\u5BF9\u8BF4\u6CD5\uFF08\u4E0A\u6B21\u3001\u6700\u8FD1\u3001\u521D\u6B21\u89C1\u9762\uFF09/ unknown \u4E0D\u77E5\u9053\u3002\n \u6362\u7B97\u4E0D\u51FA\u6765\u5C31\u7559\u7A7A\u65E5\u671F\uFF0C\u628A\u539F\u8BDD\u5199\u8FDB\"\u65F6\u95F4\u539F\u6587\"\u3002**\u6CA1\u6709\u65F6\u95F4\u5C31\u5199 unknown\uFF0C\u7EDD\u4E0D\u7F16\u65E5\u671F\u3002**\n\n**\u6765\u6E90**\uFF1Aexperienced \u4F60\u4EB2\u5386 / told \u542C\u522B\u4EBA\u8BF4 / inferred \u4F60\u63A8\u65AD\u3002\n**\u7C7B\u578B\u540D**\uFF1A\u4E00\u53E5\u81EA\u7531\u63CF\u8FF0\uFF08\u504F\u597D\u3001\u8981\u6C42\u3001\u6280\u672F\u7EA6\u675F\u3001\u4E8B\u4EF6\uFF09\u3002\u5B83\u4E0D\u662F\u5206\u7C7B\u4F53\u7CFB\uFF0C\u5199\u6700\u8D34\u5207\u7684\u8BCD\u5373\u53EF\uFF0C\u4E0D\u7528\u9020\u5206\u7C7B\u3002\n**\u7F6E\u4FE1**\uFF1A0\u20131\u3002\u62FF\u4E0D\u51C6\u5199\u4F4E\uFF0C\u4E0D\u8981\u7A7A\u7740\u2014\u2014\u7A7A\u7740\u4F1A\u88AB\u5F53\u6210\"\u6CA1\u628A\u63E1\"\u3002\n\n**\u539F\u5219\u5019\u9009**\uFF1A\u8FD9\u6761\u4E8B\u5B9E\u662F\u4E0D\u662F\"\u4EE5\u540E\u8BE5\u600E\u4E48\u505A\u4E8B\u7684\u7ECF\u9A8C\"\uFF1F\u5224\u636E\u662F\u4F1A\u4E0D\u4F1A\u5F71\u54CD\u4EE5\u540E\u7684\u505A\u6CD5\u3002\n \u7B97\uFF1A\u6539\u52A8\u524D\u5148\u8BF4\u660E\u5F71\u54CD\uFF1B\u7528\u6237\u8BA8\u538C\u53CD\u590D\u4EA4\u4EE3\n \u4E0D\u7B97\uFF1A\u4ECA\u5929\u6539\u4E86 A \u6587\u4EF6\uFF1B\u5C0F\u6797\u88AB\u88C1\u5458\n \u62FF\u4E0D\u51C6\u5C31\u7701\u7565\u2014\u2014\u4E00\u6761\u4E0D\u8BE5\u5347\u683C\u7684\u7ECF\u9A8C\u6BD4\u6F0F\u6389\u66F4\u7CDF\u3002\n\n**\u7F3A\u53E3**\uFF1A\u63D0\u8FC7\u4F46\u6CA1\u6709\u7B54\u6848\u7684\u4E8B\uFF08\"\u5979\u540E\u6765\u600E\u4E48\u6837\u4E86\"\uFF09\u3002\n**\u7EBF\u7D22**\uFF1A\u8FD9\u6BB5\u5BF9\u8BDD\u5728\u63A5\u7740\u804A\u54EA\u4E2A\u8BDD\u9898\uFF0C\u89C1 <open-threads>\u3002\n</terms>";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * GENERATED by scripts/sync-prompts.mjs — do not edit.
3
+ * Source of truth: packages/core/prompts/*.md
4
+ */
5
+ export const extractionExamples = "<examples>\n<example>\n<source>我同事小林上周被裁员了,挺难受的</source>\n<output>实体: 小林 | person | 同事\n事实: 小林在 2026-10-24 那周被裁员 | 裁员 | 2026-10-24 | 上周 | told | 0.8 | 持有=world | 精度=day | 被裁员者=@小林</output>\n</example>\n<example>\n<source>这个 bug 我上周就修好了</source>\n<output>事实: 用户说他在 2026-10-24 那周修好了这个 bug | 修复 | 2026-10-24 | 上周 | experienced | 0.7 | 持有=world | 精度=day | 修复者=用户</output>\n</example>\n<example>\n<source>好的,谢谢</source>\n<output>无</output>\n</example>\n<example>\n<source>你帮我把那个表重新跑一遍</source>\n<output>无</output>\n</example>\n<example>\n<source>我讨厌同一件事反复讲,换个工具又要重新说一遍</source>\n<output>事实: 用户讨厌同一件事反复讲,换工具也不想重复交代 | 偏好 | | | experienced | 0.9 | 持有=用户 | 精度=unknown | 偏好主体=用户</output>\n</example>\n</examples>";
6
+ export const extractionRole = "<role>\n你是记忆抽取器。你只从对话里抽取可验证的事实,不做判断、不给建议、不写总结。\n你的输出由一个确定性的程序消费,它不会读你的解释;格式错一个字段,这条记忆就丢了。\n</role>";
7
+ export const extractionRules = "<rules>\n术语的含义见 <terms>;这里只说操作和取舍。\n- 合并:与 <known-facts> 语义相同 → noop;互相矛盾 → supersede 并给 target;是补充 → update。\n 相关但独立 → add。不要用 <known-facts> 里没有的新事实去 supersede。\n- 要取代旧事实时,在事实行尾部写「操作=取代 <id>」(id 只能来自 <known-facts>)。\n 不写就是新增。代码会先确认那个 id 真实存在;编造的 id 会退化成新增并记原因。\n- 槽位只放参与者,不放话题:宁可少写一个槽位,也不要把一句话塞进槽位。\n- 不确定就省略字段,绝不编造数字、日期、关系。\n- 不要重复用户刚落的话(回声不算新事实)。\n- 来源按「谁说的」定(<source> 里标了双方):用户那一侧写 told 或 experienced;\n **我那一侧写 inferred**。我的承诺/决定写 类型名=承诺/决定;\n 我的完成声明(\"我改好了\"\"已经修了\")如果没有工具或测试结果作为依据,**不要写**。\n 自述(\"我是小灵,一个持续的伙伴\")永远不写。\n- 线索:候选见 <open-threads>,每条带它最近几轮的原文。**默认继续**:这一轮在聊候选里的某一条,\n 就写「继续 <id>」。只有出现明确转折(\"另外 / 换个话题 / 说回 / 不聊这个了\"),\n 或这一轮与所有候选的主题、实体都不相干时,才写「线索: 新 | 一句话主题」。\n 拿不准就继续最接近的那一条——线索碎掉会让后面的归纳无法进行,接错可以事后拆开。\n 继续时,如果这一轮已经不是在聊候选原来的主题(话题在同一线索内往前走了),\n 补一句「| 主题=现在聊的是什么」——线索的主题是给后续读的人看的,不更新就会一直停在第一轮的说法。\n 没有任何候选时这一行只可能是\"新\"。\n</rules>\n\n<when-to-write>\n只在这四种情况下写事实:\n1. 某个人或某件事的状态、经历(“小林换了工作”“那个项目黄了”)\n2. 一个决定,以及做这个决定的理由\n3. 用户表达的偏好、原则或习惯(“我讨厌反复讲同一件事”)\n4. 我给出的承诺(“我明天把表补上”)\n\n**不要写**(这是最常见的噪声):\n- 用户对我的要求、提问、催促、同意、否定——这些是对话动作,对话原文已经保存,再写一份只会变成噪声\n- 我问用户的澄清问题\n- 寒暄、我的自我介绍、我对能力的说明\n- 用户正在问我的事,不是“关于用户的事实”\n</when-to-write>\n\n<gaps>\n只有两种情况建缺口:\n1. 用户说自己不知道、没消息、想知道某件事(“她后来怎么样了”“不知道还在不在”)\n2. 我说过要做、但状态仍未完成的事\n\n用户只是在问我一个问题时**不要**建缺口——那是他在提问,不是他缺信息。\n</gaps>";
8
+ export const extractionSchema = "<output-format>\n不要输出 JSON。按下面的表格一行一条填写,字段之间用 | 分隔,没有内容的字段留空。\n可以只写其中几行;什么都记不住就只写:无\n\n实体: 名字 | 类型 | 关系\n事实: 陈述 | 类型名 | 日期(YYYY-MM-DD) | 时间原文 | 来源 | 置信(0-1) | 持有=world或人名 | 精度=day/range/relative/unknown | 原则候选=是 | 槽位 | 操作=取代 <id>\n缺口: 主题 | 张力(0-1) | 相关实体\n线索: 继续 <id> 或 线索: 新 | 一句话主题\n 话题在同一线索内变了,就写:线索: 继续 <id> | 主题=新的一句话主题\n\n每个字段的边界见 <terms>,这里只说格式:日期必须是 YYYY-MM-DD;槽位写成 名称=值,多个用 ; 隔开;\n\"持有/精度/原则候选\"是键值对,写在事实行的尾部;线索行二选一(继续时可附 主题=)。\n示例:\n实体: 小林 | person | 同事\n事实: 小林在 2026-10-24 那周被裁员 | 裁员 | 2026-10-24 | 上周 | told | 0.8 | 持有=world | 精度=day | 被裁员者=@小林\n事实: 用户要求改动前先说明为什么、怎么做、有什么影响 | 要求 | | | experienced | 0.9 | 持有=用户 | 精度=unknown | 原则候选=是 | 对象=@用户\n缺口: 小林后来有着落吗 | 0.6 | 小林\n线索: 继续 th_ab12\n</output-format>";
9
+ export const extractionTerms = "<terms>\n每个术语的含义和边界。写之前先判断它属于哪个术语——放错位置比漏写更糟。\n\n**实体**:一个具体的人、物、地点或平台。它是\"谁/什么\",不是\"做了什么\"。\n 算:小林、用户、TypeScript、抖音、PowerShell、config 文件夹\n 不算:不删除原始对话、从根上解决、必要、噪声记忆、情绪注入、项目进展\n 边界:能用\"这说的是谁\"问出来的才算。一句话、一个观点、一个做法都不是实体。\n 判据:超过 12 个字,或含\"的/和/与\",或能加上\"这件事\"的,基本都不是实体。\n 同一个人或物只出现一次;type 只能是 person / thing / place / self。\n\n**事实**:一句自包含的陈述,回答\"发生了什么\"或\"某人是什么样\"。\n 算:小林在 2026-10-24 那周被裁员;用户要求改动前先说明影响\n 不算:用户的提问、催促、同意、否定;寒暄;我对自己能力的说明\n 边界:把代词换成名字后仍然成立,且换个人复述不需要加问号。15–80 字,保留数字和专有名词。\n\n**持有**:这条事实在谁的视角里成立。只有两个合法值。\n world:世界上发生的事,第三个人也能核实(换工作、吃饭、下雨、约定暗号)\n 人名:某人的看法、偏好、要求、担心(\"用户认为…\"),只有那个人能确认\n 边界:能写成新闻报道的 → world;只能写成\"X 认为\"的 → 人名。人物层不写日期。\n\n**槽位**:这条事实的参与者与参数,写成 名称=值,多个用 ; 隔开。\n 只放\"谁、对谁、在哪里、多少\"这类结构位置。\n 算:被裁员者=@小林;人数=300;地点=杭州\n 不算:事项=不删除原始对话;做法=从根上解决;理由=必要\n 边界:值要以实体身份参与时,写成 **@名字**;不写 @ 的一律当文字。\n @名字 必须在本轮实体行声明(或已在 <known-entities> 里),否则仍按文字保存、不建实体。\n\n**日期与精度**:先按 <observation-date> 把\"昨天/上周/上个月\"换算成 YYYY-MM-DD 填入日期。\n 精度四值:day 确切日期 / range 只知道一周一月 / relative 只有相对说法(上次、最近、初次见面)/ unknown 不知道。\n 换算不出来就留空日期,把原话写进\"时间原文\"。**没有时间就写 unknown,绝不编日期。**\n\n**来源**:experienced 你亲历 / told 听别人说 / inferred 你推断。\n**类型名**:一句自由描述(偏好、要求、技术约束、事件)。它不是分类体系,写最贴切的词即可,不用造分类。\n**置信**:0–1。拿不准写低,不要空着——空着会被当成\"没把握\"。\n\n**原则候选**:这条事实是不是\"以后该怎么做事的经验\"?判据是会不会影响以后的做法。\n 算:改动前先说明影响;用户讨厌反复交代\n 不算:今天改了 A 文件;小林被裁员\n 拿不准就省略——一条不该升格的经验比漏掉更糟。\n\n**缺口**:提过但没有答案的事(\"她后来怎么样了\")。\n**线索**:这段对话在接着聊哪个话题,见 <open-threads>。\n</terms>";
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Every prompt lives here, once.
3
+ *
4
+ * Prompts are product surface: they decide what the soul remembers, so they are
5
+ * versioned with the code and rendered from structured inputs rather than
6
+ * concatenated at the call site. One definition, many renderers.
7
+ *
8
+ * Convention (from the extraction-prompt patterns review):
9
+ * - narrow the role, and say who consumes the output
10
+ * - schema in the prompt, with the allowed values spelled out
11
+ * - "omit if uncertain" rather than a bare "do not invent"
12
+ * - few-shot examples, cleanest last
13
+ * - wrap the data, never inline it next to the instructions
14
+ * - end with the output anchor
15
+ */
16
+ import { extractionRole, extractionTerms, extractionSchema, extractionRules, extractionExamples } from './prompts.generated.js';
17
+ import { PROFILE_DIMENSIONS_BLOCK } from './profile-prompt.js';
18
+ import { dimensionChoices } from './dimension-text.js';
19
+ /**
20
+ * Every term, defined once, with the boundary that decides it.
21
+ *
22
+ * Terms used to be explained in three places (format notes, rules, examples) and the
23
+ * copies disagreed: a slot value was "a name" in one paragraph and "a description" in
24
+ * another, so the model filled slots with topics and the parser called them entities.
25
+ * One definition per term, each with 算/不算 examples, is the only version a model can
26
+ * apply consistently.
27
+ */
28
+ function isoDate(at) {
29
+ const d = new Date(at);
30
+ const month = String(d.getMonth() + 1).padStart(2, '0');
31
+ const day = String(d.getDate()).padStart(2, '0');
32
+ return `${d.getFullYear()}-${month}-${day}`;
33
+ }
34
+ function renderKnownEntities(entities) {
35
+ if (entities.length === 0)
36
+ return '<known-entities>(空)</known-entities>';
37
+ const lines = entities.slice(0, 30).map((e) => {
38
+ const aliases = e.aliases?.length ? `(别名:${e.aliases.join('、')})` : '';
39
+ const relation = e.relation ? `,${e.relation}` : '';
40
+ return ` - ${e.name}${aliases}${relation}`;
41
+ });
42
+ return ['<known-entities>', ...lines, '</known-entities>'].join('\n');
43
+ }
44
+ function renderKnownFacts(facts) {
45
+ if (facts.length === 0)
46
+ return '<known-facts>(空)</known-facts>';
47
+ const lines = facts.slice(0, 20).map((f) => ` - [${f.id}] ${f.statement}${f.atText ? `(${f.atText})` : ''}`);
48
+ return ['<known-facts>', ...lines, '</known-facts>'].join('\n');
49
+ }
50
+ /**
51
+ * The extraction prompt. Every piece comes from a named block above, so a rule
52
+ * is edited in one place and shows up in every host's extraction call.
53
+ */
54
+ export function renderExtractionPrompt(input) {
55
+ const threads = renderOpenThreads(input.threads ?? [], input.previousThreadId);
56
+ const context = [
57
+ '<context>',
58
+ ` <observation-date>${isoDate(input.now)}</observation-date>`,
59
+ renderKnownEntities(input.entities ?? []),
60
+ renderKnownFacts(input.facts ?? []),
61
+ threads,
62
+ // The previous line is evidence for binding "继续/那个/她". Cutting it at an
63
+ // arbitrary length can remove the very referent the model needs, so it goes in
64
+ // whole; its size is bounded by the previous user message, not by us.
65
+ input.previousUserText ? `<previous-user>${input.previousUserText}</previous-user>` : '',
66
+ '</context>',
67
+ ]
68
+ .filter(Boolean)
69
+ .join('\n');
70
+ // The exchange, not the message (ADR 0035). Both sides are labelled, so a fact taken
71
+ // from my reply is visibly mine — the prompt's fourth category ("my promises") lives
72
+ // in that half, and the provenance rules depend on knowing which half it came from.
73
+ const source = input.assistantText
74
+ ? `<source>\n用户:${input.text}\n我:${input.assistantText}\n</source>`
75
+ : `<source>\n用户:${input.text}\n</source>`;
76
+ return [
77
+ extractionRole,
78
+ '<task>把 <source> 里的信息变成记忆操作,并说明它属于哪条线索。只依据 <source> 与 <previous-user>;<known-facts> 只用于判断冲突与去重。</task>',
79
+ extractionTerms,
80
+ extractionSchema,
81
+ extractionRules,
82
+ extractionExamples,
83
+ context,
84
+ source,
85
+ '<output>',
86
+ ].join('\n\n');
87
+ }
88
+ /**
89
+ * The candidate list for thread attribution.
90
+ *
91
+ * Five at most, most recently used first, each with an id the model must quote
92
+ * back and a short topic it can compare against. Too many candidates and the
93
+ * model starts keyword-matching; a wrong merge is worse than starting a new
94
+ * thread, so the list stays short on purpose.
95
+ *
96
+ * Each candidate also carries its last lines verbatim: the question "does this turn
97
+ * continue that conversation" cannot be answered from a label, and ADR 0034 makes
98
+ * continuing the default, so the evidence has to be there.
99
+ */
100
+ function renderOpenThreads(threads, previousThreadId) {
101
+ if (threads.length === 0)
102
+ return '<open-threads>(空)</open-threads>';
103
+ const lines = threads.map((thread) => {
104
+ const ago = thread.ago === undefined ? '' : ` | ${thread.ago} 轮前`;
105
+ // Whole lines, never a mid-sentence prefix (ADR 0034): a cut candidate line is
106
+ // exactly the case where "继续" and "换个话题" become indistinguishable.
107
+ const head = ` ${thread.id} | ${thread.topic}${ago}`;
108
+ const recent = (thread.recent ?? []).map((line) => {
109
+ const who = line.role === 'user' ? '用户' : '我';
110
+ return ` ${who}:${line.text}`;
111
+ });
112
+ return [head, ...recent].join('\n');
113
+ });
114
+ const previous = previousThreadId ? [` 上一轮所属:${previousThreadId}`] : [];
115
+ return ['<open-threads>', ...lines, ...previous, '</open-threads>'].join('\n');
116
+ }
117
+ const CONSOLIDATION_ROLE = `<role>
118
+ 你是归纳器。你把若干条关于同一对象的事实,压成一条能被这些事实直接支持的判断。
119
+ 你不总结对话,不做心理分析,不写建议。你的输出由一个确定性程序消费。
120
+ </role>`;
121
+ const CONSOLIDATION_RULES = `<rules>
122
+ - 一条结论至少要两条证据支持。只有一条证据的,那是事实,不是结论。
123
+ - 保留具体对象和情境;不许放大成"一切 / 所有 / 总是 / 从不"。
124
+ - 不许引入事实里没有的信息,也不许把两条不同意见合并成"用户有时A有时B"这种废话。
125
+ - 与 <existing> 里某条冲突时,用 取代=<id>;只是补充就不要取代。
126
+ - 置信按证据条数给:两条 0.6,三条 0.7,五条以上 0.85。不要凭感觉给高分。
127
+ - 这组事实撑不起一条判断,就只写:无
128
+ </rules>`;
129
+ const CONSOLIDATION_CONTRAST = `<contrast>
130
+ 坏:用户讨厌一切重复劳动 ← 把两件具体的事放大成规律
131
+ 好:用户讨厌同一件事反复交代 ← 保留具体对象
132
+ 坏:用户是个急躁的人 ← 事实里没有,属于心理分析
133
+ 好:用户在切换工具时容易不耐烦 ← 每条都能对回事实
134
+ 坏:用户有时喜欢重复、有时不喜欢 ← 把矛盾揉成不承担判断的废话
135
+ 好:无 ← 撑不起一条判断时,如实说没有
136
+ </contrast>`;
137
+ const VERIFICATION_ROLE = `<role>
138
+ 你是证据核查员。你只回答一件事:给定一条结论和一组事实,这些事实是否**直接**支持它。
139
+ 你不评价结论是否深刻、是否合理,只看证据。
140
+ </role>`;
141
+ const VERIFICATION_RULES = `<rules>
142
+ - 只依据 <facts> 里的原文,不补充常识,不推测,不替它圆场。
143
+ - 判定只能是:支持 / 部分支持 / 不支持。
144
+ - 部分支持 = 有事实支持,但结论里有一部分是事实没说的。
145
+ - 不支持 = 事实说的是另一件事,或者缺乏关键信息。
146
+ - 在"理由"里指出具体是哪个片段支撑(或缺失)。
147
+ </rules>`;
148
+ /**
149
+ * Stage one: write conclusions.
150
+ *
151
+ * The model sees only facts and existing conclusions, never the conversation.
152
+ */
153
+ export function renderConsolidationPrompt(input) {
154
+ const facts = input.facts.map((fact) => {
155
+ const when = fact.atText ? ` | ${fact.atText}` : '';
156
+ return ` ${fact.id}${when} | ${fact.statement}`;
157
+ });
158
+ const existing = input.existing.length > 0
159
+ ? input.existing.map((fact) => ` ${fact.id} | ${fact.statement}`).join('\n')
160
+ : ' (空)';
161
+ // The turns are given in order, with the resume marked, so "read to the end"
162
+ // is a rule the model can actually follow rather than one it has to infer.
163
+ const thread = input.thread
164
+ ? [
165
+ '<thread>',
166
+ ` 主题:${input.thread.topic}`,
167
+ ...input.thread.turns.map((turn, index) => ` ${index + 1}. ${turn}`),
168
+ '</thread>',
169
+ ].join('\n')
170
+ : '';
171
+ return [
172
+ CONSOLIDATION_ROLE,
173
+ '<task>判断 <facts> 是否共同指向一条判断;能就写一行结论,不能就写 无。</task>',
174
+ CONSOLIDATION_RULES,
175
+ thread
176
+ ? '<thread-rule>这段话是一条完整的话题,可能中间被打断过又回来。<br>结论必须覆盖到最后一次回来时的内容,不能只总结开头。</thread-rule>'
177
+ : '',
178
+ CONSOLIDATION_CONTRAST,
179
+ thread,
180
+ ['<facts>', ...facts, '</facts>'].join('\n'),
181
+ ['<existing>', existing, '</existing>'].join('\n'),
182
+ '<output>',
183
+ '结论: 陈述 | 证据=<id>,<id> | 置信=0.6',
184
+ ]
185
+ .filter(Boolean)
186
+ .join('\n\n');
187
+ }
188
+ /**
189
+ * Stage two: check the draft against its evidence.
190
+ *
191
+ * Deliberately does not carry the draft's reasoning: an independent check is the
192
+ * whole point (Chain-of-Verification), and a check that can see the excuse tends
193
+ * to accept it.
194
+ */
195
+ export function renderVerificationPrompt(input) {
196
+ const facts = input.facts.map((fact) => ` ${fact.id} | ${fact.statement}`).join('\n');
197
+ return [
198
+ VERIFICATION_ROLE,
199
+ VERIFICATION_RULES,
200
+ `<conclusion>${input.conclusion}</conclusion>`,
201
+ ['<facts>', facts, '</facts>'].join('\n'),
202
+ '<output>',
203
+ '判定: 支持 | 依据=<id>,<id> | 理由=哪条事实的哪个片段支撑了它',
204
+ ].join('\n\n');
205
+ }
206
+ /* ------------------------------------------------------------------ *
207
+ * Duplicate handling: is this conclusion new, or a restatement?
208
+ * ------------------------------------------------------------------ */
209
+ const RELATION_ROLE = `<role>
210
+ 你是结论库的守门人。给出一条新结论和几条已有的结论,你只回答一件事:**新结论和其中哪一条是什么关系**。
211
+ 你不评价哪条更好,不修改任何一条的措辞。
212
+ </role>`;
213
+ const RELATION_RULES = `<rules>
214
+ - 重复 = 说的是同一件事,只是措辞或证据不同。例如"用户要机制不要 hope"和"用户倾向方法论依据而不是 prompt 的 hope"。
215
+ - 细化 = 新结论包含旧结论,但更具体或有新条件。例如旧"用户讨厌重复"、新"用户讨厌同一件事反复交代"。
216
+ - 矛盾 = 两者不能同时成立。例如"用户喜欢被反复确认"和"用户讨厌反复确认"。
217
+ - 无关 = 说的是不同的事,哪怕话题相近。
218
+ - 关系必须在 <existing> 里选一个 id,或者写"无关"。
219
+ - 只能引用 <existing> 里出现过的 id,不许编。
220
+ </rules>`;
221
+ /**
222
+ * Stage three: the duplicate check.
223
+ *
224
+ * String matching can only catch the same text; a restatement like "用户要机制
225
+ * 不要 hope" versus "用户倾向方法论依据而不是 prompt 的 hope" needs meaning. The
226
+ * output is a fixed vocabulary so the caller can gate it.
227
+ */
228
+ export function renderRelationPrompt(input) {
229
+ const existing = input.existing.map((fact) => ` ${fact.id} | 证据=${fact.evidence} 条 | ${fact.statement}`);
230
+ return [
231
+ RELATION_ROLE,
232
+ RELATION_RULES,
233
+ `<new>${input.conclusion}</new>`,
234
+ ['<existing>', ...existing, '</existing>'].join('\n'),
235
+ '<output>',
236
+ '关系: 重复 <id> | 理由=…',
237
+ '关系: 细化 <id> | 理由=…',
238
+ '关系: 矛盾 <id> | 理由=…',
239
+ '关系: 无关',
240
+ ].join('\n\n');
241
+ }
242
+ /* ------------------------------------------------------------------ *
243
+ * Classifying lessons: rule (triggered) or profile (about a person)
244
+ * ------------------------------------------------------------------ */
245
+ const CLASSIFY_ROLE = `<role>
246
+ 你在整理一批"教训"。每条要么是一条**规则**(知道什么时候该做什么),要么是一条**画像**(说的是这个人是什么样)。你只做分类,不改写原文。
247
+ </role>`;
248
+ const CLASSIFY_RULES = `<rules>
249
+ 一条一条判断,每种输出一行:
250
+ - 规则 = 能回答"什么时候该做什么"。它必须有一个具体的触发场景。
251
+ - 画像 = 说的是这个人的偏好、原则、性格,没有触发场景。
252
+ - 待修 = 你判断不了是哪一种,或者想不出触发条件。
253
+
254
+ 填写规则那一行时,字段用 key=value,不要写 JSON:
255
+ 关键词=json,package.json 消息里出现任意一个就触发(1–3 个,要具体)
256
+ 正则=\.json\b 关键词不够具体时用
257
+ 状态=long-session 会话状态(长会话、连续空回复)
258
+ 动作=用 edit 工具改 JSON 祈使句,一句话
259
+ 至少给一个触发条件;一个都没有就写"待修"。
260
+
261
+ - 关键词不要用"代码""工作"这种大词,它们每天都会出现。
262
+ - 只能输出 <input> 里出现过的 id,不许编。
263
+ - 画像那一行必须带维度:${dimensionChoices()}。
264
+ 维度决定这条判断以后跟谁合并,选错等于把它归到别的类别里,所以拿不准时按下面 <dimensions> 的定义选,不要自己造词。
265
+ </rules>`;
266
+ const CLASSIFY_EXAMPLE = `<example>
267
+ <input>
268
+ - fc_a | PowerShell 的 Set-Content 会写入 BOM,改 JSON 一律用 edit 工具
269
+ - fc_b | 用户讨厌同一件事反复讲,换工具又要重新交代
270
+ </input>
271
+ <output>
272
+ 规则: fc_a | 关键词=json,package.json,配置 | 动作=用 edit 工具或 BOM 安全写入,不要用字符串替换
273
+ 画像: fc_b | 维度=沟通偏好
274
+ </output>
275
+ </example>`;
276
+ /** Classify a batch of lessons into rules (with a trigger) or profile items. */
277
+ export function renderRuleClassifyPrompt(input) {
278
+ // Whole statements: classification reads the judgement itself, and a 120-char
279
+ // prefix can hide the qualifier that decides rule vs profile.
280
+ const items = input.items.map((item) => `- ${item.id} | ${item.statement}`);
281
+ return [
282
+ CLASSIFY_ROLE,
283
+ CLASSIFY_RULES,
284
+ // The definitions are rendered from dimensions.ts, never retyped here: a second
285
+ // copy of the admission criteria is a copy that stops matching after one edit.
286
+ PROFILE_DIMENSIONS_BLOCK,
287
+ CLASSIFY_EXAMPLE,
288
+ '<format>每行:规则: id | 关键词=… | 动作=… / 画像: id | 维度=… / 待修: id | 原因=…</format>',
289
+ ['<input>', ...items, '</input>'].join('\n'),
290
+ '<output>',
291
+ ].join('\n\n');
292
+ }
293
+ export { renderProfileMergePrompt } from './profile-prompt.js';