dsh-subagent-memory 0.2.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 ADDED
@@ -0,0 +1,39 @@
1
+ # Agent Memory(子代理记忆)
2
+
3
+ 给**子代理**用的工作记忆——单一职责:没有全局仓库、没有跨会话库、没有浏览器页面。
4
+
5
+ ## 能力
6
+
7
+ **被动(子代理自己用)**:`memory_add` / `memory_list` / `memory_get` 三个工具,只能读写**调用者自己会话**的记忆。
8
+
9
+ **主动(委派方或宿主插件)**:`ctx.agentMemory` 服务 API:
10
+
11
+ | 方法 | 签名 | 说明 |
12
+ |---|---|---|
13
+ | `read` | `read(childId, callerId, {tag?})` | 读某子代理的记忆条目 |
14
+ | `write` | `await write(childId, callerId, {title, content, tags?})` | 往某子代理记忆里写一条 |
15
+ | `summary` | `summary(childId, callerId)` | 紧凑文本摘要,供委派方注入提示 |
16
+
17
+ **API 安全(邻接校验)**:调用者只能触碰**自己直接父级**的子会话记忆——`sessions.get(child).header.parentSession` 必须等于 `callerId`(或 `callerId === childId` 自省);未物化会话、非父子关系一律拒绝。
18
+
19
+ **级联清理**:子代理结束(`subagent/end`)时自动删除其全部记忆。
20
+
21
+ **能力发现**:向 `dsh-compat-layer` 的能力注册表声明 `agentMemory`(v2),其他插件可自动匹配发现,无需硬依赖本包。
22
+
23
+ ## 存储
24
+
25
+ 插件自身的 volatile 设置命名空间 `agent-memory`(官方运行时可变偏好管线),无需文件、无自定义协议。
26
+
27
+ ## 配套
28
+
29
+ 与 `dsh-subagent-pro` 自动匹配:装了本包后 subagent-pro 会多出一个 `subagent_memory` 工具(读某个子代理的记忆摘要);不装则没有该工具,双方互不影响。
30
+
31
+ ## 安装
32
+
33
+ 桌面端:插件 → 添加插件 → 输入包名 `dsh-agent-memory`(或本地路径安装 tgz)。
34
+
35
+ ## English
36
+
37
+ Narrow, subagent-scoped memory: three tools for a delegated child's own notes, an adjacency-checked service API (`read` / `write` / `summary`) for the delegating parent, cascade deletion when the child settles, and capability discovery through dsh-compat-layer. No global store, no browser page.
38
+
39
+ License: MIT. Author: jd962.
@@ -0,0 +1,7 @@
1
+ # Agent Memory — persistent install patch.
2
+ # The settings namespace `agent-memory` (the memory store) is derived from the
3
+ # row's volatile Config; the browser memory page rides the same row.
4
+ - insert:
5
+ - id: agent-memory
6
+ name: 'dsh-subagent-memory'
7
+ config: {}
Binary file
package/icon.svg ADDED
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <rect x="6" y="6" width="52" height="52" rx="8" fill="#1e293b"/>
3
+ <path d="M32 14 L36.5 24.5 L48 25.5 L39.5 33.5 L42 45 L32 39.5 L22 45 L24.5 33.5 L16 25.5 L27.5 24.5 Z" fill="#f59e0b"/>
4
+ </svg>
package/index.js ADDED
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Agent Memory — subagent-scoped memory for DeepSeek Harness.
3
+ *
4
+ * SCOPE (deliberately narrow): only a SUBAGENT's own working memory. There is
5
+ * no global store, no cross-session library, no user lock, and no browser
6
+ * page - those were removed by design so this plugin owns exactly one thing:
7
+ * "what a delegated child learned while doing its job".
8
+ *
9
+ * Two consumption modes:
10
+ * - PASSIVE (the child itself): `memory_add` / `memory_list` / `memory_get`
11
+ * tools write and read the CALLING session's own memory only.
12
+ * - ACTIVE (the delegating parent or any host plugin): the `ctx.agentMemory`
13
+ * service API, which is adjacency-checked - a caller can only touch the
14
+ * memory of a child it actually parents (`sessions.get(child).header.parentSession`
15
+ * must equal the caller's session id, or the caller IS the child).
16
+ *
17
+ * Storage: this plugin's volatile settings namespace (`agent-memory`), the
18
+ * official runtime-mutable preference pipeline. Entries are cascade-deleted
19
+ * when the owning subagent settles (`subagent/end`), so a finished child never
20
+ * leaves orphaned notes behind.
21
+ *
22
+ * Capability discovery: registers into the compat layer's capability registry
23
+ * when that service is present, so other plugins can find this API instead of
24
+ * hard-depending on this package.
25
+ */
26
+ import z from '@deepseek-ai/schemastery'
27
+ import { randomUUID } from 'node:crypto'
28
+ import { Service } from '@deepseek-ai/cordis'
29
+ import { defineTool } from '@deepseek-ai/dsh-tools'
30
+
31
+ /** One subagent memory entry. */
32
+ const MemoryEntrySchema = z.object({
33
+ id: z.string(),
34
+ title: z.string(),
35
+ content: z.string(),
36
+ tags: z.array(z.string()),
37
+ createdAt: z.number(),
38
+ updatedAt: z.number(),
39
+ /** Durable session id of the subagent this entry belongs to. */
40
+ ownerSession: z.string(),
41
+ /** 'subagent' (its own tool call) | 'parent' (service API) | 'user'. */
42
+ origin: z.string().default('subagent'),
43
+ })
44
+
45
+ /** Compact digest bound for prompt injection. */
46
+ const SUMMARY_LIMIT = 4000
47
+
48
+ export default class AgentMemory extends Service {
49
+ static inject = ['tools']
50
+
51
+ static Config = z.object({
52
+ memories: z.array(MemoryEntrySchema).default([]).volatile(),
53
+ })
54
+
55
+ constructor(ctx, config) {
56
+ super(ctx, 'agentMemory')
57
+ this._ctx = ctx
58
+ this._config = config
59
+ this._settings = ctx.get('settings')
60
+
61
+ // Store access mirrors media-input: through the settings pipeline the
62
+ // volatile field is a live read ref (writes go through settings.update);
63
+ // a plain loader config falls back to an in-process shadow array.
64
+ const raw = config?.memories
65
+ if (this._settings !== undefined) {
66
+ this._memories = raw && typeof raw.get === 'function' ? raw : { get: () => (Array.isArray(raw) ? raw : []) }
67
+ this._shadowWrite = undefined
68
+ } else {
69
+ let shadow = Array.isArray(raw) ? raw : []
70
+ this._memories = { get: () => shadow }
71
+ this._shadowWrite = (next) => { shadow = next }
72
+ }
73
+
74
+ this._registerTools(ctx)
75
+ ctx.effect(() => ctx.on('subagent/end', (info) => this._cascadeDelete(info)), 'agent-memory.cascadeDelete')
76
+
77
+ // Capability discovery through the compat layer (optional).
78
+ ctx.inject(['compat'], (scoped) => {
79
+ const registry = scoped.compat?.capabilities
80
+ if (registry === undefined) return
81
+ ctx.effect(() => registry.register({
82
+ name: 'agentMemory',
83
+ version: 2,
84
+ description: 'Subagent-scoped memory: read/write/summarize one child session\'s own notes, adjacency-checked.',
85
+ methods: ['read', 'write', 'summary'],
86
+ }), 'agent-memory.capability')
87
+ })
88
+ }
89
+
90
+ /** Live read of the whole store. */
91
+ _readAll() {
92
+ const value = this._memories.get()
93
+ return Array.isArray(value) ? value : []
94
+ }
95
+
96
+ /** Whole-store write: persistent via settings, else the in-process shadow. */
97
+ async _writeAll(memories) {
98
+ if (this._settings !== undefined) {
99
+ await this._settings.update('agent-memory', { memories })
100
+ return
101
+ }
102
+ this._shadowWrite(memories)
103
+ }
104
+
105
+ /**
106
+ * Adjacency check for the service API: the caller may touch only a child it
107
+ * parents (or itself). Unknown/dead sessions are refused.
108
+ * @param {string} targetSessionId
109
+ * @param {string} callerSessionId
110
+ */
111
+ _assertAdjacent(targetSessionId, callerSessionId) {
112
+ if (typeof targetSessionId !== 'string' || targetSessionId === '') {
113
+ throw new Error('agent-memory: a target session id is required')
114
+ }
115
+ if (typeof callerSessionId !== 'string' || callerSessionId === '') {
116
+ throw new Error('agent-memory: a caller session id is required (API is adjacency-checked)')
117
+ }
118
+ if (targetSessionId === callerSessionId) return
119
+ const sessions = this._ctx.get('sessions')
120
+ if (sessions === undefined) {
121
+ throw new Error('agent-memory: session storage is unavailable; adjacency cannot be verified')
122
+ }
123
+ const child = sessions.get(targetSessionId)
124
+ if (child === undefined) {
125
+ throw new Error(`agent-memory: session "${targetSessionId}" is not materialized; refusing access`)
126
+ }
127
+ if (child.header?.parentSession !== callerSessionId) {
128
+ throw new Error(`agent-memory: session "${targetSessionId}" is not your direct subagent`)
129
+ }
130
+ }
131
+
132
+ /* ------------------------------------------------------------------ *
133
+ * Service API (active use by the parent / other host plugins)
134
+ * ------------------------------------------------------------------ */
135
+
136
+ /**
137
+ * Read one subagent's memory entries.
138
+ * @param {string} childSessionId - the child whose notes are read.
139
+ * @param {string} callerSessionId - the delegating caller (adjacency-checked).
140
+ * @param {{ tag?: string }} [filter]
141
+ */
142
+ read(childSessionId, callerSessionId, filter) {
143
+ this._assertAdjacent(childSessionId, callerSessionId)
144
+ const tag = filter?.tag
145
+ return this._readAll()
146
+ .filter((entry) => entry.ownerSession === childSessionId)
147
+ .filter((entry) => tag === undefined || entry.tags.includes(tag))
148
+ .map((entry) => ({ ...entry, tags: [...entry.tags] }))
149
+ }
150
+
151
+ /**
152
+ * Write one memory entry into a subagent's own store.
153
+ * @param {string} childSessionId
154
+ * @param {string} callerSessionId
155
+ * @param {{ title: string, content: string, tags?: string[], origin?: string }} entry
156
+ * @returns {Promise<object>} the stored entry.
157
+ */
158
+ async write(childSessionId, callerSessionId, entry) {
159
+ this._assertAdjacent(childSessionId, callerSessionId)
160
+ const title = String(entry?.title ?? '').trim()
161
+ if (title === '') throw new Error('agent-memory: an entry title is required')
162
+ const now = Date.now()
163
+ const record = {
164
+ id: randomUUID(),
165
+ title,
166
+ content: String(entry?.content ?? ''),
167
+ tags: (entry?.tags ?? []).map((tag) => String(tag)),
168
+ createdAt: now,
169
+ updatedAt: now,
170
+ ownerSession: childSessionId,
171
+ origin: callerSessionId === childSessionId ? 'subagent' : (entry?.origin ?? 'parent'),
172
+ }
173
+ await this._writeAll([...this._readAll(), record])
174
+ return record
175
+ }
176
+
177
+ /**
178
+ * A compact text digest of one subagent's memory, ready for prompt
179
+ * injection by the delegating side.
180
+ * @param {string} childSessionId
181
+ * @param {string} callerSessionId
182
+ * @returns {string} '' when the child has no memory.
183
+ */
184
+ summary(childSessionId, callerSessionId) {
185
+ const entries = this.read(childSessionId, callerSessionId)
186
+ if (entries.length === 0) return ''
187
+ const lines = [`Subagent memory (${entries.length}):`]
188
+ for (const entry of entries) {
189
+ const tags = entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : ''
190
+ lines.push(`- ${entry.title}${tags}: ${entry.content}`)
191
+ }
192
+ const text = lines.join('\n')
193
+ return text.length > SUMMARY_LIMIT ? `${text.slice(0, SUMMARY_LIMIT)}…` : text
194
+ }
195
+
196
+ /* ------------------------------------------------------------------ *
197
+ * Tools (passive use by the subagent itself)
198
+ * ------------------------------------------------------------------ */
199
+
200
+ _registerTools(ctx) {
201
+ ctx.tools.register(defineTool({
202
+ name: 'memory_add',
203
+ description: 'Remember one note in YOUR OWN memory (this session only). Use it for findings, decisions, and context worth carrying across your steps.',
204
+ parameters: {
205
+ title: { type: 'string', required: true, description: 'Short title.' },
206
+ content: { type: 'string', required: true, description: 'The note body.' },
207
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags for retrieval.' },
208
+ },
209
+ output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: `remembered: ${v.title} (${v.id})` }] },
210
+ execute: async (args, exec) => {
211
+ const sessionId = exec.agent?.session?.header?.id
212
+ if (sessionId === undefined) throw new Error('this tool requires a session')
213
+ const now = Date.now()
214
+ const record = {
215
+ id: randomUUID(),
216
+ title: String(args.title),
217
+ content: String(args.content),
218
+ tags: (args.tags ?? []).map((tag) => String(tag)),
219
+ createdAt: now,
220
+ updatedAt: now,
221
+ ownerSession: sessionId,
222
+ origin: 'subagent',
223
+ }
224
+ await this._writeAll([...this._readAll(), record])
225
+ return record
226
+ },
227
+ }))
228
+
229
+ ctx.tools.register(defineTool({
230
+ name: 'memory_list',
231
+ description: 'List the notes in YOUR OWN memory (this session only).',
232
+ parameters: { tag: { type: 'string', description: 'Optional tag filter.' } },
233
+ output: {
234
+ schema: { type: 'json' },
235
+ render: (_a, v) => [{
236
+ type: 'text',
237
+ text: Array.isArray(v) && v.length
238
+ ? v.map((m) => `- ${m.title} (${m.id}) [${m.contentLength} chars]${m.tags.length ? ` [${m.tags.join(', ')}]` : ''}`).join('\n')
239
+ : '(no memories)',
240
+ }],
241
+ },
242
+ execute: async (args, exec) => {
243
+ const sessionId = exec.agent?.session?.header?.id
244
+ if (sessionId === undefined) throw new Error('this tool requires a session')
245
+ const tag = args.tag === undefined || args.tag === '' ? undefined : String(args.tag)
246
+ return this._readAll()
247
+ .filter((entry) => entry.ownerSession === sessionId)
248
+ .filter((entry) => tag === undefined || entry.tags.includes(tag))
249
+ .map((entry) => ({
250
+ id: entry.id,
251
+ title: entry.title,
252
+ tags: [...entry.tags],
253
+ contentLength: String(entry.content).length,
254
+ createdAt: entry.createdAt,
255
+ updatedAt: entry.updatedAt,
256
+ }))
257
+ },
258
+ }))
259
+
260
+ ctx.tools.register(defineTool({
261
+ name: 'memory_get',
262
+ description: 'Read one note from YOUR OWN memory by id.',
263
+ parameters: { id: { type: 'string', required: true } },
264
+ output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: v === null ? 'not found' : `${v.title}\n${v.content}` }] },
265
+ execute: async (args, exec) => {
266
+ const sessionId = exec.agent?.session?.header?.id
267
+ if (sessionId === undefined) throw new Error('this tool requires a session')
268
+ const found = this._readAll().find((entry) => entry.id === String(args.id) && entry.ownerSession === sessionId)
269
+ return found === undefined ? null : { ...found, tags: [...found.tags] }
270
+ },
271
+ }))
272
+ }
273
+
274
+ /** Cascade-delete a settled subagent's memory. */
275
+ async _cascadeDelete(info) {
276
+ if (info === undefined || info.id === undefined) return
277
+ const arr = this._readAll()
278
+ const next = arr.filter((entry) => entry.ownerSession !== info.id)
279
+ if (next.length !== arr.length) await this._writeAll(next)
280
+ }
281
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "dsh-subagent-memory",
3
+ "description": "Subagent-scoped memory for DeepSeek Harness: memory_add/list/get tools for a delegated child's own notes, a cascade delete when the child settles, and an adjacency-checked service API (ctx.agentMemory read/write/summary) for the delegating parent or other host plugins. No global store, no browser page - one narrow responsibility.",
4
+ "version": "0.2.0",
5
+ "author": "jd962",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "index.js",
9
+ "exports": {
10
+ ".": "./index.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "icon": "./icon.svg",
14
+ "engines": {
15
+ "node": ">=24",
16
+ "dsh": "0.2.0-rc.2"
17
+ },
18
+ "dsh": {
19
+ "manifestVersion": 1,
20
+ "bundle": {
21
+ "patch": "./cordis.patch.yml"
22
+ }
23
+ },
24
+ "peerDependencies": {
25
+ "@deepseek-ai/cordis": "*",
26
+ "@deepseek-ai/dsh-settings": "*",
27
+ "@deepseek-ai/dsh-tools": "*"
28
+ },
29
+ "dependencies": {
30
+ "@deepseek-ai/schemastery": "*"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }