test_framework_observation_e2e.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import httpx
  2. import pytest
  3. import agent as framework
  4. from agent.orchestration.api_v2 import create_orchestration_router
  5. from agent.tools.builtin.knowledge import KnowledgeConfig
  6. from agent.trace.api import router as trace_router
  7. from agent.trace.api import set_trace_store
  8. from test_mission_loop import MissionLoopLLM, ROOT_OBJECTIVE, ROOT_OBJECTIVE_V2
  9. FastAPI = pytest.importorskip("fastapi").FastAPI
  10. def _knowledge_off():
  11. return KnowledgeConfig(
  12. enable_extraction=False,
  13. enable_completion_extraction=False,
  14. enable_injection=False,
  15. )
  16. def _all_keys(value):
  17. if isinstance(value, dict):
  18. return set(value).union(*(_all_keys(item) for item in value.values()))
  19. if isinstance(value, list):
  20. return set().union(*(_all_keys(item) for item in value))
  21. return set()
  22. @pytest.mark.asyncio
  23. async def test_public_framework_mission_is_fully_observable_without_ledger_access(
  24. tmp_path,
  25. ):
  26. root_trace_id = "framework-blackbox-root"
  27. trace_store = framework.FileSystemTraceStore(str(tmp_path))
  28. task_store = framework.FileSystemTaskStore(str(tmp_path))
  29. fake_llm = MissionLoopLLM(task_store)
  30. fake_llm.root_trace_id = root_trace_id
  31. runner = framework.AgentRunner(trace_store=trace_store, llm_call=fake_llm)
  32. coordinator = framework.wire_orchestration(
  33. runner,
  34. task_store,
  35. framework.FileSystemArtifactStore(str(tmp_path)),
  36. )
  37. result = await runner.run_result(
  38. [{"role": "user", "content": "run a generic observable mission"}],
  39. framework.RunConfig(
  40. agent_type="planner",
  41. completion_policy=framework.CompletionPolicy.EXPLICIT_VALIDATION,
  42. max_iterations=30,
  43. new_trace_id=root_trace_id,
  44. tools=["task_plan", "dispatch_tasks", "task_decide"],
  45. tool_groups=[],
  46. enable_memory=False,
  47. enable_research_flow=False,
  48. knowledge=_knowledge_off(),
  49. root_task_spec={
  50. "objective": ROOT_OBJECTIVE,
  51. "acceptance_criteria": [{
  52. "criterion_id": "root-ready",
  53. "description": "the root result is ready",
  54. "hard": True,
  55. }],
  56. "context_refs": ["memory://input/root"],
  57. },
  58. ),
  59. )
  60. assert result["status"] == "completed"
  61. app = FastAPI()
  62. app.include_router(create_orchestration_router(coordinator))
  63. set_trace_store(trace_store)
  64. app.include_router(trace_router)
  65. http = httpx.AsyncClient(
  66. transport=httpx.ASGITransport(app=app), base_url="http://framework"
  67. )
  68. client = framework.OrchestrationClient("http://framework", client=http)
  69. mission = await client.get_mission_snapshot(root_trace_id)
  70. completion = await client.get_root_completion(root_trace_id)
  71. events = await client.list_events(root_trace_id, limit=1000)
  72. traces = await client.list_correlated_traces(
  73. root_trace_id=root_trace_id, limit=100
  74. )
  75. assert mission.root_objective == ROOT_OBJECTIVE_V2
  76. assert completion.root_objective == ROOT_OBJECTIVE_V2
  77. assert completion.status == framework.TaskStatus.COMPLETED
  78. assert completion.result_summary == f"submitted: {ROOT_OBJECTIVE_V2}"
  79. assert any(
  80. decision.action.value == "revise"
  81. and decision.task_id == mission.root_task_id
  82. for decision in mission.decisions
  83. )
  84. assert {event.schema_version for event in events.events} == {2}
  85. assert {trace["agent_role"] for trace in traces} == {
  86. "planner",
  87. "worker",
  88. "validator",
  89. }
  90. attempts = {item.attempt_id: item for item in mission.attempts}
  91. validations = {item.validation_id: item for item in mission.validations}
  92. trace_ids = {trace["trace_id"] for trace in traces}
  93. accepted = [
  94. decision for decision in mission.decisions
  95. if decision.action.value == "accept"
  96. ]
  97. assert len(accepted) == 2
  98. for decision in accepted:
  99. attempt = attempts[decision.attempt_id]
  100. validation = validations[decision.validation_id]
  101. artifact = await client.get_artifact_snapshot(
  102. root_trace_id, attempt.snapshot_id
  103. )
  104. assert attempt.task_id == decision.task_id
  105. assert validation.task_id == decision.task_id
  106. assert validation.attempt_id == attempt.attempt_id
  107. assert validation.snapshot_id == artifact.snapshot_id
  108. assert artifact.attempt_id == attempt.attempt_id
  109. assert attempt.worker_trace_id in trace_ids
  110. assert validation.validator_trace_id in trace_ids
  111. root_attempt = attempts[next(
  112. decision.attempt_id
  113. for decision in accepted
  114. if decision.task_id == mission.root_task_id
  115. )]
  116. child_accept = next(
  117. decision for decision in accepted
  118. if decision.task_id != mission.root_task_id
  119. )
  120. assert root_attempt.accepted_child_decision_ids == [child_accept.decision_id]
  121. forbidden = {"script", "workflow", "round", "branch", "page_nodes"}
  122. assert forbidden.isdisjoint(_all_keys(mission.model_dump(mode="json")))
  123. await http.aclose()