Parcourir la source

test(reference): 真实覆盖私有工具执行和进程重启恢复

将事实验证器注册为带隐藏 Trace/uid 上下文的应用私有工具,参考链路通过 ToolRegistry 的执行 allowlist 调用并校验工具统计,不再绕过框架直调业务对象。

为手工构造的参考子任务固化 RunConfigSnapshotV2;停止根与 writer 后重建 Registry、ApplicationRuntime 和进程内 Provider,再从 writer Trace 恢复。持久 Artifact、Candidate Repository 与 Projector 后端复用,并断言资料 Provider 不重拉、事实工具总调用仍为一次。
SamLee il y a 10 heures
Parent
commit
2b390bafc2

+ 26 - 8
examples/application_reference/application.py

@@ -42,19 +42,37 @@ class ReferenceComponents:
     projector: ReferenceProjector
 
 
-def build_reference_components() -> ReferenceComponents:
-    artifacts = ReferenceArtifactStore()
-    candidates = ReferenceCandidateRepository(artifacts)
+def build_reference_components(
+    *,
+    artifacts: ReferenceArtifactStore | None = None,
+    candidates: ReferenceCandidateRepository | None = None,
+    projector: ReferenceProjector | None = None,
+) -> ReferenceComponents:
+    """Build a fresh process-local binding around optionally persistent backends."""
+    artifacts = artifacts or ReferenceArtifactStore()
+    candidates = candidates or ReferenceCandidateRepository(artifacts)
     context = ReferenceContextProvider()
     facts = FactVerifier(artifacts)
-    projector = ReferenceProjector()
+    projector = projector or ReferenceProjector()
     registry = get_tool_registry().clone()
 
-    async def verify_fact(question: str) -> dict:
-        """Runtime tests use FactVerifier directly so hidden identity stays framework-owned."""
-        return {"question": question}
+    async def verify_fact(
+        question: str,
+        context: dict,
+        uid: str = "",
+    ) -> dict:
+        trace = await context["store"].get_trace(context["trace_id"])
+        if trace is None:
+            raise ValueError("Fact tool Trace not found")
+        root_trace_id = trace.context.get("root_trace_id") or trace.trace_id
+        ref = await facts.verify(
+            question=question,
+            root_trace_id=root_trace_id,
+            uid=uid or None,
+        )
+        return {"artifact_ref": ref.model_dump(mode="json")}
 
