Skip to content
预计 1 分钟右侧目录可快速定位

Run、Session 和 Durable State 到底各保存什么 ​

前置:Run Lifecycle;Side-effect / Retry Semantics
核心问题:同一条“状态”到底应该属于当前 Run、长期 Session 还是 Durable Store,放错位置会在 restart/resume 时造成什么后果?

同一个聊天 Session 中,用户连续发起三个 Run;第二个 Run 因高风险 Tool 等待审批,服务随后重启。系统必须同时保留聊天连续性、当前 Run 的等待状态,以及任务跨进程继续所需的 durable facts。

Mental Model ​

text
用户会话时间轴
Session S-42
├── Run R-100  "帮我分析仓库"
├── Run R-101  "继续修这个 bug"
└── Run R-102  "把测试也补上"

单次执行时间轴
Run R-101
CREATED → RUNNING → WAITING_APPROVAL → RUNNING → SUCCEEDED
             │             │
             │             └── pending action / resume token
             └── current node / tool attempt / budget / error

长任务时间轴
Task T-7
Day 1: cloned repo → located root cause
Day 2: patch applied → waiting external approval
Day 3: resumed → test passed → artifact produced

Run State ​

Run State 是某一次 run_id 对应的当前执行事实。它应该由 Runtime 写入和推进,而不是让模型通过自然语言声明。

典型字段:

python
@dataclass
class RunState:
    run_id: str
    status: RunStatus
    turn_index: int
    active_agent: str | None
    current_node_id: str | None
    deadline_at: datetime | None
    token_used: int
    cost_used: Decimal
    pending_approval_id: str | None
    last_checkpoint_id: str | None
    error: RunError | None
    version: int

Session State ​

Session 解决的是多次交互的连续性。在聊天式 Agent 中,它最典型的职责是让下一次 Run 能拿到之前的 conversation items。

OpenAI Agents SDK 当前的 Session 接口就是这个语义:当 Session 被提供给 Runner 时,历史项会在后续 turn/run 中被读取,新输入和输出也会持久化到该 Session。它强调的是 conversation history persistence,而不是完整 Runtime 状态。Sandbox Agent 的 Memory 又被官方明确区分为另一套能力。这个区别非常重要。

python
@dataclass
class SessionRecord:
    session_id: str
    user_id: str
    created_at: datetime
    last_active_at: datetime
    metadata: dict[str, str]

@dataclass
class SessionItem:
    session_id: str
    seq: int
    role: str
    item_type: str
    payload: dict

Durable Task State ​

假设 Agent 正在执行一个持续 6 小时的代码迁移任务:

  1. 已经分析 80 个文件;
  2. 已完成 5 个修改;
  3. 第 6 步需要人工批准;
  4. 服务发布,进程被重启;
  5. 第二天用户继续任务。
python
@dataclass
class DurableTaskState:
    task_id: str
    objective: str
    constraints: list[str]
    plan_version: str | None
    completed_nodes: set[str]
    pending_nodes: set[str]
    pending_approvals: list[str]
    artifact_refs: list[str]
    external_effect_records: list[str]
    latest_checkpoint_id: str
    schema_version: int

🔍 源码对照:Durable state 会按恢复职责拆成多个持久文件 ​

精确定位

ts
const durableFiles = {
  upgradeResume: "host-upgrade-resume.json",
  pendingWake: "host-pending-wakes.json",
  dedupeNonce: "host-xuser-turn-nonces.json",
  ackObligations: "ack-obligations.json",
}

对照理解

  • 真实工程没有把“durable state”收敛成一个万能 state.json;不同恢复语义有不同 durable owner。
  • 这提醒 Shadow Harness:持久化结构应围绕恢复 invariant 设计,而不是围绕内存对象结构设计。
  • 文件名只是该产品实现;可迁移的是“恢复职责分离”这个原则。

源码说明

conversation state、storage、transcript mirror 的并列存在,是理解 State 分层的好案例:用户看到的会话记录、Runtime 正在推进的执行事实、需要跨进程保留的数据,并不天然属于同一个对象。把所有东西塞进一个 session.json 会让 ownership、恢复和迁移都变得模糊。

工程边界

这些目录名只提供拆分线索,不代表其每个字段都对应我们定义的 RunState/SessionState/DurableTaskState;Domain Language 仍以本知识库和 Shadow Harness 为准。

