feat(agents): Phase 8.4-10.5 built-in plugins, bundled skills, coordinator
This commit is contained in:
@@ -16,10 +16,12 @@ from app.agents.graph import (
|
||||
_create_child_agent,
|
||||
_execute_tool_calls,
|
||||
_parse_json_action,
|
||||
_record_checkpoint,
|
||||
_record_interrupt,
|
||||
_record_recovery,
|
||||
_route_agent_from_user_query,
|
||||
_select_request_mode,
|
||||
_set_phase,
|
||||
_spawn_permission_for_role,
|
||||
_run_collaboration_flow,
|
||||
_run_sub_commander,
|
||||
@@ -78,8 +80,23 @@ def _base_state(message: str, user_llm_config: dict | None = None) -> dict:
|
||||
'verification_status': None,
|
||||
'verification_summary': None,
|
||||
'verification_evidence': [],
|
||||
'isolation_mode': 'none',
|
||||
'isolation_id': None,
|
||||
'isolation_workspace_path': None,
|
||||
'isolation_parent_conversation_id': None,
|
||||
'isolation_metadata': {},
|
||||
'input_tokens': 0,
|
||||
'output_tokens': 0,
|
||||
'estimated_cost': None,
|
||||
'budget_warning': False,
|
||||
'cost_by_agent': {},
|
||||
'cost_thresholds': {},
|
||||
'budget_state': None,
|
||||
'collaboration_budget_history': [],
|
||||
'current_phase': 'phase_0_bootstrap',
|
||||
'phase_history': [{'phase': 'phase_0_bootstrap', 'reason': 'initial_state_created'}],
|
||||
'current_checkpoint': 'bootstrap.initialized',
|
||||
'checkpoint_history': [{'checkpoint': 'bootstrap.initialized', 'phase': 'phase_0_bootstrap', 'reason': 'initial_state_created'}],
|
||||
'tool_strategy_used': None,
|
||||
'tool_round_count': 0,
|
||||
'max_tool_rounds': 2,
|
||||
@@ -310,6 +327,24 @@ def test_initial_state_sets_structured_continuity_defaults():
|
||||
assert state['clarification_context'] is None
|
||||
assert state['event_trace'] == []
|
||||
assert state['tool_outcomes'] == []
|
||||
assert state['current_phase'] == 'phase_0_bootstrap'
|
||||
assert state['current_checkpoint'] == 'bootstrap.initialized'
|
||||
assert state['phase_history'][-1]['phase'] == 'phase_0_bootstrap'
|
||||
assert state['checkpoint_history'][-1]['checkpoint'] == 'bootstrap.initialized'
|
||||
|
||||
|
||||
def test_set_phase_and_record_checkpoint_append_history():
|
||||
state = _base_state('test')
|
||||
|
||||
_set_phase(state, 'phase_1_routing', reason='entered_master')
|
||||
_record_checkpoint(state, 'routing.master_entered', reason='entered_master')
|
||||
|
||||
assert state['current_phase'] == 'phase_1_routing'
|
||||
assert state['current_checkpoint'] == 'routing.master_entered'
|
||||
assert state['phase_history'][-1]['phase'] == 'phase_1_routing'
|
||||
assert state['checkpoint_history'][-1]['checkpoint'] == 'routing.master_entered'
|
||||
assert 'agent.phase.changed' in [event['event_type'] for event in state['event_trace']]
|
||||
assert 'agent.checkpoint.recorded' in [event['event_type'] for event in state['event_trace']]
|
||||
|
||||
|
||||
def test_spawn_permission_for_role_uses_registry_policy():
|
||||
@@ -627,6 +662,15 @@ async def test_run_collaboration_flow_collects_task_results_and_verifies(monkeyp
|
||||
assert result['message_trace'][-1]['message_type'] == 'task_update'
|
||||
assert 'agent.created' in [event['event_type'] for event in result['event_trace']]
|
||||
assert 'agent.message.sent' in [event['event_type'] for event in result['event_trace']]
|
||||
assert 'agent.phase.changed' in [event['event_type'] for event in result['event_trace']]
|
||||
assert 'agent.checkpoint.recorded' in [event['event_type'] for event in result['event_trace']]
|
||||
assert result['current_phase'] == 'phase_4_visibility_and_verification'
|
||||
assert result['current_checkpoint'] == 'collaboration.completed'
|
||||
assert [entry['phase'] for entry in result['phase_history']][-3:] == [
|
||||
'phase_2_controlled_collaboration',
|
||||
'phase_3_dynamic_collaboration',
|
||||
'phase_4_visibility_and_verification',
|
||||
]
|
||||
assert 'agent.spawn.blocked' not in [event['event_type'] for event in result['event_trace']]
|
||||
assert result['spawned_agent_ids']
|
||||
assert all(not agent_id.startswith('blocked-') for agent_id in result['spawned_agent_ids'])
|
||||
@@ -637,6 +681,8 @@ async def test_master_node_enters_collaboration_mode_for_complex_multi_role_requ
|
||||
async def fake_collaboration_flow(state, user_query):
|
||||
state['execution_mode'] = 'collaboration'
|
||||
state['final_response'] = 'collaboration done'
|
||||
state['current_phase'] = 'phase_4_visibility_and_verification'
|
||||
state['current_checkpoint'] = 'collaboration.completed'
|
||||
state['messages'] = [*state.get('messages', []), AIMessage(content=state['final_response'])]
|
||||
return state
|
||||
|
||||
@@ -647,6 +693,31 @@ async def test_master_node_enters_collaboration_mode_for_complex_multi_role_requ
|
||||
|
||||
assert result['execution_mode'] == 'collaboration'
|
||||
assert result['final_response'] == 'collaboration done'
|
||||
assert result['current_phase'] == 'phase_4_visibility_and_verification'
|
||||
assert result['current_checkpoint'] == 'collaboration.completed'
|
||||
|
||||
|
||||
async def test_run_collaboration_flow_fallback_restores_routing_phase(monkeypatch):
|
||||
monkeypatch.setattr(graph_module, '_build_collaboration_tasks', lambda _user_query: [
|
||||
AgentTask(
|
||||
task_id='task-1',
|
||||
title='单任务',
|
||||
role=AgentRole.LIBRARIAN.value,
|
||||
owner_agent_id=AgentRole.LIBRARIAN.value,
|
||||
goal='检索资料',
|
||||
expected_evidence=[{'type': 'evidence'}],
|
||||
)
|
||||
])
|
||||
|
||||
state = _base_state('帮我搜一下资料')
|
||||
result = await _run_collaboration_flow(state, '帮我搜一下资料')
|
||||
|
||||
assert result['execution_mode'] == 'direct'
|
||||
assert result['routing_decision']['reason'] == 'collaboration_plan_fell_back'
|
||||
assert result['current_phase'] == 'phase_1_routing'
|
||||
assert result['current_checkpoint'] == 'routing.direct_resumed'
|
||||
assert result['checkpoint_history'][-2]['checkpoint'] == 'collaboration.fallback_to_direct'
|
||||
assert result['checkpoint_history'][-1]['checkpoint'] == 'routing.direct_resumed'
|
||||
|
||||
|
||||
async def test_master_node_returns_stable_reply_for_simple_greeting(monkeypatch):
|
||||
@@ -1404,6 +1475,78 @@ def test_build_verifier_hints_uses_capability_metadata():
|
||||
assert '提醒创建成功' in hints['result_preview']
|
||||
|
||||
|
||||
def test_prepare_isolation_context_selects_session_for_stateful_tools():
|
||||
state = _base_state('reminder request')
|
||||
|
||||
graph_module._prepare_isolation_context(
|
||||
state,
|
||||
role=AgentRole.SCHEDULE_PLANNER,
|
||||
sub_commander='schedule_planning',
|
||||
user_query='create a reminder for tomorrow morning and keep the intermediate state isolated',
|
||||
toolset=[FakeTool('create_reminder', 'ok')],
|
||||
)
|
||||
|
||||
assert state['isolation_mode'] == 'session'
|
||||
assert state['isolation_workspace_path'] is None
|
||||
assert state['isolation_metadata']['reason'] == 'stateful_or_non_parallel_tooling'
|
||||
assert state['event_trace'][-1]['event_type'] == 'agent.isolation.selected'
|
||||
|
||||
|
||||
def test_prepare_isolation_context_uses_worktree_for_repo_mutation_queries(monkeypatch):
|
||||
state = _base_state('fix repo build and create patch')
|
||||
|
||||
monkeypatch.setattr(
|
||||
graph_module,
|
||||
'prepare_worktree_isolation',
|
||||
lambda **kwargs: {
|
||||
'mode': 'worktree',
|
||||
'isolation_id': 'worktree-test',
|
||||
'workspace_path': '/tmp/jarvis/worktree-test',
|
||||
'parent_conversation_id': 'c1',
|
||||
'metadata': {
|
||||
'reason': kwargs['decision'].reason,
|
||||
'branch': 'jarvis/c1/executor-worktree-test',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
graph_module._prepare_isolation_context(
|
||||
state,
|
||||
role=AgentRole.EXECUTOR,
|
||||
sub_commander='executor_tasks',
|
||||
user_query='fix repo build and create patch for the failing tests',
|
||||
toolset=[FakeTool('create_task', 'ok')],
|
||||
)
|
||||
|
||||
assert state['isolation_mode'] == 'worktree'
|
||||
assert state['isolation_workspace_path'] == '/tmp/jarvis/worktree-test'
|
||||
assert state['isolation_metadata']['branch'] == 'jarvis/c1/executor-worktree-test'
|
||||
assert state['event_trace'][-1]['event_type'] == 'agent.isolation.selected'
|
||||
|
||||
|
||||
def test_record_response_usage_updates_state_cost_totals_and_budget_warning():
|
||||
state = _base_state('test')
|
||||
state['cost_thresholds'] = {'total_tokens': 100, 'estimated_cost': 0.0001}
|
||||
|
||||
graph_module._record_response_usage(
|
||||
state,
|
||||
AIMessage(
|
||||
content='ok',
|
||||
usage_metadata={'input_tokens': 60, 'output_tokens': 50, 'total_tokens': 110},
|
||||
),
|
||||
)
|
||||
|
||||
assert state['input_tokens'] == 60
|
||||
assert state['output_tokens'] == 50
|
||||
assert state['estimated_cost'] == 0.00093
|
||||
assert state['budget_warning'] is True
|
||||
assert state['cost_by_agent'][AgentRole.MASTER.value]['total_tokens'] == 110
|
||||
assert [event['event_type'] for event in state['event_trace']] == [
|
||||
'agent.cost.updated',
|
||||
'agent.cost.warning',
|
||||
]
|
||||
|
||||
|
||||
async def test_execute_tool_calls_records_schema_events_and_aggregate_summaries(monkeypatch):
|
||||
tool = FakeTool('create_reminder', '提醒创建成功: 开会 @ 2026-03-29 09:00')
|
||||
state = _base_state('test')
|
||||
|
||||
@@ -135,6 +135,45 @@ async def visibility_env(tmp_path):
|
||||
'verification_evidence': [
|
||||
{'task_id': 'task-1', 'status': 'passed', 'summary': 'Verified'}
|
||||
],
|
||||
'execution_mode': 'collaboration',
|
||||
'current_phase': 'phase_4_visibility_and_verification',
|
||||
'current_checkpoint': 'visibility.runtime_summary_ready',
|
||||
'phase_history': [
|
||||
{'phase': 'phase_0_bootstrap'},
|
||||
{'phase': 'phase_4_visibility_and_verification'},
|
||||
],
|
||||
'checkpoint_history': [
|
||||
{'checkpoint': 'bootstrap.initialized'},
|
||||
{'checkpoint': 'visibility.runtime_summary_ready'},
|
||||
],
|
||||
'input_tokens': 120,
|
||||
'output_tokens': 80,
|
||||
'budget_warning': True,
|
||||
'estimated_cost': 0.00156,
|
||||
'cost_thresholds': {'total_tokens': 150, 'estimated_cost': 0.001},
|
||||
'cost_by_agent': {
|
||||
'master': {
|
||||
'agent_id': 'master',
|
||||
'input_tokens': 60,
|
||||
'output_tokens': 20,
|
||||
'total_tokens': 80,
|
||||
'estimated_cost': 0.00048,
|
||||
'budget_warning': False,
|
||||
},
|
||||
'analyst-1234abcd': {
|
||||
'agent_id': 'analyst-1234abcd',
|
||||
'input_tokens': 60,
|
||||
'output_tokens': 60,
|
||||
'total_tokens': 120,
|
||||
'estimated_cost': 0.00108,
|
||||
'budget_warning': True,
|
||||
},
|
||||
},
|
||||
'isolation_mode': 'worktree',
|
||||
'isolation_id': 'iso-1',
|
||||
'isolation_workspace_path': '/tmp/jarvis/worktree-1',
|
||||
'isolation_parent_conversation_id': 'parent-conv-1',
|
||||
'isolation_metadata': {'branch': 'jarvis/test-worker'},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -396,6 +435,87 @@ async def test_visibility_verifier_returns_verdict(visibility_env):
|
||||
assert payload['evidence'][0]['task_id'] == ids['task_id']
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_visibility_runtime_summary_returns_phase_cost_and_isolation_metadata(visibility_env):
|
||||
app, ids = visibility_env
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
response = await client.get(
|
||||
'/api/agents/visibility/runtime-summary',
|
||||
params={'conversation_id': ids['conversation_id']},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload['conversation_id'] == ids['conversation_id']
|
||||
assert payload['execution_mode'] == 'collaboration'
|
||||
assert payload['current_phase'] == 'phase_4_visibility_and_verification'
|
||||
assert payload['current_checkpoint'] == 'visibility.runtime_summary_ready'
|
||||
assert payload['verifier']['status'] == 'passed'
|
||||
assert payload['isolation']['mode'] == 'worktree'
|
||||
assert payload['isolation']['workspace_path'] == '/tmp/jarvis/worktree-1'
|
||||
assert payload['isolation']['metadata']['branch'] == 'jarvis/test-worker'
|
||||
assert payload['cost']['input_tokens'] == 120
|
||||
assert payload['cost']['output_tokens'] == 80
|
||||
assert payload['cost']['total_tokens'] == 200
|
||||
assert payload['cost']['estimated_cost'] == 0.00156
|
||||
assert payload['cost']['budget_warning'] is True
|
||||
assert payload['topology_node_count'] == 2
|
||||
assert payload['active_task_count'] == 1
|
||||
assert payload['completed_task_count'] == 1
|
||||
assert payload['recent_events'][0]['event_id'] == 'evt-1'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_visibility_cost_returns_totals_thresholds_and_agent_breakdown(visibility_env):
|
||||
app, ids = visibility_env
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
response = await client.get(
|
||||
'/api/agents/visibility/cost',
|
||||
params={'conversation_id': ids['conversation_id']},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload['total']['input_tokens'] == 120
|
||||
assert payload['total']['output_tokens'] == 80
|
||||
assert payload['total']['total_tokens'] == 200
|
||||
assert payload['total']['budget_warning'] is True
|
||||
assert payload['thresholds']['total_tokens'] == 150
|
||||
assert payload['thresholds']['estimated_cost'] == 0.001
|
||||
assert payload['by_agent'][0]['agent_id'] == 'analyst-1234abcd'
|
||||
assert payload['by_agent'][0]['budget_warning'] is True
|
||||
assert payload['by_agent'][1]['agent_id'] == 'master'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_visibility_tools_returns_governance_metadata_and_usage_counts(visibility_env):
|
||||
app, ids = visibility_env
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url='http://testserver') as client:
|
||||
response = await client.get(
|
||||
'/api/agents/visibility/tools',
|
||||
params={'conversation_id': ids['conversation_id']},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload['total_tools'] >= 1
|
||||
assert payload['used_tools'] >= 1
|
||||
search_tool = next(item for item in payload['items'] if item['tool_name'] == 'search_web')
|
||||
assert search_tool['permission_class'] == 'external'
|
||||
assert search_tool['side_effect_scope'] == 'network'
|
||||
assert search_tool['usage_count'] == 1
|
||||
assert search_tool['last_result_preview'] == 'ok'
|
||||
assert payload['upgrade_candidates'] == [
|
||||
'worktree_manager',
|
||||
'cost_inspector',
|
||||
'runtime_event_drilldown',
|
||||
'tool_policy_explorer',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_visibility_events_reject_invalid_datetime(visibility_env):
|
||||
app, ids = visibility_env
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.models.conversation import Conversation, Message
|
||||
from app.models.memory import MemorySummary, UserMemory
|
||||
from app.models.user import User
|
||||
from app.services import agent_service, memory_service
|
||||
from app.services.agent_service import AgentService
|
||||
from app.services.agent_service import AgentService, _build_continuity_snapshot, _extract_continuity_snapshot
|
||||
from app.services.auth_service import get_password_hash
|
||||
from app.services.document_service import DocumentService
|
||||
|
||||
@@ -23,6 +23,32 @@ class FakeGraph:
|
||||
return {"final_response": "已记录你的请求。"}
|
||||
|
||||
|
||||
def test_continuity_snapshot_roundtrip_preserves_phase_and_checkpoint():
|
||||
payload = {
|
||||
"current_agent": "master",
|
||||
"current_phase": "phase_4_visibility_and_verification",
|
||||
"phase_history": [
|
||||
{"phase": "phase_0_bootstrap", "reason": "initial_state_created"},
|
||||
{"phase": "phase_4_visibility_and_verification", "reason": "verification_started"},
|
||||
],
|
||||
"current_checkpoint": "collaboration.completed",
|
||||
"checkpoint_history": [
|
||||
{"checkpoint": "bootstrap.initialized", "phase": "phase_0_bootstrap", "reason": "initial_state_created"},
|
||||
{"checkpoint": "collaboration.completed", "phase": "phase_4_visibility_and_verification", "reason": "collaboration_flow_finished"},
|
||||
],
|
||||
}
|
||||
|
||||
snapshot = _build_continuity_snapshot(payload)
|
||||
|
||||
assert snapshot is not None
|
||||
restored = _extract_continuity_snapshot({"kind": "agent_continuity_state", **snapshot})
|
||||
assert restored is not None
|
||||
assert restored["current_phase"] == "phase_4_visibility_and_verification"
|
||||
assert restored["current_checkpoint"] == "collaboration.completed"
|
||||
assert restored["phase_history"][-1]["phase"] == "phase_4_visibility_and_verification"
|
||||
assert restored["checkpoint_history"][-1]["checkpoint"] == "collaboration.completed"
|
||||
|
||||
|
||||
class FakeStreamingGraph:
|
||||
async def astream_events(self, state, version="v2"):
|
||||
yield {
|
||||
|
||||
Reference in New Issue
Block a user