# 记忆与状态：什么时候写，下一轮拿回什么

**可以二开的能力有两层：**一层是公共包提供的 JSON 读写工具，适合接入自己的数据库；另一层是应用已接好的日记、记忆表和 V2 状态。它们分别保存，改动一处不会自动同步到其他所有地方。

## 独立 JSON 记忆工具

入口：`@flowgpt/agent-core-tools/memory` 的 `createMemoryTools`。宿主把存储绑定到已认证的产品、用户和会话，再交给工具；模型参数里没有任意切换用户或租户的 ID。

| 动作 | 输入 | 直接结果 | 何时保存、谁用 |
| --- | --- | --- | --- |
| `get_memory` | `{}` | `{revision, memory}`；没有记录时 `revision:0, memory:null` | 只读；模型或程序接着使用返回对象，不调用 LLM |
| `update_memory` | `{expectedRevision, memory}` | 写成功后的 `{revision}` | **调用时就执行存储提交**，不是只生成候选；下次 `get_memory` 返回新对象 |

例：先读出版本 7，内容 `{"scene":"城门","keyHolder":"用户"}`；确认守卫接过钥匙后，提交版本 7 和完整新对象 `{"scene":"城门","keyHolder":"守卫"}`；存储成功返回版本 8。之后再读，拿到的是版本 8 的对象。

这里是完整替换，不是字段合并。如果新对象只写 `{"keyHolder":"守卫"}`，原来的 `scene` 会被删除。`expectedRevision` 必须使用产生该修改时依据的版本；冲突后重新读和重新判断，不能拿旧提议配一个新版本强行覆盖。

记忆必须是普通 JSON 对象，不能把数组、`null`、函数或循环对象当成整份记忆。序列化后最多 **64 KiB**，嵌套深度上限 **32**；版本是非负安全整数且小于 `Number.MAX_SAFE_INTEGER`。空记录严格对应 `revision:0, memory:null`，已有记录必须是正版本和对象。`beforeUpdate` 可以做业务字段检查，检查通过后才调用宿主存储；写入后若返回的版本不是旧版本＋1，工具报错，此时要先读回确认，不能当作一定未写入而盲目重试。

