Forráskód Böngészése

test(application): 固化同名工具并发隔离并统一当前协议文案

将应用 A/B 的同名 save 工具通过两个冻结私有 ToolRegistry 并发执行四十次,断言结果和调用统计完全隔离。

同步把当前可写协议的运行时说明从历史 revision 2 改为 Structured Recursive,避免只读历史版本文案误导模型和维护者。
SamLee 12 órája
szülő
commit
00880034e9

+ 1 - 1
cyber_agent/core/context_policy.py

@@ -140,7 +140,7 @@ def require_root_task_anchor(context: dict[str, Any]) -> RootTaskAnchor:
     raw = context.get(ROOT_TASK_ANCHOR_KEY)
     if raw is None:
         raise ContextPolicyError(
-            "This Recursive revision 2 trace predates root_task_anchor; create a new trace"
+            "This Recursive trace predates root_task_anchor; create a new trace"
         )
     normalized = normalize_root_task_anchor(raw)
     expected = root_task_anchor_hash(normalized)

+ 2 - 2
cyber_agent/core/validation.py

@@ -1,4 +1,4 @@
-"""Recursive revision 2 的分层独立验收引擎。
+"""Structured Recursive 的分层独立验收引擎。
 
 Runner 为每份 TaskReport 编译不可变 ValidationPlan;本模块按 Scope 创建独立
 Validator Trace、执行逐项检查并聚合为父级只审核一次的 ValidationResult。
@@ -540,7 +540,7 @@ def require_validation_policy(
     raw = root_context.get(VALIDATION_POLICY_CONTEXT_KEY)
     if raw is None:
         raise ValueError(
-            "This Recursive revision 2 trace predates ValidationPolicy snapshots; create a new trace"
+            "This Recursive trace predates ValidationPolicy snapshots; create a new trace"
         )
     policy = ValidationPolicy.model_validate(raw)
     if root_context.get(VALIDATION_POLICY_HASH_CONTEXT_KEY) != policy.policy_hash:

+ 7 - 6
cyber_agent/tools/builtin/subagent.py

@@ -2,8 +2,9 @@
 
 ``agent`` 由父 Agent 调用:Legacy 只创建一层,Recursive 还会执行深度、孩子数、
 权限、预算和审核门禁;本地任务进程内运行。Legacy 和 Recursive
-revision 1 的 ``remote_*`` 仍通过 KnowHub HTTP 路由,revision 2 失败关闭。
-``evaluate`` 保留给 Legacy 和旧的 Recursive revision 1;revision 2 使用框架管理的独立 Validator。
+revision 1 的 ``remote_*`` 仍通过 KnowHub HTTP 路由;受管理的 Structured
+Recursive 失败关闭。``evaluate`` 保留给 Legacy 和旧的 Recursive
+revision 1;Structured Recursive 使用框架管理的独立 Validator。
 """
 
 import asyncio
@@ -1824,12 +1825,12 @@ async def agent(
     路由规则:
     - Legacy/Recursive revision 1 中 agent_type 以 "remote_" 开头:HTTP
       调用 KnowHub 服务器的 /api/agent(仅单任务,无本地文件访问)
-    - Recursive revision 2 不支持 remote Agent,也不会发出 HTTP 请求
-    - 否则本地执行:Legacy/Recursive revision 1 使用 ``task``;revision 2 使用 ``task_brief``
+    - Structured Recursive 不支持 remote Agent,也不会发出 HTTP 请求
+    - 否则本地执行:Legacy/Recursive revision 1 使用 ``task``;Structured Recursive 使用 ``task_brief``
 
     Args:
         task: Legacy/Recursive revision 1 任务描述。
-        task_brief: Recursive revision 2 的结构化任务说明。
+        task_brief: Structured Recursive 的结构化任务说明。
         messages: 预置消息。1D 列表=所有 agent 共享;2D 列表=per-agent
         continue_from: 继续已有 trace(仅单任务)
         agent_type: 子 Agent 类型。Legacy/Recursive revision 1 带
@@ -1845,7 +1846,7 @@ async def agent(
         return {"status": "failed", "error": str(exc)}
 
     # 任何路由决策都必须来自已持久化的父 Trace 策略。先看
-    # ``remote_*`` 会让 Recursive revision 2 绕过本地协议、预算与审核门禁。
+    # ``remote_*`` 会让 Structured Recursive 绕过本地协议、预算与审核门禁。
     if not context:
         return {"status": "failed", "error": "context is required"}
 

+ 2 - 2
cyber_agent/trace/goal_tool.py

@@ -49,7 +49,7 @@ async def inject_knowledge_for_goal(
         return None
 
     # Goal focus 过去直接调用 ``_run_remote_agent``,会绕过 agent()
-    # 已实施的 Recursive revision 2 远端门禁。这里必须从当前
+    # 已实施的 Structured Recursive 远端门禁。这里必须从当前
     # Trace 恢复权威策略;读取失败时不能猜测为 Legacy 继续出网。
     if store is not None or trace_id is not None:
         if store is None or not trace_id:
@@ -73,7 +73,7 @@ async def inject_knowledge_for_goal(
             return None
         if policy.requires_task_protocol:
             logger.info(
-                "[Knowledge Inject] Recursive revision 2 forbids remote Librarian; skip"
+                "[Knowledge Inject] Structured Recursive forbids remote Librarian; skip"
             )
             goal.knowledge = []
             return None

+ 47 - 1
tests/test_application_runtime.py

@@ -1,3 +1,5 @@
+import asyncio
+import json
 import unittest
 
 from pydantic import ValidationError
@@ -162,7 +164,6 @@ class ApplicationDeclarationTest(unittest.TestCase):
                 ),
             ).application_ref.config_hash)
         self.assertTrue(all(item != base for item in hashes))
-
         changed_schema = registry("base")
         definition = changed_schema._tools["save"]
         definition["schema"] = {
@@ -181,6 +182,51 @@ class ApplicationDeclarationTest(unittest.TestCase):
         ).application_ref.config_hash
         self.assertNotEqual(base, schema_hash)
 
+
+class ApplicationToolIsolationTest(unittest.IsolatedAsyncioTestCase):
+    async def test_same_named_tools_execute_in_private_registries_concurrently(self):
+        app_a = application()
+        app_b = app_a.model_copy(update={"application_id": "creative.other"})
+        binding_a = ApplicationRegistry().register(
+            app_a,
+            ApplicationServices(
+                tool_registry=registry("A"),
+                context_provider=object(),
+            ),
+        )
+        binding_b = ApplicationRegistry().register(
+            app_b,
+            ApplicationServices(
+                tool_registry=registry("B"),
+                context_provider=object(),
+            ),
+        )
+
+        async def execute(binding, expected, index):
+            raw = await binding.tool_registry.execute(
+                "save",
+                {"value": str(index)},
+                uid="user",
+                allowed_tool_names={"save"},
+            )
+            payload = json.loads(raw) if isinstance(raw, str) else raw
+            self.assertEqual(expected, payload["application"])
+            self.assertEqual(str(index), payload["value"])
+
+        await asyncio.gather(*(
+            execute(binding, expected, index)
+            for binding, expected in ((binding_a, "A"), (binding_b, "B"))
+            for index in range(20)
+        ))
+        self.assertEqual(
+            20,
+            binding_a.tool_registry.get_stats("save")["save"]["call_count"],
+        )
+        self.assertEqual(
+            20,
+            binding_b.tool_registry.get_stats("save")["save"]["call_count"],
+        )
+
     def test_runtime_service_identity_is_not_part_of_config_hash(self):
         first = ApplicationRegistry().register(
             application(),