test_legacy_compatibility.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. from types import SimpleNamespace
  2. import pytest
  3. from agent.core.runner import AgentRunner, RunConfig
  4. from agent.orchestration.models import CompletionPolicy
  5. from agent.tools.builtin.knowledge import KnowledgeConfig
  6. from agent.tools.builtin.subagent import agent as legacy_agent
  7. from agent.tools.builtin.subagent import evaluate as legacy_evaluate
  8. from agent.tools.registry import ToolRegistry
  9. from agent.trace.goal_models import GoalTree
  10. from agent.trace.models import Trace
  11. from agent.trace.store import FileSystemTraceStore
  12. def test_run_config_defaults_to_legacy_auto():
  13. assert RunConfig().completion_policy == CompletionPolicy.LEGACY_AUTO
  14. def test_old_trace_without_agent_role_reads_as_legacy():
  15. original = Trace(trace_id="old", mode="agent").to_dict()
  16. original.pop("agent_role")
  17. trace = Trace.from_dict(original)
  18. assert trace.agent_role == "legacy"
  19. def test_legacy_schema_keeps_agent_evaluate_goal_but_hides_explicit_tools():
  20. runner = AgentRunner(llm_call=lambda **_: None)
  21. schemas = runner._get_run_tool_schemas(RunConfig(tool_groups=["core"]))
  22. names = {schema["function"]["name"] for schema in schemas}
  23. assert {"agent", "evaluate", "goal"}.issubset(names)
  24. assert names.isdisjoint({
  25. "task_plan", "dispatch_tasks", "task_decide", "validate_attempt",
  26. "submit_attempt", "submit_validation",
  27. })
  28. def test_legacy_tools_and_groups_keep_union_semantics():
  29. runner = AgentRunner(llm_call=lambda **_: None)
  30. schemas = runner._get_tool_schemas(tools=["read_file"], tool_groups=["core"])
  31. names = {schema["function"]["name"] for schema in schemas}
  32. assert "read_file" in names
  33. assert "agent" in names
  34. class FakeLegacyRunner:
  35. def __init__(self, store):
  36. self.trace_store = store
  37. self.tools = ToolRegistry()
  38. self.config = SimpleNamespace(parallel_tool_execution=True)
  39. self.debug = False
  40. self.calls = []
  41. async def run_result(self, messages, config, on_event=None):
  42. self.calls.append((messages, config))
  43. await self.trace_store.update_trace(
  44. config.trace_id,
  45. status="completed",
  46. result_summary=f"result-{len(self.calls)}",
  47. )
  48. return {
  49. "status": "completed",
  50. "summary": f"result-{len(self.calls)}",
  51. "trace_id": config.trace_id,
  52. "stats": {"total_messages": 1, "total_tokens": 2, "total_cost": 0.0},
  53. "saved_knowledge_ids": [],
  54. }
  55. async def _legacy_context(tmp_path):
  56. store = FileSystemTraceStore(str(tmp_path))
  57. await store.create_trace(Trace(trace_id="root", mode="agent", task="legacy mission"))
  58. tree = GoalTree(mission="legacy mission")
  59. goal = tree.add_goals(["legacy goal"])[0]
  60. tree.focus(goal.id)
  61. await store.update_goal_tree("root", tree)
  62. runner = FakeLegacyRunner(store)
  63. context = {
  64. "store": store,
  65. "trace_id": "root",
  66. "goal_id": goal.id,
  67. "runner": runner,
  68. "knowledge_config": _disabled_knowledge(),
  69. }
  70. return store, runner, context
  71. @pytest.mark.asyncio
  72. async def test_legacy_agent_single_and_continue_from_reuse_trace(tmp_path, monkeypatch):
  73. import agent.tools.builtin.subagent as subagent_module
  74. async def noop(*args, **kwargs):
  75. return None
  76. monkeypatch.setattr(subagent_module, "broadcast_sub_trace_started", noop)
  77. monkeypatch.setattr(subagent_module, "broadcast_sub_trace_completed", noop)
  78. store, runner, context = await _legacy_context(tmp_path)
  79. first = await legacy_agent(task="first legacy task", context=context)
  80. assert first["mode"] == "delegate"
  81. assert first["status"] == "completed"
  82. sub_trace_id = first["sub_trace_id"]
  83. sub_trace = await store.get_trace(sub_trace_id)
  84. assert sub_trace.parent_trace_id == "root"
  85. assert runner.calls[-1][1].completion_policy == CompletionPolicy.LEGACY_AUTO
  86. second = await legacy_agent(
  87. task="repair legacy task",
  88. continue_from=sub_trace_id,
  89. context=context,
  90. )
  91. assert second["sub_trace_id"] == sub_trace_id
  92. assert second["continue_from"] is True
  93. assert runner.calls[-1][1].trace_id == sub_trace_id
  94. @pytest.mark.asyncio
  95. async def test_legacy_multi_agent_keeps_parallel_result_shape(tmp_path, monkeypatch):
  96. import agent.tools.builtin.subagent as subagent_module
  97. async def noop(*args, **kwargs):
  98. return None
  99. monkeypatch.setattr(subagent_module, "broadcast_sub_trace_started", noop)
  100. monkeypatch.setattr(subagent_module, "broadcast_sub_trace_completed", noop)
  101. store, runner, context = await _legacy_context(tmp_path)
  102. result = await legacy_agent(task=["branch a", "branch b"], context=context)
  103. assert result["mode"] == "explore"
  104. assert result["status"] == "completed"
  105. assert len(result["sub_trace_ids"]) == 2
  106. assert len({item["trace_id"] for item in result["sub_trace_ids"]}) == 2
  107. assert all(call[1].agent_type == "explore" for call in runner.calls)
  108. traces = [await store.get_trace(item["trace_id"]) for item in result["sub_trace_ids"]]
  109. assert all(trace.parent_trace_id == "root" for trace in traces)
  110. @pytest.mark.asyncio
  111. async def test_legacy_evaluate_runs_real_tool_and_preserves_result_format(tmp_path, monkeypatch):
  112. import agent.tools.builtin.subagent as subagent_module
  113. async def noop(*args, **kwargs):
  114. return None
  115. monkeypatch.setattr(subagent_module, "broadcast_sub_trace_started", noop)
  116. monkeypatch.setattr(subagent_module, "broadcast_sub_trace_completed", noop)
  117. store, runner, context = await _legacy_context(tmp_path)
  118. result = await legacy_evaluate(
  119. messages=[{"role": "user", "content": "legacy output"}],
  120. context=context,
  121. )
  122. assert result["mode"] == "evaluate"
  123. assert result["status"] == "completed"
  124. assert result["summary"].startswith("result-")
  125. assert runner.calls[-1][1].agent_type == "evaluate"
  126. assert (await store.get_trace(result["sub_trace_id"])).parent_trace_id == "root"
  127. @pytest.mark.asyncio
  128. async def test_old_trace_can_continue_with_real_runner(tmp_path):
  129. calls = 0
  130. async def fake_llm(**kwargs):
  131. nonlocal calls
  132. calls += 1
  133. return {"content": f"legacy answer {calls}", "tool_calls": [], "finish_reason": "stop"}
  134. store = FileSystemTraceStore(str(tmp_path))
  135. runner = AgentRunner(trace_store=store, llm_call=fake_llm)
  136. first = await runner.run_result(
  137. [{"role": "user", "content": "first"}],
  138. RunConfig(name="legacy", knowledge=_disabled_knowledge()),
  139. )
  140. calls_after_first = calls
  141. second = await runner.run_result(
  142. [{"role": "user", "content": "continue"}],
  143. RunConfig(trace_id=first["trace_id"], knowledge=_disabled_knowledge()),
  144. )
  145. assert second["trace_id"] == first["trace_id"]
  146. assert second["status"] == "completed"
  147. assert calls > calls_after_first
  148. assert (await store.get_trace(first["trace_id"])).agent_role == "legacy"
  149. def _disabled_knowledge():
  150. return KnowledgeConfig(
  151. enable_extraction=False,
  152. enable_completion_extraction=False,
  153. enable_injection=False,
  154. )