持久化
持久化让会话可以跨进程恢复,也让产品界面拥有稳定的会话标识。恢复后的会话可以 重新填充任务列表、执行记录、制品和交付摘要,不必重放已经完成的运行。
A3S Code 维护四种相关但不同的记录:
文件会话存储
Go 通过 FileSessionStoreDir 选择内置文件存储;自定义 SessionStore trait
实现仍属于 Rust 嵌入能力。
事实日志是控制来源
每个编码回合都会把不可变事实追加到工作区的 .a3s/effect-log/<thread>.jsonl 中。
会话标识不超过 128 个字符、且只含 ASCII 字母、数字、_、.、: 或 - 时,
线程名就是会话标识;否则为 s- 加上十六进制编码的标识(截断到 128 个字符)。
因此同一工作区中的两个会话会折叠两份独立日志。send、stream、附件回合、
resumeRun 与精确恢复都用同一种方式选择下一步:用 a3s-effect 折叠日志
(新事实用 ingest_coding,继续执行用 resume_coding)。参见
core/src/fact_control.rs。
这对持久化与恢复有具体影响:
- 已保存的
model.turn事实不会再次发送给模型。 - 循环检查点和会话快照都不会决定下一次模型调用,只有日志会。
- 确认会停驻,直到出现
confirmation.answered事实;提问会停驻,直到出现question.answered事实。进程内通道或定时器都不会替它们做决定。 - 日志中缺少结果的工具调用会在恢复时执行一次。
- 引导(steer)只是另一个
user.message事实。 - 达到工具回合上限后,下一次补全会携带空工具列表。
事实日志保存在工作区中,而不在 SessionSnapshotV1 中。如果希望重启后的进程继续
一个停驻的运行,请把工作区(至少是 .a3s/effect-log/)与会话存储一起保留。
原子快照代次
session.save() 会把当前持久化状态收集成一个 SessionSnapshotV1,并且只调用一次
SessionStore::save_snapshot。信封包含:
schema_version与SessionData- 工具制品,以及制品 GC 使用的保留制品 URI
- 追踪事件与运行记录
- 验证报告
- 委派的子智能体任务快照
- 审查发现(会话中存在时)
文件存储会把完整 JSON 信封写入并同步临时文件,然后原子替换存储目录下的
v1/sessions/id_<base64url 会话标识>.json。因此读取方看到的是上一代或下一代,
不会读到“新对话搭配旧运行或追踪分片”的组合。循环检查点位于同级的
v1/loop_checkpoints/。内存存储在同一把锁下发布同一个聚合快照。
较早的文件仍可读取。存储根目录中旧版的 <session-id>.json 只作为只读迁移来源。裸 SessionData 会在加载时与制品、追踪、运行、验证报告和
子智能体任务的分片位置合并,再通过 v1 内存形状恢复。保存新聚合快照后,单个信封
会成为权威代次。已经具有聚合外形、但模式损坏或版本不支持的文档会直接被拒绝,
不会重新解释成分片数据。
自定义存储必须显式实现 save_snapshot。默认实现返回错误,不会把聚合快照拆成
多次独立写入,也不会把空操作当成成功。默认 load_snapshot 只尽力组装分片;
宿主可以通过 capabilities() 区分这种行为与原子后端。
可协商的耐久性
SessionStore::capabilities() 返回 SessionStoreCapabilities,说明后端实现了哪些
保证。宿主应先检查标志再依赖。内置存储声明如下:
即使启用静态加密,WAL 也保持未加密;它只包含 digest。trait 中
save_snapshot_cas(带期望 digest 时)、acquire_writer_lease 与 watch_commits
的默认实现都会返回错误,因此未实现它们的自定义存储会明确失败。
恢复会话
resumeSession 恢复的是已保存的会话快照。它不同于 resumeRun:用户继续一个
已保存对话时用 resumeSession;继续一个被中断的运行时用 resumeRun(runId)。
恢复操作会在恢复任何历史或运行时证据前先校验快照模式。
恢复被中断的运行
session.resumeRun(runId)(Node.js)/ session.resume_run(run_id)(Python)/
session.ResumeRun(ctx, runID)(Go)/ AgentSession::resume_run(Rust)用于
继续尚未落定的工作,例如进程崩溃之后,或确认仍在停驻时。下一步由事实日志决定:
- 如果
SessionStore中有runId的循环检查点,该检查点必须属于本运行与本会话, 否则调用以refusing to resume checkpoint '<id>'失败。随后会删除该会话的 事实日志线程,用检查点中的消息重新播种,再折叠日志。检查点只提供事实, 不决定下一次模型调用。 - 否则,如果会话的事实日志已经有事实,就按原样折叠该日志。静止的日志不会执行 任何步骤。
- 否则调用失败。未配置存储时错误为
resume_run requires a session_store on this session;配置了存储时错误为no loop checkpoint found for run '<id>'。
每次调用最多折叠 32 步。使用了检查点时,恢复的工作会记录为一个新运行(检查点所属 的运行不被修改),检查点中的 token 用量与工具调用次数会累加到结果中,并且完成门禁 生效:修改了工作区的回合,只有在存在绑定到该修改的 Passed 验证报告时才能完成。
需要精确、可安全重放的恢复标识的无界面宿主,使用
spawnRecoveryWithRunId(checkpointRunId, runId)(Python 为
spawn_recovery_with_run_id,Go 为 SpawnRecoveryWithRunID)。它要求
checkpointRunId 存在检查点,以宿主选定的 runId 准入恢复,并在后台工作线程中
继续折叠事实日志。重放同一个恢复 runId 会返回已有快照,不会再次执行。检查点
所属的运行本身保持不可变。
已完成的运行不会被恢复。它们的最终状态可通过 runs()、runEvents(runId)、
制品、验证报告和会话快照查看。
循环检查点格式
每个工具结果写入事实日志后,会通过会话的检查点接收端写出一个 LoopCheckpoint。
它的 messages 保存该工具调用及其结果;此外还记录运行与会话标识、运行的能力绑定、
回合计数、工具调用次数和 checkpoint_ms。
SessionStore 提供 save_loop_checkpoint、load_loop_checkpoint 与
delete_loop_checkpoint;文件存储采用原子写入。LoopCheckpoint::ensure_loadable()
会拒绝来自未来模式版本的检查点,ensure_owned_by 会拒绝属于其他运行或会话的
检查点。运行在进程内结束(完成、失败或取消)时,其检查点会被删除。
工作流检查点
WorkflowCheckpoint 把已完成的编排步骤记入日志,使被中断的工作流不会重复执行
它们。它的字段是 schema_version、workflow_id、steps 和
checkpoint_ms,模式由 WORKFLOW_CHECKPOINT_SCHEMA_VERSION 常量固定。恢复的
运行会跳过已记录的步骤,只重新派发其余的。
SessionStore 提供 save_workflow_checkpoint / load_workflow_checkpoint /
delete_workflow_checkpoint(默认为空操作;文件存储采用原子写入)。来自未来且
不兼容的模式版本会在加载时被 WorkflowCheckpoint::ensure_loadable() 拒绝。
SDK 通过 parallelResumable(specs, workflowId)(Node.js)、parallel_resumable
(Python)与 ParallelResumable(ctx, specs, workflowID)(Go)暴露这套日志,三者都
需要会话存储。参见
编排和多机执行。
在其他节点恢复
循环检查点与工作流检查点都可序列化,并保存在共享的 SessionStore 中。因此宿主
可以在与启动节点不同的节点上恢复被中断的运行或工作流。对运行而言,恢复节点会用
检查点为自己的事实日志播种,再从那里折叠。框架持有可序列化契约,宿主负责放置与
传输。
记忆与会话
会话持久化保存对话和可回放证据;记忆保存可复用任务事实。当你需要既可恢复、又能 从重复任务中学习的工作流时,两者一起使用。
运维注意事项
会话存储和 .a3s/effect-log/ 可能包含提示词、工具输出或私有路径,不应公开提交。