# Bridges

This page documents the bridge surface used by `pydantic-acp`.

If you are integrating `langchain-acp`, read [LangChain ACP Bridges](langchain-acp/bridges.md).

Capability bridges are the adapter’s main extension seam for ACP-visible runtime behavior.

Use a bridge when you want to contribute:

- ACP session metadata
- config options
- modes
- MCP server classification
- buffered ACP updates
- model settings derived from session state
- Pydantic AI capabilities that should be wired into the active agent

## Base Types

| Type | Purpose |
|---|---|
| `CapabilityBridge` | synchronous or async hook point for ACP-facing state and agent contributions |
| `BufferedCapabilityBridge` | base class for bridges that emit buffered ACP update objects |

## Writing A Custom `CapabilityBridge`

Most docs show how to configure built-in bridges. If you are extending the SDK, the key thing to understand is that a bridge is just a narrow, synchronous contribution surface that the runtime polls at specific points.

Use plain `CapabilityBridge` when you only need to:

- add session metadata
- classify tools
- expose MCP transport capability flags
- expose config options or mode state
- derive model settings from session state

Use `BufferedCapabilityBridge` when the bridge also needs to emit ACP transcript updates over time.

## External Hook Events

Use `ExternalHookEventBridge` when an integration already knows about lifecycle events and wants to project them into ACP without installing Pydantic AI hooks or writing directly to the ACP client.

```python
from pydantic_acp import ExternalHookEventBridge, HookEvent

bridge = ExternalHookEventBridge()
bridge.record_event(
    session,
    HookEvent(
        event_id="before_run",
        hook_name="before_run",
        tool_name=None,
        tool_filters=(),
        raw_output="completed",
        status="completed",
    ),
)
```

The bridge buffers updates and the adapter drains them through the normal bridge manager. Its session metadata appears under `external_hooks` by default and includes emission mode, pending event count, hidden event ids, and projection title prefix.

### Override Matrix

| Method | Override it when | Return value |
|---|---|---|
| `build_agent_capabilities(...)` | your bridge contributes Pydantic AI capabilities that should be attached to the active agent | `tuple[AbstractCapability, ...]` |
| `get_session_metadata(...)` | you want a metadata section under your bridge `metadata_key` | `dict[str, JsonValue]` |
| `get_tool_kind(...)` | you want custom ACP tool classification | `ToolKind` |
| `get_mcp_capabilities(...)` | your bridge requires MCP transport capability flags | `McpCapabilities` |
| `get_config_options(...)` / `set_config_option(...)` | the bridge owns ACP config surface | `list[ConfigOption]` |
| `get_mode_state(...)` / `set_mode(...)` | the bridge owns ACP-visible mode state | `ModeState` |
| `get_model_settings(...)` | session state should change model settings | `ModelSettings` |
| `drain_updates(...)` | the bridge emits buffered ACP transcript updates | `list[SessionTranscriptUpdate]` |

Practical rules:

- use `build_agent_capabilities(...)` when the bridge needs to materialize upstream Pydantic AI capabilities
- `AgentBridgeBuilder` is the adapter-local helper that turns those bridge contributions into agent constructor inputs
- set `metadata_key` if you want your metadata to appear in session metadata
- keep classification deterministic; the first bridge that returns a `ToolKind` wins
- return `None` when your bridge is not authoritative for that surface
- use bridge-local buffering only when you truly need ACP transcript updates, not just metadata

## `AgentBridgeBuilder` Is The Capability Wiring Seam

`AdapterConfig(capability_bridges=[...])` makes the adapter aware of bridge-owned ACP surfaces such as:

- session metadata
- tool classification
- config options
- mode state
- model settings

If a bridge also contributes upstream Pydantic AI capabilities, those still need to be attached to
the active agent instance.

Use `AgentBridgeBuilder(...)` inside your factory or source:

```python
builder = AgentBridgeBuilder(
    session=session,
    capability_bridges=bridges,
)
contributions = builder.build()

agent = Agent(
    model,
    capabilities=contributions.capabilities,
)
```