Shadow Harness 对照

  • RunState 只保存一次执行;SessionState 管跨 Run 的交互容器;DurableTaskState 保存恢复任务所需事实。
  • 每类 state 明确 owner、lifetime、persistence、version。

验证

关闭进程后重新打开同一 Session:新 Run 可以继承允许的 conversation/session context,但不能继承上个 Run 的 transient RUNNING node。

最小实现 / 数据结构 ​

最小可以拆成下面这些逻辑记录:

text
runs
run_events
sessions
session_items
tasks
task_checkpoints
node_executions
approval_requests
external_effects

这不表示一定要对应 9 张物理表;重点是让不同生命周期和一致性要求的数据拥有清楚的逻辑责任。

sql
CREATE TABLE runs (
    run_id            VARCHAR PRIMARY KEY,
    task_id           VARCHAR NULL,
    session_id        VARCHAR NULL,
    status            VARCHAR NOT NULL,
    current_node_id   VARCHAR NULL,
    last_checkpoint_id VARCHAR NULL,
    version           BIGINT NOT NULL,
    created_at        TIMESTAMP NOT NULL,
    updated_at        TIMESTAMP NOT NULL
);

在 Shadow Harness 中怎么落地 ​

不要用一个包罗万象的 MemoryManager 同时承载执行事实、会话历史和长任务状态。最小边界可以直接体现在 Store 接口上:

python
class RunStateStore(Protocol):
    async def get(self, run_id: str) -> RunState: ...
    async def compare_and_set(self, old: RunState, new: RunState) -> bool: ...

class SessionStore(Protocol):
    async def get_items(self, session_id: str, limit: int | None = None) -> list[SessionItem]: ...
    async def append_items(self, session_id: str, items: list[SessionItem]) -> None: ...

class TaskStateStore(Protocol):
    async def load(self, task_id: str) -> DurableTaskState: ...
    async def save(self, state: DurableTaskState) -> None: ...

这样 Runtime 恢复 Run 时读取 RunStateStore,新一轮模型调用需要聊天连续性时读取 SessionStore,长任务跨进程继续时读取 TaskStateStore。三者可以落在同一个数据库,但不能因此混成同一种状态。

工程取舍 ​

  • 一个大 State 表:早期简单,但生命周期、权限和更新频率很快耦合。
  • 分逻辑 Store:接口更多,但状态语义清晰,更容易测试和迁移。
  • Snapshot + incremental events:通常比“只有 snapshot”或“全量 event sourcing”更平衡。

最终判断标准不是表数量,而是 crash 后能否明确回答:哪些事实已提交、谁能修改、从哪里恢复。

Failure Modes ​

把 conversation history 当成所有状态 ​

症状: 系统只保存聊天记录,却没有独立 Run/Tool/Approval/External Effect 状态。crash 后只能从自然语言猜程序执行到哪里。

为什么危险: Conversation 的语义是交互历史,而外部副作用、Node completion、approval 等属于执行事实。把两者混合后,summary/trimming 会直接破坏恢复正确性。

处理: 把 conversation item、RunState、execution record、checkpoint 分开;外部动作必须有可查询的 commit/idempotency 记录。

把 Session ID 当 Run ID ​

症状: 同一个聊天线程中第二个请求覆盖第一个 Run 的状态,或者系统无法表达同 Session 下多个 Run。

为什么危险: Session 和 Run 生命周期、基数关系不同。把 ID 合并会把 conversation continuity 与 execution lifecycle 强绑在一起。

处理: 明确 session_id -> many run_id;Run 另有唯一标识和 terminal state,SessionStore 不拥有 Runtime status。

Durable State 保存运行时对象 ​

症状: Checkpoint/TaskState 中出现 HTTP client、file handle、lock、coroutine、Task 等进程内对象。

为什么危险: 这些对象无法可靠跨进程、跨版本或跨 worker 恢复;强行序列化会把实现细节写进持久化协议。

处理: 只保存可版本化 domain data 和 resource identifiers;连接、client、locks、tasks 在 resume 时重新建立。

状态没有版本控制 ​

症状: 两个 worker 同时读取 RUNNING 并各自提交更新,导致同一 Node 或副作用执行两次。

为什么危险: 跨 worker resume 时没有 optimistic version、lease 或 compare-and-set,就没有单一状态提交者。

处理: 对 Run/Task 引入 version/lease;状态提交使用 CAS,并配合 Tool idempotency key。

