Files
AgentCompany/docs/proposals/M7-ceo-mode-state-machine.md
T

11 KiB
Raw Blame History

Proposal M7: CEO 模式状态机化(resolveCeoMode 单一真源)

来源:问题.md M7(行 260-264) 状态:proposed(依赖 B5 + C4) 提出:Roundtable 2026-04-20(Linus + Karpathy + Steve Jobs) Owner: TBD 估计工时:1.5 周 Blocking dependencies:B5(eval harness)、C4(initial-user-turn 精简)

问题陈述

CEO 当前处于哪个模式,由两处独立逻辑各自决定:

位置 1:src/main/prompt-builder/ceo-modes/ 下 5 个 markdown 文件

  • awaiting-hire.en.md
  • awaiting-org.en.md
  • operating.en.md
  • proactive-footer.en.md
  • team-building.en.md

加载逻辑在 runtime 靠字符串匹配——某段代码根据 company/CEO 状态推出一个 mode 名,然后按文件名拼路径读 markdown。

位置 2:src/main/orchestration/initial-user-turn.ts:62-83

  • 独立判断 CEO 是否 "zero active reports"
  • 独立判断是否"awaiting hire approval"
  • 独立决定发什么 cue

两处都在决定 "CEO 现在该做什么",互相不知道对方存在。结果是:ceo-modes 文件可能说当前是 operating 模式(有团队在运行),而 initial-user-turn 同时决定发 "your team is empty" 的 cue——内部打架。

根因分析

Linus 视角:同一件事(CEO 当前状态)被两个代码位置各自计算——这是第一性的违规。状态应该是单一 source of truth。

当前设计把 "mode" 当成一个魔法字符串在 prompt-builder 和 initial-user-turn 之间传递(甚至不是显式传,而是两边各自推),没有一个函数说"this is where CEO mode is decided"。

加载 markdown 的路径也是隐式约定——ceo-modes/team-building.en.md 的命名约定没有 schema 保证,漏一个文件、写错一个 mode 名、加新 mode 时忘了建对应文件,都是静默失败。

Karpathy 视角:模式分支是 agent 行为最关键的决策点——CEO 在 team-building / operating / awaiting-* 下的应答完全不同。这种分支必须集中并有 eval 覆盖每个分支。

设计方案

单一 resolveCeoMode 函数

新建 src/main/orchestration/ceo-mode.ts:

export type CeoMode =
  | "team_building"    // 无 active report,需要 hire
  | "awaiting_hire"    // 已提交 hire approval,等人批
  | "awaiting_org"     // 组织架构待确认
  | "operating"        // 正常运行态
  ;

export interface CeoModeContext {
  agent: AgentRecord;
  company: CompanyRecord;
  activeAgents: AgentRecord[];
  pendingApprovals: ApprovalRecord[];
  executionMode: ExecutionMode;
}

export function resolveCeoMode(ctx: CeoModeContext): CeoMode {
  if (ctx.agent.role !== "ceo") {
    throw new Error("resolveCeoMode called on non-CEO agent");
  }

  const activeReports = ctx.activeAgents.filter(a => a.ceoId === ctx.agent.id && a.status !== "terminated");
  const pendingHires = ctx.pendingApprovals.filter(a => a.kind === "hire_agent");

  if (activeReports.length === 0 && pendingHires.length === 0) {
    return "team_building";
  }
  if (activeReports.length === 0 && pendingHires.length > 0) {
    return "awaiting_hire";
  }
  if (ctx.company.orgStatus === "pending_review") {
    return "awaiting_org";
  }
  return "operating";
}

所有判断 CEO 模式的代码都必须调这个函数。

Prompt-builder 从 mode 映射到片段

// src/main/prompt-builder/ceo-modes/index.ts(新增)
const modeToSlug: Record<CeoMode, string> = {
  team_building: "team-building",
  awaiting_hire: "awaiting-hire",
  awaiting_org: "awaiting-org",
  operating: "operating",
};