That is the intended seam for:

- `HookBridge`
- `PrepareToolsBridge`
- `ThreadExecutorBridge`
- `SetToolMetadataBridge`
- `IncludeToolReturnSchemasBridge`
- `WebSearchBridge`
- `WebFetchBridge`

## Compatibility Note: History Processor Types

`HistoryProcessorBridge` depends on Pydantic AI history-processor callable types.
ACP Kit models those callable shapes locally and wraps them as
`ProcessHistory` capabilities inside `contributions.capabilities`.

That means:

- the adapter is no longer directly coupled to upstream private
  history-processor imports
- pass only `capabilities=contributions.capabilities` when constructing the agent

## Example: Custom Hook Introspection + MCP Metadata Classification

This is the missing pattern most custom integrations need: inspect hooks already attached to the source agent, expose them in ACP metadata, and classify a subset of tools as MCP-backed search or execute tools.

```python
from dataclasses import dataclass

from acp.schema import McpCapabilities, ToolKind
from pydantic_ai import Agent
from pydantic_acp import (
    AcpSessionContext,
    AdapterConfig,
    CapabilityBridge,
    JsonValue,
    RegisteredHookInfo,
    RuntimeAgent,
    list_agent_hooks,
    run_acp,
)


@dataclass(frozen=True, slots=True, kw_only=True)
class HookAwareMcpBridge(CapabilityBridge):
    metadata_key: str | None = "workspace"
    search_prefix: str = "mcp_repo_"
    execute_prefix: str = "mcp_shell_"

    def get_session_metadata(
        self,
        session: AcpSessionContext,
        agent: RuntimeAgent,
    ) -> dict[str, JsonValue]:
        hook_infos = list_agent_hooks(agent)
        return {
            "cwd": str(session.cwd),
            "hook_count": len(hook_infos),
            "hooks": [self._serialize_hook_info(hook_info) for hook_info in hook_infos],
        }

    def get_mcp_capabilities(self, agent: RuntimeAgent | None = None) -> McpCapabilities:
        del agent
        return McpCapabilities(http=True)

    def get_tool_kind(self, tool_name: str, raw_input: JsonValue | None = None) -> ToolKind | None:
        del raw_input
        if tool_name.startswith(self.search_prefix):
            return "search"
        if tool_name.startswith(self.execute_prefix):
            return "execute"
        return None

    def _serialize_hook_info(self, hook_info: RegisteredHookInfo) -> JsonValue:
        return {
            "event_id": hook_info.event_id,
            "hook_name": hook_info.hook_name,
            "tool_filters": list(hook_info.tool_filters),
        }


agent = Agent("openai:gpt-5", name="hook-aware-agent")

run_acp(
    agent=agent,
    config=AdapterConfig(
        capability_bridges=[HookAwareMcpBridge()],
    ),
)
```

What this bridge is doing:

- `list_agent_hooks(agent)` introspects hooks that were already attached to the source agent
- `metadata_key = "workspace"` makes the returned metadata appear under `session.metadata["workspace"]`
- `get_mcp_capabilities(...)` advertises that the bridge contributes MCP-aware HTTP metadata
- `get_tool_kind(...)` classifies matching tools before the base tool classifier runs

When to promote this to `BufferedCapabilityBridge`:

- you want ACP transcript cards when the bridge itself completes work
- you need `_record_completed_event(...)` or `_record_failed_event(...)`
- metadata alone is not enough; the client should see a time-ordered update stream

## Existing Hook Introspection Helpers

The bridge example above depends on one public helper:

- `list_agent_hooks(agent)`

Use it when you want to inspect hooks that already exist on the source agent.

That is different from `HookBridge`:

- **existing hook introspection**
  inspects hooks that are already present on the source agent
- **`HookBridge`**
  contributes bridge-owned hook capability at build time

If you want to render existing hook callbacks in session metadata or ACP listings, start with `list_agent_hooks(...)`.
If you want the bridge layer itself to contribute hook behavior, use `HookBridge`.

## Event Stream Hook Contract

