Unified Agent + reusable Go agent core.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add memory integration design drafts

Lyric 2daed465 6c000c5f

+1437
+554
docs/feat/feat_20260401_integration_memory_provider.md
··· 1 + --- 2 + date: 2026-04-01 3 + title: Integration Memory Provider External Contract (V1) 4 + status: draft 5 + --- 6 + 7 + # Integration Memory Provider External Contract (V1) 8 + 9 + ## 1) 目标 10 + 11 + 定义一套尽可能小、稳定、可对外说明的 memory contract,供第三方通过 `integration` 接入自定义 memory 系统。 12 + 13 + 这份文档只关注外部接口与协议,不讨论内部实现细节。 14 + 15 + ## 2) 第一性原理 16 + 17 + 从 runtime 的角度,memory 只需要完成两件事: 18 + 19 + 1. 在本轮 LLM 调用前,提供一段可注入 prompt 的 memory context 20 + 2. 在本轮结束后,接收一条关于本轮交互的 memory record 21 + 22 + 因此,V1 public contract 只定义两个动作: 23 + 24 + - `Prepare` 25 + - `Record` 26 + 27 + 其他能力都不是 V1 必需项: 28 + 29 + - 不单独公开 `ShouldRecord` 30 + - 不公开 `NotifyRecorded` 31 + - 不要求 capabilities negotiation 32 + - 不要求 event export / WAL 订阅 33 + - 不要求定义跨进程固定传输协议 34 + 35 + ## 3) 适用范围 36 + 37 + 本 contract 面向两类接入方式: 38 + 39 + - 使用 `integration` 嵌入 mistermorph 的第三方 Go 程序 40 + - 通过独立二进制、子进程或外部服务接入的第三方系统 41 + 42 + 本 contract 不强制 provider 的实现方式: 43 + 44 + - 可以是本进程内实现 45 + - 可以是 RPC / HTTP / gRPC / subprocess 包装的远程实现 46 + - 可以是数据库前的适配层 47 + 48 + ## 4) 非目标 49 + 50 + V1 不解决以下问题: 51 + 52 + - 不定义数据库 schema 53 + - 不定义 WAL 文件格式 54 + - 不定义 markdown projection 结构 55 + - 不定义 memory 的后台投影、通知、索引任务 56 + - 不定义复杂策略引擎 57 + - 不定义 provider 能力发现协议 58 + 59 + ## 5) 生命周期语义 60 + 61 + ### 5.1 Prepare 62 + 63 + `Prepare` 的语义是: 64 + 65 + “针对当前这一轮请求,生成一段可以直接注入 prompt 的 memory text。” 66 + 67 + 它不是: 68 + 69 + - 通用 memory 查询接口 70 + - 原始事件导出接口 71 + - 写入前校验接口 72 + 73 + 它必须满足: 74 + 75 + - 只读 76 + - 无副作用 77 + - 同步 78 + - 输出可直接注入 prompt 79 + 80 + provider 可以自行决定如何生成这段内容: 81 + 82 + - 只按 `subject_id` 简单召回 83 + - 按 `subject_id + task_text` 做相关检索 84 + - 结合更多上下文字段做复杂检索 85 + 86 + 这些都属于 provider 内部实现自由。 87 + 88 + ### 5.2 Record 89 + 90 + `Record` 的语义是: 91 + 92 + “将本轮交互提交给 memory 系统。” 93 + 94 + 它不是: 95 + 96 + - projector 通知接口 97 + - 事件总线广播接口 98 + - 异步任务编排接口 99 + 100 + provider 可以自行决定如何处理这条 record: 101 + 102 + - 立即持久化 103 + - 异步入队 104 + - 策略跳过 105 + 106 + runtime 只关心返回结果。 107 + 108 + ## 6) Go 接口 109 + 110 + V1 建议的最小 Go 接口如下: 111 + 112 + ```go 113 + type MemoryProvider interface { 114 + Prepare(ctx context.Context, req MemoryPrepareRequest) (MemoryPrepareResult, error) 115 + Record(ctx context.Context, req MemoryRecordRequest) (MemoryRecordResult, error) 116 + } 117 + ``` 118 + 119 + 这就是 V1 的全部核心接口。 120 + 121 + ## 7) 二进制第三方接入 122 + 123 + 除了 Go in-process 接口,V1 也允许通过独立二进制接入。 124 + 125 + 这里的“二进制接入”指: 126 + 127 + - runtime 启动一个外部 provider 进程 128 + - runtime 将 `Prepare` / `Record` 请求发送给该进程 129 + - provider 进程返回对应响应 130 + 131 + V1 不要求唯一传输方式,但推荐一个最小 profile: 132 + 133 + - transport: `stdin/stdout` 134 + - encoding: JSON 135 + - interaction model: request/response 136 + 137 + 推荐原因: 138 + 139 + - 最简单 140 + - 与语言无关 141 + - 易于调试 142 + - 不要求额外网络监听 143 + 144 + ### 7.1 二进制接入的最小语义 145 + 146 + 外部 provider binary 只需要支持两个操作: 147 + 148 + - `prepare` 149 + - `record` 150 + 151 + 每次请求包含: 152 + 153 + - `protocol_version` 154 + - `op` 155 + - `payload` 156 + 157 + 每次响应包含: 158 + 159 + - `protocol_version` 160 + - `ok` 161 + - `result` 或 `error` 162 + 163 + ### 7.2 stdin/stdout profile 建议 164 + 165 + 建议约定: 166 + 167 + - runtime 向 provider 的 `stdin` 写入一条完整 JSON 168 + - provider 从 `stdout` 返回一条完整 JSON 169 + - provider 的日志输出写到 `stderr` 170 + 171 + 如果需要多次请求,可以使用 line-delimited JSON: 172 + 173 + - 一行一个 request 174 + - 一行一个 response 175 + 176 + V1 不要求 provider 支持并发多路复用。最简单实现可以串行处理。 177 + 178 + ## 8) 请求与响应结构 179 + 180 + ### 8.1 Prepare Request 181 + 182 + V1 核心字段: 183 + 184 + ```go 185 + type MemoryPrepareRequest struct { 186 + SubjectID string 187 + SessionID string 188 + RequestContext string 189 + TaskText string 190 + MaxItems int 191 + 192 + Extensions map[string]any 193 + } 194 + ``` 195 + 196 + 字段语义: 197 + 198 + - `SubjectID`: memory 主体标识 199 + - `SessionID`: 当前会话标识 200 + - `RequestContext`: 请求上下文,例如 `private` / `public` 201 + - `TaskText`: 当前任务文本 202 + - `MaxItems`: 建议 provider 控制注入规模的上限 203 + - `Extensions`: 可选扩展字段 204 + 205 + 为什么保留 `Extensions`: 206 + 207 + - 简单 provider 可能只需要 `SubjectID` 208 + - 复杂 provider 可能需要更多上下文 209 + - 不应在 V1 一次性冻结全部字段 210 + 211 + 可能的扩展字段例子: 212 + 213 + - `history` 214 + - `current_message` 215 + - `participants` 216 + - `channel` 217 + - `meta` 218 + 219 + ### 8.2 Prepare Result 220 + 221 + ```go 222 + type MemoryPrepareResult struct { 223 + PromptText string 224 + } 225 + ``` 226 + 227 + 字段语义: 228 + 229 + - `PromptText`: 可直接注入 prompt 的 memory 内容 230 + 231 + 如果 provider 没有命中任何内容,返回空字符串即可。 232 + 233 + ### 8.3 Record Request 234 + 235 + V1 核心字段: 236 + 237 + ```go 238 + type MemoryRecordRequest struct { 239 + SubjectID string 240 + SessionID string 241 + TaskRunID string 242 + RequestContext string 243 + TaskText string 244 + FinalOutput string 245 + RecordedAt time.Time 246 + 247 + Extensions map[string]any 248 + } 249 + ``` 250 + 251 + 字段语义: 252 + 253 + - `SubjectID`: memory 主体标识 254 + - `SessionID`: 当前会话标识 255 + - `TaskRunID`: 当前运行标识 256 + - `RequestContext`: 请求上下文 257 + - `TaskText`: 当前任务文本 258 + - `FinalOutput`: 本轮最终输出文本 259 + - `RecordedAt`: 记录时间 260 + - `Extensions`: 可选扩展字段 261 + 262 + 可能的扩展字段例子: 263 + 264 + - `history` 265 + - `participants` 266 + - `channel` 267 + - `meta` 268 + - `final` 269 + 270 + ### 8.4 Record Result 271 + 272 + ```go 273 + type MemoryRecordResult struct { 274 + Status string 275 + RecordID string 276 + } 277 + ``` 278 + 279 + `Status` 在 V1 建议只允许以下值: 280 + 281 + - `persisted` 282 + - `accepted_async` 283 + - `skipped` 284 + 285 + 字段语义: 286 + 287 + - `persisted`: 已完成持久化 288 + - `accepted_async`: 已接收,后续异步处理 289 + - `skipped`: provider 主动选择不记录 290 + - `RecordID`: 可选的 provider 内部记录标识 291 + 292 + V1 不要求更复杂的状态机。 293 + 294 + ## 9) 最小错误模型 295 + 296 + V1 只区分两类错误: 297 + 298 + - `Prepare` 返回错误 299 + - `Record` 返回错误 300 + 301 + 不把以下概念提升为 public contract: 302 + 303 + - policy failed 304 + - notify failed 305 + - post-record projection failed 306 + 307 + 这些都属于 provider 或 runtime 的内部实现细节。 308 + 309 + ## 10) Runtime 责任 310 + 311 + runtime 对 `MemoryProvider` 的调用责任如下: 312 + 313 + 1. 构造 `MemoryPrepareRequest` 314 + 2. 调用 `Prepare` 315 + 3. 将 `PrepareResult.PromptText` 注入固定 memory prompt block 316 + 4. 执行本轮 LLM 调用 317 + 5. 构造 `MemoryRecordRequest` 318 + 6. 调用 `Record` 319 + 320 + 关键点: 321 + 322 + - 第三方 provider 负责“memory 内容是什么” 323 + - runtime 负责“何时调用”和“怎样注入 prompt” 324 + 325 + 这两者不应混在一起。 326 + 327 + ## 11) Integration 边界要求 328 + 329 + 对第三方公开的 memory contract,应该挂在“memory-aware runtime boundary”上,而不是单纯挂在裸 `agent.Engine` 上。 330 + 331 + 原因: 332 + 333 + - `agent.Engine` 本身不知道 memory 生命周期 334 + - memory 需要在 LLM 调用前后各有一次稳定调用点 335 + - 如果只返回裸 engine,则第三方仍需自行保证这两个调用时机 336 + 337 + 因此,V1 对外要求的是: 338 + 339 + - `integration` 必须提供一个会自动调用 `Prepare` / `Record` 的运行边界 340 + 341 + 至于具体是: 342 + 343 + - 新的 `RunTaskWithMemory(...)` 344 + - 新的 runtime option 345 + - 还是新的 prepared runner 类型 346 + 347 + 这属于后续 API 设计问题,不在本文强行定死。 348 + 349 + ## 12) 二进制边界要求 350 + 351 + 如果第三方通过独立二进制接入,runtime 也必须提供一个对应的 provider adapter,使其能把二进制请求映射回同一套 `MemoryProvider` 语义。 352 + 353 + 也就是说: 354 + 355 + - Go embedding 看到的是 `MemoryProvider` 356 + - binary provider 看到的是 `prepare` / `record` 请求 357 + - runtime 负责在两者之间适配 358 + 359 + 关键点: 360 + 361 + - 外部 binary 不需要理解内部 hook 名称 362 + - 外部 binary 不需要直接参与 prompt 拼接 363 + - 外部 binary 只负责“生成 memory context”与“接收 memory record” 364 + 365 + ## 13) 协议映射原则 366 + 367 + 如果 provider 在进程外实现,任何 wire protocol 都应能无损表达同样的语义: 368 + 369 + - `Prepare(request) -> result` 370 + - `Record(request) -> result` 371 + 372 + V1 不强制规定协议形态,但建议遵守以下原则: 373 + 374 + - 显式版本号 375 + - 请求/响应一一对应 376 + - 核心字段与 Go struct 语义一致 377 + - 可选字段放扩展区,避免频繁破坏协议 378 + 379 + ## 14) JSON 协议示例 380 + 381 + 这不是强制协议,只是说明语义。 382 + 383 + ### 14.1 Prepare Request 384 + 385 + ```json 386 + { 387 + "protocol_version": "v1", 388 + "op": "prepare", 389 + "subject_id": "user:42", 390 + "session_id": "slack:T1:C1", 391 + "request_context": "private", 392 + "task_text": "reply to this message", 393 + "max_items": 20, 394 + "extensions": {} 395 + } 396 + ``` 397 + 398 + ### 14.2 Prepare Result 399 + 400 + ```json 401 + { 402 + "protocol_version": "v1", 403 + "ok": true, 404 + "prompt_text": "[Memory]\\n- user prefers concise replies" 405 + } 406 + ``` 407 + 408 + ### 14.3 Record Request 409 + 410 + ```json 411 + { 412 + "protocol_version": "v1", 413 + "op": "record", 414 + "subject_id": "user:42", 415 + "session_id": "slack:T1:C1", 416 + "task_run_id": "run_abc", 417 + "request_context": "private", 418 + "task_text": "reply to this message", 419 + "final_output": "Here is the final reply.", 420 + "recorded_at": "2026-04-01T10:00:00Z", 421 + "extensions": {} 422 + } 423 + ``` 424 + 425 + ### 14.4 Record Result 426 + 427 + ```json 428 + { 429 + "protocol_version": "v1", 430 + "ok": true, 431 + "status": "persisted", 432 + "record_id": "mem_123" 433 + } 434 + ``` 435 + 436 + ### 14.5 Error Result 437 + 438 + ```json 439 + { 440 + "protocol_version": "v1", 441 + "ok": false, 442 + "error": { 443 + "code": "provider_unavailable", 444 + "message": "database timeout" 445 + } 446 + } 447 + ``` 448 + 449 + ## 15) 第三方系统映射示例 450 + 451 + ### 15.1 MemoryOS 类系统 452 + 453 + 像 MemoryOS 这样的外部 memory 系统,接入时不需要让 runtime 直接理解它的内部模块或工具名。 454 + 455 + 只需要在它前面包一层 adapter,使其对 runtime 暴露同一套 V1 contract: 456 + 457 + - `Prepare` 458 + - `Record` 459 + 460 + 推荐映射方式: 461 + 462 + - `Prepare` 463 + - 输入:`subject_id`、`session_id`、`task_text`、`request_context` 464 + - adapter 调用外部系统的检索能力 465 + - adapter 将检索结果整理成 `PromptText` 466 + - `Record` 467 + - 输入:`subject_id`、`session_id`、`task_run_id`、`task_text`、`final_output` 468 + - adapter 调用外部系统的写入能力 469 + - adapter 返回 `persisted` / `accepted_async` / `skipped` 470 + 471 + 关键点: 472 + 473 + - runtime 不直接调用外部系统自己的 MCP/tool 名称 474 + - runtime 不把 prompt 拼接责任交给外部系统 475 + - runtime 只依赖 `PrepareResult.PromptText` 和 `RecordResult.Status` 476 + 477 + 如果外部系统本身已经提供: 478 + 479 + - 读取相关记忆 480 + - 写入对话记忆 481 + 482 + 那么它就可以很自然地被包成一个 binary provider。 483 + 484 + ### 15.2 为什么示例只做“映射”而不做“直连” 485 + 486 + 因为不同第三方系统的内部模型不同: 487 + 488 + - 有的系统是 query/retrieve 风格 489 + - 有的系统是 profile/facts 风格 490 + - 有的系统把 response generation 也包进 memory 框架里 491 + 492 + runtime 不应该把这些内部差异暴露进自己的 public contract。 493 + 494 + V1 的正确边界是: 495 + 496 + - 第三方系统内部做自己的事情 497 + - adapter 负责把它翻译成 `Prepare` / `Record` 498 + 499 + ## 16) 为什么不在 V1 引入更多概念 500 + 501 + ### 13.1 不引入 `MemoryPolicy` 502 + 503 + 因为 runtime 真正需要的不是一个“策略对象”,而是: 504 + 505 + - 调用 `Prepare` 506 + - 调用 `Record` 507 + 508 + 是否记录可以先由 runtime 自己决定,或者由 provider 在 `Record` 中返回 `skipped`。 509 + 510 + ### 13.2 不引入 `NotifyRecorded` 511 + 512 + 因为第三方真正关心的是: 513 + 514 + - 这次 record 是否被接受 515 + - 是否已经持久化 516 + 517 + 这已经可以通过 `MemoryRecordResult.Status` 表达。 518 + 519 + ### 13.3 不引入 capabilities 520 + 521 + 因为 V1 只有两个动作,必要信息已经在 request / result 中。 522 + 523 + 只有当未来真的出现协商需求时,再加 capabilities 更合理。 524 + 525 + ### 13.4 不引入 event export 526 + 527 + 因为“给 provider 提供 prepare/record”与“导出完整 memory 事件流”是两件不同的事。 528 + 529 + 后者未来可以单独定义,不应混入 V1 核心 contract。 530 + 531 + ## 17) 兼容性要求 532 + 533 + V1 需要满足: 534 + 535 + 1. 第一方本地 memory 实现可以适配为 `MemoryProvider` 536 + 2. 第三方 hosted memory 实现也可以适配为 `MemoryProvider` 537 + 3. `Extensions` 允许未来平滑扩展 538 + 4. V1 不依赖当前内部 hook 名称 539 + 5. 第三方 binary provider 也可以映射到同一套语义 540 + 541 + 重点是: 542 + 543 + - 对外暴露的是稳定语义 544 + - 不是内部 wiring 名称 545 + 546 + ## 18) 后续工作 547 + 548 + 在本文基础上,下一步只需要补三件事: 549 + 550 + 1. `integration` 对外 API 落点 551 + 2. 第一方 local provider 适配层 552 + 3. 第三方 hosted / binary provider 示例 553 + 554 + 在这三件事完成前,不继续扩展更多概念。
+689
docs/feat/feat_20260403_memory_plugin_api.md
··· 1 + --- 2 + date: 2026-04-04 3 + title: Memory Plugin API (Phase 1) 4 + status: draft 5 + --- 6 + 7 + # Memory Plugin API (Phase 1) 8 + 9 + 这份文档定义 Phase 1 的外部 plugin 协议。 10 + 11 + 前置概念映射见: 12 + 13 + - `docs/feat/feat_20260403_nmem_mapping.md` 14 + 15 + ## 1. Scope 16 + 17 + Phase 1 只冻结 5 个 hook: 18 + 19 + 1. `memory.prepare` 20 + 2. `memory.stm.upsert` 21 + 3. `topic.upsert` 22 + 4. `topic.append` 23 + 5. `topic.delete` 24 + 25 + Phase 1 不做: 26 + 27 + - 长期 memory 28 + - Telegram topic 映射 29 + - 通用 event export 30 + - 原始 WAL event 直出给 plugin 31 + 32 + ## 2. First Principles 33 + 34 + ### 2.1 协议只暴露已经成形的对象 35 + 36 + plugin 不应该消费内部 journal event。 37 + 38 + Phase 1 对外只暴露两个对象: 39 + 40 + - `memory` 41 + - 当前只指短期 markdown 文件 42 + - `topic` 43 + - 当前统一表达 Console topic 和 Slack thread 44 + 45 + ### 2.2 hook 名跟我们的对象走 46 + 47 + 所以协议层用: 48 + 49 + - `topic.*` 50 + 51 + 而不是: 52 + 53 + - `thread.*` 54 + 55 + `nmem` 侧如果需要,再把 `topic` 映射成它的 `thread`。 56 + 57 + ### 2.3 Phase 1 只保留最少字段 58 + 59 + 如果一个字段可以稳定推导,就先不冻结进 schema。 60 + 61 + 例如: 62 + 63 + - `memory.prepare` 不要求 `session_id` 64 + - `memory.prepare` 不要求 `runtime` 65 + - `memory.prepare` 不要求 `request_context` 66 + - `memory.stm.upsert` 不要求 `date` 67 + - `memory.stm.upsert` 不要求 `summary` 68 + - `topic.append` 不要求 `runtime` 69 + 70 + ## 3. Shared Conventions 71 + 72 + ### 3.1 IDs 73 + 74 + 短期 `memory_id`: 75 + 76 + - `stm:<YYYY-MM-DD>:<subject_id>` 77 + 78 + 例子: 79 + 80 + - `stm:2026-04-04:console:topic_123` 81 + - `stm:2026-04-04:slack--t1--c1` 82 + - `stm:2026-04-04:tg:-1001234567890` 83 + 84 + `topic_id`: 85 + 86 + - Console: `console:<topic_id>` 87 + - Slack: `slack:<team_id>:<channel_id>:thread:<thread_ts>` 88 + 89 + ### 3.2 caller-supplied ID 是前提 90 + 91 + Phase 1 的标准方案要求 backend 直接接受我们给出的 ID。 92 + 93 + 也就是说: 94 + 95 + - `memory_id` 96 + - `topic_id` 97 + 98 + 应直接成为 backend 里的 canonical ID。 99 + 100 + Phase 1 不接受“adapter 自己维护外部 ID 到 backend 内部 ID 的映射表”作为标准方案。 101 + 102 + ### 3.3 Wire Format 103 + 104 + Phase 1 不再使用 `input/output` 包裹层。 105 + 106 + 请求统一是扁平 JSON: 107 + 108 + ```json 109 + { 110 + "protocol_version": "v1", 111 + "hook": "memory.prepare", 112 + "...": "hook 自己的字段" 113 + } 114 + ``` 115 + 116 + 成功响应也统一是扁平 JSON: 117 + 118 + - 读操作: 119 + 120 + ```json 121 + { 122 + "ok": true, 123 + "...": "hook 自己的返回字段" 124 + } 125 + ``` 126 + 127 + - 写操作: 128 + 129 + ```json 130 + { 131 + "ok": true 132 + } 133 + ``` 134 + 135 + 错误响应: 136 + 137 + ```json 138 + { 139 + "ok": false, 140 + "code": "invalid_input", 141 + "message": "subject_id is required" 142 + } 143 + ``` 144 + 145 + 共享错误响应 schema: 146 + 147 + ```json 148 + { 149 + "type": "object", 150 + "additionalProperties": false, 151 + "required": ["ok", "code", "message"], 152 + "properties": { 153 + "ok": { "const": false }, 154 + "code": { "type": "string", "minLength": 1 }, 155 + "message": { "type": "string", "minLength": 1 } 156 + } 157 + } 158 + ``` 159 + 160 + ## 4. `memory.prepare` 161 + 162 + ### 4.1 语义 163 + 164 + - 用当前用户输入做召回 165 + - 返回一段可直接塞进现有 memory prompt slot 的文本 166 + - 只读,无副作用 167 + 168 + ### 4.2 Request Schema 169 + 170 + ```json 171 + { 172 + "type": "object", 173 + "additionalProperties": false, 174 + "required": ["protocol_version", "hook", "subject_id", "task_text"], 175 + "properties": { 176 + "protocol_version": { "const": "v1" }, 177 + "hook": { "const": "memory.prepare" }, 178 + "subject_id": { "type": "string", "minLength": 1 }, 179 + "task_text": { "type": "string" }, 180 + "max_items": { "type": "integer", "minimum": 1 } 181 + } 182 + } 183 + ``` 184 + 185 + ### 4.3 Response Schema 186 + 187 + ```json 188 + { 189 + "type": "object", 190 + "additionalProperties": false, 191 + "required": ["ok", "prompt_text"], 192 + "properties": { 193 + "ok": { "const": true }, 194 + "prompt_text": { "type": "string" } 195 + } 196 + } 197 + ``` 198 + 199 + ### 4.4 `nmem` 连线 200 + 201 + 最小推荐调用: 202 + 203 + ```bash 204 + nmem --json m search "<task_text>" -n <max_items> \ 205 + -l "mm:subject:<subject_id>" 206 + ``` 207 + 208 + 说明: 209 + 210 + - Phase 1 只要求一个最小 scope label: 211 + - `mm:subject:<subject_id>` 212 + - 不回退到 `t search` 213 + 214 + 如果 `m search` 返回信息不足,再补: 215 + 216 + ```bash 217 + nmem --json m show <memory_id> --content-limit <chars> 218 + ``` 219 + 220 + `prompt_text` 推荐格式: 221 + 222 + ```text 223 + <Memory:ShortTerm:Recent> 224 + - 2026-04-04: discussed release plan 225 + - 2026-04-03: reviewed deploy rollback 226 + ``` 227 + 228 + 日期优先从 `memory_id` 解析。 229 + 230 + ### 4.5 示例 231 + 232 + 请求: 233 + 234 + ```json 235 + { 236 + "hook": "memory.prepare", 237 + "protocol_version": "v1", 238 + "subject_id": "console:topic_123", 239 + "task_text": "summarize the release risks", 240 + "max_items": 8 241 + } 242 + ``` 243 + 244 + 响应: 245 + 246 + ```json 247 + { 248 + "ok": true, 249 + "prompt_text": "<Memory:ShortTerm:Recent>\n- 2026-04-04: discussed release plan" 250 + } 251 + ``` 252 + 253 + ## 5. `memory.stm.upsert` 254 + 255 + ### 5.1 语义 256 + 257 + - 把一整份短期 markdown 文件同步给 plugin 258 + - 最小单位是整个文件,不是单条 summary item 259 + 260 + ### 5.2 Request Schema 261 + 262 + ```json 263 + { 264 + "type": "object", 265 + "additionalProperties": false, 266 + "required": ["protocol_version", "hook", "memory_id", "subject_id", "markdown"], 267 + "properties": { 268 + "protocol_version": { "const": "v1" }, 269 + "hook": { "const": "memory.stm.upsert" }, 270 + "memory_id": { "type": "string", "minLength": 1 }, 271 + "subject_id": { "type": "string", "minLength": 1 }, 272 + "markdown": { "type": "string" }, 273 + "updated_at": { "type": "string", "format": "date-time" }, 274 + "source_relpath": { "type": "string" } 275 + } 276 + } 277 + ``` 278 + 279 + ### 5.3 Response Schema 280 + 281 + ```json 282 + { 283 + "type": "object", 284 + "additionalProperties": false, 285 + "required": ["ok"], 286 + "properties": { 287 + "ok": { "const": true } 288 + } 289 + } 290 + ``` 291 + 292 + ### 5.4 `nmem` 连线 293 + 294 + 先从 markdown 里解析 frontmatter。 295 + 296 + `nmem m add` / `m update` 里的 `-t` 应优先使用短期 memory 文件 frontmatter 里的 `summary`。 297 + 298 + 如果 frontmatter 里没有 `summary`: 299 + 300 + - 可以省略 `-t` 301 + - 不建议回退成 `memory_id` 302 + 303 + 前提条件: 304 + 305 + - `nmem m add --id <memory_id>` 已可用 306 + 307 + 创建: 308 + 309 + ```bash 310 + nmem --json m add "<markdown>" \ 311 + --id "<memory_id>" \ 312 + -t "<frontmatter_summary>" \ 313 + --unit-type context \ 314 + -l "mm:subject:<subject_id>" 315 + ``` 316 + 317 + 更新: 318 + 319 + ```bash 320 + nmem --json m update <memory_id> -c "<markdown>" -t "<frontmatter_summary>" 321 + ``` 322 + 323 + ### 5.5 示例 324 + 325 + ```json 326 + { 327 + "hook": "memory.stm.upsert", 328 + "protocol_version": "v1", 329 + "memory_id": "stm:2026-04-04:console:topic_123", 330 + "subject_id": "console:topic_123", 331 + "markdown": "---\nsummary: discussed release plan\n---\n\n- ...", 332 + "updated_at": "2026-04-04T09:20:00Z" 333 + } 334 + ``` 335 + 336 + ## 6. `topic.upsert` 337 + 338 + ### 6.1 语义 339 + 340 + - 保证一个 topic 在 plugin/backend 里存在 341 + - 用于 create-if-missing 342 + - 也允许顺手更新轻量 metadata,例如 title 343 + 344 + ### 6.2 Request Schema 345 + 346 + ```json 347 + { 348 + "type": "object", 349 + "additionalProperties": false, 350 + "required": ["protocol_version", "hook", "topic_id"], 351 + "properties": { 352 + "protocol_version": { "const": "v1" }, 353 + "hook": { "const": "topic.upsert" }, 354 + "topic_id": { "type": "string", "minLength": 1 }, 355 + "title": { "type": "string" }, 356 + "source_ref": { "type": "string" } 357 + } 358 + } 359 + ``` 360 + 361 + ### 6.3 Response Schema 362 + 363 + ```json 364 + { 365 + "type": "object", 366 + "additionalProperties": false, 367 + "required": ["ok"], 368 + "properties": { 369 + "ok": { "const": true } 370 + } 371 + } 372 + ``` 373 + 374 + ### 6.4 `nmem` 连线 375 + 376 + `topic.upsert` 的职责是 ensure-exists。 377 + 378 + 注意: 379 + 380 + - `nmem t` 没有 `update` 381 + - topic 侧的后续变更统一走 `t append` 382 + 383 + 推荐流程: 384 + 385 + 1. 先看 topic 是否已存在 386 + 2. 不存在再 create 387 + 3. 已存在则 no-op 388 + 389 + 查询: 390 + 391 + ```bash 392 + nmem --json t show <topic_id> -n 1 393 + ``` 394 + 395 + 创建: 396 + 397 + ```bash 398 + nmem --json t create \ 399 + --id "<topic_id>" \ 400 + -t "<title>" 401 + ``` 402 + 403 + 如果需要记录来源,可以在 `t create` 里额外传: 404 + 405 + ```bash 406 + -s "<source_ref>" 407 + ``` 408 + 409 + ### 6.5 示例 410 + 411 + ```json 412 + { 413 + "hook": "topic.upsert", 414 + "protocol_version": "v1", 415 + "topic_id": "console:topic_123", 416 + "title": "Release Plan", 417 + "source_ref": "tasks/console/log/2026-04-04_topic_123.jsonl" 418 + } 419 + ``` 420 + 421 + ## 7. `topic.append` 422 + 423 + ### 7.1 语义 424 + 425 + - 向一个已有 topic 追加新消息 426 + - payload 是增量消息,不是完整快照 427 + 428 + ### 7.2 Request Schema 429 + 430 + ```json 431 + { 432 + "type": "object", 433 + "additionalProperties": false, 434 + "required": ["protocol_version", "hook", "topic_id", "messages"], 435 + "properties": { 436 + "protocol_version": { "const": "v1" }, 437 + "hook": { "const": "topic.append" }, 438 + "topic_id": { "type": "string", "minLength": 1 }, 439 + "messages": { 440 + "type": "array", 441 + "items": { 442 + "type": "object", 443 + "additionalProperties": false, 444 + "required": ["kind", "sent_at", "text"], 445 + "properties": { 446 + "kind": { 447 + "enum": [ 448 + "inbound_user", 449 + "inbound_reaction", 450 + "outbound_agent", 451 + "outbound_reaction", 452 + "system" 453 + ] 454 + }, 455 + "sent_at": { "type": "string", "format": "date-time" }, 456 + "text": { "type": "string" }, 457 + "message_id": { "type": "string" }, 458 + "reply_to_message_id": { "type": "string" }, 459 + "sender": { 460 + "type": "object", 461 + "additionalProperties": false, 462 + "properties": { 463 + "user_id": { "type": "string" }, 464 + "username": { "type": "string" }, 465 + "nickname": { "type": "string" }, 466 + "is_bot": { "type": "boolean" }, 467 + "display_ref": { "type": "string" } 468 + } 469 + } 470 + } 471 + } 472 + } 473 + } 474 + } 475 + ``` 476 + 477 + ### 7.3 Response Schema 478 + 479 + ```json 480 + { 481 + "type": "object", 482 + "additionalProperties": false, 483 + "required": ["ok"], 484 + "properties": { 485 + "ok": { "const": true } 486 + } 487 + } 488 + ``` 489 + 490 + ### 7.4 `nmem` 连线 491 + 492 + 直接追加增量消息: 493 + 494 + ```bash 495 + nmem --json t append <topic_id> -m '<messages_json>' 496 + ``` 497 + 498 + `t append` 不支持 `-s`,也不需要 `-s`。 499 + 500 + ### 7.5 示例 501 + 502 + ```json 503 + { 504 + "hook": "topic.append", 505 + "protocol_version": "v1", 506 + "topic_id": "console:topic_123", 507 + "messages": [ 508 + { 509 + "kind": "inbound_user", 510 + "sent_at": "2026-04-04T09:00:00Z", 511 + "text": "summarize the release risks" 512 + }, 513 + { 514 + "kind": "outbound_agent", 515 + "sent_at": "2026-04-04T09:00:10Z", 516 + "text": "here are the release risks" 517 + } 518 + ] 519 + } 520 + ``` 521 + 522 + ## 8. `topic.delete` 523 + 524 + ### 8.1 语义 525 + 526 + - 删除一个 topic 527 + 528 + Phase 1 主要对应: 529 + 530 + - Console topic 删除 531 + 532 + ### 8.2 Request Schema 533 + 534 + ```json 535 + { 536 + "type": "object", 537 + "additionalProperties": false, 538 + "required": ["protocol_version", "hook", "topic_id"], 539 + "properties": { 540 + "protocol_version": { "const": "v1" }, 541 + "hook": { "const": "topic.delete" }, 542 + "topic_id": { "type": "string", "minLength": 1 } 543 + } 544 + } 545 + ``` 546 + 547 + ### 8.3 Response Schema 548 + 549 + ```json 550 + { 551 + "type": "object", 552 + "additionalProperties": false, 553 + "required": ["ok"], 554 + "properties": { 555 + "ok": { "const": true } 556 + } 557 + } 558 + ``` 559 + 560 + ### 8.4 `nmem` 连线 561 + 562 + ```bash 563 + nmem --json t delete <topic_id> -f 564 + ``` 565 + 566 + ### 8.5 示例 567 + 568 + ```json 569 + { 570 + "hook": "topic.delete", 571 + "protocol_version": "v1", 572 + "topic_id": "console:topic_123" 573 + } 574 + ``` 575 + 576 + ## 9. Runtime Matrix 577 + 578 + | Runtime | `memory.prepare` | `memory.stm.upsert` | `topic.upsert` | `topic.append` | `topic.delete` | 579 + | --- | --- | --- | --- | --- | --- | 580 + | Telegram | 支持 | 支持 | 不支持 | 不支持 | 不支持 | 581 + | Console | 支持 | 支持 | 支持 | 支持 | 支持 | 582 + | Slack | 支持 | 支持 | 支持 | 支持 | 暂不要求 | 583 + 584 + ## 10. Implementation Tasks 585 + 586 + ### 10.1 协议与类型 587 + 588 + 建议落点: 589 + 590 + - `integration/memoryplugin/` 591 + - `schema/integration/memory-plugin/v1/` 592 + 593 + 任务: 594 + 595 + 1. 定义 5 个 hook 常量 596 + 2. 定义扁平请求/响应格式 597 + 3. 定义最小 input/output types 598 + 4. 落实际 `.schema.json` 文件 599 + 5. 确认 `nmem m add --id` 已可用 600 + 601 + ### 10.2 `memory.prepare` 602 + 603 + 代码落点: 604 + 605 + - `internal/channelruntime/taskruntime/runtime.go` 606 + 607 + 任务: 608 + 609 + 1. 在现有 memory 注入点前调用 plugin 610 + 2. 将 `prompt_text` 直接接进现有 memory prompt block 611 + 3. plugin 失败只记日志,不阻断主流程 612 + 613 + ### 10.3 `memory.stm.upsert` 614 + 615 + 代码落点: 616 + 617 + - `memory/projector.go` 618 + 619 + 任务: 620 + 621 + 1. `WriteShortTerm(...)` 成功后拿到最新 markdown 622 + 2. 计算 `memory_id` 623 + 3. 发 `memory.stm.upsert` 624 + 4. plugin 失败不影响本地投影成功 625 + 626 + ### 10.4 Console topic 627 + 628 + 代码落点: 629 + 630 + - `cmd/mistermorph/consolecmd/local_runtime_history.go` 631 + - `internal/daemonruntime/console_store.go` 632 + - `internal/daemonruntime/server.go` 633 + 634 + 任务: 635 + 636 + 1. topic 首次出现时发 `topic.upsert` 637 + 2. 每轮新增 turn 后发 `topic.append` 638 + 3. 删除 topic 时发 `topic.delete` 639 + 640 + ### 10.5 Slack thread 641 + 642 + 代码落点: 643 + 644 + - `internal/channelruntime/slack/runtime.go` 645 + - `internal/channelruntime/slack/runtime_task.go` 646 + 647 + 任务: 648 + 649 + 1. 只在存在真实 `thread_ts` 时生成 `topic_id` 650 + 2. 首次出现 thread 时发 `topic.upsert` 651 + 3. 每轮新增消息后发 `topic.append` 652 + 4. 若未开启 task persistence,则回退到 live runtime hook 653 + 654 + ### 10.6 测试 655 + 656 + 至少补这些: 657 + 658 + 1. protocol types roundtrip 659 + 2. `memory_id` 规则 660 + 3. `topic_id` 规则 661 + 4. projector 成功写文件后触发 `memory.stm.upsert` 662 + 5. console topic create/append/delete 663 + 6. slack thread create/append 664 + 7. plugin 失败不阻断主流程 665 + 666 + ## 11. 推荐顺序 667 + 668 + 1. 先定 5 个 hook 和 schema 669 + 2. 接 `memory.prepare` 670 + 3. 接 `memory.stm.upsert` 671 + 4. 先做 Console `topic.upsert/append/delete` 672 + 5. 再做 Slack `topic.upsert/append` 673 + 674 + ## 12. 一句话总结 675 + 676 + Phase 1 的协议只做两类对象: 677 + 678 + - `memory` 679 + - 只处理短期文件 680 + - `topic` 681 + - 统一表达 Console topic 和 Slack thread 682 + 683 + 对应 5 个 hook: 684 + 685 + - `memory.prepare` 686 + - `memory.stm.upsert` 687 + - `topic.upsert` 688 + - `topic.append` 689 + - `topic.delete`
+194
docs/feat/feat_20260403_nmem_mapping.md
··· 1 + # MisterMorph 的 nmem 映射结论 2 + 3 + 这份文档只写当前已经达成的约定。 4 + 5 + ## 1. 分阶段范围 6 + 7 + ### Phase 1 8 + 9 + 只做下面两件事: 10 + 11 + 1. 短期记忆文件 <-> `nmem memory` 12 + 2. `console topic` / 真实 `slack thread` <-> `nmem thread` 13 + 14 + ### Phase 2 15 + 16 + 再研究下面这些问题: 17 + 18 + - 长期记忆如何映射成 `nmem memory` 19 + - 长期记忆如何稳定地产生 `memory_id` 20 + - `sourceThreadId` 如何回连到长期记忆 21 + 22 + ## 2. 先记住四条总规则 23 + 24 + 1. `subject_id` 不是 `memory_id` 25 + 2. `task_run_id` / `event_id` 不是 `thread_id` 26 + 3. `memory/log/*.jsonl` 是 WAL,不是 thread store 27 + 4. `nmem` 不需要 `bucket_id` 28 + 29 + 说明: 30 + 31 + - `bucket_id` 如果被提到,只是内部思考“短期记忆按谁分桶”的术语 32 + - 对外协议里只需要 `memory_id` 和 `thread_id` 33 + 34 + ## 3. Phase 1 的 `nmem memory` 35 + 36 + Phase 1 里,`nmem memory` 只对应短期记忆文件。 37 + 38 + 来源: 39 + 40 + - `memory/YYYY-MM-DD/*.md` 41 + 42 + 映射规则: 43 + 44 + - 每个短期 markdown 文件,对应一条 synthetic `nmem memory` 45 + 46 + 结论: 47 + 48 + - 短期 memory 的最小单位是“整个文件” 49 + - 不是单条 summary line 50 + 51 + ## 4. 当前实现 vs 目标实现 52 + 53 + ### 4.1 当前实现 54 + 55 + 现在短期文件名的底层构造函数是: 56 + 57 + - `memory/YYYY-MM-DD/{sanitize(sessionID)}.md` 58 + 59 + 但 projector 实际上传进去的是 `event.subject_id`,不是 `event.session_id`。 60 + 61 + 所以当前真实效果是: 62 + 63 + - `memory/YYYY-MM-DD/{sanitize(subject_id)}.md` 64 + 65 + ### 4.2 Phase 1 目标实现 66 + 67 + Phase 1 不改变短期 memory 的聚合边界。 68 + 69 + 继续沿用当前规则: 70 + 71 + - 短期 memory 文件按 `subject_id` 分桶 72 + - 不因为 runtime 里存在 thread,就把短期 memory 再拆成 thread-scoped 文件 73 + 74 + 这意味着: 75 + 76 + - 短期 memory 和 `nmem thread` 可以不是一一对应关系 77 + - `nmem thread` 是原始对话容器 78 + - 短期 `nmem memory` 是当前系统已有的摘要文件 79 + 80 + ## 5. 短期 `memory_id` 规则 81 + 82 + Phase 1 里,短期 `memory_id` 用确定性规则生成: 83 + 84 + - `stm:<YYYY-MM-DD>:<subject_id>` 85 + 86 + 例子: 87 + 88 + - Console 89 + - `stm:2026-04-04:console:topic_123` 90 + - Slack thread 91 + - 当前短期 memory 仍然按 channel 聚合,因此是 `stm:2026-04-04:slack--t1--c1` 92 + - Telegram 93 + - `stm:2026-04-04:tg:-1001234567890` 94 + 95 + ## 6. Phase 1 的 `nmem thread` 96 + 97 + `nmem thread` 只对应真实存在的 runtime 对话容器。 98 + 99 + ### 6.1 Console 100 + 101 + 映射: 102 + 103 + - `console topic -> nmem thread` 104 + 105 + `thread_id`: 106 + 107 + - `console:<topic_id>` 108 + 109 + raw 数据来源: 110 + 111 + - `tasks/console/topic.json` 112 + - `tasks/console/log/YYYY-MM-DD_<topic_key>.jsonl` 113 + 114 + ### 6.2 Slack 115 + 116 + 映射: 117 + 118 + - 真实 `slack thread -> nmem thread` 119 + 120 + `thread_id`: 121 + 122 + - `slack:<team_id>:<channel_id>:thread:<thread_ts>` 123 + 124 + raw 数据来源: 125 + 126 + - `tasks/slack/log/tasks.jsonl` 127 + 128 + 补充说明: 129 + 130 + - Slack 的 runtime history 已经是 thread-aware 131 + - 但当前 memory persistence 仍然是 channel-scoped 132 + - Phase 1 不要求让短期 memory 与 Slack thread 对齐 133 + - 也就是说: 134 + - 短期 memory 继续按 channel 聚合 135 + - `nmem thread` 单独按真实 Slack thread 建立 136 + 137 + ### 6.3 Telegram 138 + 139 + Phase 1 不要求支持 Telegram -> `nmem thread`。 140 + 141 + 原因: 142 + 143 + - Telegram 当前没有真正独立的 thread 对应物 144 + - 现在只有 chat-scoped 语义: 145 + - `subject_id = tg:<chat_id>` 146 + - `session_id = tg:<chat_id>` 147 + 148 + ## 7. `slack:<team>:<channel>:thread:<thread_ts>` 格式约定 149 + 150 + 这个格式作为通用 internal id / `thread_id` 是合理的。 151 + 152 + 原因: 153 + 154 + - 我们通用的 reference id 规则本质上是 `protocol:id` 155 + - `id` 部分允许继续带 `:` 156 + - 所以 `slack:T1:C1:thread:1739667600.000100` 是合法的 generic refid 157 + 158 + 但要注意: 159 + 160 + - 它今天还不是现成可用的 `contacts_send chat_id` hint 161 + - 因为当前 Slack chat hint parser 只支持 `slack:<team_id>:<channel_id>` 162 + 163 + 所以: 164 + 165 + - 它可以直接作为 `thread_id` 166 + - 但不要默认它已经是当前 contacts/chat hint 体系里的现成格式 167 + 168 + ## 8. 各 runtime 对照表 169 + 170 + | Runtime | Phase 1 `nmem memory` | Phase 1 `nmem thread` | 171 + | --- | --- | --- | 172 + | Telegram | 按 `subject_id` 聚合的短期文件 | 无 | 173 + | Console | 按 `subject_id` 聚合的短期文件 | `console topic` | 174 + | Slack | 按 `subject_id` 聚合的短期文件 | 真实 Slack thread | 175 + 176 + ## 9. 对实现的直接要求 177 + 178 + Phase 1 至少要补下面几件事: 179 + 180 + 1. 短期 `memory_id` 按 `日期 + subject_id` 生成 181 + 2. `console topic` / 真实 `slack thread` 分别建立 `thread_id` 182 + 3. 如果某个 runtime 没开 task persistence: 183 + - `thread` 同步不能依赖本地文件 replay 184 + - 只能依赖 live runtime hook 185 + 186 + ## 10. 一句话总结 187 + 188 + Phase 1 的原则就是: 189 + 190 + - `memory` 只处理短期文件 191 + - `thread` 只处理真实会话容器 192 + - `memory_id` 用确定性规则生成 193 + - 短期 memory 继续按当前 `subject_id` 聚合,不按 thread 再拆 194 + - `thread_id` 直接复用 runtime 的原始对话容器 identity