六个月前,在开发一个内部效率工具时,我们的团队做了一个在当时颇具争议的决定:用零人工编写的代码来构建代码库。项目仓库中的每一行代码都必须由 Codex 生成。
为了实现这一点,我们从零开始重新设计了工程工作流。我们构建了一个对智能体友好的仓库,在自动化测试和护栏上投入了大量精力,并将 Codex 视为团队中的正式成员。我们在之前的赋能工程博文中记录了这段经历。
这个方法奏效了,但随后我们遇到了下一个瓶颈:上下文切换。
为了解决这个新问题,我们构建了一个名为 Symphony 的系统。Symphony(在新窗口中打开) 是一个智能体编排器,它将 Linear 这类项目管理看板变成了编码智能体的控制平面。每个未完成的任务都会有一个专门的智能体,智能体持续运行,由人工审查结果。
本文将解释我们如何创建 Symphony——在某些团队中,合并的拉取请求数量增加了500%——以及如何用它将自己的问题追踪器变成一个永不离线的智能体编排器。
尽管编码智能体——无论是通过网页应用还是命令行界面使用——越来越容易上手,但它们本质上仍然是交互式工具。
随着 OpenAI 内部智能体工作规模的扩大,我们发现了一种新的负担。每位工程师都会打开几个 Codex 会话,分配任务,审查输出,引导智能体,然后重复这个过程。实际上,大多数人一次只能舒适地管理三到五个会话,超过这个数量,上下文切换就会变得痛苦。之后,生产力就会下降。我们会忘记哪个会话在做什么,在终端之间跳转以推动智能体回到正轨,并调试那些中途停滞的长时间运行任务。
智能体速度很快,但我们的系统瓶颈在于:人的注意力。我们实际上组建了一支由极其能干的初级工程师组成的团队,然后让人类工程师去微观管理他们。这无法扩展。
我们意识到自己优化错了方向。我们围绕编码会话和合并的 PR 来组织系统,而 PR 和会话本身只是达到目的的手段。软件工作流在很大程度上是围绕可交付成果来组织的:问题、任务、工单、里程碑。
因此,我们问自己,如果我们不再直接监督智能体,而是让它们从任务追踪器中拉取工作,会发生什么?
这个想法演变成了 Symphony,一份书面的技术规范,充当监督者来编排智能体工作。
Symphony 从一个简单的概念开始:任何未完成的任务都应该由一个智能体接手并完成。我们不再在多个标签页中管理 Codex 会话,而是将问题追踪器作为控制平面。
https://openai.com/index/open-source-codex-orchestration-symphony/?video=1186371009

