数据截至 (上游 commit 0b46c8430636)
上下文与持久化:注入、记忆、会话与检查点
30 秒导读: agent 是个「一边跑一边攒状态」的循环(01-agent-loop)。这一章讲它怎么管住那些状态:临时喂给模型一段话(注入)、跨会话记住用户偏好(记忆)、把每条消息落库好让进程重启后接着聊(会话)、以及在循环的暂停点打个标记好让它崩溃后从原地续跑(检查点/快照)。
1. 这节讲什么
一个能长期干活的 agent,状态并不只有「当前这段对话」。它至少要回答四个问题:
- 这一次模型调用,要不要临时塞进去一点额外上下文(而且塞完就忘)?
- 上周用户说过「我喜欢深色模式」,这周新开一段对话,还能记得吗?
- 进程重启了,之前聊到一半的对话还在吗?
- 一次多步任务跑到第 5 步崩了,能不能从第 5 步接着跑,而不是从头再来?
Strands 把这四件事拆成四层各管一段生命的机制。它们互相独立、可单独开关,但常常叠在一起用:
| 机制 | 管什么 | 生命周期 | 核心类型 |
|---|---|---|---|
| 即时注入 | 单次调用临时喂料 | 一次模型调用,用完即弃 | InjectionContext、RenderContentCallback |
| 记忆 | 跨会话的长期事实/偏好 | 跨进程、跨会话 | MemoryManager、MemoryStore |
| 会话 | 对话历史 + agent 状态落库 | 跨进程,逐消息持久化 | SessionManager、SessionAgent |
| 检查点/快照 | 循环暂停点标记 + 状态定格 | 一次可中断的执行 | Checkpoint、Snapshot |
| agent 状态 | 工具/hook 之间共享的键值袋 | 单次 invocation 内共享,可随会话落库 | AgentState |
一句话直觉: 把注入当成「便利贴」——贴上去给模型看一眼就撕掉;记忆是「档案柜」——换一间办公室也还在;会话是「录音带」——断电了倒带还能听;检查点是「书签」——夹在书里下次翻到那一页接着读。
本章不覆盖可观测性(telemetry);记忆/会话操作都会开 tracing span,但那属于代码地图里指向
telemetry/的另一条线。
2. 顶层全景
先看这四层怎么围着事件循环排布。从左到右是一次对话的时间流;上半是「进循环之前/之中」的临时加工,下半是「跨越进程」的持久化底座。
┌────────────────────────── ─────────────────┐
用户输入 ─────▶ │ 事件循环 (event_loop) │ ─────▶ 回复
│ │
┌───────────┤ ① 模型调用前:InvokeModelStage.Input │
│ │ └─ 注入中间件把文本折进最新 user 消息 │
│ │ │
② 记忆检索 │ ③ 循环暂停点:after_model / after_tools │
(search→注入) │ └─ 发射 Checkpoint 停机事件 │
│ └───────────────┬─────────────────────────────┘
│ │ 每条消息 / 每次 invocation
▼ ▼
┌───────────┐ ┌───────────── ─┐ ┌──────────────┐
│ MemoryStore│ │ SessionManager│ │ take_snapshot│
│ (档案柜) │ │ (逐消息落库) │ │ (状态定格) │
└───────────┘ └──────────────┘ └──────────────┘
长期记忆 会话持久化 快照/检查点
各部件一句话职责:
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
| 注入中间件 | 把 just-in-time 文本临时折进最新 user 消息,只影响本次调用 | strands-py/src/strands/injection/_message_injection.py |
MemoryManager | 检索长期记忆并驱动注入,也管 search/add 工具与后台抽取 | strands-py/src/strands/memory/memory_manager.py |
SessionManager | 监听 hook,把消息与 agent 状态逐条落进 repository | strands-py/src/strands/session/session_manager.py |
Checkpoint | 循环暂停点的位置标记(不含状态) | strands-py/src/strands/experimental/checkpoint/checkpoint.py |
Snapshot | agent 状态的点定格(含状态,可选字段) | strands-py/src/strands/types/_snapshot.py |
AgentState | 工 具/hook 之间共享的 JSON 键值袋 | strands-py/src/strands/agent/state.py |
一条主线走一遍(高层): 用户发问 → MemoryManager 用这句话当 query 去 MemoryStore 检索 → 检索到的记忆经注入中间件临时折进这条 user 消息 → 模型作答 → SessionManager 把新消息落库、把 agent 状态同步 → 若开了 checkpointing,在 after_model/after_tools 暂停点发射一个 Checkpoint,调用方可以据此中断、之后原地续跑。
3. 即时上下文注入:一次性的「便利贴」
3.1 它要解决的小问题
有时你想在某一次模型调用里多塞一点上下文——检索到的记忆、当前时间、一段工具刚算出来的中间结果——但你不想让它变成对话历史的一部分永久留着。留着会有两个坏处:占上下文窗口、而且下一轮它就过期了(比如「当前时间」)。
Strands 的答案是ephemeral(一次性)注入:文本只在这一次调用时折进输入,agent 存储的 durable 历史一字不动。
3.2 思路:折进最新 user 消息,而不是新插一条
最直觉的做法是「再 append 一条 system/user 消息」。但那会破坏 role 交替(很多 provider 要求 user/assistant 严格轮流),而且真的改了历史。
Strands 改成把文本折进已经存在的最新 user 消息,并且返回一个新列表、绝不改原消息。折进的位置还分两种情况:
普通用户提问: [注入文本] + 用户原话
└ prepend:注入在前,用户的话留在「最近位」——模型最后读到的是用户
工具结果回合: 工具结果块 ... + [注入文本]
└ append:tool result 必须是该回合的第一个 content 块,只能塞在后面
这段判断在 _fold_into_last_user_message:
# strands-py/src/strands/injection/_message_injection.py:181
has_tool_result = any("toolResult" in block for block in target["content"])
content = [*target["content"], injected] if has_tool_result else [injected, *target["content"]]
它返回一个新 list 外加一个「末条消息里属于本次调用的尾部块数」、逐条不改原 message(strands-py/src/strands/injection/_message_injection.py:148-192,_fold_into_last_user_message)。为什么必须不可变? 因为注入是在中间件里对「本次调用的上下文」做的,而上游 InvokeModelContext 本就是 agent 状态的防御性拷贝——两层保护叠起来,durable 历史怎么都安全。
3.3 什么时候注入:trigger
注入不是每次都跑。默认策略叫 "userTurn"——只在最新消息是一句「新鲜的用户提问」时注入(即 role 是 user 且不带 tool result):
# strands-py/src/strands/injection/_message_injection.py:128 _is_user_turn
last = messages[-1]
return last["role"] == "user" and not any("toolResult" in block for block in last["content"])
三种 trigger,列在 injection/types.py:
| trigger | 何时注入 | 适合谁 |
|---|---|---|
"userTurn"(默认) | 仅新鲜用户提问那一轮 | 聊天型 agent,保持用户的话在最近位 |
"everyTurn" | 每次模型调用,含中途工具回合 | 自主 agent,每步都要参考注入内容 |
| 自定义谓词 | 你说了算 | 精细控制的逃生口 |
3.4 关键设计:fail open(坏回调绝不拖垮模型调用)
注入是「锦上添花」,不能因为一个 render 回调抛异常就让整次模型调用失败。所以每一处能出错的地方都 fail open——记一条 warning,跳过注入,让模型照常跑:
- render 回调抛异常 → 跳过(
strands-py/src/strands/injection/_message_injection.py:86-88) - 回调返回
None或空串 → 跳过(strands-py/src/strands/injection/_message_injection.py:90) - trigger 谓词抛异常 → 当作不注入(
strands-py/src/strands/injection/_message_injection.py:120-126,guarded)
3.5 别把不可信文本原样拼进 XML
注入的内容常常是用户派生的(记忆条目就是)。若直接拼进 <entry>…</entry>,一个乱入的 </entry> 就能破坏结构,更糟的是构成存储型 prompt 注入面。injection/_xml.py 提供两个极小的转义器堵这个口:
| 函数 | 转义什么 | 用在哪 |
|---|---|---|
_escape_xml_text | & → < → >(先转 & 避免二次转义) | 元素正文 |
_escape_xml_attr | 在上面基础上再转 " 和 ' | 双引号属性值 |
注意:这些原语是内部的。正常用法是通过
MemoryManager(见 §4)或ContextInjector插件去用注入,而不是直接碰_message_injection(见injection/__init__.py的说明)。
4. 记忆管理:跨会话的「档案柜」
4.1 它要解决的小问题
注入解决「这一次塞什么」,但塞什么内容从哪来?对长期记忆而言,来自一个或多个 MemoryStore——一个能 search(必选)、可选能 add/add_messages 的后端(向量库、知识库、本地文件……)。MemoryManager 是把这些 store 编排起来、并驱动注入的那个插件。
它本身是个 Plugin,名字 strands:memory-manager(memory_manager.py:88)。用起来极简:
# 示意,非源码
from strands import Agent
from strands.memory import MemoryManager
mm = MemoryManager(stores=[my_store]) # 一个或多个 store
agent = Agent(model=model, memory_manager=mm)
agent("记住我喜欢深色模式") # add 工具写入
# ……换一段全新会话……
agent("我的界面偏好是什么?") # 注入自动把记忆折进这句话
4.2 三条职责,在 init_agent 里接线
MemoryManager.init_agent(memory_manager.py:585)一次接好三件事:
| 职责 | 做什么 | 入口方法 |
|---|---|---|
| store 初始化 | 调每个 store 的 initialize(),让它提前解析远端资源/校验配置 | _init_stores (:607) |
| 抽取(extraction) | 为配了 ExtractionConfig 的 store 缓冲对话消息、挂触发器,后台把对话蒸馏成记忆 | _init_extraction (:613) |
| 注入 | 注册一个 InvokeModelStage.Input 中间件,把检索到的记忆折进模型输入 | _init_injection (:628) |
注入这条线正是 §3 和记忆的接缝——_init_injection 复用了 §3 的 _create_injection_middleware,把「render 什么」交给 _provide_memory_context:
# strands-py/src/strands/memory/memory_manager.py:639
agent._middleware_registry.add_middleware(
InvokeModelStage.Input,
_create_injection_middleware(
lambda context: self._provide_memory_context(context.messages, config),
trigger=config.get("trigger"),
),
)
4.3 一次记忆注入的端到端路径
_provide_memory_context(memory_manager.py:647)就是那个 render_content 回调,它每次模型调用被叫一次:
messages ──▶ _resolve_injection_query ──▶ query 为空? ──是──▶ 返回 None(跳过)
│ │否
│ ▼
│ search(query) ← 各 store 并发检索
│ │
│ 结果为空? ──是──▶ 返回 None(跳过)
│ │否
│ ▼
│ format(entries) → <memory> 块 ──▶ 注入
其中 query 的推导是自适应的(_resolve_injection_query, :704):用户回合就取最新 user 消息的文本,否则取最近 assistant 消息的文本(即上一步的自主输出)。默认渲染成一个 <memory> 块、每条 一个 <entry source="...">,并对内容和来源做 §3.5 的转义(_default_injection_format, :735)。
search 本身(memory_manager.py:296)对所有目标 store 并发检索(asyncio.gather(..., return_exceptions=True)),单个 store 失败只记 warning、不拖垮整体,每条结果都用 store_name 标注来源。默认每 store 最多 DEFAULT_MAX_SEARCH_RESULTS = 3(:55)、注入最多 DEFAULT_MAX_ENTRIES = 5(:58)。
4.4 配置的三种「简写」哲学
MemoryManager 的构造参数都接受 True / False / 配置对象 三态(memory_manager.py:90):
| 参数 | True | False | 配置对象 |
|---|---|---|---|
search_tool_config | 注册默认 search_memory 工具(默认开) | 不注册 | MemoryToolConfig 自定义名/描述 |
add_tool_config | 允许写所有可写 store | 不注册(默认关) | MemoryAddToolConfig 限定范围 |
injection | 默认注入设置(默认开) | 不注入 | MemoryInjectionConfig 自定义 query/format/timing |
MemoryInjectionConfig(memory/types.py:160)扩展了 §3 的通用 InjectionConfig(继承 trigger),再加上记忆自己的旋钮:max_entries、query、format。
4.5 store 契约与「能力探测」
MemoryStore 是个 Protocol(memory/types.py:241):必须有 search,可选有 add/add_messages/initialize/get_tools。麻烦在于 Protocol 会给出「stub 方法」,一个子类哪怕没真正实现也会「继承」到那个 stub。Strands 用 _has_method(memory/types.py:299)按类型精确探测——如果某方法就是 Protocol 那个 stub,就当作「没实现」:
# strands-py/src/strands/memory/types.py:305
method = getattr(type(store), name, None)
if method is None:
return False
if method is getattr(MemoryStore, name, None): # 只是继承了 Protocol 的 stub
return False
return callable(method)
这条探测决定了一堆构造期校验:可写 store 必须至少有一个写 sink(_has_write_sink,:314);配了 extractor 的必须有 add;配了无 extractor 的抽取必须有 add_messages——都在 __init__ 里提前 raise ValueError,把「配置错」挡在 agent 构造阶段而非运行时。
现成的 store 实现在
strands-py/src/strands/vended_memory_stores/(local/、bedrock_knowledge_base/),可当作实现MemoryStore契约的参考样板。
5. 会话持久化:跨进程的「录音带」
5.1 它要解决的小问题
记忆是「蒸馏后的长期事实」,而会话是原样的对话历史 + agent 状态。目标是:进程重 启后,Agent(session_manager=...) 一构造,就把上次聊到哪、状态是什么全量恢复,像没断过一样。
5.2 靠 hook 逐条落库,而不是「结束时存一次」
SessionManager(session/session_manager.py:31)是个 HookProvider + ABC。它的关键设计:改一次存一次,靠订阅生命周期 hook 实现(register_hooks, :40):
| hook 事件 | 触发的持久化动作 |
|---|---|
AgentInitializedEvent | initialize(agent)——从会话恢复 agent |
MessageAddedEvent | append_message + sync_agent——每加一条消息就落库并同步状态 |
AfterInvocationEvent | sync_agent——收尾时再同步一次,捕捉 conversation manager 的状态更新 |
多 agent / bidi agent 各有对应 hook,默认实现直接 raise NotImplementedError,由支持的子类覆盖。
5.3 落库的数据模型
types/session.py 定义了三个层次的 dataclass:
Session (一次会话,session_id + type)
└── SessionAgent (一个 agent 的状态定格)
├── state ← 用户管理的 AgentState
├── conversation_manager_state ← 会话管理器状态
└── _internal_state ← Strands 内部态(interrupt_state / model_state)
└── SessionMessage[] (逐条消息 + message_id 索引 + 可选 redact)
SessionAgent.from_agent(types/session.py:126)是「打包」那一刻:
# strands-py/src/strands/types/session.py:131
return cls(
agent_id=agent.agent_id,
conversation_manager_state=agent.conversation_manager.get_state(),
state=agent.state.get(),
_internal_state={
"interrupt_state": agent._interrupt_state.to_dict(),
"model_state": agent._model_state,
},
)
两个值得记的细节:
- bytes 要 base64。 消息里可能有二进制(图片等),
encode_bytes_values/decode_bytes_values(types/session.py:28、:43)递归地把 bytes 编成{"__bytes_encoded__": True, "data": ...}再落 JSON。 - redact 不删原文。 guardrail 触发时不是抹掉消息,而是给
SessionMessage.redact_message挂一份替代内容;to_message()优先返回 redact 版(types/session.py:86)。
5.4 恢复:分层还原 + 修复历史
RepositorySessionManager(session/repository_session_manager.py:28)把抽象的 repository CRUD(SessionRepository,session/session_repository.py:12,由 file/S3 等实现)接上 agent。initialize(:169)是恢复的核心,顺序很讲究:
- 恢复
agent.state = AgentState(session_agent.state)(:207) initialize_internal_state——还原 interrupt/model 内部态(:209)conversation_manager.restore_from_session——顺带拿回可选的 prepend 消息(:212)- 用
removed_message_count当 offset 拉取消息列表(会话管理器可能删过前面的消息)(:219) - server-side 会话跳过消息还原——若
agent.model.stateful,对话由服务端管,本地不重放(:228) - 否则拼回
agent.messages并调_fix_broken_tool_use修补
第 6 步的 _fix_broken_tool_use(:245)是一段防御性代码:1.15.0 之前有 bug 会落下「有 toolUse 没 toolResult」的破损历史,或分页截断导致「有 toolResult 没 toolUse」——它给孤儿 toolUse 补上占位 toolResult、删掉开头的孤儿 toolResult,保证喂给模型的历史结构合法(issue #859)。
5.5 省 I/O:版本号比对
每条消息都触发 sync_agent,若每次都全量写库会很浪费。sync_agent(:102)用版本号 + 值比对判断有没有真变化——agent.state._get_version()、_interrupt_state._get_version()、以及 conversation manager state 和 model_state 的值——没变就直接 return、跳过写库(:137)。这也是为什么 AgentState 要带版本号(见 §7)。
6. 检查点与快照:「书签」与「定格照」
这是全章最容易混的一对。它们都服务「中断后恢复」,但分工完全不同:
Checkpoint | Snapshot | |
|---|---|---|
| 装什么 | 位置标记——哪个暂停点 + 第几个 cycle | 状态定格——messages/state 等字段的深拷贝 |
| 含不含状态 | 不含(要配 SessionManager 才有状态续跑) | 含 |
| 谁发射 | 事件循环在 after_model/after_tools 自动发 | 调用方手动 agent.take_snapshot() |
| 定义在 | experimental/checkpoint/checkpoint.py | types/_snapshot.py |
6.1 Checkpoint:循环暂停点的标记
Checkpoint(checkpoint.py:45)是个 frozen dataclass,只记两件核心事:position(after_model 或 after_tools)和 cycle_index(第几个 ReAct cycle,0 起)。它刻意不装对话状态——文件顶部注释就写明「pair with a SessionManager for cross-process state continuity」。
两个暂停点的语义:
一个 ReAct cycle:
模型调用 ──▶ [after_model] 模型返回了 tool_use,但工具还没跑
│
▼
工具执行 ──▶ [after_tools] 工具跑完了,下一次模型调用还没发生
发射点在 §1 讲的事件循环里,由 _build_checkpoint_stop_event(event_loop/event_loop.py:161)统一构造:
after_model:仅在stop_reason == "tool_use"且开了 checkpointing 时发,且「刚从 after_model 续跑回来的那次」不重复发(event_loop.py:316-333)。after_tools:工具跑完后发,只在 tool_use cycle 触发——一个直接end_turn的回合永远到不了这里(event_loop.py:854-866)。
优先级(见 checkpoint.py:18-23):interrupt > checkpoint(中断会返回 stop_reason="interrupt" 并跳过 after_tools);cancel > checkpoint(取消信号在任一边界都返回 "cancelled")。
6.2 恢复:把 checkpoint 传回去
暂停时,AgentResult 的 stop_reason="checkpoint" 且 checkpoint 字段被填。续跑时把它当一个特殊 prompt 块传回:{"checkpointResume": {"checkpoint": ckpt.to_dict()}}。
agent 侧在 _try_consume_checkpoint_resume(agent/agent.py:1388)拆这个块:checkpointing=False 却收到它会 raise ValueError;缺 checkpoint 键 raise KeyError;schema 版本不匹配则在 Checkpoint.from_dict(checkpoint.py:70)raise CheckpointException。消费成功后 _convert_prompt_to_messages 返回空消息列表(agent.py:1415)——因为续跑不是新提问。
循环开头再把这个 resume 标记「一次性消费」掉,并据 position 决定 cycle_index 是否 +1(after_tools 意味着那个 cycle 已完成,续跑要进下一个)(event_loop.py:252-261):
# strands-py/src/strands/event_loop/event_loop.py:257
next_cycle = (
resume_context.cycle_index + 1 if resume_context.position == "after_tools" else resume_context.cycle_index
)
6.3 Snapshot:可选字段的状态定格
Snapshot(types/_snapshot.py:49)才是真装状态的那个。它是一个带版本、JSON 兼容的对象,由 agent.take_snapshot()(agent/agent.py:1539)按「要哪些字段」深拷贝而成。
可选字段由 SnapshotField 枚举(_snapshot.py:11):messages、state、conversation_manager_state、interrupt_state、system_prompt、model_state。字段的解析走 resolve_snapshot_fields(_snapshot.py:105),顺序是 preset → include → exclude:
preset="session" ──▶ {messages, state, conv_mgr_state, interrupt_state, model_state}
│
∪ include (加上你额外点名的字段)
│
− exclude (再减掉你排除的)
│
结果为空? ──是──▶ raise SnapshotException
目前只有一个 preset:"session"(_snapshot.py:35,注意它不含 system_prompt)。load_snapshot(agent/agent.py:1587)反向还原——只还原 snapshot 里存在的字段,缺的保持不变,还原前先 snapshot.validate() 校验 schema 版本(必须 "1.0")和 scope(必须 "agent")。
一句话对比: 需要「崩溃后原地续跑同一次多步执行」→ Checkpoint(标记)+ SessionManager(状态);需要「主动把某一刻状态整个拍下来搬走/回放」→ Snapshot(自带状态)。
7. agent 状态:工具与 hook 之间的共享键值袋
AgentState(agent/state.py)如今只是一个类型别名:
# strands-py/src/strands/agent/state.py
from ..types.json_dict import JSONSerializableDict
AgentState = JSONSerializableDict
它是工具、hook、注入回调之间共享的临时便签——一个工具这一步 stash 进去、注入回调下一步就能读到(注入的 InjectionContext.state 正是它,见 injection/types.py:32)。
真身 JSONSerializableDict(types/json_dict.py:8)有两条设计值得记:
- 写时即校验 JSON 可序列化。
set里每次json.dumps(value)试一遍,不可序列化就raise ValueError(json_dict.py:92)——把「存了个存不进库的东西」的错提前到写入点,而不是等到会话落库才炸。 - 带版本号,免脏标记。 每次
set/delete让_version += 1(json_dict.py:38)。消费者(比如 §5.5 的sync_agent)靠比较版本号就能检测「变没变」,无需显式清脏标记。读写都走copy.deepcopy,调用方拿到的永远是副本、改 不脏内部数据。
8. 边界与局限
- 注入是一次性的,不是记忆。 折进去的文本只影响本次调用、绝不进 durable 历史。要「记住」得走记忆或会话。
- Checkpoint 不含状态。 单独一个 checkpoint 无法跨进程续跑,必须配
SessionManager才有对话状态;而且它只在 tool_use cycle 发射——纯end_turn的一问一答不会产生 checkpoint,那种情况的持久化靠SessionManager逐消息落库。 - 记忆检索是 fail open 的近似。 单 store 失败只 warning;多 store 结果按注册顺序拼接、无跨 store 排序,
max_entries的截断会偏向靠前注册的 store(见memory/types.py:160对max_entries的说明)。语义相似 ≠ 上下文有用,所以默认注入一小撮候选而非只赌 top1。 - 抽取(extraction)是后台、at-least-once。 同步
Agent(...)入口每次 invocation 后会await flush();自己驱动事件循环的调用方要在收尾处手动flush。store 实现要容忍重复写。 - 快照 preset 目前只有
"session",且不含system_prompt。 要连 system prompt 一起拍,得显式include=["system_prompt"]。 - checkpoint 实验性。 在
experimental/,schema 可能不兼容变更(from_dict会拒绝不匹配的schema_version)。
9. 横向对比
- 注入的「ephemeral 折进最新消息、绝不改历史」和控制面里 hook/middleware 的「拦截即改」是一对取舍:注入刻意不落地,middleware 可落地——两者都挂在
InvokeModelStage,只是注入选择了不可变。 - 检查点发射点属于事件循环的
after_model/after_tools边界;本章讲快照结构与 resume 语义,循环里「何时发」的时序细节看第 1 章。 - 记忆检索用到的并发
asyncio.gather+ 单点失败隔离,和工具系统的并发执行是同一套容错哲学。
10. 代码地图(导航索引)
| 主题 | 文件路径 | 关键符号 |
|---|---|---|
| 注入中间件工厂 | strands-py/src/strands/injection/_message_injection.py | _create_injection_middleware、RenderContentCallback |
| 折进最新 user 消息 | strands-py/src/strands/injection/_message_injection.py | _fold_into_last_user_message、_is_user_turn |
| 注入配置/上下文/trigger | strands-py/src/strands/injection/types.py | InjectionContext、InjectionConfig、InjectionTrigger |
| XML 转义(防注入) | strands-py/src/strands/injection/_xml.py | _escape_xml_text、_escape_xml_attr |
| 记忆编排插件 | strands-py/src/strands/memory/memory_manager.py | MemoryManager、init_agent、_provide_memory_context |
| 记忆注入接线 | strands-py/src/strands/memory/memory_manager.py | _init_injection、_default_injection_format |
| 记忆类型与 store 契约 | strands-py/src/strands/memory/types.py | MemoryStore、MemoryInjectionConfig、_has_method、_has_write_sink |
| 现成 store 实现 | strands-py/src/strands/vended_memory_stores/ | local、bedrock_knowledge_base |
| 会话管理器(hook 落库) | strands-py/src/strands/session/session_manager.py | SessionManager、register_hooks |
| 仓库 CRUD 抽象 | strands-py/src/strands/session/session_repository.py | SessionRepository |
| 会话恢复 + 修复历史 | strands-py/src/strands/session/repository_session_manager.py | RepositorySessionManager.initialize、sync_agent、_fix_broken_tool_use |
| 会话数据模型 | strands-py/src/strands/types/session.py | SessionAgent、SessionMessage、Session、encode_bytes_values |
| 检查点标记 | strands-py/src/strands/experimental/checkpoint/checkpoint.py | Checkpoint、CheckpointPosition |