From 5276163596df61b4eaf582dcf4ed30451dd8c977 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Mon, 15 Jun 2026 17:57:25 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20(examples):=20Add=20reference?= =?UTF-8?q?=20template=20framework=20adapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A minimal runnable FrameworkAdapter implementing all four required methods (get_framework_name, get_supported_versions, register_hooks, unregister_hooks) over a self-contained fictional framework. Demonstrates the wrapper-based monkey-patch pattern, idempotent teardown, and a __main__ lifecycle demo. Passes `aasm adapter validate` (7/7). Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/adapters/template_adapter.py | 235 ++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 examples/adapters/template_adapter.py diff --git a/examples/adapters/template_adapter.py b/examples/adapters/template_adapter.py new file mode 100644 index 00000000..cba51c3d --- /dev/null +++ b/examples/adapters/template_adapter.py @@ -0,0 +1,235 @@ +"""Reference template adapter for the Agent Assembly Python SDK. + +This module is a **minimal, runnable** ``FrameworkAdapter`` implementation that you +can copy as the starting point for a real community adapter (e.g. ``aa-adapter-``). + +It governs a tiny *fictional* in-process framework defined at the bottom of this file +(``ExampleFramework``) so the whole thing runs offline with no third-party +dependencies and no reachable gateway. Replace ``ExampleFramework`` and the +monkey-patch logic in ``TemplateAdapter`` with your real framework's classes. + +What this template demonstrates: + +* The four required abstract methods of ``FrameworkAdapter`` + (``get_framework_name``, ``get_supported_versions``, ``register_hooks``, + ``unregister_hooks``). +* A **wrapper-based monkey-patch**: ``register_hooks`` wraps the framework's + tool-execution method so every call is routed through the governance + interceptor *before* the original runs, enforcing an allow/deny decision. +* **Idempotent teardown**: ``unregister_hooks`` restores the original method and + is safe to call more than once (the in-tree validator enforces this). + +The ``GovernanceInterceptor`` contract is **structural** (a duck-typed +``typing.Protocol`` with no required methods). Adapters call whatever methods +the active interceptor exposes, guarding each call with ``getattr(...)`` so the +adapter degrades gracefully when a method is absent. This template uses a single +convention method, ``check_tool_call``, returning a decision mapping +``{"status": "allow" | "deny", "reason": str | None}``. + +Run it directly to see the full lifecycle:: + + uv run python examples/adapters/template_adapter.py +""" + +from __future__ import annotations + +from collections.abc import Callable +from functools import wraps +from typing import Any + +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor + +# Sentinel attribute names stored on the patched class so we can detect a +# double-apply and find the original method during revert. Namespacing the +# attributes avoids collisions with the framework's own attributes. +_PATCHED_FLAG = "_aa_template_patched" +_ORIGINAL_RUN_TOOL = "_aa_template_original_run_tool" + + +def _format_blocked_message(reason: str | None) -> str: + """Build the string returned to the agent when a tool call is denied. + + Returning a message (instead of raising) lets the agent observe the denial + and choose a different action, which is the behaviour every built-in adapter + follows. + """ + reason_text = reason or "No reason provided." + return f"[BLOCKED by governance policy] {reason_text}" + + +def _normalize_decision(decision: object) -> tuple[str, str | None]: + """Normalize an interceptor's return value into ``(status, reason)``. + + Interceptors may return a bare status string or a mapping; anything we do + not recognise is treated as ``allow`` so a misbehaving interceptor fails + open *for this template*. A production adapter governing sensitive actions + may prefer to fail closed (deny on unknown) — pick the posture that matches + your threat model. + """ + if isinstance(decision, str): + return ("deny" if decision.strip().lower() == "deny" else "allow", None) + if isinstance(decision, dict): + status = str(decision.get("status", "allow")).strip().lower() + reason = decision.get("reason") + return ("deny" if status == "deny" else "allow", str(reason) if reason is not None else None) + return ("allow", None) + + +class TemplateAdapter(FrameworkAdapter): + """Reference adapter that governs the fictional ``ExampleFramework``. + + Copy this class, rename it ``Adapter``, and repoint + ``get_framework_name`` / the monkey-patch at your real framework. + """ + + def __init__(self) -> None: + # Hold onto the interceptor and a flag so teardown is idempotent. A real + # adapter that installs several patches typically holds a list of patch + # objects instead. + self._interceptor: GovernanceInterceptor | None = None + self._target_cls: type[Any] | None = None + + # --- Required abstract method 1 ------------------------------------------------- + def get_framework_name(self) -> str: + """Return the canonical importable package name of the framework. + + Must be a non-empty string. For a real adapter this is the framework's + import name (e.g. ``"langchain"``, ``"crewai"``) so the registry can + check availability via ``importlib.import_module``. + """ + return "example_framework" + + # --- Required abstract method 2 ------------------------------------------------- + def get_supported_versions(self) -> list[str]: + """Return the semantic-version ranges this adapter supports. + + Must be a non-empty list of non-empty strings (PEP 440 specifiers). + """ + return [">=1.0.0,<2.0.0"] + + # --- Required abstract method 3 ------------------------------------------------- + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + """Install the governance hooks (wrapper-based monkey-patch). + + Called by ``init_assembly()`` (via ``AdapterRegistry.auto_detect``) with + the live governance interceptor. Here we wrap ``ExampleFramework.run_tool`` + so every tool call is checked before it executes. + """ + target_cls = _load_example_framework_class() + if target_cls is None: # framework not importable — nothing to patch + return + if getattr(target_cls, _PATCHED_FLAG, False): # already patched — stay idempotent + return + + self._interceptor = interceptor + self._target_cls = target_cls + original_run_tool: Callable[..., Any] = target_cls.run_tool + + @wraps(original_run_tool) + def patched_run_tool(framework_self: Any, tool_name: str, **kwargs: Any) -> Any: + # Ask the interceptor for a decision *before* running the tool. + # getattr-guard the call so a minimal interceptor (one without + # check_tool_call) simply allows the action through. + check = getattr(interceptor, "check_tool_call", None) + if callable(check): + status, reason = _normalize_decision(check(tool_name=tool_name, args=dict(kwargs))) + if status == "deny": + return _format_blocked_message(reason) + # Allowed: run the real tool, then optionally report the result. + result = original_run_tool(framework_self, tool_name, **kwargs) + record = getattr(interceptor, "record_result", None) + if callable(record): + record(tool_name=tool_name, result=result) + return result + + # Stash the original and flip the patched flag so revert can undo this. + setattr(target_cls, _ORIGINAL_RUN_TOOL, original_run_tool) + target_cls.run_tool = patched_run_tool + setattr(target_cls, _PATCHED_FLAG, True) + + # --- Required abstract method 4 ------------------------------------------------- + def unregister_hooks(self) -> None: + """Revert every patch installed by ``register_hooks`` — idempotently. + + Must not raise when no hooks are active; the in-tree validator calls it + twice in a row to enforce this. + """ + target_cls = self._target_cls + if target_cls is None or not getattr(target_cls, _PATCHED_FLAG, False): + return + original_run_tool = getattr(target_cls, _ORIGINAL_RUN_TOOL, None) + if callable(original_run_tool): + target_cls.run_tool = original_run_tool + for attr in (_ORIGINAL_RUN_TOOL, _PATCHED_FLAG): + if hasattr(target_cls, attr): + delattr(target_cls, attr) + self._interceptor = None + self._target_cls = None + + +# --------------------------------------------------------------------------------- +# Fictional framework + demo interceptor (replace with your real framework). +# --------------------------------------------------------------------------------- +class ExampleFramework: + """A stand-in for a real AI framework with one governable entry point.""" + + def run_tool(self, tool_name: str, **kwargs: Any) -> str: + """Pretend to execute a tool and return its output.""" + return f"ran '{tool_name}' with {kwargs}" + + +def _load_example_framework_class() -> type[Any] | None: + """Return the framework class to patch, or ``None`` if unavailable. + + A real adapter imports the framework lazily here (e.g. + ``importlib.import_module("crewai.tools").BaseTool``) so importing the + adapter never hard-fails when the framework is not installed. + """ + return ExampleFramework + + +class _DenyShellInterceptor: + """Tiny demo interceptor: denies the ``shell`` tool, allows everything else. + + This is *not* part of the public API — it only exists to make ``__main__`` + self-contained. In production the real governance interceptor is supplied by + ``init_assembly()``. + """ + + def check_tool_call(self, *, tool_name: str, args: dict[str, Any]) -> dict[str, Any]: + # A real interceptor would inspect ``args`` (tool inputs) too; this demo + # only gates on the tool name. + del args + if tool_name == "shell": + return {"status": "deny", "reason": "shell access is not permitted"} + return {"status": "allow", "reason": None} + + def record_result(self, *, tool_name: str, result: object) -> None: + print(f" [interceptor] recorded result of '{tool_name}': {result!r}") + + +def main() -> None: + """Demonstrate the full adapter lifecycle end to end, offline.""" + adapter = TemplateAdapter() + interceptor = _DenyShellInterceptor() + framework = ExampleFramework() + + print(f"framework name : {adapter.get_framework_name()}") + print(f"supported vers : {adapter.get_supported_versions()}") + + # Validate the contract before wiring (same checks the CLI validator runs). + adapter.validate_registration() + + # Install hooks, then exercise both an allowed and a denied tool call. + adapter.register_hooks(interceptor) + print("allowed call :", framework.run_tool("search", query="agent governance")) + print("denied call :", framework.run_tool("shell", cmd="rm -rf /")) + + # Tear down — and prove it is idempotent (second call must not raise). + adapter.unregister_hooks() + adapter.unregister_hooks() + print("after teardown :", framework.run_tool("shell", cmd="rm -rf /")) + + +if __name__ == "__main__": + main() From d3f945dcc42c003831fbea43da358bb8d0f90734 Mon Sep 17 00:00:00 2001 From: Chisanan232 Date: Mon, 15 Jun 2026 18:00:43 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20(docs):=20Add=20framework=20?= =?UTF-8?q?adapter=20authoring=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New guides/authoring-adapters.md covering prerequisites, quickstart from the template adapter, the FrameworkAdapter four-method interface reference, the duck-typed GovernanceInterceptor contract and conventional event methods, the three hook patterns (callback/wrapper/monkey-patch), testing via the in-tree `aasm adapter validate` contract validator, entry-point publishing (aa-adapter-), and a PR checklist. Wired into mkdocs nav and the guides index. Grounded in real code: agent_assembly/adapters/base.py, agent_assembly/adapters/registry.py, agent_assembly/cli/adapter_validator.py. Notes that no AdapterTestHarness exists in the current tree. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/guides/authoring-adapters.md | 337 ++++++++++++++++++++++++++++++ docs/guides/index.md | 1 + mkdocs.yml | 1 + 3 files changed, 339 insertions(+) create mode 100644 docs/guides/authoring-adapters.md diff --git a/docs/guides/authoring-adapters.md b/docs/guides/authoring-adapters.md new file mode 100644 index 00000000..c9dc42e4 --- /dev/null +++ b/docs/guides/authoring-adapters.md @@ -0,0 +1,337 @@ +# Authoring a framework adapter + +A **framework adapter** teaches the Agent Assembly SDK how to govern a third-party AI +framework (LangChain, CrewAI, OpenAI Agents, …) *without* that framework being aware of +Agent Assembly. This guide walks you from zero to a published, installable adapter package. + +Every adapter implements one ABC — [`FrameworkAdapter`][src-base] — and is discovered either +by being registered in-tree or by advertising a Python **entry point**. The companion +reference implementation is [`examples/adapters/template_adapter.py`][template]: a minimal, +runnable adapter you can copy and adapt. + +[src-base]: https://github.com/ai-agent-assembly/python-sdk/blob/master/agent_assembly/adapters/base.py +[template]: https://github.com/ai-agent-assembly/python-sdk/blob/master/examples/adapters/template_adapter.py + +--- + +## 1. Prerequisites + +- **Python 3.10+** (the SDK targets 3.12 in CI; the public API is 3.10-compatible). +- A working understanding of your **target framework's hook/callback system** — the + method, callback, or class you can wrap to observe and gate tool/agent execution. + Adapters work by intercepting that point, so you need to know *where* in the framework + a tool call happens before you can govern it. +- The SDK installed in a virtualenv: + + ```bash + uv sync + ``` + +--- + +## 2. Quickstart: copy the reference template + +The fastest start is to copy the runnable template adapter and run it: + +```bash +cp examples/adapters/template_adapter.py my_adapter.py +uv run python my_adapter.py # runs the lifecycle demo offline +uv run aasm adapter validate my_adapter.py # checks the contract — expect 7/7 PASS +``` + +The template governs a self-contained fictional framework so it runs with no third-party +dependencies and no reachable gateway. Replace the fictional `ExampleFramework` and the +monkey-patch in `register_hooks` with your real framework's classes, then re-run the two +commands above. + +--- + +## 3. The `FrameworkAdapter` interface + +`FrameworkAdapter` is an `ABC` with **four required (abstract) methods**. The base class also +provides several concrete helpers (`register`, `validate_registration`, `is_available`, +`get_active_version`, `set_process_agent_id`) that you normally do **not** override. + +| Method | Signature | What you must do | +| --- | --- | --- | +| `get_framework_name` | `() -> str` | Return the **canonical importable package name** (e.g. `"crewai"`). Must be non-empty. The registry uses it to check availability via `importlib.import_module`. | +| `get_supported_versions` | `() -> list[str]` | Return a **non-empty list** of non-empty PEP 440 version-range strings (e.g. `[">=0.1.0"]`). | +| `register_hooks` | `(interceptor: GovernanceInterceptor) -> None` | Install your framework-specific monkey-patches, routing intercepted calls through `interceptor`. | +| `unregister_hooks` | `() -> None` | Tear down every patch installed by `register_hooks`. **Must be idempotent** — calling it twice must not raise. | + +### Implementation notes + +- **`get_framework_name`** — must match the framework's import name so `is_available()` + (which the registry calls before activating you) returns `True` only when the framework is + actually installed. Empty/whitespace names raise `AdapterValidationError` during + `register()`. +- **`get_supported_versions`** — an empty list, or any empty range string, raises + `AdapterValidationError`. These ranges document compatibility; the base class validates + their *shape*, not the running framework version. +- **`register_hooks`** — receives the live `interceptor`. Do the actual monkey-patching here + (see [§5 Hook patterns](#5-hook-patterns)). Built-in adapters delegate to one or more + internal *patch* objects exposing `apply()` / `revert()`; see ADR-0001 in + [Development → ADR-0001](../development/adr/0001-hook-architecture.md). Lazily import the + framework inside this method so importing your adapter never hard-fails when the framework + is absent. +- **`unregister_hooks`** — must revert patches (ideally in reverse install order) and be + safe to call when no hooks are active. Guard with a "patched" flag so a double-call is a + no-op. The validator enforces this by calling it twice. + +Do **not** call `register_hooks` directly — call `adapter.register(interceptor)`, which runs +`validate_registration()` first so contract errors surface *before* any hook is attached. + +A complete, working version of all four methods is in the +[template adapter][template]; here is the shape: + +```python +from agent_assembly.adapters.base import FrameworkAdapter, GovernanceInterceptor + + +class MyFrameworkAdapter(FrameworkAdapter): + def get_framework_name(self) -> str: + return "my_framework" + + def get_supported_versions(self) -> list[str]: + return [">=1.0.0,<2.0.0"] + + def register_hooks(self, interceptor: GovernanceInterceptor) -> None: + # Install monkey-patches that route framework calls through interceptor. + ... + + def unregister_hooks(self) -> None: + # Revert all patches installed by register_hooks(); must be idempotent. + ... +``` + +--- + +## 4. The `GovernanceInterceptor` contract and the events you emit + +[`GovernanceInterceptor`][src-base] is a **structural** `typing.Protocol` with **no required +methods**. It is a duck-typed marker: any object can be an interceptor. Adapters therefore +call interceptor methods **defensively** — look the method up with `getattr` and only call it +when it exists: + +```python +check = getattr(interceptor, "check_tool_call", None) +if callable(check): + decision = check(tool_name=tool_name, args=args) +``` + +This keeps an adapter working against both the full governance interceptor wired by +`init_assembly()` and a minimal stub used in tests. + +### Conventional interceptor methods + +Because the Protocol is open, there is **no fixed enum of event types**. The built-in +adapters use the following method-name conventions; mirror the ones that fit your framework's +execution model. Each returns either nothing (a pure notification) or a **decision** — a +status string `"allow" | "deny" | "pending"`, or a mapping `{"status": ..., "reason": ...}`. + +| Convention method | Direction | Purpose | +| --- | --- | --- | +| `check_tool_start` / `check_tool_call` | adapter → interceptor, returns decision | Pre-execution gate for a tool call; `deny` blocks it. | +| `wait_for_tool_approval` | adapter → interceptor, returns decision | Block until a `pending` tool call is approved or rejected (human-in-the-loop). | +| `get_pending_tool_approval_timeout_seconds` | adapter → interceptor | Configurable timeout for the approval wait. | +| `record_result` / `on_tool_end` | adapter → interceptor, no return | Report a completed tool call's output for audit. | +| `record` | adapter → interceptor, no return | Generic structured event (e.g. `action="task_start"`). | + +!!! note "These are conventions, not a typed contract" + The names above are what today's built-in adapters happen to call (see the CrewAI patch + in `agent_assembly/adapters/crewai/patch.py`). Always `getattr`-guard them. A future + release may formalise this into typed methods on the Protocol. + +A **pending → approval** flow looks like: + +```python +decision = check_tool_start(...) # may return {"status": "pending"} +if status == "pending": + decision = wait_for_tool_approval(...) # blocks until resolved or times out +# now decision is "allow" or "deny" +``` + +--- + +## 5. Hook patterns + +There are three ways to wire an adapter into a framework. Pick the one your framework +supports best. + +### Callback-based + +Register a callback/handler object with the framework's own callback system (LangChain's +`BaseCallbackHandler` is the canonical example; the `LangChainAdapter` builds one in +`register_hooks` and exposes it via `get_callback_handler()`). + +- **Pros:** first-class, framework-sanctioned, survives framework internal refactors, no + reliance on private attributes. +- **Cons:** only available if the framework *has* a callback system, and only sees the events + that system emits — anything outside it is invisible. + +### Wrapper-based + +Wrap a public method on a framework class with `functools.wraps`, run your governance logic, +then delegate to the original. This is what the [template adapter][template] does, and what +the CrewAI adapter does to `BaseTool.run` / `Task.execute_sync`. + +- **Pros:** works on any framework with a public entry point; you control exactly when the + check runs relative to the real call; clean revert by restoring the saved original. +- **Cons:** couples you to the wrapped method's signature; if the framework changes that + method you must follow. + +### Monkey-patch-based + +Replace a function or attribute outright (a degenerate case of wrapping where you may target +module-level functions or private internals). + +- **Pros:** can reach things with no public hook at all. +- **Cons:** most brittle; most likely to break on framework upgrades; easiest to leave the + process in a bad state if revert is incomplete. Use only as a last resort, and always store + the original + a "patched" flag so `unregister_hooks` is exact and idempotent. + +Whichever you choose, `unregister_hooks` must fully undo it. The template's +`_PATCHED_FLAG` / `_ORIGINAL_RUN_TOOL` sentinel pattern is the recommended approach. + +--- + +## 6. Testing your adapter + +!!! warning "There is no `AdapterTestHarness` fixture in the SDK today" + Earlier planning referenced an `AdapterTestHarness` pytest fixture. It does **not** exist + in the current codebase. Test your adapter with the two real mechanisms below; if a + shared harness is added later this section will be updated. + +### Contract validation (the in-tree validator) + +The SDK ships a contract validator, exposed both as a CLI command and as a Python function. +Run it against your adapter file or dotted module: + +```bash +uv run aasm adapter validate path/to/my_adapter.py +# or: uv run aasm adapter validate my_package.my_adapter +``` + +It runs seven checks — inheritance, all four abstract methods implemented, non-empty +framework name, non-empty version ranges, `register_hooks` signature, `unregister_hooks` +idempotency, and entry-point metadata (when a `pyproject.toml` is present). The reference +template passes all seven: + +```text +Results: 7 passed, 0 failed, 7 total +``` + +You can also call it programmatically in a unit test: + +```python +from agent_assembly.cli.adapter_validator import validate_adapter +from my_package.my_adapter import MyFrameworkAdapter + + +def test_adapter_passes_contract() -> None: + results = validate_adapter(MyFrameworkAdapter, "my_package/my_adapter.py") + assert all(r.passed for r in results), [r.message for r in results if not r.passed] +``` + +### Lifecycle unit tests with a stub interceptor + +Drive `register_hooks` / `unregister_hooks` with a tiny recording interceptor and a mocked +(or fictional) framework class, then assert that an allowed call passes through, a denied call +is blocked, and teardown restores the original. The template adapter's `__main__` demo is a +runnable model of exactly this flow. + +```python +class RecordingInterceptor: + def __init__(self) -> None: + self.calls: list[str] = [] + + def check_tool_call(self, *, tool_name: str, args: dict) -> dict: + self.calls.append(tool_name) + return {"status": "deny" if tool_name == "shell" else "allow", "reason": None} + + +def test_denies_shell() -> None: + from examples.adapters.template_adapter import ExampleFramework, TemplateAdapter + + adapter, interceptor, fw = TemplateAdapter(), RecordingInterceptor(), ExampleFramework() + adapter.register_hooks(interceptor) + try: + assert fw.run_tool("shell", cmd="rm -rf /").startswith("[BLOCKED") + assert "shell" in interceptor.calls + finally: + adapter.unregister_hooks() + adapter.unregister_hooks() # idempotent +``` + +Place real adapter tests under `test/unit/adapters//` (lifecycle, with the +framework mocked) and `test/integration/adapters//` (a minimal end-to-end flow +with the real framework imported), as described in +[CONTRIBUTING.md](https://github.com/ai-agent-assembly/python-sdk/blob/master/CONTRIBUTING.md#adding-a-new-framework-adapter). + +--- + +## 7. Publishing: entry points and naming + +A community adapter ships as its own pip-installable package and is discovered at runtime via +a Python **entry point** in the `agent_assembly.adapters` group. `AdapterRegistry` loads every +entry point in that group, verifies the loaded object is a `FrameworkAdapter` subclass, and +registers it — so `init_assembly()` activates it automatically when the framework is present. + +### Naming convention + +Name the distribution **`aa-adapter-`** (e.g. `aa-adapter-myframework`). The +import package can be whatever you like, but the framework name returned by +`get_framework_name()` must equal the framework's import name. + +### `pyproject.toml` entry-point config + +```toml +[project] +name = "aa-adapter-myframework" +version = "0.1.0" +dependencies = ["agent-assembly", "myframework>=1.0.0"] + +[project.entry-points."agent_assembly.adapters"] +myframework = "aa_adapter_myframework.adapter:MyFrameworkAdapter" +``` + +The entry-point **value** is `module.path:ClassName`. The `aasm adapter validate` +`entry_point_metadata` check confirms this points at your adapter class when it finds a +`pyproject.toml` alongside the adapter. + +### Verify discovery end to end + +```bash +pip install -e . # install your adapter package +uv run aasm adapter validate aa_adapter_myframework.adapter # 7/7 PASS, incl. entry point +python -c "from agent_assembly.adapters.registry import AdapterRegistry; \ +print([a.get_framework_name() for a in AdapterRegistry().get_available_adapters_by_priority()])" +``` + +`get_available_adapters_by_priority()` triggers entry-point discovery; your framework name +appears in the list once the framework is importable. + +--- + +## 8. PR checklist + +When contributing a built-in adapter back to this repo (community packages live in their own +repos but should still meet this bar), confirm: + +- [ ] Adapter subclasses `FrameworkAdapter` and implements all four abstract methods. +- [ ] `uv run aasm adapter validate ` reports **7 passed, 0 failed**. +- [ ] `unregister_hooks` is idempotent and fully reverts every patch. +- [ ] Framework is imported **lazily** (inside `register_hooks` / availability helpers), so + importing the adapter never fails when the framework is absent. +- [ ] Unit tests under `test/unit/adapters//` cover the patch install/revert + lifecycle (framework mocked). +- [ ] Integration test under `test/integration/adapters//` exercises a minimal + real flow. +- [ ] `uv run ruff check .`, `uv run ruff format --check .`, and `uv run mypy agent_assembly` + are clean. +- [ ] Entry point declared in `pyproject.toml` under + `[project.entry-points."agent_assembly.adapters"]` (for standalone packages). +- [ ] Distribution named `aa-adapter-`; `get_framework_name()` matches the + framework's import name. +- [ ] Follows the repo + [PR checklist](https://github.com/ai-agent-assembly/python-sdk/blob/master/CONTRIBUTING.md#pull-request-checklist). diff --git a/docs/guides/index.md b/docs/guides/index.md index dea6b431..d78498c5 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -6,6 +6,7 @@ do the [Quick Start](../quick-start.md) first. | Guide | What it covers | | --- | --- | | [Handling allow/deny decisions](handling-decisions.md) | Catch a policy denial, the exception hierarchy, MCP-specific blocks, and observe (dry-run) mode. | +| [Authoring a framework adapter](authoring-adapters.md) | Build, test, and publish a `FrameworkAdapter` for a new framework — interface reference, hook patterns, the contract validator, and entry-point publishing. | | [Type checking](type-checking.md) | Use the SDK's shipped types (PEP 561) with mypy / Pyright in your own project. | For runnable, end-to-end framework integrations — LangChain, LangGraph, CrewAI, OpenAI Agents, diff --git a/mkdocs.yml b/mkdocs.yml index a1469f88..322ee1ed 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -177,6 +177,7 @@ nav: - Guides: - guides/index.md - Handling allow/deny decisions: guides/handling-decisions.md + - Authoring a framework adapter: guides/authoring-adapters.md - Type checking: guides/type-checking.md - Examples: - examples/index.md