在这个设置中,每个未完成的 Linear 工单都对应一个专用的智能体工作空间。Symphony 持续监控任务看板,确保每个活跃的任务都有一个智能体在循环中运行,直到完成为止。如果某个智能体崩溃或停滞,Symphony 会重新启动它。如果有新任务出现,Symphony 会接手并开始组织工作。
我们基于工单状态构建了工作流,将任务管理器 Linear 用作一个状态机。
在实践中,Symphony 将工作与会话和拉取请求解耦。有些问题会在多个仓库中产生多个 PR;有些则纯粹是调查或分析,从不触及代码库。
一旦工作以这种方式被抽象,工单就可以代表更大的工作单元。
我们经常使用 Symphony 来编排复杂的功能和基础设施迁移。例如,我们可能会提交一个任务,要求智能体分析代码库、Slack 或 Notion,并生成一个实施计划。一旦我们对计划满意,智能体就会生成一个任务树,将工作分解为多个阶段,并定义任务之间的依赖关系。
智能体只会开始处理那些未被阻塞的任务,因此执行会自然地、最优地并行进行,以符合这个 DAG(一系列执行步骤)。例如,我们将 React 升级标记为等待迁移到 Vite。正如预期的那样,智能体只有在 Vite 迁移完成后才开始升级 React。
智能体也可以自己创建工作。在实施或审查过程中,它们经常注意到当前任务范围之外的改进点:性能问题、重构机会或更好的架构。当这种情况发生时,它们只需提交一个新工单,我们可以稍后评估和安排——这些后续任务中的许多也会被智能体接走。在我们监督这个过程的同时,智能体保持有序,推动工作向前推进。
这种工作方式极大地降低了启动模糊任务的认知成本。如果智能体做错了什么,那仍然是有用的信息,而我们的成本几乎为零。我们可以非常廉价地提交工单,让智能体去原型开发和探索,然后丢弃任何我们不喜欢的探索结果。
由于编排器在开发机上运行且从不休眠,我们可以从任何地方添加任务,并知道某个智能体会接手。例如,我们团队的一名工程师在一个网络糟糕的舒适小屋里,通过手机上的 Linear 应用完成了三项重大更改。
在观察 Symphony 工作效果时,最明显的变化是产出。在 OpenAI 的一些团队中,我们看到合并 PR 的数量在前三周内增加了500%。在 OpenAI 之外,Linear 创始人 Karri Saarinen 指出,随着我们发布 Symphony,创建工作空间的数量激增(在新窗口中打开)。然而,更深层次的变化在于团队如何看待工作。
当我们的工程师不再花时间监督 Codex 会话时,代码变更的经济性就完全改变了。每次变更的感知成本下降了,因为我们不再需要投入人力去驱动实现本身。
这改变了我们的行为。在 Symphony 中启动推测性任务变得微不足道。尝试一个想法,探索一次重构,测试一个假设,只保留看起来有希望的结果。
这也拓宽了可以发起工作的人员范围。我们的产品经理和设计师现在可以直接将功能请求提交到 Symphony 中。他们不需要检出仓库或管理 Codex 会话。他们描述功能,然后收到一个审查包,其中包含该功能在实际产品中运行的视频演示。
Symphony 在大型单体仓库(比如我们在 OpenAI 使用的那个)中也能大放异彩,因为在这样的仓库中,合并 PR 的最后一英里既慢又脆弱。该系统会监控 CI,在需要时变基,解决冲突,重试不稳定的检查,并通常引导变更通过管线。到工单达到“合并”状态时,我们有很高的信心,该变更会在没有人工照看的情况下进入主分支。
实施 Symphony 后,我们将更多工作委托给智能体,专注于更困难、更具探索性的任务。
在这种水平上操作需要权衡取舍。当我们从交互式地引导智能体转变为在工单级别分配工作时,我们失去了在任务中途不断推动和必要时纠正方向的能力。有时智能体产生的结果完全偏离目标。这很有用——这些失败揭示了系统的漏洞,并帮助我们使其更加健壮。
我们没有手动修补结果,而是添加了护栏和技能,以便智能体下次能够成功。随着时间的推移,这使我们为赋能工程增加了新能力,例如运行端到端测试、通过 Chrome DevTools 驱动应用以及管理 QA 冒烟测试。我们显著改进了文档,明确了什么样的结果才算好。
并非所有任务都适合 Symphony 的工作方式。有些问题仍然需要工程师直接与交互式 Codex 会话一起工作,特别是模糊的问题或需要强大判断力和专业知识的工作。在实践中,这些通常是我们的工程师最感兴趣和最享受的任务。
区别在于,Symphony 可以处理大量的常规实现工作。这使得工程师可以一次专注于一个困难的问题,而不是在较小的任务之间不断上下文切换。
我们还了解到,将智能体视为状态机中的刚性节点效果并不好。模型会变得更聪明,能够解决比我们试图将它们装入的盒子更大的问题。我们早期版本的智能体工作只要求 Codex 实现任务。事实证明这种方法过于局限。Codex 完全能够创建多个 PR,以及阅读审查反馈并处理它。因此,我们给了它工具——gh 命令行界面、读取 CI 日志的技能等——现在我们可以要求 Codex 做更多事情,比如关闭旧的 PR 或拉取已完成和已放弃工作的报告。这些类型的任务远远超出了最初的功能实现范围。
因此,我们最终转向给智能体设定目标,而不是严格的转换规则,这就像一位好经理给团队中的直接下属分配目标一样。模型的力量来自于它们推理的能力,所以给它们工具和上下文,让它们去发挥吧。
当你打开 Symphony 仓库(在新窗口中打开)时,你首先会注意到的是,Symphony 实际上只是一个 SPEC.md 文件——定义了问题和预期的解决方案。我们没有构建一个复杂的监督系统,而是定义了问题和预期解决方案,为智能体提供高层次的方向指导。
# Symphony 服务规范
状态:草案 v1(语言无关)
目的:定义一种编排编码智能体以完成项目工作的服务。
## 1. 问题陈述
Symphony 是一个长期运行的自动化服务,它持续从问题追踪器(本规范版本中为 Linear)读取工作,为每个问题创建一个隔离的工作空间,并在该工作空间内为该问题运行一个编码智能体会话。
该服务解决了四个操作性问题:
- 它将问题执行变成可重复的守护进程工作流,而不是手动脚本。
- 它隔离了每个工作空间中的智能体执行,以便智能体命令仅在每个问题的工作空间目录中运行。
- 它将工作流策略保留在仓库中(`WORKFLOW.md`),以便团队将智能体提示和运行时设置与他们的代码一起版本化。
- 它提供了足够的可观测性来操作和调试多个并发运行的智能体。
实施应明确记录其信任与安全立场。本规范不要求单一的审批、沙箱或操作员确认策略;某些实现可能针对高度信任的环境采用高信任度配置,而其他实现可能需要更严格的审批或沙箱。
重要边界:
- Symphony 是一个调度器/运行器和追踪器读取器。
- 工单写入(状态转换、评论、PR 链接)通常由编码智能体使用工作流/运行时环境中的工具执行。
- 一次成功的运行可能结束于工作流定义的交接状态(例如“人工审查”),而不一定是“完成”。
## 2. 目标与非目标
### 2.1 目标
- 按固定节奏轮询问题追踪器,并在有界并发度内分发工作。
- 维护一个单一的权威编排器状态,用于分发、重试和协调。
- 为每个问题创建确定性的工作空间,并在多次运行之间保留它们。
- 当问题状态变化导致它们不符合条件时,停止正在运行的会话。
- 使用指数退避从瞬态故障中恢复。
- 从仓库拥有的 `WORKFLOW.md` 契约中加载运行时行为。
- 提供操作员可见的可观测性(至少是结构化日志)。
- 支持无需持久化数据库的重新启动恢复。
### 2.2 非目标
- 丰富的网页界面或多租户控制平面。
- 指定特定的仪表盘或终端界面实现。
- 通用工作流引擎或分布式任务调度器。
- 内置的关于如何编辑工单、PR 或评论的业务逻辑。(该逻辑存在于工作流提示和智能体工具中。)
- 强制要求超出编码智能体和主机操作系统提供的强沙箱控制。
- 为所有实现强制要求单一的默认审批、沙箱或操作员确认立场。
## 3. 系统概述
### 3.1 主要组件
1. `工作流加载器`
- 读取 `WORKFLOW.md`。
- 解析 YAML 前置元数据和提示正文。
- 返回 `{config, prompt_template}`。
2. `配置层`
- 为工作流配置值提供类型化 getter。
- 应用默认值和环境变量间接引用。
- 在分发前执行由编排器使用的验证。
3. `问题追踪器客户端`
- 获取处于活跃状态下的候选问题。
- 获取特定问题 ID 的当前状态(协调)。
- 在启动清理时获取终端状态问题。
- 将追踪器负载标准化为稳定的问题模型。
4. `编排器`
- 拥有轮询滴答。
- 拥有内存中的运行时状态。
- 决定哪些问题要分发、重试、停止或释放。
- 跟踪会话指标和重试队列状态。
5. `工作空间管理器`
- 将问题标识符映射到工作空间路径。
- 确保每个问题的工作空间目录存在。
- 运行工作空间生命周期钩子。
- 清理终端问题的工作空间。
6. `智能体运行器`
- 创建工作空间。
- 从问题 + 工作流模板构建提示。
- 启动编码智能体应用服务器客户端。
- 将智能体更新流式传输回编排器。
7. `状态界面`(可选)
- 呈现人类可读的运行时状态(例如终端输出、仪表盘或其他面向操作员的视图)。
8. `日志`
- 将结构化的运行时日志发送到一个或多个配置的目标。
### 3.2 抽象层次
为了便于移植,Symphony 应保持在这些层次中:
1. `策略层`(仓库定义)
- `WORKFLOW.md` 提示正文。
- 团队特定的工单处理、验证和交接规则。
2. `配置层`(类型化 getter)
- 将前置元数据解析为类型化的运行时设置。
- 处理默认值、环境令牌和路径标准化。
3. `协调层`(编排器)
- 轮询循环、问题资格、并发、重试、协调。
4. `执行层`(工作空间 + 智能体子进程)
- 文件系统生命周期、工作空间准备、编码智能体协议。
5. `集成层`(Linear 适配器)
- API 调用和追踪器数据的标准化。
6. `可观测性层`(日志 + 可选状态界面)
- 操作员对编排器和智能体行为的可见性。
### 3.3 外部依赖
- 问题追踪器 API(本规范版本中为 `tracker.kind: linear` 的 Linear)。
- 用于工作空间和日志的本地文件系统。
- 可选的工作空间填充工具(例如 Git CLI,如果使用)。
- 支持通过 stdio 使用类似 JSON-RPC 的应用服务器模式的编码智能体可执行文件。
- 用于问题追踪器和编码智能体的主机环境身份验证。
## 4. 核心领域模型
### 4.1 实体
#### 4.1.1 问题
由编排、提示渲染和可观测性输出使用的标准化问题记录。
字段:
- `id`(字符串)
- 稳定的内部追踪器 ID。
- `identifier`(字符串)
- 人类可读的工单键(例如:`ABC-123`)。
- `title`(字符串)
- `description`(字符串或 null)
- `priority`(整数或 null)
- 在分发排序中,数字越小优先级越高。
- `state`(字符串)
- 当前追踪器状态名称。
- `branch_name`(字符串或 null)
- 如果可用,追踪器提供的分支元数据。
- `url`(字符串或 null)
- `labels`(字符串列表)
- 标准化为小写。
- `blocked_by`(阻塞引用列表)
- 每个阻塞引用包含:
- `id`(字符串或 null)
- `identifier`(字符串或 null)
- `state`(字符串或 null)
- `created_at`(时间戳或 null)
- `updated_at`(时间戳或 null)
#### 4.1.2 工作流定义
解析后的 `WORKFLOW.md` 负载:
- `config`(映射)
- YAML 前置元数据根对象。
- `prompt_template`(字符串)
- 前置元数据之后的 Markdown 正文,已修剪。
#### 4.1.3 服务配置(类型化视图)
从 `WorkflowDefinition.config` 派生的类型化运行时值,加上环境解析。
示例:
- 轮询间隔
- 工作空间根目录
- 活跃和终端问题状态
- 并发限制
- 编码智能体可执行文件/参数/超时
- 工作空间钩子
#### 4.1.4 工作空间
分配给一个问题标识符的文件系统工作空间。
字段(逻辑):
- `path`(工作空间路径;当前运行时通常使用绝对路径,但如果配置为没有路径分隔符,则可以使用相对根目录)
- `workspace_key`(经过消毒的问题标识符)
- `created_now`(布尔值,用于控制 `after_create` 钩子)
#### 4.1.5 运行尝试
一个问题的一次执行尝试。
字段(逻辑):
- `issue_id`
- `issue_identifier`
- `attempt`(整数或 null,首次运行为 `null`,重试/继续为 `>=1`)
- `workspace_path`
- `started_at`
- `status`
- `error`(可选)
#### 4.1.6 实时会话(智能体会话元数据)
在编码智能体子进程运行时跟踪的状态。
字段:
- `session_id`(字符串,`<thread_id>-<turn_id>`)
- `thread_id`(字符串)
- `turn_id`(字符串)
- `codex_app_server_pid`(字符串或 null)
- `last_codex_event`(字符串/枚举或 null)
- `last_codex_timestamp`(时间戳或 null)
- `last_codex_message`(汇总负载)
- `codex_input_tokens`(整数)
- `codex_output_tokens`(整数)
- `codex_total_tokens`(整数)
- `last_reported_input_tokens`(整数)
- `last_reported_output_tokens`(整数)
- `last_reported_total_tokens`(整数)
- `turn_count`(整数)
- 在当前工作线程生命周期内开始的编码智能体轮次数。
#### 4.1.7 重试条目
问题的计划重试状态。
字段:
- `issue_id`
- `identifier`(尽力而为的人类可读 ID,用于状态界面/日志)
- `attempt`(整数,重试队列中从1开始)
- `due_at_ms`(单调时钟时间戳)
- `timer_handle`(运行时特定的定时器引用)
- `error`(字符串或 null)
#### 4.1.8 编排器运行时状态
编排器拥有的单一权威内存状态。
字段:
- `poll_interval_ms`(当前有效的轮询间隔)
- `max_concurrent_agents`(当前有效的全局并发限制)
- `running`(映射 `issue_id -> running entry`)
- `claimed`(已保留/运行/重试的问题 ID 集合)
- `retry_attempts`(映射 `issue_id -> RetryEntry`)
- `completed`(问题 ID 集合;仅用于记账,不用于分发门控)
- `codex_totals`(聚合令牌 + 运行时秒数)
- `codex_rate_limits`(来自智能体事件的最新速率限制快照)
### 4.2 稳定标识符和标准化规则
- `Issue ID`
- 用于追踪器查找和内部映射键。
- `Issue Identifier`
- 用于人类可读的日志和工作空间命名。
- `Workspace Key`
- 通过将 `issue.identifier` 中不在 `[A-Za-z0-9._-]` 内的任何字符替换为 `_` 来派生。
- 使用消毒后的值作为工作空间目录名称。
- `Normalized Issue State`
- 比较状态时先转换为小写。
- `Session ID`
- 由编码智能体的 `thread_id` 和 `turn_id` 组成,格式为 `<thread_id>-<turn_id>`。
## 5. 工作流规范(仓库契约)
### 5.1 文件发现和路径解析
工作流文件路径优先级:
1. 显式应用程序/运行时设置(由 CLI 启动路径设置)。
2. 默认值:当前进程工作目录中的 `WORKFLOW.md`。
加载器行为:
- 如果无法读取文件,返回 `missing_workflow_file` 错误。
- 工作流文件应由仓库拥有并受版本控制。
### 5.2 文件格式
`WORKFLOW.md` 是一个带有可选 YAML 前置元数据的 Markdown 文件。
设计说明:
- `WORKFLOW.md` 应足够自包含,以描述和运行不同的工作流(提示、运行时设置、钩子和追踪器选择/配置),而无需带外服务特定配置。
解析规则:
- 如果文件以 `---` 开头,则解析直到下一个 `---` 的行为 YAML 前置元数据。
- 剩余行成为提示正文。
- 如果缺少前置元数据,则将整个文件视为提示正文,并使用空配置映射。
- YAML 前置元数据必须解码为映射/对象;非映射的 YAML 是错误的。
- 提示正文在使用前被修剪。
返回的工作流对象:
- `config`:前置元数据根对象(不嵌套在 `config` 键下)。
- `prompt_template`:修剪后的 Markdown 正文。
### 5.3 前置元数据模式
顶层键:
- `tracker`
- `polling`
- `workspace`
- `hooks`
- `agent`
- `codex`
为了向前兼容,应忽略未知键。
注意:
- 工作流前置元数据是可扩展的。可选扩展可以定义额外的顶层键(例如 `server`),而无需更改上述核心模式。
- 扩展应记录其字段模式、默认值、验证规则以及更改是动态应用还是需要重新启动。
- 常见扩展:`server.port`(整数)启用第13.7节中描述的可选 HTTP 服务器。
#### 5.3.1 `tracker`(对象)
字段:
- `kind`(字符串)
- 分发必需。
- 当前支持的值:`linear`
- `endpoint`(字符串)
- 当 `tracker.kind == "linear"` 时的默认值:`https://api.linear.app/graphql`
- `api_key`(字符串)
- 可以是字面令牌或 `$VAR_NAME`。
- 当 `tracker.kind == "linear"` 时的规范环境变量:`LINEAR_API_KEY`。
- 如果 `$VAR_NAME` 解析为空字符串,则将键视为缺失。
- `project_slug`(字符串)
- 当 `tracker.kind == "linear"` 时分发必需。
- `active_states`(字符串列表)
- 默认值:`Todo`, `In Progress`
- `terminal_states`(字符串列表)
- 默认值:`Closed`, `Cancelled`, `Canceled`, `Duplicate`, `Done`
#### 5.3.2 `polling`(对象)
字段:
- `interval_ms`(整数或字符串整数)
- 默认值:`30000`
- 更改应在运行时重新应用,并影响未来的滴答调度,无需重新启动。
#### 5.3.3 `workspace`(对象)
字段:
- `root`(路径字符串或 `$VAR`)
- 默认值:`<system-temp>/symphony_workspaces`
- `~` 和包含路径分隔符的字符串会被展开。
- 没有路径分隔符的裸字符串原样保留(允许相对根目录,但不鼓励)。
#### 5.3.4 `hooks`(对象)
字段:
- `after_create`(多行 shell 脚本字符串,可选)
- 仅在工作空间目录新创建时运行。
- 失败会中止工作空间创建。
- `before_run`(多行 shell 脚本字符串,可选)
- 在每次智能体尝试之前,工作空间准备之后、启动编码智能体之前运行。
- 失败会中止当前尝试。
- `after_run`(多行 shell 脚本字符串,可选)
- 在每次智能体尝试之后(成功、失败、超时或取消),只要工作空间存在,就运行。
- 失败会被记录但忽略。
- `before_remove`(多行 shell 脚本字符串,可选)
- 在工作空间目录存在的情况下,于删除之前运行。
- 失败会被记录但忽略;清理仍会继续。
- `timeout_ms`(整数,可选)
- 默认值:`60000`
- 应用于所有工作空间钩子。
- 非正值应视为无效并回退到默认值。
- 更改应在运行时重新应用,以影响未来的钩子执行。
#### 5.3.5 `agent`(对象)
字段:
- `max_concurrent_agents`(整数或字符串整数)
- 默认值:`10`
- 更改应在运行时重新应用,并影响后续的分发决策。
- `max_retry_backoff_ms`(整数或字符串整数)
- 默认值:`300000`(5分钟)
- 更改应在运行时重新应用,并影响未来的重试调度。
- `max_concurrent_agents_by_state`(映射 `state_name -> positive integer`)
- 默认值:空映射。
- 状态键在查找时标准化(小写)。
- 无效条目(非正数或非数字)将被忽略。
#### 5.3.6 `codex`(对象)
字段:
对于 Codex 拥有的配置值,例如 `approval_policy`、`thread_sandbox` 和 `turn_sandbox_policy`,支持的值由目标 Codex 应用服务器版本定义。实现者应将它们视为透传的 Codex 配置值,而不是依赖本规范中手动维护的枚举。要检查已安装的 Codex 模式,运行 `codex app-server generate-json-schema --out <dir>` 并检查 `v2/ThreadStartParams.json` 和 `v2/TurnStartParams.json` 引用的相关定义。如果实现想要更严格的启动检查,可以在本地验证这些字段。
- `command`(字符串 shell 命令)
- 默认值:`codex app-server`
- 运行时通过 `bash -lc` 在工作空间目录中启动此命令。
- 启动的进程必须通过 stdio 说兼容的应用服务器协议。
- `approval_policy`(Codex `AskForApproval` 值)
- 默认值:实现定义。
- `thread_sandbox`(Codex `SandboxMode` 值)
- 默认值:实现定义。
- `turn_sandbox_policy`(Codex `SandboxPolicy` 值)
- 默认值:实现定义。
- `turn_timeout_ms`(整数)
- 默认值:`3600000`(1小时)
- `read_timeout_ms`(整数)
- 默认值:`5000`
- `stall_timeout_ms`(整数)
- 默认值:`300000`(5分钟)
- 如果 `<= 0`,则禁用停滞检测。
### 5.4 提示模板契约
`WORKFLOW.md` 的 Markdown 正文是每个问题的提示模板。
渲染要求:
- 使用严格的模板引擎(兼容 Liquid 的语义足够)。
- 未知变量必须使渲染失败。
- 未知过滤器必须使渲染失败。
模板输入变量:
- `issue`(对象)
- 包括所有标准化的问题字段,包括标签和阻止者。
- `attempt`(整数或 null)
- 首次尝试时为 `null`/absent。
- 重试或继续运行时为整数。
回退提示行为:
- 如果工作流提示正文为空,运行时可以使用一个最小的默认提示("You are working on an issue from Linear.")。
- 工作流文件读取/解析失败是配置/验证错误,不应静默回退到提示。
### 5.5 工作流验证和错误表面
错误类别:
- `missing_workflow_file`
- `workflow_parse_error`
- `workflow_front_matter_not_a_map`
- `template_parse_error`(提示渲染期间)
- `template_render_error`(未知变量/过滤器,无效插值)
分发门控行为:
- 工作流文件读取/YAML 错误会阻止新的分发,直到修复。
- 模板错误仅使受影响的运行尝试失败。
## 6. 配置规范
### 6.1 来源优先级和解析语义
配置优先级:
1. 工作流文件路径选择(运行时设置 -> cwd 默认值)。
2. YAML 前置元数据值。
3. 通过选定 YAML 值内的 `$VAR_NAME` 进行环境间接引用。
4. 内置默认值。
值强制语义:
- 路径/命令字段支持:
- `~` 主目录扩展
- `$VAR` 扩展用于环境支持的路径值
- 仅对旨在成为本地文件系统路径的值应用扩展;不要重写 URI 或任意 shell 命令字符串。
### 6.2 动态重载语义
需要动态重载:
- 软件应监视 `WORKFLOW.md` 的变化。
- 变化发生时,应重新读取并重新应用工作流配置和提示模板,无需重新启动。
- 软件应尝试将实时行为调整为新的配置(例如轮询节奏、并发限制、活跃/终端状态、codex 设置、工作空间路径/钩子以及未来运行的提示内容)。
- 重新加载的配置适用于未来的分发、重试调度、协调决策、钩子执行和智能体启动。
- 实现不需要在配置更改时自动重启进行中的智能体会话。
- 管理自己监听器/资源的扩展(例如 HTTP 服务器端口更改)可能需要重启,除非实现明确支持实时重新绑定。
- 实现还应在运行时操作期间(例如分发前)防御性地重新验证/重新加载,以防文件系统监视事件丢失。
- 无效的重载不应使服务崩溃;应继续使用最后已知的良好有效配置运行,并发出操作员可见的错误。
### 6.3 分发前检查验证
此验证是在尝试分发新工作之前的调度器前检查。它验证轮询和启动工作线程所需的工作流/配置,而不是对所有可能的工作流行为进行全面审计。
启动验证:
- 在启动调度循环之前验证配置。
- 如果启动验证失败,则失败启动并发出操作员可见的错误。
每次滴答的分发验证:
- 在每个分发周期之前重新验证。
- 如果验证失败,则跳过该滴答的分发,保持协调活动,并发出操作员可见的错误。
验证检查:
- 工作流文件可以加载和解析。
- `tracker.kind` 存在且受支持。
- `tracker.api_key` 在 `$` 解析后存在。
- `tracker.project_slug` 在所选追踪器类型需要时存在。
- `codex.command` 存在且非空。
### 6.4 配置字段摘要(速查表)
本节有意重复,以便编码智能体可以快速实现配置层。
- `tracker.kind`:字符串,必需,当前为 `linear`
- `tracker.endpoint`:字符串,当 `tracker.kind=linear` 时默认 `https://api.linear.app/graphql`
- `tracker.api_key`:字符串或 `$VAR`,当 `tracker.kind=linear` 时规范 env `LINEAR_API_KEY`
- `tracker.project_slug`:字符串,当 `tracker.kind=linear` 时必需
- `tracker.active_states`:字符串列表,默认 `["Todo", "In Progress"]`
- `tracker.terminal_states`:字符串列表,默认 `["Closed", "Cancelled", "Canceled", "Duplicate", "Done"]`
- `polling.interval_ms`:整数,默认 `30000`
- `workspace.root`:路径,默认 `<system-temp>/symphony_workspaces`
- `worker.ssh_hosts`(扩展):SSH 主机字符串列表,可选;省略时工作在本地运行
- `worker.max_concurrent_agents_per_host`(扩展):正整数,可选;跨配置的 SSH 主机应用的共享每主机上限
- `hooks.after_create`:shell 脚本或 null
- `hooks.before_run`:shell 脚本或 null
- `hooks.after_run`:shell 脚本或 null
- `hooks.before_remove`:shell 脚本或 null
- `hooks.timeout_ms`:整数,默认 `60000`
- `agent.max_concurrent_agents`:整数,默认 `10`
- `agent.max_turns`:整数,默认 `20`
- `agent.max_retry_backoff_ms`:整数,默认 `300000`(5分钟)
- `agent.max_concurrent_agents_by_state`:正整数映射,默认 `{}`
- `codex.command`:shell 命令字符串,默认 `codex app-server`
- `codex.approval_policy`:Codex `AskForApproval` 值,默认实现定义
- `codex.thread_sandbox`:Codex `SandboxMode` 值,默认实现定义
- `codex.turn_sandbox_policy`:Codex `SandboxPolicy` 值,默认实现定义
- `codex.turn_timeout_ms`:整数,默认 `3600000`
- `codex.read_timeout_ms`:整数,默认 `5000`
- `codex.stall_timeout_ms`:整数,默认 `300000`
- `server.port`(扩展):整数,可选;启用可选的 HTTP 服务器,`0` 可用于临时本地绑定,CLI `--port` 覆盖它
## 7. 编排状态机
编排器是唯一改变调度状态的组件。所有工作线程的结果都会报告给它,并转换为显式的状态转换。
### 7.1 问题编排状态
这与追踪器状态(`Todo`、`In Progress` 等)不同。这是服务的内部声明状态。
1. `Unclaimed`
- 问题未运行且没有计划重试。
2. `Claimed`
- 编排器已保留该问题以防止重复分发。
- 实际上,已声明的问题要么处于 `Running` 状态,要么处于 `RetryQueued` 状态。
3. `Running`
- 存在工作线程任务,并且该问题在 `running` 映射中。
4. `RetryQueued`
- 工作线程未运行,但在 `retry_attempts` 中存在重试定时器。
5. `Released`
- 声明被移除,因为问题是终端状态、非活跃状态、缺失,或者重试路径完成后没有重新分发。
重要细微差别:
- 成功的工作线程退出并不意味着问题永远完成。
- 工作线程可能会在退出前通过多个连续的编码智能体轮次继续。
- 在每个正常轮次完成后,工作线程重新检查追踪器问题状态。
- 如果问题仍处于活跃状态,工作线程应在同一工作空间内的同一个实时编码智能体线程上开始另一个轮次,最多到 `agent.max_turns`。
- 第一个轮次应使用完整的渲染任务提示。
- 后续轮次应仅向现有线程发送继续指导,而不是重新发送线程历史中已有的原始任务提示。
- 一旦工作线程正常退出,编排器仍会调度一个短暂的重试(约1秒),以便重新检查问题是否仍然活跃并需要另一个工作线程会话。
### 7.2 运行尝试生命周期
一次运行尝试经历以下阶段:
1. `PreparingWorkspace`
2. `BuildingPrompt`
3. `LaunchingAgentProcess`
4. `InitializingSession`
5. `StreamingTurn`
6. `Finishing`
7. `Succeeded`
8. `Failed`
9. `TimedOut`
10. `Stalled`
11. `CanceledByReconciliation`
不同的终端原因很重要,因为重试逻辑和日志不同。
### 7.3 转换触发器
- `Poll Tick`
- 协调正在运行的活动。
- 验证配置。
- 获取候选问题。
- 分发直到槽位耗尽。
- `Worker Exit (normal)`
- 移除运行条目。
- 更新聚合运行时总数。
- 在工作线程耗尽或完成其进程内轮次循环后,调度继续重试(尝试 `1`)。
- `Worker Exit (abnormal)`
- 移除运行条目。
- 更新聚合运行时总数。
- 调度指数退避重试。
- `Codex Update Event`
- 更新实时会话字段、令牌计数器和速率限制。
- `Retry Timer Fired`
- 重新获取活跃候选并尝试重新分发,或者如果不再符合条件则释放声明。
- `Reconciliation State Refresh`
- 停止其问题状态为终端或不再活跃的正在运行的任务。
- `Stall Timeout`
- 杀死工作线程并调度重试。
### 7.4 幂等性和恢复规则
- 编排器通过一个权威序列化状态突变,以避免重复分发。
- 在启动任何工作线程之前,需要检查 `claimed` 和 `running`。
- 每次滴答时,协调在分发之前运行。
- 重新启动恢复由追踪器和文件系统驱动(不需要持久的编排器数据库)。
- 启动终端清理会移除已经处于终端状态的问题的陈旧工作空间。
## 8. 轮询、调度和协调
### 8.1 轮询循环
启动时,服务验证配置,执行启动清理,调度一个立即滴答,然后每 `polling.interval_ms` 重复一次。
当工作流配置更改重新应用时,有效轮询间隔应更新。
滴答顺序:
1. 协调正在运行的问题。
2. 运行分发前检查验证。
3. 使用活跃状态从追踪器获取候选问题。
4. 按分发优先级对问题排序。
5. 在有剩余槽位时分发符合条件的问题。
6. 通知可观测性/状态消费者状态变化。
如果每次滴答验证失败,该滴答的分发被跳过,但协调仍然先执行。
### 8.2 候选选择规则
只有满足所有条件的问题才有资格分发:
- 它具有 `id`、`identifier`、`title` 和 `state`。
- 其状态在 `active_states` 中且不在 `terminal_states` 中。
- 它尚未在 `running` 中。
- 它尚未在 `claimed` 中。
- 全局并发槽位可用。
- 每状态并发槽位可用。
- `Todo` 状态的阻塞规则通过:
- 如果问题状态为 `Todo`,当任何阻塞者处于非终端状态时,不分发。
排序顺序(稳定意图):
1. `priority` 升序(1..4 优先;null/未知排序最后)
2. `created_at` 最早优先
3. `identifier` 字典序平局决胜
### 8.3 并发控制
全局限制:
- `available_slots = max(max_concurrent_agents - running_count, 0)`
每状态限制:
- `max_concurrent_agents_by_state[state]`(如果存在,状态键标准化)
- 否则回退到全局限制
运行时通过 `running` 映射中当前跟踪的状态来计数问题。
可选的 SSH 主机限制:
- 当设置了 `worker.max_concurrent_agents_per_host` 时,每个配置的 SSH 主机最多同时运行这么多智能体。
- 达到上限的主机将被跳过新的分发,直到容量释放。
### 8.4 重试和退避
重试条目创建:
- 取消同一问题的任何现有重试定时器。
- 存储 `attempt`、`identifier`、`error`、`due_at_ms` 和新定时器句柄。
退避公式:
- 正常工作线程退出后的继续重试使用固定的短延迟 `1000` 毫秒。
- 失败驱动的重试使用 `delay = min(10000 * 2^(attempt - 1), agent.max_retry_backoff_ms)`。
- 指数上限由配置的最大重试退避限制(默认 `300000` / 5分钟)。
重试处理行为:
1. 获取活跃候选问题(不是所有问题)。
2. 通过 `issue_id` 找到特定问题。
3. 如果未找到,释放声明。
4. 如果找到且仍符合候选条件:
- 如果槽位可用,则分发。
- 否则重新入队,错误为 `no available orchestrator slots`。
5. 如果找到但不再活跃,释放声明。
注意:
- 终端状态工作空间清理由启动清理和正在运行的任务协调处理(包括当前正在运行的问题的终端转换)。
- 重试处理主要对活跃候选进行操作,并在问题不存在时释放声明,而不是自行执行终端清理。
### 8.5 正在运行的任务协调
协调每个滴答运行一次,有两个部分。
A部分:停滞检测
- 对于每个正在运行的问题,计算自以下时间以来的 `elapsed_ms`:
- 如果已看到任何事件,则为 `last_codex_timestamp`,否则
- `started_at`
- 如果 `elapsed_ms > codex.stall_timeout_ms`,则终止工作线程并排队重试。
- 如果 `stall_timeout_ms <= 0`,完全跳过停滞检测。
B部分:追踪器状态刷新
- 获取所有正在运行的问题 ID 的当前问题状态。
- 对于每个正在运行的问题:
- 如果追踪器状态是终端:终止工作线程并清理工作空间。
- 如果追踪器状态仍然活跃:更新内存中的问题快照。
- 如果追踪器状态既不是活跃也不是终端:终止工作线程但不清理工作空间。
- 如果状态刷新失败,保持工作线程运行,并在下一次滴答时重试。
### 8.6 启动终端工作空间清理
服务启动时:
1. 查询追踪器中处于终端状态的问题。
2. 对于每个返回的问题标识符,删除相应的工作空间目录。
3. 如果终端问题获取失败,记录警告并继续启动。
这可以防止重启后陈旧的终端工作空间累积。
## 9. 工作空间管理和安全
### 9.1 工作空间布局
工作空间根目录:
- `workspace.root`(标准化路径;当前配置层扩展类似路径的值并保留裸相对名称)
每个问题的工作空间路径:
- `<workspace.root>/<sanitized_issue_identifier>`
工作空间持久性:
- 工作空间在同一问题的多次运行之间重复使用。
- 成功的运行不会自动删除工作空间。
### 9.2 工作空间创建和重用
输入:`issue.identifier`
算法摘要:
1. 将标识符消毒为 `workspace_key`。
2. 在工作空间根目录下计算工作空间路径。
3. 确保工作空间路径作为目录存在。
4. 仅当在此调用期间创建了目录时,标记 `created_now=true`;否则 `created_now=false`。
5. 如果 `created_now=true`,如果配置了 `after_create` 钩子,则运行它。
注意:
- 本节不假定任何特定的仓库/VCS 工作流。
- 超出目录创建的工作空间准备(例如依赖引导、检出/同步、代码生成)由实现定义,通常通过钩子处理。
### 9.3 可选的工作空间填充(实现定义)
本规范不要求任何内置的 VCS 或仓库引导行为。
实现可以使用实现定义的逻辑和/或钩子(例如 `after_create` 和/或 `before_run`)来填充或同步工作空间。
失败处理:
- 工作空间填充/同步失败会为当前尝试返回错误。
- 如果在创建全新工作空间时发生故障,实现可以移除部分准备的目录。
- 除非明确选择并记录了该策略,否则在填充失败时不应破坏性重置重用中的工作空间。
### 9.4 工作空间钩子
支持的钩子:
- `hooks.after_create`
- `hooks.before_run`
- `hooks.after_run`
- `hooks.before_remove`
执行契约:
- 在与主机操作系统相适应的本地 shell 上下文中执行,工作空间目录为 `cwd`。
- 在 POSIX 系统上,`sh -lc <script>`(或更严格的等价物如 `bash -lc <script>`)是合规的默认值。
- 钩子超时使用 `hooks.timeout_ms`;默认值:`60000 ms`。
- 记录钩子开始、失败和超时。
失败语义:
- `after_create` 失败或超时对工作空间创建是致命的。
- `before_run` 失败或超时对当前运行尝试是致命的。
- `after_run` 失败或超时被记录并忽略。
- `before_remove` 失败或超时被记录并忽略。
### 9.5 安全不变量
这是最重要的可移植性约束。
不变量1:仅在每个问题的工作空间路径中运行编码智能体。
- 在启动编码智能体子进程之前,验证:
- `cwd == workspace_path`
不变量2:工作空间路径必须保持在工作空间根目录内。
- 将两个路径标准化为绝对路径。
- 要求 `workspace_path` 以 `workspace_root` 作为前缀目录。
- 拒绝工作空间根目录之外的任何路径。
不变量3:工作空间键已消毒。
- 工作空间目录名称中只允许 `[A-Za-z0-9._-]`。
- 将所有其他字符替换为 `_`。
## 10. 智能体运行器协议(编码智能体集成)
本节定义了集成编码智能体应用服务器的语言无关契约。
兼容性简介:
- 规范性契约是消息顺序、所需行为以及必须提取的逻辑字段(例如会话 ID、完成状态、审批处理和用量/速率限制遥测)。
- 准确的 JSON 字段名称在不同兼容的应用服务器版本中可能略有不同。
- 当有效负载具有相同逻辑含义时,实现应容忍等效的有效负载形状,特别是对于嵌套 ID、审批请求、需要用户输入的信号以及令牌/速率限制元数据。
### 10.1 启动契约
子进程启动参数:
- 命令:`codex.command`
- 调用:`bash -lc <codex.command>`
- 工作目录:工作空间路径
- 标准输出/标准错误:独立的流
- 帧结构:标准输出上的行分隔协议消息(每行一个类似 JSON-RPC 的 JSON)
注意:
- 默认命令是 `codex app-server`。
- 审批策略、cwd 和提示在 10.2 节的协议消息中表达。
推荐的额外进程设置:
- 最大行大小:10 MB(用于安全缓冲)
### 10.2 会话启动握手
参考:https://developers.openai.com/codex/app-server/
客户端必须按顺序发送以下协议消息:
说明性启动记录(如果保留相同语义,等效的有效负载形状也可以接受):
```json
{"id":1,"method":"initialize","params":{"clientInfo":{"name":"symphony","version":"1.0"},"capabilities":{}}}
{"method":"initialized","params":{}}
{"id":2,"method":"thread/start","params":{"approvalPolicy":"<implementation-defined>","sandbox":"<implementation-defined>","cwd":"/abs/workspace"}}
{"id":3,"method":"turn/start","params":{"threadId":"<thread-id>","input":[{"type":"text","text":"<rendered prompt-or-continuation-guidance>"}],"cwd":"/abs/workspace","title":"ABC-123: Example","approvalPolicy":"<implementation-defined>","sandboxPolicy":{"type":"<implementation-defined>"}}}
```
1. `initialize` 请求
- 参数包括:
- `clientInfo` 对象(例如 `{name, version}`)
- `capabilities` 对象(可以为空)
- 如果目标 Codex 应用服务器需要能力协商来支持动态工具,请在此处包含必要的能力标志。
- 等待响应(`read_timeout_ms`)
2. `initialized` 通知
3. `thread/start` 请求
- 参数包括:
- `approvalPolicy` = 实现定义的会话审批策略值
- `sandbox` = 实现定义的会话沙箱值
- `cwd` = 绝对工作空间路径
- 如果实现了可选的客户端工具,请使用目标 Codex 应用服务器版本支持的协议机制包含它们通告的工具规范。
4. `turn/start` 请求
- 参数包括:
- `threadId`
- `input` = 单个文本项,包含第一个轮次的渲染提示,或同一线程后续轮次的继续指导
- `cwd`
- `title` = `<issue.identifier>: <issue.title>`
- `approvalPolicy` = 实现定义的轮次审批策略值
- `sandboxPolicy` = 实现定义的对象形式沙箱策略负载(当目标应用服务器版本要求时)
会话标识符:
- 从 `thread/start` 结果 `result.thread.id` 读取 `thread_id`
- 从每个 `turn/start` 结果 `result.turn.id` 读取 `turn_id`
- 发出 `session_id = "<thread_id>-<turn_id>"`
- 在一个工作线程运行内,对所有后续轮次重用相同的 `thread_id`
### 10.3 流式轮次处理
客户端读取行分隔的消息,直到轮次终止。
完成条件:
- `turn/completed` -> 成功
- `turn/failed` -> 失败
- `turn/cancelled` -> 失败
- 轮次超时(`turn_timeout_ms`)-> 失败
- 子进程退出 -> 失败
继续处理:
- 如果工作线程在成功轮次后决定继续,它应针对同一个实时 `threadId` 发出另一个 `turn/start`。
- 应用服务器子进程应在这些继续轮次期间保持存活,并且仅在工作线程运行结束时停止。
行处理要求:
- 仅从标准输出读取协议消息。
- 缓冲部分标准输出行,直到收到换行符。
- 尝试解析完整标准输出行的 JSON。
- 标准错误不是协议流的一部分:
- 忽略它或将其记录为诊断信息
- 不要尝试在标准错误上解析协议 JSON
### 10.4 发出的运行时事件(上游到编排器)
应用服务器客户端向编排器回调发出结构化事件。每个事件应包括:
- `event`(枚举/字符串)
- `timestamp`(UTC 时间戳)
- `codex_app_server_pid`(如果可用)
- 可选的 `usage` 映射(令牌计数)
- 根据需要携带有效负载字段
可能发出的重要事件包括:
- `session_started`
- `startup_failed`
- `turn_completed`
- `turn_failed`
- `turn_cancelled`
- `turn_ended_with_error`
- `turn_input_required`
- `approval_auto_approved`
- `unsupported_tool_call`
- `notification`
- `other_message`
- `malformed`
### 10.5 审批、工具调用和用户输入策略
审批、沙箱和用户输入行为由实现定义。
策略要求:
- 每个实现应记录其选择的审批、沙箱和操作员确认立场。
- 审批请求和需要用户输入的事件不得导致运行无限期停滞。实现应根据其记录的策略满足它们、将其呈现给操作员、自动解决它们或使运行失败。
示例高信任行为:
- 自动批准会话的命令执行审批。
- 自动批准会话的文件更改审批。
- 将需要用户输入的轮次视为硬性失败。
不支持的动态工具调用:
- 运行时显式实现和通告的支持的动态工具调用应根据其扩展契约处理。
- 如果智能体请求不支持的动态工具调用(`item/tool/call`),返回工具失败响应并继续会话。
- 这可以防止会话在不支持的工具执行路径上停滞。
可选的客户端工具扩展:
- 实现可以向应用服务器会话暴露一组有限的客户端工具。
- 当前可选标准化工具:`linear_graphql`。
- 如果实现了,应在启动期间使用目标 Codex 应用服务器版本支持的协议机制向应用服务器会话通告支持的工具。
- 不支持的工具名称仍应返回失败结果并继续会话。
`linear_graphql` 扩展契约:
- 目的:使用当前会话的 Symphony 配置的追踪器身份验证,针对 Linear 执行原始 GraphQL 查询或变更。
- 可用性:仅当 `tracker.kind == "linear"` 且配置了有效的 Linear 身份验证时有意义。
- 首选输入形状:
```json
{
"query": "single GraphQL query or mutation document",
"variables": {
"optional": "graphql variables object"
}
}
```
- `query` 必须是非空字符串。
- `query` 必须恰好包含一个 GraphQL 操作。
- `variables` 是可选的,如果存在,必须是 JSON 对象。
- 实现还可以接受原始 GraphQL 查询字符串作为简写输入。
- 每次工具调用执行一个 GraphQL 操作。
- 如果提供的文档包含多个操作,则拒绝该工具调用视为无效输入。
- `operationName` 选择有意不在本扩展范围内。
- 重用来自活动 Symphony 工作流/运行时配置的已配置 Linear 端点和身份验证;不需要编码智能体从磁盘读取原始令牌。
- 工具结果语义:
- 传输成功 + 没有顶层 GraphQL `errors` -> `success=true`
- 存在顶层 GraphQL `errors` -> `success=false`,但保留 GraphQL 响应体用于调试
- 无效输入、缺少身份验证或传输失败 -> `success=false` 并带有错误负载
- 返回 GraphQL 响应或错误负载作为结构化工具输出,模型可以在会话中检查。
说明性响应(如果保留相同结果,等效有效负载形状也可以接受):
```json
{"id":"<approval-id>","result":{"approved":true}}
{"id":"<tool-call-id>","result":{"success":false,"error":"unsupported_tool_call"}}
```
用户输入要求上的硬性失败:
- 如果智能体请求用户输入,立即使运行尝试失败。
- 客户端通过以下方式检测:
- 显式方法(`item/tool/requestUserInput`),或
- 指示需要输入的轮次方法/标志。
### 10.6 超时和错误映射
超时:
- `codex.read_timeout_ms`:启动和同步请求期间的请求/响应超时
- `codex.turn_timeout_ms`:总轮次流超时
- `codex.stall_timeout_ms`:由编排器基于事件不活跃性强制执行
错误映射(推荐的标准化类别):
- `codex_not_found`
- `invalid_workspace_cwd`
- `response_timeout`
- `turn_timeout`
- `port_exit`
- `response_error`
- `turn_failed`
- `turn_cancelled`
- `turn_input_required`
### 10.7 智能体运行器契约
`Agent Runner` 包装了工作空间 + 提示 + 应用服务器客户端。
行为:
1. 为问题创建/重用工作空间。
2. 从工作流模板构建提示。
3. 启动应用服务器会话。
4. 将应用服务器事件转发给编排器。
5. 出现任何错误时,使工作线程尝试失败(编排器将重试)。
注意:
- 工作空间在成功运行后有意保留。
## 11. 问题追踪器集成契约(Linear 兼容)
### 11.1 必需的操作
实现必须支持以下追踪器适配器操作:
1. `fetch_candidate_issues()`
- 返回配置的项目中处于配置的活跃状态的问题。
2. `fetch_issues_by_states(state_names)`
- 用于启动终端清理。
3. `fetch_issue_states_by_ids(issue_ids)`
- 用于正在运行的任务协调。
### 11.2 查询语义(Linear)
`tracker.kind == "linear"` 的 Linear 特定要求:
- `tracker.kind == "linear"`
- GraphQL 端点(默认 `https://api.linear.app/graphql`)
- 身份验证令牌在 `Authorization` 头中发送
- `tracker.project_slug` 映射到 Linear 项目 `slugId`
- 候选问题查询使用 `project: { slugId: { eq: $projectSlug } }` 过滤项目
- 问题状态刷新查询使用 GraphQL 问题 ID,变量类型为 `[ID!]`
- 候选问题需要分页
- 默认页面大小:`50`
- 网络超时:`30000 ms`
重要:
- Linear GraphQL 模式可能会漂移。保持查询构造隔离,并测试本规范要求的精确查询字段/类型。
非 Linear 实现可以更改传输细节,但标准化输出必须与第4节中的领域模型匹配。
### 11.3 标准化规则
候选问题标准化应生成第4.1.1节中列出的字段。
额外的标准化细节:
- `labels` -> 小写字符串
- `blocked_by` -> 从关系类型为 `blocks` 的反向关系派生
- `priority` -> 仅限整数(非整数变为 null)
- `created_at` 和 `updated_at` -> 解析 ISO-8601 时间戳
### 11.4 错误处理契约
推荐的错误类别:
- `unsupported_tracker_kind`
- `missing_tracker_api_key`
- `missing_tracker_project_slug`
- `linear_api_request`(传输故障)
- `linear_api_status`(非200 HTTP)
- `linear_graphql_errors`
- `linear_unknown_payload`
- `linear_missing_end_cursor`(分页完整性错误)
追踪器错误上的编排器行为:
- 候选获取失败:记录并跳过该滴答的分发。
- 运行状态刷新失败:记录并保持活跃工作线程运行。
- 启动终端清理失败:记录警告并继续启动。
### 11.5 追踪器写入(重要边界)
Symphony 不需要编排器中的一级追踪器写入 API。
- 工单变更(状态转换、评论、PR 元数据)通常由编码智能体使用工作流提示定义的工具处理。
- 服务仍然是调度器/运行器和追踪器读取器。
- 工作流特定的成功通常意味着“达到了下一个交接状态”(例如 `Human Review`),而不是追踪器终端状态 `Done`。
- 如果实现了可选的 `linear_graphql` 客户端工具扩展,它仍然是智能体工具链的一部分,而不是编排器业务逻辑。
## 12. 提示构建和上下文组装
### 12.1 输入
提示渲染的输入:
- `workflow.prompt_template`
- 标准化的 `issue` 对象
- 可选的 `attempt` 整数(重试/继续元数据)
### 12.2 渲染规则
- 使用严格的变量检查渲染。
- 使用严格的过滤器检查渲染。
- 将问题对象键转换为字符串以实现模板兼容性。
- 保留嵌套的数组/映射(标签、阻止者),以便模板可以迭代。
### 12.3 重试/继续语义
`attempt` 应传递给模板,因为工作流提示可能为以下情况提供不同的指令:
- 首次运行(`attempt` 为 null 或 absent)
- 在成功的先前会话后的继续运行
- 错误/超时/停滞后的重试
### 12.4 失败语义
如果提示渲染失败:
- 立即使运行尝试失败。
- 让编排器像对待任何其他工作线程失败一样决定重试行为。
## 13. 日志、状态和可观测性
### 13.1 日志约定
问题相关日志的必需上下文字段:
- `issue_id`
- `issue_identifier`
编码智能体会话生命周期日志的必需上下文:
- `session_id`
消息格式化要求:
- 使用稳定的 `key=value` 短语。
- 包括操作结果(`completed`、`failed`、`retrying` 等)。
- 如果存在,包括简洁的失败原因。
- 除非必要,避免记录大型原始负载。
### 13.2 日志输出和目标
本规范不规定日志必须去向何处(标准错误、文件、远程目标等)。
要求:
- 操作员必须能够看到启动/验证/分发失败,而无需附加调试器。
- 实现可以写入一个或多个目标。
- 如果配置的日志目标失败,服务应在可能的情况下继续运行,并通过任何剩余目标发出操作员可见的警告。
### 13.3 运行时快照/监控接口(可选但推荐)
如果实现暴露同步运行时快照(用于仪表板或监控),它应返回:
- `running`(正在运行的会话行列表)
- 每行应包括 `turn_count`
- `retrying`(重试队列行列表)
- `codex_totals`
- `input_tokens`
- `output_tokens`
- `total_tokens`
- `seconds_running`(截至快照时间的聚合运行时秒数,包括活动会话)
- `rate_limits`(最新的编码智能体速率限制负载,如果可用)
推荐的快照错误模式:
- `timeout`
- `unavailable`
### 13.4 可选的人类可读状态界面
人类可读的状态界面(终端输出、仪表板等)是可选的,由实现定义。
如果存在,它应仅从编排器状态/指标中提取,并且不得要求用于正确性。
### 13.5 会话指标和令牌记账
令牌记账规则:
- 智能体事件可能包括多种有效负载形状中的令牌计数。
- 优先使用绝对线程总数,例如:
- `thread/tokenUsage/updated` 负载
- 令牌计数包装事件中的 `total_token_usage`
- 忽略仪表板/API 总计的增量式负载,例如 `last_token_usage`。
- 从选定负载中的常见字段名称宽松地提取输入/输出/总令牌计数。
- 对于绝对总数,跟踪相对于上次报告总数的增量,以避免重复计数。
- 不要将通用 `usage` 映射视为累积总数,除非事件类型将其定义为这样。
- 在编排器状态中累积聚合总数。
运行时记账:
- 运行时应在快照/渲染时间作为实时聚合报告。
- 实现可以维护已结束会话的累积计数器,并在生成快照/状态视图时添加从 `running` 条目派生的活动会话已用时间(例如 `started_at`)。
- 当会话结束时(正常退出或取消/终止),将运行持续时间秒数添加到累积的已结束会话运行时中。
- 不需要持续的后台滴答来累计运行时总数。
速率限制跟踪:
- 跟踪在任何智能体更新中看到的最新速率限制负载。
- 任何速率限制数据的人类可读呈现由实现定义。
### 13.6 人类可读的智能体事件摘要(可选)
原始智能体协议事件的人类可读摘要是可选的。
如果实现:
- 将它们视为仅用于可观测性的输出。
- 不要使编排器逻辑依赖于人类可读的字符串。
参考实现是用 Elixir 编写的——因为当代码实际上免费时,你终于可以根据语言的优势来选择,比如 Elixir 的并发性——但核心思想可以用一个简单的 Markdown 文档来表达。我们鼓励你把你最喜欢的编码智能体指向这个规范,让它实现自己的版本。
Symphony 的第一个版本只是一个在 tmux 中运行的 Codex 会话,轮询 Linear 并为新任务生成子智能体。它有效,但不太可靠。第二个版本存在于我们的主项目仓库中,这个仓库是为智能体设计的。我们已经构建了智能体赋能工程,为智能体提供在此仓库中高质量工作所需的技能和上下文,因此 Symphony 只是将它们连接起来。
一旦基本功能存在,我们就使用 Symphony 来构建 Symphony。
当我们内部演示该系统管理任务并附加其工作量证明视频时,反应非常积极:我们的 Symphony 项目频道增长,组织内的团队开始自然使用它。内部产品市场契合度是在 OpenAI 对外发布的前提条件。基于我们在 OpenAI 看到的使用情况,很明显我们应该在公司之外分享 Symphony。
因此,我们将这个想法提炼成一个独立的 SPEC.md,并要求 Codex 实现它。对于参考实现,我们选择了 Elixir,这是一种相对小众的语言,但具有编排和监督并发进程的出色原语。Codex 一次性构建了 Elixir 实现,我们在此基础上不断迭代规范和实现。为了完善规范,我们甚至要求 Codex 用其他几种语言——TypeScript、Go、Rust、Java、Python——实现它,并使用结果来识别歧义并简化系统。它在每种语言上都成功了。
通过构建 Symphony 的过程,我们移除了很多附带复杂性,比如对特定仓库或 Linear MCP 的依赖。Symphony 不再依赖我们的内部仓库或工作流。核心方法变得简单:
对于每个未完成的任务,保证有一个智能体在自己的工作空间中运行。
除了帮助进行活跃工作外,开发工作流现在也是智能体知道并遵循的。开发工作流——处理一个问题,检出仓库,将其设为“进行中”以便 PM 知道它正在被处理,添加 PR,将其移至“审查”状态,附加视频等——现在被捕获在一个简单的 WORKFLOW.md 文件中。所有这些过程都是人类遵循的,但从未被记录过。我们现在不再依赖这套隐式步骤,而是将其记录下来,Symphony 确保智能体遵循它。这让我们能够构建与我们并肩工作的智能体。如果我们决定智能体还应该为已完成的工作附加自我反思,我们会将其添加到 WORKFLOW.md 中,Symphony 会引导智能体执行该步骤。
我们还得以在应用服务器模式(在新窗口中打开)中使用 Codex,这是 Codex 的一个内置无头模式。这种模式让我们能够运行 Codex,并通过一个文档完善的 JSON-RPC API 以编程方式与之通信,比如启动线程或对轮次做出反应。这比试图通过 CLI 或实时 tmux 会话与 Codex 交互更方便、更可扩展。
Codex App Server 非常适合我们的用例:我们利用 Codex 提供的赋能工程,同时拥有可以插入的旋钮和钩子。例如,为了避免向子智能体暴露 Linear 访问令牌,我们使用动态工具调用(在新窗口中打开)来暴露原始的 linear_graphql 函数,该函数针对 Linear 执行任意请求,而不依赖 MCP 或将访问令牌暴露给容器。
Symphony 是一个有意保持极简的编排层。我们将其开源,以展示 Codex App Server 与不同工作流工具(如 Linear)结合时的威力。因此,我们不打算将 Symphony 维护为一个独立产品。把它看作一个参考实现。就像许多开发者将他们的编码智能体指向赋能工程博文来搭建他们的仓库一样,我们希望你将最喜欢的编码智能体指向 Symphony 规范(在新窗口中打开)和仓库(在新窗口中打开),构建根据你的环境定制的版本。
威力来自 Codex 及其应用服务器。Symphony 是将 Codex 连接到 Linear(我们已经在使用这两者)以解决工作管理问题的一种方式。随着编码智能体在推理和遵循指令方面变得更好,我们怀疑其他公司的瓶颈也将从编写代码转向管理智能体工作。令人兴奋的部分是,现在实验这些编码智能体系统的门槛非常低。你可以直接用 Codex 构建东西。
我们很高兴看到工程社区在发布后的几周内使用 Symphony,截至4月23日已获得超过 15K GitHub 星标。