如何验证 ​

Case断言
把 conversation history 当成所有状态模拟 Tool 已成功但进程在下一轮模型调用前 crash;恢复后不得因为 transcript 不完整而重复执行 Tool。
把 Session ID 当 Run ID在同一 session 创建两个 Run,分别暂停/完成,断言两份 RunState 完全独立且 Session history 仍按顺序追加。
Durable State 保存运行时对象序列化 DurableTaskState 后在新进程恢复,断言无需复用旧进程对象即可继续任务。
状态没有版本控制让两个 worker 同时 claim 同一 run_version,断言只有一个 CAS 成功。

关联源码 ​

Codex 源码对照:SessionState Ownership / Model Projection ​

要回答的问题 ​

哪些状态属于 Session lifetime,哪些只属于当前 active Turn,哪些可以跨多次 sampling,哪些才会被投影给模型?

精确定位 ​

Tag: rust-v0.150.1

File:

Symbols:

  • Session
  • SessionState
  • ActiveTurn
  • TurnState
  • ContextManager
  • ContextManager::for_prompt(...)
  • run_turn(...)

最小调用链 ​

text
Session
│
├─ state: SessionState
│  ├─ history: ContextManager
│  ├─ latest_rate_limits
│  ├─ previous_turn_settings
│  ├─ auto_compact_window
│  ├─ session-level granted permissions
│  └─ ...
│
└─ active_turn: ActiveTurn
   ├─ RunningTask / cancellation
   └─ TurnState
      ├─ pending approvals / permission requests / user input
      ├─ pending input / mailbox phase
      ├─ turn-granted permissions
      ├─ tool_calls
      └─ token_usage_at_turn_start

SessionState.history
→ clone_history()
→ ContextManager::for_prompt(...)
→ sampling_request_input
→ model

Observable Fact ​

state/session.rs 文件级注释直接标明它是 Session-wide mutable state。SessionState 中的 history、rate limit snapshot、previous turn settings、auto-compaction window、connector selection 等可跨一次 run_turn() 存活。

state/turn.rs 则把 TurnState 定义为 single-turn mutable state,包含 pending approvals / permissions / input、turn-granted permissions、tool call counter 等。run_turn() 内部多次 sampling 时,这些 active-turn state 可以继续存在。

ContextManager::for_prompt() 只生成 model-visible history projection;Session 中的 permission authority、approval state、rate-limit bookkeeping、execution controls 并不会因此自动变成 model input。

Design Inference ​

状态至少要按四个问题分类:

  1. owner 是 Session、Run 还是 single logical Turn?
  2. lifetime 是否跨 sampling / 跨 user turn / 跨进程?
  3. 它是 source-of-truth authority 还是 model-visible projection?
  4. resume 时是直接恢复,还是需要 revalidate?

同名结构体不是映射依据;职责和 lifecycle 才是。

Codex-specific Trade-off ​

Codex rust-v0.150.1 把 history 与 token bookkeeping 放进 SessionState,把 pending interaction 放进 TurnState,并另外通过 thread store/session persistence 支撑恢复。这种具体拆分是 Codex 产品结构,不能据此推出“所有 durable facts 都应放 SessionState”。

Shadow Harness Delta ​

职责对照而非名字对照:

CodexShadow Harness应吸收的职责
SessionState.historySession/Run history store跨 sampling 保存已接受事实
TurnStateactive control state / TurnState 的一部分pending interaction、tool/approval control
ContextManager::for_prompt() 输出ContextSnapshot一次 model request 的 projection
permission / approval configRuntime authority不允许模型自行修改

另外为 durable resume 增加显式 revalidation:恢复后的 environment/tool schema/policy 与旧 execution fact 不应因为“Session 还在”就默认兼容。

不要过度推导 ​

SessionState 是 session-wide mutable state,不等于“所有字段都是 durable state”;TurnState 也不等于本知识库 canonical TurnState。源码结构本身不能替代 durability contract 与 resume semantics 的单独设计。

完整映射见 OpenAI Codex Source Map。

自测 ​

  1. 用户 Session 还在,但某个 Run 已结束。哪些状态属于 Run,哪些属于 Session,哪些必须持久化成跨进程 Durable State?
  2. 如果把临时 turn scratch 全部写进 durable store,会带来什么生命周期和恢复噪声?

Sources ​

AI Engineering · Agent · Harness · Backend Systems