| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- import asyncio
- import json
- import unittest
- from pydantic import ValidationError
- from cyber_agent.application import (
- AgentApplication,
- AgentRole,
- ApplicationRegistry,
- ApplicationServices,
- ProviderRef,
- RoleRunLimits,
- RunLimits,
- SkillSpec,
- ToolSet,
- ToolSpec,
- )
- from cyber_agent.tools.registry import ToolRegistry
- def application(*, prompt="write carefully", provider_version="1", limit=20):
- return AgentApplication(
- application_id="creative.reference",
- application_version="0.1.0",
- root_role="writer",
- roles=(AgentRole(
- role_id="writer",
- model="fake-model",
- system_prompt=prompt,
- skills=(SkillSpec(
- name="evidence-writing",
- version="1",
- content="Use checked evidence.",
- ),),
- tool_sets=("writing",),
- allowed_child_roles=("writer",),
- run_limits=RoleRunLimits(max_iterations=limit),
- ),),
- tool_sets=(ToolSet(
- tool_set_id="writing",
- tools=(ToolSpec(
- tool_name="save",
- implementation_version="1",
- side_effect_class="write",
- idempotent=True,
- ),),
- ),),
- context_provider_ref=ProviderRef(
- provider_id="context",
- provider_version=provider_version,
- ),
- run_limits=RunLimits(max_iterations=30),
- )
- def registry(output):
- tools = ToolRegistry()
- async def save(value: str):
- return {"application": output, "value": value}
- tools.register(save, groups=["application"])
- return tools
- class ApplicationDeclarationTest(unittest.TestCase):
- def test_role_graph_and_tool_set_references_fail_closed(self):
- with self.assertRaisesRegex(ValidationError, "unknown child roles"):
- AgentApplication(
- application_id="broken",
- application_version="1",
- root_role="root",
- roles=(AgentRole(
- role_id="root",
- model="fake",
- system_prompt="root",
- allowed_child_roles=("missing",),
- ),),
- )
- with self.assertRaisesRegex(ValidationError, "unknown tool sets"):
- AgentApplication(
- application_id="broken",
- application_version="1",
- root_role="root",
- roles=(AgentRole(
- role_id="root",
- model="fake",
- system_prompt="root",
- tool_sets=("missing",),
- ),),
- )
- def test_registry_freezes_private_tool_copy_and_resets_stats(self):
- source = registry("A")
- source._stats["save"].call_count = 7
- binding = ApplicationRegistry().register(
- application(),
- ApplicationServices(
- tool_registry=source,
- context_provider=object(),
- ),
- )
- self.assertTrue(binding.tool_registry.frozen)
- self.assertEqual(0, binding.tool_registry.get_stats("save")["save"]["call_count"])
- with self.assertRaisesRegex(RuntimeError, "frozen"):
- binding.tool_registry.register(lambda: None)
- self.assertFalse(source.frozen)
- def test_same_tool_name_is_isolated_between_application_bindings(self):
- app_a = application()
- app_b = app_a.model_copy(update={
- "application_id": "creative.other",
- "roles": (
- app_a.roles[0].model_copy(update={"system_prompt": "other prompt"}),
- ),
- })
- 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(),
- ),
- )
- self.assertIsNot(binding_a.tool_registry, binding_b.tool_registry)
- self.assertNotEqual(
- binding_a.application_ref.config_hash,
- binding_b.application_ref.config_hash,
- )
- self.assertNotEqual(
- binding_a.role("writer").role_hash,
- binding_b.role("writer").role_hash,
- )
- def test_hash_covers_prompt_provider_limits_and_tool_schema(self):
- base = ApplicationRegistry().register(
- application(),
- ApplicationServices(
- tool_registry=registry("base"),
- context_provider=object(),
- ),
- ).application_ref.config_hash
- variants = [
- application(prompt="changed prompt"),
- application(provider_version="2"),
- application(limit=10),
- ]
- hashes = []
- for item in variants:
- hashes.append(ApplicationRegistry().register(
- item,
- ApplicationServices(
- tool_registry=registry("base"),
- context_provider=object(),
- ),
- ).application_ref.config_hash)
- self.assertTrue(all(item != base for item in hashes))
- changed_schema = registry("base")
- definition = changed_schema._tools["save"]
- definition["schema"] = {
- **definition["schema"],
- "function": {
- **definition["schema"]["function"],
- "description": "changed schema",
- },
- }
- schema_hash = ApplicationRegistry().register(
- application(),
- ApplicationServices(
- tool_registry=changed_schema,
- context_provider=object(),
- ),
- ).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(),
- ApplicationServices(
- tool_registry=registry("same"),
- context_provider=object(),
- ),
- )
- second = ApplicationRegistry().register(
- application(),
- ApplicationServices(
- tool_registry=registry("same"),
- context_provider=object(),
- ),
- )
- self.assertEqual(first.application_ref, second.application_ref)
- def test_missing_declared_provider_is_rejected(self):
- with self.assertRaisesRegex(ValueError, "context_provider"):
- ApplicationRegistry().register(
- application(),
- ApplicationServices(tool_registry=registry("A")),
- )
- if __name__ == "__main__":
- unittest.main()
|