@team-harness/memory-algorithms 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.
Files changed (69) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +203 -0
  3. package/dist/contracts.d.ts +178 -0
  4. package/dist/contracts.js +1 -0
  5. package/dist/index.d.ts +14 -0
  6. package/dist/index.js +6 -0
  7. package/dist/runtime/documents.d.ts +15 -0
  8. package/dist/runtime/documents.js +209 -0
  9. package/dist/runtime/l1.d.ts +10 -0
  10. package/dist/runtime/l1.js +172 -0
  11. package/dist/runtime/run.d.ts +44 -0
  12. package/dist/runtime/run.js +191 -0
  13. package/dist/runtime/skill-workspace.d.ts +70 -0
  14. package/dist/runtime/skill-workspace.js +156 -0
  15. package/dist/runtime/skills.d.ts +9 -0
  16. package/dist/runtime/skills.js +48 -0
  17. package/dist/runtime/telemetry.d.ts +10 -0
  18. package/dist/runtime/telemetry.js +5 -0
  19. package/dist/runtime/tools.d.ts +15 -0
  20. package/dist/runtime/tools.js +5 -0
  21. package/dist/upstream/config.d.ts +1 -0
  22. package/dist/upstream/config.js +1 -0
  23. package/dist/upstream/core/conversation/l0-recorder.d.ts +6 -0
  24. package/dist/upstream/core/conversation/l0-recorder.js +1 -0
  25. package/dist/upstream/core/memory-prompt/composer.d.ts +6 -0
  26. package/dist/upstream/core/memory-prompt/composer.js +33 -0
  27. package/dist/upstream/core/memory-prompt/types.d.ts +103 -0
  28. package/dist/upstream/core/memory-prompt/types.js +21 -0
  29. package/dist/upstream/core/prompts/l1-dedup.d.ts +33 -0
  30. package/dist/upstream/core/prompts/l1-dedup.js +202 -0
  31. package/dist/upstream/core/prompts/l1-extraction.d.ts +24 -0
  32. package/dist/upstream/core/prompts/l1-extraction.js +400 -0
  33. package/dist/upstream/core/prompts/persona-generation.d.ts +29 -0
  34. package/dist/upstream/core/prompts/persona-generation.js +284 -0
  35. package/dist/upstream/core/prompts/scene-extraction.d.ts +40 -0
  36. package/dist/upstream/core/prompts/scene-extraction.js +534 -0
  37. package/dist/upstream/core/record/l1-dedup.d.ts +10 -0
  38. package/dist/upstream/core/record/l1-dedup.js +108 -0
  39. package/dist/upstream/core/record/l1-extractor.d.ts +33 -0
  40. package/dist/upstream/core/record/l1-extractor.js +128 -0
  41. package/dist/upstream/core/record/l1-writer.d.ts +95 -0
  42. package/dist/upstream/core/record/l1-writer.js +1 -0
  43. package/dist/upstream/core/scene/filename-normalizer.d.ts +6 -0
  44. package/dist/upstream/core/scene/filename-normalizer.js +30 -0
  45. package/dist/upstream/core/scene/scene-format.d.ts +26 -0
  46. package/dist/upstream/core/scene/scene-format.js +53 -0
  47. package/dist/upstream/core/scene/scene-index.d.ts +7 -0
  48. package/dist/upstream/core/scene/scene-index.js +1 -0
  49. package/dist/upstream/core/scene/scene-navigation.d.ts +66 -0
  50. package/dist/upstream/core/scene/scene-navigation.js +107 -0
  51. package/dist/upstream/core/skill/conversation-add/message-compressor.d.ts +47 -0
  52. package/dist/upstream/core/skill/conversation-add/message-compressor.js +58 -0
  53. package/dist/upstream/core/skill/conversation-add/oversize-strategy.d.ts +41 -0
  54. package/dist/upstream/core/skill/conversation-add/oversize-strategy.js +100 -0
  55. package/dist/upstream/core/skill/prompts/skill-review-prompt.d.ts +39 -0
  56. package/dist/upstream/core/skill/prompts/skill-review-prompt.js +197 -0
  57. package/dist/upstream/core/skill/skill-extractor.d.ts +146 -0
  58. package/dist/upstream/core/skill/skill-extractor.js +432 -0
  59. package/dist/upstream/core/skill/skill-format.d.ts +46 -0
  60. package/dist/upstream/core/skill/skill-format.js +191 -0
  61. package/dist/upstream/core/skill/skill-tools.d.ts +75 -0
  62. package/dist/upstream/core/skill/skill-tools.js +193 -0
  63. package/dist/upstream/core/skill/types.d.ts +324 -0
  64. package/dist/upstream/core/skill/types.js +7 -0
  65. package/dist/upstream/utils/sanitize.d.ts +96 -0
  66. package/dist/upstream/utils/sanitize.js +359 -0
  67. package/package.json +28 -0
  68. package/upstream/baseline.json +426 -0
  69. package/upstream/changes.md +81 -0
