From e0cf62f1c106a384dafaa554c2758c5d726c48f6 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:20:29 +0800 Subject: [PATCH 01/23] =?UTF-8?q?=F0=9F=93=9D=20(adr):=20Add=20hook=20arch?= =?UTF-8?q?itecture=20decision=20record?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the canonical hook architecture: Python owns monkey-patching and hook installation, Rust owns IPC transport. Architecture B (Rust-side hook dispatch) was superseded by AAASM-162. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- docs/adr/0001-hook-architecture.md | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/adr/0001-hook-architecture.md diff --git a/docs/adr/0001-hook-architecture.md b/docs/adr/0001-hook-architecture.md new file mode 100644 index 0000000..e3bc071 --- /dev/null +++ b/docs/adr/0001-hook-architecture.md @@ -0,0 +1,70 @@ +# ADR-0001: Canonical Hook Architecture + +## Status + +Accepted + +## Context + +The Python SDK needed a strategy for intercepting AI framework calls (LangChain, +LangGraph, CrewAI, Pydantic AI, OpenAI Agents, MCP) to enforce governance policies. + +Two architectures were considered: + +- **Architecture A (Python adapters)** — `FrameworkAdapter` ABC with + `register_hooks()`/`unregister_hooks()`, `AdapterRegistry` with `auto_detect()` + and entry-point discovery, per-framework patch classes with `apply()`/`revert()`. + +- **Architecture B (Rust FFI hooks)** — Rust-level `HOOK_MODULES` constant mapping + framework names to Python modules, `install_hooks()` Rust function calling + `install(handle)` on each module. + +Architecture B was planned but superseded by the AAASM-162 design decision before +implementation. + +## Decision + +**Python-side `.py` modules own monkey-patching and hook installation. +Rust owns IPC transport.** + +### Layer responsibilities + +| Layer | Owns | Does NOT own | +|---|---|---| +| Python adapters (`agent_assembly/adapters/`) | Framework detection, `auto_detect()`, monkey-patch installation/removal, framework-specific hook logic | IPC transport, protobuf serialization | +| Rust FFI (`agent_assembly/_core`) | IPC transport to sidecar (`RuntimeClient`), protobuf serialization, governance event types (`GovernanceEvent`, `PolicyResult`) | Framework detection, hook installation, monkey-patching | + +### Two-level naming convention + +The architecture uses two abstraction levels with distinct naming: + +- **Public adapter API** — `FrameworkAdapter` ABC with `register_hooks(interceptor)` + and `unregister_hooks()`. This is the interface SDK users and third-party plugin + authors interact with. It represents "register governance hooks for this framework." + +- **Internal patch mechanism** — `RuntimePatch` protocol with `apply()` and `revert()`. + This is how monkey-patching is implemented internally. Each adapter's + `register_hooks()` delegates to one or more `RuntimePatch` instances. + +Both naming levels are intentional and serve different audiences. + +### Single detection path + +`AdapterRegistry.auto_detect()` is the single entry point for framework detection. +`init_assembly()` routes through the registry — there is no parallel detection path. + +### Integration boundary + +The adapter's `register_hooks()` method is where the two layers connect. Adapter +patches intercept framework calls and route them through the `GatewayClient` (or +`AssemblyCallbackHandler`) to the governance gateway. When the native Rust FFI +(`RuntimeClient`) is available, it provides the IPC transport; otherwise the +`GatewayClient` uses HTTP. + +## Consequences + +- No `hooks.rs`, `detect.rs`, or Rust-side `install()/uninstall()` will be built. +- Third-party framework adapters can be registered via Python entry points and will + be automatically discovered by `init_assembly()`. +- All framework-specific patching logic remains in Python where it can be tested + without compiling Rust. From d7872bbcd15e85e2420f48ef904acd2df7bb971c Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:20:54 +0800 Subject: [PATCH 02/23] =?UTF-8?q?=F0=9F=93=9D=20(adapters):=20Add=20docstr?= =?UTF-8?q?ing=20to=20FrameworkAdapter=20ABC=20clarifying=20public=20adapt?= =?UTF-8?q?er=20contract?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explains the two-level naming: register_hooks()/unregister_hooks() is the public API, apply()/revert() is the internal RuntimePatch mechanism. References ADR-0001. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/base.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/agent_assembly/adapters/base.py b/agent_assembly/adapters/base.py index ca06325..7e8ba2a 100644 --- a/agent_assembly/adapters/base.py +++ b/agent_assembly/adapters/base.py @@ -16,8 +16,33 @@ class GovernanceInterceptor(Protocol): class FrameworkAdapter(ABC): """Abstract contract implemented by every framework adapter. - Adapters should be registered through `register()` so contract validation - errors are raised before framework hooks are attached. + This is the **public adapter API** — the interface that SDK users and + third-party plugin authors interact with. Each concrete adapter represents + one AI framework (e.g. LangChain, CrewAI) and knows how to install + governance hooks for that framework. + + The two key lifecycle methods are: + + - ``register_hooks(interceptor)`` — install framework-specific + monkey-patches that route intercepted calls through the governance + interceptor. Internally, each adapter delegates to one or more + ``RuntimePatch`` instances whose ``apply()`` method performs the + actual monkey-patching. + + - ``unregister_hooks()`` — tear down all patches installed by this + adapter, delegating to each patch's ``revert()`` method. + + Adapters are discovered and activated by ``AdapterRegistry.auto_detect()`` + which is the single detection path used by ``init_assembly()``. + + Adapters should be registered through ``register()`` so contract + validation errors are raised before framework hooks are attached. + + See Also: + ``RuntimePatch`` in ``core/assembly.py`` — the internal + monkey-patch protocol with ``apply()`` / ``revert()`` methods. + ADR-0001 (``docs/adr/0001-hook-architecture.md``) for the full + architecture rationale. """ @abstractmethod From bad70313e37fb54a1f869051b1f65a9292414112 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:21:14 +0800 Subject: [PATCH 03/23] =?UTF-8?q?=F0=9F=93=9D=20(core):=20Add=20docstring?= =?UTF-8?q?=20to=20RuntimePatch=20protocol=20clarifying=20internal=20mecha?= =?UTF-8?q?nism=20role?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Explains that RuntimePatch with apply()/revert() is the internal monkey-patch mechanism, distinct from the public FrameworkAdapter API. References ADR-0001. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/core/assembly.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/agent_assembly/core/assembly.py b/agent_assembly/core/assembly.py index 90a8cbe..01fd77f 100644 --- a/agent_assembly/core/assembly.py +++ b/agent_assembly/core/assembly.py @@ -28,6 +28,23 @@ class RuntimePatch(Protocol): + """Internal monkey-patch mechanism used by framework adapters. + + This is the **internal mechanism layer** — not intended for SDK users + or plugin authors. Each ``RuntimePatch`` knows how to apply and + revert a single monkey-patch on a specific framework class or function. + + A ``FrameworkAdapter``'s ``register_hooks()`` creates one or more + ``RuntimePatch`` instances and calls ``apply()`` on each. The + adapter's ``unregister_hooks()`` calls ``revert()`` on each in + reverse order. + + See Also: + ``FrameworkAdapter`` in ``adapters/base.py`` — the public adapter + API with ``register_hooks()`` / ``unregister_hooks()`` methods. + ADR-0001 (``docs/adr/0001-hook-architecture.md``). + """ + def apply(self) -> bool: ... def revert(self) -> None: ... From 58d15b74598e3ea27f675264f94d97f4e58c3ff9 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:22:02 +0800 Subject: [PATCH 04/23] =?UTF-8?q?=E2=9C=A8=20(langchain):=20Add=20LangChai?= =?UTF-8?q?nAdapter=20as=20FrameworkAdapter=20subclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps existing LangChainPatch with the public FrameworkAdapter API. register_hooks() creates and applies the patch, unregister_hooks() reverts it. Exposes get_callback_handler() for downstream adapters. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/langchain/adapter.py | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 agent_assembly/adapters/langchain/adapter.py diff --git a/agent_assembly/adapters/langchain/adapter.py b/agent_assembly/adapters/langchain/adapter.py new file mode 100644 index 0000000..869a63c --- /dev/null +++ b/agent_assembly/adapters/langchain/adapter.py @@ -0,0 +1,47 @@ +"""LangChain framework adapter.""" + +from __future__ import annotations + +from typing import Any + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor +from agent_assembly.adapters.langchain.patch import LangChainPatch +from agent_assembly.adapters.langchain.runtime import get_active_callback_handler + + +class LangChainAdapter(FrameworkAdapter): + """Adapter for LangChain framework governance hook installation.""" + + def __init__(self, *, process_agent_id: str | None = None) -> None: + self._process_agent_id = process_agent_id + self._patch: LangChainPatch | None = None + + @property + def process_agent_id(self) -> str | None: + return self._process_agent_id + + @process_agent_id.setter + def process_agent_id(self, value: str | None) -> None: + self._process_agent_id = value + + def get_framework_name(self) -> str: + return "langchain" + + def get_supported_versions(self) -> list[str]: + return [">=0.1.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + self._patch = LangChainPatch( + interceptor, + process_agent_id=self._process_agent_id, + ) + self._patch.apply() + + def unregister_hooks(self) -> None: + if self._patch is not None: + self._patch.revert() + self._patch = None + + def get_callback_handler(self) -> Any: + """Return the active callback handler created during hook registration.""" + return get_active_callback_handler() From 2175a355af1190cbc141cd82e82d5d43428997ca Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:22:11 +0800 Subject: [PATCH 05/23] =?UTF-8?q?=E2=9C=A8=20(langgraph):=20Add=20LangGrap?= =?UTF-8?q?hAdapter=20as=20FrameworkAdapter=20subclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps existing LangGraphPatch with the public FrameworkAdapter API. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/langgraph/adapter.py | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 agent_assembly/adapters/langgraph/adapter.py diff --git a/agent_assembly/adapters/langgraph/adapter.py b/agent_assembly/adapters/langgraph/adapter.py new file mode 100644 index 0000000..19d0a79 --- /dev/null +++ b/agent_assembly/adapters/langgraph/adapter.py @@ -0,0 +1,28 @@ +"""LangGraph framework adapter.""" + +from __future__ import annotations + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor +from agent_assembly.adapters.langgraph.patch import LangGraphPatch + + +class LangGraphAdapter(FrameworkAdapter): + """Adapter for LangGraph framework governance hook installation.""" + + def __init__(self) -> None: + self._patch: LangGraphPatch | None = None + + def get_framework_name(self) -> str: + return "langgraph" + + def get_supported_versions(self) -> list[str]: + return [">=0.1.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + self._patch = LangGraphPatch(interceptor) + self._patch.apply() + + def unregister_hooks(self) -> None: + if self._patch is not None: + self._patch.revert() + self._patch = None From 55d2d2bec4d2bd754b6f5b51dcf56c49ecd0ad0b Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:22:21 +0800 Subject: [PATCH 06/23] =?UTF-8?q?=E2=9C=A8=20(crewai):=20Add=20CrewAIAdapt?= =?UTF-8?q?er=20as=20FrameworkAdapter=20subclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps existing CrewAIPatch with the public FrameworkAdapter API. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/crewai/adapter.py | 28 +++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 agent_assembly/adapters/crewai/adapter.py diff --git a/agent_assembly/adapters/crewai/adapter.py b/agent_assembly/adapters/crewai/adapter.py new file mode 100644 index 0000000..2bb27f4 --- /dev/null +++ b/agent_assembly/adapters/crewai/adapter.py @@ -0,0 +1,28 @@ +"""CrewAI framework adapter.""" + +from __future__ import annotations + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor +from agent_assembly.adapters.crewai.patch import CrewAIPatch + + +class CrewAIAdapter(FrameworkAdapter): + """Adapter for CrewAI framework governance hook installation.""" + + def __init__(self) -> None: + self._patch: CrewAIPatch | None = None + + def get_framework_name(self) -> str: + return "crewai" + + def get_supported_versions(self) -> list[str]: + return [">=0.1.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + self._patch = CrewAIPatch(interceptor) + self._patch.apply() + + def unregister_hooks(self) -> None: + if self._patch is not None: + self._patch.revert() + self._patch = None From 263faee0eb802ec7bd0d052a525de271f498a085 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:22:32 +0800 Subject: [PATCH 07/23] =?UTF-8?q?=E2=9C=A8=20(pydantic=5Fai):=20Add=20Pyda?= =?UTF-8?q?nticAIAdapter=20as=20FrameworkAdapter=20subclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps existing PydanticAIPatch with the public FrameworkAdapter API. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- .../adapters/pydantic_ai/adapter.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 agent_assembly/adapters/pydantic_ai/adapter.py diff --git a/agent_assembly/adapters/pydantic_ai/adapter.py b/agent_assembly/adapters/pydantic_ai/adapter.py new file mode 100644 index 0000000..4903d30 --- /dev/null +++ b/agent_assembly/adapters/pydantic_ai/adapter.py @@ -0,0 +1,28 @@ +"""Pydantic AI framework adapter.""" + +from __future__ import annotations + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor +from agent_assembly.adapters.pydantic_ai.patch import PydanticAIPatch + + +class PydanticAIAdapter(FrameworkAdapter): + """Adapter for Pydantic AI framework governance hook installation.""" + + def __init__(self) -> None: + self._patch: PydanticAIPatch | None = None + + def get_framework_name(self) -> str: + return "pydantic_ai" + + def get_supported_versions(self) -> list[str]: + return [">=0.1.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + self._patch = PydanticAIPatch(interceptor) + self._patch.apply() + + def unregister_hooks(self) -> None: + if self._patch is not None: + self._patch.revert() + self._patch = None From c82780cf3595e4cd304fbbe7ef84ad0d6f02ee21 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:22:46 +0800 Subject: [PATCH 08/23] =?UTF-8?q?=E2=9C=A8=20(openai=5Fagents):=20Add=20Op?= =?UTF-8?q?enAIAgentsAdapter=20as=20FrameworkAdapter=20subclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps existing OpenAIAgentsPatch with the public FrameworkAdapter API. Overrides is_available() to check for openai.agents module specifically. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- .../adapters/openai_agents/adapter.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 agent_assembly/adapters/openai_agents/adapter.py diff --git a/agent_assembly/adapters/openai_agents/adapter.py b/agent_assembly/adapters/openai_agents/adapter.py new file mode 100644 index 0000000..642d836 --- /dev/null +++ b/agent_assembly/adapters/openai_agents/adapter.py @@ -0,0 +1,46 @@ +"""OpenAI Agents framework adapter.""" + +from __future__ import annotations + +import importlib.util + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor +from agent_assembly.adapters.openai_agents.patch import OpenAIAgentsPatch + + +class OpenAIAgentsAdapter(FrameworkAdapter): + """Adapter for OpenAI Agents SDK governance hook installation.""" + + def __init__(self, *, process_agent_id: str | None = None) -> None: + self._process_agent_id = process_agent_id + self._patch: OpenAIAgentsPatch | None = None + + @property + def process_agent_id(self) -> str | None: + return self._process_agent_id + + @process_agent_id.setter + def process_agent_id(self, value: str | None) -> None: + self._process_agent_id = value + + def get_framework_name(self) -> str: + return "openai" + + def get_supported_versions(self) -> list[str]: + return [">=1.0.0"] + + def is_available(self) -> bool: + """Check specifically for openai.agents module, not just openai base.""" + return importlib.util.find_spec("openai.agents") is not None + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + self._patch = OpenAIAgentsPatch( + callback_handler=interceptor, + process_agent_id=self._process_agent_id, + ) + self._patch.apply() + + def unregister_hooks(self) -> None: + if self._patch is not None: + self._patch.revert() + self._patch = None From fa6958ecfd33440d803829e69218c4dbdf3fd018 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:22:57 +0800 Subject: [PATCH 09/23] =?UTF-8?q?=E2=9C=A8=20(mcp):=20Add=20MCPAdapter=20a?= =?UTF-8?q?s=20FrameworkAdapter=20subclass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps existing MCPClientPatch with the public FrameworkAdapter API. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/mcp/adapter.py | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 agent_assembly/adapters/mcp/adapter.py diff --git a/agent_assembly/adapters/mcp/adapter.py b/agent_assembly/adapters/mcp/adapter.py new file mode 100644 index 0000000..4c0f9b2 --- /dev/null +++ b/agent_assembly/adapters/mcp/adapter.py @@ -0,0 +1,40 @@ +"""MCP framework adapter.""" + +from __future__ import annotations + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor +from agent_assembly.adapters.mcp.patch import MCPClientPatch + + +class MCPAdapter(FrameworkAdapter): + """Adapter for MCP client governance hook installation.""" + + def __init__(self, *, process_agent_id: str | None = None) -> None: + self._process_agent_id = process_agent_id + self._patch: MCPClientPatch | None = None + + @property + def process_agent_id(self) -> str | None: + return self._process_agent_id + + @process_agent_id.setter + def process_agent_id(self, value: str | None) -> None: + self._process_agent_id = value + + def get_framework_name(self) -> str: + return "mcp" + + def get_supported_versions(self) -> list[str]: + return [">=1.0.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + self._patch = MCPClientPatch( + callback_handler=interceptor, + process_agent_id=self._process_agent_id, + ) + self._patch.apply() + + def unregister_hooks(self) -> None: + if self._patch is not None: + self._patch.revert() + self._patch = None From 834c5309777b2f6113e5baada29ed2c8764fad37 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:23:28 +0800 Subject: [PATCH 10/23] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(registry):=20Replac?= =?UTF-8?q?e=20=5FBuiltinPlaceholderAdapter=20with=20real=20adapter=20clas?= =?UTF-8?q?ses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swap placeholder stubs for LangChainAdapter, LangGraphAdapter, CrewAIAdapter, and PydanticAIAdapter in AdapterRegistry.__init__(). Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/registry.py | 39 ++++++++--------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/agent_assembly/adapters/registry.py b/agent_assembly/adapters/registry.py index 3c620e6..b239295 100644 --- a/agent_assembly/adapters/registry.py +++ b/agent_assembly/adapters/registry.py @@ -6,6 +6,10 @@ from typing import Callable, Literal from agent_assembly.adapters.base import FrameworkAdapter +from agent_assembly.adapters.crewai.adapter import CrewAIAdapter +from agent_assembly.adapters.langchain.adapter import LangChainAdapter +from agent_assembly.adapters.langgraph.adapter import LangGraphAdapter +from agent_assembly.adapters.pydantic_ai.adapter import PydanticAIAdapter @dataclass(frozen=True, slots=True) @@ -16,24 +20,6 @@ class AdapterInfo: hooks_registered: int -class _BuiltinPlaceholderAdapter(FrameworkAdapter): - def __init__(self, framework_name: str, import_name: str | None = None) -> None: - self._framework_name = framework_name - self._import_name = import_name or framework_name - - def get_framework_name(self) -> str: - return self._import_name - - def get_supported_versions(self) -> list[str]: - return [">=0.0.0"] - - def register_hooks(self, interceptor: object) -> None: - return None - - def unregister_hooks(self) -> None: - return None - - def _noop_interceptor_method(*args: object, **kwargs: object) -> None: del args, kwargs return None @@ -55,17 +41,14 @@ def __init__(self) -> None: self._active: dict[str, FrameworkAdapter] = {} self._errors: dict[str, str] = {} self._discovered_entry_points: set[str] = set() - builtin_frameworks = [ - ("langchain", "langchain"), - ("langgraph", "langgraph"), - ("crewai", "crewai"), - ("pydantic-ai", "pydantic_ai"), + builtin_adapters: list[FrameworkAdapter] = [ + LangChainAdapter(), + LangGraphAdapter(), + CrewAIAdapter(), + PydanticAIAdapter(), ] - for registry_name, import_name in builtin_frameworks: - self._registered[registry_name] = _BuiltinPlaceholderAdapter( - framework_name=registry_name, - import_name=import_name, - ) + for adapter in builtin_adapters: + self._registered[adapter.get_framework_name()] = adapter def register(self, adapter: FrameworkAdapter) -> None: adapter_name = adapter.get_framework_name() From b3fdf9c0c3cb195f4af99c403b1a17422e61bef0 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:23:58 +0800 Subject: [PATCH 11/23] =?UTF-8?q?=E2=9C=A8=20(registry):=20Register=20Open?= =?UTF-8?q?AIAgentsAdapter=20and=20MCPAdapter=20as=20builtin=20adapters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously missing from registry — now all 6 framework adapters are registered as builtins. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/registry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/agent_assembly/adapters/registry.py b/agent_assembly/adapters/registry.py index b239295..39d6f17 100644 --- a/agent_assembly/adapters/registry.py +++ b/agent_assembly/adapters/registry.py @@ -9,6 +9,8 @@ from agent_assembly.adapters.crewai.adapter import CrewAIAdapter from agent_assembly.adapters.langchain.adapter import LangChainAdapter from agent_assembly.adapters.langgraph.adapter import LangGraphAdapter +from agent_assembly.adapters.mcp.adapter import MCPAdapter +from agent_assembly.adapters.openai_agents.adapter import OpenAIAgentsAdapter from agent_assembly.adapters.pydantic_ai.adapter import PydanticAIAdapter @@ -46,6 +48,8 @@ def __init__(self) -> None: LangGraphAdapter(), CrewAIAdapter(), PydanticAIAdapter(), + OpenAIAgentsAdapter(), + MCPAdapter(), ] for adapter in builtin_adapters: self._registered[adapter.get_framework_name()] = adapter From daf6b936337502e87ef3d050741c1b4f86f10478 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:24:41 +0800 Subject: [PATCH 12/23] =?UTF-8?q?=E2=9C=A8=20(registry):=20Add=20adapter?= =?UTF-8?q?=20priority=20ordering=20and=20get=5Favailable=5Fadapters=5Fby?= =?UTF-8?q?=5Fpriority?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LangChain must be first (callback handler threading), MCP must be last (fallback). New method returns available adapters in priority order without calling register_hooks() — init_assembly() handles that. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/registry.py | 40 +++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/agent_assembly/adapters/registry.py b/agent_assembly/adapters/registry.py index 39d6f17..6e32717 100644 --- a/agent_assembly/adapters/registry.py +++ b/agent_assembly/adapters/registry.py @@ -14,6 +14,21 @@ from agent_assembly.adapters.pydantic_ai.adapter import PydanticAIAdapter +# LangChain must be first: its callback handler threads through to all +# subsequent adapters. MCP must be last: it acts as a fallback for +# remaining tool dispatch paths. +_ADAPTER_PRIORITY: dict[str, int] = { + "langchain": 0, + "langgraph": 1, + "crewai": 2, + "pydantic_ai": 3, + "openai": 4, + "mcp": 99, +} + +_DEFAULT_PRIORITY = 50 + + @dataclass(frozen=True, slots=True) class AdapterInfo: name: str @@ -104,6 +119,31 @@ def list_active(self) -> list[AdapterInfo]: return sorted(result, key=lambda info: info.name) + def get_available_adapters_by_priority(self) -> list[FrameworkAdapter]: + """Return available adapters sorted by registration priority. + + This method discovers entry-point adapters, checks availability, + and returns adapters in the order they should be registered by + ``init_assembly()``. It does **not** call ``register_hooks()`` + — that is the caller's responsibility. + """ + self._discover_entry_point_adapters() + + with self._lock: + registered_items = list(self._registered.items()) + + available: list[FrameworkAdapter] = [] + for _name, adapter in registered_items: + if adapter.is_available(): + available.append(adapter) + + available.sort( + key=lambda a: _ADAPTER_PRIORITY.get( + a.get_framework_name(), _DEFAULT_PRIORITY + ) + ) + return available + def _discover_entry_point_adapters(self) -> list[str]: discovered: list[str] = [] entry_points = metadata.entry_points() From 79ffb15257cb54483d5311b7f3216f8d1aedae2e Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:25:19 +0800 Subject: [PATCH 13/23] =?UTF-8?q?=E2=9C=A8=20(adapters):=20Add=20set=5Fpro?= =?UTF-8?q?cess=5Fagent=5Fid=20to=20FrameworkAdapter=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows init_assembly() to propagate the agent ID to adapters that need it without knowing which adapters require it. No-op for adapters without a process_agent_id property. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/agent_assembly/adapters/base.py b/agent_assembly/adapters/base.py index 7e8ba2a..8a8bffc 100644 --- a/agent_assembly/adapters/base.py +++ b/agent_assembly/adapters/base.py @@ -136,6 +136,16 @@ def is_available(self) -> bool: return True + def set_process_agent_id(self, agent_id: str | None) -> None: + """Set the process-level agent ID for governance event attribution. + + Adapters that need an agent ID (e.g. LangChain, OpenAI Agents, MCP) + override ``process_agent_id`` as a property. This base method is + a no-op for adapters that do not use an agent ID. + """ + if hasattr(self, "process_agent_id"): + self.process_agent_id = agent_id # type: ignore[attr-defined] + def get_active_version(self) -> str | None: """Return framework `__version__` when present, otherwise `None`. From a427a7fe2678067a437b4b2199077a6d054772af Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:26:54 +0800 Subject: [PATCH 14/23] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(core):=20Route=20in?= =?UTF-8?q?it=5Fassembly()=20through=20AdapterRegistry=20as=20single=20det?= =?UTF-8?q?ection=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace _build_patch_plan() with AdapterRegistry.get_available_adapters_by_priority(). AssemblyContext now holds adapters instead of patches. Shutdown calls adapter.unregister_hooks() instead of patch.revert(). Remove dead code: _build_patch_plan, _is_installed, _has_agents_sdk, _apply_runtime_patches, _revert_patches, _replace_callback_targets. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/core/assembly.py | 123 ++++++++++++-------------------- 1 file changed, 45 insertions(+), 78 deletions(-) diff --git a/agent_assembly/core/assembly.py b/agent_assembly/core/assembly.py index 01fd77f..5327380 100644 --- a/agent_assembly/core/assembly.py +++ b/agent_assembly/core/assembly.py @@ -3,18 +3,14 @@ from __future__ import annotations from dataclasses import dataclass, field -import importlib.util import sys from threading import Lock from typing import Any, Callable, Literal, Protocol -from agent_assembly.adapters.crewai.patch import CrewAIPatch -from agent_assembly.adapters.langchain.patch import LangChainPatch +from agent_assembly.adapters.base import FrameworkAdapter +from agent_assembly.adapters.langchain.adapter import LangChainAdapter from agent_assembly.adapters.langchain.runtime import get_active_callback_handler -from agent_assembly.adapters.langgraph import LangGraphPatch -from agent_assembly.adapters.mcp import MCPClientPatch -from agent_assembly.adapters.openai_agents import OpenAIAgentsPatch -from agent_assembly.adapters.pydantic_ai.patch import PydanticAIPatch +from agent_assembly.adapters.registry import AdapterRegistry from agent_assembly.client.gateway import GatewayClient from agent_assembly.exceptions import AssemblyError, ConfigurationError @@ -55,7 +51,7 @@ class AssemblyContext: """Represents an active assembly runtime session.""" client: GatewayClient - patches: list[RuntimePatch] + adapters: list[FrameworkAdapter] network_mode: NetworkMode _network_shutdown: Callable[[], None] _lock: Lock = field(default_factory=Lock, init=False, repr=False) @@ -86,11 +82,11 @@ def shutdown(self) -> None: except Exception as error: # pragma: no cover - defensive guard shutdown_errors.append(f"network shutdown failed: {error}") - for patch in reversed(self.patches): + for adapter in reversed(self.adapters): try: - patch.revert() + adapter.unregister_hooks() except Exception as error: # pragma: no cover - defensive guard - shutdown_errors.append(f"patch revert failed: {error}") + shutdown_errors.append(f"adapter hook removal failed: {error}") try: self.client.close() @@ -113,7 +109,11 @@ def init_assembly( mode: RuntimeMode = "auto", **kwargs: Any, ) -> AssemblyContext: - """Initialize the Agent Assembly SDK runtime for this process.""" + """Initialize the Agent Assembly SDK runtime for this process. + + Uses ``AdapterRegistry.get_available_adapters_by_priority()`` as the + single detection path for framework adapters (see ADR-0001). + """ del kwargs _validate_inputs(gateway_url=gateway_url, api_key=api_key, mode=mode) resolved_agent_id = agent_id or _DEFAULT_AGENT_ID @@ -135,23 +135,23 @@ def init_assembly( api_key=api_key, ) - patches: list[RuntimePatch] = [] + registered_adapters: list[FrameworkAdapter] = [] network_mode: NetworkMode = "sdk-only" network_shutdown: Callable[[], None] = _noop_shutdown try: - patches = _apply_runtime_patches( + registered_adapters = _register_adapters( client=client, process_agent_id=resolved_agent_id, ) network_mode, network_shutdown = _start_network_layer(client=client, mode=mode) except Exception as error: - _revert_patches(patches) + _unregister_adapters(registered_adapters) client.close() raise ConfigurationError(f"Failed to initialize assembly runtime: {error}") from error context = AssemblyContext( client=client, - patches=patches, + adapters=registered_adapters, network_mode=network_mode, _network_shutdown=network_shutdown, ) @@ -170,73 +170,46 @@ def _validate_inputs(*, gateway_url: str, api_key: str, mode: RuntimeMode) -> No ) -def _is_installed(package: str) -> bool: - """Check if a package is importable without importing it.""" - try: - return importlib.util.find_spec(package) is not None - except (ImportError, AttributeError, ValueError): - return False - - -def _has_agents_sdk() -> bool: - """Check specifically for openai.agents module (not just openai base).""" - return _is_installed("openai.agents") +def _register_adapters( + client: GatewayClient, + process_agent_id: str, +) -> list[FrameworkAdapter]: + """Detect available frameworks via AdapterRegistry and register hooks. + Adapters are returned in priority order. LangChain is registered first + so its ``AssemblyCallbackHandler`` can thread through to subsequent + adapters as the governance interceptor. + """ + registry = AdapterRegistry() + adapters = registry.get_available_adapters_by_priority() -def _build_patch_plan(client: GatewayClient, process_agent_id: str) -> list[RuntimePatch]: - patch_plan: list[RuntimePatch] = [] - langchain_installed = _is_installed("langchain") - langgraph_installed = _is_installed("langgraph") - callback_target: Any = client - - if langchain_installed or langgraph_installed: - patch_plan.append(LangChainPatch(client, process_agent_id=process_agent_id)) - callback_handler = get_active_callback_handler() - if callback_handler is not None: - callback_target = callback_handler - - if langgraph_installed: - patch_plan.append(LangGraphPatch(callback_target)) + registered: list[FrameworkAdapter] = [] + interceptor: Any = client - if _is_installed("crewai"): - patch_plan.append(CrewAIPatch(callback_target)) - if _is_installed("pydantic_ai"): - patch_plan.append(PydanticAIPatch(callback_target)) - if _is_installed("openai") and _has_agents_sdk(): - patch_plan.append( - OpenAIAgentsPatch( - callback_handler=callback_target, - process_agent_id=process_agent_id, - ) - ) - if _is_installed("mcp"): - # Keep MCP patch last as fallback for remaining tool dispatch paths. - patch_plan.append( - MCPClientPatch( - callback_handler=callback_target, - process_agent_id=process_agent_id, - ) - ) + for adapter in adapters: + adapter.set_process_agent_id(process_agent_id) - return patch_plan + try: + adapter.register_hooks(interceptor) + except Exception: + continue + registered.append(adapter) -def _apply_runtime_patches(client: GatewayClient, process_agent_id: str) -> list[RuntimePatch]: - applied: list[RuntimePatch] = [] - patch_plan = _build_patch_plan(client=client, process_agent_id=process_agent_id) - for index, patch in enumerate(patch_plan): - if patch.apply(): - applied.append(patch) + # After LangChain registers, its callback handler becomes the + # interceptor for all subsequent adapters. + if isinstance(adapter, LangChainAdapter): callback_handler = get_active_callback_handler() if callback_handler is not None: - _replace_callback_targets(patch_plan[index + 1 :], callback_handler) - return applied + interceptor = callback_handler + + return registered -def _revert_patches(patches: list[RuntimePatch]) -> None: - for patch in reversed(patches): +def _unregister_adapters(adapters: list[FrameworkAdapter]) -> None: + for adapter in reversed(adapters): try: - patch.revert() + adapter.unregister_hooks() except Exception: continue @@ -283,12 +256,6 @@ def _clear_active_context(context: AssemblyContext) -> None: _ACTIVE_CONTEXT = None -def _replace_callback_targets(patches: list[RuntimePatch], callback_handler: Any) -> None: - for patch in patches: - if hasattr(patch, "callback_handler"): - setattr(patch, "callback_handler", callback_handler) - - def _validate_active_context_compatibility( context: AssemblyContext, *, From 21c575536c8be487aef2a2ad7a49504360c1c560 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:27:41 +0800 Subject: [PATCH 15/23] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(adapters):=20Update?= =?UTF-8?q?=20adapter=20=5F=5Finit=5F=5F.py=20exports=20to=20include=20new?= =?UTF-8?q?=20adapter=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All six adapter packages now export their FrameworkAdapter subclass alongside the existing patch class. Refs AAASM-193 Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/crewai/__init__.py | 3 ++- agent_assembly/adapters/langchain/__init__.py | 2 ++ agent_assembly/adapters/langgraph/__init__.py | 3 ++- agent_assembly/adapters/mcp/__init__.py | 3 ++- agent_assembly/adapters/openai_agents/__init__.py | 3 ++- agent_assembly/adapters/pydantic_ai/__init__.py | 3 ++- 6 files changed, 12 insertions(+), 5 deletions(-) diff --git a/agent_assembly/adapters/crewai/__init__.py b/agent_assembly/adapters/crewai/__init__.py index e050dae..1675f01 100644 --- a/agent_assembly/adapters/crewai/__init__.py +++ b/agent_assembly/adapters/crewai/__init__.py @@ -1,5 +1,6 @@ """CrewAI adapter package.""" +from agent_assembly.adapters.crewai.adapter import CrewAIAdapter from agent_assembly.adapters.crewai.patch import CrewAIPatch -__all__ = ["CrewAIPatch"] +__all__ = ["CrewAIAdapter", "CrewAIPatch"] diff --git a/agent_assembly/adapters/langchain/__init__.py b/agent_assembly/adapters/langchain/__init__.py index 15a820a..17676f7 100644 --- a/agent_assembly/adapters/langchain/__init__.py +++ b/agent_assembly/adapters/langchain/__init__.py @@ -1,5 +1,6 @@ """LangChain adapter package.""" +from agent_assembly.adapters.langchain.adapter import LangChainAdapter from agent_assembly.adapters.langchain.callback_handler import AssemblyCallbackHandler from agent_assembly.adapters.langgraph import LangGraphPatch from agent_assembly.adapters.langchain.langgraph_patch import patch_stategraph_compile @@ -10,6 +11,7 @@ ) __all__ = [ + "LangChainAdapter", "AssemblyCallbackHandler", "LangChainPatch", "LangGraphPatch", diff --git a/agent_assembly/adapters/langgraph/__init__.py b/agent_assembly/adapters/langgraph/__init__.py index 6f7f2db..d40eb2c 100644 --- a/agent_assembly/adapters/langgraph/__init__.py +++ b/agent_assembly/adapters/langgraph/__init__.py @@ -1,5 +1,6 @@ """LangGraph adapter package.""" +from agent_assembly.adapters.langgraph.adapter import LangGraphAdapter from agent_assembly.adapters.langgraph.patch import LangGraphPatch -__all__ = ["LangGraphPatch"] +__all__ = ["LangGraphAdapter", "LangGraphPatch"] diff --git a/agent_assembly/adapters/mcp/__init__.py b/agent_assembly/adapters/mcp/__init__.py index 6dd19af..b10cbfe 100644 --- a/agent_assembly/adapters/mcp/__init__.py +++ b/agent_assembly/adapters/mcp/__init__.py @@ -1,5 +1,6 @@ """MCP adapter package.""" +from agent_assembly.adapters.mcp.adapter import MCPAdapter from agent_assembly.adapters.mcp.patch import MCPClientPatch -__all__ = ["MCPClientPatch"] +__all__ = ["MCPAdapter", "MCPClientPatch"] diff --git a/agent_assembly/adapters/openai_agents/__init__.py b/agent_assembly/adapters/openai_agents/__init__.py index bcfe1ac..467b0f8 100644 --- a/agent_assembly/adapters/openai_agents/__init__.py +++ b/agent_assembly/adapters/openai_agents/__init__.py @@ -1,5 +1,6 @@ """OpenAI Agents adapter package.""" +from agent_assembly.adapters.openai_agents.adapter import OpenAIAgentsAdapter from agent_assembly.adapters.openai_agents.patch import OpenAIAgentsPatch -__all__ = ["OpenAIAgentsPatch"] +__all__ = ["OpenAIAgentsAdapter", "OpenAIAgentsPatch"] diff --git a/agent_assembly/adapters/pydantic_ai/__init__.py b/agent_assembly/adapters/pydantic_ai/__init__.py index 328e92a..b8afb8c 100644 --- a/agent_assembly/adapters/pydantic_ai/__init__.py +++ b/agent_assembly/adapters/pydantic_ai/__init__.py @@ -1,5 +1,6 @@ """Pydantic AI adapter package.""" +from agent_assembly.adapters.pydantic_ai.adapter import PydanticAIAdapter from agent_assembly.adapters.pydantic_ai.patch import PydanticAIPatch -__all__ = ["PydanticAIPatch"] +__all__ = ["PydanticAIAdapter", "PydanticAIPatch"] From 9cbf2e8b13a481c4cac92241351271253717bd77 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:30:50 +0800 Subject: [PATCH 16/23] =?UTF-8?q?=E2=9C=85=20test(assembly):=20Update=20te?= =?UTF-8?q?st=5Fassembly.py=20for=20adapter-based=20architecture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all references to removed functions (_apply_runtime_patches, _revert_patches, _build_patch_plan) with new adapter-based equivalents. Change context.patches to context.adapters throughout. Co-Authored-By: Claude Opus 4.6 --- test/unit/test_assembly.py | 287 +++++++------------------------------ 1 file changed, 52 insertions(+), 235 deletions(-) diff --git a/test/unit/test_assembly.py b/test/unit/test_assembly.py index 77afe9c..6873a99 100644 --- a/test/unit/test_assembly.py +++ b/test/unit/test_assembly.py @@ -7,10 +7,31 @@ import pytest from agent_assembly import init_assembly +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor from agent_assembly.core import assembly as core_assembly from agent_assembly.exceptions import AssemblyError, ConfigurationError +class _FakeAdapter(FrameworkAdapter): + """Minimal adapter for testing init_assembly plumbing.""" + + def __init__(self, name: str = "fake") -> None: + self._name = name + self._registered = False + + def get_framework_name(self) -> str: + return self._name + + def get_supported_versions(self) -> list[str]: + return [">=0.0.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + self._registered = True + + def unregister_hooks(self) -> None: + self._registered = False + + @pytest.fixture(autouse=True) def cleanup_active_context() -> None: active_context = core_assembly._ACTIVE_CONTEXT @@ -20,7 +41,7 @@ def cleanup_active_context() -> None: def test_init_assembly_with_valid_config_returns_context(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(core_assembly, "_apply_runtime_patches", lambda **kwargs: []) + monkeypatch.setattr(core_assembly, "_register_adapters", lambda **kwargs: []) monkeypatch.setattr( core_assembly, "_start_network_layer", @@ -38,7 +59,7 @@ def test_init_assembly_with_valid_config_returns_context(monkeypatch: pytest.Mon assert context.client.gateway_url == "http://localhost:8080" assert context.client.api_key == "test-api-key" assert context.network_mode == "sdk-only" - assert context.patches == [] + assert context.adapters == [] finally: context.shutdown() @@ -66,199 +87,6 @@ def test_init_assembly_with_invalid_config() -> None: ) -def test_is_installed_uses_find_spec(monkeypatch: pytest.MonkeyPatch) -> None: - calls: list[str] = [] - - def fake_find_spec(package: str) -> object | None: - calls.append(package) - if package == "installed_pkg": - return object() - return None - - monkeypatch.setattr(core_assembly.importlib.util, "find_spec", fake_find_spec) - - assert core_assembly._is_installed("installed_pkg") is True - assert core_assembly._is_installed("missing_pkg") is False - assert calls == ["installed_pkg", "missing_pkg"] - - -def test_is_installed_handles_find_spec_errors(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr( - core_assembly.importlib.util, - "find_spec", - lambda package: (_ for _ in ()).throw(ValueError(package)), - ) - assert core_assembly._is_installed("bad_pkg") is False - - -def test_has_agents_sdk_checks_openai_agents_module(monkeypatch: pytest.MonkeyPatch) -> None: - checked: list[str] = [] - monkeypatch.setattr( - core_assembly, - "_is_installed", - lambda package: checked.append(package) or True, - ) - assert core_assembly._has_agents_sdk() is True - assert checked == ["openai.agents"] - - -def test_build_patch_plan_langgraph_order_and_mcp_last( - monkeypatch: pytest.MonkeyPatch, -) -> None: - created: list[str] = [] - - class _FakePatch: - def __init__(self, name: str) -> None: - self.name = name - - def apply(self) -> bool: - return True - - def revert(self) -> None: - return None - - monkeypatch.setattr( - core_assembly, - "_is_installed", - lambda package: package - in {"langchain", "langgraph", "crewai", "pydantic_ai", "openai", "mcp"}, - ) - monkeypatch.setattr(core_assembly, "_has_agents_sdk", lambda: True) - monkeypatch.setattr(core_assembly, "get_active_callback_handler", lambda: object()) - - monkeypatch.setattr( - core_assembly, - "LangChainPatch", - lambda *args, **kwargs: created.append("langchain") or _FakePatch("langchain"), - ) - monkeypatch.setattr( - core_assembly, - "LangGraphPatch", - lambda *args, **kwargs: created.append("langgraph") or _FakePatch("langgraph"), - ) - monkeypatch.setattr( - core_assembly, - "CrewAIPatch", - lambda *args, **kwargs: created.append("crewai") or _FakePatch("crewai"), - ) - monkeypatch.setattr( - core_assembly, - "PydanticAIPatch", - lambda *args, **kwargs: created.append("pydantic_ai") or _FakePatch("pydantic_ai"), - ) - monkeypatch.setattr( - core_assembly, - "OpenAIAgentsPatch", - lambda *args, **kwargs: created.append("openai_agents") or _FakePatch("openai_agents"), - ) - monkeypatch.setattr( - core_assembly, - "MCPClientPatch", - lambda *args, **kwargs: created.append("mcp") or _FakePatch("mcp"), - ) - - patch_plan = core_assembly._build_patch_plan(client=object(), process_agent_id="agent-1") - - assert [patch.name for patch in patch_plan] == [ - "langchain", - "langgraph", - "crewai", - "pydantic_ai", - "openai_agents", - "mcp", - ] - assert created[-1] == "mcp" - - -def test_build_patch_plan_uses_langchain_bridge_for_langgraph_only( - monkeypatch: pytest.MonkeyPatch, -) -> None: - created: list[str] = [] - - class _FakePatch: - def __init__(self, name: str) -> None: - self.name = name - - def apply(self) -> bool: - return True - - def revert(self) -> None: - return None - - monkeypatch.setattr( - core_assembly, - "_is_installed", - lambda package: package in {"langgraph", "crewai", "pydantic_ai", "mcp"}, - ) - monkeypatch.setattr(core_assembly, "_has_agents_sdk", lambda: False) - monkeypatch.setattr(core_assembly, "get_active_callback_handler", lambda: None) - monkeypatch.setattr( - core_assembly, - "LangChainPatch", - lambda *args, **kwargs: created.append("langchain") or _FakePatch("langchain"), - ) - monkeypatch.setattr( - core_assembly, - "LangGraphPatch", - lambda *args, **kwargs: created.append("langgraph") or _FakePatch("langgraph"), - ) - monkeypatch.setattr( - core_assembly, - "CrewAIPatch", - lambda *args, **kwargs: created.append("crewai") or _FakePatch("crewai"), - ) - monkeypatch.setattr( - core_assembly, - "PydanticAIPatch", - lambda *args, **kwargs: created.append("pydantic_ai") or _FakePatch("pydantic_ai"), - ) - monkeypatch.setattr( - core_assembly, - "MCPClientPatch", - lambda *args, **kwargs: created.append("mcp") or _FakePatch("mcp"), - ) - - patch_plan = core_assembly._build_patch_plan(client=object(), process_agent_id="agent-1") - assert [patch.name for patch in patch_plan] == [ - "langchain", - "langgraph", - "crewai", - "pydantic_ai", - "mcp", - ] - - -def test_apply_runtime_patches_replaces_callback_targets( - monkeypatch: pytest.MonkeyPatch, -) -> None: - callback_targets: list[object] = [] - - class _FakePatch: - def __init__(self, name: str, *, callback_handler: object | None = None) -> None: - self.name = name - self.callback_handler = callback_handler - - def apply(self) -> bool: - callback_targets.append(self.callback_handler) - return True - - def revert(self) -> None: - return None - - patch_plan = [ - _FakePatch("langchain"), - _FakePatch("crewai", callback_handler="initial"), - _FakePatch("mcp", callback_handler="initial"), - ] - - monkeypatch.setattr(core_assembly, "_build_patch_plan", lambda **kwargs: patch_plan) - monkeypatch.setattr(core_assembly, "get_active_callback_handler", lambda: "runtime-callback") - - applied = core_assembly._apply_runtime_patches(client=object(), process_agent_id="agent-1") - assert applied == patch_plan - assert callback_targets == [None, "runtime-callback", "runtime-callback"] - - def test_mode_sdk_only_skips_network_layer() -> None: network_mode, shutdown = core_assembly._start_network_layer(client=object(), mode="sdk-only") assert network_mode == "sdk-only" @@ -306,26 +134,22 @@ def test_mode_ebpf_raises_on_unsupported_platform( core_assembly._start_network_layer(client=object(), mode="ebpf") -def test_context_manager_shutdown_reverts_applied_patches( +def test_context_manager_shutdown_calls_adapter_unregister_hooks( monkeypatch: pytest.MonkeyPatch, ) -> None: events: list[str] = [] - class _Patch: - def __init__(self, name: str) -> None: - self.name = name - - def apply(self) -> bool: - events.append(f"apply:{self.name}") - return True + class _TrackingAdapter(_FakeAdapter): + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + events.append(f"register:{self._name}") - def revert(self) -> None: - events.append(f"revert:{self.name}") + def unregister_hooks(self) -> None: + events.append(f"unregister:{self._name}") monkeypatch.setattr( core_assembly, - "_apply_runtime_patches", - lambda **kwargs: [_Patch("a"), _Patch("b")], + "_register_adapters", + lambda **kwargs: [_TrackingAdapter("a"), _TrackingAdapter("b")], ) monkeypatch.setattr( core_assembly, @@ -339,14 +163,14 @@ def revert(self) -> None: ) as context: assert context.is_shutdown is False - assert events == ["revert:b", "revert:a"] + assert events == ["unregister:b", "unregister:a"] assert context.is_shutdown is True def test_init_assembly_rejects_conflicting_reinit( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(core_assembly, "_apply_runtime_patches", lambda **kwargs: []) + monkeypatch.setattr(core_assembly, "_register_adapters", lambda **kwargs: []) monkeypatch.setattr( core_assembly, "_start_network_layer", @@ -372,7 +196,7 @@ def test_init_assembly_rejects_conflicting_reinit( def test_init_assembly_rejects_conflicting_gateway_and_api_key( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(core_assembly, "_apply_runtime_patches", lambda **kwargs: []) + monkeypatch.setattr(core_assembly, "_register_adapters", lambda **kwargs: []) monkeypatch.setattr( core_assembly, "_start_network_layer", @@ -402,12 +226,9 @@ def test_init_assembly_rejects_conflicting_gateway_and_api_key( def test_context_shutdown_aggregates_errors() -> None: - class _FailingPatch: - def apply(self) -> bool: - return True - - def revert(self) -> None: - raise RuntimeError("patch failure") + class _FailingAdapter(_FakeAdapter): + def unregister_hooks(self) -> None: + raise RuntimeError("adapter failure") class _FailingClient: gateway_url = "http://localhost:8080" @@ -419,7 +240,7 @@ def close(self) -> None: context = core_assembly.AssemblyContext( client=_FailingClient(), # type: ignore[arg-type] - patches=[_FailingPatch()], + adapters=[_FailingAdapter("fail")], network_mode="sdk-only", _network_shutdown=lambda: (_ for _ in ()).throw(RuntimeError("network failure")), ) @@ -428,22 +249,18 @@ def close(self) -> None: context.shutdown() -def test_revert_patches_ignores_revert_failures() -> None: - class _PatchOk: - def apply(self) -> bool: - return True - - def revert(self) -> None: +def test_unregister_adapters_ignores_unregister_failures() -> None: + class _AdapterOk(_FakeAdapter): + def unregister_hooks(self) -> None: return None - class _PatchFails: - def apply(self) -> bool: - return True - - def revert(self) -> None: + class _AdapterFails(_FakeAdapter): + def unregister_hooks(self) -> None: raise RuntimeError("boom") - core_assembly._revert_patches([_PatchOk(), _PatchFails(), _PatchOk()]) # no raise + core_assembly._unregister_adapters( + [_AdapterOk("ok1"), _AdapterFails("fail"), _AdapterOk("ok2")] + ) # no raise def test_init_assembly_is_thread_safe_and_idempotent( @@ -451,16 +268,16 @@ def test_init_assembly_is_thread_safe_and_idempotent( ) -> None: started = Event() release = Event() - apply_call_count = 0 + register_call_count = 0 - def fake_apply_runtime_patches(**kwargs: Any) -> list[Any]: - nonlocal apply_call_count - apply_call_count += 1 + def fake_register_adapters(**kwargs: Any) -> list[Any]: + nonlocal register_call_count + register_call_count += 1 started.set() release.wait(timeout=2) return [] - monkeypatch.setattr(core_assembly, "_apply_runtime_patches", fake_apply_runtime_patches) + monkeypatch.setattr(core_assembly, "_register_adapters", fake_register_adapters) monkeypatch.setattr( core_assembly, "_start_network_layer", @@ -481,6 +298,6 @@ def initialize() -> core_assembly.AssemblyContext: try: assert context_a is context_b - assert apply_call_count == 1 + assert register_call_count == 1 finally: context_a.shutdown() From 77ecafa3e4edec940c39b8e8a1fca057512cebdd Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:31:37 +0800 Subject: [PATCH 17/23] =?UTF-8?q?=F0=9F=90=9B=20adapter(openai):=20Guard?= =?UTF-8?q?=20is=5Favailable=20against=20missing=20parent=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit find_spec("openai.agents") raises ModuleNotFoundError when the openai package is not installed at all. Catch the exception so is_available() returns False gracefully. Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/openai_agents/adapter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/agent_assembly/adapters/openai_agents/adapter.py b/agent_assembly/adapters/openai_agents/adapter.py index 642d836..05a59df 100644 --- a/agent_assembly/adapters/openai_agents/adapter.py +++ b/agent_assembly/adapters/openai_agents/adapter.py @@ -31,7 +31,10 @@ def get_supported_versions(self) -> list[str]: def is_available(self) -> bool: """Check specifically for openai.agents module, not just openai base.""" - return importlib.util.find_spec("openai.agents") is not None + try: + return importlib.util.find_spec("openai.agents") is not None + except (ModuleNotFoundError, ValueError): + return False def register_hooks(self, interceptor: GovernanceInterceptor) -> None: self._patch = OpenAIAgentsPatch( From c4ebc6eab8b18c6dde480c8e0eab559b0b8acabd Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:32:00 +0800 Subject: [PATCH 18/23] =?UTF-8?q?=E2=9C=85=20test(registry):=20Update=20te?= =?UTF-8?q?st=20for=20real=20adapter=20registry=20keys?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename test to reflect that builtin registry keys now match get_framework_name() directly, since real adapter classes replaced placeholder stubs. Co-Authored-By: Claude Opus 4.6 --- test/unit/adapters/test_registry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/unit/adapters/test_registry.py b/test/unit/adapters/test_registry.py index 4721b4b..4742982 100644 --- a/test/unit/adapters/test_registry.py +++ b/test/unit/adapters/test_registry.py @@ -265,9 +265,8 @@ def fake_import_module(module_name: str) -> object: assert adapter.hook_registered is True -def test_builtin_registry_name_uses_pydantic_ai_label_with_python_import_name() -> None: +def test_builtin_registry_keys_match_adapter_framework_name() -> None: registry = AdapterRegistry() - assert "pydantic-ai" in registry._registered - assert "pydantic_ai" not in registry._registered - assert registry._registered["pydantic-ai"].get_framework_name() == "pydantic_ai" + assert "pydantic_ai" in registry._registered + assert registry._registered["pydantic_ai"].get_framework_name() == "pydantic_ai" From d34c4fdcff6e6a5f6dd85b4981f0a5e28d409379 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:32:45 +0800 Subject: [PATCH 19/23] =?UTF-8?q?=E2=9C=85=20test(langchain):=20Update=20r?= =?UTF-8?q?untime=20tests=20for=20=5Fregister=5Fadapters=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace monkeypatched _is_installed with _register_adapters to match the new adapter-based init_assembly flow. Co-Authored-By: Claude Opus 4.6 --- test/unit/adapters/langchain/test_runtime.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/unit/adapters/langchain/test_runtime.py b/test/unit/adapters/langchain/test_runtime.py index 21364e4..4c8305b 100644 --- a/test/unit/adapters/langchain/test_runtime.py +++ b/test/unit/adapters/langchain/test_runtime.py @@ -33,10 +33,16 @@ def test_auto_inject_callback_handler_is_idempotent() -> None: def test_init_assembly_auto_injects_callback_handler(monkeypatch) -> None: _reset_runtime_state_for_tests() _reset_assembly_state() + + def fake_register_adapters(**kwargs): # type: ignore[no-untyped-def] + auto_inject_callback_handler(kwargs["client"]) + return [] + + monkeypatch.setattr(core_assembly, "_register_adapters", fake_register_adapters) monkeypatch.setattr( core_assembly, - "_is_installed", - lambda package: package == "langchain", + "_start_network_layer", + lambda **kwargs: ("sdk-only", core_assembly._noop_shutdown), ) context = init_assembly( @@ -53,10 +59,16 @@ def test_init_assembly_auto_injects_callback_handler(monkeypatch) -> None: def test_init_assembly_reuses_existing_callback_handler(monkeypatch) -> None: _reset_runtime_state_for_tests() _reset_assembly_state() + + def fake_register_adapters(**kwargs): # type: ignore[no-untyped-def] + auto_inject_callback_handler(kwargs["client"]) + return [] + + monkeypatch.setattr(core_assembly, "_register_adapters", fake_register_adapters) monkeypatch.setattr( core_assembly, - "_is_installed", - lambda package: package == "langchain", + "_start_network_layer", + lambda **kwargs: ("sdk-only", core_assembly._noop_shutdown), ) first_context = init_assembly( From d6cfa31e9393fa3990617944bfe41feca8c7e52a Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:33:38 +0800 Subject: [PATCH 20/23] =?UTF-8?q?=F0=9F=93=9D=20adr:=20Add=20integration?= =?UTF-8?q?=20path=20status=20table=20to=20ADR-0001?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the end-to-end integration status for all six framework adapters, confirming each is fully wired through the canonical FrameworkAdapter → RuntimePatch → interceptor path. Co-Authored-By: Claude Opus 4.6 --- docs/adr/0001-hook-architecture.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/adr/0001-hook-architecture.md b/docs/adr/0001-hook-architecture.md index e3bc071..a407066 100644 --- a/docs/adr/0001-hook-architecture.md +++ b/docs/adr/0001-hook-architecture.md @@ -61,6 +61,22 @@ patches intercept framework calls and route them through the `GatewayClient` (or (`RuntimeClient`) is available, it provides the IPC transport; otherwise the `GatewayClient` uses HTTP. +## Integration Path Status + +Each adapter's end-to-end path: framework → adapter → patch → interceptor → gateway. + +| Adapter | Patch | Hook interception | Gateway integration | Integration test | +|---|---|---|---|---| +| LangChain | `LangChainPatch` | `AssemblyCallbackHandler` (sync + async) | via callback handler → `GatewayClient` | `test_langchain_mcp_coexistence_integration` | +| LangGraph | `LangGraphPatch` | `StateGraph.compile()` node wrapping | via LangChain callback handler | `test_langgraph_compile_patch_*` | +| CrewAI | `CrewAIPatch` | `Crew._execute_tasks()` wrapping | direct `interceptor.check_tool()` | `test_crewai_two_task_flow_*` | +| Pydantic AI | `PydanticAIPatch` | `Tool.run()` + model wrapper | direct `interceptor.check_tool()` | `test_pydantic_ai_two_tool_flow_*` | +| OpenAI Agents | `OpenAIAgentsPatch` | `FunctionTool.__call__()` wrapping | direct `interceptor.check_tool()` | `test_direct_openai_agents_functiontool_*` | +| MCP | `MCPClientPatch` | `ClientSession.call_tool()` wrapping | direct `interceptor.check_tool()` | `test_direct_mcp_clientsession_*` | + +All six adapters are fully wired through the `FrameworkAdapter` → `RuntimePatch` → +interceptor path. No adapter bypasses the registry or uses a parallel detection path. + ## Consequences - No `hooks.rs`, `detect.rs`, or Rust-side `install()/uninstall()` will be built. From 76f56321234c1d16d9006f4905589f3467db1022 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:34:09 +0800 Subject: [PATCH 21/23] =?UTF-8?q?=F0=9F=9A=A8=20lint:=20Apply=20ruff=20for?= =?UTF-8?q?matting=20and=20import=20sorting=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-formatted by pre-commit hooks: import reordering and style adjustments across adapters, tests, and examples. Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/base.py | 16 +++-------- agent_assembly/adapters/crewai/patch.py | 5 ++-- agent_assembly/adapters/langchain/__init__.py | 2 +- .../adapters/langchain/callback_handler.py | 2 -- .../adapters/langchain/langgraph_patch.py | 2 -- agent_assembly/adapters/langgraph/patch.py | 10 ++++--- agent_assembly/adapters/mcp/patch.py | 14 +++++----- .../adapters/openai_agents/patch.py | 8 +++--- agent_assembly/adapters/pydantic_ai/patch.py | 8 +++--- agent_assembly/adapters/registry.py | 7 +---- agent_assembly/core/assembly.py | 6 ++--- agent_assembly/exceptions/__init__.py | 8 ------ agent_assembly/types.py | 14 ---------- examples/type_checking/README.md | 8 +++--- .../type_checking/type_checking_example.py | 5 ---- .../test_direct_functiontool_integration.py | 4 +-- ...test_openai_mcp_coexistence_integration.py | 22 +++++++++------ test/integration/test_assembly_integration.py | 4 +-- ...test_langgraph_interception_integration.py | 11 +++++--- test/integration/test_native_core_runtime.py | 4 +-- test/unit/adapters/crewai/test_patch.py | 26 +++++++++--------- .../langchain/test_langgraph_patch.py | 8 ++---- test/unit/adapters/langchain/test_runtime.py | 2 +- .../unit/adapters/openai_agents/test_patch.py | 8 ++---- .../pydantic_ai/test_pydantic_ai_patch.py | 27 ++++++++++++------- test/unit/adapters/test_base.py | 3 ++- test/unit/adapters/test_registry.py | 6 ++++- test/unit/test_assembly.py | 4 +-- 28 files changed, 106 insertions(+), 138 deletions(-) diff --git a/agent_assembly/adapters/base.py b/agent_assembly/adapters/base.py index 8a8bffc..378d20b 100644 --- a/agent_assembly/adapters/base.py +++ b/agent_assembly/adapters/base.py @@ -1,7 +1,7 @@ from __future__ import annotations -from abc import ABC, abstractmethod import importlib +from abc import ABC, abstractmethod from typing import Protocol from agent_assembly.exceptions import AdapterValidationError @@ -10,8 +10,6 @@ class GovernanceInterceptor(Protocol): """Protocol implemented by governance interceptors used by adapters.""" - pass - class FrameworkAdapter(ABC): """Abstract contract implemented by every framework adapter. @@ -97,21 +95,15 @@ def validate_registration(self) -> None: """ framework_name = self.get_framework_name() if not framework_name.strip(): - raise AdapterValidationError( - "Adapter contract invalid: framework name must be non-empty." - ) + raise AdapterValidationError("Adapter contract invalid: framework name must be non-empty.") supported_versions = self.get_supported_versions() if not supported_versions: - raise AdapterValidationError( - "Adapter contract invalid: supported versions must not be empty." - ) + raise AdapterValidationError("Adapter contract invalid: supported versions must not be empty.") for version_range in supported_versions: if not version_range.strip(): - raise AdapterValidationError( - "Adapter contract invalid: version ranges must be non-empty strings." - ) + raise AdapterValidationError("Adapter contract invalid: version ranges must be non-empty strings.") def register(self, interceptor: GovernanceInterceptor) -> None: """Validate contract values and then attach framework hooks. diff --git a/agent_assembly/adapters/crewai/patch.py b/agent_assembly/adapters/crewai/patch.py index d99f24a..b326806 100644 --- a/agent_assembly/adapters/crewai/patch.py +++ b/agent_assembly/adapters/crewai/patch.py @@ -2,9 +2,9 @@ from __future__ import annotations +import importlib from dataclasses import dataclass from functools import wraps -import importlib from threading import local from typing import Any, Literal, Mapping @@ -111,8 +111,7 @@ def _extract_agent_id_from_inputs(args: tuple[Any, ...], kwargs: dict[str, Any]) def _format_blocked_message(reason: str | None) -> str: reason_text = reason or "No reason provided." return ( - f"[BLOCKED by governance policy] {reason_text}. " - "Please choose a different approach to accomplish this task." + f"[BLOCKED by governance policy] {reason_text}. " "Please choose a different approach to accomplish this task." ) diff --git a/agent_assembly/adapters/langchain/__init__.py b/agent_assembly/adapters/langchain/__init__.py index 17676f7..4179c93 100644 --- a/agent_assembly/adapters/langchain/__init__.py +++ b/agent_assembly/adapters/langchain/__init__.py @@ -2,13 +2,13 @@ from agent_assembly.adapters.langchain.adapter import LangChainAdapter from agent_assembly.adapters.langchain.callback_handler import AssemblyCallbackHandler -from agent_assembly.adapters.langgraph import LangGraphPatch from agent_assembly.adapters.langchain.langgraph_patch import patch_stategraph_compile from agent_assembly.adapters.langchain.patch import LangChainPatch from agent_assembly.adapters.langchain.runtime import ( auto_inject_callback_handler, get_active_callback_handler, ) +from agent_assembly.adapters.langgraph import LangGraphPatch __all__ = [ "LangChainAdapter", diff --git a/agent_assembly/adapters/langchain/callback_handler.py b/agent_assembly/adapters/langchain/callback_handler.py index b015224..22e7859 100644 --- a/agent_assembly/adapters/langchain/callback_handler.py +++ b/agent_assembly/adapters/langchain/callback_handler.py @@ -13,8 +13,6 @@ class _FallbackBaseCallbackHandler: """Fallback base type when langchain-core is not installed.""" - pass - _CallbackHandlerBase: type[object] = _FallbackBaseCallbackHandler try: # pragma: no cover - import availability depends on installed extras. diff --git a/agent_assembly/adapters/langchain/langgraph_patch.py b/agent_assembly/adapters/langchain/langgraph_patch.py index 161995c..d1ecc86 100644 --- a/agent_assembly/adapters/langchain/langgraph_patch.py +++ b/agent_assembly/adapters/langchain/langgraph_patch.py @@ -1,7 +1,5 @@ """Backward-compatible shim for LangGraph patch utilities.""" -import importlib - from agent_assembly.adapters.langgraph import patch as _impl LangGraphPatch = _impl.LangGraphPatch diff --git a/agent_assembly/adapters/langgraph/patch.py b/agent_assembly/adapters/langgraph/patch.py index cdca45d..8e4da6f 100644 --- a/agent_assembly/adapters/langgraph/patch.py +++ b/agent_assembly/adapters/langgraph/patch.py @@ -2,9 +2,9 @@ from __future__ import annotations -from dataclasses import dataclass import importlib import inspect +from dataclasses import dataclass from typing import Any _PATCHED_FLAG = "_agent_assembly_compile_patched" @@ -197,9 +197,7 @@ def _wrap_node_map(node_map: Any, callback_handler: Any) -> bool: wrapped_any = False for node_name, node_executor in list(items_method()): if callable(node_executor): - wrapped_executor = _make_assembly_node_wrapper( - str(node_name), node_executor, callback_handler - ) + wrapped_executor = _make_assembly_node_wrapper(str(node_name), node_executor, callback_handler) if wrapped_executor is node_executor: continue try: @@ -268,6 +266,7 @@ def _wrap_graph_invoke_fallback(compiled_graph: Any, callback_handler: Any) -> N return None if inspect.iscoroutinefunction(invoke): + async def wrapped_async_invoke(*invoke_args: Any, **invoke_kwargs: Any) -> Any: state = _extract_state(invoke_args, invoke_kwargs) config = _extract_config(invoke_args, invoke_kwargs) @@ -281,8 +280,10 @@ async def wrapped_async_invoke(*invoke_args: Any, **invoke_kwargs: Any) -> Any: config=config, ) return result + wrapped_invoke: Any = wrapped_async_invoke else: + def wrapped_sync_invoke(*invoke_args: Any, **invoke_kwargs: Any) -> Any: state = _extract_state(invoke_args, invoke_kwargs) config = _extract_config(invoke_args, invoke_kwargs) @@ -296,6 +297,7 @@ def wrapped_sync_invoke(*invoke_args: Any, **invoke_kwargs: Any) -> Any: config=config, ) return result + wrapped_invoke = wrapped_sync_invoke setattr(wrapped_invoke, _INVOKE_WRAPPED_FLAG, True) diff --git a/agent_assembly/adapters/mcp/patch.py b/agent_assembly/adapters/mcp/patch.py index cfefe9a..a51ca1e 100644 --- a/agent_assembly/adapters/mcp/patch.py +++ b/agent_assembly/adapters/mcp/patch.py @@ -2,16 +2,18 @@ from __future__ import annotations -from dataclasses import dataclass import importlib import importlib.util import inspect +from dataclasses import dataclass from typing import Any, Literal, Mapping from agent_assembly.adapters.crewai.patch import ( _get_pending_tool_approval_timeout_seconds as _resolve_pending_timeout_seconds, ) -from agent_assembly.adapters.crewai.patch import _normalize_decision as _normalize_governance_decision +from agent_assembly.adapters.crewai.patch import ( + _normalize_decision as _normalize_governance_decision, +) _ORIGINAL_CALL_TOOL = "_agent_assembly_original_mcp_call_tool" _PATCHED_FLAG = "_agent_assembly_mcp_clientsession_patched" @@ -222,14 +224,10 @@ def _build_blocked_error( reason_text = reason or "No reason provided." if is_pending_rejection: - message = ( - f"MCP tool '{tool_name}' on server '{server_identifier}' " - f"rejected during approval: {reason_text}" - ) + message = f"MCP tool '{tool_name}' on server '{server_identifier}' " f"rejected during approval: {reason_text}" else: message = ( - f"MCP tool '{tool_name}' on server '{server_identifier}' " - f"blocked by governance policy: {reason_text}" + f"MCP tool '{tool_name}' on server '{server_identifier}' " f"blocked by governance policy: {reason_text}" ) return MCPToolBlockedError( diff --git a/agent_assembly/adapters/openai_agents/patch.py b/agent_assembly/adapters/openai_agents/patch.py index 0624eb8..59a28d8 100644 --- a/agent_assembly/adapters/openai_agents/patch.py +++ b/agent_assembly/adapters/openai_agents/patch.py @@ -2,17 +2,19 @@ from __future__ import annotations -from dataclasses import dataclass -from functools import wraps import importlib import importlib.util import inspect +from dataclasses import dataclass +from functools import wraps from typing import Any, Literal from agent_assembly.adapters.crewai.patch import ( _get_pending_tool_approval_timeout_seconds as _resolve_pending_timeout_seconds, ) -from agent_assembly.adapters.crewai.patch import _normalize_decision as _normalize_governance_decision +from agent_assembly.adapters.crewai.patch import ( + _normalize_decision as _normalize_governance_decision, +) _ORIGINAL_FUNCTION_TOOL_CALL = "_agent_assembly_original_openai_agents_function_tool_call" _PATCHED_FLAG = "_agent_assembly_openai_agents_function_tool_patched" diff --git a/agent_assembly/adapters/pydantic_ai/patch.py b/agent_assembly/adapters/pydantic_ai/patch.py index 96140ae..d233885 100644 --- a/agent_assembly/adapters/pydantic_ai/patch.py +++ b/agent_assembly/adapters/pydantic_ai/patch.py @@ -2,16 +2,18 @@ from __future__ import annotations -from dataclasses import dataclass -from functools import wraps import importlib import inspect +from dataclasses import dataclass +from functools import wraps from typing import Any, Literal, Mapping from agent_assembly.adapters.crewai.patch import ( _get_pending_tool_approval_timeout_seconds as _resolve_pending_timeout_seconds, ) -from agent_assembly.adapters.crewai.patch import _normalize_decision as _normalize_governance_decision +from agent_assembly.adapters.crewai.patch import ( + _normalize_decision as _normalize_governance_decision, +) _ORIGINAL_TOOL_RUN = "_agent_assembly_original_pydantic_ai_tool_run" _TOOLS_PATCHED_FLAG = "_agent_assembly_pydantic_ai_tools_patched" diff --git a/agent_assembly/adapters/registry.py b/agent_assembly/adapters/registry.py index 6e32717..122d6f6 100644 --- a/agent_assembly/adapters/registry.py +++ b/agent_assembly/adapters/registry.py @@ -13,7 +13,6 @@ from agent_assembly.adapters.openai_agents.adapter import OpenAIAgentsAdapter from agent_assembly.adapters.pydantic_ai.adapter import PydanticAIAdapter - # LangChain must be first: its callback handler threads through to all # subsequent adapters. MCP must be last: it acts as a fallback for # remaining tool dispatch paths. @@ -137,11 +136,7 @@ def get_available_adapters_by_priority(self) -> list[FrameworkAdapter]: if adapter.is_available(): available.append(adapter) - available.sort( - key=lambda a: _ADAPTER_PRIORITY.get( - a.get_framework_name(), _DEFAULT_PRIORITY - ) - ) + available.sort(key=lambda a: _ADAPTER_PRIORITY.get(a.get_framework_name(), _DEFAULT_PRIORITY)) return available def _discover_entry_point_adapters(self) -> list[str]: diff --git a/agent_assembly/core/assembly.py b/agent_assembly/core/assembly.py index 5327380..d24c1e5 100644 --- a/agent_assembly/core/assembly.py +++ b/agent_assembly/core/assembly.py @@ -2,8 +2,8 @@ from __future__ import annotations -from dataclasses import dataclass, field import sys +from dataclasses import dataclass, field from threading import Lock from typing import Any, Callable, Literal, Protocol @@ -165,9 +165,7 @@ def _validate_inputs(*, gateway_url: str, api_key: str, mode: RuntimeMode) -> No if not api_key: raise ConfigurationError("api_key is required") if mode not in _VALID_RUNTIME_MODES: - raise ConfigurationError( - "mode must be one of: auto, ebpf, proxy, sdk-only" - ) + raise ConfigurationError("mode must be one of: auto, ebpf, proxy, sdk-only") def _register_adapters( diff --git a/agent_assembly/exceptions/__init__.py b/agent_assembly/exceptions/__init__.py index 49b2b5b..45b764c 100644 --- a/agent_assembly/exceptions/__init__.py +++ b/agent_assembly/exceptions/__init__.py @@ -17,37 +17,30 @@ class AssemblyError(Exception): """Base exception for Agent Assembly SDK errors.""" - pass class AgentError(AssemblyError): """Exception raised for agent-related errors.""" - pass class PolicyError(AssemblyError): """Exception raised for policy-related errors.""" - pass class GatewayError(AssemblyError): """Exception raised for gateway communication errors.""" - pass class ConfigurationError(AssemblyError): """Exception raised for configuration errors.""" - pass class AdapterValidationError(AssemblyError): """Exception raised when an adapter contract is invalid.""" - pass class ToolExecutionBlockedError(AssemblyError): """Exception raised when a tool run is blocked by governance.""" - pass class MCPToolBlockedError(ToolExecutionBlockedError): @@ -67,4 +60,3 @@ def __init__( class PolicyViolationError(ToolExecutionBlockedError): """Exception raised when policy blocks tool execution.""" - pass diff --git a/agent_assembly/types.py b/agent_assembly/types.py index 2a9709d..478fe63 100644 --- a/agent_assembly/types.py +++ b/agent_assembly/types.py @@ -16,20 +16,6 @@ from __future__ import annotations -from typing import ( - TYPE_CHECKING, - Any, - AsyncIterator, - Awaitable, - Callable, - Dict, - List, - Literal, - Optional, - Protocol, - Union, - runtime_checkable, -) __all__ = [ "AgentId", diff --git a/examples/type_checking/README.md b/examples/type_checking/README.md index 74b8a2c..3264155 100644 --- a/examples/type_checking/README.md +++ b/examples/type_checking/README.md @@ -152,11 +152,11 @@ def example_type_guards() -> None: def main() -> None: """Run all type checking examples.""" print("=== Type Checking Examples ===\n") - + example_basic_types() example_protocols() example_type_guards() - + print("\n✓ All type checking examples completed successfully!") if __name__ == "__main__": @@ -173,7 +173,7 @@ Your example should produce clear, structured output: Example 1: Basic Types ✓ Type annotations working correctly -Example 2: Protocol Types +Example 2: Protocol Types ✓ Protocol compliance verified Example 3: Type Guards @@ -231,7 +231,7 @@ When implementing your type checking examples: - Demonstrate Protocol implementations - Include examples of generic type parameters -### For Application Projects +### For Application Projects - Show internal type usage patterns - Demonstrate configuration type safety - Include data validation examples diff --git a/examples/type_checking/type_checking_example.py b/examples/type_checking/type_checking_example.py index b964203..c4a14a5 100644 --- a/examples/type_checking/type_checking_example.py +++ b/examples/type_checking/type_checking_example.py @@ -10,15 +10,11 @@ from __future__ import annotations -from typing import Any - # Example 1: Using type annotations with A -pass # Example 2: Using XXX types -pass # Main demonstration @@ -30,7 +26,6 @@ def main() -> None: # Example 2: Custom handler with protocol compliance - print("\n✓ All type checking examples completed successfully!") diff --git a/test/integration/openai_agents/test_direct_functiontool_integration.py b/test/integration/openai_agents/test_direct_functiontool_integration.py index 901dff0..3339639 100644 --- a/test/integration/openai_agents/test_direct_functiontool_integration.py +++ b/test/integration/openai_agents/test_direct_functiontool_integration.py @@ -32,9 +32,7 @@ async def __call__(self, ctx: Any, tool_input: Any) -> dict[str, Any]: monkeypatch.setattr( openai_patch.importlib, "import_module", - lambda name: fake_openai_agents_module - if name == "openai.agents" - else (_ for _ in ()).throw(ImportError(name)), + lambda name: fake_openai_agents_module if name == "openai.agents" else (_ for _ in ()).throw(ImportError(name)), ) class Interceptor: diff --git a/test/integration/openai_agents/test_openai_mcp_coexistence_integration.py b/test/integration/openai_agents/test_openai_mcp_coexistence_integration.py index f53d8c1..f3eea92 100644 --- a/test/integration/openai_agents/test_openai_mcp_coexistence_integration.py +++ b/test/integration/openai_agents/test_openai_mcp_coexistence_integration.py @@ -63,14 +63,20 @@ async def record_result(self, **kwargs: object) -> None: records.append(dict(kwargs)) interceptor = Interceptor() - assert openai_patch.OpenAIAgentsPatch( - callback_handler=interceptor, - process_agent_id="agent-openai", - ).apply() is True - assert mcp_patch.MCPClientPatch( - callback_handler=interceptor, - process_agent_id="agent-openai", - ).apply() is True + assert ( + openai_patch.OpenAIAgentsPatch( + callback_handler=interceptor, + process_agent_id="agent-openai", + ).apply() + is True + ) + assert ( + mcp_patch.MCPClientPatch( + callback_handler=interceptor, + process_agent_id="agent-openai", + ).apply() + is True + ) result = await FakeFunctionTool("openai_tool")(SimpleNamespace(agent_id="agent-openai"), {"step": "run"}) diff --git a/test/integration/test_assembly_integration.py b/test/integration/test_assembly_integration.py index c65fa73..79cdf04 100644 --- a/test/integration/test_assembly_integration.py +++ b/test/integration/test_assembly_integration.py @@ -50,9 +50,9 @@ def test_gateway_client_context_manager(): api_key="test-api-key", agent_id="test-agent-001", ) - + with context: assert context.client.client is not None - + # Client should be closed after exiting context assert context.client._client is None diff --git a/test/integration/test_langgraph_interception_integration.py b/test/integration/test_langgraph_interception_integration.py index c90f185..33d5a5a 100644 --- a/test/integration/test_langgraph_interception_integration.py +++ b/test/integration/test_langgraph_interception_integration.py @@ -5,7 +5,10 @@ import pytest -from agent_assembly.adapters.langchain import AssemblyCallbackHandler, patch_stategraph_compile +from agent_assembly.adapters.langchain import ( + AssemblyCallbackHandler, + patch_stategraph_compile, +) from agent_assembly.exceptions import ToolExecutionBlockedError @@ -137,9 +140,9 @@ def compile(self) -> FakeCompiledGraph: fake_module = SimpleNamespace(StateGraph=FakeStateGraph) monkeypatch.setattr( "agent_assembly.adapters.langchain.langgraph_patch.importlib.import_module", - lambda module_name: fake_module - if module_name == "langgraph.graph.state" - else (_ for _ in ()).throw(ImportError(module_name)), + lambda module_name: ( + fake_module if module_name == "langgraph.graph.state" else (_ for _ in ()).throw(ImportError(module_name)) + ), ) assert patch_stategraph_compile(handler) is True diff --git a/test/integration/test_native_core_runtime.py b/test/integration/test_native_core_runtime.py index 114e683..68f91bd 100644 --- a/test/integration/test_native_core_runtime.py +++ b/test/integration/test_native_core_runtime.py @@ -213,9 +213,7 @@ def test_runtime_client_has_no_thread_deadlock(native_core) -> None: def worker(worker_id: int) -> None: try: for index in range(100): - client.send_event( - native_core.GovernanceEvent(make_audit_entry_payload(index, worker_id=worker_id)) - ) + client.send_event(native_core.GovernanceEvent(make_audit_entry_payload(index, worker_id=worker_id))) client.query_policy({"action": "tool.call", "timeout_ms": 50}) except Exception as error: # pragma: no cover - runtime guard errors.append(error) diff --git a/test/unit/adapters/crewai/test_patch.py b/test/unit/adapters/crewai/test_patch.py index 399f85e..812aace 100644 --- a/test/unit/adapters/crewai/test_patch.py +++ b/test/unit/adapters/crewai/test_patch.py @@ -110,9 +110,7 @@ def test_helper_branch_coverage_for_decision_and_agent_extraction() -> None: assert crewai_patch._normalize_decision("allow") == ("allow", None) assert crewai_patch._normalize_decision(12345) == ("allow", None) - assert ( - crewai_patch._extract_agent_id_from_inputs((), {"agent_id": "agent-direct"}) == "agent-direct" - ) + assert crewai_patch._extract_agent_id_from_inputs((), {"agent_id": "agent-direct"}) == "agent-direct" assert ( crewai_patch._extract_agent_id_from_inputs( (), @@ -155,12 +153,18 @@ def get_pending_tool_approval_timeout_seconds(self) -> str: return "42" assert crewai_patch._get_pending_tool_approval_timeout_seconds(TimeoutProvider()) == 42 - assert crewai_patch._get_pending_tool_approval_timeout_seconds( - SimpleNamespace(pending_tool_approval_timeout_seconds=0) - ) == 300 - assert crewai_patch._get_pending_tool_approval_timeout_seconds( - SimpleNamespace(pending_tool_approval_timeout_seconds=True) - ) == 300 + assert ( + crewai_patch._get_pending_tool_approval_timeout_seconds( + SimpleNamespace(pending_tool_approval_timeout_seconds=0) + ) + == 300 + ) + assert ( + crewai_patch._get_pending_tool_approval_timeout_seconds( + SimpleNamespace(pending_tool_approval_timeout_seconds=True) + ) + == 300 + ) def test_record_result_and_task_fallback_handlers_are_used( @@ -399,9 +403,7 @@ def fake_import_module(module_name: str) -> object: class ConcurrencyInterceptor: def check_tool_start(self, **kwargs: object) -> dict[str, str]: - observed_agent_ids.append( - str(kwargs.get("agent_id")) if kwargs.get("agent_id") is not None else None - ) + observed_agent_ids.append(str(kwargs.get("agent_id")) if kwargs.get("agent_id") is not None else None) return {"status": "allow"} patcher = crewai_patch.CrewAIPatch(ConcurrencyInterceptor()) diff --git a/test/unit/adapters/langchain/test_langgraph_patch.py b/test/unit/adapters/langchain/test_langgraph_patch.py index f2c9fce..2e91794 100644 --- a/test/unit/adapters/langchain/test_langgraph_patch.py +++ b/test/unit/adapters/langchain/test_langgraph_patch.py @@ -34,12 +34,8 @@ def on_graph_node_end(self, *, node_name: str, state: object, result: object) -> def test_extract_state_prefers_args_and_falls_back_to_kwargs() -> None: - assert langgraph_patch._extract_state(({"from": "args"},), {"state": {"from": "kwargs"}}) == { - "from": "args" - } - assert langgraph_patch._extract_state((), {"state": {"from": "kwargs"}}) == { - "from": "kwargs" - } + assert langgraph_patch._extract_state(({"from": "args"},), {"state": {"from": "kwargs"}}) == {"from": "args"} + assert langgraph_patch._extract_state((), {"state": {"from": "kwargs"}}) == {"from": "kwargs"} def test_invoke_hooks_handle_missing_methods_and_awaitables() -> None: diff --git a/test/unit/adapters/langchain/test_runtime.py b/test/unit/adapters/langchain/test_runtime.py index 4c8305b..d68e5cd 100644 --- a/test/unit/adapters/langchain/test_runtime.py +++ b/test/unit/adapters/langchain/test_runtime.py @@ -3,12 +3,12 @@ import pytest from agent_assembly import init_assembly -from agent_assembly.core import assembly as core_assembly from agent_assembly.adapters.langchain.runtime import ( _reset_runtime_state_for_tests, auto_inject_callback_handler, get_active_callback_handler, ) +from agent_assembly.core import assembly as core_assembly from agent_assembly.exceptions import ConfigurationError diff --git a/test/unit/adapters/openai_agents/test_patch.py b/test/unit/adapters/openai_agents/test_patch.py index 2808977..a9ee57e 100644 --- a/test/unit/adapters/openai_agents/test_patch.py +++ b/test/unit/adapters/openai_agents/test_patch.py @@ -84,9 +84,7 @@ def test_loader_edge_cases_and_apply_false_when_functiontool_missing( monkeypatch.setattr( openai_patch.importlib, "import_module", - lambda name: fake_module - if name == "openai.agents" - else (_ for _ in ()).throw(ImportError(name)), + lambda name: fake_module if name == "openai.agents" else (_ for _ in ()).throw(ImportError(name)), ) monkeypatch.setattr(openai_patch.importlib.util, "find_spec", lambda package: object()) assert openai_patch._is_openai_agents_available() is True @@ -360,9 +358,7 @@ def __init__(self, **kwargs: object) -> None: monkeypatch.setattr( openai_patch.importlib, "import_module", - lambda name: fake_module - if name == "openai.agents" - else (_ for _ in ()).throw(ImportError(name)), + lambda name: fake_module if name == "openai.agents" else (_ for _ in ()).throw(ImportError(name)), ) # _build_tool_result_error fallback dict branch diff --git a/test/unit/adapters/pydantic_ai/test_pydantic_ai_patch.py b/test/unit/adapters/pydantic_ai/test_pydantic_ai_patch.py index d18d96e..ff88a76 100644 --- a/test/unit/adapters/pydantic_ai/test_pydantic_ai_patch.py +++ b/test/unit/adapters/pydantic_ai/test_pydantic_ai_patch.py @@ -103,12 +103,18 @@ def get_pending_tool_approval_timeout_seconds(self) -> str: return "42" assert pydantic_ai_patch._get_pending_tool_approval_timeout_seconds(TimeoutProvider()) == 42 - assert pydantic_ai_patch._get_pending_tool_approval_timeout_seconds( - SimpleNamespace(pending_tool_approval_timeout_seconds=0) - ) == 300 - assert pydantic_ai_patch._get_pending_tool_approval_timeout_seconds( - SimpleNamespace(pending_tool_approval_timeout_seconds=True) - ) == 300 + assert ( + pydantic_ai_patch._get_pending_tool_approval_timeout_seconds( + SimpleNamespace(pending_tool_approval_timeout_seconds=0) + ) + == 300 + ) + assert ( + pydantic_ai_patch._get_pending_tool_approval_timeout_seconds( + SimpleNamespace(pending_tool_approval_timeout_seconds=True) + ) + == 300 + ) assert pydantic_ai_patch._normalize_decision("deny") == ("deny", None) assert pydantic_ai_patch._normalize_decision("pending") == ("pending", None) @@ -318,9 +324,12 @@ class NoHandlers: ) assert fallback_wait == {"status": "deny", "reason": "Approval handler is unavailable."} - assert pydantic_ai_patch._get_pending_tool_approval_timeout_seconds( - SimpleNamespace(pending_tool_approval_timeout_seconds="NaN") - ) == 300 + assert ( + pydantic_ai_patch._get_pending_tool_approval_timeout_seconds( + SimpleNamespace(pending_tool_approval_timeout_seconds="NaN") + ) + == 300 + ) class SyncInterceptor: def check_tool_start(self, **kwargs: object) -> dict[str, str]: diff --git a/test/unit/adapters/test_base.py b/test/unit/adapters/test_base.py index d32ce31..1da5d37 100644 --- a/test/unit/adapters/test_base.py +++ b/test/unit/adapters/test_base.py @@ -1,6 +1,7 @@ -import pytest from types import SimpleNamespace +import pytest + from agent_assembly.adapters import FrameworkAdapter, GovernanceInterceptor from agent_assembly.exceptions import AdapterValidationError diff --git a/test/unit/adapters/test_registry.py b/test/unit/adapters/test_registry.py index 4742982..a54ea23 100644 --- a/test/unit/adapters/test_registry.py +++ b/test/unit/adapters/test_registry.py @@ -5,7 +5,11 @@ import pytest -from agent_assembly.adapters import AdapterRegistry, FrameworkAdapter, GovernanceInterceptor +from agent_assembly.adapters import ( + AdapterRegistry, + FrameworkAdapter, + GovernanceInterceptor, +) class DummyAdapter(FrameworkAdapter): diff --git a/test/unit/test_assembly.py b/test/unit/test_assembly.py index 6873a99..0005f9b 100644 --- a/test/unit/test_assembly.py +++ b/test/unit/test_assembly.py @@ -258,9 +258,7 @@ class _AdapterFails(_FakeAdapter): def unregister_hooks(self) -> None: raise RuntimeError("boom") - core_assembly._unregister_adapters( - [_AdapterOk("ok1"), _AdapterFails("fail"), _AdapterOk("ok2")] - ) # no raise + core_assembly._unregister_adapters([_AdapterOk("ok1"), _AdapterFails("fail"), _AdapterOk("ok2")]) # no raise def test_init_assembly_is_thread_safe_and_idempotent( From a144ca5e6b8ad9e2a25aea41c578ec548b891ebe Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:35:17 +0800 Subject: [PATCH 22/23] =?UTF-8?q?=F0=9F=90=9B=20shim(langgraph):=20Restore?= =?UTF-8?q?=20importlib=20import=20removed=20by=20linter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The import is used as a monkeypatch target by tests — add noqa to prevent ruff from removing it again. Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/langchain/langgraph_patch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/agent_assembly/adapters/langchain/langgraph_patch.py b/agent_assembly/adapters/langchain/langgraph_patch.py index d1ecc86..2cfaad6 100644 --- a/agent_assembly/adapters/langchain/langgraph_patch.py +++ b/agent_assembly/adapters/langchain/langgraph_patch.py @@ -1,5 +1,7 @@ """Backward-compatible shim for LangGraph patch utilities.""" +import importlib # noqa: F401 — re-exported for monkeypatch targets in tests + from agent_assembly.adapters.langgraph import patch as _impl LangGraphPatch = _impl.LangGraphPatch From fbe687b32ff2f51aa4921872f6a6f0bdd8835e9f Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Fri, 1 May 2026 17:37:14 +0800 Subject: [PATCH 23/23] =?UTF-8?q?=F0=9F=9A=A8=20lint:=20Fix=20mypy=20unuse?= =?UTF-8?q?d-ignore=20and=20missing=20type=20annotations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove unused type: ignore comment in base.py, add type annotations to test_runtime.py monkeypatch parameters, apply remaining linter formatting. Co-Authored-By: Claude Opus 4.6 --- agent_assembly/adapters/base.py | 2 +- agent_assembly/types.py | 1 - examples/type_checking/type_checking_example.py | 1 - test/unit/adapters/langchain/test_runtime.py | 8 ++++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/agent_assembly/adapters/base.py b/agent_assembly/adapters/base.py index 378d20b..d5db9a9 100644 --- a/agent_assembly/adapters/base.py +++ b/agent_assembly/adapters/base.py @@ -136,7 +136,7 @@ def set_process_agent_id(self, agent_id: str | None) -> None: a no-op for adapters that do not use an agent ID. """ if hasattr(self, "process_agent_id"): - self.process_agent_id = agent_id # type: ignore[attr-defined] + self.process_agent_id = agent_id def get_active_version(self) -> str | None: """Return framework `__version__` when present, otherwise `None`. diff --git a/agent_assembly/types.py b/agent_assembly/types.py index 478fe63..e0a4198 100644 --- a/agent_assembly/types.py +++ b/agent_assembly/types.py @@ -16,7 +16,6 @@ from __future__ import annotations - __all__ = [ "AgentId", "PolicyId", diff --git a/examples/type_checking/type_checking_example.py b/examples/type_checking/type_checking_example.py index c4a14a5..3660727 100644 --- a/examples/type_checking/type_checking_example.py +++ b/examples/type_checking/type_checking_example.py @@ -10,7 +10,6 @@ from __future__ import annotations - # Example 1: Using type annotations with A diff --git a/test/unit/adapters/langchain/test_runtime.py b/test/unit/adapters/langchain/test_runtime.py index d68e5cd..eef381a 100644 --- a/test/unit/adapters/langchain/test_runtime.py +++ b/test/unit/adapters/langchain/test_runtime.py @@ -30,11 +30,11 @@ def test_auto_inject_callback_handler_is_idempotent() -> None: assert get_active_callback_handler() is first -def test_init_assembly_auto_injects_callback_handler(monkeypatch) -> None: +def test_init_assembly_auto_injects_callback_handler(monkeypatch: pytest.MonkeyPatch) -> None: _reset_runtime_state_for_tests() _reset_assembly_state() - def fake_register_adapters(**kwargs): # type: ignore[no-untyped-def] + def fake_register_adapters(**kwargs: object) -> list[object]: auto_inject_callback_handler(kwargs["client"]) return [] @@ -56,11 +56,11 @@ def fake_register_adapters(**kwargs): # type: ignore[no-untyped-def] context.shutdown() -def test_init_assembly_reuses_existing_callback_handler(monkeypatch) -> None: +def test_init_assembly_reuses_existing_callback_handler(monkeypatch: pytest.MonkeyPatch) -> None: _reset_runtime_state_for_tests() _reset_assembly_state() - def fake_register_adapters(**kwargs): # type: ignore[no-untyped-def] + def fake_register_adapters(**kwargs: object) -> list[object]: auto_inject_callback_handler(kwargs["client"]) return []