[参数、写入与结果](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/memory-tool.ts#L8) · [JSON范围、大小和深度校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/src/modules/memory/json.ts#L1) · [完整读取、冲突和业务接受示例](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/examples/sdk/05-memory.ts#L1)

### 最小宿主接线

```ts
import { createMemoryTools } from '@flowgpt/agent-core-tools/memory';

const [getMemory, updateMemory] = createMemoryTools({
  store: {
    get: signal => scopedStore.get(signal),
    compareAndSet: (commit, signal) => scopedStore.compareAndSet(commit, signal),
  },
  beforeUpdate(memory) {
    if (typeof memory.scene !== 'string') throw new Error('scene 必填');
  },
});
const current = (await getMemory.execute('read-1', {}, signal)).details;
const saved = await updateMemory.execute('write-1', {
  expectedRevision: current.revision,
  memory: { scene: '城门', keyHolder: '守卫' },
}, signal);
```

这是接线片段：`scopedStore` 必须由你实现，数据库中要原子检查版本并写入，返回版本应为原版本＋1。若业务要求用户接受正文后才能改变事实，不要提前让模型直接调用写工具；先产出提议，接受后再由宿主执行写入。工具设置 `replay: "never"`，不应盲目重放结果不确定的写操作。

`/memory` 是工具包的正式导出入口。仓库的发布配置为 GitHub Packages、`restricted` 访问；使用发布包还需要相应包读取权限，不能默认从公共 npm 匿名安装。若按源码构建，也需安装该入口使用的 TypeBox 等依赖。

[memory导出与发布权限配置](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/packages/tools/package.json#L12)

## 产品里已经接好的几种资料

| 资料 | 什么条件下处理 | 什么时候存 | 下次取回什么、怎样用 |
| --- | --- | --- | --- |
| 聊天正文 | 聊天业务接受并保存消息 | 聊天服务自己的保存流程 | 原始消息；业务作为历史提供给生成链路 |
| V2 后台状态 | 普通 V2 后台决定更新并产出有效候选 | 状态校验和版本检查后，通过 Axon CAS 提交 | 文本状态＋分类记录，再投影为 Actor / 后台各自的上下文；详见 [V2 产品](product-agent-v2.md) |
| Axon 日记 | 配置 `compassMemory` 来源、相应运行时与日记编排器；轮次、额度等策略允许 | 回复完成事件安排后台；规划允许后生成、解析并 `appendDiary` | 摘要取为 `compassMemory`；需要模板实际引用才会进入正文模型 |
| Axon 记忆表 | 配置 `memoryTable` 来源；回复里有可应用的表格编辑 | 回复完成后提取编辑，确有变化才替换表格；版本冲突至多重读重试一次 | 表格提示文本 `memoryTable`，版本另留供后续写入 |
| Hinos 外部记忆 | 选择 `memoryRuntime:"hinos"` 并配置 `hinosMemory` 来源 | 本轮 Actor 成功产出正文、通过取消检查后，提交该轮用户输入和正文；提交成功要求服务返回 `committed:true` | 下轮向 Hinos 重新 `prepare`，取回服务生成的 `memory` 文本，经模板引用进入 Actor |
| Compact 历史摘要 | 预算或检查点边界触发 | Actor 生成前，摘要和最终输入通过预算及持久条件检查后 | 摘要＋剩余原文；详见 [Compact](atoms-compact.md) |
| Story 世界/叙事快照 | Story 自己的准备、正文确认与结算流程 | 按 Story 对消息锚点和存储版本的检查提交 | 世界事实、事件和叙事状态；详见 [Story](product-story.md) |

**日记的“累计轮次”不是普通 V2 后台的全局触发规则。**日记节点默认 `min_rounds=20`、免费周期 `free_rounds_per_diary=10`；另一组 memory_v2 配置默认首次 20、后续 10，且是否实际生成、只存摘要还是也存日记，由 `planDiary` 返回及免费/付费策略共同决定。不能简化成“所有记忆每十轮保存一次”。

日记和表格此处收到的是回复完成事件，代码没有在这里额外等待“用户接受这段正文”的确认事件。业务若需要接受之后再保存，应在接入层明确实现该条件，不能用“后台保存”替代产品的接受规则。

[取回摘要和表格](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/axon-memory-runtime.ts#L98) · [何时安排写入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/axon-memory-runtime.ts#L151) · [日记节点默认参数](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/diary-orchestrator.ts#L91) · [规划、生成和实际保存](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/diary-orchestrator.ts#L269)

### Hinos：外部记忆服务怎样参加一轮

这是已接入应用的另一种记忆运行时，需要部署或接通 Hinos 服务。它不调用 `get_memory` / `update_memory`，也不把 Hinos 返回内容存进 V2 的 Director 状态。

| 时机 | Harness 发送什么 | 服务返回什么、接下来怎么用 |
| --- | --- | --- |
| Actor 回复前 | 调用 `/v1/memory/prepare`，发送已渲染的角色 system 消息、业务提供的 user/assistant 正式聊天（末条必须是当前用户），以及请求、场景、模型配置、Prompt 等来源标识 | 校验协议后取得 `claim_id`、`memory` 文本、`turn`、`state_through_round`。`claim_id` 标识本次准备；它不是 JSON 记忆工具的版本号 |
| 本轮组装 Actor 输入 | `memory` 非空时，把它设为 `hinosMemory`，再执行基础上下文准备 | 模板实际引用 `hinosMemory` 后，Actor 才能读到这段资料；返回空文本则继续使用原有准备结果 |
| Actor 正文生成完成后 | 调用 `/v1/memory/commit`，发送同一 `claim_id`、本轮 `user` 输入和 `assistant` 正文 | 要求响应协议正确且 `committed:true`；这是 Hinos 接收本轮对话的回执，其内部如何提炼、存储不在本仓实现 |
| 下一轮 | 再次调用 `prepare`，提供新的正式聊天 | 取得服务为新一轮准备的 `memory`；不保证上一轮输入每个字都出现在返回记忆中 |

例如，本轮用户说“我答应明天归还铜钥匙”，Actor 回应“守卫点头，记下了约定”。Harness 完成正文后把这两段文本提交给 Hinos。下一轮 Hinos **可能**返回“用户答应明天归还铜钥匙”等记忆文本，实际内容由该服务决定；Harness 负责把返回文本交给本轮模板，不自行把例子中的句子硬编码为记忆。

当前闭环路径会等待 `commit` 调用返回，再发出 `turn.completed`，并未另等用户接受正文。`prepare` 失败会阻止本轮继续生成；`commit` 失败会发出记忆写入失败事件，而当前 Router 的 `memoryIngest:"best_effort"` 允许正文仍完成。已经流出的正文与记忆保存成功是两个结果。取消检查或请求终止可能阻止提交，不能仅凭“看到正文”认定记忆已写入。

启用时，在已有可运行 Scenario 中加入以下字段，并设置服务环境变量 `HINOS_MEMORY_URL`；`HINOS_MEMORY_TIMEOUT_MS` 默认 **10000 ms**。`memorySources` 中必须恰好有一项 `variable:"hinosMemory"`；`id`、`agentRef` 仍是 Scenario 来源配置的必填字段。

```json
{
  "memoryRuntime": "hinos",
  "memorySources": [{
    "id": "external-memory",
    "agentRef": "your-hinos-source",
    "variable": "hinosMemory"
  }]
}
```

服务收到 `x-flow-conversation-id` 与 `x-request-id` 请求头。当前适配器不附带独立认证凭证，接入部署需要自行配置受信服务访问及其权限边界；这里不能把会话 ID 当成鉴权。

[来源检查、prepare协议与记忆返回](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/hinos-memory-runtime.ts#L21) · [commit、请求头与超时](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/hinos-memory-runtime.ts#L112) · [放回Actor输入](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/hinos-memory-runtime.ts#L161) · [生成后提交与完成顺序](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/harness.ts#L317) · [闭环记忆失败策略](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L744) · [服务地址与超时](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L995)

<details>
<summary>应用接入开关与需要接通的依赖</summary>

Scenario 的 `memorySources` 每项包含 `id`、`agentRef`、`variable`，可有 `priority`。Axon Go 接法只支持 `compassMemory` 和 `memoryTable`，没有配置相应来源就不发对应召回请求。当前召回 RPC 按用户、会话和来源取资料，没有把本轮“铜钥匙”这个 query 传去做语义搜索。

`memoryRuntime` 可以选 `personalization`、`axon_shadow`、`axon_go`、`hinos`。`axon_shadow` 比较两套召回，不能理解为已经切换到新的主写入路径；日记编排器在 `axon_go` 下按来源装配。Hinos 还需自己的服务地址和超时。业务接线应跟随这些实际分支，而不是只换文档中的“记忆系统”名称。

不指定 `memoryRuntime` 时，当前 Router 使用 personalization 路径。Axon Go 的日记召回条数还受 `subscriptionCategory` 影响：默认 1、plus 2、ultra 3、max 5；这是召回资料量，不是生成／保存日记的轮数。表格与日记返回的是本次可用的提示文本，日记缓存 `agentRef` 来自所配来源，不能把它误当成模型自动选择的检索工具。

日记依赖额度、锁、轮次规划、模型、解析、保存与可选通知；表格依赖编辑解析、版本替换，以及可选提炼上传。表格替换成功后，后续提炼步骤失败不能证明表格没有写入。模板没有引用返回变量时，“取到了”仍不等于“模型看到了”。

[多种记忆运行时装配](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/adapters/agent-router/dispatch.ts#L811) · [来源与运行时配置校验](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/server.ts#L649) · [表格保存与后续副作用](https://github.com/FlowGPT/roleplay-harness/blob/fa41d220d48327e58d994f936517b23f2f15f658/apps/emochi/memory/axon-memory-runtime.ts#L177)

</details>

## 二开时的最小验收

连续执行三步：读取旧值 → 触发一次明确修改并检查保存回执 → 新请求读回新值并确认真正放入模型输入。同时测一次版本冲突和一次保存失败。若接日记/表格，还要验证“不满足轮次/没有编辑”时确实不产生相应写入。