package/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Tencent is pleased to support the open source community by making TencentDB Agent Memory available.
2
+
3
+ Copyright (C) 2026 Tencent. All rights reserved.
4
+
5
+ TencentDB Agent Memory is licensed under the MIT.
6
+
7
+
8
+ Terms of the MIT:
9
+ --------------------------------------------------------------------
10
+ Permission is hereby granted, free of charge, to any person obtaining
11
+ a copy of this software and associated documentation files (the
12
+ "Software"), to deal in the Software without restriction, including
13
+ without limitation the rights to use, copy, modify, merge, publish,
14
+ distribute, sublicense, and/or sell copies of the Software, and to
15
+ permit persons to whom the Software is furnished to do so, subject to
16
+ the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be
19
+ included in all copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # @team-harness/memory-algorithms
2
+
3
+ 将对话逐层提取为原子记忆(L1)、场景经验(L2)、画像或工作准则(L3),也可从对话提取可复用的 Skill。独立 TypeScript / ESM 包,算法源自 [TencentDB-Agent-Memory](https://github.com/TencentCloud/TencentDB-Agent-Memory),无需启动其服务或数据库。
4
+
5
+ 你提供对话、模型调用和已有知识的读取接口;算法返回带版本、证据和运行记录的变更;你的应用决定何时分析、如何保存以及是否启用结果。
6
+
7
+ ## 安装
8
+
9
+ 要求 Node.js 22 或更高版本,使用 ESM 导入。
10
+
11
+ 使用 npm 安装:
12
+
13
+ ```sh
14
+ npm install @team-harness/memory-algorithms
15
+ ```
16
+
17
+ 也可以从有访问权限的源码仓库构建安装包:
18
+
19
+ ```sh
20
+ git clone https://github.com/team-harness/memory-algorithms.git
21
+ cd memory-algorithms
22
+ npm ci
23
+ npm pack
24
+ ```
25
+
26
+ 在你的应用目录安装生成的文件:
27
+
28
+ ```sh
29
+ npm install /path/to/memory-algorithms/team-harness-memory-algorithms-0.1.0.tgz
30
+ ```
31
+
32
+ ## 提取流程
33
+
34
+ | 接口 | 输入 | 返回的实体 |
35
+ | --- | --- | --- |
36
+ | `extractL1` | 对话消息,以及可选的历史记忆检索 | `Atom`:原子记忆,包含类型、内容及证据 |
37
+ | `extractL2` | L1 记忆和已有场景文档 | `Document`:按场景组织的经验 |
38
+ | `extractL3` | 场景文档、可选的已有画像及上次场景版本 | `Document`:画像或工作准则候选 |
39
+ | `extractSkills` | 对话消息,以及可选的已有 Skill 读取接口 | `Skill`:文档、描述及配套资源候选 |
40
+
41
+ 典型顺序为 `L0 对话 → L1 Atom → L2 Scenario → L3 Persona`。Skill 从对话直接提取,不必等待 L3。各接口独立调用,包内不自动调度整条流程,也不保存 L0。
42
+
43
+ L1/L2/L3 默认使用 `mode: "code"`,适合开发工作的事实、任务、方法和产物;`mode: "chat"` 用于个人画像、事件和指令等对话记忆。Skill 不接收 `mode`。
44
+
45
+ ## 第一次提取 L1
46
+
47
+ 下面的函数接收你实现的模型驱动,返回一次分析结果,不会写入数据库:
48
+
49
+ ```ts
50
+ import {
51
+ extractL1,
52
+ type Message,
53
+ type ModelDriver,
54
+ } from "@team-harness/memory-algorithms";
55
+
56
+ export async function analyzeConversation(model: ModelDriver) {
57
+ const messages: Message[] = [{
58
+ id: "message-1",
59
+ role: "user",
60
+ content: "发布前必须完成数据库迁移验收。",
61
+ timestamp: Date.now(),
62
+ evidence: [{ id: "archive/item-1", version: 2 }],
63
+ }];
64
+
65
+ return extractL1({ model }, {
66
+ runId: "analysis-1",
67
+ scopeKey: "host-1/project-1",
68
+ mode: "code",
69
+ messages,
70
+ });
71
+ }
72
+ ```
73
+
74
+ `messages` 按对话顺序传入。每条消息需要稳定的 `id`、毫秒时间戳和至少一个带版本的 `evidence` 引用。角色支持 `user`、`assistant`、`tool_call`、`tool_result`。引用应能定位到应用保存的不可变对话证据;已裁剪内容标记 `completeness: "truncated"`,证据引用也可携带 `hash` 和完整性标记。
75
+
76
+ `scopeKey` 是应用明确选择的分析范围,例如一个项目或一个团队。它不自动推导项目、组织或成员关系,也不代替权限校验。参与同一次分析的历史实体必须属于该范围。
77
+
78
+ ## 接入模型与检索
79
+
80
+ 所有接口的第一个参数都是 `Dependencies`。必填的 `model` 实现如下契约;包不绑定任何模型厂商 SDK:
81
+
82
+ ```ts
83
+ import type {
84
+ ModelDriver,
85
+ ModelRequest,
86
+ ModelResponse,
87
+ } from "@team-harness/memory-algorithms";
88
+
89
+ export function createModelDriver(
90
+ id: string,
91
+ complete: (request: ModelRequest) => Promise<ModelResponse>,
92
+ ): ModelDriver {
93
+ return { id, complete };
94
+ }
95
+ ```
96
+
97
+ 传入的 `complete` 负责一次模型 API 请求的协议转换:
98
+
99
+ - 请求包含 `systemPrompt`、`messages`、JSON Schema 工具定义 `tools`、`maxOutputTokens` 和 `signal`。
100
+ - 消息历史中的助手工具调用和工具结果必须保留调用 ID;结果消息使用 `toolCallId` 关联。
101
+ - 响应包含 `text`、可选的 `toolCalls` 和 `finishReason`。工具调用返回 `"tool_calls"`,正常结束返回 `"stop"`;`"length"` 或 `"error"` 会使该阶段失败。
102
+ - 工具调用的 `arguments` 应转换为解析后的参数对象。工具执行和多轮循环由本包负责,驱动只做模型传输。
103
+ - 传递取消信号,返回模型实际报告的 `usage`;无法取得用量时省略,不要填造假的零值。
104
+
105
+ 只有纯文本响应、无法保留工具调用的接口不足以支持全部阶段。
106
+
107
+ | 可选依赖 | 用途 | 应用需要提供 |
108
+ | --- | --- | --- |
109
+ | `memories` | L1 召回冲突候选、判断更新或合并 | `hybrid`,或 `fts` 与 `embed`/`vector` |
110
+ | `skills` | Skill 提取时查找和复用已有技能 | `list`、`search`、`get` |
111
+ | `now` / `createId` | 可控时间与 ID,便于回放或测试 | 返回当前时间 / 新 ID 的函数 |
112
+
113
+ 记忆检索优先使用原生 `hybrid`,否则通过 RRF(k=60)合并全文与向量排名。`fts` 接收自然语言查询,应用负责安全转换为数据库查询。没有 `memories` 时无法基于历史记忆做语义去重;检索降级记录在 `diagnostics`,应用应据此决定是否接受结果。
114
+
115
+ 读取接口必须执行权限过滤,返回匹配 `scopeKey` 的完整实体快照;同一实体在一次运行中保持版本一致。Skill 的 `writable` 必须来自实际权限。返回候选不等于获得正式写入授权。
116
+
117
+ ## 调用 L2、L3 与 Skill
118
+
119
+ 每个阶段使用已提交的当前实体快照。下面的函数展示调用参数;应用应在调用阶段之间完成保存与重新读取:
120
+
121
+ ```ts
122
+ import {
123
+ extractL2, extractL3, extractSkills,
124
+ type Atom, type Dependencies, type Document,
125
+ type Message, type VersionRef,
126
+ } from "@team-harness/memory-algorithms";
127
+
128
+ export function analyzeScenarios(
129
+ deps: Dependencies, runId: string, scopeKey: string,
130
+ memories: Atom[], scenes: Document[],
131
+ ) {
132
+ return extractL2(deps, { runId, scopeKey, memories, scenes });
133
+ }
134
+
135
+ export function analyzePersona(
136
+ deps: Dependencies, runId: string, scopeKey: string,
137
+ scenes: Document[], persona?: Document,
138
+ previousSceneVersions?: VersionRef[],
139
+ ) {
140
+ return extractL3(deps, {
141
+ runId, scopeKey, scenes, persona, previousSceneVersions,
142
+ });
143
+ }
144
+
145
+ export function analyzeSkills(
146
+ deps: Dependencies, runId: string, scopeKey: string,
147
+ messages: Message[],
148
+ ) {
149
+ return extractSkills(deps, { runId, scopeKey, messages });
150
+ }
151
+ ```
152
+
153
+ 首次提取 L2 时 `scenes` 传空数组,首次提取 L3 时省略 `persona`。后续传入已有快照。L3 成功提交时,一并保存本次输入的场景 `{ id, version }` 列表,下次作为 `previousSceneVersions` 传入,用于识别变化与删除;不传则重新评估现有场景。
154
+
155
+ ## 处理与保存结果
156
+
157
+ 四个接口统一返回 `ExtractionResult<T>`:
158
+
159
+ | 字段 | 应用如何使用 |
160
+ | --- | --- |
161
+ | `status` | `completed` 完成;`partial` 仅覆盖部分输入;`failed` / `cancelled` 不可提交 |
162
+ | `changes` | `create`、`update`、`merge`、`retire`、`skip`;`before` 是被替换的版本,`after` 是完整的新实体 |
163
+ | `readSet` | 提交前检查所读取的历史实体版本是否仍然有效 |
164
+ | `coverage` | 区分已处理、背景、过滤、裁剪和剩余输入 |
165
+ | `continuation` | L1 分批续跑位置,需与本批结果一起保存 |
166
+ | `diagnostics` | 失败原因、检索降级和输入裁剪等信息 |
167
+ | `provenance` | 算法与上游版本、模型、输入/提示词 hash、调用次数及 token 用量 |
168
+
169
+ 建议将版本校验、变更应用、证据与运行记录保存、分析进度更新放在同一事务中。按 `runId`、阶段和批次实现幂等;发生版本冲突时重新读取并分析。`completed` 也可能没有变更,表示没有需要沉淀的内容。
170
+
171
+ L1 的 `update` / `merge` 会产生新的 `after.id`,不是原 ID 就地更新:需要将所有 `before` 记录标为被替代,同时插入 `after` 并保留历史。Skill 一次运行可以经过多次编辑,最终版本可能增长多次,`before` 仍指向原持久版本。提交时还需重新检查权限及 Skill 名称/ID 唯一性。
172
+
173
+ 失败或取消时没有可提交变更,不推进分析进度。参数或依赖配置错误也可能直接抛出异常,调用层仍需捕获异常。L3 和 Skill 是候选,是否启用由应用决定。
174
+
175
+ L1 的证据按记录关联;L2/L3/Skill 使用保守的整次运行输入依赖,不是逐句引用。`provenance.evidenceGranularity` 标明 `record` 或 `run`;token 用量无法取得时为 `null`。
176
+
177
+ ## 分批、预算与取消
178
+
179
+ L1 默认每次处理最早的最多 10 条新消息,以之前最多 5 条合格消息作为背景,可通过 `maxNewMessages` / `maxBackgroundMessages` 调整。
180
+
181
+ 当 L1 返回 `partial` 和 `continuation` 时,先原子保存本批结果与续跑位置,再使用**同一份完整消息快照**和该 `continuation` 调用 `extractL1`。保持范围、模式、策略和窗口配置不变,下一批检索必须能读到上一批已提交的记忆。不要把续跑位置用于另一份输入或另一算法版本。
182
+
183
+ 每个阶段可传 `limits` 和 `signal`。主要默认预算如下:
184
+
185
+ | 参数 | 默认值 | 含义 |
186
+ | --- | --- | --- |
187
+ | `limits.maxCalls` | 20 | 模型调用上限 |
188
+ | `limits.maxIterations` | 16 | 工具循环迭代上限 |
189
+ | `limits.maxInputChars` | 120000 | 输入字符预算,不是 tokenizer 精确 token 数 |
190
+ | `limits.maxOutputTokens` | 8192 | 单次模型输出预算 |
191
+ | `limits.timeoutMs` | 180000 | 阶段超时毫秒数 |
192
+
193
+ 单个超大消息或场景放不进预算时会明确失败,不自动摘要或跳过。L2/L3 暂无自动分批续跑,应用需选择有界输入。
194
+
195
+ Skill 默认保留格式化对话的前 8000 / 后 32000 字符,可通过 `headChars` / `tailChars` 调整。发生截取会返回 `partial` 和诊断,但没有自动续跑位置;不能把它当作全量分析完成。包另导出 `compressMessage`、`compressMessages` 和 `applyOversizeStrategy`,仅供显式预处理,默认提取不叠加压缩。
196
+
197
+ ## 当前能力边界
198
+
199
+ 应用负责分析触发、存储、同步、权限和候选启用。本包的文档编辑工具只操作运行内存,不写应用文件或数据库;模型请求通过你提供的驱动执行,无默认遥测上传。
200
+
201
+ 当前算法配置为 `bounded-analysis-v1`。已完成原仓库差分、确定性演化回归,以及基于改写会话的初步真实模型评测。评测观察到模型格式错误、文档编辑成本偏高和 L3 过度推导,因此生成结果仍应作为候选审查,不能视为全面质量保证。详见 [验收结果](https://github.com/team-harness/memory-algorithms/blob/main/evaluation/results/2026-09-15.md)。
202
+
203
+ 完整类型随 npm 包提供,亦可查看 [API 类型](https://github.com/team-harness/memory-algorithms/blob/main/src/contracts.ts)。算法来源与适配差异见 [适配记录](upstream/changes.md)。保留上游版权,采用 [MIT 许可](LICENSE)。
@@ -0,0 +1,178 @@
1
+ import type { MemoryRecord } from "./upstream/core/record/l1-writer.js";
2
+ import type { ResolvedMemoryPrompt } from "./upstream/core/memory-prompt/types.js";
3
+ export type Stage = "l1" | "l2" | "l3" | "skill";
4
+ export type PromptMode = "chat" | "code";
5
+ export interface Logger {
6
+ debug?(message: string): void;
7
+ warn?(message: string): void;
8
+ }
9
+ export interface EvidenceRef {
10
+ id: string;
11
+ version: string | number;
12
+ hash?: string;
13
+ completeness?: "full" | "truncated";
14
+ }
15
+ export interface Message {
16
+ id: string;
17
+ role: "user" | "assistant" | "tool_call" | "tool_result";
18
+ content: string;
19
+ timestamp: number;
20
+ evidence: EvidenceRef[];
21
+ completeness?: "full" | "truncated";
22
+ }
23
+ export interface VersionRef {
24
+ id: string;
25
+ version: number;
26
+ }
27
+ export interface Atom extends MemoryRecord {
28
+ version: number;
29
+ scopeKey: string;
30
+ evidence: EvidenceRef[];
31
+ }
32
+ export interface Document {
33
+ id: string;
34
+ version: number;
35
+ scopeKey: string;
36
+ filename: string;
37
+ content: string;
38
+ evidence: EvidenceRef[];
39
+ }
40
+ export interface SkillResource {
41
+ path: string;
42
+ content: string;
43
+ encoding?: "utf-8" | "base64";
44
+ mime_type?: string;
45
+ is_executable?: boolean;
46
+ }
47
+ export interface Skill extends Document {
48
+ name: string;
49
+ description: string;
50
+ resources: SkillResource[];
51
+ writable: boolean;
52
+ }
53
+ export interface Change<T> {
54
+ action: "create" | "update" | "merge" | "retire" | "skip";
55
+ before: VersionRef[];
56
+ after?: T;
57
+ reason?: string;
58
+ }
59
+ export interface ToolCall {
60
+ id: string;
61
+ name: string;
62
+ arguments: unknown;
63
+ }
64
+ export interface ModelMessage {
65
+ role: "user" | "assistant" | "tool";
66
+ content: string;
67
+ toolCalls?: ToolCall[];
68
+ toolCallId?: string;
69
+ }
70
+ export interface ToolDefinition {
71
+ name: string;
72
+ description: string;
73
+ inputSchema: Record<string, unknown>;
74
+ }
75
+ export interface ModelRequest {
76
+ stage: Stage;
77
+ systemPrompt: string;
78
+ messages: ModelMessage[];
79
+ tools: ToolDefinition[];
80
+ maxOutputTokens: number;
81
+ signal: AbortSignal;
82
+ }
83
+ export interface ModelResponse {
84
+ text: string;
85
+ toolCalls?: ToolCall[];
86
+ finishReason: "stop" | "tool_calls" | "length" | "error";
87
+ usage?: {
88
+ inputTokens: number;
89
+ outputTokens: number;
90
+ };
91
+ }
92
+ export interface ModelDriver {
93
+ id: string;
94
+ complete(request: ModelRequest): Promise<ModelResponse>;
95
+ }
96
+ export interface Limits {
97
+ maxCalls: number;
98
+ maxIterations: number;
99
+ maxInputChars: number;
100
+ maxOutputTokens: number;
101
+ timeoutMs: number;
102
+ maxToolCalls: number;
103
+ maxWorkspaceChars: number;
104
+ maxRecallQueries: number;
105
+ maxExtractedMemories: number;
106
+ }
107
+ export interface RunInput {
108
+ runId: string;
109
+ scopeKey: string;
110
+ mode?: PromptMode;
111
+ signal?: AbortSignal;
112
+ limits?: Partial<Limits>;
113
+ strategy?: ResolvedMemoryPrompt;
114
+ }
115
+ export interface Continuation {
116
+ algorithmVersion: string;
117
+ inputHash: string;
118
+ nextIndex: number;
119
+ previousSceneName?: string;
120
+ }
121
+ export interface Coverage {
122
+ processed: string[];
123
+ background: string[];
124
+ filtered: string[];
125
+ truncated: string[];
126
+ remaining: string[];
127
+ }
128
+ export interface ExtractionResult<T> {
129
+ status: "completed" | "partial" | "failed" | "cancelled";
130
+ changes: Change<T>[];
131
+ readSet: VersionRef[];
132
+ coverage: Coverage;
133
+ continuation?: Continuation;
134
+ diagnostics: string[];
135
+ provenance: {
136
+ algorithmVersion: string;
137
+ upstreamCommit: string;
138
+ profile: "bounded-analysis-v1";
139
+ evidenceGranularity: "record" | "run";
140
+ runId: string;
141
+ scopeKey: string;
142
+ stage: Stage;
143
+ model: string;
144
+ inputHash: string;
145
+ promptHashes: string[];
146
+ calls: number;
147
+ usage: {
148
+ inputTokens: number;
149
+ outputTokens: number;
150
+ } | null;
151
+ };
152
+ sceneNames?: string[];
153
+ personaUpdateReason?: string;
154
+ }
155
+ export interface MemorySearch {
156
+ hybrid?(query: string, scopeKey: string, topK: number, signal?: AbortSignal): Promise<Atom[]>;
157
+ fts?(query: string, scopeKey: string, topK: number, signal?: AbortSignal): Promise<Atom[]>;
158
+ embed?(query: string, signal?: AbortSignal): Promise<number[]>;
159
+ vector?(embedding: number[], scopeKey: string, topK: number, signal?: AbortSignal): Promise<Atom[]>;
160
+ }
161
+ export interface SkillReader {
162
+ list(scopeKey: string, limit: number, signal?: AbortSignal): Promise<{
163
+ items: Skill[];
164
+ total: number;
165
+ }>;
166
+ search(scopeKey: string, query: string, limit: number, signal?: AbortSignal): Promise<Array<{
167
+ skill: Skill;
168
+ score: number;
169
+ }>>;
170
+ get(scopeKey: string, id: string, signal?: AbortSignal): Promise<Skill>;
171
+ }
172
+ export interface Dependencies {
173
+ model: ModelDriver;
174
+ memories?: MemorySearch;
175
+ skills?: SkillReader;
176
+ now?: () => Date;
177
+ createId?: () => string;
178
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export type * from "./contracts.js";
2
+ export type { MemoryType } from "./upstream/core/record/l1-writer.js";
3
+ export type { ResolvedMemoryPrompt } from "./upstream/core/memory-prompt/types.js";
4
+ export { extractL1 } from "./runtime/l1.js";
5
+ export type { L1Input } from "./runtime/l1.js";
6
+ export { extractL2, extractL3 } from "./runtime/documents.js";
7
+ export type { L2Input, L3Input } from "./runtime/documents.js";
8
+ export { extractSkills } from "./runtime/skills.js";
9
+ export type { SkillInput } from "./runtime/skills.js";
10
+ export { ALGORITHM_VERSION, UPSTREAM_COMMIT } from "./runtime/run.js";
11
+ export { compressMessage, compressMessages } from "./upstream/core/skill/conversation-add/message-compressor.js";
12
+ export type { CompressibleMessage, CompressOptions } from "./upstream/core/skill/conversation-add/message-compressor.js";
13
+ export { applyOversizeStrategy } from "./upstream/core/skill/conversation-add/oversize-strategy.js";
14
+ export type { OversizeMessage, OversizeOptions, OversizeResult } from "./upstream/core/skill/conversation-add/oversize-strategy.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { extractL1 } from "./runtime/l1.js";
2
+ export { extractL2, extractL3 } from "./runtime/documents.js";
3
+ export { extractSkills } from "./runtime/skills.js";
4
+ export { ALGORITHM_VERSION, UPSTREAM_COMMIT } from "./runtime/run.js";
5
+ export { compressMessage, compressMessages } from "./upstream/core/skill/conversation-add/message-compressor.js";
6
+ export { applyOversizeStrategy } from "./upstream/core/skill/conversation-add/oversize-strategy.js";
@@ -0,0 +1,15 @@
1
+ import type { Atom, Dependencies, Document, ExtractionResult, RunInput, VersionRef } from "../contracts.js";
2
+ export interface L2Input extends RunInput {
3
+ memories: Atom[];
4
+ scenes: Document[];
5
+ maxScenes?: number;
6
+ }
7
+ export declare function extractL2(deps: Dependencies, input: L2Input): Promise<ExtractionResult<Document>>;
8
+ export interface L3Input extends RunInput {
9
+ scenes: Document[];
10
+ persona?: Document;
11
+ previousSceneVersions?: VersionRef[];
12
+ totalProcessed?: number;
13
+ triggerReason?: string;
14
+ }
15
+ export declare function extractL3(deps: Dependencies, input: L3Input): Promise<ExtractionResult<Document>>;
@@ -0,0 +1,209 @@
1
+ import { buildSceneExtractionPrompt } from "../upstream/core/prompts/scene-extraction.js";
2
+ import { buildPersonaPrompt } from "../upstream/core/prompts/persona-generation.js";
3
+ import { parseSceneBlock } from "../upstream/core/scene/scene-format.js";
4
+ import { normalizeSceneFilename } from "../upstream/core/scene/filename-normalizer.js";
5
+ import { renderSceneNavigation, storageNavFooter, stripSceneNavigation } from "../upstream/core/scene/scene-navigation.js";
6
+ import { escapeXmlTags } from "../upstream/utils/sanitize.js";
7
+ import { composeMemorySystemPrompt } from "../upstream/core/memory-prompt/composer.js";
8
+ import { Run, coverage, hash, uniqueRefs } from "./run.js";
9
+ import { safePath } from "./skill-workspace.js";
10
+ import { tool, jsonSchema } from "./tools.js";
11
+ class DocumentWorkspace {
12
+ run;
13
+ persona;
14
+ files = new Map();
15
+ written = new Set();
16
+ constructor(run, documents, persona = false) {
17
+ this.run = run;
18
+ this.persona = persona;
19
+ for (const d of documents) {
20
+ this.path(d.filename);
21
+ if (this.files.has(d.filename))
22
+ throw new Error("Duplicate document filename");
23
+ this.files.set(d.filename, d.content);
24
+ }
25
+ this.checkSize();
26
+ }
27
+ path(path) {
28
+ safePath(path);
29
+ if (path.includes("/") || !path.endsWith(".md") || (this.persona && path !== "persona.md"))
30
+ throw new Error("Document path outside workspace");
31
+ }
32
+ checkSize() {
33
+ if ([...this.files.values()].reduce((n, s) => n + s.length, 0) > this.run.limits.maxWorkspaceChars)
34
+ throw new Error("Document workspace budget exhausted");
35
+ }
36
+ put(path, content) {
37
+ this.run.check();
38
+ this.path(path);
39
+ if (!content.trim())
40
+ throw new Error("Empty document write");
41
+ const old = this.files.get(path);
42
+ this.files.set(path, content);
43
+ try {
44
+ this.checkSize();
45
+ }
46
+ catch (error) {
47
+ if (old === undefined)
48
+ this.files.delete(path);
49
+ else
50
+ this.files.set(path, old);
51
+ throw error;
52
+ }
53
+ this.written.add(path);
54
+ return JSON.stringify({ ok: true, path });
55
+ }
56
+ tools() {
57
+ return {
58
+ read: tool({ description: "Read a document in the current workspace.", inputSchema: jsonSchema({ type: "object", properties: { path: { type: "string" } }, required: ["path"], additionalProperties: false }), execute: async ({ path }) => {
59
+ this.path(path);
60
+ const value = this.files.get(path);
61
+ if (value === undefined)
62
+ throw new Error("Document not found");
63
+ return value;
64
+ } }),
65
+ write: tool({ description: "Write a complete Markdown document.", inputSchema: jsonSchema({ type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"], additionalProperties: false }), execute: async ({ path, content }) => this.put(path, content) }),
66
+ edit: tool({ description: "Apply exact, unique text replacements atomically.", inputSchema: jsonSchema({ type: "object", properties: { path: { type: "string" }, edits: { type: "array", minItems: 1, items: { type: "object", properties: { oldText: { type: "string", minLength: 1 }, newText: { type: "string" } }, required: ["oldText", "newText"], additionalProperties: false } } }, required: ["path", "edits"], additionalProperties: false }), execute: async ({ path, edits }) => {
67
+ this.path(path);
68
+ let content = this.files.get(path);
69
+ if (content === undefined)
70
+ throw new Error("Document not found");
71
+ for (const edit of edits) {
72
+ if (content.split(edit.oldText).length !== 2)
73
+ throw new Error("Edit target missing or ambiguous");
74
+ content = content.replace(edit.oldText, () => edit.newText);
75
+ }
76
+ return this.put(path, content);
77
+ } }),
78
+ };
79
+ }
80
+ }
81
+ function index(documents) {
82
+ return documents.map(d => ({ filename: d.filename, ...parseSceneBlock(d.content, d.filename).meta }));
83
+ }
84
+ export async function extractL2(deps, input) {
85
+ input = { ...input, memories: structuredClone(input.memories), scenes: structuredClone(input.scenes), strategy: structuredClone(input.strategy) };
86
+ const run = new Run(deps, input, "l2", hash({ scope: input.scopeKey, mode: input.mode, memories: input.memories, scenes: input.scenes, maxScenes: input.maxScenes, strategy: input.strategy }));
87
+ const cov = coverage(input.memories.map(m => m.id));
88
+ try {
89
+ run.check();
90
+ const memories = input.memories.map(m => run.read(m));
91
+ const scenes = input.scenes.map(s => run.read(s));
92
+ if (!memories.length)
93
+ return run.result([], cov);
94
+ const maxScenes = input.maxScenes ?? 15;
95
+ if (!Number.isSafeInteger(maxScenes) || maxScenes < 2)
96
+ throw new Error("Invalid scene limit");
97
+ const entries = index(scenes);
98
+ // Adapted from SceneExtractor: tiered pressure to merge before adding scenes.
99
+ const warning = entries.length >= maxScenes ? `当前场景数量为 **${entries.length} 个**,已达到或超过 ${maxScenes} 个上限!\n**你必须先执行 MERGE 操作**,将最相似的 2-4 个场景合并为 1 个,然后再处理新记忆。\n参考合并对象:热度最低或主题高度重叠的场景。`
100
+ : entries.length === maxScenes - 1 ? `当前场景数量为 **${entries.length} 个**,距离上限只差 1 个!\n本次处理**只能 UPDATE 现有场景,不能 CREATE 新场景**。`
101
+ : entries.length >= maxScenes - 3 ? `当前场景数量为 **${entries.length} 个**,建议优先考虑 UPDATE 或主动 MERGE 相似场景。` : undefined;
102
+ const summaries = entries.length ? [`**当前场景总数:${entries.length} / ${maxScenes}**`, "",
103
+ ...entries.flatMap(e => [`### ${e.filename}`, `**热度**: ${e.heat} | **更新**: ${e.updated}`, `**summary**: ${e.summary}`, ""])].join("\n") : "";
104
+ const prompts = buildSceneExtractionPrompt({ memoriesJson: JSON.stringify(memories.map(m => ({ content: m.content, created_at: m.createdAt, id: m.id })), null, 2),
105
+ sceneSummaries: summaries || "(无已有场景)", currentTimestamp: run.time,
106
+ sceneCountWarning: warning, existingSceneFiles: entries.map(e => e.filename), maxScenes, promptMode: input.mode ?? "code" });
107
+ const workspace = new DocumentWorkspace(run, scenes);
108
+ const output = await run.model(composeMemorySystemPrompt(prompts.systemPrompt, input.strategy), prompts.userPrompt, workspace.tools());
109
+ const normalized = new Map();
110
+ for (const [name, content] of workspace.files) {
111
+ const parsed = parseSceneBlock(content, name);
112
+ if (content.trim() === "[DELETED]" || !parsed.content || parsed.content.trim() === "[DELETED]")
113
+ continue;
114
+ const base = normalizeSceneFilename(name);
115
+ let filename = base, suffix = 2;
116
+ while (normalized.has(filename) || (filename !== name && workspace.files.has(filename)))
117
+ filename = `${base.slice(0, -3)}-${suffix++}.md`;
118
+ normalized.set(filename, { oldName: name, content });
119
+ }
120
+ if (normalized.size > maxScenes)
121
+ throw new Error("Scene count exceeds the configured maximum");
122
+ const dependencies = uniqueRefs([...memories.flatMap(m => m.evidence), ...scenes.flatMap(s => s.evidence)]);
123
+ const changes = [];
124
+ const kept = new Set();
125
+ for (const [filename, file] of normalized) {
126
+ const old = scenes.find(s => s.filename === file.oldName);
127
+ if (old)
128
+ kept.add(old.id);
129
+ if (old?.content === file.content && old.filename === filename)
130
+ continue;
131
+ changes.push({ action: old ? "update" : "create", before: old ? [{ id: old.id, version: old.version }] : [], after: {
132
+ id: old?.id ?? run.id(), version: (old?.version ?? 0) + 1, scopeKey: input.scopeKey,
133
+ filename, content: file.content, evidence: old?.content === file.content ? old.evidence : dependencies,
134
+ } });
135
+ }
136
+ for (const old of scenes)
137
+ if (!kept.has(old.id))
138
+ changes.push({ action: "retire", before: [{ id: old.id, version: old.version }], reason: workspace.files.get(old.filename)?.trim() === "[DELETED]" ? "explicit-deletion" : "empty-scene-body" });
139
+ const newIds = changes.flatMap(c => c.after ? [c.after.id] : []);
140
+ if (new Set(newIds).size !== newIds.length || changes.some(c => c.action === "create" && scenes.some(s => s.id === c.after?.id)))
141
+ throw new Error("Duplicate generated document ID");
142
+ cov.processed = cov.remaining;
143
+ cov.remaining = [];
144
+ run.check();
145
+ const result = run.result(changes, cov);
146
+ result.personaUpdateReason = output.match(/\[PERSONA_UPDATE_REQUEST\]\s*(?:reason:\s*)?(.+?)\s*\[\/PERSONA_UPDATE_REQUEST\]/s)?.[1]?.trim()
147
+ ?? output.match(/PERSONA_UPDATE_REQUEST:\s*(.+?)(?:\n|$)/)?.[1]?.trim();
148
+ return result;
149
+ }
150
+ catch (error) {
151
+ return run.failure(error, cov);
152
+ }
153
+ }
154
+ export async function extractL3(deps, input) {
155
+ input = { ...input, scenes: structuredClone(input.scenes), persona: structuredClone(input.persona), strategy: structuredClone(input.strategy) };
156
+ const run = new Run(deps, input, "l3", hash({ scope: input.scopeKey, mode: input.mode, scenes: input.scenes, persona: input.persona, previousSceneVersions: input.previousSceneVersions, strategy: input.strategy }));
157
+ const cov = coverage(input.scenes.map(s => s.id));
158
+ try {
159
+ run.check();
160
+ const scenes = input.scenes.map(s => run.read(s));
161
+ const persona = input.persona ? run.read(input.persona) : undefined;
162
+ const previous = new Map(input.previousSceneVersions?.map(s => [s.id, s.version]));
163
+ const removed = [...previous.keys()].some(id => !scenes.some(s => s.id === id));
164
+ const changed = !persona || removed ? scenes : scenes.filter(s => previous.get(s.id) !== s.version);
165
+ if (!changed.length && !removed && persona) {
166
+ cov.processed = cov.remaining;
167
+ cov.remaining = [];
168
+ return run.result([], cov);
169
+ }
170
+ if (!scenes.length && !persona) {
171
+ cov.processed = [];
172
+ cov.remaining = [];
173
+ return run.result([], cov);
174
+ }
175
+ const existing = persona ? stripSceneNavigation(persona.content).trim() : undefined;
176
+ const changedContent = changed.map((s, i) => `### [${i + 1}] ${s.filename}\n\n\`\`\`markdown\n${s.content}\n\`\`\``).join("\n\n");
177
+ const changedScenesContent = changed.length
178
+ ? `\n\n## 📄 变化场景完整内容\n\n` +
179
+ `*自上次 Persona 更新后,以下 ${changed.length} 个场景发生了变化。工程已为你预加载完整内容:*\n\n` + changedContent +
180
+ `\n\n---\n\n` + `⚠️ **重点分析变化场景**:上述场景是自上次更新后的**新增/修改内容**,请**重点分析**这些场景中的新信息。\n`
181
+ : `\n\n⚠️ **无变化场景**:所有场景均已在上次 Persona 更新中分析过,本次可直接读取所有场景进行全局审视。\n`;
182
+ const prompts = buildPersonaPrompt({ mode: existing ? "incremental" : "first", promptMode: input.mode ?? "code", currentTime: run.time,
183
+ totalProcessed: input.totalProcessed ?? 0, sceneCount: scenes.length, changedSceneCount: changed.length,
184
+ changedScenesContent: removed ? `Previously used scenes were removed. Reassess unsupported conclusions.\n${changedScenesContent}` : changedScenesContent,
185
+ existingPersona: existing, triggerInfo: input.triggerReason, personaFilePath: "persona.md", checkpointPath: "" });
186
+ const workspace = new DocumentWorkspace(run, persona ? [{ ...persona, filename: "persona.md", content: existing ?? "" }] : [], true);
187
+ const { write, edit } = workspace.tools();
188
+ await run.model(composeMemorySystemPrompt(prompts.systemPrompt, input.strategy), prompts.userPrompt, { write, edit });
189
+ if (!workspace.written.has("persona.md"))
190
+ throw new Error("Model did not write persona.md");
191
+ const body = escapeXmlTags(stripSceneNavigation(workspace.files.get("persona.md")).trim());
192
+ if (!body)
193
+ throw new Error("Empty persona output");
194
+ const navigation = escapeXmlTags(renderSceneNavigation(index(scenes), { pathFor: e => `scene_blocks/${e.filename}`, readTool: "read", footer: storageNavFooter("read") }));
195
+ const content = navigation ? `${body}\n\n${navigation}\n` : body;
196
+ const changes = content === persona?.content ? [] : [{ action: persona ? "update" : "create",
197
+ before: persona ? [{ id: persona.id, version: persona.version }] : [], after: {
198
+ id: persona?.id ?? run.id(), version: (persona?.version ?? 0) + 1, scopeKey: input.scopeKey,
199
+ filename: "persona.md", content, evidence: uniqueRefs([...scenes.flatMap(s => s.evidence), ...(persona?.evidence ?? [])]),
200
+ } }];
201
+ cov.processed = cov.remaining;
202
+ cov.remaining = [];
203
+ run.check();
204
+ return run.result(changes, cov);
205
+ }
206
+ catch (error) {
207
+ return run.failure(error, cov);
208
+ }
209
+ }