test_runner_components.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import pytest
  2. from agent import AgentRunner as PublicAgentRunner
  3. from agent import RunConfig as PublicRunConfig
  4. from agent.core.runner import AgentRunner, RunConfig
  5. from agent.tools.registry import ToolRegistry
  6. from agent.trace.models import Trace
  7. def test_runner_public_imports_remain_stable():
  8. assert PublicAgentRunner is AgentRunner
  9. assert PublicRunConfig is RunConfig
  10. @pytest.mark.asyncio
  11. async def test_prompt_runtime_preserves_host_prompt_and_iteration_constraint(tmp_path):
  12. runner = AgentRunner(skills_dir=str(tmp_path))
  13. prompt = await runner._build_system_prompt(
  14. RunConfig(max_iterations=7),
  15. base_prompt="host contract",
  16. )
  17. assert prompt.startswith("host contract\n\n## Execution Constraint")
  18. assert "最多可以用 7 轮交互" in prompt
  19. @pytest.mark.asyncio
  20. async def test_task_name_runtime_uses_current_utility_model_callback():
  21. runner = AgentRunner()
  22. async def utility_llm_call(**_kwargs):
  23. return {"content": "stable title"}
  24. # Preserve the historical ability to configure this dependency after
  25. # construction; the internal component must not capture a stale value.
  26. runner.utility_llm_call = utility_llm_call
  27. assert (
  28. await runner._generate_task_name([{"role": "user", "content": "task"}])
  29. == "stable title"
  30. )
  31. @pytest.mark.asyncio
  32. async def test_task_name_runtime_keeps_text_fallback_on_utility_failure():
  33. async def failing_utility(**_kwargs):
  34. raise RuntimeError("unavailable")
  35. runner = AgentRunner(utility_llm_call=failing_utility)
  36. text = "x" * 60
  37. assert await runner._generate_task_name([{"role": "user", "content": text}]) == (
  38. "x" * 50 + "..."
  39. )
  40. @pytest.mark.asyncio
  41. async def test_tool_runtime_keeps_runner_override_hooks() -> None:
  42. registry = ToolRegistry()
  43. async def sample() -> str:
  44. return "ok"
  45. registry.register(
  46. sample,
  47. schema={
  48. "type": "function",
  49. "function": {
  50. "name": "sample",
  51. "description": "sample",
  52. "parameters": {"type": "object", "properties": {}},
  53. },
  54. },
  55. )
  56. class ExtensibleRunner(AgentRunner):
  57. policy_calls = 0
  58. context_calls = 0
  59. schema_calls = 0
  60. def _resolve_run_policy(self, config, trace=None):
  61. self.policy_calls += 1
  62. return super()._resolve_run_policy(config, trace)
  63. def _build_protected_tool_context(
  64. self,
  65. config,
  66. trace,
  67. goal_tree,
  68. sequence,
  69. side_branch=None,
  70. ):
  71. self.context_calls += 1
  72. return super()._build_protected_tool_context(
  73. config,
  74. trace,
  75. goal_tree,
  76. sequence,
  77. side_branch=side_branch,
  78. )
  79. def _get_tool_schemas(
  80. self,
  81. tools=None,
  82. tool_groups=None,
  83. exclude_tools=None,
  84. ):
  85. self.schema_calls += 1
  86. return super()._get_tool_schemas(
  87. tools,
  88. tool_groups,
  89. exclude_tools,
  90. )
  91. runner = ExtensibleRunner(tool_registry=registry)
  92. runner._get_run_tool_schemas(RunConfig(tools=["sample"], tool_groups=[]))
  93. result = await runner._execute_authorized_tool(
  94. "sample",
  95. {},
  96. "call-1",
  97. RunConfig(tools=["sample"], tool_groups=[]),
  98. Trace(trace_id="legacy", mode="agent", agent_role="legacy"),
  99. None,
  100. 1,
  101. )
  102. assert result == "ok"
  103. assert runner.policy_calls == 2
  104. assert runner.context_calls == 1
  105. assert runner.schema_calls == 1
  106. @pytest.mark.asyncio
  107. async def test_image_runtime_keeps_runner_override_hooks() -> None:
  108. class ExtensibleRunner(AgentRunner):
  109. image_calls = 0
  110. async def _process_image_size(
  111. self,
  112. base64_url,
  113. max_size=512,
  114. min_size=11,
  115. ):
  116. self.image_calls += 1
  117. return "data:image/jpeg;base64,processed"
  118. runner = ExtensibleRunner()
  119. messages = [
  120. {
  121. "role": "tool",
  122. "content": [
  123. {
  124. "type": "image_url",
  125. "image_url": {"url": "data:image/png;base64,source"},
  126. }
  127. ],
  128. },
  129. {"role": "assistant", "content": "observed"},
  130. ]
  131. optimized = await runner._optimize_images(messages, "gpt-4o")
  132. assert runner.image_calls == 1
  133. assert optimized[0]["content"][0]["image_url"]["url"].endswith("processed")
  134. assert messages[0]["content"][0]["image_url"]["url"].endswith("source")