| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- import json
- import os
- import re
- import tempfile
- import unittest
- from unittest.mock import patch
- from cyber_agent.core.context_policy import (
- canonical_json,
- context_ref_descriptors,
- require_root_task_anchor,
- )
- from cyber_agent.core.resource_budget import ResourceBudgetController
- from cyber_agent.core.runner import AgentRunner, RunConfig
- from cyber_agent.core.task_protocol import ensure_task_protocol
- from cyber_agent.tools import get_tool_registry
- from cyber_agent.tools.builtin.knowledge import KnowledgeConfig
- from cyber_agent.trace.store import FileSystemTraceStore
- ROOT_ANCHOR = {
- "objective": "通过五层局部分析得出可追溯的根结论",
- "completion_criteria": ["五层任务都经过直接父级审核", "根结果通过独立验收"],
- "constraints": ["不得编造证据"],
- }
- def recursive_env():
- return patch.dict(os.environ, {
- "AGENT_MODE": "recursive",
- "AGENT_RESOURCE_BUDGET_ENABLED": "true",
- "AGENT_MAX_TOTAL_AGENTS": "10",
- "AGENT_MAX_LLM_CALLS": "80",
- "AGENT_MAX_TOTAL_TOKENS": "100000",
- "AGENT_MAX_TOTAL_COST_USD": "10",
- "AGENT_MAX_DURATION_SECONDS": "600",
- "AGENT_RESERVED_FINAL_CALLS": "1",
- }, clear=False)
- def knowledge_disabled():
- return KnowledgeConfig(
- enable_extraction=False,
- enable_completion_extraction=False,
- enable_injection=False,
- )
- def schema_names(schemas):
- return {
- schema["function"]["name"]
- for schema in schemas or []
- }
- def message_text(messages):
- parts = []
- for message in messages:
- content = message.get("content", "")
- if isinstance(content, str):
- parts.append(content)
- elif isinstance(content, list):
- parts.extend(
- item.get("text", "")
- for item in content
- if isinstance(item, dict) and item.get("type") == "text"
- )
- return "\n".join(parts)
- def trace_depth(messages):
- match = re.search(r"## Objective\s*\n\s*depth-(\d+)", message_text(messages))
- return int(match.group(1)) if match else 0
- def last_tool_name(messages):
- for message in reversed(messages):
- if message.get("role") == "tool":
- return message.get("name")
- return None
- def pending_child_id(messages):
- for message in reversed(messages):
- if message.get("role") != "tool":
- continue
- content = message.get("content", "")
- text = content if isinstance(content, str) else json.dumps(content)
- match = re.search(r'"child_trace_id"\s*:\s*"([^"]+)"', text)
- if match:
- return match.group(1)
- raise AssertionError("agent tool result did not contain child_trace_id")
- def available_ref(messages):
- text = message_text(messages)
- match = re.search(
- r'"ref_id":"([^"]+)","source_trace_id":"[^"]+",'
- r'"summary":"[^"]+","version":"([0-9a-f]{64})"',
- text,
- )
- if not match:
- # 字段按 sort_keys 排序,但摘要可能含有转义字符;用宽松回退只解析 ID/版本。
- ref_id = re.search(r'"ref_id":"([^"]+)"', text)
- version = re.search(r'"version":"([0-9a-f]{64})"', text)
- if not ref_id or not version:
- raise AssertionError("no authorized ContextRef in child task prompt")
- return ref_id.group(1), version.group(1)
- return match.group(1), match.group(2)
- def tool_call(name, arguments, call_index):
- return {
- "content": "",
- "tool_calls": [{
- "id": f"call-{call_index}-{name}",
- "type": "function",
- "function": {
- "name": name,
- "arguments": json.dumps(arguments, ensure_ascii=False),
- },
- }],
- "finish_reason": "tool_calls",
- "prompt_tokens": 3,
- "completion_tokens": 2,
- "cost": 0.0001,
- }
- class FiveLevelRecursiveContextTest(unittest.IsolatedAsyncioTestCase):
- async def test_real_runner_keeps_anchor_refs_permissions_and_reviews_through_depth_five(self):
- with tempfile.TemporaryDirectory() as directory:
- store = FileSystemTraceStore(directory)
- call_count = 0
- token_total = 0
- read_depths = []
- observed_tools = {}
- validator_packets = []
- async def fake_llm(**kwargs):
- nonlocal call_count, token_total
- call_count += 1
- messages = kwargs["messages"]
- tools = schema_names(kwargs.get("tools"))
- depth = trace_depth(messages)
- observed_tools.setdefault(depth, []).append(tools)
- if any(
- message.get("role") == "system"
- and "independent validator" in str(message.get("content", ""))
- for message in messages
- ):
- packet = json.loads(messages[-1]["content"])
- validator_packets.append(packet)
- response = {
- "content": json.dumps({
- "outcome": "passed",
- "scope": packet["validation_scope"],
- "reason": "已核对持久化轨迹、标准和输出",
- "issues": [],
- "retry_from": None,
- }, ensure_ascii=False),
- "tool_calls": [],
- "finish_reason": "stop",
- "prompt_tokens": 3,
- "completion_tokens": 2,
- "cost": 0.0001,
- }
- token_total += 5
- return response
- last_tool = last_tool_name(messages)
- if tools == {"review_task_result", "read_context_ref"}:
- response = tool_call(
- "review_task_result",
- {
- "child_trace_id": pending_child_id(messages),
- "decision": "ASCEND",
- "reason": f"depth-{depth + 1} 已通过独立验收",
- },
- call_count,
- )
- elif depth > 0 and "submit_task_report" not in tools:
- response = {
- "content": f"depth-{depth} TaskReport 已提交",
- "tool_calls": [],
- "finish_reason": "stop",
- "prompt_tokens": 3,
- "completion_tokens": 2,
- "cost": 0.0001,
- }
- elif last_tool == "review_task_result":
- if depth == 0:
- response = {
- "content": "根任务的五层结果已逐层审核完成",
- "tool_calls": [],
- "finish_reason": "stop",
- "prompt_tokens": 3,
- "completion_tokens": 2,
- "cost": 0.0001,
- }
- else:
- response = tool_call(
- "submit_task_report",
- {"task_report": self._report(depth)},
- call_count,
- )
- elif depth in {4, 5} and last_tool != "read_context_ref":
- ref_id, version = available_ref(messages)
- read_depths.append(depth)
- response = tool_call(
- "read_context_ref",
- {"ref_id": ref_id, "version": version},
- call_count,
- )
- elif depth == 5:
- response = tool_call(
- "submit_task_report",
- {"task_report": self._report(depth)},
- call_count,
- )
- else:
- next_depth = depth + 1
- response = tool_call(
- "agent",
- {"task_brief": self._brief(next_depth)},
- call_count,
- )
- token_total += 5
- return response
- runner = AgentRunner(
- trace_store=store,
- tool_registry=get_tool_registry(),
- llm_call=fake_llm,
- )
- config = RunConfig(
- tools=[
- "agent",
- "submit_task_report",
- "review_task_result",
- "read_context_ref",
- ],
- tool_groups=[],
- enable_research_flow=False,
- root_task_anchor=ROOT_ANCHOR,
- knowledge=knowledge_disabled(),
- child_execution_mode="sequential",
- )
- read_stats_before = get_tool_registry().get_stats("read_context_ref")[
- "read_context_ref"
- ]["call_count"]
- with recursive_env():
- result = await runner.run_result(
- [{"role": "user", "content": "请逐层拆解并验收根任务"}],
- config,
- )
- self.assertEqual("completed", result["status"])
- traces = await store.list_traces(limit=100)
- business = [
- trace for trace in traces
- if trace.context.get("created_by_tool") == "agent"
- or trace.trace_id == result["trace_id"]
- ]
- business.sort(key=lambda trace: trace.context["agent_depth"])
- self.assertEqual(list(range(6)), [trace.context["agent_depth"] for trace in business])
- self.assertEqual(6, len(business))
- for index, trace in enumerate(business):
- self.assertEqual(result["trace_id"], trace.context["root_trace_id"])
- self.assertEqual(
- canonical_json(ROOT_ANCHOR),
- canonical_json(require_root_task_anchor(trace.context).model_dump(mode="json")),
- )
- messages = await store.get_trace_messages(trace.trace_id)
- first_user = next(message for message in messages if message.role == "user")
- first_user_text = (
- first_user.content
- if isinstance(first_user.content, str)
- else json.dumps(first_user.content, ensure_ascii=False)
- )
- self.assertEqual(1, first_user_text.count("# Root Task Anchor"))
- metrics = trace.context["context_access"]["metrics"]
- self.assertGreater(metrics["root_anchor_chars"], 0)
- self.assertEqual(
- len(context_ref_descriptors(trace.context)),
- metrics["authorized_ref_count"],
- )
- if index:
- self.assertEqual(business[index - 1].trace_id, trace.parent_trace_id)
- state = ensure_task_protocol(trace.context)
- self.assertEqual(1, state["task_brief_version"])
- self.assertEqual(
- ["不得编造证据", *[f"约束-{level}" for level in range(1, index + 1)]],
- state["task_brief"]["constraints"],
- )
- self.assertEqual(
- [f"直接父级结论 depth-{index - 1}"],
- state["task_brief"]["parent_findings"],
- )
- self.assertEqual(
- {"local_depth": index},
- state["task_brief"]["context"],
- )
- self.assertEqual([], state["task_brief"]["context_refs"])
- descriptors = context_ref_descriptors(trace.context)
- expected_kinds = (
- ["reviewed_task_result"]
- if index == 1
- else ["task_brief"]
- if index == 5
- else ["task_brief", "reviewed_task_result"]
- )
- self.assertEqual(
- expected_kinds,
- [item["kind"] for item in descriptors],
- )
- self.assertEqual([4, 5], read_depths)
- read_stats_after = get_tool_registry().get_stats("read_context_ref")[
- "read_context_ref"
- ]["call_count"]
- self.assertEqual(2, read_stats_after - read_stats_before)
- self.assertTrue(any("agent" in tools for tools in observed_tools[4]))
- self.assertTrue(all("evaluate" not in tools and "bash_command" not in tools for tools in observed_tools[5]))
- self.assertTrue(all("agent" not in tools for tools in observed_tools[5]))
- self.assertTrue(any("read_context_ref" in tools for tools in observed_tools[5]))
- validators = [
- trace for trace in traces
- if trace.context.get("created_by_tool") == "validator"
- ]
- self.assertEqual(6, len(validators))
- self.assertEqual(6, len(validator_packets))
- self.assertTrue(all(packet["root_task_anchor"] == ROOT_ANCHOR for packet in validator_packets))
- packets_with_real_ref_reads = [
- packet
- for packet in validator_packets
- if any(
- item.get("role") == "tool"
- and item.get("name") == "read_context_ref"
- for item in packet["trajectory"]
- )
- ]
- self.assertEqual(2, len(packets_with_real_ref_reads))
- self.assertTrue(ensure_task_protocol(business[0].context)["root_validation_passed"])
- usage = await ResourceBudgetController(store).get_usage(result["trace_id"])
- self.assertEqual(6, usage.total_agents)
- self.assertEqual(call_count, usage.llm_calls)
- self.assertEqual(token_total, usage.total_tokens)
- @staticmethod
- def _brief(depth):
- return {
- "objective": f"depth-{depth}",
- "reason": f"depth-{depth - 1} 需要直属子任务结果",
- "completion_criteria": [f"depth-{depth} 结果通过验收"],
- "expected_outputs": [f"depth-{depth} 可追溯结论"],
- "parent_findings": [f"直接父级结论 depth-{depth - 1}"],
- "context": {"local_depth": depth},
- "constraints": [f"约束-{depth}"],
- }
- @staticmethod
- def _report(depth):
- return {
- "summary": f"depth-{depth} 局部任务完成",
- "outcome": "satisfied",
- "validation": {"hard_passed": True, "open_issues": []},
- "next_step_suggestion": {
- "direction": "ASCEND",
- "reason": "当前层已满足完成标准",
- },
- "outputs": [{"depth": depth, "result": f"结论-{depth}"}],
- "evidence": [{"depth": depth, "source": "fake-persisted-trace"}],
- "remaining_issues": [],
- }
- if __name__ == "__main__":
- unittest.main()
|