---
title: "部署 Agent"
description: "本页介绍如何在生产环境中运行 agent framework 的 session。"
canonical_url: "https://docs.superdurable.io/zh-Hans/agent-frameworks/deploy-agents/"
source: "docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/agent-frameworks/deploy-agents.mdx"
locale: "zh-Hans"
---

# 部署 Agent

本页介绍如何在生产环境中运行 [agent framework](https://docs.superdurable.io/zh-Hans/agent-frameworks.md) 的 session。
一次 Flow 执行就是一个 agent session。每次模型调用是一个 **RunModel** Step，每个
tool call 是独立的 Step。本页中的服务器限制都是 server-zd 的默认值。其余配置见
[Server 参考](https://docs.superdurable.io/zh-Hans/reference/server.md) 和
[Application](https://docs.superdurable.io/zh-Hans/primitives/components/application.md)。

<a id="install"></a>

## 安装包

连同 framework 一起安装 adapter。Python adapter 发布在 PyPI 上。每个 adapter 都会带上
**dex-agents** 核心包和它支持的 Dex SDK。需要 Python 3.11 或更高版本。

```bash
pip install dex-agents-pydantic-ai "pydantic-ai-slim[openai]"
pip install dex-agents-langgraph
pip install dex-agents-openai-agents
pip install dex-agents-google-adk
pip install dex-agents-claude-agent-sdk
```

TypeScript adapter 发布在 npm 上。核心包和 Dex SDK 是 peer 依赖，所以应用只持有各自的
一份副本。需要 Node 22 或 24。

```bash
npm i @superdurable/dex-agents-vercel-ai @superdurable/dex-agents @superdurable/dex ai zod
npm i @superdurable/dex-agents-mastra @superdurable/dex-agents @superdurable/dex @mastra/core zod
npm i @superdurable/dex-agents-openai-agents @superdurable/dex-agents @superdurable/dex @openai/agents zod
npm i @superdurable/dex-agents-claude-agent-sdk @superdurable/dex-agents @superdurable/dex zod
```

如果要自己编写 runner，只安装核心包：

```bash
pip install dex-agents
npm i @superdurable/dex-agents @superdurable/dex@1.30.0
```

所有 agents 包共用一条版本线。Python adapter 要求同一版本的核心包，npm adapter 以
peer 依赖的方式依赖它。每个版本都固定了它支持的 Dex SDK，以及测试过的 framework 版本范围，
见[兼容性表](https://github.com/superdurable/dex/blob/main/agents/README.md#versions-and-compatibility)。
这些包还没到 1.0，minor 版本可能改变 API。在 lockfile 中固定确切版本，升级前先读
[changelog](https://github.com/superdurable/dex/blob/main/agents/CHANGELOG.md)。

在每个启动 session 或调用其 RPC 的进程中，构建同一个 session Flow。

<a id="stream-backend"></a>

## Stream 后端

模型的实时文本、推理摘要、tool 进度和审批事件都写入 **AgentEvents**
[Stream](https://docs.superdurable.io/zh-Hans/primitives/stream.md)。无论是 **dexcli dev** 还是生产 server，**streamStore.backend**
的默认值都是 **memory**，所以实时事件不需要额外的服务器配置。不要为 agent 把后端设为
**disabled**。设为 **disabled** 时，Step 写入会被丢弃，但不会让 Step 失败；Client 写入和
Stream 读取则返回 **FAILED_PRECONDITION**。默认配置等同于：

```yaml
streamStore:
  backend: memory
```

memory 后端把 Stream 保存在 active server-zd 进程的内存中。一个 server-zd 集群只有一个
active 进程，所以单节点不需要额外配置。[standby](https://docs.superdurable.io/zh-Hans/reference/server/high-availability.md#hot-standby)
不保存 Stream。进程每次成为 active 进程时，Stream 都从空开始：重启之后、standby 接管之后，
以及[让位](https://docs.superdurable.io/zh-Hans/reference/server/high-availability.md#step-down)之后。standby 对 Stream 调用返回
**UNAVAILABLE**，客户端会重新连接到 active 进程。

丢失 Stream 数据不会丢失 session。对话、tool call 和待审批项都保存在 Attribute 中。
错过事件的客户端用 **get_agent_snapshot** RPC 重新加载 session，然后从那里继续读取
Stream。收到 **turn_completed** 或 **turn_failed** 后，客户端用已保存的消息替换这一轮的
实时文本。不要把 Stream 当作事实记录。

<a id="size-agent-events"></a>

## 规划 AgentEvents 容量

**AgentEvents** 的容量按 Flow type 和 Stream name 计算。同一个 agent 的所有 session
共享这份容量，它不是每个 session 的预算。默认值是 10 MiB。在 Python 中用
**AgentSessionOptions** 的 **event_stream_capacity_bytes** 设置，在 TypeScript 中用
**createAgentSessionFlow** 选项的 **eventStreamCapacityBytes** 设置。

服务器为每条消息计入序列化后的值、Flow ID 和 source，再加 512 字节。用量达到容量的
90% 时，后台 trimmer 会删除所有 session 中最旧的消息，直到用量降到 80%。如果 trim
跟不上，用量达到容量，写入会被拒绝。Step 会丢弃被拒绝的写入，但不会失败。容量应能装下
每个有客户端在看的 session 的最新一轮：

```text
bytes per turn = messages per turn × (average message bytes + 512)
capacity       ≥ concurrent sessions × bytes per turn ÷ 0.8
```

假设一轮写入 200 条消息，每条约 300 字节（含 Flow ID 和 source），那么每轮约 162 KB。
500 个 session 同时输出时，实时事件约占 81 MB。除以 0.8 约为 102 MB，所以 128 MiB 的容量
可以保留每个 session 的最新一轮。默认的 10 MiB 大约只能保留 50 个这样的轮次。

固定的 512 字节在小文本增量中占大头。Python 核心包为 runner 写入的每个文本增量写一条
Stream 消息。TypeScript 核心包会把连续的增量合并，最多合并 100 ms 或 1,024 个字符。
Python adapter 每轮的消息数会更多。

一条 Stream 消息最大 100 KiB，由 **streamStore.maxMessageBytes** 设置，更大的写入会被丢弃。
memory 后端把所有 Stream 都放在服务器内存中，所以服务器内存要按所有 agent Flow type
和其他 Stream 的容量之和来规划。

<a id="size-workers"></a>

## 规划 Worker 容量

每个正在运行的模型调用、tool call 和 RPC handler 都占用一个 Worker task slot。等待用户
消息、审批或 tool 结果的 session 不占用 slot，等待发生在 Dex Server 中。每个 Worker 池的
容量应覆盖同时调用模型的轮次、同时运行的 tool call，以及 RPC 预留。

同一次模型响应中的 tool call 作为并行 Step 运行。Worker 的 slot 至少要够模型一次发出的
最大一批 tool call。默认的 task 上限是 CPU 数量。模型调用大部分时间在等网络，所以 agent
Worker 通常需要更高的 **maxConcurrentTasks**。各 SDK 的选项名见
[容量和关闭](https://docs.superdurable.io/zh-Hans/reference/server/workers.md#capacity-and-shutdown)。

保留 [RPC 预留容量](https://docs.superdurable.io/zh-Hans/reference/server/workers.md#reserved-rpc-capacity)。UI 以 RPC 的形式调用
**get_agent_snapshot**、**send_user_message** 和 **resolve_interrupt**。没有预留时，这些
调用要排在占满 Worker 的模型调用后面。不设置时，task 上限至少为 2 则预留一个 slot，否则为零。
Step 从不使用预留 slot。Step 任务队列持续增长，说明 Step 需要更多容量。相关指标见
[Step 任务队列](https://docs.superdurable.io/zh-Hans/reference/server/workers.md#step-task-queue)。

server-zd 默认为每个 app Worker ID 允许 256 个 stream
（**matching.maxConcurrentTasksCap**）。一个 Worker 为每个 task slot 打开一个 stream，
所以它的 task 上限要落在这个范围内。需要更多时，运行更多 Worker 进程。

留出余量。没有 Worker 有空闲 slot 时，Step 会在队列中等待。等待不消耗 attempt，
也不计入 method timeout 和 heartbeat timeout。等待 slot 的一轮或一次 tool call 会花更长时间。
见 [Tool call 的限制](https://docs.superdurable.io/zh-Hans/agent-frameworks/deploy-agents.md#never-run-outcome-unknown)。

<a id="timeouts"></a>

### 长时间模型调用和超时

这些 Step 选项由包设置，**AgentSessionOptions** 不能修改。

| Step                      | 方法超时         | Heartbeat 超时 | Attempt    | 失败路由                         |
| ------------------------- | ------------ | ------------ | ---------- | ---------------------------- |
| **RunModel**              | 15 分钟        | 2 分钟         | 30 分钟内 3 次 | **RecordTurnFailure**        |
| **CompactContext**        | 15 分钟        | 2 分钟         | 30 分钟内 3 次 | **RecordTurnFailure**        |
| **CallTool**              | 2 小时，Dex 默认值 | 1 分钟，Dex 默认值 | 3          | **RecordToolFailure**        |
| **CallSingleAttemptTool** | 2 小时，Dex 默认值 | 1 分钟，Dex 默认值 | 1          | **RecordToolOutcomeUnknown** |

runner 工作时，**RunModel**、**CompactContext** 和两种 tool Step 每 20 秒发送一次 heartbeat。所以慢但没有输出
的模型调用或 tool call 仍然在 heartbeat 超时之内。方法超时依然会结束卡住的调用。在 Python
中，runner 和 tool 代码要保持非阻塞：在 event loop 上的阻塞调用也会让后台 heartbeat 停下来。

一次模型调用必须在 15 分钟内完成。把 provider 或 framework 的请求超时设得比它短，这样慢
调用会带着 provider 的错误失败，后续 attempt 也还能落在 30 分钟的重试窗口内。Claude Agent
SDK session 在一个 **RunModel** 中运行整段轮次，所以这一段必须在 15 分钟内完成。Dex 如何
发现丢失的 attempt，见 [Heartbeat](https://docs.superdurable.io/zh-Hans/reference/server/runtime-steps.md#heartbeat)。

<a id="worker-pools"></a>

### 分开的 Worker 池

Dex 按 Flow type 路由工作。注册了某个 agent Flow type 的每个 Worker，都可能收到它的模型
调用、tool call 和 RPC。包会一起注册 session 的所有 Step，所以同一个 agent 的模型调用和
tool call 共用一个 Worker 池。某类工作挤占另一类工作时，可以这样拆分：

- 给每个 agent 独立的 Flow type 和独立的 Worker 部署，这样繁忙的 agent 不会占用其他 agent
  的 slot。
- 把重量级或特殊的 tool 工作，比如浏览器、沙箱或 GPU 任务，放到有自己主机和扩缩容的服务中。
  tool 用这次调用的幂等键调用该服务。session Worker 只需要等待网络，服务可以独立扩缩容。

<a id="limits"></a>

## 长 session 的限制

一个 session 可以持续几个月。以下 server-zd [限制](https://docs.superdurable.io/zh-Hans/reference/server/configuration.md#limits)
决定它能增长到多大。

| Key                              | 默认值          | 限制对象                                                                |
| -------------------------------- | ------------ | ------------------------------------------------------------------- |
| **limits.maxFlowStateBytes**     | 64 MiB       | 一个 session 的全部存储：当前状态、Attribute 和 Channel 值，以及为 time travel 保留的被替换值 |
| **values.maxValueBytes**         | 16 MiB       | 单个值的上限，比如一条消息、一条 tool call 记录或一份 framework run state                |
| **history.windowSemanticEvents** | 约 10,000 个事件 | 每个 run 保留的 Flow history。更早的事件会被裁剪。                                  |
| **retention.closedFlowDuration** | 7 天          | 已停止的 session 还能读取多久                                                 |

每个 run 在启动时记录这些值。新值只对服务器带着新值重启之后启动的 session 生效。

包这样保存状态：

- **AgentMessages** 保存最新的消息，每条消息一个 AttributeMap 实例。保存一条消息只写这一条。
  其中超过 64 条消息时，最早的消息移入不再重写的 **AgentMessageChunks** 实例。一个 chunk 最多
  32 条消息、1 MiB，因此远低于 **values.maxValueBytes**。更大的消息不进入窗口，保存时直接放进
  自己的 chunk，所以超过 **values.maxValueBytes** 的消息会让它所在的轮次立即失败，和以前一样。
  **AgentMessageIndex** 记录消息所在的位置，只在写入或删除 chunk 时改变。每条消息写两次：一次在窗口中，一次在
  它的 chunk 中；history 保留窗口副本期间，它作为被替换值计入预算。启用[压缩](https://docs.superdurable.io/zh-Hans/agent-frameworks/deploy-agents.md#compaction)时，**RunModel** 只加载
  摘要之后的消息。不启用压缩时，每次模型调用都加载并发送完整历史，也不会删除任何消息。
- **FrameworkRunState** 在一轮等待 tool 结果时保存 framework 的状态。每次模型调用请求 tool
  后，**RunModel** 都会重写它。它保存这一轮到目前为止的工作，包括 tool 输出。OpenAI Agents
  SDK 的状态还包含这一轮的输入，Vercel AI SDK 的状态会把每个 tool 输出保存两次。之前的每个值
  都会作为被替换值保留，只要 history 还保留它，就计入 run 预算。
- **AgentToolCalls** 记录在一轮结束时删除。被删除的值同样会成为被替换值，在 history 窗口
  裁剪掉它之前，仍计入 run 预算。

服务器会对 Step heartbeat 和 Step 完成等写入检查 run 预算，并且只在写入增加存储时检查。
session 超出预算后，这样的 Step 会失败且不重试。模型调用或 tool call 失败时，它所在的轮次
随之失败。没有失败路由的 Step 在它自己的写入越过预算时，会让整个 session 失败。保存下一条
用户消息的 **AwaitUserMessage** 就是这样的 Step。事务型 RPC 的写入，比如
**send_user_message**，不做检查。规划时让 session 远低于 64 MiB。

Flow history 大约保留一个 session 最新的 10,000 个事件。读取对话用
**get_agent_snapshot** 或 **list_agent_messages**，不要从 Flow history 读。
[Time travel](https://docs.superdurable.io/zh-Hans/reference/server/runtime-client.md#time-travel) 只能以
[history 窗口](https://docs.superdurable.io/zh-Hans/reference/server/configuration.md#history-window)内的时间点为目标。
Time travel 到起点始终可用。

session 不会自己结束。完成后用 **StopFlow** 停止它。已停止的 session 及其
**get_agent_snapshot** RPC 在[保留期](https://docs.superdurable.io/zh-Hans/reference/server/configuration.md#retention)内仍可用，
之后 Dex 会清除它。需要比 session 保存更久的对话记录，要在此之前用
**list_agent_messages** 复制出来。

<a id="compaction"></a>

### 压缩与存储上限

压缩从 agents 0.2.0 包开始提供，用来让模型输入保持在上下文窗口之内。在
**AgentSessionOptions** 中，或者在 Stepwise agent 的定义中设置 **compaction**，并给出模型的
上下文窗口 token 数。runner 必须支持压缩；Stepwise 支持，framework adapter 目前还不支持。
当一次模型调用的输入将达到窗口的 85% 时，session 会先运行一个 **CompactContext** Step。它用
一次模型调用把较早的消息折叠成一份摘要，下一次模型调用收到的是这份摘要和最新的消息。如果
较早的消息放不进窗口，Stepwise 会在摘要调用中略去其中最早的部分，并告诉模型。一个包含很多
tool call 的长轮次会在轮次中途压缩。

只有在该 Flow 类型的每个 Worker 都运行带有 **CompactContext** Step 的版本之后，才启用压缩。
读取 session 快照的客户端也需要 agents 0.2.0：0.1.0 客户端无法读取压缩期间快照中的
**compacting_context** 状态。

压缩也限定了 session 的存储。当最早的 chunk 中每条消息都已被摘要，且它之后至少还保存着
**retained_message_limit**（默认 1,000）条消息时，保留策略会删除这个 chunk。最新的 1,000 条
消息和摘要之后的每条消息始终保留，所以无论 session 运行多久，保存的消息都不再增长。在
**AgentSessionOptions** 中用 **retained_message_limit**、**message_window_size**、
**message_chunk_size** 和 **message_chunk_max_bytes** 设置上限、窗口大小和 chunk 限制。上限设为
**None**（TypeScript 中为 **null**）时保留每条消息。如果把 **values.maxValueBytes** 调到约 2 MiB
以下，也要同时调低 **message_chunk_max_bytes**。

被删除的消息不再出现在 **list_agent_messages** 和 **get_agent_snapshot** 中。分页在最早保存的
消息处结束，每一页的 **deleted_before_sequence** 表示保留策略删除了它之前的消息。如果应用
需要完整对话，就在消息之后又保存 **retained_message_limit** 条消息之前，用
**list_agent_messages** 把新消息复制出去。一个简单的做法是在每个 **turn_completed** 事件之后，
从上次复制到的序号之后开始复制。如果一轮可能保存的消息多于上限，就提高上限或设为 **None**。

根据保留的消息估算 run 预算。在一个实测的 session 中，每轮保存一个 100 字符的问题和一个
600 字符的回答，约 2.9 KB，session 在 8,000 token 的窗口下压缩。上限为 100 时，在第 200、
400、600 和 800 轮，保存的消息都在 162 KB 到 185 KB 之间，session 的全部活动状态在 167 KB 到
191 KB 之间，而 session 一共保存了 2.3 MB 的消息。使用默认上限 1,000 时，按上限乘以一条消息的
大小，加上摘要之后的消息，再加上 history 窗口保留的被替换值来规划。被删除的 chunk 在 history
窗口裁剪掉它之前，作为被替换值计入预算。

让长 session 保持在预算之内：

- 新的、无关的工作开一个新 session，不要让一个 session 无限增长。
- 让 tool 输出保持小。返回模型需要的内容，比如 ID、摘要或排名靠前的结果，而不是整份文档。
  每个输出都会保存在它的 tool call 记录中、这一轮运行期间的 run state 中，以及消息中。
- 把文件、图片和长文档等大型产物放到你自己的 blob 存储中，让 tool 返回它们的引用。

<a id="approvals"></a>

## 审批和截止时间

需要审批的 tool call 在独立的 **AwaitToolApproval** Step 中等待。等待不占用 Worker slot，
也不受 Worker 重启和部署影响。

没有截止时间时，审批会一直等到有人回答，这一轮也跟着等待。新的用户消息会排队，直到这一轮
结束。在 Python 的 **AgentSessionOptions** 中设置 **interrupt_deadline**，或在 TypeScript
中设置 **interruptDeadlineMs**。到截止时间仍未处理的调用会被拒绝，消息是
"approval deadline passed"，模型会看到这次拒绝。

session 在模型请求调用时，用 Worker 的时钟计算截止时间。保持 Worker 时钟同步。修改截止时间
选项后，只对之后产生的审批生效。Dex Timer 以整秒为单位，所以截止时间的 Timer 会向上取整，
最多晚一秒触发。从截止时间起，**resolve_interrupt** 返回 false，所以迟到的回答不会生效。

Step 持有同一把锁时，**resolve_interrupt** 可能以 **RpcLockConflictError** 失败。重试是
安全的。Python 的 **AgentSessionClient** 最多重试 5 秒。

<a id="worker-failures"></a>

## Worker 被杀和重新部署

崩溃最多重跑当时正在运行的那一次调用。

| Worker 停掉时正在运行            | 结果                                                                                                                                                                                              |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **RunModel**              | 这次模型调用重新运行，30 分钟内最多 3 次 attempt，之后这一轮失败。如果 tool 已经运行过，history 中的一条说明会告诉下一轮的模型不要重复它们。                                                                                                            |
| **CompactContext**        | 只有摘要调用重新运行，30 分钟内最多 3 次 attempt。session 只保存一份摘要。如果所有 attempt 都失败，这一轮失败，下一条用户消息会再次压缩。                                                                                                            |
| **CallTool**，读取或幂等写入      | 用同一个幂等键再次运行。最后一次 attempt 之后，读取记录 **failed**，写入记录 **outcome_unknown**。                                                                                                                           |
| **CallSingleAttemptTool** | 记录 **outcome_unknown**，不再运行。模型会看到结果未知。                                                                                                                                                          |
| 等待用户消息、审批或 tool 结果        | 不会重跑任何东西。等待发生在 Dex Server 中。                                                                                                                                                                    |
| framework 无法推迟的 tool      | tool 在 **RunModel** 内部运行，**RunModel** 重跑时它也会重复，属于至少一次。                                                                                                                                          |
| Claude Agent SDK 的一段轮次    | 被推迟的自定义 tool 在各自的 **CallTool** 或 **CallSingleAttemptTool** Step 中运行，规则同上。模型调用、inline 读取 tool，以及 **Bash**、**Write**、**Edit**、**Task** 等 CLI tool 在 **RunModel** 内部运行，**RunModel** 重跑时会重复，属于至少一次。 |

单次尝试的 tool 在少数很窄的情况下会运行两次，例如节点崩溃后 ASYNC 的重新运行。见
[Tool call 的限制](https://docs.superdurable.io/zh-Hans/agent-frameworks/deploy-agents.md#tool-runs-twice)。

优雅停止 Worker 时，它会在 drain 超时内等待正在运行的 handler，默认 30 秒，然后取消它们。
被取消的模型调用会重新运行，被取消的 **CallTool** 会重试。因取消而停止的单次尝试 tool 会记录
**outcome_unknown**。把 drain 超时设得比通常的模型调用和 tool call 时间长，再把容器的终止
宽限期设得比 drain 超时长。先等新 Worker [注册](https://docs.superdurable.io/zh-Hans/reference/server/workers.md#readiness)完成，
再停止旧 Worker。

<a id="tool-call-limits"></a>

## Tool call 的限制

单次尝试的 tool 在 **CallSingleAttemptTool** Step 中运行。这个 Step 负责
**non_idempotent_write** 和 **unclassified** tool。它应当最多运行一次 tool，并在 tool 可能
已经运行过时记录 **outcome_unknown**。tool 确定没有运行时，这次调用改为记录 **failed**，模型
可以再次调用这个 tool。有两种情况会打破“最多运行一次”这一承诺，两种都要提前准备。它们源于 Dex 交出任何
Step attempt 的方式。见[失败与重新投递](https://docs.superdurable.io/zh-Hans/reference/server/runtime-steps.md#failure-and-redelivery)。

<a id="tool-runs-twice"></a>

### 单次尝试的 tool 可能运行两次

Dex 在把 attempt 交给 Worker 时开始这个 attempt，并且不会再把同一个 attempt 编号交给另一个
Worker。如果 Worker 在 tool 运行期间停掉，或者断开了与 Dex 的连接，这个 attempt 会失败。单次
尝试的 tool 没有剩余的 retry，所以这次调用记录 **outcome_unknown**。之前的那次运行可能已经
完成了副作用，因此 session 不会再次运行这个 tool。

只有两种很窄的情况仍可能让单次尝试的 tool 运行两次：

- 这个 Step 使用 ASYNC durability，并且 Dex Server 节点在 Dex 把 attempt 的开始记录写入
  object storage 之前崩溃。Dex 会回滚这次开始，并再次运行同一个 attempt 编号，可能在另一个
  Worker 上。两次运行可能重叠。
- 取消是协作式的。Dex 取消一个 attempt 时，被中断的运行可能仍在继续，而 failure route 已经开始。

要让第二次运行无害，把这次调用的幂等键传给 tool 写入的服务。每个 tool call 都会收到一个幂等键：
session 的 Flow ID、轮次 ID 和 tool call ID，用斜杠连接，例如 **session-42/turn-3/call-7**。
同一次调用的每次 attempt 都使用同一个键。服务按这个键去重时，把这个 tool 归类为
**idempotent_write**，这样它失败后也会重试。

<a id="never-run-outcome-unknown"></a>

### 从未运行的调用也可能记录 outcome_unknown

tool Step 失败时，它的 failure route 会从 recovery error 中读取 **attempt_progress**。见
[失败与重新投递](https://docs.superdurable.io/zh-Hans/reference/server/runtime-steps.md#unknown-outcome)。**CallTool** 和
**CallSingleAttemptTool** 的处理方式相同：

| **attempt_progress**                                | 这次调用记录的结果                                                                                                                                                                                   |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **NOT_STARTED**                                     | 没有 attempt 到达 Worker，所以 tool 没有运行。这次调用记录 **failed**，错误信息以 "the tool did not run, so it is safe to call again" 开头。模型会看到一次失败的 tool call，可以再次调用这个 tool。如果这次调用的 tool 已经向用户提过问题，说明它之前运行过，按下一行处理。 |
| **NOT_COMPLETED**，或 server 没有报告进度时的 **UNSPECIFIED** | tool 可能已经运行过。**read** 调用记录为 **failed**，其他调用记录为 **outcome_unknown**，session 不会再次运行它的 tool。                                                                                                   |

要在 **get_agent_snapshot** 或你自己的代码中识别这种调用，把它的错误信息开头与 Python 的
**TOOL_CALL_NOT_RUN_ERROR_PREFIX**、TypeScript 的 **toolCallNotRunErrorPrefix** 或 Go 的
**ToolCallNotRunErrorPrefix** 比较。

**NOT_COMPLETED** 是保守的结果，所以从未运行的调用仍可能记录 **outcome_unknown**。例如，
server 在记录了 attempt 的开始之后、任务到达 Worker 之前崩溃。请把这种结果当作未知。

如果没有 Worker 接收 tool Step，这次调用会在队列中等待。当这个 Flow 类型的每个 Worker 都达到
task 上限，或者一个 Worker 都没有运行时，就会这样。调用不会失败，也不消耗 attempt，有 Worker
接收后才运行。如果 Flow 先超时或被停止，这次调用就不会运行。

让 tool Step 始终能被分发，避免调用长时间等待：

- 按[规划 Worker 容量](https://docs.superdurable.io/zh-Hans/agent-frameworks/deploy-agents.md#size-workers)规划每个 Worker 池，并为最大的一批 tool call 留出余量。
- 每个 agent Flow 类型至少保持一个 Worker 在运行。Worker 池缩容到零时，tool call 会在队列中等待，
  直到有 Worker 启动。
- 部署时，先启动新 Worker，等它们[注册](https://docs.superdurable.io/zh-Hans/reference/server/workers.md#readiness)完成，再停止旧
  Worker。

<a id="reconcile-outcome-unknown"></a>

### 核对 outcome_unknown

**outcome_unknown** 表示效果可能发生了，也可能没有。session 从不重试这样的调用，模型会看到结果
未知。在 session 之外核对它：

1. 找到这次调用。在这一轮运行期间，**get_agent_snapshot** 会在 **tool_calls** 中列出它，失败
   信息在 **error** 中。**AgentEvents** Stream 中有一条状态为 **outcome_unknown** 的
   **tool_call_finished** 事件。这一轮结束后，消息 history 中它的 tool 结果会说明结果未知。
2. 用这次调用的幂等键或你自己的业务键，在目标系统中检查效果是否发生。
3. Dex 确定从未运行的调用会记录 **failed**，而不是 **outcome_unknown**。如果 Flow 的 history
   仍然显示 tool Step 的 attempt 从未到达 Worker，例如在 server 升级之前开始的调用，这次调用就
   没有运行。
4. 用 **send_user_message** 把实际结果告诉 session，让下一轮的模型基于它工作。

<a id="upgrades"></a>

## 在 session 等待时升级 agent 代码

等待中的 session 运行的是接手它下一个 Step 的 Worker 上的代码。agent 循环不会重放，所以
部署在下一次模型调用或 tool call 时生效，即使在一轮中间也一样。滚动部署期间，同一个
session 的两个 Step 可能运行在不同版本上。

- 保持每个 agent 的 Flow type 不变。运行中的 session 靠它恢复。
- 只要运行中的轮次或待处理的审批还引用某个 tool，就保留这个 tool。
- 已记录的 tool call 保留模型请求它时适用的 effect 和审批规则。修改后的 tool policy 对之后
  的调用生效。
- 修改后的 prompt 或模型在下一次模型调用时生效。修改后的 tool 在它下一次被调用时生效。
- 在一轮的各个 Step 之间，**FrameworkRunState** 保存 framework 序列化的状态。新 Worker 上的
  framework 版本必须能恢复它。保持在 adapter 固定的 framework 版本范围内，并且同时升级
  adapter 和 framework。
- **从 agents 0.1.0 升级时：先停止该 Flow type 的所有 0.1.0 Worker，再启动任何 0.2.0
  Worker。** 不要让两者同时运行。0.1.0 的 session 把每条消息都保存在 **AgentMessages** 中。
  0.2.0 Worker 一旦在其中保存消息，就会把较早的消息移入 **AgentMessageChunks**，每个 Step 移动
  少量 chunk。此后如果 0.1.0 Worker 运行这个 session 的 Step，这一轮会失败，因为它读不到被移走
  的消息。

通用规则见 [Flow 代码版本控制](https://docs.superdurable.io/zh-Hans/primitives/components/application.md#versioning-flow-code)。

<a id="session-settings-rollout"></a>

### 滚动部署期间的 session settings

只有在该 Flow type 的每个 Worker 都运行 agents 0.2.0 之后，才用 session settings 启动
session，或调用 **update_session_settings**。0.1.0 的 Worker 会丢弃它启动的 session 的
settings，也不会写入或读取一轮所保留的 settings 副本。由 0.1.0 Worker 启动的一轮没有
settings，所以它的部分模型调用可能使用 Worker 的默认模型。

<a id="credentials"></a>

## 凭证

把模型 API key 和 tool 凭证放在 Worker 的环境中，或放在 Worker 调用时读取的 secret store
中。每个 framework 都从环境中读取自己的模型 key，比如 **OPENAI_API_KEY**、
**GOOGLE_API_KEY** 或 **ANTHROPIC_API_KEY**。adapter 本身不处理凭证。

不要在 session 的启动输入、用户消息、tool 参数、tool 输出或 Attribute 中传递凭证。Session
settings 属于启动输入和 session 状态，**get_agent_snapshot** 和浏览器 snapshot JSON 都会
返回它们，所以其中只放 Worker 能解析的引用，比如 model 名称或凭证名称。Dex 会把
它们保存在 Flow history 和状态中。Dex Web 会显示它们，history 会为 time travel 保留被替换
的值，已停止的 session 在保留期内仍可读取。任何获得授权的 Client 都能读取 **AgentEvents**。

属于某个用户的凭证，让 tool 在调用时从 secret store 中查找，查找用的 key 不能是凭证本身。
当你自己的 Flow 必须持有短期 secret 时，把它存放在
[hard-delete Attribute](https://docs.superdurable.io/zh-Hans/primitives/attribute.md#hard-delete-attributes) 中，Flow 不再需要时
删除它。

Worker 和 Client 使用 bearer 凭证向 Dex Server 认证。见
[身份验证](https://docs.superdurable.io/zh-Hans/reference/server/authentication.md)。

## 相关内容

- [Agent Framework](https://docs.superdurable.io/zh-Hans/agent-frameworks.md)
- [AI Agent](https://docs.superdurable.io/zh-Hans/use-cases/ai-agent.md)
- [Stream](https://docs.superdurable.io/zh-Hans/primitives/stream.md)
- [Application](https://docs.superdurable.io/zh-Hans/primitives/components/application.md)
- [Server 参考](https://docs.superdurable.io/zh-Hans/reference/server.md)
- [Agents 包 README](https://github.com/superdurable/dex/blob/main/agents/README.md)
