test_agent_mode.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import os
  2. import tempfile
  3. import unittest
  4. from unittest.mock import AsyncMock, patch
  5. from cyber_agent.core.agent_mode import (
  6. AgentMode,
  7. assert_removed_config_absent,
  8. policy_from_context,
  9. policy_from_environment,
  10. )
  11. from cyber_agent.core.runner import AgentRunner, RunConfig
  12. from cyber_agent.trace.goal_models import GoalTree
  13. from cyber_agent.trace.models import Trace
  14. from cyber_agent.trace.store import FileSystemTraceStore
  15. class AgentModePolicyTest(unittest.IsolatedAsyncioTestCase):
  16. async def asyncSetUp(self):
  17. self.temp_dir = tempfile.TemporaryDirectory()
  18. self.store = FileSystemTraceStore(self.temp_dir.name)
  19. self.runner = AgentRunner(trace_store=self.store)
  20. self.previous = {
  21. name: os.environ.get(name)
  22. for name in ("AGENT_MODE", "AGENT_RECURSION_ENABLED")
  23. }
  24. os.environ.pop("AGENT_MODE", None)
  25. os.environ.pop("AGENT_RECURSION_ENABLED", None)
  26. async def asyncTearDown(self):
  27. for name, value in self.previous.items():
  28. if value is None:
  29. os.environ.pop(name, None)
  30. else:
  31. os.environ[name] = value
  32. self.temp_dir.cleanup()
  33. async def _prepare_new(self, mode=None):
  34. if mode is None:
  35. os.environ.pop("AGENT_MODE", None)
  36. else:
  37. os.environ["AGENT_MODE"] = mode
  38. return await self.runner._prepare_trace(
  39. [{"role": "user", "content": "test"}],
  40. RunConfig(
  41. name="mode test",
  42. root_task_anchor={
  43. "objective": "return a checked answer",
  44. "completion_criteria": ["return a checked answer"],
  45. },
  46. ),
  47. )
  48. async def test_unset_defaults_to_legacy(self):
  49. trace, _, _ = await self._prepare_new()
  50. self.assertEqual("legacy", trace.context["agent_mode"])
  51. self.assertEqual(1, trace.context["agent_mode_revision"])
  52. self.assertEqual(0, trace.context["agent_depth"])
  53. self.assertEqual(trace.trace_id, trace.context["root_trace_id"])
  54. async def test_legacy_and_recursive_are_parsed_explicitly(self):
  55. self.assertIs(AgentMode.LEGACY, policy_from_environment().mode)
  56. os.environ["AGENT_MODE"] = "recursive"
  57. self.assertIs(AgentMode.RECURSIVE, policy_from_environment().mode)
  58. async def test_legacy_ignores_invalid_recursive_budget_environment(self):
  59. with patch.dict(
  60. os.environ,
  61. {
  62. "AGENT_MODE": "legacy",
  63. "AGENT_MAX_TOTAL_AGENTS": "not-a-number",
  64. "AGENT_RESOURCE_BUDGET_ENABLED": "not-a-boolean",
  65. },
  66. clear=False,
  67. ):
  68. trace, _, _ = await self._prepare_new("legacy")
  69. self.assertEqual("legacy", trace.context["agent_mode"])
  70. self.assertNotIn("resource_budget", trace.context)
  71. async def test_invalid_mode_does_not_create_trace(self):
  72. with self.assertRaisesRegex(ValueError, "Invalid AGENT_MODE"):
  73. await self._prepare_new("unsupported")
  74. traces = await self.store.list_traces(limit=10)
  75. self.assertEqual([], traces)
  76. async def test_invalid_mode_is_rejected_before_task_name_generation(self):
  77. os.environ["AGENT_MODE"] = "unsupported"
  78. with patch.object(
  79. self.runner,
  80. "_generate_task_name",
  81. new=AsyncMock(return_value="should not run"),
  82. ) as generate_name:
  83. with self.assertRaisesRegex(ValueError, "Invalid AGENT_MODE"):
  84. await self.runner._prepare_trace(
  85. [{"role": "user", "content": "test"}],
  86. RunConfig(),
  87. )
  88. generate_name.assert_not_awaited()
  89. async def test_removed_recursion_variable_fails_explicitly(self):
  90. os.environ["AGENT_RECURSION_ENABLED"] = "true"
  91. with self.assertRaisesRegex(ValueError, "has been removed"):
  92. assert_removed_config_absent()
  93. with self.assertRaisesRegex(ValueError, "AGENT_MODE"):
  94. await self._prepare_new("recursive")
  95. async def test_existing_trace_keeps_persisted_mode_after_env_switch(self):
  96. trace, _, _ = await self._prepare_new("recursive")
  97. self.assertEqual(2, trace.context["agent_mode_revision"])
  98. self.assertIn("task_protocol", trace.context)
  99. os.environ["AGENT_MODE"] = "legacy"
  100. continued, _, _ = await self.runner._prepare_trace(
  101. [],
  102. RunConfig(trace_id=trace.trace_id, name="continue"),
  103. )
  104. self.assertEqual("recursive", continued.context["agent_mode"])
  105. persisted = await self.store.get_trace(trace.trace_id)
  106. self.assertEqual("recursive", persisted.context["agent_mode"])
  107. async def test_historical_trace_is_persisted_as_legacy_on_continue(self):
  108. trace = Trace(trace_id="historical", mode="agent", task="old", context={})
  109. await self.store.create_trace(trace)
  110. os.environ["AGENT_MODE"] = "recursive"
  111. continued, _, _ = await self.runner._prepare_trace(
  112. [],
  113. RunConfig(trace_id=trace.trace_id, name="continue"),
  114. )
  115. self.assertEqual("legacy", continued.context["agent_mode"])
  116. self.assertIs(AgentMode.LEGACY, policy_from_context(continued.context).mode)
  117. async def test_legacy_goal_tree_keeps_automatic_sibling_and_parent_progress(self):
  118. tree = GoalTree(mission="legacy goals")
  119. parent = tree.add_goals(["parent"])[0]
  120. first, second = tree.add_goals(["first", "second"], parent_id=parent.id)
  121. tree.focus(parent.id)
  122. tree.focus(first.id)
  123. tree.complete(first.id, "done")
  124. self.assertEqual(second.id, tree.current_id)
  125. self.assertEqual("in_progress", parent.status)
  126. tree.complete(second.id, "done")
  127. self.assertEqual("completed", parent.status)
  128. self.assertEqual("所有子目标已完成", parent.summary)
  129. if __name__ == "__main__":
  130. unittest.main()