-    registry.register(verify_fact)
+    registry.register(verify_fact, hidden_params=["context", "uid"])
     application = AgentApplication(
         application_id="application_reference",
         application_version="1",

+ 4 - 0
examples/application_reference/providers/context.py

@@ -4,12 +4,15 @@ from cyber_agent.application.models import stable_hash
 
 class ReferenceContextProvider:
     def __init__(self) -> None:
+        self.list_calls = 0
+        self.resolve_calls = 0
         self.content = {
             "source": "reference-fixture",
             "topic": "agent framework responsibilities",
         }
 
     async def list_context(self, request):
+        self.list_calls += 1
         return [ContextResourceDescriptor(
             application_ref=request.application_ref,
             root_trace_id=request.root_trace_id,
@@ -26,5 +29,6 @@ class ReferenceContextProvider:
         )]
 
     async def resolve_context(self, request, descriptor):
+        self.resolve_calls += 1
         del request
         return ContextMaterial(descriptor=descriptor, content=self.content)

+ 71 - 7
tests/test_application_reference.py

@@ -10,6 +10,12 @@ from cyber_agent.application import (
     CandidateReviewAction,
 )
 from cyber_agent.application.candidate import CandidateLedger, CandidatePointer
+from cyber_agent.core.artifacts import ArtifactRef
+from cyber_agent.core.run_snapshot import (
+    RunConfigSnapshotV2,
+    persist_run_config_snapshot,
+)
+from cyber_agent.core.runner import RunConfig
 from cyber_agent.core.task_protocol import (
     Finding,
     Hypothesis,
@@ -105,6 +111,25 @@ class ApplicationReferenceEndToEndTest(unittest.IsolatedAsyncioTestCase):
                 state = new_task_protocol(brief)
                 initialize_task_progress(state, effective_at_sequence=1)
                 context["task_protocol"] = state
+                child_config = RunConfig(
+                    trace_id=trace_id,
+                    uid=root.uid,
+                    parent_trace_id=root.trace_id,
+                    context=context,
+                    enable_research_flow=False,
+                )
+                binding.configure_run_config(child_config, role_id)
+                context["effective_run_limits"] = dict(
+                    child_config.effective_run_limits
+                )
+                child_config.context = context
+                persist_run_config_snapshot(
+                    context,
+                    RunConfigSnapshotV2.from_run_config(
+                        child_config,
+                        memory_identity=None,
+                    ),
+                )
                 child = Trace(
                     trace_id=trace_id,
                     mode="agent",
@@ -128,10 +153,18 @@ class ApplicationReferenceEndToEndTest(unittest.IsolatedAsyncioTestCase):
                     validation_scopes=["evidence"],
                 ),
             )
-            fact_ref = await components.facts.verify(
-                question="What does an agent framework supply?",
-                root_trace_id=root.trace_id,
+            raw_fact_result = await binding.tool_registry.execute(
+                "verify_fact",
+                {"question": "What does an agent framework supply?"},
                 uid=root.uid,
+                context={
+                    "trace_id": fact_child.trace_id,
+                    "store": store,
+                },
+                allowed_tool_names={"verify_fact"},
+            )
+            fact_ref = ArtifactRef.model_validate(
+                json.loads(raw_fact_result)["artifact_ref"]
             )
             await runner.task_protocol_service.update_progress(
                 fact_child.trace_id,
@@ -228,10 +261,38 @@ class ApplicationReferenceEndToEndTest(unittest.IsolatedAsyncioTestCase):
             self.assertEqual("passed", validation_a.result.outcome)
             self.assertEqual("failed", validation_b.result.outcome)
             self.assertEqual(1, components.facts.calls)
+            self.assertEqual(
+                1,
+                binding.tool_registry.get_stats("verify_fact")[
+                    "verify_fact"
+                ]["call_count"],
+            )
             self.assertEqual(1, llm_calls)
             frozen_a = validation_a.result.model_dump(mode="json")
 
-            restored_runner, _restored_config = await runtime.restore(root.trace_id)
+            await runner._mark_trace_stopped(root.trace_id, root.head_sequence)
+            await runner._mark_trace_stopped(writer.trace_id, writer.head_sequence)
+            restarted_components = build_reference_components(
+                artifacts=components.artifacts,
+                candidates=components.candidates,
+                projector=components.projector,
+            )
+            restarted_registry = ApplicationRegistry()
+            restarted_registry.register(
+                restarted_components.application,
+                restarted_components.services,
+            )
+            restarted_runtime = ApplicationRuntime(
+                registry=restarted_registry,
+                trace_store=store,
+                llm_call=llm_call,
+            )
+            restored_runner, restored_config = await restarted_runtime.restore(
+                writer.trace_id
+            )
+            self.assertEqual("writer", restored_config.role_id)
+            self.assertEqual(0, restarted_components.context.list_calls)
+            self.assertEqual(0, restarted_components.context.resolve_calls)
             await restored_runner.candidate_service.apply_review_actions(
                 root.trace_id,
                 writer.trace_id,
@@ -259,7 +320,10 @@ class ApplicationReferenceEndToEndTest(unittest.IsolatedAsyncioTestCase):
             )
             self.assertEqual("passed", validation_b2.result.outcome)
             self.assertEqual(2, llm_calls)
-            self.assertEqual(1, components.facts.calls)
+            self.assertEqual(
+                1,
+                components.facts.calls + restarted_components.facts.calls,
+            )
             self.assertEqual(
                 frozen_a,
                 (await restored_runner.validate_recursive_trace(
@@ -288,8 +352,8 @@ class ApplicationReferenceEndToEndTest(unittest.IsolatedAsyncioTestCase):
 
             await restored_runner.event_service.try_pump(root.trace_id)
             before_replay = dict(components.projector.rows)
-            await runtime.reconcile_events(root.trace_id)
-            await runtime.reconcile_events(root.trace_id)
+            await restarted_runtime.reconcile_events(root.trace_id)
+            await restarted_runtime.reconcile_events(root.trace_id)
             self.assertEqual(before_replay, components.projector.rows)
             adopted_keys = [
                 key for key in components.projector.rows