diff --git a/docs/design/spawner-monitor-design.md b/docs/design/spawner-monitor-design.md index f2fadec..34c29f9 100644 --- a/docs/design/spawner-monitor-design.md +++ b/docs/design/spawner-monitor-design.md @@ -410,25 +410,95 @@ await self.spawner.spawn_full_agent( 存储在 `task_attempts.metadata` JSON 中。 -### counter 生命周期(v2.0:调用级) +### counter v2.1:per (agent, session) 粒度(2026-05-26) + +#### 问题 + +v2.0 的 `max_per_agent=1` 是 per agent 粒度,导致: +- 张飞正在跑一个 task(sub session,use_main_session=False) +- 这时又来一个新 task 需要张飞 spawn 新 sub session +- counter 拦住 → AgentBusyError +- 但 sub session 之间是独立的(不同 session_id,不同 lock),不应该互相限制 + +#### 新设计:三层控制 + +| 层级 | 配置 | 默认 | 作用 | +|------|------|------|------| +| per session key | `max_per_session` | 1 | 同一 (agent, session) 不能并发 spawn | +| per agent | `max_concurrent_sessions` | 3 | 同一 agent 最多同时跑 N 个不同 session | +| global | `max_global_agents` | 5 | 全局总并发上限 | + +**counter key**:`f"{agent_id}:{session_id}"` +- main session → `zhangfei:main` +- sub session → `zhangfei:abc-123` +- 不同 key 互不影响,同一 key 最多 1 个 + +**session_id 分配前移**: + +```python +# 新顺序(spawn_full_agent 内部) +1. 分配 session_id(main → "main", reuse → 传入值, new → uuid4) +2. session state 检查(main 时才检查) +3. counter.can_acquire(agent_id, session_id) # 检查三层 +4. counter.acquire(agent_id, session_id) # 占用 key +5. spawn +``` + +#### cooldown + +cooldown 保持 per agent 粒度(API 429 限制的是 agent 的 API key,不是 session)。 +冷却期间该 agent 的所有 spawn 都被拒绝。 + +#### 超限行为 + +- `can_acquire` 返回 False → `AgentBusyError` +- 任务/mail 保持 pending,等下个 tick(30s)重试 +- 不会丢失 + +#### release 调用点 + +所有 release 必须带 session_id: + +| 调用点 | 当前 | 改后 | +|--------|------|------| +| wrapped_on_complete 闭包 | `release(aid)` | `release(aid, session_id)` | +| spawn 失败 fallback(行 441) | `release(agent_id)` | `release(agent_id, session_id)` | +| _do_retry 续杯 | `release(agent_id)` | `release(agent_id, old_session_id)` | + +wrapped_on_complete 闭包需要捕获外层 session_id 变量。 + +#### 配置变更 + +```yaml +# config/default.yaml +max_global_agents: 5 # 不变 +max_per_session: 1 # 新增,替代 max_per_agent +max_concurrent_sessions: 3 # 新增,per agent 总并发 +# max_per_agent: 已删除 +``` + +### counter 生命周期(v2.1:per session 级) ``` -spawn_full_agent 内部 acquire +spawn_full_agent 内部 │ - ├─ 进程退出 → wrapped_on_complete → counter.release() + ├─ 1. 分配 session_id + ├─ 2. session state 检查(main 时) + ├─ 3. can_acquire(agent_id, session_id) → 检查三层 + ├─ 4. acquire(agent_id, session_id) → 占用 key + ├─ 5. spawn 进程 + │ + ├─ 进程退出 → wrapped_on_complete(aid, session_id) → release(aid, session_id) │ ├─ A1/A4 完成 → 结束 - │ ├─ A2/A3 timeout → _do_retry 手动 release → spawn_full_agent 重新 acquire + │ ├─ A2/A3 timeout → _do_retry: release(aid, old_sid) → spawn_full_agent(acquire 新的) │ └─ A7-A12 → release → 等 ticker │ ├─ monitor timeout(B)→ counter 不 release(进程还在跑) - │ └─ B1 假死 → 手动 release + 复活 + │ └─ B1 假死 → 手动 release(aid, session_id) + 复活 │ - └─ 进程崩溃/PM2 重启 → ticker _check_timeouts 检测 → 手动 release + └─ 进程崩溃/PM2 重启 → ticker _check_timeouts 检测 → 手动 release(aid, session_id) ``` -counter 生命周期 = 调用级:spawn 时 acquire,进程退出时 release。 -只有情况 B(进程不退出)counter 保持占用。 - wrapped_on_complete 保证 release(try/finally),即使业务回调异常也不泄漏。 ## 9. escalate 消息格式