ADR 0088 — Staff:代码就绪,自动触发处于休眠
- 状态: 已接受
- 日期: 2026-08-16
- 背景:
docs/fluxo.yml,块id: staff(camada_decisao_tecnica), 自 ADR 0085 起为status: planned - 直接先例: ADR 0053 (Dev Lead——独立对话型 agent,无区域), ADR 0068(无表的工件,在 event log 中版本化)
背景
自 ADR 0085 起,docs/fluxo.yml 将 Staff/Principal Engineer 声明为
status: planned:“合约就绪,启用已决定,等待触发器”。为它设想的触发器是 Anamnese 发现一个反复出现的系统性问题——但 Anamnese 处于暂停状态(ANAMNESE_ENABLED=false,2026-08-10 的产品决策),暂停期间不会提出任何 handoff。
产品负责人决定提前交付代码,即使知道自动触发器不会触发:Staff 已就绪并可手动启动(今天任何 agent 或用户都可以通过通用内部路由向它发起 handoff),只有自动触发仍待完成。这不是半成品实现——这与 ADR 0081 对容器生命周期所做的区分相同(表已就绪,编排器尚未):缺失的部分是被声明的,而不是被隐藏的。
决策
Staff 作为第五个独立对话型 agent 诞生(与 Criativo、PO、Arquiteto 和 Dev Lead 并列),完全沿用 Arquiteto 的模子——每个会话一个 GenServer、从 event log 重建(rehydration)、自己的有界工具使用循环(上限 14,与 Arquiteto/Dev Lead 同一量级:推理,而非轻量对话)——只有两处刻意的差异。
1. 没有 kickoff/1
所有其他对话型 lead 在全新启动时从会话的 event log 合成一条开场指令(各自的 kickoff_instruction/1)——总有之前的工件(product_brief、module_map、backlog)可以概括。Staff 没有这样的来源:让它浮现的系统性问题来自外部(Anamnese,当它存在时;或今天手动创建 handoff 的人),而不是来自其所在会话的 event log。没有 kickoff,Engine.Agents.StaffSupervisor.start_agent/2 启动进程(重建历史),然后它保持空闲,直到第一条 user_message——这就是发起 handoff 的人解释问题的方式。
apps/engine/lib/engine_web/controllers/agent_command_controller.ex 体现了这一点:“staff”的 start/2 子句从不调用 StaffServer.kickoff/1(该函数根本不存在),不同于 po/arquiteto/dev-lead/infra,它们在 origin == :started 时有条件地调用。
2. 启用:通用路径,不使用 USER_STARTED_AGENTS
USER_STARTED_AGENTS(apps/api/src/domain/sessions/agent-activation.ts)是 Criativo 的例外——不经 handoff、由用户直接命令启动。调查确认 Staff 不需要进入这个列表:canActivateAgent 已经会启用任何有一个指向它的 accepted handoff 的 agent,这与今天 dev-lead/arquiteto/infra 使用的路径相同。为此,api 中不需要任何领域改动:
assertHandoffTargetAllowed(apps/api/src/domain/agents/agent-areas.ts)只拒绝指向区域子 agent 的 handoff(ADR 0038);Staff 没有区域——像 Criativo/PO/Arquiteto/Dev Lead 一样自由通过。ActivateAgentUseCase/ApiToEngineClient.startAgent对agent: string是通用的——没有需要扩展的枚举。
后果是坦诚的:“可手动启动”意味着领域机制允许 handoff 和启用,而不是说今天存在一个专门的界面,让人在不通过其他 agent 的代码或直接调用内部路由(POST .../internal/sessions/:sessionId/handoffs)的情况下选择“向 Staff 发起 handoff”——“向任选 agent 手动 handoff”的通用 UI 仍在 backlog 中(docs/explanation/backlog.md),本次变更之前就是如此。
3. propose_rfc——唯一的工具,不是 proposed_action
Engine.Agents.StaffTools.propose_rfc 写入 artifact.rfc_staff(问题、带权衡的选项、建议、一次性 PoC),并在同一个 tool call 中把 handoff 交回 Arquiteto——中间没有人工确认,与 CriativoServer 在同一回复中发出 product_brief 并向 PO 提供 handoff 的模式相同。
两个真实先例决定了这一机制,因此 api 中没有诞生任何新用例:
Engine.Harness.Tools.EmitInsight(Arquiteto 的emit_insight)是无推导工件的模式:整个 payload 来自 tool call,事件直接通过EngineApiClient.append_event_returning/3写入——无表,无专用用例。artifact.c4_diagram(ADR 0068)是证实这一选择的对照:它在 api 中有自己的用例,因为 Container 层级是从当前module_map推导出来的——只有 api 拥有该数据。RFC 不需要从那边推导任何东西。
propose_rfc 也不是 proposed_action:登记一份架构文档不是外部效果(不是 git、终端,也不是 agent 花费)——真正的决定(采纳、调整、拒绝)属于 Arquiteto,在该工具已交回的 handoff 中做出。与 emit_insight 同样的理由。
后果
有利
- Staff 在 engine 中端到端存在(服务器、工具、supervisor、路由),今天就可测试,无需等 Anamnese 结束细化——当它回来时,只差 Anamnese 的代码调用
create_handoff(..., "staff", ...);其余都已就绪。 - 启用不需要 api 中的任何领域改动——调查证明通用路径已足够,避免开一个以后还得撤销的新例外(
USER_STARTED_AGENTS)。 docs/fluxo.yml不再是唯一知道 Staff 存在的来源:代码与注册表已声明的内容一致,真实缺口(自动触发器)变得精确,不再笼统(“等待触发器”)。
不利,已声明
- 团队面板的 roster(
apps/web/src/lib/agent-status.ts)和仪表盘卡片(ProjectCardSummary.roster,经由apps/api/.../projects-summary.repository.ts)在 Staff 活跃时会显示它——staffActive已加入两条路径,标准与infraActive相同,以免重蹈RosterFacts注释早已警告过的分歧(“两条规则会在第一个新 agent 出现时分歧”)。 - 会话界面(
SessionPage.tsx)没有改动。staff不进入AGENTES_DE_CHAT——与infra已被接受的模式相同(真实、活跃的 agent,同样不在该列表中:该文件自己的注释记录了指向 Infra 的 handoff“从不在这里被接受”)。这不是 Staff 的新缺口:这是产品对没有由会话 composer 驱动对话的 lead 早已采用的模式。声明的真实差异:Infra 有kickoff/1(被接受后自己运行);Staff 没有——因此,在没有能向它发送第一条user_message的聊天界面的情况下,今天端到端的使用路径是内部路由(POST .../agent/message,agent: "staff"),而不是界面。补上这一点意味着改动SessionPage.tsx——产品中争用最多的文件——不属于本决策:提前的是代码,不是聊天 UI。 Engine.Harness.Agents.identity/1(apps/engine/lib/engine/harness/agents.ex)中没有条目——与今天的dev-lead状态相同,它同样落入通用回退("Você é o agente staff.")。这不是新缺口:是一个活跃先例的真实状态,没有顺手修正。
考虑过的备选方案
等 Anamnese 结束细化再构建 Staff。 被拒绝——产品负责人的明确决定:提前写代码可以免去 Anamnese 重新开启那天的“第二个 PR”,而手动启动的价值独立于自动触发器而存在。
把 staff 加入 USER_STARTED_AGENTS。 被拒绝——调查表明没有必要:canActivateAgent 的标准路径已经会通过被接受的 handoff 启用,而该列表是为相反的例外(不经 handoff 启动的 agent)存在的。在这里添加会凭空创造一条领域不需要的规则。
给 Staff 一个带通用指令(“调查系统性问题”)的 kickoff/1。 被拒绝——没有真实工件可概括(只有 Anamnese 才会产出),kickoff 将是一个空洞的邀请;会话保持空闲直到第一条消息,这样是坦诚的。
参考
docs/fluxo.yml——块id: staffdocs/business-rules.md——RN-305/RN-306- ADR 0053——无区域的独立对话型 agent 的模子
- ADR 0068——为 RFC 决定“无专用用例”的对照
- ADR 0081—— “部件就绪,真实消费者尚未”且不弱化声明的先例
apps/engine/lib/engine/agents/staff_server.ex、staff_tools.ex、staff_supervisor.exapps/engine/lib/engine_web/controllers/agent_command_controller.exapps/api/src/domain/sessions/agent-activation.ts、apps/api/src/domain/agents/agent-areas.tsapps/web/src/lib/agents.ts、apps/web/src/lib/agent-status.ts