Pydantic AI treats `run_event_stream` differently from the ordinary async hook callbacks.

The contract is:

- `run_event_stream` must return an `AsyncIterable[AgentStreamEvent]`
- it must not return a coroutine that later resolves to a stream
- if you instrument or wrap that hook, preserve the async-iterable boundary

This matters for both custom `Hooks(...)` usage and hook introspection wrappers.
If you accidentally return a coroutine, the run will fail when the runtime reaches `async for`.

## Built-in Bridges

### `PrepareToolsBridge`

Shapes tool availability per mode.

Use it for:

- read-only vs write-enabled modes
- hiding dangerous tools in planning mode
- activating native ACP plan state
- exposing plan progress tools only in execution modes

It is the bridge most real coding-agent setups start with.

### `PrepareOutputToolsBridge`

Shapes output-tool availability per mode.

Use it when:

- structured-output tools should be filtered separately from normal function tools
- ACP session metadata should expose the active output-tool mode
- output-tool preparation should emit ACP-visible progress and failure updates

This mirrors `PrepareToolsBridge`, but targets Pydantic AI's
`PrepareOutputTools` capability.

### `ThinkingBridge`

Exposes Pydantic AI’s `Thinking` capability through ACP session config.

Use it when:

- you want a session-local reasoning effort selector
- ACP clients should be able to inspect or change thinking effort

### `HookBridge`

Adds a `Hooks` capability into the active agent.

Useful when you want ACP-visible hook updates that come from bridge-owned hooks rather than only from hooks already attached to the source agent.

The bridge covers the current Pydantic AI hook surface, including tool
preparation, output-tool preparation, output validation, output processing, and
deferred tool-call observation.

You can also suppress noisy default hook rendering with:

```python
HookBridge(hide_all=True)
```

### `HistoryProcessorBridge`

Wraps history processors so their activity can be reflected into ACP updates.

### `WebSearchBridge`

Adds Pydantic AI's `WebSearch` capability into the active agent and classifies matching tools as ACP `search`.

Use it when:

- the runtime should expose builtin or local web search through one bridge-owned capability
- ACP transcript cards should classify `web_search` or local fallback search tools as search operations
- session metadata should show search configuration such as allowed domains or context size

Default classified tool names:

- `web_search`
- `duckduckgo_search`
- `exa_search`
- `tavily_search`

UI note:

- add `WebToolProjectionMap()` or `BuiltinToolProjectionMap()` when you want ACP transcript cards to show query/domain context at start and search results at completion instead of generic tool output

### `WebFetchBridge`

Adds Pydantic AI's `WebFetch` capability into the active agent and classifies matching tools as ACP `fetch`.

Use it when:

- the runtime should expose builtin or local URL fetching through one bridge-owned capability
- ACP transcript cards should classify `web_fetch` as a fetch operation instead of generic execute
- session metadata should show fetch guardrails such as allowed domains, citations, or token limits

This is useful when you want message-history trimming or contextual rewriting to remain observable.

UI note:

- add `WebToolProjectionMap()` or `BuiltinToolProjectionMap()` when you want ACP transcript cards to show fetched URLs, page titles, text previews, and binary-fetch status

### `ImageGenerationBridge`

Adds upstream `ImageGeneration` through the bridge-builder seam.

Use it when:

- the runtime should expose builtin image generation or a local fallback subagent through one ACP-owned seam
- session metadata should reflect image-generation policy such as quality, size, or output format
- projection maps should recognize `image_generation` or `generate_image` as intentional builtin work instead of generic tool noise

UI note:

- add `BuiltinToolProjectionMap()` when you want ACP transcript cards to show prompt, quality, size, and revised prompt summary for builtin image generation

### `ThreadExecutorBridge`

Adds Pydantic AI's `ThreadExecutor` capability through the bridge-builder seam.

Use it when:

- your ACP service is long-lived
- sync tools or callbacks should run on a bounded executor
- you want bridge-owned agent construction to keep executor policy explicit

### `SetToolMetadataBridge`

Adds upstream `SetToolMetadata` capability through the bridge-builder seam.