export function loadCeoModePrompt(mode: CeoMode, locale: AppLocale): string {
  const slug = modeToSlug[mode];
  // 加载 `ceo-modes/{slug}.{locale}.md`
}

通过 TypeScript 枚举保证 mode 和 markdown 文件的一一对应——加新 mode 必须加枚举 + 对应 markdown,编译期强制。

initial-user-turn 走 resolveCeoMode

src/main/orchestration/initial-user-turn.ts 不再重复判断 CEO 状态,调 resolveCeoMode(ctx):

const mode = resolveCeoMode(modeCtx);
const cue = getCeoModeCue(mode, trigger);

这样 prompt-builder 里的 markdown 和 initial-user-turn 的 cue 通过同一个 mode 驱动——永远一致。

当前的 proactive-footer.en.md 不是一个 mode——它是所有 CEO 模式共享的 footer。应该:

  • 重命名目录结构:ceo-modes/{mode}.md 专门装 mode-specific
  • 另起 ceo-shared/proactive-footer.md 放共享片段
  • 避免把 footer 误当作第 5 个模式

Migration 计划

  1. Phase 0(前置):B5 eval harness + C4 完成
  2. Phase 1(2 天):实现 resolveCeoMode 函数 + 单测(不动 call site)
  3. Phase 2(3 天):prompt-builder 从 mode enum 加载 markdown,删除字符串拼接
  4. Phase 3(3 天):initial-user-turn 重构——删除自行判断逻辑,全走 resolveCeoMode
  5. Phase 4(3 天):eval 对比——相同 CEO 状态下行为一致性
  6. Phase 5(1 天):重组目录——proactive-footer 从 ceo-modes 移出

风险与回滚

风险 触发条件 回滚方法
模式分类漏 corner case awaiting_hire 和 awaiting_org 同时成立 resolveCeoMode 函数里明确 priority + 单测覆盖
eval 发现行为偏移 新 mode decision 逻辑和旧隐式判断不等价 保留旧 initial-user-turn 分支 flag,对比 N 轮 run
Markdown 内容过期 enum 加了新 mode 但没写 markdown 加载时 assert 文件存在,启动 fail fast
Locale 缺失 awaiting-hire.zh.md 不存在但 locale=zh fallback 到 .en.md + telemetry 告警

验证标准

  1. Typecheck 保证完整性:新增 mode 必须同时更新 enum + modeToSlug map + markdown 文件,否则编译 fail
  2. Eval 行为对比:每个 mode 至少 3 种代表性 company 状态,新旧行为 99% 一致
  3. Call site 归一:grep -rn "team_building\|awaiting_hire\|operating" src/main/ 只在 ceo-mode.ts 里出现枚举定义,其他地方通过 import 使用
  4. Markdown coverage:所有 mode 在所有支持的 locale 下都有文件,启动时校验通过
  5. Initial-user-turn 简化:initial-user-turn.ts 行数降低(假设从 159 行 → < 100 行)

不在范围

  • 不重写 ceo-modes 的 prompt 内容:只是让加载机制严谨,具体文字走 Owner 业务决策
  • 不加新 mode:当前 4 个 mode 足够(proactive-footer 不是 mode)
  • 不改 non-CEO agent 的模式逻辑:他们没有这种 mode 概念
  • 不做 mode transition 审计 log:有需要另开 event 表
  • 不做 mode-specific tool gating:tool 可用性和 mode 解耦(不同 mode 可用相同 tool,只是 prompt 引导不同)

参考文件清单

  • src/main/prompt-builder/ceo-modes/awaiting-hire.en.md
  • src/main/prompt-builder/ceo-modes/awaiting-org.en.md
  • src/main/prompt-builder/ceo-modes/operating.en.md
  • src/main/prompt-builder/ceo-modes/proactive-footer.en.md(将重组)
  • src/main/prompt-builder/ceo-modes/team-building.en.md
  • src/main/orchestration/initial-user-turn.ts:62-83(重复逻辑)
  • src/main/prompt-builder/system-prompt.ts(mode 注入点)
  • src/main/database/agents.ts(CEO 状态 query)
  • src/main/database/approvals.ts(pending hire approval query)

