@yandy0725/memory-core 1.0.1 → 2.0.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 (53) hide show
  1. package/README.md +77 -11
  2. package/README.zh.md +66 -10
  3. package/dist/config.d.ts +40 -22
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +36 -36
  6. package/dist/config.js.map +1 -1
  7. package/dist/index.d.ts +10 -9
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +5 -5
  10. package/dist/index.js.map +1 -1
  11. package/dist/injection.d.ts +9 -1
  12. package/dist/injection.d.ts.map +1 -1
  13. package/dist/injection.js +10 -3
  14. package/dist/injection.js.map +1 -1
  15. package/dist/orchestration/conversation.d.ts +1 -1
  16. package/dist/orchestration/dream.d.ts +3 -30
  17. package/dist/orchestration/dream.d.ts.map +1 -1
  18. package/dist/orchestration/dream.js +32 -57
  19. package/dist/orchestration/dream.js.map +1 -1
  20. package/dist/orchestration/extract.d.ts +8 -38
  21. package/dist/orchestration/extract.d.ts.map +1 -1
  22. package/dist/orchestration/extract.js +43 -31
  23. package/dist/orchestration/extract.js.map +1 -1
  24. package/dist/orchestration/ports.d.ts +41 -4
  25. package/dist/orchestration/ports.d.ts.map +1 -1
  26. package/dist/orchestration/status.d.ts +19 -0
  27. package/dist/orchestration/status.d.ts.map +1 -0
  28. package/dist/orchestration/status.js +23 -0
  29. package/dist/orchestration/status.js.map +1 -0
  30. package/dist/orchestration/surfacing.d.ts +9 -21
  31. package/dist/orchestration/surfacing.d.ts.map +1 -1
  32. package/dist/orchestration/surfacing.js +31 -35
  33. package/dist/orchestration/surfacing.js.map +1 -1
  34. package/dist/orchestration/task.d.ts +84 -0
  35. package/dist/orchestration/task.d.ts.map +1 -0
  36. package/dist/orchestration/task.js +145 -0
  37. package/dist/orchestration/task.js.map +1 -0
  38. package/dist/orchestration/tasks.d.ts +18 -0
  39. package/dist/orchestration/tasks.d.ts.map +1 -0
  40. package/dist/orchestration/tasks.js +16 -0
  41. package/dist/orchestration/tasks.js.map +1 -0
  42. package/package.json +1 -1
  43. package/src/config.ts +58 -46
  44. package/src/index.ts +19 -9
  45. package/src/injection.ts +18 -4
  46. package/src/orchestration/conversation.ts +1 -1
  47. package/src/orchestration/dream.ts +33 -75
  48. package/src/orchestration/extract.ts +50 -65
  49. package/src/orchestration/ports.ts +29 -4
  50. package/src/orchestration/status.ts +37 -0
  51. package/src/orchestration/surfacing.ts +45 -45
  52. package/src/orchestration/task.ts +236 -0
  53. package/src/orchestration/tasks.ts +22 -0