Use it when:

- tool metadata should be attached centrally instead of per-tool definition
- downstream selectors, MCP logic, or provider behavior depend on consistent metadata

### `IncludeToolReturnSchemasBridge`

Adds upstream `IncludeToolReturnSchemas` capability through the bridge-builder seam.

Use it when:

- you want richer tool return contracts sent to models
- downstream integrations should enable return-schema support consistently across selected tools

### `ToolsetBridge`

Adds upstream `Toolset` capability through the bridge-builder seam.

Use it when:

- a maintained `FunctionToolset` or other agent toolset should be injected through the same ACP-owned bridge path as other capabilities
- integration code wants one explicit place to wire toolset-owned instructions or wrappers

Compatibility notes:

- toolset `get_instructions()` output passes through to the upstream model request as `instruction_parts`
- ordering is explicit: `AgentBridgeBuilder.build(capabilities=...)` keeps user-supplied capabilities first, then appends bridge capabilities in configured bridge order

### `PrefixToolsBridge`

Adds upstream `PrefixTools` capability through the bridge-builder seam.

Use it when:

- a wrapped capability's tool names need a stable namespace prefix
- downstream clients should see prefixed tool names without custom tool re-registration logic

### `McpCapabilityBridge`

Adds upstream `MCP` capability through the bridge-builder seam.

Use it when:

- the model should use builtin MCP server support when available and local HTTP fallback otherwise
- ACP session metadata should expose the connected MCP URL, resolved server id, or allowlist shape
- projection maps should summarize `mcp_server:*` builtin tool calls

Compatibility note:

- this bridge is separate from MCP toolsets such as `MCPServerStdio` or `MCPServerStreamableHTTP`
- if those toolsets are attached directly to the agent with `include_instructions=True`, their server instructions still flow through the normal upstream toolset path into `instruction_parts`

UI note:

- add `BuiltinToolProjectionMap()` when you want ACP transcript cards to summarize builtin MCP calls such as `call_tool`, `list_tools`, and output previews instead of generic execute cards

### `SessionMcpBridge`

Turns ACP client-provided `session/new.mcpServers` payloads into a real Pydantic AI
`MCPToolset` capability for the active session.

Use it when:

- the client, editor, or host process should decide which MCP servers attach to a session
- `session.mcp_servers` should become runnable model tools instead of observability metadata only
- server instructions and tool return schemas should flow through Pydantic AI's normal toolset path

Without this bridge, `pydantic-acp` still stores session MCP server payloads and shows them in
`/mcp-servers`, but it does not connect to those servers or register their tools.

```python
from pydantic_acp import AdapterConfig, SessionMcpBridge

config = AdapterConfig(
    capability_bridges=[
        SessionMcpBridge(
            include_instructions=True,
            include_return_schema=True,
            tool_name_prefixes=frozenset({"docs_", "repo_"}),
        ),
    ],
)
```

Client-side `mcpServers` payloads may contain stdio, streamable HTTP, or SSE servers. The bridge
keeps env/header values available to the underlying MCP connection but only publishes env/header
names in session metadata, so secrets are not echoed into ACP-visible metadata.

### `OpenAICompactionBridge`

Adds provider-owned OpenAI Responses compaction through the bridge-builder seam.

Use it when:

- long-running ACP sessions should compact history without looking stalled in the client
- the ACP transcript should show a visible `Context Compaction` card before and after OpenAI compaction runs
- session metadata should still expose the configured trigger and instructions

UI behavior:

- no extra projection map is required
- when compaction triggers, ACP emits a visible `Context Compaction` start/update pair
- OpenAI shows provider status and round-trip payload preservation instead of a blank wait
- OpenAI completion is emitted by the bridge-owned wrapper so the same card opens and closes around the compaction request

### `AnthropicCompactionBridge`

Adds provider-owned Anthropic context-management compaction through the bridge-builder seam.

Use it when:

- Anthropic context management should be configured through the bridge seam
- Anthropic compaction summaries should be visible in ACP transcripts instead of disappearing into raw provider behavior

