test_agent_mode.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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. "AGENT_MAX_VALIDATION_TOOL_CALLS": "not-a-number",
  66. "AGENT_VALIDATOR_SEARCH_PROVIDER": "unsupported",
  67. },
  68. clear=False,
  69. ):
  70. trace, _, _ = await self._prepare_new("legacy")
  71. self.assertEqual("legacy", trace.context["agent_mode"])
  72. self.assertNotIn("resource_budget", trace.context)
  73. async def test_recursive_rejects_invalid_validator_provider_before_trace(self):
  74. with patch.dict(
  75. os.environ,
  76. {
  77. "AGENT_MODE": "recursive",
  78. "AGENT_VALIDATOR_SEARCH_PROVIDER": "unsupported",
  79. },
  80. clear=False,
  81. ):
  82. with self.assertRaisesRegex(ValueError, "search_provider"):
  83. await self._prepare_new("recursive")
  84. self.assertEqual([], await self.store.list_traces(limit=10))
  85. async def test_invalid_mode_does_not_create_trace(self):
  86. with self.assertRaisesRegex(ValueError, "Invalid AGENT_MODE"):
  87. await self._prepare_new("unsupported")
  88. traces = await self.store.list_traces(limit=10)
  89. self.assertEqual([], traces)
  90. async def test_invalid_mode_is_rejected_before_task_name_generation(self):
  91. os.environ["AGENT_MODE"] = "unsupported"
  92. with patch.object(
  93. self.runner,
  94. "_generate_task_name",
  95. new=AsyncMock(return_value="should not run"),
  96. ) as generate_name:
  97. with self.assertRaisesRegex(ValueError, "Invalid AGENT_MODE"):
  98. await self.runner._prepare_trace(
  99. [{"role": "user", "content": "test"}],
  100. RunConfig(),
  101. )
  102. generate_name.assert_not_awaited()
  103. async def test_removed_recursion_variable_fails_explicitly(self):
  104. os.environ["AGENT_RECURSION_ENABLED"] = "true"
  105. with self.assertRaisesRegex(ValueError, "has been removed"):
  106. assert_removed_config_absent()
  107. with self.assertRaisesRegex(ValueError, "AGENT_MODE"):
  108. await self._prepare_new("recursive")
  109. async def test_existing_trace_keeps_persisted_mode_after_env_switch(self):
  110. trace, _, _ = await self._prepare_new("recursive")
  111. self.assertEqual(2, trace.context["agent_mode_revision"])
  112. self.assertIn("task_protocol", trace.context)
  113. os.environ["AGENT_MODE"] = "legacy"
  114. continued, _, _ = await self.runner._prepare_trace(
  115. [],
  116. RunConfig(trace_id=trace.trace_id, name="continue"),
  117. )
  118. self.assertEqual("recursive", continued.context["agent_mode"])
  119. persisted = await self.store.get_trace(trace.trace_id)
  120. self.assertEqual("recursive", persisted.context["agent_mode"])
  121. async def test_historical_trace_is_persisted_as_legacy_on_continue(self):
  122. trace = Trace(trace_id="historical", mode="agent", task="old", context={})
  123. await self.store.create_trace(trace)
  124. os.environ["AGENT_MODE"] = "recursive"
  125. continued, _, _ = await self.runner._prepare_trace(
  126. [],
  127. RunConfig(trace_id=trace.trace_id, name="continue"),
  128. )
  129. self.assertEqual("legacy", continued.context["agent_mode"])
  130. self.assertIs(AgentMode.LEGACY, policy_from_context(continued.context).mode)
  131. async def test_legacy_goal_tree_keeps_automatic_sibling_and_parent_progress(self):
  132. tree = GoalTree(mission="legacy goals")
  133. parent = tree.add_goals(["parent"])[0]
  134. first, second = tree.add_goals(["first", "second"], parent_id=parent.id)
  135. tree.focus(parent.id)
  136. tree.focus(first.id)
  137. tree.complete(first.id, "done")
  138. self.assertEqual(second.id, tree.current_id)
  139. self.assertEqual("in_progress", parent.status)
  140. tree.complete(second.id, "done")
  141. self.assertEqual("completed", parent.status)
  142. self.assertEqual("所有子目标已完成", parent.summary)
  143. if __name__ == "__main__":
  144. unittest.main()