| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907 |
- import asyncio
- import json
- import os
- import tempfile
- import unittest
- from unittest.mock import patch
- from cyber_agent.core.agent_mode import RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY
- from cyber_agent.core.resource_budget import (
- RESOURCE_BUDGET_CONTEXT_KEY,
- ResourceBudget,
- ResourceBudgetController,
- )
- from cyber_agent.core.runner import AgentRunner, RunConfig
- from cyber_agent.core.task_protocol import ensure_task_protocol, new_task_protocol
- from cyber_agent.tools import get_tool_registry
- from cyber_agent.tools.builtin.knowledge import KnowledgeConfig
- from cyber_agent.tools.builtin.subagent import agent
- from cyber_agent.tools.registry import ToolRegistry
- from cyber_agent.trace.models import Trace
- from cyber_agent.trace.store import FileSystemTraceStore
- ROOT_CRITERIA = ["return a checked final answer"]
- CHILD_BRIEF = {
- "objective": "check one concrete fact",
- "reason": "the root needs independently checked evidence",
- "completion_criteria": ["return one supported conclusion"],
- "expected_outputs": ["one checked conclusion"],
- "constraints": ["do not guess"],
- }
- def knowledge_disabled():
- return KnowledgeConfig(
- enable_extraction=False,
- enable_completion_extraction=False,
- enable_injection=False,
- )
- def recursive_env(**overrides):
- values = {
- "AGENT_MODE": "recursive",
- "AGENT_RESOURCE_BUDGET_ENABLED": "true",
- "AGENT_MAX_TOTAL_AGENTS": "50",
- "AGENT_MAX_LLM_CALLS": "150",
- "AGENT_MAX_TOTAL_TOKENS": "1500000",
- "AGENT_MAX_TOTAL_COST_USD": "15",
- "AGENT_MAX_DURATION_SECONDS": "3600",
- "AGENT_RESERVED_FINAL_CALLS": "1",
- }
- values.update(overrides)
- return patch.dict(os.environ, values, clear=False)
- def tool_names(schemas):
- return {
- schema["function"]["name"]
- for schema in schemas or []
- }
- def is_validator_call(messages):
- return any(
- message.get("role") == "system"
- and "independent validator" in str(message.get("content", ""))
- for message in messages
- )
- def validator_response(messages, outcome="passed"):
- packet = json.loads(messages[-1]["content"])
- scope = packet["validation_scope"]
- passed = outcome == "passed"
- return {
- "content": json.dumps({
- "outcome": outcome,
- "scope": scope,
- "reason": "the persisted record was checked",
- "issues": [] if passed else ["the answer lacks required support"],
- "retry_from": None if passed else "evidence",
- }),
- "tool_calls": [],
- "finish_reason": "stop",
- "prompt_tokens": 3,
- "completion_tokens": 2,
- "cost": 0.001,
- }
- class RootValidationIntegrationTest(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.temp_dir = tempfile.TemporaryDirectory()
- self.store = FileSystemTraceStore(self.temp_dir.name)
- async def asyncTearDown(self):
- self.temp_dir.cleanup()
- def config(self, **overrides):
- values = {
- "tools": [],
- "tool_groups": [],
- "enable_research_flow": False,
- "root_completion_criteria": ROOT_CRITERIA,
- "knowledge": knowledge_disabled(),
- }
- values.update(overrides)
- return RunConfig(**values)
- async def test_root_completes_only_after_independent_validator_passes(self):
- calls = []
- async def llm_call(**kwargs):
- calls.append(kwargs)
- if is_validator_call(kwargs["messages"]):
- return validator_response(kwargs["messages"])
- return {
- "content": "checked final answer",
- "tool_calls": [],
- "finish_reason": "stop",
- "prompt_tokens": 5,
- "completion_tokens": 4,
- "cost": 0.002,
- }
- runner = AgentRunner(trace_store=self.store, llm_call=llm_call)
- with recursive_env():
- result = await runner.run_result(
- messages=[{"role": "user", "content": "answer carefully"}],
- config=self.config(),
- )
- self.assertEqual("completed", result["status"])
- root = await self.store.get_trace(result["trace_id"])
- state = ensure_task_protocol(root.context)
- self.assertTrue(state["root_validation_passed"])
- self.assertEqual(1, state["root_validation_attempts"])
- validators = await self.store.list_traces(
- parent_trace_id=root.trace_id,
- created_by_tool="validator",
- limit=10,
- )
- self.assertEqual(1, len(validators))
- self.assertEqual(root.trace_id, validators[0].context["root_trace_id"])
- self.assertEqual([], validators[0].tools)
- usage = await ResourceBudgetController(self.store).get_usage(root.trace_id)
- self.assertEqual(1, usage.total_agents)
- self.assertEqual(2, usage.llm_calls)
- self.assertEqual(14, usage.total_tokens)
- self.assertEqual(2, len(calls))
- async def test_root_gets_one_revision_then_fails_on_second_rejection(self):
- ordinary_calls = 0
- validation_calls = 0
- async def llm_call(**kwargs):
- nonlocal ordinary_calls, validation_calls
- if is_validator_call(kwargs["messages"]):
- validation_calls += 1
- return validator_response(kwargs["messages"], outcome="failed")
- ordinary_calls += 1
- return {
- "content": f"candidate-{ordinary_calls}",
- "tool_calls": [],
- "finish_reason": "stop",
- }
- runner = AgentRunner(trace_store=self.store, llm_call=llm_call)
- with recursive_env():
- result = await runner.run_result(
- messages=[{"role": "user", "content": "answer carefully"}],
- config=self.config(),
- )
- self.assertEqual("failed", result["status"])
- self.assertEqual(2, ordinary_calls)
- self.assertEqual(2, validation_calls)
- root = await self.store.get_trace(result["trace_id"])
- state = ensure_task_protocol(root.context)
- self.assertEqual(2, state["root_validation_attempts"])
- self.assertEqual(
- ["failed", "failed"],
- [item["outcome"] for item in state["root_validation_history"]],
- )
- self.assertFalse(state["root_validation_passed"])
- async def test_root_can_complete_after_one_validator_requested_revision(self):
- ordinary_calls = 0
- validation_outcomes = iter(["failed", "passed"])
- second_candidate_messages = None
- async def llm_call(**kwargs):
- nonlocal ordinary_calls, second_candidate_messages
- if is_validator_call(kwargs["messages"]):
- return validator_response(
- kwargs["messages"],
- outcome=next(validation_outcomes),
- )
- ordinary_calls += 1
- if ordinary_calls == 2:
- second_candidate_messages = kwargs["messages"]
- return {
- "content": f"candidate-{ordinary_calls}",
- "tool_calls": [],
- "finish_reason": "stop",
- }
- runner = AgentRunner(trace_store=self.store, llm_call=llm_call)
- with recursive_env():
- result = await runner.run_result(
- messages=[{"role": "user", "content": "revise if needed"}],
- config=self.config(),
- )
- self.assertEqual("completed", result["status"])
- root = await self.store.get_trace(result["trace_id"])
- state = ensure_task_protocol(root.context)
- self.assertEqual(2, state["root_validation_attempts"])
- self.assertEqual(
- ["failed", "passed"],
- [item["outcome"] for item in state["root_validation_history"]],
- )
- self.assertTrue(any(
- message.get("role") == "user"
- and "framework-owned ValidationResult" in str(message.get("content"))
- for message in second_candidate_messages
- ))
- persisted_messages = await self.store.get_trace_messages(root.trace_id)
- self.assertTrue(any(
- message.role == "user"
- and "framework-owned ValidationResult" in str(message.content)
- for message in persisted_messages
- ))
- calls_after_completion = 0
- async def must_not_call(**_kwargs):
- nonlocal calls_after_completion
- calls_after_completion += 1
- raise AssertionError("a third validation attempt must not start")
- resumed = AgentRunner(trace_store=self.store, llm_call=must_not_call)
- with recursive_env():
- with self.assertRaisesRegex(ValueError, "two independent"):
- await resumed.run_result(
- [],
- self.config(trace_id=root.trace_id),
- )
- self.assertEqual(0, calls_after_completion)
- async def test_root_validator_reads_current_tool_result_and_candidate(self):
- registry = ToolRegistry()
- async def evidence_tool():
- return {"verified": "persisted-tool-result"}
- registry.register(evidence_tool, groups=["test"])
- ordinary_calls = 0
- validator_packet = None
- async def llm_call(**kwargs):
- nonlocal ordinary_calls, validator_packet
- if is_validator_call(kwargs["messages"]):
- validator_packet = json.loads(kwargs["messages"][-1]["content"])
- return validator_response(kwargs["messages"])
- ordinary_calls += 1
- if ordinary_calls == 1:
- return {
- "content": "",
- "tool_calls": [{
- "id": "evidence-call",
- "type": "function",
- "function": {
- "name": "evidence_tool",
- "arguments": "{}",
- },
- }],
- "finish_reason": "tool_calls",
- }
- return {
- "content": "candidate-from-real-tool-result",
- "tool_calls": [],
- "finish_reason": "stop",
- }
- runner = AgentRunner(
- trace_store=self.store,
- tool_registry=registry,
- llm_call=llm_call,
- )
- with recursive_env():
- result = await runner.run_result(
- [{"role": "user", "content": "use the evidence tool"}],
- self.config(tools=["evidence_tool"]),
- )
- self.assertEqual("completed", result["status"])
- trajectory = validator_packet["trajectory"]
- self.assertTrue(any(
- item.get("role") == "tool"
- and "persisted-tool-result" in str(item.get("content"))
- for item in trajectory
- ))
- self.assertTrue(any(
- item.get("role") == "assistant"
- and "candidate-from-real-tool-result" in str(item.get("content"))
- for item in trajectory
- ))
- async def test_new_recursive_root_requires_explicit_completion_criteria(self):
- async def llm_call(**_kwargs):
- raise AssertionError("LLM must not be called")
- runner = AgentRunner(trace_store=self.store, llm_call=llm_call)
- with recursive_env():
- with self.assertRaisesRegex(ValueError, "root_completion_criteria"):
- await runner.run_result(
- messages=[{"role": "user", "content": "missing criteria"}],
- config=self.config(root_completion_criteria=[]),
- )
- self.assertEqual([], await self.store.list_traces(limit=10))
- async def test_old_recursive_revision_two_trace_is_not_silently_migrated(self):
- trace_id = "old-recursive-experiment"
- await self.store.create_trace(Trace(
- trace_id=trace_id,
- mode="agent",
- context={
- "agent_mode": "recursive",
- "agent_mode_revision": 2,
- "agent_depth": 0,
- "root_trace_id": trace_id,
- "task_protocol": {},
- },
- ))
- runner = AgentRunner(trace_store=self.store, llm_call=lambda **_: None)
- with self.assertRaisesRegex(ValueError, "create a new trace"):
- await runner.run_result([], RunConfig(trace_id=trace_id))
- async def test_validator_trace_cannot_be_continued_as_an_agent(self):
- validator_id = "root@validator-fixture"
- await self.store.create_trace(Trace(
- trace_id=validator_id,
- mode="agent",
- agent_type="validator",
- tools=[],
- status="completed",
- context={
- "created_by_tool": "validator",
- "agent_mode": "recursive",
- "agent_mode_revision": 2,
- "root_trace_id": "root",
- },
- ))
- calls = 0
- async def llm_call(**_kwargs):
- nonlocal calls
- calls += 1
- return {"content": "must not run", "tool_calls": []}
- runner = AgentRunner(trace_store=self.store, llm_call=llm_call)
- with self.assertRaisesRegex(ValueError, "cannot be continued"):
- await runner.run_result(
- [{"role": "user", "content": "gain tools"}],
- RunConfig(trace_id=validator_id),
- )
- self.assertEqual(0, calls)
- validator = await self.store.get_trace(validator_id)
- self.assertEqual("completed", validator.status)
- self.assertEqual([], validator.tools)
- class RecursiveBudgetIntegrationTest(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.temp_dir = tempfile.TemporaryDirectory()
- self.store = FileSystemTraceStore(self.temp_dir.name)
- async def asyncTearDown(self):
- self.temp_dir.cleanup()
- async def test_response_over_token_budget_never_executes_its_tool_call(self):
- dangerous_calls = 0
- registry = ToolRegistry()
- async def dangerous():
- nonlocal dangerous_calls
- dangerous_calls += 1
- return "must not run"
- registry.register(dangerous, groups=["test"])
- async def llm_call(**_kwargs):
- return {
- "content": "over-budget response",
- "tool_calls": [{
- "id": "dangerous-call",
- "type": "function",
- "function": {"name": "dangerous", "arguments": "{}"},
- }],
- "finish_reason": "tool_calls",
- "prompt_tokens": 8,
- "completion_tokens": 8,
- }
- runner = AgentRunner(
- trace_store=self.store,
- tool_registry=registry,
- llm_call=llm_call,
- )
- config = RunConfig(
- tools=["dangerous"],
- tool_groups=[],
- enable_research_flow=False,
- root_completion_criteria=ROOT_CRITERIA,
- knowledge=knowledge_disabled(),
- )
- with recursive_env(AGENT_MAX_TOTAL_TOKENS="10"):
- result = await runner.run_result(
- [{"role": "user", "content": "do not overspend"}],
- config,
- )
- self.assertEqual("failed", result["status"])
- self.assertEqual(0, dangerous_calls)
- root = await self.store.get_trace(result["trace_id"])
- self.assertEqual("budget_exhausted:tokens", root.context["termination_reason"])
- usage = await ResourceBudgetController(self.store).get_usage(root.trace_id)
- self.assertEqual(16, usage.total_tokens)
- self.assertEqual("budget_exhausted:tokens", usage.exhausted_reason)
- assistant = [
- message
- for message in await self.store.get_trace_messages(root.trace_id)
- if message.role == "assistant"
- ]
- self.assertIsNone(assistant[-1].content["tool_calls"])
- async def test_agent_budget_denial_blocks_child_but_preserves_root_validator(self):
- ordinary_calls = 0
- async def llm_call(**kwargs):
- nonlocal ordinary_calls
- if is_validator_call(kwargs["messages"]):
- return validator_response(kwargs["messages"])
- ordinary_calls += 1
- if ordinary_calls == 1:
- return {
- "content": "",
- "tool_calls": [{
- "id": "delegate",
- "type": "function",
- "function": {
- "name": "agent",
- "arguments": json.dumps({"task_brief": CHILD_BRIEF}),
- },
- }],
- "finish_reason": "tool_calls",
- }
- return {
- "content": "root can still finish without a child",
- "tool_calls": [],
- "finish_reason": "stop",
- }
- runner = AgentRunner(
- trace_store=self.store,
- tool_registry=get_tool_registry(),
- llm_call=llm_call,
- )
- config = RunConfig(
- tools=["agent"],
- tool_groups=[],
- enable_research_flow=False,
- root_completion_criteria=ROOT_CRITERIA,
- knowledge=knowledge_disabled(),
- )
- with recursive_env(AGENT_MAX_TOTAL_AGENTS="1"):
- result = await runner.run_result(
- [{"role": "user", "content": "try one delegated check"}],
- config,
- )
- self.assertEqual("completed", result["status"])
- root = await self.store.get_trace(result["trace_id"])
- children = await self.store.list_traces(
- parent_trace_id=root.trace_id,
- created_by_tool="agent",
- limit=10,
- )
- self.assertEqual([], children)
- usage = await ResourceBudgetController(self.store).get_usage(root.trace_id)
- self.assertEqual(1, usage.total_agents)
- self.assertEqual("budget_exhausted:agents", usage.exhausted_reason)
- self.assertTrue(ensure_task_protocol(root.context)["root_validation_passed"])
- async def test_persisted_child_is_not_refunded_when_goal_write_fails(self):
- root_id = "partial-child-create-root"
- budget = ResourceBudget()
- await self.store.create_trace(Trace(
- trace_id=root_id,
- mode="agent",
- uid="user-1",
- model="fake",
- context={
- "agent_mode": "recursive",
- "agent_mode_revision": 2,
- "agent_depth": 0,
- "root_trace_id": root_id,
- "root_completion_criteria": ROOT_CRITERIA,
- RESOURCE_BUDGET_CONTEXT_KEY: budget.to_dict(),
- "task_protocol": new_task_protocol(),
- },
- ))
- await ResourceBudgetController(self.store).initialize(root_id, budget)
- original_update_goal_tree = self.store.update_goal_tree
- async def fail_child_goal_write(trace_id, goal_tree):
- if trace_id != root_id:
- raise RuntimeError("simulated child GoalTree write failure")
- return await original_update_goal_tree(trace_id, goal_tree)
- self.store.update_goal_tree = fail_child_goal_write
- runner = AgentRunner(
- trace_store=self.store,
- tool_registry=get_tool_registry(),
- llm_call=lambda **_: None,
- )
- with self.assertRaisesRegex(RuntimeError, "GoalTree write failure"):
- await agent(
- task_brief=CHILD_BRIEF,
- context={
- "store": self.store,
- "trace_id": root_id,
- "runner": runner,
- RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY: ["agent"],
- },
- )
- children = await self.store.list_traces(
- parent_trace_id=root_id,
- created_by_tool="agent",
- limit=10,
- )
- self.assertEqual(1, len(children))
- self.assertEqual("failed", children[0].status)
- self.assertIn("Child initialization failed", children[0].error_message)
- usage = await ResourceBudgetController(self.store).get_usage(root_id)
- self.assertEqual(2, usage.total_agents)
- async def test_parent_goal_projection_failure_closes_precreated_child(self):
- root_id = "parent-goal-projection-root"
- budget = ResourceBudget()
- await self.store.create_trace(Trace(
- trace_id=root_id,
- mode="agent",
- uid="user-1",
- model="fake",
- context={
- "agent_mode": "recursive",
- "agent_mode_revision": 2,
- "agent_depth": 0,
- "root_trace_id": root_id,
- "root_completion_criteria": ROOT_CRITERIA,
- RESOURCE_BUDGET_CONTEXT_KEY: budget.to_dict(),
- "task_protocol": new_task_protocol(),
- },
- ))
- await ResourceBudgetController(self.store).initialize(root_id, budget)
- runner = AgentRunner(
- trace_store=self.store,
- tool_registry=get_tool_registry(),
- llm_call=lambda **_: None,
- )
- async def fail_parent_projection(*_args, **_kwargs):
- raise RuntimeError("simulated parent Goal projection failure")
- with patch(
- "cyber_agent.tools.builtin.subagent._update_goal_start",
- new=fail_parent_projection,
- ):
- with self.assertRaisesRegex(RuntimeError, "projection failure"):
- await agent(
- task_brief=CHILD_BRIEF,
- context={
- "store": self.store,
- "trace_id": root_id,
- "runner": runner,
- RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY: ["agent"],
- },
- )
- children = await self.store.list_traces(
- parent_trace_id=root_id,
- created_by_tool="agent",
- limit=10,
- )
- self.assertEqual(1, len(children))
- self.assertEqual("failed", children[0].status)
- usage = await ResourceBudgetController(self.store).get_usage(root_id)
- self.assertEqual(2, usage.total_agents)
- async def test_approved_action_write_failure_closes_precreated_child(self):
- root_id = "approved-action-write-root"
- budget = ResourceBudget()
- context = {
- "agent_mode": "recursive",
- "agent_mode_revision": 2,
- "agent_depth": 0,
- "root_trace_id": root_id,
- "root_completion_criteria": ROOT_CRITERIA,
- RESOURCE_BUDGET_CONTEXT_KEY: budget.to_dict(),
- "task_protocol": new_task_protocol(),
- }
- state = ensure_task_protocol(context)
- state["next_actions"] = [{
- "decision": "ACCEPT_REFINE",
- "child_trace_id": "reviewed-child",
- "goal_id": None,
- "task_brief": CHILD_BRIEF,
- "created_at_sequence": 1,
- }]
- await self.store.create_trace(Trace(
- trace_id=root_id,
- mode="agent",
- uid="user-1",
- model="fake",
- context=context,
- ))
- await ResourceBudgetController(self.store).initialize(root_id, budget)
- runner = AgentRunner(
- trace_store=self.store,
- tool_registry=get_tool_registry(),
- llm_call=lambda **_: None,
- )
- original_update_trace = self.store.update_trace
- rejected_parent_write = False
- async def fail_approved_action_write(trace_id, **updates):
- nonlocal rejected_parent_write
- if trace_id == root_id and "context" in updates and not rejected_parent_write:
- rejected_parent_write = True
- raise RuntimeError("simulated approved action write failure")
- return await original_update_trace(trace_id, **updates)
- self.store.update_trace = fail_approved_action_write
- with self.assertRaisesRegex(RuntimeError, "approved action write failure"):
- await agent(
- task_brief=CHILD_BRIEF,
- context={
- "store": self.store,
- "trace_id": root_id,
- "runner": runner,
- RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY: ["agent"],
- },
- )
- children = await self.store.list_traces(
- parent_trace_id=root_id,
- created_by_tool="agent",
- limit=10,
- )
- self.assertEqual(1, len(children))
- self.assertEqual("failed", children[0].status)
- usage = await ResourceBudgetController(self.store).get_usage(root_id)
- self.assertEqual(2, usage.total_agents)
- class RecursiveLifecycleIntegrationTest(unittest.IsolatedAsyncioTestCase):
- async def asyncSetUp(self):
- self.temp_dir = tempfile.TemporaryDirectory()
- self.store = FileSystemTraceStore(self.temp_dir.name)
- async def asyncTearDown(self):
- self.temp_dir.cleanup()
- async def test_child_report_is_validated_before_parent_review(self):
- child_submitted = False
- root_delegated = False
- root_reviewed = False
- async def llm_call(**kwargs):
- nonlocal child_submitted, root_delegated, root_reviewed
- messages = kwargs["messages"]
- if is_validator_call(messages):
- return validator_response(messages)
- names = tool_names(kwargs.get("tools"))
- user_text = "\n".join(
- str(message.get("content", ""))
- for message in messages
- if message.get("role") == "user"
- )
- if "submit_task_report" in names and not child_submitted:
- child_submitted = True
- return {
- "content": "",
- "tool_calls": [{
- "id": "submit-report",
- "type": "function",
- "function": {
- "name": "submit_task_report",
- "arguments": json.dumps({
- "task_report": {
- "summary": "one fact was checked",
- "outcome": "satisfied",
- "validation": {
- "hard_passed": True,
- "open_issues": [],
- },
- "next_step_suggestion": {
- "direction": "NONE",
- "reason": "the delegated check is complete",
- },
- "outputs": [{"kind": "answer", "value": "checked"}],
- "evidence": [{"kind": "tool", "value": "persisted"}],
- "remaining_issues": [],
- }
- }),
- },
- }],
- "finish_reason": "tool_calls",
- }
- if "# Task Brief" in user_text:
- return {
- "content": "structured child report submitted",
- "tool_calls": [],
- "finish_reason": "stop",
- }
- if names == {"review_task_result"}:
- roots = [
- trace
- for trace in await self.store.list_traces(limit=20)
- if trace.parent_trace_id is None
- ]
- root = roots[0]
- pending = ensure_task_protocol(root.context)["pending_reviews"]
- child_id = next(iter(pending))
- root_reviewed = True
- return {
- "content": "",
- "tool_calls": [{
- "id": "review-report",
- "type": "function",
- "function": {
- "name": "review_task_result",
- "arguments": json.dumps({
- "child_trace_id": child_id,
- "decision": "ASCEND",
- "reason": "the validated child satisfies this goal",
- }),
- },
- }],
- "finish_reason": "tool_calls",
- }
- if "agent" in names and not root_delegated:
- root_delegated = True
- return {
- "content": "",
- "tool_calls": [{
- "id": "delegate-child",
- "type": "function",
- "function": {
- "name": "agent",
- "arguments": json.dumps({"task_brief": CHILD_BRIEF}),
- },
- }],
- "finish_reason": "tool_calls",
- }
- return {
- "content": "final answer based on the validated child",
- "tool_calls": [],
- "finish_reason": "stop",
- }
- runner = AgentRunner(
- trace_store=self.store,
- tool_registry=get_tool_registry(),
- llm_call=llm_call,
- )
- config = RunConfig(
- tools=["agent", "submit_task_report", "review_task_result"],
- tool_groups=[],
- enable_research_flow=False,
- root_completion_criteria=ROOT_CRITERIA,
- knowledge=knowledge_disabled(),
- )
- with recursive_env():
- result = await runner.run_result(
- [{"role": "user", "content": "perform one delegated check"}],
- config,
- )
- self.assertEqual("completed", result["status"])
- self.assertTrue(root_reviewed)
- root = await self.store.get_trace(result["trace_id"])
- business_children = await self.store.list_traces(
- parent_trace_id=root.trace_id,
- created_by_tool="agent",
- limit=10,
- )
- self.assertEqual(1, len(business_children))
- child = business_children[0]
- child_validators = await self.store.list_traces(
- parent_trace_id=child.trace_id,
- created_by_tool="validator",
- limit=10,
- )
- root_validators = await self.store.list_traces(
- parent_trace_id=root.trace_id,
- created_by_tool="validator",
- limit=10,
- )
- self.assertEqual(1, len(child_validators))
- self.assertEqual(1, len(root_validators))
- self.assertEqual(
- child.trace_id,
- child_validators[0].context["evaluated_trace_id"],
- )
- child_state = ensure_task_protocol(child.context)
- self.assertEqual(
- child_validators[0].trace_id,
- child_state["task_report_validation"]["validation_result"][
- "validator_trace_id"
- ],
- )
- root_state = ensure_task_protocol(root.context)
- self.assertEqual({}, root_state["pending_reviews"])
- self.assertEqual(child.trace_id, root_state["reviews"][0]["child_trace_id"])
- usage = await ResourceBudgetController(self.store).get_usage(root.trace_id)
- self.assertEqual(2, usage.total_agents)
- self.assertEqual(7, usage.llm_calls)
- async def test_parent_stop_reaches_validator_after_child_runner_finished(self):
- root_id = "validator-cancel-root"
- child_id = "validator-cancel-child"
- budget = ResourceBudget()
- await self.store.create_trace(Trace(
- trace_id=root_id,
- mode="agent",
- model="fake-model",
- context={
- "agent_mode": "recursive",
- "agent_mode_revision": 2,
- "agent_depth": 0,
- "root_trace_id": root_id,
- "root_completion_criteria": ROOT_CRITERIA,
- RESOURCE_BUDGET_CONTEXT_KEY: budget.to_dict(),
- "task_protocol": new_task_protocol(),
- },
- ))
- await self.store.create_trace(Trace(
- trace_id=child_id,
- mode="agent",
- model="fake-model",
- parent_trace_id=root_id,
- context={
- "agent_mode": "recursive",
- "agent_mode_revision": 2,
- "agent_depth": 1,
- "root_trace_id": root_id,
- "created_by_tool": "agent",
- "task_protocol": new_task_protocol(CHILD_BRIEF),
- },
- ))
- await ResourceBudgetController(self.store).initialize(
- root_id,
- budget,
- initial_agents=2,
- )
- started = asyncio.Event()
- release = asyncio.Event()
- async def llm_call(**kwargs):
- started.set()
- await release.wait()
- return validator_response(kwargs["messages"])
- runner = AgentRunner(trace_store=self.store, llm_call=llm_call)
- root_event = asyncio.Event()
- runner._cancel_events[root_id] = root_event
- runner._recursive_active_traces[root_id] = root_event
- validation_task = asyncio.create_task(runner.validate_recursive_trace(
- child_id,
- scope="task",
- task_brief=CHILD_BRIEF,
- task_report={"summary": "candidate"},
- ))
- await asyncio.wait_for(started.wait(), timeout=1)
- self.assertIn(child_id, runner._active_children[root_id])
- self.assertTrue(await runner.stop(root_id))
- release.set()
- validation_run = await asyncio.wait_for(validation_task, timeout=1)
- self.assertEqual("error", validation_run.result.outcome)
- validator = await self.store.get_trace(validation_run.trace_id)
- self.assertEqual("failed", validator.status)
- self.assertEqual(child_id, validator.parent_trace_id)
- self.assertEqual(root_id, validator.context["root_trace_id"])
- self.assertNotIn(child_id, runner._recursive_active_traces)
- self.assertNotIn(validation_run.trace_id, runner._recursive_active_traces)
- if __name__ == "__main__":
- unittest.main()
|