持久化

持久化让会话可以跨进程恢复,也让产品界面拥有稳定的会话标识。恢复后的会话可以 重新填充任务列表、执行记录、制品和交付摘要,不必重放已经完成的运行。

A3S Code 维护四种相关但不同的记录:

记录写入方读取方用途
事实日志每个编码回合(send、stream、附件回合)下一个回合、resumeRun 与精确恢复唯一的控制来源。下一次状态转移只由折叠这份日志决定。
SessionSnapshotV1session.save() 或 autoSaveagent.resumeSession(id, options)恢复一个带版本号的完整代次,其中包含对话、制品、追踪、运行记录、验证报告与子智能体任务快照。
循环检查点每个写入事实日志的工具结果resumeRun(runId) 与 spawnRecoveryWithRunId运行中状态的可移植证据,保存在 SessionStore 中,供其他进程或节点恢复。运行在进程内结束时会被删除。
工作流检查点parallelResumable / 工作流组合子parallelResumable(specs, workflowId)进程重启后跳过已经完成的编排步骤。

文件会话存储

Rust
Node.js
Python
Go
Rust
use a3s_code_core::{Agent, SessionOptions};
let options = SessionOptions::new()
.with_session_id("release-review")
.with_file_session_store("./.a3s/sessions")
.with_auto_save(true);
let session = agent
.session_builder("/repo")
.options(options)
.build()
.await?;
session.send("Review release readiness", None).await?;
session.save().await?;

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,说明后端实现了哪些 保证。宿主应先检查标志再依赖。内置存储声明如下:

能力文件存储内存存储含义
atomic_session_snapshots是是save_snapshot 一次提交一个完整代次。
aggregate_cas是是save_snapshot_cas 仅在期望 digest 匹配(或省略)时提交。
append_only_event_log是否每次原子替换前后追加仅含 digest 的 Intent/Committed WAL 记录。
lease_fencing是否acquire_writer_lease 发布耐久 epoch;旧持有者 fail-closed。
encrypted_at_rest配置密钥时否FileSessionStore::with_encryption_key 用 AES-256-GCM 密封文档。
watch是是watch_commits 在耐久提交后投递 SessionStoreCommitEventV1。
reference_aware_artifact_gc是是制品驱逐会保留从保留 URI 可达的内容。

即使启用静态加密,WAL 也保持未加密;它只包含 digest。trait 中 save_snapshot_cas(带期望 digest 时)、acquire_writer_lease 与 watch_commits 的默认实现都会返回错误,因此未实现它们的自定义存储会明确失败。

恢复会话

Rust
Node.js
Python
Go
Rust
use a3s_code_core::SessionOptions;
let resumed = agent
.resume_session_async(
"release-review",
SessionOptions::new().with_file_session_store("./.a3s/sessions"),
)
.await?;

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)用于 继续尚未落定的工作,例如进程崩溃之后,或确认仍在停驻时。下一步由事实日志决定:

  1. 如果 SessionStore 中有 runId 的循环检查点,该检查点必须属于本运行与本会话, 否则调用以 refusing to resume checkpoint '<id>' 失败。随后会删除该会话的 事实日志线程,用检查点中的消息重新播种,再折叠日志。检查点只提供事实, 不决定下一次模型调用。
  2. 否则,如果会话的事实日志已经有事实,就按原样折叠该日志。静止的日志不会执行 任何步骤。
  3. 否则调用失败。未配置存储时错误为 resume_run requires a session_store on this session;配置了存储时错误为 no loop checkpoint found for run '<id>'。

每次调用最多折叠 32 步。使用了检查点时,恢复的工作会记录为一个新运行(检查点所属 的运行不被修改),检查点中的 token 用量与工具调用次数会累加到结果中,并且完成门禁 生效:修改了工作区的回合,只有在存在绑定到该修改的 Passed 验证报告时才能完成。

Rust
Node.js
Python
Go
Rust
let result = session.resume_run("run-abc123").await?;
println!("{}", result.usage.total_tokens);

需要精确、可安全重放的恢复标识的无界面宿主,使用 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/ 可能包含提示词、工具输出或私有路径,不应公开提交。