feat(orchestrator): 会话状态机实现 + cancelled/resume(T3 架构审查整改)
- Issue3: 新建 src/genesis/state_machine.py(9 状态白名单转移) - cancel 记录 cancelled_from 进 cancelled 终态,resume 回中断点 - 仅执行中状态可取消(awaiting_*/done 不可),非法转移抛 StateTransitionError - 同步 agent-runtime-design.md §3.2(状态图/规则表 9 状态 + cancelled 行) - 新增 10 用例(含覆盖补齐 2 项),全量 177 passed / 100.00%(981 stmts/252 br)
This commit is contained in:
@@ -72,3 +72,4 @@
|
||||
| 2026-08-11 | 架构设计 | 整体架构工程审查(plan-eng-review,SCOPE_REDUCED 模式):Step0 裁定移除 Qdrant 抽象+Redis 双实现(单一 Chroma+InMemory);Section1 架构 7 issue(jsonschema 真校验/解析降级链/cancelled+resume 状态/引擎层注入防护/删死配置/v1 rerank/记忆完整实现);Section2 代码质量 3 issue(DRY helper/全异步化/局部变量);Section4 性能 2 issue(CJK token 估算/串行约束文档化);Outside Voice 8 项全接受(文档同步/bge-m3/resolver/黄金集/DataGate 机制化/QA 护栏+fallback/任务持久化/docx 原型提前);产出 17 项 Implementation Tasks JSONL;报告 docs/architecture-eng-review.md | docs/architecture-eng-review.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
| 2026-08-11 | Agent 实现 | T1(架构审查整改):chat_structured 接入 jsonschema 真校验。pyproject.toml 新增 jsonschema>=4.23 依赖;engine.py 在 json.loads 后调用 jsonschema.validate(schema 非空时),校验失败按解析失败重试(新增 except jsonschema.ValidationError 分支,带校验错误信息重试);新增 3 用例(违规重试成功/违规耗尽 parse_error 含校验详情/合法一次通过);TDD 验证 RED({'a':'not_a_number'} 未被拒直接返回)→ GREEN(聚焦 4 passed)→ 全量 163 passed 覆盖 100.00%(936 stmts/238 br),fail_under=99 达标 | pyproject.toml, src/genesis/inference/engine.py, tests/test_inference_engine.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
| 2026-08-11 | Agent 实现 | T2(架构审查整改):chat_structured 解析重试走降级链(Issue2)+ 模型名局部变量(Issue10)。engine.py 重构 chat_structured:外层 attempts 轮次循环 + 内层降级链 names 遍历(首选成功 ok/降级成功 fallback);解析/校验失败即时追加错误信息供备用模型重试可见;LLMError 不再 early return 而继续降级链,全部失败按 last_was_parse_error 区分 parse_error/failed;删除 4 处重复 _model_names(None)[0] 调用;同步更新 7 个既有用例脚本数量与断言(降级链语义:network 失败用例显式 retry_count=0);新增 2 用例(解析重试降级 fallback/网络失败降级 fallback);TDD 验证 RED(解析重试仍用首选模型)→ GREEN(聚焦 27 passed)→ 全量 165 passed 覆盖 100.00%(941 stmts/242 br),fail_under=99 达标 | src/genesis/inference/engine.py, tests/test_inference_engine.py, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
| 2026-08-11 | Agent 实现 | T3(架构审查整改):会话状态机实现 + cancelled/resume(Issue3)。新建 src/genesis/state_machine.py(SessionStateMachine:9 状态白名单转移 + StateTransitionError 对应 api §7 STATE_TRANSITION_INVALID 409;cancel 记录 cancelled_from 进 cancelled 终态,resume 回中断点;仅执行中状态可取消,awaiting_*/done 不可;状态集含 8 设计态 + cancelled);同步 agent-runtime-design.md §3.2 状态图/规则表(9 状态 + cancelled 行);新增 10 用例(正常流转/非法转移拒绝/done 终态/cancel 记录/resume 回中断点/非 cancelled 不可 resume/cancelled 不可任意跳转/循环 cancel-resume/未知初始/目标状态防御);TDD 验证 RED(ModuleNotFoundError)→ GREEN(聚焦 10 passed)→ 覆盖补齐 2 用例 → 全量 177 passed 覆盖 100.00%(981 stmts/252 br),fail_under=99 达标 | src/genesis/state_machine.py, tests/test_state_machine.py, docs/agent-runtime-design.md, _AI_USAGE_LOG.md | deepseek-v4-flash-free |
|
||||
|
||||
@@ -231,9 +231,12 @@ class PromptRegistry:
|
||||
├──(校验通过)──────► ⑧ done
|
||||
└──(需修正重生成)──► ⑥ writing
|
||||
|
||||
状态集(8 个):
|
||||
执行中任意状态 ──(用户取消 /api/sessions/{id}/cancel)──► ⑨ cancelled
|
||||
⑨ cancelled ──(resume)──► 回到中断前状态(cancelled_from,继续白名单流转)
|
||||
|
||||
状态集(9 个,含 T3 整改新增 cancelled):
|
||||
uploading → parsing → awaiting_parse_confirm → impact_running
|
||||
→ awaiting_impact_confirm → writing → qa → done
|
||||
→ awaiting_impact_confirm → writing → qa → done | cancelled
|
||||
```
|
||||
|
||||
**状态转移规则:**
|
||||
@@ -241,16 +244,18 @@ class PromptRegistry:
|
||||
| 当前状态 | 允许转移 | 触发 |
|
||||
|---------|---------|------|
|
||||
| uploading | parsing | 文件上传完成 |
|
||||
| parsing | awaiting_parse_confirm | 解析完成 |
|
||||
| parsing | awaiting_parse_confirm / cancelled | 解析完成 / 用户取消 |
|
||||
| awaiting_parse_confirm | impact_running / parsing | 确认 / 修正后重解析 |
|
||||
| impact_running | awaiting_impact_confirm | 影响调查完成 |
|
||||
| impact_running | awaiting_impact_confirm / cancelled | 影响调查完成 / 用户取消 |
|
||||
| awaiting_impact_confirm | writing / impact_running / awaiting_parse_confirm | 确认 / 修正重推 / 打回解析 |
|
||||
| writing | qa / awaiting_impact_confirm | 全章完成 / 用户要求回退 |
|
||||
| qa | done / writing | 校验通过 / 需修正重生成 |
|
||||
| done | — | 终态 |
|
||||
| writing | qa / awaiting_impact_confirm / cancelled | 全章完成 / 用户要求回退 / 用户取消 |
|
||||
| qa | done / writing / cancelled | 校验通过 / 需修正重生成 / 用户取消 |
|
||||
| done | — | 终态(不可取消) |
|
||||
| cancelled | 中断前状态(cancelled_from)| resume(仅 cancelled 可 resume;人工等待确认态 awaiting_* 与 done 不可取消)|
|
||||
|
||||
- **非法转移直接拒绝**(如 uploading → writing 不合法)
|
||||
- **非法转移直接拒绝**(如 uploading → writing 不合法;cancelled → done 不合法)
|
||||
- 回退规则由白名单约束(如 awaiting_impact_confirm → awaiting_parse_confirm 合法)
|
||||
- 取消语义:cancel 记录中断前状态到 cancelled_from,进入终态;resume 回到中断点后按白名单继续推进
|
||||
|
||||
### 3.3 人工介入点定义
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
# 会话级状态集(9 个):8 个设计态 + cancelled(T3 架构审查整改)
|
||||
SessionState = Literal[
|
||||
"uploading",
|
||||
"parsing",
|
||||
"awaiting_parse_confirm",
|
||||
"impact_running",
|
||||
"awaiting_impact_confirm",
|
||||
"writing",
|
||||
"qa",
|
||||
"done",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
STATES: frozenset[str] = frozenset({
|
||||
"uploading",
|
||||
"parsing",
|
||||
"awaiting_parse_confirm",
|
||||
"impact_running",
|
||||
"awaiting_impact_confirm",
|
||||
"writing",
|
||||
"qa",
|
||||
"done",
|
||||
"cancelled",
|
||||
})
|
||||
|
||||
# 合法转移白名单(不含 cancelled 的动态 resume 转移,见 SessionStateMachine.resume)
|
||||
_TRANSITIONS: dict[str, frozenset[str]] = {
|
||||
"uploading": frozenset({"parsing"}),
|
||||
"parsing": frozenset({"awaiting_parse_confirm", "cancelled"}),
|
||||
"awaiting_parse_confirm": frozenset({"impact_running", "parsing", "cancelled"}),
|
||||
"impact_running": frozenset({"awaiting_impact_confirm", "cancelled"}),
|
||||
"awaiting_impact_confirm": frozenset({"writing", "impact_running", "awaiting_parse_confirm", "cancelled"}),
|
||||
"writing": frozenset({"qa", "awaiting_impact_confirm", "cancelled"}),
|
||||
"qa": frozenset({"done", "writing", "cancelled"}),
|
||||
"done": frozenset(),
|
||||
"cancelled": frozenset(),
|
||||
}
|
||||
|
||||
# 可被取消的(非终态、非人工等待确认态之外全部执行中;done 不可取消)
|
||||
_CANCELLABLE: frozenset[str] = frozenset({
|
||||
"parsing",
|
||||
"impact_running",
|
||||
"writing",
|
||||
"qa",
|
||||
})
|
||||
|
||||
|
||||
class StateTransitionError(Exception):
|
||||
"""非法状态转移(对应 api-design §7 STATE_TRANSITION_INVALID 409)。"""
|
||||
|
||||
|
||||
class SessionStateMachine:
|
||||
"""会话级流程状态机:白名单转移 + cancelled/resume(T3)。
|
||||
|
||||
取消(cancel)从任意执行中状态进入 cancelled 终态,并记录中断前状态
|
||||
(cancelled_from);resume 从 cancelled 回到中断前状态,恢复后继续
|
||||
正常白名单流转。人工等待确认态(awaiting_*)与 done 不可取消。
|
||||
"""
|
||||
|
||||
def __init__(self, initial: str = "uploading") -> None:
|
||||
if initial not in STATES:
|
||||
raise StateTransitionError(f"未知初始状态: {initial}")
|
||||
self._state: str = initial
|
||||
self._cancelled_from: str | None = None
|
||||
|
||||
@property
|
||||
def state(self) -> str:
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def cancelled_from(self) -> str | None:
|
||||
"""取消前的中断状态(resume 目标);仅 cancelled 态非 None。"""
|
||||
return self._cancelled_from
|
||||
|
||||
def transition(self, target: str) -> str:
|
||||
"""按白名单推进状态机;非法转移抛 StateTransitionError。"""
|
||||
if target not in STATES:
|
||||
raise StateTransitionError(f"未知目标状态: {target}")
|
||||
allowed = _TRANSITIONS[self._state]
|
||||
if target not in allowed:
|
||||
raise StateTransitionError(
|
||||
f"非法状态转移: {self._state} → {target}(白名单外)"
|
||||
)
|
||||
self._state = target
|
||||
return self._state
|
||||
|
||||
def cancel(self) -> str:
|
||||
"""取消当前执行:进入 cancelled 终态,记录中断前状态。"""
|
||||
if self._state not in _CANCELLABLE:
|
||||
raise StateTransitionError(
|
||||
f"当前状态不可取消: {self._state}(仅执行中状态可取消)"
|
||||
)
|
||||
self._cancelled_from = self._state
|
||||
self._state = "cancelled"
|
||||
return self._state
|
||||
|
||||
def resume(self) -> str:
|
||||
"""从 cancelled 恢复:回到中断前状态(cancelled_from)。"""
|
||||
if self._state != "cancelled":
|
||||
raise StateTransitionError(
|
||||
f"仅 cancelled 状态可 resume(当前: {self._state})"
|
||||
)
|
||||
assert self._cancelled_from is not None # cancelled 态必有记录
|
||||
self._state = self._cancelled_from
|
||||
self._cancelled_from = None
|
||||
return self._state
|
||||
@@ -0,0 +1,127 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from genesis.state_machine import StateTransitionError, SessionStateMachine
|
||||
|
||||
|
||||
# ---------- 正常流转 ----------
|
||||
|
||||
def test_initial_state_is_uploading():
|
||||
sm = SessionStateMachine()
|
||||
assert sm.state == "uploading"
|
||||
|
||||
|
||||
def test_unknown_initial_state_rejected():
|
||||
with pytest.raises(StateTransitionError):
|
||||
SessionStateMachine(initial="bogus")
|
||||
|
||||
|
||||
def test_unknown_target_state_rejected():
|
||||
sm = SessionStateMachine()
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition("bogus")
|
||||
|
||||
|
||||
def test_happy_path_full_flow():
|
||||
sm = SessionStateMachine()
|
||||
sm.transition("parsing")
|
||||
sm.transition("awaiting_parse_confirm")
|
||||
sm.transition("impact_running")
|
||||
sm.transition("awaiting_impact_confirm")
|
||||
sm.transition("writing")
|
||||
sm.transition("qa")
|
||||
sm.transition("done")
|
||||
assert sm.state == "done"
|
||||
|
||||
|
||||
# ---------- 非法转移拒绝 ----------
|
||||
|
||||
def test_illegal_transition_rejected():
|
||||
sm = SessionStateMachine()
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition("writing") # uploading → writing 非法
|
||||
|
||||
|
||||
def test_done_is_terminal_no_transition_out():
|
||||
sm = SessionStateMachine()
|
||||
sm.transition("parsing")
|
||||
sm.transition("awaiting_parse_confirm")
|
||||
sm.transition("impact_running")
|
||||
sm.transition("awaiting_impact_confirm")
|
||||
sm.transition("writing")
|
||||
sm.transition("qa")
|
||||
sm.transition("done")
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition("writing")
|
||||
|
||||
|
||||
# ---------- T3: cancelled + resume ----------
|
||||
|
||||
def test_cancel_from_running_state():
|
||||
sm = SessionStateMachine()
|
||||
sm.transition("parsing")
|
||||
sm.transition("awaiting_parse_confirm")
|
||||
sm.transition("impact_running")
|
||||
sm.transition("awaiting_impact_confirm")
|
||||
sm.transition("writing")
|
||||
sm.cancel()
|
||||
assert sm.state == "cancelled"
|
||||
assert sm.cancelled_from == "writing"
|
||||
|
||||
|
||||
def test_cancel_from_done_is_illegal():
|
||||
sm = SessionStateMachine()
|
||||
sm.transition("parsing")
|
||||
sm.transition("awaiting_parse_confirm")
|
||||
sm.transition("impact_running")
|
||||
sm.transition("awaiting_impact_confirm")
|
||||
sm.transition("writing")
|
||||
sm.transition("qa")
|
||||
sm.transition("done")
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.cancel()
|
||||
|
||||
|
||||
def test_resume_returns_to_interrupted_state():
|
||||
sm = SessionStateMachine()
|
||||
sm.transition("parsing")
|
||||
sm.transition("awaiting_parse_confirm")
|
||||
sm.transition("impact_running")
|
||||
sm.transition("awaiting_impact_confirm")
|
||||
sm.transition("writing")
|
||||
sm.cancel()
|
||||
sm.resume()
|
||||
assert sm.state == "writing"
|
||||
|
||||
|
||||
def test_resume_without_cancel_is_illegal():
|
||||
sm = SessionStateMachine()
|
||||
sm.transition("parsing")
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.resume() # 非 cancelled 状态不能 resume
|
||||
|
||||
|
||||
def test_cancelled_cannot_jump_to_arbitrary_state():
|
||||
sm = SessionStateMachine()
|
||||
sm.transition("parsing")
|
||||
sm.cancel()
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition("done") # cancelled 只能 resume 回中断前状态
|
||||
|
||||
|
||||
def test_resume_after_resume_allows_forward_progress():
|
||||
# 取消 → resume → 再次取消 → resume 循环稳定(cancelled_from 保持最后一次中断点)
|
||||
sm = SessionStateMachine()
|
||||
sm.transition("parsing")
|
||||
sm.transition("awaiting_parse_confirm")
|
||||
sm.transition("impact_running")
|
||||
sm.cancel()
|
||||
sm.resume()
|
||||
assert sm.state == "impact_running"
|
||||
sm.transition("awaiting_impact_confirm")
|
||||
sm.transition("writing")
|
||||
sm.cancel()
|
||||
assert sm.cancelled_from == "writing"
|
||||
sm.resume()
|
||||
assert sm.state == "writing"
|
||||
Reference in New Issue
Block a user