dsh-continual-evolve 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -21
- package/README.zh.md +33 -21
- package/client/client.js +140 -91
- package/lib/card-routes.d.ts +3 -3
- package/lib/card-routes.js +38 -50
- package/lib/extraction-prompt.d.ts +52 -0
- package/lib/extraction-prompt.js +139 -0
- package/lib/extraction-surface.d.ts +66 -0
- package/lib/extraction-surface.js +186 -0
- package/lib/extraction.d.ts +60 -0
- package/lib/extraction.js +269 -0
- package/lib/import-md.d.ts +15 -0
- package/lib/import-md.js +140 -0
- package/lib/index.d.ts +63 -27
- package/lib/index.js +121 -65
- package/lib/memory-guide.d.ts +22 -14
- package/lib/memory-guide.js +33 -76
- package/lib/memory-rules.d.ts +56 -0
- package/lib/memory-rules.js +152 -0
- package/lib/memory-section.d.ts +26 -26
- package/lib/memory-section.js +48 -82
- package/lib/memory-snapshot.d.ts +43 -59
- package/lib/memory-snapshot.js +101 -141
- package/lib/memory-tools.d.ts +34 -0
- package/lib/memory-tools.js +142 -0
- package/lib/store-apply.d.ts +46 -0
- package/lib/store-apply.js +177 -0
- package/lib/store.d.ts +95 -0
- package/lib/store.js +207 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The extraction prompt: what deserves a memory, how each type reads, and
|
|
3
|
+
* the closed proposal format the model must answer in.
|
|
4
|
+
*
|
|
5
|
+
* The taxonomy moved here verbatim-in-spirit from the main-session guide
|
|
6
|
+
* (the 4a1e57d version) when the store became a gated database: the
|
|
7
|
+
* extraction model is the only writer left that needs the write-quality
|
|
8
|
+
* rules, so teaching them in the main session would be spent tokens twice.
|
|
9
|
+
* The model proposes; the store's gates (constraints, secret screen, dedup,
|
|
10
|
+
* feedback contract) decide — the proposal is never trusted on its own.
|
|
11
|
+
*/
|
|
12
|
+
/** The extraction model's system prompt (Chinese — memory content is Chinese-first). */
|
|
13
|
+
export declare const EXTRACTION_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u63D0\u53D6\u4E13\u5458\u3002\u4F60\u7684\u552F\u4E00\u4EFB\u52A1\uFF1A\u4ECE\u4F1A\u8BDD\u8F68\u8FF9\u589E\u91CF\u91CC\u5224\u65AD\u54EA\u4E9B\u4FE1\u606F\u503C\u5F97\u5B58\u5165\u7528\u6237\u7684\u8DE8\u4F1A\u8BDD\u6301\u4E45\u8BB0\u5FC6\uFF0C\u5E76\u63D0\u51FA\u7ED3\u6784\u5316\u63D0\u6848\u3002\u4F60\u4E0D\u6267\u884C\u4EFB\u52A1\u3001\u4E0D\u56DE\u7B54\u7528\u6237\u3001\u4E0D\u89E3\u91CA\u63A8\u7406\u3002\n\n## \u4F55\u65F6\u5199\u5165\u8BB0\u5FC6\n\n<type name=\"user\">\n <when_to_save>\u5F97\u77E5\u7528\u6237\u7684\u753B\u50CF\u4E0E\u73AF\u5883\u4E8B\u5B9E\uFF1A\u89D2\u8272\u3001\u6280\u672F\u6808\u3001\u7ECF\u9A8C\u6C34\u5E73\u3001\u504F\u597D\uFF0C\u4EE5\u53CA\u673A\u5668\u4E0E\u5DE5\u5177\u94FE\uFF08\u64CD\u4F5C\u7CFB\u7EDF\u3001\u88C5\u4E86\u4EC0\u4E48\u3001\u914D\u7F6E\u5728\u54EA\uFF09\u3002\u4F8B\uFF1A\"\u6211\u5728 Fedora \u4E0A\u5DE5\u4F5C\uFF0C\u65E5\u5E38\u8DD1 dotnet-desktop profile\"\u3002</when_to_save>\n</type>\n\n<type name=\"feedback\">\n <when_to_save>\u4E24\u7C7B\u4FE1\u53F7\uFF1A\n \uFF081\uFF09\u7EA0\u6B63\u2014\u2014\u7528\u6237\u8BF4\"\u4E0D\u662F\u8FD9\u6837\"\"\u522B\"\"\u505C\u4E0B\"\u7684\u90A3\u4E00\u8F6E\uFF0C\u5B58\u88AB\u7EA0\u6B63\u7684\u505A\u6CD5\u4E0E\u6B63\u786E\u505A\u6CD5\uFF1B\n \uFF082\uFF09\u786E\u8BA4\u2014\u2014\u66F4\u5B89\u9759\u4F46\u540C\u6837\u8981\u7D27\uFF1A\u7528\u6237\u63A5\u53D7\u4E86\u4F60\u4E0D\u5BFB\u5E38\u7684\u9009\u62E9\u800C\u65E0\u5F02\u8BAE\uFF0C\u6216\u660E\u786E\u8BF4\"\u5BF9\uFF0C\u5C31\u8FD9\u6837\"\u3002\u53EA\u5B58\u7EA0\u9519\u4F1A\u8BA9\u884C\u4E3A\u8D8A\u6765\u8D8A\u4FDD\u5B88\u3002\n \u4F8B\uFF1A\"\u6D4B\u8BD5\u522B mock \u6570\u636E\u5E93\uFF0C\u4E0A\u6B21 mock \u8FC7\u4E86\u7EBF\u4E0A\u8FC1\u79FB\u6302\u4E86\"\u2014\u2014\u5B58\u89C4\u5219\u5E76\u5E26\u4E0A\u4E8B\u6545\u539F\u56E0\u3002</when_to_save>\n <body_structure>\u89C4\u5219\u5148\u884C\uFF0C\u968F\u540E\u5FC5\u5E26\u4E24\u884C\uFF1A**Why:**\uFF08\u7528\u6237\u7ED9\u7684\u7406\u7531\u2014\u2014\u77E5\u9053\u4E3A\u4EC0\u4E48\u624D\u80FD\u5224\u65AD\u8FB9\u7F18\u60C5\u51B5\uFF09\u548C **How to apply:**\uFF08\u4F55\u65F6\u4F55\u5730\u751F\u6548\uFF09\u3002\u7F3A\u8FD9\u4E24\u884C\u7684\u63D0\u6848\u4F1A\u88AB\u673A\u68B0\u62D2\u7EDD\u3002</body_structure>\n</type>\n\n<type name=\"reference\">\n <when_to_save>\u7528\u6237\u63D0\u5230\u672A\u6765\u4F1A\u7528\u5230\u7684\u8D44\u6E90\u6307\u9488\uFF1AURL\u3001\u4EEA\u8868\u76D8\u3001\u5DE5\u5355\u3001\u672C\u5730\u6570\u636E\u5E93\u3001\u6587\u6863\u4F4D\u7F6E\u3002</when_to_save>\n</type>\n\n**\u8FB9\u754C\u5224\u636E\uFF1A\u6362\u4E00\u4E2A\u4ED3\u5E93\u8FD8\u6709\u7528\u7684\u4FE1\u606F\u624D\u8FDB\u8BB0\u5FC6\u3002**\u51B3\u7B56\u3001\u67B6\u6784\u53D6\u820D\u3001\u8FDB\u884C\u4E2D\u72B6\u6001\u53EA\u5BF9\u5F53\u524D\u4ED3\u5E93\u6709\u4EF7\u503C\u2014\u2014\u5B83\u4EEC\u8D70\u4ED3\u5E93\u7684 ADR/\u6587\u6863\u8DEF\u7EBF\uFF0C\u7EDD\u4E0D\u63D0\u6848\u3002\u80FD\u4ECE\u4EE3\u7801\u3001git \u5386\u53F2\u3001\u4ED3\u5E93\u6587\u4EF6\u91CC\u91CD\u65B0\u63A8\u5BFC\u51FA\u6765\u7684\u4E8B\u5B9E\u4E0D\u5B58\uFF1B\u4E34\u65F6\u4EFB\u52A1\u72B6\u6001\u3001\u4E00\u6B21\u6027\u8C03\u8BD5\u8FC7\u7A0B\u3001\u65E0\u590D\u73B0\u8BC1\u636E\u7684\u95EE\u9898\u4FEE\u590D\u4E0D\u5B58\u3002\u76F8\u5BF9\u65E5\u671F\u4E00\u5F8B\u8F6C\u6210\u7EDD\u5BF9\u65E5\u671F\uFF08\"\u5468\u56DB\"\u2192\"2026-10-08\"\uFF09\u3002\n\n## \u66F4\u65B0\u4F18\u5148\u4E8E\u65B0\u5EFA\n\n\u5DF2\u6709\u8BB0\u5FC6\u7684\u72B6\u6001\u53D8\u4E86\uFF0C\u63D0\u6848 update \u6539\u539F\u6761\u76EE\uFF1B\u65B0\u65E7\u5E76\u5B58\u6BD4\u6CA1\u6709\u8BB0\u5FC6\u66F4\u7CDF\u3002\u8981\u66F4\u65B0\u6216\u5220\u9664\u7684\u6761\u76EE\u4F1A\u8FDE\u540C\u5168\u6587\u7ED9\u4F60\uFF0C\u63D0\u6848\u91CC\u5199\u5B83\u7684 id\u3002\u6CA1\u6709\u503C\u5F97\u6C89\u6DC0\u7684\u4E1C\u897F\u5C31\u660E\u786E\u8BF4 skip\u2014\u2014\u5B81\u7F3A\u52FF\u6EE5\u3002\n\n## \u8F93\u51FA\u683C\u5F0F\n\n\u53EA\u8F93\u51FA\u4E00\u4E2A JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EE3\u7801\u56F4\u680F\u3001\u4E0D\u8981\u89E3\u91CA\uFF1A\n\n{\"decision\":\"apply\"|\"skip\",\"reason\":\"<\u4E00\u53E5\u8BDD\u4F9D\u636E>\",\"proposals\":[{\"action\":\"create\"|\"update\"|\"delete\",\"id\":\"<kebab-slug\uFF0Cupdate/delete \u5FC5\u586B>\",\"type\":\"user\"|\"feedback\"|\"reference\",\"title\":\"<\u77ED\u6807\u9898>\",\"description\":\"<\u4E00\u53E5\u8BDD\u76F8\u5173\u6027\u94A9\u5B50>\",\"body\":\"<\u6B63\u6587>\",\"sourceSeqs\":\"<\u7528\u6237\u539F\u8BDD\u6240\u5728 seq\uFF0C\u5982 820-831\uFF0C\u53EF\u7701\u7565>\"}]}";
|
|
14
|
+
/** The existing-memory manifest line shown to the model. */
|
|
15
|
+
export declare function manifestLineOf(record: {
|
|
16
|
+
id: string;
|
|
17
|
+
type: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
}): string;
|
|
21
|
+
/** The user-prompt block for one extraction run. */
|
|
22
|
+
export declare function extractionPrompt(input: {
|
|
23
|
+
increment: string;
|
|
24
|
+
manifest: string[];
|
|
25
|
+
candidates: string[];
|
|
26
|
+
userSeqs: readonly number[];
|
|
27
|
+
}): string;
|
|
28
|
+
/** The parsed model answer before store gating. */
|
|
29
|
+
export interface RawProposal {
|
|
30
|
+
action: "create" | "update" | "delete";
|
|
31
|
+
id?: string | undefined;
|
|
32
|
+
type?: string | undefined;
|
|
33
|
+
title?: string | undefined;
|
|
34
|
+
description?: string | undefined;
|
|
35
|
+
body?: string | undefined;
|
|
36
|
+
sourceSeqs?: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
export interface ParsedAnswer {
|
|
39
|
+
decision: "apply" | "skip" | "error";
|
|
40
|
+
reason: string;
|
|
41
|
+
proposals: RawProposal[];
|
|
42
|
+
}
|
|
43
|
+
/** Proposals per run cap — bounds the damage a bad model answer can do. */
|
|
44
|
+
export declare const MAX_PROPOSALS_PER_RUN = 8;
|
|
45
|
+
/**
|
|
46
|
+
* Parse the model's JSON answer. Tolerates code fences; rejects anything
|
|
47
|
+
* that is not the agreed shape. A genuine "skip" is a model decision the
|
|
48
|
+
* caller may act on (consume the increment); "error" marks an unparseable
|
|
49
|
+
* or shape-violating answer, which must never land as a silent success.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseExtractionAnswer(text: string): ParsedAnswer;
|
|
52
|
+
//# sourceMappingURL=extraction-prompt.d.ts.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The extraction prompt: what deserves a memory, how each type reads, and
|
|
3
|
+
* the closed proposal format the model must answer in.
|
|
4
|
+
*
|
|
5
|
+
* The taxonomy moved here verbatim-in-spirit from the main-session guide
|
|
6
|
+
* (the 4a1e57d version) when the store became a gated database: the
|
|
7
|
+
* extraction model is the only writer left that needs the write-quality
|
|
8
|
+
* rules, so teaching them in the main session would be spent tokens twice.
|
|
9
|
+
* The model proposes; the store's gates (constraints, secret screen, dedup,
|
|
10
|
+
* feedback contract) decide — the proposal is never trusted on its own.
|
|
11
|
+
*/
|
|
12
|
+
/** The extraction model's system prompt (Chinese — memory content is Chinese-first). */
|
|
13
|
+
export const EXTRACTION_SYSTEM_PROMPT = `你是记忆提取专员。你的唯一任务:从会话轨迹增量里判断哪些信息值得存入用户的跨会话持久记忆,并提出结构化提案。你不执行任务、不回答用户、不解释推理。
|
|
14
|
+
|
|
15
|
+
## 何时写入记忆
|
|
16
|
+
|
|
17
|
+
<type name="user">
|
|
18
|
+
<when_to_save>得知用户的画像与环境事实:角色、技术栈、经验水平、偏好,以及机器与工具链(操作系统、装了什么、配置在哪)。例:"我在 Fedora 上工作,日常跑 dotnet-desktop profile"。</when_to_save>
|
|
19
|
+
</type>
|
|
20
|
+
|
|
21
|
+
<type name="feedback">
|
|
22
|
+
<when_to_save>两类信号:
|
|
23
|
+
(1)纠正——用户说"不是这样""别""停下"的那一轮,存被纠正的做法与正确做法;
|
|
24
|
+
(2)确认——更安静但同样要紧:用户接受了你不寻常的选择而无异议,或明确说"对,就这样"。只存纠错会让行为越来越保守。
|
|
25
|
+
例:"测试别 mock 数据库,上次 mock 过了线上迁移挂了"——存规则并带上事故原因。</when_to_save>
|
|
26
|
+
<body_structure>规则先行,随后必带两行:**Why:**(用户给的理由——知道为什么才能判断边缘情况)和 **How to apply:**(何时何地生效)。缺这两行的提案会被机械拒绝。</body_structure>
|
|
27
|
+
</type>
|
|
28
|
+
|
|
29
|
+
<type name="reference">
|
|
30
|
+
<when_to_save>用户提到未来会用到的资源指针:URL、仪表盘、工单、本地数据库、文档位置。</when_to_save>
|
|
31
|
+
</type>
|
|
32
|
+
|
|
33
|
+
**边界判据:换一个仓库还有用的信息才进记忆。**决策、架构取舍、进行中状态只对当前仓库有价值——它们走仓库的 ADR/文档路线,绝不提案。能从代码、git 历史、仓库文件里重新推导出来的事实不存;临时任务状态、一次性调试过程、无复现证据的问题修复不存。相对日期一律转成绝对日期("周四"→"2026-10-08")。
|
|
34
|
+
|
|
35
|
+
## 更新优先于新建
|
|
36
|
+
|
|
37
|
+
已有记忆的状态变了,提案 update 改原条目;新旧并存比没有记忆更糟。要更新或删除的条目会连同全文给你,提案里写它的 id。没有值得沉淀的东西就明确说 skip——宁缺勿滥。
|
|
38
|
+
|
|
39
|
+
## 输出格式
|
|
40
|
+
|
|
41
|
+
只输出一个 JSON 对象,不要代码围栏、不要解释:
|
|
42
|
+
|
|
43
|
+
{"decision":"apply"|"skip","reason":"<一句话依据>","proposals":[{"action":"create"|"update"|"delete","id":"<kebab-slug,update/delete 必填>","type":"user"|"feedback"|"reference","title":"<短标题>","description":"<一句话相关性钩子>","body":"<正文>","sourceSeqs":"<用户原话所在 seq,如 820-831,可省略>"}]}`;
|
|
44
|
+
/** The existing-memory manifest line shown to the model. */
|
|
45
|
+
export function manifestLineOf(record) {
|
|
46
|
+
return `- id=${record.id}|type=${record.type}|title=${record.title}|钩子=${record.description}`;
|
|
47
|
+
}
|
|
48
|
+
/** The user-prompt block for one extraction run. */
|
|
49
|
+
export function extractionPrompt(input) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
parts.push(`<manifest>
|
|
52
|
+
本工作区现有记忆(提案 update/delete 时引用这里的 id;与增量相似度最高的候选全文附后):
|
|
53
|
+
${input.manifest.length > 0 ? input.manifest.join("\n") : "(本工作区还没有记忆)"}
|
|
54
|
+
</manifest>`);
|
|
55
|
+
if (input.candidates.length > 0) {
|
|
56
|
+
parts.push(`<candidates>
|
|
57
|
+
${input.candidates.join("\n\n")}
|
|
58
|
+
</candidates>`);
|
|
59
|
+
}
|
|
60
|
+
parts.push(`<increment>
|
|
61
|
+
(用户直接发言所在 seq:${input.userSeqs.length > 0 ? input.userSeqs.join(",") : "无"})
|
|
62
|
+
${input.increment}
|
|
63
|
+
</increment>`);
|
|
64
|
+
parts.push("按系统指令输出 JSON 提案。");
|
|
65
|
+
return parts.join("\n\n");
|
|
66
|
+
}
|
|
67
|
+
/** Proposals per run cap — bounds the damage a bad model answer can do. */
|
|
68
|
+
export const MAX_PROPOSALS_PER_RUN = 8;
|
|
69
|
+
/**
|
|
70
|
+
* Parse the model's JSON answer. Tolerates code fences; rejects anything
|
|
71
|
+
* that is not the agreed shape. A genuine "skip" is a model decision the
|
|
72
|
+
* caller may act on (consume the increment); "error" marks an unparseable
|
|
73
|
+
* or shape-violating answer, which must never land as a silent success.
|
|
74
|
+
*/
|
|
75
|
+
export function parseExtractionAnswer(text) {
|
|
76
|
+
const stripped = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
|
|
77
|
+
const start = stripped.indexOf("{");
|
|
78
|
+
const end = stripped.lastIndexOf("}");
|
|
79
|
+
if (start < 0 || end <= start) {
|
|
80
|
+
return { decision: "error", reason: "unparseable output: no JSON object found", proposals: [] };
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(stripped.slice(start, end + 1));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
decision: "error",
|
|
89
|
+
reason: `unparseable output: ${error instanceof Error ? error.message : String(error)}`,
|
|
90
|
+
proposals: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// Defensive: the sliced text always starts with "{", so a successful
|
|
94
|
+
// JSON.parse here is always an object — the branch is unreachable today
|
|
95
|
+
// but stays as a guard against future slicing changes.
|
|
96
|
+
/* v8 ignore next 3 */
|
|
97
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
98
|
+
return { decision: "error", reason: "unparseable output: not an object", proposals: [] };
|
|
99
|
+
}
|
|
100
|
+
const answer = parsed;
|
|
101
|
+
const reason = typeof answer.reason === "string" ? answer.reason : "";
|
|
102
|
+
if (answer.decision === "skip") {
|
|
103
|
+
return { decision: "skip", reason: reason || "model chose to skip", proposals: [] };
|
|
104
|
+
}
|
|
105
|
+
if (answer.decision !== "apply") {
|
|
106
|
+
return { decision: "error", reason: `unknown decision ${String(answer.decision)}`, proposals: [] };
|
|
107
|
+
}
|
|
108
|
+
if (!Array.isArray(answer.proposals)) {
|
|
109
|
+
return { decision: "error", reason: "apply without a proposals array", proposals: [] };
|
|
110
|
+
}
|
|
111
|
+
const proposals = [];
|
|
112
|
+
for (const raw of answer.proposals) {
|
|
113
|
+
if (typeof raw !== "object" || raw === null) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const item = raw;
|
|
117
|
+
const action = item.action;
|
|
118
|
+
if (action !== "create" && action !== "update" && action !== "delete") {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
proposals.push({
|
|
122
|
+
action,
|
|
123
|
+
id: typeof item.id === "string" ? item.id : undefined,
|
|
124
|
+
type: typeof item.type === "string" ? item.type : undefined,
|
|
125
|
+
title: typeof item.title === "string" ? item.title : undefined,
|
|
126
|
+
description: typeof item.description === "string" ? item.description : undefined,
|
|
127
|
+
body: typeof item.body === "string" ? item.body : undefined,
|
|
128
|
+
sourceSeqs: typeof item.sourceSeqs === "string" ? item.sourceSeqs : undefined,
|
|
129
|
+
});
|
|
130
|
+
if (proposals.length >= MAX_PROPOSALS_PER_RUN) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (proposals.length === 0) {
|
|
135
|
+
return { decision: "error", reason: reason || "apply carried no usable proposals", proposals: [] };
|
|
136
|
+
}
|
|
137
|
+
return { decision: "apply", reason, proposals };
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=extraction-prompt.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trajectory slicing for the extraction pipeline: the bounded, mechanically
|
|
3
|
+
* filtered increment of one session since the last extraction cursor.
|
|
4
|
+
*
|
|
5
|
+
* The mechanics carry over from the v0.10.4 turn snapshots (proven against
|
|
6
|
+
* real DSH session surfaces): a `seq:<n>` cursor selects rows after the last
|
|
7
|
+
* boundary, direct-user rows drive the eligibility gate, and only
|
|
8
|
+
* user/assistant text is serialized. Judgment about what deserves a memory
|
|
9
|
+
* is the extraction model's job — this module only removes mechanical noise
|
|
10
|
+
* before any tokens are spent.
|
|
11
|
+
*/
|
|
12
|
+
/** ZCode's minimum lexical words in the increment's direct user text. */
|
|
13
|
+
export declare const MEMORY_MIN_USER_WORDS = 3;
|
|
14
|
+
/** Default ceiling for the serialized increment handed to the model. */
|
|
15
|
+
export declare const DEFAULT_INCREMENT_MAX_CHARS = 40000;
|
|
16
|
+
/** Why an extraction run was skipped without (or before) an LLM call. */
|
|
17
|
+
export type SkipReason = "internal-agent" | "no-new-events" | "no-user-prose" | "direct-memory-write" | "no-route" | "empty-increment";
|
|
18
|
+
/** The session surface reader this plugin consumes (duck-typed). */
|
|
19
|
+
export interface SurfaceReader {
|
|
20
|
+
readSurface(sessionId: string): Promise<{
|
|
21
|
+
events: unknown[];
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export interface SliceInput {
|
|
25
|
+
events: readonly unknown[];
|
|
26
|
+
internalAgent: boolean;
|
|
27
|
+
minUserWords?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface Slice {
|
|
30
|
+
/** The increment rows after the cursor (already selected by the caller). */
|
|
31
|
+
events: readonly unknown[];
|
|
32
|
+
eligible: boolean;
|
|
33
|
+
skipReason?: SkipReason;
|
|
34
|
+
/** Direct user text in the increment (the extraction model's evidence). */
|
|
35
|
+
userText: string;
|
|
36
|
+
/** Seq numbers of the direct user rows, for record provenance. */
|
|
37
|
+
userSeqs: number[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Judge whether the increment deserves a model call. This intentionally does
|
|
41
|
+
* not decide whether a fact is valuable — only that there is new, real user
|
|
42
|
+
* prose to extract from.
|
|
43
|
+
*/
|
|
44
|
+
export declare function evaluateSlice(input: SliceInput): Slice;
|
|
45
|
+
/**
|
|
46
|
+
* True when the increment already contains a `memory_write` tool call: the
|
|
47
|
+
* user's explicit instruction was handled in-turn by the gated write path,
|
|
48
|
+
* so re-extracting the same turn would only duplicate it.
|
|
49
|
+
*/
|
|
50
|
+
export declare function containsExplicitMemoryWrite(events: readonly unknown[]): boolean;
|
|
51
|
+
/** The new boundary after processing these events: `seq:<max>` or the count. */
|
|
52
|
+
export declare function boundaryCursorOf(events: readonly unknown[]): string;
|
|
53
|
+
/** Numeric boundary of a `seq:<n>` cursor (undefined for index cursors). */
|
|
54
|
+
export declare function seqOfCursor(cursor: string | undefined): number | undefined;
|
|
55
|
+
/** Rows after the cursor: everything with a seq beyond it (seq-less rows stay). */
|
|
56
|
+
export declare function eventsAfterCursor(events: readonly unknown[], cursor: string | undefined): readonly unknown[];
|
|
57
|
+
/**
|
|
58
|
+
* Serialize only user/assistant text, keeping the bounded tail. Tool calls,
|
|
59
|
+
* reasoning, and metadata never enter the prompt.
|
|
60
|
+
*/
|
|
61
|
+
export declare function serializeIncrement(events: readonly unknown[], maxChars?: number): string;
|
|
62
|
+
/** A DSH subagent header is an internal source when its origin says so. */
|
|
63
|
+
export declare function isInternalAgent(agent: unknown): boolean;
|
|
64
|
+
/** Lexical word count via Intl.Segmenter — CJK text counts characters as words. */
|
|
65
|
+
export declare function countWords(text: string): number;
|
|
66
|
+
//# sourceMappingURL=extraction-surface.d.ts.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trajectory slicing for the extraction pipeline: the bounded, mechanically
|
|
3
|
+
* filtered increment of one session since the last extraction cursor.
|
|
4
|
+
*
|
|
5
|
+
* The mechanics carry over from the v0.10.4 turn snapshots (proven against
|
|
6
|
+
* real DSH session surfaces): a `seq:<n>` cursor selects rows after the last
|
|
7
|
+
* boundary, direct-user rows drive the eligibility gate, and only
|
|
8
|
+
* user/assistant text is serialized. Judgment about what deserves a memory
|
|
9
|
+
* is the extraction model's job — this module only removes mechanical noise
|
|
10
|
+
* before any tokens are spent.
|
|
11
|
+
*/
|
|
12
|
+
/** ZCode's minimum lexical words in the increment's direct user text. */
|
|
13
|
+
export const MEMORY_MIN_USER_WORDS = 3;
|
|
14
|
+
/** Default ceiling for the serialized increment handed to the model. */
|
|
15
|
+
export const DEFAULT_INCREMENT_MAX_CHARS = 40_000;
|
|
16
|
+
/** CJK-aware word segmenter (lexical words, not whitespace splits). */
|
|
17
|
+
const WORD_SEGMENTER = new Intl.Segmenter("und", { granularity: "word" });
|
|
18
|
+
/**
|
|
19
|
+
* Judge whether the increment deserves a model call. This intentionally does
|
|
20
|
+
* not decide whether a fact is valuable — only that there is new, real user
|
|
21
|
+
* prose to extract from.
|
|
22
|
+
*/
|
|
23
|
+
export function evaluateSlice(input) {
|
|
24
|
+
const { events } = input;
|
|
25
|
+
const userSeqs = directUserSeqs(events);
|
|
26
|
+
if (input.internalAgent) {
|
|
27
|
+
return { events, eligible: false, skipReason: "internal-agent", userText: "", userSeqs };
|
|
28
|
+
}
|
|
29
|
+
if (events.length === 0) {
|
|
30
|
+
return { events, eligible: false, skipReason: "no-new-events", userText: "", userSeqs };
|
|
31
|
+
}
|
|
32
|
+
if (containsExplicitMemoryWrite(events)) {
|
|
33
|
+
return { events, eligible: false, skipReason: "direct-memory-write", userText: "", userSeqs };
|
|
34
|
+
}
|
|
35
|
+
const userText = directUserText(events);
|
|
36
|
+
if (countWords(userText) < (input.minUserWords ?? MEMORY_MIN_USER_WORDS)) {
|
|
37
|
+
return { events, eligible: false, skipReason: "no-user-prose", userText, userSeqs };
|
|
38
|
+
}
|
|
39
|
+
return { events, eligible: true, userText, userSeqs };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* True when the increment already contains a `memory_write` tool call: the
|
|
43
|
+
* user's explicit instruction was handled in-turn by the gated write path,
|
|
44
|
+
* so re-extracting the same turn would only duplicate it.
|
|
45
|
+
*/
|
|
46
|
+
export function containsExplicitMemoryWrite(events) {
|
|
47
|
+
for (const raw of events) {
|
|
48
|
+
if (!raw || typeof raw !== "object") {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const row = raw;
|
|
52
|
+
if (row.type !== "tool/call" && row.type !== "tool-call") {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const name = row.data?.name ?? row.data?.tool ?? row.data?.toolName;
|
|
56
|
+
if (typeof name === "string" && name.trim() === "memory_write") {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/** The new boundary after processing these events: `seq:<max>` or the count. */
|
|
63
|
+
export function boundaryCursorOf(events) {
|
|
64
|
+
const seqs = events.map(eventSeq).filter((seq) => seq !== undefined);
|
|
65
|
+
return seqs.length > 0 ? `seq:${Math.max(...seqs)}` : `index:${events.length}`;
|
|
66
|
+
}
|
|
67
|
+
/** Numeric boundary of a `seq:<n>` cursor (undefined for index cursors). */
|
|
68
|
+
export function seqOfCursor(cursor) {
|
|
69
|
+
if (cursor === undefined || !cursor.startsWith("seq:")) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const value = Number(cursor.slice(4));
|
|
73
|
+
return Number.isInteger(value) ? value : undefined;
|
|
74
|
+
}
|
|
75
|
+
/** Rows after the cursor: everything with a seq beyond it (seq-less rows stay). */
|
|
76
|
+
export function eventsAfterCursor(events, cursor) {
|
|
77
|
+
const boundary = seqOfCursor(cursor);
|
|
78
|
+
if (boundary === undefined) {
|
|
79
|
+
return cursor === undefined ? events : [];
|
|
80
|
+
}
|
|
81
|
+
return events.filter((event) => {
|
|
82
|
+
const seq = eventSeq(event);
|
|
83
|
+
return seq === undefined || seq > boundary;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Serialize only user/assistant text, keeping the bounded tail. Tool calls,
|
|
88
|
+
* reasoning, and metadata never enter the prompt.
|
|
89
|
+
*/
|
|
90
|
+
export function serializeIncrement(events, maxChars = DEFAULT_INCREMENT_MAX_CHARS) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
for (const raw of events) {
|
|
93
|
+
if (!raw || typeof raw !== "object") {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const event = raw;
|
|
97
|
+
const role = event.type === "user/message" ? "user" : event.type === "assistant/message" ? "assistant" : null;
|
|
98
|
+
if (role === null) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (role === "user" && !isDirectUserEvent(raw)) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const text = contentText(event.data?.content).trim();
|
|
105
|
+
if (text.length > 0) {
|
|
106
|
+
lines.push(`${role}: ${text}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const joined = lines.join("\n");
|
|
110
|
+
return joined.length <= maxChars ? joined : joined.slice(-maxChars);
|
|
111
|
+
}
|
|
112
|
+
/** A DSH subagent header is an internal source when its origin says so. */
|
|
113
|
+
export function isInternalAgent(agent) {
|
|
114
|
+
const header = agent
|
|
115
|
+
?.session?.header;
|
|
116
|
+
return header?.origin === "subagent" || header?.origin === "internal" || header?.isSubagent === true;
|
|
117
|
+
}
|
|
118
|
+
/** Lexical word count via Intl.Segmenter — CJK text counts characters as words. */
|
|
119
|
+
export function countWords(text) {
|
|
120
|
+
return [...WORD_SEGMENTER.segment(text)].filter((segment) => segment.isWordLike).length;
|
|
121
|
+
}
|
|
122
|
+
/** Direct user rows: real user text, not synthetic injections or tool output. */
|
|
123
|
+
function isDirectUserEvent(event) {
|
|
124
|
+
if (!event || typeof event !== "object") {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const row = event;
|
|
128
|
+
if (row.type !== "user/message") {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (row.data?.visibility === "model-only") {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (row.data?.source?.synthetic === true) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
if (row.data?.source?.kind !== undefined && row.data.source.kind !== "user") {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return contentText(row.data?.content).trim().length > 0;
|
|
141
|
+
}
|
|
142
|
+
function directUserText(events) {
|
|
143
|
+
return events
|
|
144
|
+
.filter(isDirectUserEvent)
|
|
145
|
+
.map((event) => contentText(event.data?.content).trim())
|
|
146
|
+
.filter(Boolean)
|
|
147
|
+
.join("\n");
|
|
148
|
+
}
|
|
149
|
+
function directUserSeqs(events) {
|
|
150
|
+
return events
|
|
151
|
+
.filter(isDirectUserEvent)
|
|
152
|
+
.map(eventSeq)
|
|
153
|
+
.filter((seq) => seq !== undefined);
|
|
154
|
+
}
|
|
155
|
+
function contentText(content) {
|
|
156
|
+
if (typeof content === "string") {
|
|
157
|
+
return content;
|
|
158
|
+
}
|
|
159
|
+
if (!Array.isArray(content)) {
|
|
160
|
+
return "";
|
|
161
|
+
}
|
|
162
|
+
return content
|
|
163
|
+
.map((block) => {
|
|
164
|
+
if (typeof block === "string") {
|
|
165
|
+
return block;
|
|
166
|
+
}
|
|
167
|
+
if (!block || typeof block !== "object") {
|
|
168
|
+
return "";
|
|
169
|
+
}
|
|
170
|
+
const value = block;
|
|
171
|
+
if (value.synthetic === true || value.ignored === true || (value.type !== undefined && value.type !== "text")) {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
return typeof value.text === "string" ? value.text : "";
|
|
175
|
+
})
|
|
176
|
+
.filter(Boolean)
|
|
177
|
+
.join(" ");
|
|
178
|
+
}
|
|
179
|
+
function eventSeq(event) {
|
|
180
|
+
if (!event || typeof event !== "object") {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
const seq = event.seq;
|
|
184
|
+
return typeof seq === "number" && Number.isInteger(seq) ? seq : undefined;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=extraction-surface.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type SkipReason, type SurfaceReader } from "./extraction-surface.js";
|
|
2
|
+
import type { LedgerEntry, MemoryStore } from "./store.js";
|
|
3
|
+
/** The minimal LLM face extraction calls (duck-typed host service). */
|
|
4
|
+
export interface LlmStream {
|
|
5
|
+
stream(options: Record<string, unknown>): AsyncIterable<unknown>;
|
|
6
|
+
}
|
|
7
|
+
/** Everything a run needs: the store, the session reader, and the LLM. */
|
|
8
|
+
export interface ExtractionDeps {
|
|
9
|
+
store: MemoryStore;
|
|
10
|
+
surface: SurfaceReader | undefined;
|
|
11
|
+
llm: LlmStream | undefined;
|
|
12
|
+
}
|
|
13
|
+
/** One extraction target: whose session, in which workspace, on which route. */
|
|
14
|
+
export interface ExtractionTarget {
|
|
15
|
+
workspaceId: string | undefined;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
internalAgent: boolean;
|
|
18
|
+
provider: string | undefined;
|
|
19
|
+
model: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Terminal outcome of one run. */
|
|
22
|
+
export interface ExtractionResult {
|
|
23
|
+
status: "applied" | "rejected" | "skipped" | "failed";
|
|
24
|
+
skipReason?: SkipReason | string | undefined;
|
|
25
|
+
/** Mutations landed (status "applied"). */
|
|
26
|
+
applied?: number | undefined;
|
|
27
|
+
/** The seq boundary the cursor advanced to (status "applied" or model skip). */
|
|
28
|
+
cursor?: number | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Options for one run. */
|
|
31
|
+
export interface RunOptions {
|
|
32
|
+
trigger: LedgerEntry["trigger"];
|
|
33
|
+
/** Character ceiling for the serialized increment (default 40000). */
|
|
34
|
+
incrementMaxChars?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Run one extraction pass for a target. Never throws: every terminal path
|
|
38
|
+
* is either a landed application, a ledgered skip, or a ledgered failure.
|
|
39
|
+
*/
|
|
40
|
+
export declare function runExtraction(deps: ExtractionDeps, target: ExtractionTarget, opts: RunOptions): Promise<ExtractionResult>;
|
|
41
|
+
/** The event wiring surface (duck-typed cordis context). */
|
|
42
|
+
export interface SchedulerHost {
|
|
43
|
+
on(event: string, handler: (...args: never[]) => void): unknown;
|
|
44
|
+
logger(name: string): {
|
|
45
|
+
info(message: string): void;
|
|
46
|
+
warn(message: string): void;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface SchedulerOptions {
|
|
50
|
+
/** Idle debounce in minutes (default 10); 0 fires on every turn. */
|
|
51
|
+
debounceMin?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Wire the extraction triggers onto the host context. Returns a disposer
|
|
55
|
+
* that removes the listeners and clears the timer. The scheduler is the
|
|
56
|
+
* only background automation the plugin runs, and it is bounded by design:
|
|
57
|
+
* one timer, one in-flight run, one pending slot.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createExtractionScheduler(host: SchedulerHost, deps: ExtractionDeps, options?: SchedulerOptions): () => void;
|
|
60
|
+
//# sourceMappingURL=extraction.d.ts.map
|