Files
aiagent/docs/M2_control_plan.md
renjianbo 495eafcd78 feat: virtual company M2 control + tier-3/4 capabilities + per-dept session fix
- M2-a/b/c: stop/pause/resume/skip dept, plan/rework approval gates,
  budget & timeout circuit breakers, per-dept cancel and on-demand rework
- tier-3: batched event persistence (_EventBuffer), project/dept
  concurrency semaphores, company_ws event streaming
- tier-4: cross-dept deliverable handoff, CEO memory feedback loop,
  per-dept model override, one-click Markdown report export (RFC5987)
- fix: per-department independent SQLAlchemy session in parallel waves
  (shared Session race across asyncio tasks in company_orchestrator)
- fix: rename process_open_fds gauge to app_process_open_fds
  (collides with prometheus_client ProcessCollector on Linux)
- fix: dept role resolution ladder, real token usage & cost tracking,
  deliverable file capture (mtime scan of dept workspace)
- tests: test_company_orchestrator.py 22 cases with mocked LLM
- frontend: CompanyControlRoom, companyExecution store, EChart,
  dept model config dialog, ws proxy, build/typecheck decoupling
- docs: virtual company design/usage docs, M2 control plan,
  deepseek4pro handover docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-26 18:16:39 +08:00

70 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 虚拟公司「运行时控制」(M2) 规划文档
> 状态:M2-a(能力 1–4)已排期实施;M2-b/c 登记待办。
> 关联:M1 白盒控制室(可观测)已完成;一档(部门角色匹配 + 可信真实 token/成本¥)已完成并验证。
> 更新:2026-07-12
## 背景与动机
白盒控制室(M1)解决了「看得见」,一档解决了「数据可信」(真实 token + 成本¥,实测一次运行约 ¥9.88、耗时 ~60 分钟)。但目前**只能看不能动**:一次运行跑歪了,只能干等到结束或直接杀进程。M2 给控制室补上「运行时控制」,让人能在长运行过程中干预。
M2 拆成 **10 项可独立验收的原子能力**,按依赖与价值分三片交付。一次全做改动面广、验证成本高(单次运行 ~1 小时),故分片。
## 架构基线(现状,已逐点核实)
- **同进程模型**:`company_runner.spawn()` 在 API(uvicorn)进程内 `asyncio.create_task` 起编排任务,句柄存 `_RUNNING: Dict[str, asyncio.Task]`。API handler 与编排任务同进程 → 控制用「进程内注册表 + 协作检查点」即可,无需 DB/消息队列。
- **停止半成品**:已有 `CancelledError → status=stopped` + 落 `company_terminated` 事件的雏形,缺「按 project_id 取消」的 helper 与对外端点。
- **编排检查点**:`company_orchestrator.execute_stream` 有干净的注入点——轮次顶、就绪部门选取、事件抽取循环、波次边界。
- **事件单写者**:编排器 `yield` → runner `_persist_event` 落库并分配单调 `seq`。控制确认事件必须走编排器 yield,避免 API 直接写库造成 seq 竞争。
- **可复用件**:`approval_manager.py`(asyncio.Event 式 create/wait/resolve,供审批);round-to-round rework 过滤(供单部门返工);一档已累计的 `company_cost_yuan`(供预算熔断)。
## 10 项能力总览
| # | 能力 | 片 | 机制 / 复用点 | 关键取舍 |
|---|------|----|----------------|----------|
| 1 | **停止**(硬取消整公司运行) | M2-a | `company_runner.stop(pid)` = `_RUNNING[pid].cancel()`,复用 `CancelledError→stopped` 雏形 | 打断在飞 LLM,立即生效 |
| 2 | **暂停**(hold,不开新波次) | M2-a | `ProjectControl.pause_event.clear()`;编排器波次/轮次边界 `await wait()` | 在飞部门跑完才 hold(部门粒度 ~15min 延迟) |
| 3 | **继续**(resume) | M2-a | `pause_event.set()` | — |
| 4 | **跳过部门**(skip 未开始部门) | M2-a | `control.skip_depts`;就绪选取处拦截并 `completed_names.add` 让下游推进 | 仅未开始部门;在跑部门不停 |
| 5 | **单部门返工**(对已完成项目只重跑一个部门,不重跑整公司) | M2-c | 复用 rework 过滤(`failed_names→filtered_plans→current_plans`),单部门子集外部驱动;新 `POST …/rework {department_name}` | 需读回历史 ceo_plan/产出做上下文 |
| 6 | **审批·CEO 计划**(花钱前把关) | M2-b | 复用 `approval_manager.py`;CEO 规划后 `yield awaiting_approval → await → 放行/改计划` | 阻塞式,带 timeout 兜底 |
| 7 | **审批·部门评分/返工**(返工前把关) | M2-b | 同上,插在 CEO 打分后、决定返工前 | — |
| 8 | **预算熔断**(成本超 ¥X 自动停) | M2-b | 把 advisory 预算检查改**强制**:读运行中 `company_cost_yuan`,超阈值 → `budget_tripped` + 触发停止 | 阈值入 `company.config["budget"]`;按 ¥ 而非 token |
| 9 | **超时熔断**(总耗时超 N 分钟自动停) | M2-b | 新增 deadline:编排器起始记 `t0`,检查点比对(当前无任何 timeout,仅事后判僵尸) | 与预算熔断共用「自动停」出口 |
| 10 | **单部门取消**(取消在跑的某部门,非停整公司) | M2-c | 为每个 `_run_department_stream` task 建 per-dept 句柄表,`cancel` 单个;team 层需容错半途取消 | 比 skip 复杂,需 team_orchestrator 配合 |
## 分片与依赖
- **M2-a(本片)· 核心四控**:1 停止、2 暂停、3 继续、4 跳过。共用「进程内控制注册表 + 编排检查点」,自成闭环、最易验证。
- **M2-b · 把关与自动停**:6/7 人在环审批(`approval_manager`)+ 8 预算熔断 + 9 超时熔断(8/9 共用「自动停」出口,与一档成本累计联动)。
- **M2-c · 部门粒度**:5 单部门返工 + 10 单部门取消(都要 per-dept 粒度的重跑/取消,最深,放最后)。
**依赖顺序**:8/9 熔断依赖 1 停止的出口;5 返工依赖一档产出落库;10 取消依赖 per-dept task 句柄。故 1–4 必须先行。
## M2-a 设计要点(本片实施)
### 后端
- `company_runner.py`:`ProjectControl{pause_event, skip_depts}` + `_CONTROL` 注册表 + `get_control/clear_control` + `stop(pid)`(取消 `_RUNNING[pid]`,复用 stopped 雏形);done-callback 清理控制态。
- `company_orchestrator.execute_stream`:惰性 `import company_runner`,起始取 `control`;**暂停闸**(轮次顶 + 波次边界,`await pause_event.wait()`,yield `paused`/`resumed`);**跳过闸**(就绪选取处拦 `skip_depts`,标记 `completed_names` 让下游推进,yield `dept_skipped`)。
- `companies.py`:`POST …/projects/{pid}/{stop|pause|resume|skip}` 四端点,`_assert_company_owner`;pause/resume/skip 需 `is_running`(否则 409),pause/resume 同步 `project.status`。
### 前端
- `api/companies.ts`:`stopProject/pauseProject/resumeProject/skipDepartment`(镜像 `recoverProject` POST)。
- `stores/companyExecution.ts`:state `paused/controlPending`;action `stop/pause/resume/skipDept`;`applyEvent` 加 `paused/resumed/dept_skipped` 归并。
- `views/CompanyControlRoom.vue`:控制栏(暂停/继续/停止,停止带 `ElMessageBox.confirm`)+ 每部门 `waiting` 时显「跳过」;状态标签显「已暂停」。
### 语义取舍(写进 UI 提示)
- **停止** = 硬取消,立即 → stopped(打断在飞 LLM)。
- **暂停** = 只拦新波次/新轮次,在飞部门跑完才 hold(部门粒度延迟);`/pause` 即时置 `status=paused` 缓解 UI 反馈。
- **跳过** = 仅未开始部门;在跑部门跳过留 M2-c(能力 10)。
- **进程内状态**:进程重启则控制态随运行任务一起消失(任务本就死),无持久化需求;单 worker 假设(与 `_RUNNING` 一致)。
## 验证(端到端)
1. 重启后端 + `npm run build`(= vite)。
2. 起一次 async 运行。
3. 暂停/继续:`/pause` → status=paused、下一波不启动、`paused` 事件;`/resume` → `resumed` 且继续。
4. 跳过:对 `waiting` 部门 `/skip` → `dept_skipped`、下游依赖照常推进、该部门不产生 agent 事件。
5. 停止:`/stop` → 取消、落 `company_terminated`、status=stopped、WS `_stream_end`。
6. 前端按钮可用(停止有确认)、断线重连控制态从事件流恢复。
7. 回归:不点任何控制的普通运行仍正常完成(检查点在未暂停/无跳过时是 no-op)。