Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions docs/adapters/certification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Protocol adapter certification suite

`layerlens.instrument.adapters.protocols.certification.ProtocolCertificationSuite`
runs GA-readiness checks against protocol adapter classes. It validates
that an adapter class complies with the `BaseProtocolAdapter` contract
required for General Availability release.

This is a developer/CI tool — there's no runnable end-user sample. The
suite is invoked from your certification CI to gate adapter releases.

## Install

The certification suite is part of the base SDK install — no extra is
required:

```bash
pip install layerlens
```

## Quick start

```python
from layerlens.instrument.adapters.protocols.a2a import A2AAdapter
from layerlens.instrument.adapters.protocols.certification import (
ProtocolCertificationSuite,
)

suite = ProtocolCertificationSuite()
result = suite.certify(A2AAdapter)
assert result.passed, result.summary()

# Or certify all GA protocol adapters at once:
results = suite.certify_all()
assert all(r.passed for r in results)
```

## What gets checked

The suite runs the following categories of checks against every
candidate adapter class:

| Category | What's verified |
|---|---|
| Inheritance | Class extends both `BaseAdapter` and `BaseProtocolAdapter`. |
| Required class attributes | `FRAMEWORK`, `PROTOCOL`, `PROTOCOL_VERSION`, `VERSION` are non-empty strings. |
| Required methods | `connect`, `disconnect`, `health_check`, `get_adapter_info`, `serialize_for_replay`, `probe_health` all defined and not abstract. |
| Lifecycle correctness | `connect()` then `disconnect()` succeeds without exception, leaves the adapter in `DISCONNECTED` state. |
| Error handling | Adapter does not raise on construction with default args; `health_check()` returns an `AdapterHealth` even before `connect()`. |
| Return-type contracts | `get_adapter_info()` returns `AdapterInfo`, `probe_health()` returns `dict`, `serialize_for_replay()` returns `ReplayableTrace`. |

Each check produces a `CheckResult` with `passed: bool`, `message: str`,
and `severity: "error" | "warning"`. Warnings do not fail the
certification, errors do.

## Result types

```python
@dataclass
class CheckResult:
name: str
passed: bool
message: str
severity: str # "error" | "warning"

@dataclass
class CertificationResult:
passed: bool
adapter_name: str
protocol_version: str
checks: list[dict[str, Any]] # serialised CheckResult entries

def summary(self) -> str: ...
```

## Integrating into CI

A typical CI step:

```python
# tests/instrument/test_protocol_certification.py
from layerlens.instrument.adapters.protocols.certification import (
ProtocolCertificationSuite,
)


def test_all_protocol_adapters_pass_ga_certification() -> None:
suite = ProtocolCertificationSuite()
results = suite.certify_all()

failures = [r for r in results if not r.passed]
assert not failures, "\n".join(r.summary() for r in failures)
```

`certify_all()` covers the three current GA protocol adapters:

- `A2AAdapter`
- `AGUIAdapter`
- `MCPExtensionsAdapter`

For commerce-protocol adapters (AP2, A2UI, UCP) certify each one
explicitly with `suite.certify(MyAdapterClass)` — they share the same
`BaseProtocolAdapter` contract.

## Adding a new check

To add a new check to the suite, append a private `_check_*` method that
returns a `CheckResult` (or `list[CheckResult]`) and call it from
`certify()`. Keep the contract narrow: each check should test one
invariant and produce a clear failure message naming the adapter class.

## What this suite does NOT verify

- Per-protocol semantic correctness (does A2A actually emit the right
events for the protocol?). That belongs in the per-adapter unit and
live tests under `tests/instrument/adapters/protocols/`.
- Performance under load. The suite runs a single `connect()`/`disconnect()`
pair — load-testing is out of scope.
- Backward compatibility. Use the schema-compatibility test in
`tests/instrument/test_event_schema_compat.py` for that.
120 changes: 120 additions & 0 deletions docs/adapters/protocols-a2a.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# A2A protocol adapter

