@cmnyhh/dsh-mnemo 0.0.0-stage → 0.9.4

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/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@cmnyhh/dsh-mnemo",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.9.4",
4
+ "description": "Long-term memory for dsh: capture finished turns, recall them into the first step of a later turn, and expose recall and write as tools. Ships a bundle layer and a settings panel for the service address and token.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "exports": {
9
+ ".": "./index.js",
10
+ "./client": "./client.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "client.js",
16
+ "cordis.patch.yml",
17
+ "smoke.mjs",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "dsh-plugin",
22
+ "dsh",
23
+ "memory",
24
+ "long-term-memory",
25
+ "recall"
26
+ ],
27
+ "dsh": {
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "platform": "web"
33
+ }
34
+ },
35
+ "publishConfig": {
36
+ "access": "public",
37
+ "registry": "https://registry.npmjs.org/"
38
+ },
39
+ "peerDependencies": {
40
+ "@deepseek-ai/cordis": "^0.2.1-alpha.1",
41
+ "@deepseek-ai/dsh-agent": "^0.2.1-alpha.1",
42
+ "@deepseek-ai/dsh-llm": "^0.2.1-alpha.1",
43
+ "@deepseek-ai/dsh-session": "^0.2.1-alpha.1",
44
+ "@deepseek-ai/dsh-tools": "^0.2.1-alpha.1"
45
+ }
46
+ }
package/smoke.mjs ADDED
@@ -0,0 +1,161 @@
1
+ // 假宿主冒烟:证明 bundle 能被 cordis 式挂载、写入、下一轮召回、工具可调用。
2
+ //
3
+ // 用法:node smoke.mjs [index.js 的路径],缺省为同目录的 ./index.js。
4
+ //
5
+ // 插件把 @deepseek-ai/dsh-llm、dsh-tools 留给宿主提供,所以这里在临时目录里
6
+ // 复制一份 bundle、补上最小桩模块再加载,不动你机器上的任何目录。
7
+ import { mkdtemp, mkdir, writeFile, copyFile, rm, readdir, stat } from 'node:fs/promises'
8
+ import { tmpdir } from 'node:os'
9
+ import { dirname, join, resolve } from 'node:path'
10
+ import { fileURLToPath, pathToFileURL } from 'node:url'
11
+
12
+ const STUBS = {
13
+ 'dsh-llm': 'let n = 0\nexport function createUserMessage(input) { return { id: `msg-${++n}`, role: \'user\', ...input } }\n',
14
+ 'dsh-tools': 'export function defineTool(definition) { return definition }\n',
15
+ }
16
+
17
+ /** 缺省目标:同目录的 index.js,否则本目录下最新的 dsh-memory-core-<版本>/index.js。 */
18
+ async function defaultSource() {
19
+ const here = dirname(fileURLToPath(import.meta.url))
20
+ const same = join(here, 'index.js')
21
+ try {
22
+ await stat(same)
23
+ return same
24
+ } catch {}
25
+ const candidates = (await readdir(here)).filter(name => name.startsWith('dsh-memory-core-')).sort()
26
+ const latest = candidates.at(-1)
27
+ if (latest === undefined) throw new Error('找不到 index.js:把解压后的 index.js 路径作为参数传入')
28
+ return join(here, latest, 'index.js')
29
+ }
30
+
31
+ const source = resolve(process.argv[2] ?? await defaultSource())
32
+ const sandbox = await mkdtemp(join(tmpdir(), 'memcore-smoke-'))
33
+
34
+ async function stage() {
35
+ await copyFile(source, join(sandbox, 'index.js'))
36
+ for (const [name, body] of Object.entries(STUBS)) {
37
+ const dir = join(sandbox, 'node_modules', '@deepseek-ai', name)
38
+ await mkdir(dir, { recursive: true })
39
+ await writeFile(join(dir, 'package.json'), JSON.stringify({
40
+ name: `@deepseek-ai/${name}`, version: '0.0.0', type: 'module', main: 'index.js',
41
+ }))
42
+ await writeFile(join(dir, 'index.js'), body)
43
+ }
44
+ return pathToFileURL(join(sandbox, 'index.js')).href
45
+ }
46
+
47
+ const checks = []
48
+ const check = (name, ok, detail = '') => checks.push({ name, ok, detail })
49
+
50
+ console.log(`目标: ${source}`)
51
+ const entry = await stage()
52
+ const plugin = await import(entry)
53
+
54
+ const handlers = new Map()
55
+ const tools = []
56
+ const logs = []
57
+ const ctx = {
58
+ logger: { warn: (m) => logs.push(['warn', m]), info: () => {}, debug: () => {} },
59
+ on(event, fn) { handlers.set(event, [...(handlers.get(event) ?? []), fn]) },
60
+ tools: { register(tool) { tools.push(tool) } },
61
+ }
62
+
63
+ plugin.apply(ctx, plugin.Config({}))
64
+
65
+ check('导出 name', plugin.name === 'memory-core')
66
+ check('注册两个工具', tools.length === 2
67
+ && tools.some(t => t.name === 'memory_search')
68
+ && tools.some(t => t.name === 'memory_write'), tools.map(t => t.name).join(','))
69
+ check('挂载 session/event', handlers.has('session/event'))
70
+ check('挂载 agent/pre-step', handlers.has('agent/pre-step'))
71
+
72
+ const text = (t) => [{ type: 'text', text: t }]
73
+ const emit = (event, session, payload) => {
74
+ for (const fn of handlers.get(event) ?? []) fn(session, payload)
75
+ }
76
+ const preStep = async (prompt) => {
77
+ let injected
78
+ const next = async () => ({ kind: 'continue', messages: [] })
79
+ for (const fn of handlers.get('agent/pre-step') ?? []) {
80
+ const decision = await fn({
81
+ agent: { session: { id: 'sess-1' } },
82
+ messages: [{ source: { kind: 'user' }, content: text(prompt) }],
83
+ step: 1,
84
+ signal: { aborted: false },
85
+ }, next)
86
+ injected = decision.messages
87
+ }
88
+ return injected.map(m => m.content.map(b => b.text).join('')).join('\n')
89
+ }
90
+
91
+ // 一轮完整对话
92
+ const session = { id: 'sess-1' }
93
+ const time = Date.now()
94
+ emit('session/event', session, { type: 'turn/start', data: { turn: 1 }, time })
95
+ emit('session/event', session, {
96
+ type: 'user/message',
97
+ data: { source: { kind: 'user' }, content: text('记住:mnemo 的部署端口是 8090,部署目录 /srv/memory-service。') },
98
+ time,
99
+ })
100
+ emit('session/event', session, {
101
+ type: 'assistant/message',
102
+ data: { message: { content: text('好的,已记录 mnemo 的部署端口是 8090,部署目录 /srv/memory-service。') } },
103
+ time,
104
+ })
105
+ emit('session/event', session, { type: 'turn/end', data: {}, time })
106
+
107
+ const block = await preStep('mnemo 的部署端口是多少?')
108
+ check('下一轮注入非空', block.length > 0, `${block.length} 字符`)
109
+ check('注入命中端口', block.includes('8090'), block.slice(0, 120).replace(/\n/g, ' ⏎ '))
110
+ // `<memory_context>` 信封由服务端渲染,本地 in-process 模式没有;
111
+ // 这里只断言本地渲染头,信封由服务端不变量巡检覆盖。
112
+ check('注入带本地渲染头', block.includes('Recalled long-term memory'), '')
113
+
114
+ // 工具调用
115
+ const tool = tools[0]
116
+ const value = await tool.execute({ query: '部署端口' }, { agent: { session: { id: 'sess-1' } } })
117
+ check('工具召回命中', Array.isArray(value.hits) && value.hits.some(h => h.text.includes('8090')), JSON.stringify(value.hits.map(h => h.id)))
118
+ const rendered = tool.output.render({}, value)
119
+ check('工具可渲染', Array.isArray(rendered) && rendered[0].text.length > 0)
120
+
121
+ // 远端模式:服务不可达时必须说"请求失败",不能冒充"没有记忆"
122
+ const remoteTools = []
123
+ const remoteLogs = []
124
+ const remoteCtx = {
125
+ logger: { warn: (m) => remoteLogs.push(String(m)), info: () => {}, debug: () => {} },
126
+ on() {},
127
+ tools: { register(tool) { remoteTools.push(tool) } },
128
+ }
129
+ plugin.apply(remoteCtx, plugin.Config({
130
+ serviceUrl: 'http://127.0.0.1:1',
131
+ serviceToken: 'dummy',
132
+ projectId: 'smoke',
133
+ serviceTimeoutMs: 800,
134
+ }))
135
+ const remoteTool = remoteTools.find(t => t.name === 'memory_search')
136
+ const missed = await remoteTool.execute({ query: '任何问题' }, { agent: { session: { id: 'sess-2' } } })
137
+ check('远端失败不冒充零命中',
138
+ missed.hits.length === 0 && typeof missed.note === 'string' && missed.note.includes('没响应'),
139
+ missed.note ?? '(无 note)')
140
+ const missedText = remoteTool.output.render({}, missed)[0].text
141
+ check('失败时不渲染 No memory matched',
142
+ !missedText.includes('No memory matched'), missedText.slice(0, 80).replace(/\n/g, ' '))
143
+
144
+ // 写入闸门:寒暄不写
145
+ emit('session/event', session, { type: 'turn/start', data: { turn: 2 }, time: time + 1 })
146
+ emit('session/event', session, { type: 'user/message', data: { source: { kind: 'user' }, content: text('你好') }, time: time + 1 })
147
+ emit('session/event', session, { type: 'assistant/message', data: { message: { content: text('你好!') } }, time: time + 1 })
148
+ emit('session/event', session, { type: 'turn/end', data: {}, time: time + 1 })
149
+ const afterGreeting = await preStep('你好')
150
+ check('寒暄未被写入', !afterGreeting.includes('你好!'), afterGreeting.length + ' 字符')
151
+
152
+ let failed = 0
153
+ for (const c of checks) {
154
+ if (!c.ok) failed++
155
+ console.log(`${c.ok ? 'PASS' : 'FAIL'} ${c.name}${c.detail ? ' — ' + c.detail : ''}`)
156
+ }
157
+ console.log(failed === 0 ? '\n冒烟通过' : `\n${failed} 项失败`)
158
+ if (logs.length > 0) console.log('日志:', JSON.stringify(logs))
159
+
160
+ await rm(sandbox, { recursive: true, force: true })
161
+ process.exit(failed === 0 ? 0 : 1)