代码提交

This commit is contained in:
董宏凯 committed 2026-08-03 19:27:54 +08:00
1 parent d93cc62806
commit ddf83f7728
25 files changed
+4086 -424

No files matched your search

@@ -2,6 +2,7 @@
from __future__ import annotations
import json
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock
@@ -12,12 +13,19 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from app.maf_roundtable.fact_ledger import executed_action_ids, proposed_action_ids
from app.maf_roundtable.workflow_executors import (
ApprovalGateExecutor,
CoordinatorExecutor,
CrossReviewExecutor,
DispatchPreparationTerminalExecutor,
FinalizerExecutor,
ManagerAnswerExecutor,
ManagerDispatchExecutor,
PlannerExecutor,
RoundtableState,
SeatExecutor,
WaitingTerminalExecutor,
)
from app.maf_roundtable.workflow_factory import create_workflow
from app.maf_roundtable.workflow_factory import build_manager_dispatch_workflow, create_workflow
from deerflow.persistence.base import Base
from deerflow.persistence.maf_roundtable import make_maf_roundtable_store
@@ -53,7 +61,7 @@ def _state(seat_ids: list[str] | None = None) -> RoundtableState:
reasoning_effort=None,
max_rounds=8,
write_policy="confirm_high_risk",
manager_agent_id=None,
manager_agent_id="manager",
seat_agent_ids=seats,
thread_map={a: f"t-{a}" for a in seats},
capability_snapshot=None,
@@ -62,6 +70,390 @@ def _state(seat_ids: list[str] | None = None) -> RoundtableState:
)
@pytest.mark.asyncio
async def test_direct_answer_is_manager_only():
state = _state([])
state.manager_agent_id = "manager"
state.thread_map = {"manager": "manager-thread"}
adapter = SimpleNamespace(
run_seat_turn=AsyncMock(return_value=_FakeSeatResponse(content="direct answer")),
_emit_critical=AsyncMock(),
)
await ManagerAnswerExecutor(adapter, state).handle("task", _FakeCtx())
adapter.run_seat_turn.assert_awaited_once()
assert adapter.run_seat_turn.await_args.kwargs["agent_id"] == "manager"
assert adapter.run_seat_turn.await_args.kwargs["execution_stage"] == "direct_answer"
assert state.status == "completed"
dispatch = next(
call.args[1]
for call in adapter._emit_critical.await_args_list
if call.args[1].get("event_type") == "manager.dispatch"
)
assert dispatch["payload"]["execution_mode"] == "manager_only"
assert dispatch["payload"]["selected_seat_agent_ids"] == []
@pytest.mark.asyncio
async def test_manager_only_topology_runs_real_maf_workflow_without_seats():
pytest.importorskip("agent_framework")
state = _state([])
state.manager_agent_id = "manager"
state.thread_map = {"manager": "manager-thread"}
state.intent_type = "direct_answer"
state.dispatch_plan = {
"manager_agent_id": "manager",
"seat_agent_ids": [],
"assignments": [],
"intent_type": "direct_answer",
"max_rounds": 8,
"candidate_seat_agent_ids": ["seat-a", "seat-b", "seat-c"],
"execution_mode": "manager_only",
"convergence_path": ["manager answer"],
}
adapter = SimpleNamespace(
run_seat_turn=AsyncMock(return_value=_FakeSeatResponse(content="manager answer")),
_emit_critical=AsyncMock(),
)
manager = ManagerAnswerExecutor(adapter, state)
workflow = create_workflow(
mode="manager_only",
manager_answer=manager,
seat_executors=[object(), object()],
approval_gate=object(),
finalizer=object(),
waiting_terminal=object(),
seat_agent_ids=["seat-a", "seat-b"],
)
async for _ in workflow.run(state.task_text, stream=True, include_status_events=True):
pass
adapter.run_seat_turn.assert_awaited_once()
assert adapter.run_seat_turn.await_args.kwargs["agent_id"] == "manager"
assert state.status == "completed"
@pytest.mark.asyncio
async def test_v2_direct_answer_runs_manager_dispatch_then_manager_only_workflow():
pytest.importorskip("agent_framework")
state = _state(["seat-a", "seat-b"])
state.thread_map = {"manager": "manager-thread"}
state.manager_agent_id = "manager"
state.candidate_seat_agent_ids = ["seat-a", "seat-b"]
state.dispatch_contract_enabled = True
decision = {
"intent_type": "direct_answer",
"selected_seat_agent_ids": [],
"assignments": [],
"execution_mode": "manager_only",
"convergence_path": ["manager answer"],
"reason": "manager can answer directly",
}
async def run_turn(**kwargs):
if kwargs["execution_stage"] == "manager_dispatch":
return _FakeSeatResponse(content=json.dumps(decision))
return _FakeSeatResponse(content="direct answer")
adapter = SimpleNamespace(
run_seat_turn=AsyncMock(side_effect=run_turn),
_emit_critical=AsyncMock(),
)
preparation = build_manager_dispatch_workflow(
manager_dispatch=ManagerDispatchExecutor(adapter, state),
terminal=DispatchPreparationTerminalExecutor(state),
)
async for _ in preparation.run(state.task_text, stream=True, include_status_events=True):
pass
workflow = create_workflow(
mode="manager_only",
manager_answer=ManagerAnswerExecutor(adapter, state),
seat_executors=[],
approval_gate=object(),
finalizer=object(),
waiting_terminal=object(),
seat_agent_ids=[],
)
async for _ in workflow.run(state.task_text, stream=True, include_status_events=True):
pass
stages = [call.kwargs["execution_stage"] for call in adapter.run_seat_turn.await_args_list]
assert stages == ["manager_dispatch", "direct_answer"]
assert state.selected_seat_agent_ids == []
@pytest.mark.asyncio
async def test_invalid_manager_plan_falls_back_to_clarification_without_starting_seats():
state = _state()
state.manager_agent_id = "manager"
state.thread_map["manager"] = "manager-thread"
state.candidate_seat_agent_ids = ["seat-a", "seat-b", "seat-c"]
state.dispatch_contract_enabled = True
adapter = SimpleNamespace(
run_seat_turn=AsyncMock(return_value=_FakeSeatResponse(content="not a dispatch plan")),
_emit_critical=AsyncMock(),
)
ctx = _FakeCtx()
await ManagerDispatchExecutor(adapter, state).handle("task", ctx)
assert state.status == "waiting_input"
assert state.intent_type == "clarification"
assert state.selected_seat_agent_ids == []
assert ctx.messages[-1]["decision"] == "clarification"
assert adapter.run_seat_turn.await_args.kwargs["execution_stage"] == "manager_dispatch"
@pytest.mark.asyncio
async def test_manager_dispatch_uses_capability_summary_and_selects_only_a_subset():
state = _state()
state.manager_agent_id = "manager"
state.thread_map["manager"] = "manager-thread"
state.candidate_seat_agent_ids = ["seat-a", "seat-b", "seat-c"]
state.dispatch_contract_enabled = True
state.capability_snapshot = {
"agents": [
{"agent_id": "manager", "tools": [{"tool_name": "manager_write", "side_effect": "write", "risk_level": "R3"}]},
{"agent_id": "seat-a", "model_name": "m-a", "tools": [{"tool_name": "read_a", "side_effect": "read", "risk_level": "R1"}]},
{"agent_id": "seat-b", "model_name": "m-b", "tools": [{"tool_name": "read_b", "side_effect": "read", "risk_level": "R1"}]},
{"agent_id": "seat-c", "model_name": "m-c", "tools": [{"tool_name": "write_c", "side_effect": "write", "risk_level": "R3"}], "secret": "never-prompted"},
]
}
decision = {
"intent_type": "parallel_consultation",
"selected_seat_agent_ids": ["seat-a", "seat-b"],
"assignments": [
{"agent_id": "seat-a", "task_goal": "collect evidence", "delivery_requirements": ["sources"], "order": 1},
{"agent_id": "seat-b", "task_goal": "compare evidence", "delivery_requirements": ["gaps"], "order": 2},
],
"execution_mode": "parallel",
"convergence_path": ["manager synthesis"],
"reason": "two independent perspectives are needed",
}
adapter = SimpleNamespace(
run_seat_turn=AsyncMock(return_value=_FakeSeatResponse(content=json.dumps(decision))),
_emit_critical=AsyncMock(),
)
await ManagerDispatchExecutor(adapter, state).handle("task", _FakeCtx())
prompt = adapter.run_seat_turn.await_args.kwargs["message"]
assert "trusted capability summary" in prompt.lower()
assert "never-prompted" not in prompt
assert adapter.run_seat_turn.await_args.kwargs["excluded_tools"] == ["manager_write"]
assert state.selected_seat_agent_ids == ["seat-a", "seat-b"]
assert state.plan["assignments"].keys() == {"seat-a", "seat-b"}
@pytest.mark.asyncio
@pytest.mark.parametrize("manager_result", [
"{\"intent_type\": \"unknown\"}",
json.dumps({
"intent_type": "single_seat",
"selected_seat_agent_ids": ["untrusted-seat"],
"assignments": [{"agent_id": "untrusted-seat", "task_goal": "x", "delivery_requirements": ["y"], "order": 1}],
"execution_mode": "single",
"convergence_path": ["manager synthesis"],
"reason": "invalid seat",
}),
])
async def test_invalid_intent_or_seat_falls_back_to_clarification(manager_result):
state = _state()
state.manager_agent_id = "manager"
state.thread_map["manager"] = "manager-thread"
state.candidate_seat_agent_ids = ["seat-a", "seat-b", "seat-c"]
state.dispatch_contract_enabled = True
adapter = SimpleNamespace(
run_seat_turn=AsyncMock(return_value=_FakeSeatResponse(content=manager_result)),
_emit_critical=AsyncMock(),
)
await ManagerDispatchExecutor(adapter, state).handle("task", _FakeCtx())
assert state.status == "waiting_input"
assert state.selected_seat_agent_ids == []
@pytest.mark.asyncio
async def test_manager_dispatch_timeout_falls_back_to_clarification():
state = _state()
state.manager_agent_id = "manager"
state.thread_map["manager"] = "manager-thread"
state.candidate_seat_agent_ids = ["seat-a", "seat-b", "seat-c"]
state.dispatch_contract_enabled = True
adapter = SimpleNamespace(
run_seat_turn=AsyncMock(side_effect=TimeoutError()),
_emit_critical=AsyncMock(),
)
await ManagerDispatchExecutor(adapter, state).handle("task", _FakeCtx())
assert state.status == "waiting_input"
assert state.dispatch_plan["reason"] == "manager_dispatch_timeout"
@pytest.mark.asyncio
async def test_manager_dispatch_persists_and_binds_only_selected_seat_threads(tmp_path):
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path / 'dispatch-subset.db'}")
async with engine.begin() as conn:
import deerflow.persistence.maf_roundtable.model # noqa: F401
await conn.run_sync(Base.metadata.create_all)
store = make_maf_roundtable_store(async_sessionmaker(engine, expire_on_commit=False))
session = await store.sessions.create_session({
"user_id": "u1",
"task_text": "analyze",
"mode": "structured",
"manager_agent_id": "manager",
"max_rounds": 8,
})
await store.participants.replace_participants(session["id"], [{
"agent_id": "manager",
"role": "manager",
"sort_order": 0,
}])
run = await store.runs.create_run(session["id"])
state = _state()
state.session_id = session["id"]
state.run_id = run["id"]
state.thread_map = {"manager": "manager-thread"}
state.candidate_seat_agent_ids = ["seat-a", "seat-b", "seat-c"]
state.dispatch_contract_enabled = True
decision = {
"intent_type": "parallel_consultation",
"selected_seat_agent_ids": ["seat-a", "seat-b"],
"assignments": [
{"agent_id": "seat-a", "task_goal": "evidence", "delivery_requirements": ["sources"], "order": 1},
{"agent_id": "seat-b", "task_goal": "review", "delivery_requirements": ["gaps"], "order": 2},
],
"execution_mode": "parallel",
"convergence_path": ["manager synthesis"],
"reason": "selected subset",
}
adapter = SimpleNamespace(
_service=SimpleNamespace(store=store),
run_seat_turn=AsyncMock(return_value=_FakeSeatResponse(content=json.dumps(decision))),
ensure_threads=AsyncMock(return_value={"seat-a": "thread-a", "seat-b": "thread-b"}),
_emit_critical=AsyncMock(),
)
try:
await ManagerDispatchExecutor(adapter, state).handle("task", _FakeCtx())
participants = await store.participants.list_participants(session["id"])
assert [row["agent_id"] for row in participants] == ["manager", "seat-a", "seat-b"]
adapter.ensure_threads.assert_awaited_once()
bound_agents = [row["agent_id"] for row in adapter.ensure_threads.await_args.args[2]]
assert bound_agents == ["seat-a", "seat-b"]
assert "seat-c" not in state.thread_map
finally:
await engine.dispose()
@pytest.mark.asyncio
async def test_group_chat_workflow_executes_verified_manager_subset_only():
pytest.importorskip("agent_framework")
state = _state(["seat-a", "seat-b", "seat-c"])
state.mode = "group_chat"
state.thread_map = {"manager": "manager-thread"}
state.manager_agent_id = "manager"
state.candidate_seat_agent_ids = ["seat-a", "seat-b", "seat-c"]
state.dispatch_contract_enabled = True
participant_rows = [{"agent_id": "manager", "role": "manager", "thread_id": "manager-thread"}]
async def ensure_participants(_session_id, rows):
participant_rows.extend(row for row in rows if row["agent_id"] not in {p["agent_id"] for p in participant_rows})
async def list_participants(_session_id):
return list(participant_rows)
dispatch = {
"intent_type": "parallel_consultation",
"selected_seat_agent_ids": ["seat-a", "seat-b"],
"assignments": [
{"agent_id": "seat-a", "task_goal": "evidence", "delivery_requirements": ["sources"], "order": 1},
{"agent_id": "seat-b", "task_goal": "review", "delivery_requirements": ["gaps"], "order": 2},
],
"execution_mode": "group_chat",
"convergence_path": ["manager synthesis"],
"reason": "discussion requires two seats",
}
async def run_turn(**kwargs):
if kwargs["execution_stage"] == "manager_dispatch":
return _FakeSeatResponse(content=json.dumps(dispatch))
if kwargs["execution_stage"] == "finalizer":
return _FakeSeatResponse(content="final")
return _FakeSeatResponse(content=f"{kwargs['agent_id']} result")
adapter = SimpleNamespace(
_service=SimpleNamespace(
store=SimpleNamespace(
participants=SimpleNamespace(
ensure_participants=ensure_participants,
list_participants=list_participants,
),
runs=SimpleNamespace(update_run=AsyncMock()),
actions=SimpleNamespace(list_actions=AsyncMock(return_value=[])),
)
),
run_seat_turn=AsyncMock(side_effect=run_turn),
ensure_threads=AsyncMock(return_value={"seat-a": "thread-a", "seat-b": "thread-b"}),
_emit_critical=AsyncMock(),
)
preparation = build_manager_dispatch_workflow(
manager_dispatch=ManagerDispatchExecutor(adapter, state),
terminal=DispatchPreparationTerminalExecutor(state),
)
async for _ in preparation.run(state.task_text, stream=True, include_status_events=True):
pass
workflow = create_workflow(
mode="group_chat",
coordinator=CoordinatorExecutor(adapter, state),
seat_executors=[
SeatExecutor(adapter, state, agent_id)
for agent_id in state.selected_seat_agent_ids or []
],
approval_gate=ApprovalGateExecutor(adapter, state, SimpleNamespace()),
finalizer=FinalizerExecutor(adapter, state),
waiting_terminal=WaitingTerminalExecutor(),
seat_agent_ids=state.selected_seat_agent_ids or [],
)
async for _ in workflow.run(state.task_text, stream=True, include_status_events=True):
pass
stages = [call.kwargs["execution_stage"] for call in adapter.run_seat_turn.await_args_list]
assert stages.count("manager_dispatch") == 1
assert stages.count("group_chat") == 2
assert "seat-c" not in [call.kwargs["agent_id"] for call in adapter.run_seat_turn.await_args_list]
assert [row["agent_id"] for row in participant_rows] == ["manager", "seat-a", "seat-b"]
@pytest.mark.parametrize(
("execution_mode", "seat_count"),
[("single", 1), ("parallel", 2), ("chain", 2)],
)
def test_structured_factory_builds_each_trusted_topology(execution_mode, seat_count):
pytest.importorskip("agent_framework")
state = _state(["seat-a", "seat-b"][:seat_count])
adapter = SimpleNamespace(_emit_critical=AsyncMock())
seat_executors = [SeatExecutor(adapter, state, agent_id) for agent_id in state.seat_agent_ids]
workflow = create_workflow(
mode="structured",
planner=PlannerExecutor(adapter, state),
cross_review=CrossReviewExecutor(adapter, state),
seat_executors=seat_executors,
approval_gate=ApprovalGateExecutor(adapter, state, SimpleNamespace()),
finalizer=FinalizerExecutor(adapter, state),
waiting_terminal=WaitingTerminalExecutor(),
seat_agent_ids=state.seat_agent_ids,
execution_mode=execution_mode,
)
assert workflow is not None
@pytest.mark.asyncio
async def test_planner_deterministic_assignments():
state = _state()