`layerlens.instrument.adapters.protocols.a2a.A2AAdapter` instruments the
[Agent-to-Agent (A2A) protocol](https://github.com/google/A2A) via
dual-channel instrumentation: server-side wrapping intercepts incoming
JSON-RPC requests and SSE streams, client-side wrapping traces outgoing
task submissions and streamed updates.

The adapter also handles ACP-origin payloads (IBM Agent Communication
Protocol, merged into A2A in August 2025) via a built-in
`ACPNormalizer`.

## Install

```bash
pip install 'layerlens[protocols-a2a]'
```

The base SDK already includes `httpx`, so no additional packages are
strictly required. To use the official A2A SDK, install `a2a-sdk`
separately.

## Quick start

```python
from layerlens.instrument.adapters.protocols.a2a import A2AAdapter
from layerlens.instrument.transport.sink_http import HttpEventSink

sink = HttpEventSink(adapter_name="a2a")
adapter = A2AAdapter()
adapter.add_sink(sink)
adapter.connect()

# Register an Agent Card discovered from a peer agent
adapter.register_agent_card(
{
"name": "research-agent",
"url": "https://research.example.com/.well-known/agent.json",
"protocolVersion": "0.2.0",
"skills": [{"id": "search", "name": "Web search"}],
},
source="discovery",
)

# Trace a task submission + completion
adapter.on_task_submitted(
task_id="t-001",
receiver_url="https://research.example.com/a2a",
task_type="research",
submitter_agent_id="orchestrator-1",
)
adapter.on_task_completed(
task_id="t-001",
final_status="completed",
artifacts=[{"type": "text", "content": "..."}],
)

adapter.disconnect()
sink.close()
```

## What's wrapped

`A2AAdapter` provides a set of `on_*` methods that the host application
calls at the appropriate hook points:

- `register_agent_card(card_data, source)` — emits `protocol.agent_card`.
- `on_task_submitted(task_id, ...)` — emits `protocol.task_submitted`.
- `on_task_completed(task_id, final_status, ...)` — emits
`protocol.task_completed`.
- `on_task_delegation(from_agent, to_agent, ...)` — emits
`protocol.task_delegation` and `agent.handoff`.
- `on_stream_event(task_id, event_type, data)` — emits
`protocol.stream_event` for each SSE message.

Server- and client-side wrappers (`A2AClient`, `A2AServer` helpers in
`a2a/client.py` and `a2a/server.py`) automatically call these hooks from
the JSON-RPC layer.

## Events emitted

| Event | Layer | When |
|---|---|---|
| `protocol.agent_card` | L4a | Per `register_agent_card` call. |
| `protocol.task_submitted` | L4a | Per outbound or inbound task submission. |
| `protocol.task_completed` | L4a | Per task terminal status. |
| `protocol.task_delegation` | L4a | Per cross-agent delegation. |
| `protocol.stream_event` | L4a | Per SSE event in a streamed task. |
| `agent.handoff` | L4a | Mirrors `protocol.task_delegation` for the agent timeline. |

## A2A specifics

- **ACP normalization**: `ACPNormalizer` detects ACP-shaped payloads and
rewrites them to A2A shape before emission. The original protocol
origin (`acp` vs `a2a`) is preserved on the event.
- **Task state machine**: `TaskStateMachine` (in `a2a/task_lifecycle.py`)
validates terminal-state transitions; invalid transitions emit
`policy.violation` rather than `task_completed`.
- **Memory sharing**: pass `memory_service=...` to the constructor and
the adapter will store completed task contexts as episodic memory and
share to a target agent via `AgentMemoryService.share_memory()`.
- **Artifact hashing**: returned artifacts are sha256-hashed and stored
on the event as `artifact_hashes` for later content-addressing.
- **Health**: `probe_health(endpoint)` fetches `endpoint/.well-known/agent.json`
and returns reachability + latency.

## Capture config

```python
from layerlens.instrument.adapters._base import CaptureConfig

# All protocol.* events are L4a — the standard preset captures them.
adapter = A2AAdapter(capture_config=CaptureConfig.standard())
```

## BYOK

A2A authentication is per-agent (`authScheme` in the Agent Card). The
adapter does not own those credentials; they belong to the underlying
A2A client. For platform-managed BYOK see `docs/adapters/byok.md`.
89 changes: 89 additions & 0 deletions docs/adapters/protocols-a2ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# A2UI (Agent-to-User Interface) protocol adapter

`layerlens.instrument.adapters.protocols.a2ui.A2UIAdapter` instruments
the A2UI protocol — surface lifecycle and user-action events for
agent-driven UI experiences (checkout widgets, confirmation dialogs,
inline agent panels).

## Install

```bash
pip install 'layerlens[protocols-a2ui]'
```

The `protocols-a2ui` extra has no required dependencies; the adapter
operates on protocol payloads, not on a specific SDK.

## Quick start

```python
from layerlens.instrument.adapters.protocols.a2ui import A2UIAdapter
from layerlens.instrument.transport.sink_http import HttpEventSink

sink = HttpEventSink(adapter_name="a2ui")
adapter = A2UIAdapter()
adapter.add_sink(sink)
adapter.connect()

adapter.on_surface_created(
surface_id="surf-checkout-1",
org_id="org-123",
root_component_id="cmp-checkout-root",
component_count=12,
)

adapter.on_user_action(
surface_id="surf-checkout-1",
action_name="confirm_purchase",
org_id="org-123",
component_id="cmp-confirm-btn",
context={"cart_total": 49.99, "currency": "USD"},
)

adapter.disconnect()
sink.close()
```

## What's wrapped

`A2UIAdapter` exposes two primary hooks the host UI runtime calls:

- `on_surface_created(surface_id, org_id, root_component_id, component_count)`
— emits `commerce.ui.surface_created` and registers the surface
in-process for action correlation.
- `on_user_action(surface_id, action_name, org_id, component_id, context)`
— emits `commerce.ui.user_action`. The `context` dict is **always**
sha256-hashed before emission; cleartext context never leaves the host.

## Events emitted

| Event | Layer | When |
|---|---|---|
| `commerce.ui.surface_created` | L7c | Per `on_surface_created`. |
| `commerce.ui.user_action` | L7c | Per `on_user_action`. |

Like AP2 events, `commerce.ui.*` events bypass `CaptureConfig` gating
via `ALWAYS_ENABLED_EVENT_TYPES` — these are audit-critical UI events.

## A2UI specifics

- **PII safety by construction**: `context` is hashed before emission.
This is a hard guarantee — the cleartext value is never stored on the
event, never logged at INFO level, and never written to disk. Only the
hash is suitable for cross-event correlation; for content inspection,
use the host's own logging at debug level.
- **Component-level granularity**: `component_id` is optional but
recommended for analytics — surfaces can drill into per-component
conversion funnels.
- **Surface tree summary**: only `component_count` and `root_component_id`
are captured at surface creation. Full tree introspection is left to
the host application.

## Capture config

`commerce.*` events are always captured regardless of `CaptureConfig`
flags.

## BYOK

Not applicable — A2UI is transport-only at the protocol layer.
Loading