@max-null/dsh-memory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/bm25.d.ts +9 -0
- package/dist/bm25.js +42 -0
- package/dist/engine.d.ts +90 -0
- package/dist/engine.js +158 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +156 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Max-Null
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# dsh-memory
|
|
2
|
+
|
|
3
|
+
一个面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**跨会话明文记忆插件**。遵循「一切皆插件」——它不修改 DSH 源码,声明 `name`/`inject`/`apply`,由 Loader 从 `cordis.yml` 加载。
|
|
4
|
+
|
|
5
|
+
## 设计原则
|
|
6
|
+
|
|
7
|
+
1. **人是所有者**:模型只能写入 `suggested` 状态的记忆,绝不自我提升;只有人工确认(`setStatus`)才能让记忆生效。
|
|
8
|
+
2. **可观测先于精准**:每条记忆是明文,`memory_list` 随时可见、`memory_forget` 随时删除——不存在"静默暗礁"。
|
|
9
|
+
3. **明文是人机共享的审计窗口**:记忆是可读文本,模型可自检其是否过期或出错(规划中的 v2)。
|
|
10
|
+
4. **确定性且缓存安全**:BM25 关键词检索是存储的纯函数、无 LLM 调用;固定指引进 system-prompt section,`auto` 记忆进 context。
|
|
11
|
+
|
|
12
|
+
## 用法
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @max-null/dsh-memory
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
在你的 `cordis.yml` 加一条(其余 storage / system-prompt / tools 由宿主已有;记忆的存储后端由插件自己注册):
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
- id: memory
|
|
22
|
+
name: '@max-null/dsh-memory'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 提供的服务与工具
|
|
26
|
+
|
|
27
|
+
- **服务** `ctx.memory`:`remember` / `list` / `search` / `forget` / `setStatus`
|
|
28
|
+
- **工具**:`memory_save`、`memory_list`、`memory_search`、`memory_forget`
|
|
29
|
+
- **注入**:`tool:memory` 指引 section + `memory:recall` 召回 context(`auto` 记忆,带 `[memory:<id>]` 来源标记)
|
|
30
|
+
|
|
31
|
+
## 两层存储(global / project)
|
|
32
|
+
|
|
33
|
+
记忆按 `namespace` 分两层物理存储,各落在独立的明文 JSON:
|
|
34
|
+
|
|
35
|
+
| namespace | 默认位置 | 用途 |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `global` | `$DSH_HOME/storages/memory.json` | 跨项目的个人偏好 |
|
|
38
|
+
| `project` | `<cwd>/.dsh/storages/memory_project.json` | 跟随仓库的项目共识,可 git 分享 |
|
|
39
|
+
|
|
40
|
+
两个根都可用 config 覆盖(`globalRoot` / `projectRoot`)。`memory_list` / `memory_search` 不带 `namespace` 过滤时会同时查两层。
|
|
41
|
+
|
|
42
|
+
## 使用流程(人工确认闸门)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
模型 memory_save → status: suggested(只是建议,未生效)
|
|
46
|
+
人工 setStatus → status: auto(生效,自动进入每轮的 recall context)
|
|
47
|
+
status: suggest(参考,不自动注入)
|
|
48
|
+
memory_search → 随时按关键词召回任意状态的记忆
|
|
49
|
+
memory_forget → 随时删除
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
模型**永远不能自我提升**一条记忆——`memory_save` 只写 `suggested`,只有人(或未来的 v2 自检)通过 `setStatus` 才能让它生效。这保证了记忆不是黑盒:人随时能看、能改、能删。
|
|
53
|
+
|
|
54
|
+
## 为什么明文 + BM25,而不是向量检索
|
|
55
|
+
|
|
56
|
+
向量检索的记忆本体是一串不可读的数字,过期信息会成为**无法观测、无法修复的静默暗礁**;BM25 + 明文让每一次召回都可解释、每一条记忆都可见可删。语义(向量)检索留作 v2 的可选项,且以"可观测 + 可修复"为门槛,而非时间表。
|
|
57
|
+
|
|
58
|
+
明文还有一层**跟随仓库分享**的好处:`project` 命名空间的记忆落在项目文件夹内(`<cwd>/.dsh/storages/memory_project.json`),随 `git` 提交、分享给所有协作者;`global` 命名空间的记忆留在本地 `$DSH_HOME`。团队的共识("本项目统一用 Vue3 `<script setup>`")能沉淀进仓库,而不是散落在每个人的本地。FTS5 的 SQLite 二进制、向量的数字串,都无法这样"跟着仓库走"。
|
|
59
|
+
|
|
60
|
+
## 开发
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install
|
|
64
|
+
npm run typecheck # tsc 严格类型检查
|
|
65
|
+
npm test # vitest 单测
|
|
66
|
+
npm run build # 产出 dist/
|
|
67
|
+
node scripts/verify-loader.mjs # 用 Loader 端到端验证插件可加载
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 依赖(peerDependencies,由宿主提供)
|
|
71
|
+
|
|
72
|
+
`@deepseek-ai/cordis`、`@deepseek-ai/dsh-storage`、`@deepseek-ai/dsh-storage-domain`、`@deepseek-ai/dsh-storage-json`、`@deepseek-ai/dsh-system-prompt`、`@deepseek-ai/dsh-tools`
|
package/dist/bm25.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 keyword scoring over plaintext memory records. A pure function of the
|
|
3
|
+
* store — no model call — so recall is deterministic and a miss is explainable
|
|
4
|
+
* as "no keyword match".
|
|
5
|
+
*/
|
|
6
|
+
/** Split text into lowercase terms: English/number runs stay whole, each CJK ideograph its own term. */
|
|
7
|
+
export declare function tokenize(text: string): string[];
|
|
8
|
+
/** Score one query against each document with BM25, in document order. */
|
|
9
|
+
export declare function bm25Scores(query: string, docs: readonly string[]): number[];
|
package/dist/bm25.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BM25 keyword scoring over plaintext memory records. A pure function of the
|
|
3
|
+
* store — no model call — so recall is deterministic and a miss is explainable
|
|
4
|
+
* as "no keyword match".
|
|
5
|
+
*/
|
|
6
|
+
/** Split text into lowercase terms: English/number runs stay whole, each CJK ideograph its own term. */
|
|
7
|
+
export function tokenize(text) {
|
|
8
|
+
return text.toLowerCase().match(/[a-z0-9]+|[\u3400-\u9fff]/g) ?? [];
|
|
9
|
+
}
|
|
10
|
+
const K1 = 1.2;
|
|
11
|
+
const B = 0.75;
|
|
12
|
+
/** Score one query against each document with BM25, in document order. */
|
|
13
|
+
export function bm25Scores(query, docs) {
|
|
14
|
+
const queryTerms = tokenize(query);
|
|
15
|
+
const tokenized = docs.map(tokenize);
|
|
16
|
+
const documentFrequency = new Map();
|
|
17
|
+
for (const doc of tokenized) {
|
|
18
|
+
for (const term of new Set(doc)) {
|
|
19
|
+
documentFrequency.set(term, (documentFrequency.get(term) ?? 0) + 1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const n = docs.length;
|
|
23
|
+
const totalLength = tokenized.reduce((sum, doc) => sum + doc.length, 0);
|
|
24
|
+
const averageLength = n === 0 ? 0 : totalLength / n;
|
|
25
|
+
return tokenized.map((doc) => {
|
|
26
|
+
let score = 0;
|
|
27
|
+
for (const term of queryTerms) {
|
|
28
|
+
let termFrequency = 0;
|
|
29
|
+
for (const candidate of doc) {
|
|
30
|
+
if (candidate === term)
|
|
31
|
+
termFrequency++;
|
|
32
|
+
}
|
|
33
|
+
if (termFrequency === 0)
|
|
34
|
+
continue;
|
|
35
|
+
const frequency = documentFrequency.get(term) ?? 0;
|
|
36
|
+
const idf = Math.log(1 + (n - frequency + 0.5) / (frequency + 0.5));
|
|
37
|
+
const denominator = termFrequency + K1 * (1 - B + B * (doc.length / (averageLength || 1)));
|
|
38
|
+
score += idf * (termFrequency * (K1 + 1)) / denominator;
|
|
39
|
+
}
|
|
40
|
+
return score;
|
|
41
|
+
});
|
|
42
|
+
}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory service (`ctx.memory`): durable plaintext records over two
|
|
3
|
+
* storage roots — `global` in the harness home, `project` in the current
|
|
4
|
+
* project folder (`.dsh/`), so project memory follows the repository. A record
|
|
5
|
+
* is always created `suggested` and becomes effective only through `setStatus`.
|
|
6
|
+
*/
|
|
7
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
8
|
+
declare const memoryIdBrand: unique symbol;
|
|
9
|
+
/** Opaque identity of one stored memory record. */
|
|
10
|
+
export type MemoryId = string & {
|
|
11
|
+
readonly [memoryIdBrand]: never;
|
|
12
|
+
};
|
|
13
|
+
/** Brand a string as a {@link MemoryId} (compile-time only). */
|
|
14
|
+
export declare function MemoryId(id: string): MemoryId;
|
|
15
|
+
export type MemoryNamespace = 'global' | 'project';
|
|
16
|
+
/** `suggested` is model-written; only a human confirmation promotes it. */
|
|
17
|
+
export type MemoryStatus = 'suggested' | 'auto' | 'suggest';
|
|
18
|
+
export interface MemoryRecord {
|
|
19
|
+
id: MemoryId;
|
|
20
|
+
namespace: MemoryNamespace;
|
|
21
|
+
status: MemoryStatus;
|
|
22
|
+
content: string;
|
|
23
|
+
keywords: string[];
|
|
24
|
+
createdAt: number;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
}
|
|
27
|
+
export interface MemoryWrite {
|
|
28
|
+
content: string;
|
|
29
|
+
namespace?: MemoryNamespace;
|
|
30
|
+
keywords?: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface MemoryFilter {
|
|
33
|
+
namespace?: MemoryNamespace;
|
|
34
|
+
status?: MemoryStatus;
|
|
35
|
+
}
|
|
36
|
+
export interface MemoryHit {
|
|
37
|
+
record: MemoryRecord;
|
|
38
|
+
score: number;
|
|
39
|
+
}
|
|
40
|
+
/** One durable memory change, emitted after the backend acknowledges the write. */
|
|
41
|
+
export type MemoryChange = {
|
|
42
|
+
operation: 'remembered';
|
|
43
|
+
record: MemoryRecord;
|
|
44
|
+
} | {
|
|
45
|
+
operation: 'forgotten';
|
|
46
|
+
id: MemoryId;
|
|
47
|
+
} | {
|
|
48
|
+
operation: 'status';
|
|
49
|
+
id: MemoryId;
|
|
50
|
+
status: MemoryStatus;
|
|
51
|
+
};
|
|
52
|
+
declare module '@deepseek-ai/cordis' {
|
|
53
|
+
interface Context {
|
|
54
|
+
memory: MemoryEngine;
|
|
55
|
+
}
|
|
56
|
+
interface Events {
|
|
57
|
+
/** A memory record was created, deleted, or promoted/demoted. */
|
|
58
|
+
'memory/changed'(change: MemoryChange): void;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Engine configuration: the two storage roots, defaulted to home and project. */
|
|
62
|
+
export interface MemoryConfig {
|
|
63
|
+
/** Root for `global` memories; defaults to `$DSH_HOME/storages`. */
|
|
64
|
+
globalRoot?: string;
|
|
65
|
+
/** Root for `project` memories; defaults to `<cwd>/.dsh/storages`. */
|
|
66
|
+
projectRoot?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Cross-session plaintext memory over the storage hub, split by namespace:
|
|
70
|
+
* `global` lives in the harness home, `project` in the project folder.
|
|
71
|
+
*/
|
|
72
|
+
export declare class MemoryEngine extends Service {
|
|
73
|
+
private readonly config;
|
|
74
|
+
static inject: string[];
|
|
75
|
+
private globalTable?;
|
|
76
|
+
private projectTable?;
|
|
77
|
+
constructor(ctx: import('@deepseek-ai/cordis').Context, config?: MemoryConfig);
|
|
78
|
+
protected [Service.init](): Promise<void>;
|
|
79
|
+
/** Create one record in `suggested` status — never self-promoting. */
|
|
80
|
+
remember(input: MemoryWrite): Promise<MemoryRecord>;
|
|
81
|
+
list(filter?: MemoryFilter): MemoryRecord[];
|
|
82
|
+
search(query: string, filter?: MemoryFilter): MemoryHit[];
|
|
83
|
+
forget(id: MemoryId): Promise<boolean>;
|
|
84
|
+
setStatus(id: MemoryId, status: MemoryStatus): Promise<MemoryRecord>;
|
|
85
|
+
private allRecords;
|
|
86
|
+
private recordsOf;
|
|
87
|
+
private tableFor;
|
|
88
|
+
private requireTable;
|
|
89
|
+
}
|
|
90
|
+
export {};
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The memory service (`ctx.memory`): durable plaintext records over two
|
|
3
|
+
* storage roots — `global` in the harness home, `project` in the current
|
|
4
|
+
* project folder (`.dsh/`), so project memory follows the repository. A record
|
|
5
|
+
* is always created `suggested` and becomes effective only through `setStatus`.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { defineDomain, domainTable, DomainFacility } from '@deepseek-ai/dsh-storage-domain';
|
|
13
|
+
import { JsonStorageBackend } from '@deepseek-ai/dsh-storage-json';
|
|
14
|
+
import { bm25Scores } from "./bm25.js";
|
|
15
|
+
/** Brand a string as a {@link MemoryId} (compile-time only). */
|
|
16
|
+
export function MemoryId(id) {
|
|
17
|
+
return id;
|
|
18
|
+
}
|
|
19
|
+
const blockSchema = z.object({
|
|
20
|
+
namespace: z.enum(['global', 'project']),
|
|
21
|
+
status: z.enum(['suggested', 'auto', 'suggest']),
|
|
22
|
+
content: z.string(),
|
|
23
|
+
keywords: z.array(z.string()),
|
|
24
|
+
createdAt: z.number(),
|
|
25
|
+
updatedAt: z.number(),
|
|
26
|
+
});
|
|
27
|
+
/** Shared table shape; the two domains differ only by name and backend route. */
|
|
28
|
+
function memorySpec(name) {
|
|
29
|
+
return defineDomain({
|
|
30
|
+
name,
|
|
31
|
+
version: 1,
|
|
32
|
+
tables: { blocks: domainTable(blockSchema) },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function toRecord(id, block) {
|
|
36
|
+
return { id: MemoryId(id), ...block };
|
|
37
|
+
}
|
|
38
|
+
/** Harness-home root for `global` memories. */
|
|
39
|
+
function globalRoot() {
|
|
40
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'storages');
|
|
41
|
+
}
|
|
42
|
+
/** Project-folder root for `project` memories — follows the repository. */
|
|
43
|
+
function projectRoot() {
|
|
44
|
+
return join(process.cwd(), '.dsh', 'storages');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Cross-session plaintext memory over the storage hub, split by namespace:
|
|
48
|
+
* `global` lives in the harness home, `project` in the project folder.
|
|
49
|
+
*/
|
|
50
|
+
export class MemoryEngine extends Service {
|
|
51
|
+
config;
|
|
52
|
+
static inject = ['storage'];
|
|
53
|
+
globalTable;
|
|
54
|
+
projectTable;
|
|
55
|
+
constructor(ctx, config = {}) {
|
|
56
|
+
super(ctx, 'memory');
|
|
57
|
+
this.config = config;
|
|
58
|
+
}
|
|
59
|
+
async [Service.init]() {
|
|
60
|
+
const globalBackend = new JsonStorageBackend(this.config.globalRoot ?? globalRoot());
|
|
61
|
+
const projectBackend = new JsonStorageBackend(this.config.projectRoot ?? projectRoot());
|
|
62
|
+
this.ctx.storage.backend.register('memory-global', globalBackend);
|
|
63
|
+
this.ctx.storage.backend.register('memory-project', projectBackend);
|
|
64
|
+
const facility = new DomainFacility(this.ctx, {
|
|
65
|
+
backend: 'memory-global',
|
|
66
|
+
routes: { memory_project: 'memory-project' },
|
|
67
|
+
});
|
|
68
|
+
const globalDomain = await facility.open(memorySpec('memory'));
|
|
69
|
+
const projectDomain = await facility.open(memorySpec('memory_project'));
|
|
70
|
+
this.ctx.effect(() => () => { void facility.closeAll(); }, 'memory.domainsClose');
|
|
71
|
+
this.globalTable = globalDomain.table('blocks');
|
|
72
|
+
this.projectTable = projectDomain.table('blocks');
|
|
73
|
+
}
|
|
74
|
+
/** Create one record in `suggested` status — never self-promoting. */
|
|
75
|
+
async remember(input) {
|
|
76
|
+
const namespace = input.namespace ?? 'global';
|
|
77
|
+
const table = this.tableFor(namespace);
|
|
78
|
+
const id = randomUUID();
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const block = {
|
|
81
|
+
namespace,
|
|
82
|
+
status: 'suggested',
|
|
83
|
+
content: input.content,
|
|
84
|
+
keywords: (input.keywords ?? []).map(keyword => keyword.toLowerCase()),
|
|
85
|
+
createdAt: now,
|
|
86
|
+
updatedAt: now,
|
|
87
|
+
};
|
|
88
|
+
await table.put(id, block);
|
|
89
|
+
const record = toRecord(id, block);
|
|
90
|
+
this.ctx.emit('memory/changed', { operation: 'remembered', record });
|
|
91
|
+
return record;
|
|
92
|
+
}
|
|
93
|
+
list(filter) {
|
|
94
|
+
const records = this.allRecords(filter?.namespace);
|
|
95
|
+
return records.filter(record => (filter?.status === undefined || record.status === filter.status));
|
|
96
|
+
}
|
|
97
|
+
search(query, filter) {
|
|
98
|
+
const records = this.list(filter);
|
|
99
|
+
const docs = records.map(record => `${record.content} ${record.keywords.join(' ')}`);
|
|
100
|
+
const scores = bm25Scores(query, docs);
|
|
101
|
+
return records
|
|
102
|
+
.map((record, index) => ({ record, score: scores[index] ?? 0 }))
|
|
103
|
+
.filter(hit => hit.score > 0)
|
|
104
|
+
.sort((left, right) => right.score - left.score);
|
|
105
|
+
}
|
|
106
|
+
async forget(id) {
|
|
107
|
+
if (await this.requireTable('global').delete(id)) {
|
|
108
|
+
this.ctx.emit('memory/changed', { operation: 'forgotten', id });
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (await this.requireTable('project').delete(id)) {
|
|
112
|
+
this.ctx.emit('memory/changed', { operation: 'forgotten', id });
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
async setStatus(id, status) {
|
|
118
|
+
const global = this.requireTable('global').get(id);
|
|
119
|
+
if (global !== undefined) {
|
|
120
|
+
const updated = { ...global, status, updatedAt: Date.now() };
|
|
121
|
+
await this.requireTable('global').put(id, updated);
|
|
122
|
+
const record = toRecord(id, updated);
|
|
123
|
+
this.ctx.emit('memory/changed', { operation: 'status', id, status });
|
|
124
|
+
return record;
|
|
125
|
+
}
|
|
126
|
+
const project = this.requireTable('project').get(id);
|
|
127
|
+
if (project !== undefined) {
|
|
128
|
+
const updated = { ...project, status, updatedAt: Date.now() };
|
|
129
|
+
await this.requireTable('project').put(id, updated);
|
|
130
|
+
const record = toRecord(id, updated);
|
|
131
|
+
this.ctx.emit('memory/changed', { operation: 'status', id, status });
|
|
132
|
+
return record;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`cannot set status of unknown memory '${id}'`);
|
|
135
|
+
}
|
|
136
|
+
allRecords(namespace) {
|
|
137
|
+
if (namespace === 'project')
|
|
138
|
+
return this.recordsOf(this.requireTable('project'));
|
|
139
|
+
if (namespace === 'global')
|
|
140
|
+
return this.recordsOf(this.requireTable('global'));
|
|
141
|
+
return [
|
|
142
|
+
...this.recordsOf(this.requireTable('global')),
|
|
143
|
+
...this.recordsOf(this.requireTable('project')),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
recordsOf(table) {
|
|
147
|
+
return [...table.entries()].map(([id, block]) => toRecord(id, block));
|
|
148
|
+
}
|
|
149
|
+
tableFor(namespace) {
|
|
150
|
+
return this.requireTable(namespace);
|
|
151
|
+
}
|
|
152
|
+
requireTable(namespace) {
|
|
153
|
+
const table = namespace === 'global' ? this.globalTable : this.projectTable;
|
|
154
|
+
if (table === undefined)
|
|
155
|
+
throw new Error('memory engine is not started yet');
|
|
156
|
+
return table;
|
|
157
|
+
}
|
|
158
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-memory`: a cross-session, deterministic, human-owned memory plugin for
|
|
3
|
+
* the DeepSeek Harness. Load it in `cordis.yml` beside the storage, storage
|
|
4
|
+
* domain, system prompt, and tools plugins; it registers `ctx.memory`, four
|
|
5
|
+
* model tools, a stable guidance section, and a recall context.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
+
import type { MemoryConfig } from './engine.ts';
|
|
9
|
+
export { MemoryEngine } from './engine.ts';
|
|
10
|
+
export type { MemoryChange, MemoryConfig, MemoryFilter, MemoryHit, MemoryNamespace, MemoryRecord, MemoryStatus, MemoryWrite, } from './engine.ts';
|
|
11
|
+
export { MemoryId } from './engine.ts';
|
|
12
|
+
export { bm25Scores, tokenize } from './bm25.ts';
|
|
13
|
+
export declare const name = "dsh-memory";
|
|
14
|
+
export declare const inject: string[];
|
|
15
|
+
export declare function apply(ctx: Context, config?: MemoryConfig): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dsh-memory`: a cross-session, deterministic, human-owned memory plugin for
|
|
3
|
+
* the DeepSeek Harness. Load it in `cordis.yml` beside the storage, storage
|
|
4
|
+
* domain, system prompt, and tools plugins; it registers `ctx.memory`, four
|
|
5
|
+
* model tools, a stable guidance section, and a recall context.
|
|
6
|
+
*/
|
|
7
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
import { MemoryEngine } from "./engine.js";
|
|
9
|
+
export { MemoryEngine } from "./engine.js";
|
|
10
|
+
export { MemoryId } from "./engine.js";
|
|
11
|
+
export { bm25Scores, tokenize } from "./bm25.js";
|
|
12
|
+
export const name = 'dsh-memory';
|
|
13
|
+
export const inject = ['storage', 'systemPrompt', 'tools'];
|
|
14
|
+
const RECORD_SCHEMA = {
|
|
15
|
+
type: 'object',
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
id: { type: 'string', required: true },
|
|
19
|
+
namespace: { type: 'string', required: true, enum: ['global', 'project'] },
|
|
20
|
+
status: { type: 'string', required: true, enum: ['suggested', 'auto', 'suggest'] },
|
|
21
|
+
content: { type: 'string', required: true },
|
|
22
|
+
keywords: { type: 'array', required: true, items: { type: 'string' } },
|
|
23
|
+
createdAt: { type: 'number', required: true },
|
|
24
|
+
updatedAt: { type: 'number', required: true },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const HIT_SCHEMA = {
|
|
28
|
+
type: 'object',
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
record: { ...RECORD_SCHEMA, required: true },
|
|
32
|
+
score: { type: 'number', required: true },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
function recordValue(record) {
|
|
36
|
+
return {
|
|
37
|
+
id: String(record.id),
|
|
38
|
+
namespace: record.namespace,
|
|
39
|
+
status: record.status,
|
|
40
|
+
content: record.content,
|
|
41
|
+
keywords: record.keywords,
|
|
42
|
+
createdAt: record.createdAt,
|
|
43
|
+
updatedAt: record.updatedAt,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function hitValue(hit) {
|
|
47
|
+
return { record: recordValue(hit.record), score: hit.score };
|
|
48
|
+
}
|
|
49
|
+
const GUIDANCE = 'Use memory tools for cross-session preferences, habits, and project conventions. '
|
|
50
|
+
+ 'memory_save always records a suggestion (`suggested`) and never makes it effective itself — '
|
|
51
|
+
+ 'a human confirms it. When earlier context may be relevant, call memory_search to recall it. '
|
|
52
|
+
+ 'Every memory is plaintext and inspectable with memory_list; memory_forget removes one.';
|
|
53
|
+
function recallText(memory) {
|
|
54
|
+
const auto = memory.list({ status: 'auto' });
|
|
55
|
+
if (auto.length === 0)
|
|
56
|
+
return '';
|
|
57
|
+
const lines = auto.map(record => `- [memory:${String(record.id)}] ${record.content}`);
|
|
58
|
+
return `Remembered preferences and conventions — apply these:\n${lines.join('\n')}`;
|
|
59
|
+
}
|
|
60
|
+
function renderJson(value) {
|
|
61
|
+
return [{ type: 'text', text: JSON.stringify(value) }];
|
|
62
|
+
}
|
|
63
|
+
function present(title, kind, rawInput) {
|
|
64
|
+
return { card: 'generic', title, kind, ...rawInput === undefined ? {} : { rawInput } };
|
|
65
|
+
}
|
|
66
|
+
export async function apply(ctx, config) {
|
|
67
|
+
await ctx.plugin(MemoryEngine, config);
|
|
68
|
+
const memory = ctx.get('memory');
|
|
69
|
+
if (memory === undefined)
|
|
70
|
+
throw new Error('memory engine failed to register');
|
|
71
|
+
ctx.systemPrompt.section({
|
|
72
|
+
name: 'tool:memory',
|
|
73
|
+
order: 115,
|
|
74
|
+
text: GUIDANCE,
|
|
75
|
+
});
|
|
76
|
+
ctx.systemPrompt.context({
|
|
77
|
+
name: 'memory:recall',
|
|
78
|
+
order: 50,
|
|
79
|
+
text: () => recallText(memory),
|
|
80
|
+
});
|
|
81
|
+
ctx.tools.register(defineTool({
|
|
82
|
+
name: 'memory_save',
|
|
83
|
+
description: 'Record one cross-session memory as a suggestion. It never becomes effective until a human confirms it; the model must not present a suggestion as confirmed.',
|
|
84
|
+
parameters: {
|
|
85
|
+
content: { type: 'string', required: true, description: 'Plaintext memory content.' },
|
|
86
|
+
namespace: { type: 'string', enum: ['global', 'project'], description: 'Where it applies; defaults to global.' },
|
|
87
|
+
keywords: { type: 'array', items: { type: 'string' }, description: 'Explicit searchable anchors for memory_search.' },
|
|
88
|
+
},
|
|
89
|
+
output: {
|
|
90
|
+
schema: RECORD_SCHEMA,
|
|
91
|
+
render: (_args, value) => renderJson(value),
|
|
92
|
+
},
|
|
93
|
+
execute(args, _exec) {
|
|
94
|
+
return memory.remember({
|
|
95
|
+
content: args.content,
|
|
96
|
+
...args.namespace === undefined ? {} : { namespace: args.namespace },
|
|
97
|
+
...args.keywords === undefined ? {} : { keywords: args.keywords },
|
|
98
|
+
}).then(recordValue);
|
|
99
|
+
},
|
|
100
|
+
presentCall: args => present('Save memory', 'other', args.content),
|
|
101
|
+
}));
|
|
102
|
+
ctx.tools.register(defineTool({
|
|
103
|
+
name: 'memory_list',
|
|
104
|
+
description: 'List every stored memory, optionally filtered by namespace or status. Every memory is plaintext and inspectable.',
|
|
105
|
+
parameters: {
|
|
106
|
+
namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace.' },
|
|
107
|
+
status: { type: 'string', enum: ['suggested', 'auto', 'suggest'], description: 'Restrict to one status.' },
|
|
108
|
+
},
|
|
109
|
+
output: {
|
|
110
|
+
schema: { type: 'array', items: RECORD_SCHEMA },
|
|
111
|
+
render: (_args, value) => renderJson(value),
|
|
112
|
+
},
|
|
113
|
+
execute(args, _exec) {
|
|
114
|
+
return Promise.resolve(memory.list({
|
|
115
|
+
...args.namespace === undefined ? {} : { namespace: args.namespace },
|
|
116
|
+
...args.status === undefined ? {} : { status: args.status },
|
|
117
|
+
}).map(recordValue));
|
|
118
|
+
},
|
|
119
|
+
presentCall: () => present('List memories', 'read'),
|
|
120
|
+
}));
|
|
121
|
+
ctx.tools.register(defineTool({
|
|
122
|
+
name: 'memory_search',
|
|
123
|
+
description: 'Recall stored memories by keyword. Deterministic literal matching — a miss means no stored term matched the query.',
|
|
124
|
+
parameters: {
|
|
125
|
+
query: { type: 'string', required: true, description: 'Keyword query.' },
|
|
126
|
+
namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace.' },
|
|
127
|
+
status: { type: 'string', enum: ['suggested', 'auto', 'suggest'], description: 'Restrict to one status.' },
|
|
128
|
+
},
|
|
129
|
+
output: {
|
|
130
|
+
schema: { type: 'array', items: HIT_SCHEMA },
|
|
131
|
+
render: (_args, value) => renderJson(value),
|
|
132
|
+
},
|
|
133
|
+
execute(args, _exec) {
|
|
134
|
+
return Promise.resolve(memory.search(args.query, {
|
|
135
|
+
...args.namespace === undefined ? {} : { namespace: args.namespace },
|
|
136
|
+
...args.status === undefined ? {} : { status: args.status },
|
|
137
|
+
}).map(hitValue));
|
|
138
|
+
},
|
|
139
|
+
presentCall: args => present('Search memory', 'read', args.query),
|
|
140
|
+
}));
|
|
141
|
+
ctx.tools.register(defineTool({
|
|
142
|
+
name: 'memory_forget',
|
|
143
|
+
description: 'Delete one stored memory by id. The human owner may remove any memory.',
|
|
144
|
+
parameters: {
|
|
145
|
+
id: { type: 'string', required: true, description: 'Exact memory id from memory_list or memory_search.' },
|
|
146
|
+
},
|
|
147
|
+
output: {
|
|
148
|
+
schema: { type: 'object', additionalProperties: false, properties: { deleted: { type: 'boolean', required: true } } },
|
|
149
|
+
render: (_args, value) => renderJson(value),
|
|
150
|
+
},
|
|
151
|
+
execute(args, _exec) {
|
|
152
|
+
return memory.forget(args.id).then(deleted => ({ deleted }));
|
|
153
|
+
},
|
|
154
|
+
presentCall: args => present('Forget memory', 'other', args.id),
|
|
155
|
+
}));
|
|
156
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@max-null/dsh-memory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Cross-session plaintext memory plugin for the DeepSeek Harness — deterministic BM25 recall, human-owned, no embeddings",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"types": "dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"deepseek",
|
|
23
|
+
"harness",
|
|
24
|
+
"dsh",
|
|
25
|
+
"memory",
|
|
26
|
+
"plugin",
|
|
27
|
+
"cordis"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/Max-Null/dsh-memory.git"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"prepublishOnly": "npm run build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"zod": "^4.4.3"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
44
|
+
"@deepseek-ai/dsh-storage": ">=0.0.1-rc.1",
|
|
45
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.0.1-rc.1",
|
|
46
|
+
"@deepseek-ai/dsh-storage-json": ">=0.0.1-rc.1",
|
|
47
|
+
"@deepseek-ai/dsh-system-prompt": ">=0.0.1-rc.1",
|
|
48
|
+
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
52
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
|
|
53
|
+
"@deepseek-ai/dsh-storage": ">=0.0.1-rc.1",
|
|
54
|
+
"@deepseek-ai/dsh-storage-domain": ">=0.0.1-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-storage-json": ">=0.0.1-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-system-prompt": ">=0.0.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1",
|
|
58
|
+
"@types/node": "^26.2.0",
|
|
59
|
+
"tsx": "^4.0.0",
|
|
60
|
+
"typescript": "^5.5.0",
|
|
61
|
+
"vitest": "^2.1.0"
|
|
62
|
+
}
|
|
63
|
+
}
|