跳到主要内容

上下文与持久化:注入、记忆、会话与检查点

30 秒导读: agent 是个「一边跑一边攒状态」的循环(01-agent-loop)。这一章讲它怎么管住那些状态:临时喂给模型一段话(注入)、跨会话记住用户偏好(记忆)、把每条消息落库好让进程重启后接着聊(会话)、以及在循环的暂停点打个标记好让它崩溃后从原地续跑(检查点/快照)。


1. 这节讲什么

一个能长期干活的 agent,状态并不只有「当前这段对话」。它至少要回答四个问题:

  • 这一次模型调用,要不要临时塞进去一点额外上下文(而且塞完就忘)?
  • 上周用户说过「我喜欢深色模式」,这周新开一段对话,还能记得吗?
  • 进程重启了,之前聊到一半的对话还在吗?
  • 一次多步任务跑到第 5 步崩了,能不能从第 5 步接着跑,而不是从头再来?

Strands 把这四件事拆成四层各管一段生命的机制。它们互相独立、可单独开关,但常常叠在一起用:

机制管什么生命周期核心类型
即时注入单次调用临时喂料一次模型调用,用完即弃InjectionContextRenderContentCallback
记忆跨会话的长期事实/偏好跨进程、跨会话MemoryManagerMemoryStore
会话对话历史 + agent 状态落库跨进程,逐消息持久化SessionManagerSessionAgent
检查点/快照循环暂停点标记 + 状态定格一次可中断的执行CheckpointSnapshot
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 状态逐条落进 repositorystrands-py/src/strands/session/session_manager.py
Checkpoint循环暂停点的位置标记(不含状态)strands-py/src/strands/experimental/checkpoint/checkpoint.py
Snapshotagent 状态的点定格(含状态,可选字段)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(_message_injection.py:146-190,_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 回调抛异常 → 跳过(_message_injection.py:86-88)
  • 回调返回 None 或空串 → 跳过(_message_injection.py:90)
  • trigger 谓词抛异常 → 当作不注入(_message_injection.py:118-124,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):

参数TrueFalse配置对象
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_entriesqueryformat

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 事件触发的持久化动作
AgentInitializedEventinitialize(agent)——从会话恢复 agent
MessageAddedEventappend_message + sync_agent——每加一条消息就落库并同步状态
AfterInvocationEventsync_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)是恢复的核心,顺序很讲究:

  1. 恢复 agent.state = AgentState(session_agent.state)(:207)
  2. initialize_internal_state——还原 interrupt/model 内部态(:209)
  3. conversation_manager.restore_from_session——顺带拿回可选的 prepend 消息(:212)
  4. removed_message_count 当 offset 拉取消息列表(会话管理器可能删过前面的消息)(:219)
  5. server-side 会话跳过消息还原——若 agent.model.stateful,对话由服务端管,本地不重放(:228)
  6. 否则拼回 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. 检查点与快照:「书签」与「定格照」

这是全章最容易混的一对。它们都服务「中断后恢复」,但分工完全不同:

CheckpointSnapshot
装什么位置标记——哪个暂停点 + 第几个 cycle状态定格——messages/state 等字段的深拷贝
含不含状态不含(要配 SessionManager 才有状态续跑)
谁发射事件循环在 after_model/after_tools 自动发调用方手动 agent.take_snapshot()
定义在experimental/checkpoint/checkpoint.pytypes/_snapshot.py

6.1 Checkpoint:循环暂停点的标记

Checkpoint(checkpoint.py:45)是个 frozen dataclass,只记两件核心事:position(after_modelafter_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:853-865)。

优先级(见 checkpoint.py:18-23):interrupt > checkpoint(中断会返回 stop_reason="interrupt" 并跳过 after_tools);cancel > checkpoint(取消信号在任一边界都返回 "cancelled")。

6.2 恢复:把 checkpoint 传回去

暂停时,AgentResultstop_reason="checkpoint"checkpoint 字段被填。续跑时把它当一个特殊 prompt 块传回:{"checkpointResume": {"checkpoint": ckpt.to_dict()}}

agent 侧在 _try_consume_checkpoint_resume(agent/agent.py:1370)拆这个块:checkpointing=False 却收到它会 raise ValueError;缺 checkpointraise KeyError;schema 版本不匹配则在 Checkpoint.from_dict(checkpoint.py:70)raise CheckpointException。消费成功后 _convert_prompt_to_messages 返回空消息列表(agent.py:1397)——因为续跑不是新提问。

循环开头再把这个 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:1521)按「要哪些字段」深拷贝而成。

可选字段由 SnapshotField 枚举(_snapshot.py:11):messagesstateconversation_manager_stateinterrupt_statesystem_promptmodel_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:1569)反向还原——只还原 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:160max_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_middlewareRenderContentCallback
折进最新 user 消息strands-py/src/strands/injection/_message_injection.py_fold_into_last_user_message_is_user_turn
注入配置/上下文/triggerstrands-py/src/strands/injection/types.pyInjectionContextInjectionConfigInjectionTrigger
XML 转义(防注入)strands-py/src/strands/injection/_xml.py_escape_xml_text_escape_xml_attr
记忆编排插件strands-py/src/strands/memory/memory_manager.pyMemoryManagerinit_agent_provide_memory_context
记忆注入接线strands-py/src/strands/memory/memory_manager.py_init_injection_default_injection_format
记忆类型与 store 契约strands-py/src/strands/memory/types.pyMemoryStoreMemoryInjectionConfig_has_method_has_write_sink
现成 store 实现strands-py/src/strands/vended_memory_stores/localbedrock_knowledge_base
会话管理器(hook 落库)strands-py/src/strands/session/session_manager.pySessionManagerregister_hooks
仓库 CRUD 抽象strands-py/src/strands/session/session_repository.pySessionRepository
会话恢复 + 修复历史strands-py/src/strands/session/repository_session_manager.pyRepositorySessionManager.initializesync_agent_fix_broken_tool_use
会话数据模型strands-py/src/strands/types/session.pySessionAgentSessionMessageSessionencode_bytes_values
检查点标记strands-py/src/strands/experimental/checkpoint/checkpoint.pyCheckpointCheckpointPositionCHECKPOINT_SCHEMA_VERSION
检查点发射点strands-py/src/strands/event_loop/event_loop.py_build_checkpoint_stop_event(after_model/after_tools)
检查点续跑消费strands-py/src/strands/agent/agent.py_try_consume_checkpoint_resume
快照结构与字段解析strands-py/src/strands/types/_snapshot.pySnapshotSnapshotFieldresolve_snapshot_fields
快照拍/还原strands-py/src/strands/agent/agent.pytake_snapshotload_snapshot
agent 状态strands-py/src/strands/agent/state.pystrands-py/src/strands/types/json_dict.pyAgentStateJSONSerializableDict