附:为什么现在修

C4 已经把 initial-user-turn 的"实现细节泄漏"修了——如果不顺便做 M7,就会出现 initial-user-turn 变干净了但还在自行判断 mode 的状态。一次做完,两件事共享同一个 resolveCeoMode 函数,改 prompt 的团队只需要 reason about 一个真源。

M7 也是未来扩展 CEO 行为的基础——如果要加 "awaiting_funding" / "pivoting" 之类的新模式,没有 state machine 就是继续堆字符串判断。

附:gating matrix

Gate 条件 放行判断人
G1 B5 eval harness 可 record CEO 在各 mode 下的 baseline 行为 Karpathy
G2 resolveCeoMode 单测覆盖 4 种 mode × 边缘状态 Linus
G3 prompt-builder 通过 enum 加载 markdown,启动时验证文件存在 Owner
G4 initial-user-turn 重构后行为 eval 与 baseline 一致 Karpathy
G5 proactive-footer 从 ceo-modes 目录移出,路径调整不破坏现有 prompt Owner

附:state machine 扩展指南

未来加新 mode 的最小改动点:

  1. src/main/orchestration/ceo-mode.ts 的 CeoMode enum 增加成员
  2. resolveCeoMode 函数加对应 if 分支
  3. ceo-modes/<new-mode>.en.md(+ 其他 locale)新建
  4. modeToSlug 映射加一行
  5. 对新 mode 写单测(至少 1 个 positive + 1 个 negative case)

TypeScript 会强制 1-4 的完整性——漏一步编译 fail。

附:反模式对比

Before(initial-user-turn.ts:62-83 + ceo-modes/*.md 两处各自判断):

// 位置 1: initial-user-turn.ts
if (activeReports.length === 0) {
  return teamBuildingCue;
}

// 位置 2: prompt-builder/system-prompt.ts (加载 ceo-modes)
const modeFile = loadCeoModeFile(company, agent);  // 内部逻辑不一致

After(单一真源):

const mode = resolveCeoMode(ctx);
const cue = getCeoModeCue(mode, trigger);
const modeSection = loadCeoModePrompt(mode, locale);

两处都 derive from mode,保证一致。

附:为什么不选其他方案

方案 A(当前 proposal):enum + resolve 函数 + markdown 映射。

方案 B(把 mode 存 DB):不必要——mode 完全由当前状态 derive,存 DB 反而要维护一致性。

方案 C(把 markdown 内容放代码里):失去给产品经理 / 业务 owner 迭代 prompt 的低成本路径。

方案 A 保留 markdown 灵活性同时让加载逻辑类型安全。

附:mode priority 排序规则

resolveCeoMode 判断顺序(先匹配即返回):

  1. team_building:无 active report + 无 pending hire
  2. awaiting_hire:无 active report + 有 pending hire
  3. awaiting_org:有 active report + org 待 review
  4. operating:默认

顺序基于"外部依赖度"——团队空时必须先 team_building,其次考虑组织结构审查,最后才是运行态。单测覆盖所有 transition:

from → to 条件 覆盖点
team_building → awaiting_hire 提交 hire approval unit
awaiting_hire → team_building hire approval 被拒 unit
awaiting_hire → operating hire approval 通过 unit
operating → awaiting_org org review 触发 unit
awaiting_org → operating org review 通过 unit

ceo-modes/proactive-footer.en.md 当前存在于 ceo-modes 目录但不是 mode。重组:

Before:
ceo-modes/
├── awaiting-hire.en.md
├── awaiting-org.en.md
├── operating.en.md
├── proactive-footer.en.md  ← 伪 mode
└── team-building.en.md

After:
ceo-modes/                    ← 真 modes
├── awaiting-hire.en.md
├── awaiting-org.en.md
├── operating.en.md
└── team-building.en.md
ceo-shared/                   ← 跨 mode 共享片段
└── proactive-footer.en.md

system-prompt.ts 加载 mode 片段后,独立拼接 ceo-shared/proactive-footer.en.md。