@@ -0,0 +1,236 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import {
4
+ requiredModel,
5
+ type MemoryConfig,
6
+ type ModelTask,
7
+ type SessionPersistenceConfig,
8
+ } from "../config.js";
9
+ import { BACKUP_DIR, type MemoryStore } from "../store/memory-store.js";
10
+ import { createSnapshot } from "../store/snapshot.js";
11
+ import { isReservedWindowsName } from "../store/windows-names.js";
12
+ import type { MemoryAction } from "../tool/actions.js";
13
+ import type {
14
+ HeadlessOutcome,
15
+ HeadlessRunRequest,
16
+ HostTranscriptPort,
17
+ IncompleteOutcome,
18
+ OrchestrationPorts,
19
+ } from "./ports.js";
20
+
21
+ /** 任务描述符里的编排骨架:全是 core 常量;两个 skip 由 `lock` 派生,不再单独编码。 */
22
+ export interface TaskPlan {
23
+ maxTurns?: number;
24
+ timeoutMs: number;
25
+ /** 注入子会话的 memory 工具 action 集;`null` = 不注入。三个任务的 builtinTools 恒为 "none"。 */
26
+ memoryTool: readonly MemoryAction[] | null;
27
+ /** 互斥与快照:none / 整轮逻辑锁 / 逻辑锁 + 进入时全目录快照。 */
28
+ lock: "none" | "logical" | "logical+snapshot";
29
+ /** 端口抛错(传输级失败)时怎么办:surfacing 吞、extract 与 dream 上抛。 */
30
+ errorPolicy: "swallow" | "rethrow";
31
+ }
32
+
33
+ /** 任务需要的 core 侧原料:store 恒有;transcript 只有 extract 用。 */
34
+ export interface TaskDeps {
35
+ store: MemoryStore;
36
+ transcript?: HostTranscriptPort;
37
+ }
38
+
39
+ /** 一次 `runTask` 调用的上下文:config 由 core 自己消费,适配层不再把它摊平成参数。 */
40
+ export interface TaskContext {
41
+ cfg: MemoryConfig;
42
+ cwd: string;
43
+ store: MemoryStore;
44
+ ports: OrchestrationPorts;
45
+ signal?: AbortSignal;
46
+ }
47
+
48
+ /** `buildRequest` 的宿主侧参数:`cfg` 的值与 plan 一起在这里合流成端口请求。 */
49
+ export interface RequestHost {
50
+ /** 子会话 cwd;三个任务恒为 memoryDir。 */
51
+ cwd: string;
52
+ /** 宿主自己的模型标识,已由 `runTask` 从 `cfg` 解析(`requiredModel`)。 */
53
+ model: string;
54
+ /** 宿主自己的思考/推理档位,core 原样透传。 */
55
+ effort?: string;
56
+ /** 宿主的会话持久化开关,core 原样透传。 */
57
+ sessionPersistence?: SessionPersistenceConfig;
58
+ signal?: AbortSignal;
59
+ }
60
+
61
+ /** `memory` 工具的写入回调;只有实现了 `memoryToolHooks` 的描述符会产出它。 */
62
+ export type MemoryToolOnWrite = (info: { action: MemoryAction; name?: string }) => void;
63
+
64
+ /** `notCompleted` 收的 `why`:core 自己的判定,或端口给的非 completed 结果。 */
65
+ export type Incomplete =
66
+ | { status: "no-input" }
67
+ | { status: "lock-busy" }
68
+ | IncompleteOutcome;
69
+
70
+ export interface MemoryTask<I, R> {
71
+ readonly id: ModelTask;
72
+ readonly plan: TaskPlan;
73
+ /** `null` = core 自己判定这一轮不跑(候选为空、消息为空)。预算值自己从 `cfg` 读。 */
74
+ buildPrompt(cfg: MemoryConfig, deps: TaskDeps, input: I): string | null;
75
+ /** 回复 → 结果。必须防御式:dsh 的 stream 通道拿不到强制结构化输出。 */
76
+ parse(cfg: MemoryConfig, deps: TaskDeps, input: I, text: string): R | Promise<R>;
77
+ /** core 没跑、或端口没跑成时怎么呈现(三个任务词汇不同的唯一合法处)。 */
78
+ notCompleted(cfg: MemoryConfig, deps: TaskDeps, input: I, why: Incomplete): R;
79
+ /** 只有 extractMemories 实现:把 `input.onWrite` 透给子会话的工具。 */
80
+ memoryToolHooks?(input: I): { onWrite?: MemoryToolOnWrite } | undefined;
81
+ }
82
+
83
+ /**
84
+ * 构造端口请求:plan 的取值与 `runTask` 从 `cfg` 解析出的宿主参数在这里合流。
85
+ *
86
+ * 只收构造请求真正需要的入参,`cfg` 与 `TaskContext` 都不进来:解析留在 `runTask`
87
+ * (model / effort / sessionPersistence 都从 `cfg` 读),请求形状因此只在这里写一遍。
88
+ */
89
+ export function buildRequest(
90
+ task: MemoryTask<unknown, unknown>,
91
+ prompt: string,
92
+ host: RequestHost,
93
+ onWrite?: MemoryToolOnWrite,
94
+ ): HeadlessRunRequest {
95
+ const plan = task.plan;
96
+ return {
97
+ task: prompt,
98
+ cwd: host.cwd,
99
+ model: host.model,
100
+ effort: host.effort,
101
+ maxTurns: plan.maxTurns,
102
+ timeoutMs: plan.timeoutMs,
103
+ signal: host.signal,
104
+ // 三个任务的 builtinTools 恒为 "none":侧查询只要一个 JSON,dream / extract 只走 memory 工具。
105
+ builtinTools: "none",
106
+ memoryTool:
107
+ plan.memoryTool === null
108
+ ? null
109
+ : {
110
+ actions: plan.memoryTool,
111
+ skipLogicalLock: plan.lock !== "none",
112
+ skipSnapshot: plan.lock === "logical+snapshot",
113
+ onWrite,
114
+ },
115
+ // 继承规则(per-task ?? defaults)由调用方在传参前解析完,这里只做透传。
116
+ sessionPersistence: host.sessionPersistence,
117
+ };
118
+ }
119
+
120
+ /** 会话持久化的继承规则:per-task ?? `defaults`。config 消费点收在 core,适配层不再各写一份。 */
121
+ function resolveSessionPersistence(cfg: MemoryConfig, task: ModelTask) {
122
+ return cfg[task]?.sessionPersistence ?? cfg.defaults?.sessionPersistence;
123
+ }
124
+
125
+ /** 「宿主/库抛出来的任意值」→ 一句可读文本。core 不能 import 适配层的 errors.ts(中立性约束),
126
+ * 这里只保留 `not-run.detail` 需要的最小口径:文本、Error、普通对象 `.message`,再退到 JSON / 兜底。 */
127
+ function errorMessage(error: unknown): string {
128
+ if (typeof error === "string") return error;
129
+ if (error instanceof Error) return error.message;
130
+ if (typeof error === "object" && error !== null) {
131
+ const message = (error as { message?: unknown }).message;
132
+ if (typeof message === "string") return message;
133
+ }
134
+ try {
135
+ const json = JSON.stringify(error);
136
+ return typeof json === "string" ? json : "unknown error";
137
+ } catch {
138
+ return "unknown error";
139
+ }
140
+ }
141
+
142
+ /** 进入时快照要拍的目录条目:跳过 dotfile;win32 上再跳过保留设备名(CON 等)。 */
143
+ async function snapshotFiles(memoryDir: string, platform: NodeJS.Platform = process.platform): Promise<string[]> {
144
+ const entries = await readdir(memoryDir, { withFileTypes: true }).catch(() => []);
145
+ return (
146
+ entries
147
+ .filter((entry) => entry.isFile() && !entry.name.startsWith("."))
148
+ // win32:保留设备名文件按名打开会命中设备(CON 等),`createSnapshot` 的 cp 会失败;
149
+ // dream 是 fail-closed 的,一次 cp 失败就打断整轮(与 `MemoryStore` 的 `#entryFiles` 同一契约)。
150
+ .filter((entry) => !(platform === "win32" && isReservedWindowsName(entry.name)))
151
+ .map((entry) => entry.name)
152
+ .sort()
153
+ );
154
+ }
155
+
156
+ /**
157
+ * 三个 LLM 任务的通用编排。顺序是契约:
158
+ *
159
+ * 1. 读模型(缺模型在这里抛可读错误,而不是发出 `model: ""` 的请求);
160
+ * 2. 读 effort / sessionPersistence;
161
+ * 3. `buildPrompt`,`null` → `notCompleted({ status: "no-input" })`(在**任何锁之前**);
162
+ * 4. 施加 `plan.lock`(`logical` 拿不到锁 → `notCompleted({ status: "lock-busy" })`);
163
+ * 5. 端口调用(传输级异常按 `plan.errorPolicy` 收口);
164
+ * 6. `completed` → `parse`;其余 → `notCompleted(outcome)`。
165
+ */
166
+ export async function runTask<I, R>(task: MemoryTask<I, R>, input: I, ctx: TaskContext): Promise<R> {
167
+ const cfg = ctx.cfg;
168
+ const deps: TaskDeps = { store: ctx.store, transcript: ctx.ports.transcript };
169
+
170
+ const model = requiredModel(cfg, task.id);
171
+ const effort = cfg[task.id]?.thinkLevel;
172
+ const sessionPersistence = resolveSessionPersistence(cfg, task.id);
173
+
174
+ const prompt = task.buildPrompt(cfg, deps, input);
175
+ if (prompt === null) return task.notCompleted(cfg, deps, input, { status: "no-input" });
176
+
177
+ const run = async (): Promise<R> => {
178
+ const request = buildRequest(
179
+ task,
180
+ prompt,
181
+ { cwd: ctx.cwd, model, effort, sessionPersistence, signal: ctx.signal },
182
+ task.memoryToolHooks?.(input)?.onWrite,
183
+ );
184
+
185
+ let outcome: HeadlessOutcome;
186
+ try {
187
+ outcome = await ctx.ports.headless.run(request);
188
+ } catch (e) {
189
+ if (task.plan.errorPolicy === "swallow") {
190
+ return task.notCompleted(cfg, deps, input, {
191
+ status: "not-run",
192
+ cause: "host-refused",
193
+ detail: errorMessage(e),
194
+ });
195
+ }
196
+ throw e;
197
+ }
198
+ if (outcome.status === "completed") return await task.parse(cfg, deps, input, outcome.text);
199
+ return task.notCompleted(cfg, deps, input, outcome);
200
+ };
201
+
202
+ switch (task.plan.lock) {
203
+ case "none":
204
+ return run();
205
+ case "logical": {
206
+ const result = await ctx.store.tryWithLogicalLock(run);
207
+ if (result === null) return task.notCompleted(cfg, deps, input, { status: "lock-busy" });
208
+ return result;
209
+ }
210
+ case "logical+snapshot": {
211
+ // 只有 dream 用这档锁。整轮持锁是刻意的:dream 的多次 `replace` / `remove` 必须对
212
+ // 同进程的 `memory add` 与 extract 呈现为**一个**原子区间,否则用户会在 dream 中途读到
213
+ // 半合并的状态。锁在进程内(不是 `.lock`),所以跨进程的其它 worktree 仍能正常写入 ——
214
+ // 它们的每次物理写入只被挡毫秒级。
215
+ //
216
+ // 其内部的每个原语调用必须带 `{ skipLogicalLock: true, skipSnapshot: true }`(由
217
+ // `memoryTool` 请求字段表达,适配层据此构造子会话的工具),否则它们会去抢自己已持有的
218
+ // 锁而自锁到超时,并且会为同一批变更各拍一次快照。
219
+ return ctx.store.withLogicalLock(async () => {
220
+ // 启动自检:忘了包 withLogicalLock 会静默失去整轮互斥。
221
+ if (!ctx.store.logicalLockActive()) {
222
+ throw new Error(`${task.id} must run under the memory logical lock`);
223
+ }
224
+
225
+ // 进入时对整目录拍一次快照;这一轮里各原语的逐文件快照被 skipSnapshot 跳过。
226
+ const files = await snapshotFiles(ctx.cwd, ctx.store.cfg.platform);
227
+ await createSnapshot(join(ctx.cwd, BACKUP_DIR), task.id, files, ctx.cwd, {
228
+ keep: ctx.store.cfg.lock.snapshotKeep,
229
+ platform: ctx.store.cfg.platform,
230
+ });
231
+
232
+ return run();
233
+ });
234
+ }
235
+ }
236
+ }
@@ -0,0 +1,22 @@
1
+ import type { ModelTask } from "../config.js";
2
+ import { dream } from "./dream.js";
3
+ import { extractMemories } from "./extract.js";
4
+ import { autoSurfacing } from "./surfacing.js";
5
+ import type { MemoryTask } from "./task.js";
6
+
7
+ /**
8
+ * 任务注册表:描述符的唯一声明处。
9
+ *
10
+ * 这一层单独成文件是为了让依赖方向单向:`tasks.ts → 描述符 → task.ts`。注册表原本住在 `task.ts`
11
+ * 里,而描述符要从 `task.ts` 拿 `runTask` / `MemoryTask`,于是构成模块环 —— 当时只靠「没人
12
+ * 在模块求值时读 `runTask`」才没炸(TDZ 侥幸)。把注册表挪出来后环消失,描述符里出现顶层
13
+ * 使用也不会在 import 时崩。
14
+ *
15
+ * `satisfies Record<ModelTask, MemoryTask<any, any>>` 让注册表**可证明完整**:漏一个描述符、
16
+ * 或键名写错,这里直接编译报错,而不是等调用方在运行时撞 `TASKS.dream === undefined`。
17
+ */
18
+ export const TASKS = { autoSurfacing, extractMemories, dream } as const satisfies Record<
19
+ ModelTask,
20
+ // biome-ignore lint/suspicious/noExplicitAny: registry completeness needs per-descriptor type erasure
21
+ MemoryTask<any, any>
22
+ >;