UI behavior:

- no extra projection map is required
- when Anthropic returns a `CompactionPart`, ACP emits a visible `Context Compaction` card
- readable Anthropic compaction summaries are shown in the completion update

## Builtin Capability Projection

Use `BuiltinToolProjectionMap()` when the agent exposes upstream builtin capability tools and you want ACP-visible cards instead of generic execute noise.

Current builtin projection coverage:

- web search
- web fetch
- image generation
- builtin MCP server tools

Compaction visibility is built into the runtime path and does not require a projection map.

### `McpBridge`

Adds MCP-aware metadata and tool classification:

- server definitions
- tool-to-server mapping
- tool kind classification
- approval-policy key routing
- optional config surface for MCP-backed state

## Example: Mode-aware Tools + MCP Metadata + Thinking

```python
from pydantic_acp import (
    AdapterConfig,
    McpBridge,
    McpServerDefinition,
    McpToolDefinition,
    PrepareToolsBridge,
    PrepareToolsMode,
    ThinkingBridge,
)
from pydantic_ai.tools import RunContext, ToolDefinition


def ask_tools(
    ctx: RunContext[None],
    tool_defs: list[ToolDefinition],
) -> list[ToolDefinition]:
    del ctx
    return [tool_def for tool_def in tool_defs if tool_def.name == "mcp_repo_search_paths"]


def agent_tools(
    ctx: RunContext[None],
    tool_defs: list[ToolDefinition],
) -> list[ToolDefinition]:
    del ctx
    return list(tool_defs)


config = AdapterConfig(
    capability_bridges=[
        ThinkingBridge(),
        PrepareToolsBridge(
            default_mode_id="ask",
            modes=[
                PrepareToolsMode(
                    id="ask",
                    name="Ask",
                    description="Read-only inspection mode.",
                    prepare_func=ask_tools,
                ),
                PrepareToolsMode(
                    id="agent",
                    name="Agent",
                    description="Full workspace mode.",
                    prepare_func=agent_tools,
                    plan_tools=True,
                ),
            ],
        ),
        McpBridge(
            servers=[
                McpServerDefinition(
                    server_id="repo",
                    name="Repository",
                    transport="http",
                    tool_prefix="mcp_repo_",
                    description="Repository inspection tools.",
                )
            ],
            tools=[
                McpToolDefinition(
                    tool_name="mcp_repo_search_paths",
                    server_id="repo",
                    kind="search",
                )
            ],
        ),
    ],
)
```

## Bridge Builder

`AgentBridgeBuilder` is the intended way to assemble bridge contributions into a session-specific agent build:

```python
from pydantic_acp import AgentBridgeBuilder

builder = AgentBridgeBuilder(
    session=session,
    capability_bridges=bridges,
)
contributions = builder.build()
```

It returns:

- `capabilities` (including `ProcessHistory` wrappers for configured history processors)

That makes it a natural fit inside `agent_factory` or `AgentSource.get_agent(...)`.

## Common Failure Modes

- defining multiple `PrepareToolsMode(..., plan_mode=True)` entries raises an error; native plan mode is singular
- using reserved mode ids such as `model`, `thinking`, `tools`, `hooks`, or `mcp-servers` raises an error because those names are reserved for slash commands
- `HookBridge(hide_all=True)` hides hook listing output; it does not remove hook capability wiring
- `run_event_stream` wrappers must return an async iterable; returning a coroutine or plain object breaks stream execution
- `McpBridge` only contributes MCP metadata and classification; it does not register the underlying tools for you
- `SessionMcpBridge` registers client-provided `session/new.mcpServers`; use it when those payloads should become real Pydantic AI MCP toolsets

## Existing Hook Introspection vs HookBridge

These are related but not identical:

- **existing hook introspection**
  observes a `Hooks` capability that was already present on the source agent
- **`HookBridge`**
  contributes a bridge-owned `Hooks` capability during the session build

If you want to render existing hook callbacks, use `HookProjectionMap`.
If you want the bridge layer to contribute hooks itself, use `HookBridge`.
