Skip to content

pydantic_acp API

This page documents the public surface re-exported by pydantic_acp.

Functions

create_acp_agent(agent=None, *, agent_factory=None, agent_source=None, config=None, projection_maps=None)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/runtime/server.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def create_acp_agent(
    agent: PydanticAgent[AgentDepsT, OutputDataT] | None = None,
    *,
    agent_factory: AgentFactory[AgentDepsT, OutputDataT] | None = None,
    agent_source: AgentSource[AgentDepsT, OutputDataT] | None = None,
    config: AdapterConfig | None = None,
    projection_maps: Sequence[ProjectionMap | HookProjectionMap] | None = None,
) -> PydanticAcpAgent[AgentDepsT, OutputDataT]:
    resolved_source = _resolve_agent_source(
        agent=agent,
        agent_factory=agent_factory,
        agent_source=agent_source,
    )
    resolved_config = _resolve_config(
        config=config,
        agent_name=agent.name if agent is not None else None,
        projection_maps=projection_maps,
    )
    adapter = PydanticAcpAgent(resolved_source, config=resolved_config)
    return adapter

create_acp_model(*, acp_agent=None, acp_command=None, model_name=None, cwd=None, env=None, stderr_mode='inherit', terminate_timeout=5.0, prompt_renderer=None, history_mode='latest_user', delegate_client=None, enable_pydantic_acp_meta=None, auth_method_id=None, raise_on_empty_turn=False, settings=None, profile=None)

Create a Pydantic AI model backed by an ACP agent or ACP stdio command.

Exactly one of acp_agent or acp_command must be provided. Passing model_name=None leaves ACP model selection to the remote agent's session default and does not send a session/set_config_option request for "model". auth_method_id selects an explicit ACP authentication method when session/new reports auth_required. Set raise_on_empty_turn=True when a silent ACP turn should fail as UnexpectedModelBehavior instead of producing an empty Pydantic AI response.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/factory.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def create_acp_model(
    *,
    acp_agent: AcpAgent | None = None,
    acp_command: Sequence[str] | None = None,
    model_name: str | None = None,
    cwd: str | Path | None = None,
    env: Mapping[str, str] | None = None,
    stderr_mode: CommandStderrMode = "inherit",
    terminate_timeout: float = 5.0,
    prompt_renderer: AcpPromptRenderer | None = None,
    history_mode: HistoryMode = "latest_user",
    delegate_client: AcpClient | None = None,
    enable_pydantic_acp_meta: bool | None = None,
    auth_method_id: str | None = None,
    raise_on_empty_turn: bool = False,
    settings: ModelSettings | None = None,
    profile: ModelProfileSpec | None = None,
) -> AcpModel:
    """Create a Pydantic AI model backed by an ACP agent or ACP stdio command.

    Exactly one of ``acp_agent`` or ``acp_command`` must be provided. Passing
    ``model_name=None`` leaves ACP model selection to the remote agent's session
    default and does not send a ``session/set_config_option`` request for ``"model"``.
    ``auth_method_id`` selects an explicit ACP authentication method when
    ``session/new`` reports ``auth_required``. Set ``raise_on_empty_turn=True``
    when a silent ACP turn should fail as ``UnexpectedModelBehavior`` instead
    of producing an empty Pydantic AI response.
    """

    command = _normalize_command(acp_command)
    resolved_cwd = Path.cwd() if cwd is None else Path(cwd)
    if command is None:
        if acp_agent is None:
            raise ValueError("Exactly one of acp_agent or acp_command must be provided.")
        source_agent = acp_agent
    else:
        if acp_agent is not None:
            raise ValueError("Exactly one of acp_agent or acp_command must be provided.")
        source_agent = AcpCommandAgent(
            options=AcpCommandOptions(
                command=command,
                cwd=resolved_cwd,
                env=env,
                stderr_mode=stderr_mode,
                terminate_timeout=terminate_timeout,
            ),
        )

    provider = AcpProvider(
        acp_agent=source_agent,
        host_client=delegate_client,
        cwd=resolved_cwd,
        prompt_renderer=prompt_renderer,
        history_mode=history_mode,
        enable_pydantic_acp_meta=enable_pydantic_acp_meta,
        auth_method_id=auth_method_id,
        raise_on_empty_turn=raise_on_empty_turn,
    )
    return provider.model(
        model_name,
        settings=settings,
        profile=profile,
        history_mode=history_mode,
    )

run_acp(agent=None, *, agent_factory=None, agent_source=None, config=None, projection_maps=None)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/runtime/server.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def run_acp(
    agent: PydanticAgent[AgentDepsT, OutputDataT] | None = None,
    *,
    agent_factory: AgentFactory[AgentDepsT, OutputDataT] | None = None,
    agent_source: AgentSource[AgentDepsT, OutputDataT] | None = None,
    config: AdapterConfig | None = None,
    projection_maps: Sequence[ProjectionMap | HookProjectionMap] | None = None,
) -> None:
    adapter = create_acp_agent(
        agent=agent,
        agent_factory=agent_factory,
        agent_source=agent_source,
        config=config,
        projection_maps=projection_maps,
    )
    asyncio.run(run_agent(adapter))

compose_projection_maps(projection_maps)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/projection.py
171
172
173
174
175
176
177
178
179
180
def compose_projection_maps(
    projection_maps: Sequence[ProjectionMap] | None,
) -> ProjectionMap | None:
    if projection_maps is None:
        return None
    if len(projection_maps) == 0:
        return None
    if len(projection_maps) == 1:
        return projection_maps[0]
    return CompositeProjectionMap(maps=tuple(projection_maps))

Core Classes And Data Types

AdapterConfig(*, agent_name=DEFAULT_AGENT_NAME, agent_title=DEFAULT_AGENT_TITLE, agent_version=DEFAULT_AGENT_VERSION, allow_model_selection=False, approval_bridge=NativeApprovalBridge(), approval_state_provider=None, authentication_provider=None, capability_bridges=list(), config_options_provider=None, contextual_extension_router=None, enable_generic_tool_projection=True, enable_model_config_option=True, extension_router=None, host_access_policy=None, hook_projection_map=HookProjectionMap(), models_provider=None, modes_provider=None, native_plan_additional_instructions=None, native_plan_persistence_provider=None, plan_id='acpkit.plan', plan_provider=None, plan_update_mode='full', prompt_capabilities=AdapterPromptCapabilities(), prompt_model_override_provider=None, replay_history_on_load=True, slash_command_provider=None, available_models=list(), session_store=MemorySessionStore(), output_serializer=DefaultOutputSerializer(), projection_maps=tuple(), tool_classifier=DefaultToolClassifier()) dataclass

AdapterModel(*, model_id, name, override, description=None) dataclass

AdapterPromptCapabilities(*, audio=True, image=True, embedded_context=True) dataclass

AcpSessionContext(*, session_id, cwd, created_at, updated_at, additional_directories=(), title=None, session_model_id=None, message_history_json=None, plan_markdown=None, plan_entries=list(), active_plan_id=None, config_values=dict(), mcp_servers=list(), metadata=dict(), transcript=list(), client=None, client_capabilities=None) dataclass

supports_config_options()

Return whether the connected client accepts session config options.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/session/state.py
119
120
121
122
123
124
def supports_config_options(self) -> bool:
    """Return whether the connected client accepts session config options."""
    capabilities = self.client_capabilities
    if capabilities is None:
        return True
    return capabilities.session is not None and capabilities.session.config_options is not None

supports_boolean_config_options()

Return whether the connected client accepts boolean config options.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/session/state.py
126
127
128
129
130
131
132
133
134
135
136
def supports_boolean_config_options(self) -> bool:
    """Return whether the connected client accepts boolean config options."""
    capabilities = self.client_capabilities
    if capabilities is None:
        return True
    session_capabilities = capabilities.session
    return (
        session_capabilities is not None
        and session_capabilities.config_options is not None
        and session_capabilities.config_options.boolean is not None
    )

supports_plan_content_updates()

Return whether the client supports unstable plan delta updates.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/session/state.py
138
139
140
141
def supports_plan_content_updates(self) -> bool:
    """Return whether the client supports unstable plan delta updates."""
    capabilities = self.client_capabilities
    return capabilities is None or capabilities.plan is not None

ask_choice(question, choices, *, fallback=None) async

Ask the connected client to select one typed value.

The helper compiles choices to the standard ACP form-elicitation schema. Clients control presentation and may ignore option descriptions.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/session/state.py
164
165
166
167
168
169
170
171
172
173
174
175
176
async def ask_choice(
    self,
    question: str,
    choices: Sequence[ElicitationChoice[ChoiceValueT]],
    *,
    fallback: ChoiceElicitationFallback[ChoiceValueT] | None = None,
) -> ChoiceElicitationResult[ChoiceValueT]:
    """Ask the connected client to select one typed value.

    The helper compiles choices to the standard ACP form-elicitation schema.
    Clients control presentation and may ignore option descriptions.
    """
    return await _ask_choice(self, question, choices, fallback=fallback)

JsonValue = JsonPrimitive | list['JsonValue'] | dict[str, 'JsonValue'] module-attribute

RuntimeAgent = PydanticAgent[Any, Any] module-attribute

Typed Elicitation

ElicitationChoice(*, value, label, description=None, default=False) dataclass

Bases: Generic[ChoiceValueT]

One typed value offered by :meth:AcpSessionContext.ask_choice.

ChoiceElicitationResult = ChoiceElicitationAccepted[ChoiceValueT] | ChoiceElicitationDeclined | ChoiceElicitationCancelled module-attribute

ChoiceElicitationFallback = Callable[[], ChoiceElicitationResult[ChoiceValueT] | ChoiceValueT | Awaitable[ChoiceElicitationResult[ChoiceValueT] | ChoiceValueT]] module-attribute

ChoiceElicitationAccepted(*, value) dataclass

Bases: Generic[ChoiceValueT]

An accepted elicitation containing the selected typed value.

ChoiceElicitationDeclined() dataclass

An elicitation the user explicitly declined to answer.

ChoiceElicitationCancelled() dataclass

An elicitation cancelled without an answer.

ElicitationUnsupportedError

Bases: RuntimeError

Raised when the connected ACP client cannot render the requested form.

InvalidElicitationResponseError

Bases: RuntimeError

Raised when a client accepts an elicitation with an invalid selection.

InvalidElicitationFallbackError

Bases: ValueError

Raised when a legacy fallback value is not one of the offered choices.

Protocol Extension And Authentication Contracts

ExtensionRouter

Bases: Protocol

Handle application-owned ACP extension methods and notifications.

ContextualExtensionRouter

Bases: Protocol

Handle extension traffic with public connection-scoped ACP state.

ExtensionContext(*, client, protocol_version, client_capabilities, client_info) dataclass

Public state negotiated for one ACP client connection.

AuthenticationProvider

Bases: Protocol

Provide ACP authentication methods and execute authentication requests.

AuthenticationMethod = EnvVarAuthMethod | TerminalAuthMethod | AuthMethodAgent module-attribute

ACP Client Provider Bridge

AcpProvider(*, acp_agent, host_client=None, cwd='.', name='acp', base_url='acp://local', protocol_version=PROTOCOL_VERSION, client_capabilities=None, client_info=None, mcp_servers=None, prompt_renderer=None, history_mode='latest_user', enable_pydantic_acp_meta=None, auth_method_id=None, raise_on_empty_turn=False)

Bases: Provider[Agent]

Pydantic AI provider that treats an ACP agent as the model backend.

This is the inverse of the normal pydantic-acp server adapter. The server adapter exposes a pydantic_ai.Agent through ACP. AcpProvider consumes an existing ACP agent and makes it available to Pydantic AI as a provider/model pair, so application code can write ordinary Pydantic AI agents while delegating the underlying model turn to ACP.

The provider owns ACP protocol/session setup, model selection through the remote agent's model config option, host/client update capture, and prompt rendering. It deliberately remains a provider rather than an alternate agent framework: Pydantic AI still owns the outer agent run, result validation, usage accumulation, and history shape.

Create a new ACP provider.

Parameters:

Name Type Description Default
acp_agent Agent

The ACP agent to wrap as a Pydantic AI provider/model.

required
host_client Client | None

Optional upstream :class:AcpClient to delegate real host operations to (file I/O, terminals, permissions, etc.).

None
cwd str | Path

Working directory passed to the ACP session on creation.

'.'
name str

Provider name reported via :attr:name. Defaults to "acp".

'acp'
base_url str

Base URL reported via :attr:base_url. Defaults to "acp://local".

'acp://local'
protocol_version int

ACP protocol version used during initialize. Defaults to :data:acp.PROTOCOL_VERSION.

PROTOCOL_VERSION
client_capabilities ClientCapabilities | None

ACP client capabilities sent during initialize. Defaults to an empty :class:~acp.schema.ClientCapabilities.

None
client_info Implementation | None

ACP implementation info sent during initialize. Defaults to a pydantic-acp-client stub.

None
mcp_servers Sequence[Any] | None

Optional list of MCP servers forwarded to new_session.

None
prompt_renderer AcpPromptRenderer | None

Custom callable that converts Pydantic AI messages into ACP prompt blocks. When None the built-in renderer is used.

None
history_mode HistoryMode

Controls how previous messages are rendered into the ACP prompt. "latest_user" (default) sends only the latest user turn; "full" sends the entire conversation.

'latest_user'
enable_pydantic_acp_meta bool | None

Enables the private pydantic_acp _meta extension used for ACP-backed Pydantic AI structured output. None auto-enables it only for ACP agents produced by this package; arbitrary ACP agents must not be trusted to implement this private contract.

None
auth_method_id str | None

ACP authenticate method id to use when the agent rejects session/new with an auth_required (-32000) error. When None the provider falls back to the first authentication method advertised by the agent's initialize response.

None
raise_on_empty_turn bool

When True, a prompt turn that produces no visible text for a text-output request raises :class:~pydantic_ai.exceptions.UnexpectedModelBehavior with an ACP-specific diagnostic instead of returning an empty response. Defaults to False to preserve the standard contract where an empty ACP turn yields a response with no parts.

False
Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
def __init__(
    self,
    *,
    acp_agent: AcpAgent,
    host_client: AcpClient | None = None,
    cwd: str | Path = ".",
    name: str = "acp",
    base_url: str = "acp://local",
    protocol_version: int = PROTOCOL_VERSION,
    client_capabilities: ClientCapabilities | None = None,
    client_info: Implementation | None = None,
    mcp_servers: Sequence[Any] | None = None,
    prompt_renderer: AcpPromptRenderer | None = None,
    history_mode: HistoryMode = "latest_user",
    enable_pydantic_acp_meta: bool | None = None,
    auth_method_id: str | None = None,
    raise_on_empty_turn: bool = False,
) -> None:
    """Create a new ACP provider.

    Args:
        acp_agent: The ACP agent to wrap as a Pydantic AI provider/model.
        host_client: Optional upstream :class:`AcpClient` to delegate real
            host operations to (file I/O, terminals, permissions, etc.).
        cwd: Working directory passed to the ACP session on creation.
        name: Provider name reported via :attr:`name`. Defaults to
            ``"acp"``.
        base_url: Base URL reported via :attr:`base_url`. Defaults to
            ``"acp://local"``.
        protocol_version: ACP protocol version used during
            ``initialize``. Defaults to :data:`acp.PROTOCOL_VERSION`.
        client_capabilities: ACP client capabilities sent during
            ``initialize``. Defaults to an empty
            :class:`~acp.schema.ClientCapabilities`.
        client_info: ACP implementation info sent during ``initialize``.
            Defaults to a ``pydantic-acp-client`` stub.
        mcp_servers: Optional list of MCP servers forwarded to
            ``new_session``.
        prompt_renderer: Custom callable that converts Pydantic AI
            messages into ACP prompt blocks. When ``None`` the built-in
            renderer is used.
        history_mode: Controls how previous messages are rendered into
            the ACP prompt. ``"latest_user"`` (default) sends only the
            latest user turn; ``"full"`` sends the entire conversation.
        enable_pydantic_acp_meta: Enables the private ``pydantic_acp``
            ``_meta`` extension used for ACP-backed Pydantic AI structured
            output. ``None`` auto-enables it only for ACP agents produced
            by this package; arbitrary ACP agents must not be trusted to
            implement this private contract.
        auth_method_id: ACP ``authenticate`` method id to use when the
            agent rejects ``session/new`` with an ``auth_required``
            (``-32000``) error. When ``None`` the provider falls back to
            the first authentication method advertised by the agent's
            ``initialize`` response.
        raise_on_empty_turn: When ``True``, a prompt turn that produces no
            visible text for a text-output request raises
            :class:`~pydantic_ai.exceptions.UnexpectedModelBehavior` with an
            ACP-specific diagnostic instead of returning an empty response.
            Defaults to ``False`` to preserve the standard contract where an
            empty ACP turn yields a response with no parts.

    """
    self._client = acp_agent

    self._host = AcpHostBridge(delegate=host_client)
    self._name = name
    self._base_url = base_url
    self._cwd = str(cwd)
    self._protocol_version = protocol_version
    self._client_capabilities = client_capabilities
    self._client_info = client_info or Implementation(
        name="pydantic-acp-client",
        version=__version__,
    )
    self._mcp_servers = list(mcp_servers or [])
    self._prompt_renderer = prompt_renderer
    self._history_mode = history_mode
    self._enable_pydantic_acp_meta = (
        _agent_supports_pydantic_acp_meta(acp_agent)
        if enable_pydantic_acp_meta is None
        else enable_pydantic_acp_meta
    )
    self._auth_method_id = auth_method_id
    self._raise_on_empty_turn = raise_on_empty_turn
    self._initialized = False
    self._session_id: str | None = None
    self._current_model_name: str | None = None
    self._model_config_option_available: bool | None = None
    self._auth_methods: list[AuthMethod] = []
    self._authenticated = False
    self._session_lock: asyncio.Lock | None = None
    self._session_lock_loop: asyncio.AbstractEventLoop | None = None

    if hasattr(self._client, "on_connect"):
        self._client.on_connect(self._host)

host property

The ACP host bridge connected to the wrapped ACP agent.

updates property

All ACP updates recorded by the host bridge so far.

enable_pydantic_acp_meta property

Whether this provider opts into private pydantic_acp ACP metadata.

raise_on_empty_turn property

Whether an empty ACP turn raises instead of returning empty parts.

close() async

Close the wrapped ACP agent when it exposes an async-compatible close hook.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
626
627
628
629
630
631
632
633
async def close(self) -> None:
    """Close the wrapped ACP agent when it exposes an async-compatible close hook."""
    close = getattr(self._client, "close", None)
    if close is None:
        return
    result = close()
    if inspect.isawaitable(result):
        await result

model(model_name=None, *, settings=None, profile=None, history_mode=None)

Build an :class:AcpModel bound to this provider.

When model_name is None, the bridge does not call ACP session/set_config_option for "model" and leaves model selection to the wrapped agent's session default. The visible Pydantic AI model name remains "agent".

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
def model(
    self,
    model_name: str | None = None,
    *,
    settings: ModelSettings | None = None,
    profile: ModelProfileSpec | None = None,
    history_mode: HistoryMode | None = None,
) -> AcpModel:
    """Build an :class:`AcpModel` bound to this provider.

    When ``model_name`` is ``None``, the bridge does not call ACP
    ``session/set_config_option`` for ``"model"`` and leaves model selection
    to the wrapped agent's session default. The visible Pydantic AI model
    name remains ``"agent"``.
    """
    return AcpModel(
        model_name=model_name,
        provider=self,
        settings=settings,
        profile=profile,
        history_mode=history_mode,
    )

render_prompt_blocks(messages, model_request_parameters, *, history_mode=None) async

Render Pydantic AI model messages into ACP prompt blocks.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
async def render_prompt_blocks(
    self,
    messages: Sequence[ModelMessage],
    model_request_parameters: ModelRequestParameters,
    *,
    history_mode: HistoryMode | None = None,
) -> list[AgentPromptBlock]:
    """Render Pydantic AI model messages into ACP prompt blocks."""
    if self._prompt_renderer is None:
        return _default_render_prompt_blocks(
            messages,
            model_request_parameters,
            history_mode=history_mode or self._history_mode,
        )
    rendered = self._prompt_renderer(messages, model_request_parameters)
    if inspect.isawaitable(rendered):
        rendered = await rendered
    return list(cast(Sequence[AgentPromptBlock], rendered))

request_prompt(*, model_name, prompt, model_request_parameters) async

Send one prompt turn to the ACP agent and collect its visible text.

Errors raised by the ACP agent (rate limits, auth rejection, upstream API failures) are propagated with anyio TaskGroup wrapping stripped so the caller sees the agent's real error rather than an opaque ExceptionGroup: unhandled errors in a TaskGroup.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
async def request_prompt(
    self,
    *,
    model_name: str | None,
    prompt: Sequence[AgentPromptBlock],
    model_request_parameters: ModelRequestParameters,
) -> _AcpPromptResult:
    """Send one prompt turn to the ACP agent and collect its visible text.

    Errors raised by the ACP agent (rate limits, auth rejection, upstream
    API failures) are propagated with anyio TaskGroup wrapping stripped so
    the caller sees the agent's real error rather than an opaque
    ``ExceptionGroup: unhandled errors in a TaskGroup``.
    """
    try:
        session_id = await self._ensure_session(model_name=model_name)
        start_index = self._host.snapshot_index()
        request_meta = None
        if self._enable_pydantic_acp_meta:
            request_meta = build_structured_output_request_meta(model_request_parameters)
        prompt_kwargs: dict[str, Any] = {
            "prompt": list(prompt),
            "session_id": session_id,
            "message_id": uuid4().hex,
        }
        if request_meta is not None:
            prompt_kwargs["_meta"] = request_meta
        prompt_response = await self._client.prompt(**prompt_kwargs)
        text = await self._agent_message_text_after_prompt(
            start_index,
            session_id=session_id,
            prompt_response=prompt_response,
        )

        response_meta = extract_field_meta(prompt_response)
        usage = _usage_from_acp(getattr(prompt_response, "usage", None))
        if not usage.has_values():
            usage = self._host.usage_update_since(start_index, session_id=session_id)
        stop_reason = getattr(prompt_response, "stop_reason", None) or getattr(
            prompt_response,
            "stopReason",
            None,
        )
        return _AcpPromptResult(
            text=text,
            usage=usage,
            stop_reason=stop_reason,
            session_id=session_id,
            structured_output=extract_structured_output(response_meta),
        )
    except asyncio.CancelledError:
        raise
    except Exception as exc:
        cleaned = _unwrap_acp_error(exc)
        if cleaned is exc:
            raise
        raise cleaned from None

ensure_session(*, model_name=None) async

Initialize the ACP connection and return an active session id.

This is the public entry point for bootstrapping a session without sending a prompt turn: it performs initialize (authenticating if the agent demands it), creates the session, and optionally selects a model. Callers that need to configure a session up front (for example to set a session mode) should use this instead of reaching into the private _ensure_session.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
846
847
848
849
850
851
852
853
854
855
856
async def ensure_session(self, *, model_name: str | None = None) -> str:
    """Initialize the ACP connection and return an active session id.

    This is the public entry point for bootstrapping a session without
    sending a prompt turn: it performs ``initialize`` (authenticating if the
    agent demands it), creates the session, and optionally selects a model.
    Callers that need to configure a session up front (for example to set a
    session mode) should use this instead of reaching into the private
    ``_ensure_session``.
    """
    return await self._ensure_session(model_name=model_name)

set_session_mode(mode_id) async

Set the ACP session mode, bootstrapping the session if needed.

ACP session modes (for example the permission mode) are distinct from the model session config option and are set via session/set_mode. This ensures a session exists, then delegates to the wrapped agent's set_session_mode when available.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
async def set_session_mode(self, mode_id: str) -> None:
    """Set the ACP session mode, bootstrapping the session if needed.

    ACP session modes (for example the permission mode) are distinct from
    the ``model`` session config option and are set via ``session/set_mode``.
    This ensures a session exists, then delegates to the wrapped agent's
    ``set_session_mode`` when available.
    """
    session_id = await self._ensure_session(model_name=None)
    set_mode = getattr(self._client, "set_session_mode", None)
    if set_mode is None:
        raise UserError(
            "The ACP agent does not expose 'set_session_mode', so the session "
            f"mode {mode_id!r} cannot be selected."
        )
    result = set_mode(session_id=session_id, mode_id=mode_id)
    if inspect.isawaitable(result):
        await result

AcpModel(model_name=None, *, provider, settings=None, profile=None, history_mode=None)

Bases: Model[Agent]

Pydantic AI Model backed by an ACP agent provider.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
def __init__(
    self,
    model_name: str | None = None,
    *,
    provider: AcpProvider,
    settings: ModelSettings | None = None,
    profile: ModelProfileSpec | None = None,
    history_mode: HistoryMode | None = None,
) -> None:
    self._model_name = model_name
    self._history_mode = history_mode
    self._provider = provider
    super().__init__(
        settings=settings,
        profile=profile or _profile_for_provider(provider),
    )

AcpHostBridge(*, delegate=None)

Minimal ACP host/client implementation used by :class:AcpProvider.

ACP agents send their visible output to a connected ACP client via session_update. Pydantic AI models, however, return a ModelResponse. This bridge is the seam between the two contracts: it records ACP updates so AcpModel can fold agent message chunks back into Pydantic AI response parts, while optionally delegating real host operations to an upstream ACP client supplied by the caller.

The bridge intentionally does not emulate a filesystem, terminal, approval UI, or extension namespace. When an ACP agent asks for such host operations and no delegate was supplied, the request fails explicitly instead of inventing host behavior that is not present.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
157
158
159
def __init__(self, *, delegate: AcpClient | None = None) -> None:
    self.delegate = delegate
    self.updates: list[AcpUpdateRecord] = []

snapshot_index()

Return an index that can later be used to read only new updates.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
161
162
163
def snapshot_index(self) -> int:
    """Return an index that can later be used to read only new updates."""
    return len(self.updates)

records_since(index, *, session_id=None)

Return recorded updates after index, optionally scoped to a session.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
165
166
167
168
169
170
171
172
173
174
175
def records_since(
    self,
    index: int,
    *,
    session_id: str | None = None,
) -> list[AcpUpdateRecord]:
    """Return recorded updates after ``index``, optionally scoped to a session."""
    records = self.updates[index:]
    if session_id is None:
        return list(records)
    return [record for record in records if record.session_id == session_id]

agent_message_text_since(index, *, session_id)

Concatenate ACP agent message chunks recorded after index.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
177
178
179
180
181
182
183
184
185
186
187
188
def agent_message_text_since(self, index: int, *, session_id: str) -> str:
    """Concatenate ACP agent message chunks recorded after ``index``."""
    text_parts: list[str] = []
    for record in self.records_since(index, session_id=session_id):
        update = record.update
        if not _is_agent_message_chunk(update):
            continue
        content = getattr(update, "content", None)
        text = getattr(content, "text", None)
        if isinstance(text, str):
            text_parts.append(text)
    return "".join(text_parts)

usage_update_since(index, *, session_id)

Collect the latest ACP usage update observed after index.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
190
191
192
193
194
195
196
197
198
def usage_update_since(self, index: int, *, session_id: str) -> RequestUsage:
    """Collect the latest ACP usage update observed after ``index``."""
    usage = RequestUsage()
    for record in self.records_since(index, session_id=session_id):
        update = record.update
        if not isinstance(update, UsageUpdate):
            continue
        usage = _usage_from_acp(getattr(update, "usage", None))
    return usage

session_update(session_id, update, **kwargs) async

Record an ACP update and optionally forward it to a real host client.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
async def session_update(
    self,
    session_id: str,
    update: Any,
    **kwargs: Any,
) -> None:
    """Record an ACP update and optionally forward it to a real host client."""
    self.updates.append(
        AcpUpdateRecord(
            session_id=session_id,
            update=update,
            source=kwargs.get("source"),
        ),
    )
    if self.delegate is not None and hasattr(self.delegate, "session_update"):
        await self._call_delegate(
            "session_update",
            session_id=session_id,
            update=update,
            **kwargs,
        )

on_connect(conn)

Forward reverse connections to a delegate host client when it supports them.

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/client.py
370
371
372
373
def on_connect(self, conn: AcpAgent) -> None:
    """Forward reverse connections to a delegate host client when it supports them."""
    if self.delegate is not None and hasattr(self.delegate, "on_connect"):
        self.delegate.on_connect(conn)

Agent Source Classes And Protocols

AgentFactory

Bases: Protocol[AgentFactoryDepsT, AgentFactoryOutputDataT]

AgentSource

Bases: Protocol[AgentDepsT, OutputDataT]

StaticAgentSource(agent, deps=None) dataclass

Bases: Generic[AgentDepsT, OutputDataT]

FactoryAgentSource(factory) dataclass

Bases: Generic[AgentDepsT, OutputDataT]

Session Store Classes

SessionStore

Bases: Protocol

MemorySessionStore(_sessions=dict()) dataclass

FileSessionStore(root) dataclass

Provider State Classes And Protocols

ModelSelectionState(*, available_models, current_model_id, allow_any_model_id=False, enable_config_option=True, config_option_name='Model', config_option_description='Session-local model override.') dataclass

ModeState(*, modes, current_mode_id=None) dataclass

SessionModelsProvider

Bases: Protocol

SessionModesProvider

Bases: Protocol

ConfigOptionsProvider

Bases: Protocol

PlanProvider

Bases: Protocol

NativePlanPersistenceProvider

Bases: Protocol

ApprovalStateProvider

Bases: Protocol

ApprovalPolicy = Literal['allow', 'reject'] module-attribute

ApprovalPolicyStore

Bases: Protocol

SessionMetadataApprovalPolicyStore(metadata_key='approval_policies') dataclass

PermissionOptionSet(*, allow_once_name='Allow', reject_once_name='Deny', allow_always_name='Always Allow', reject_always_name='Always Deny') dataclass

Bridge Classes

CapabilityBridge

BufferedCapabilityBridge() dataclass

PrepareToolsBridge(*, metadata_key='prepare_tools', default_mode_id, modes, mode_config_key='mode', plan_generation_config_id='plan_generation_type', plan_generation_config_name='Plan Generation', plan_generation_config_description='How plan mode records ACP plan state.', default_plan_generation_type='structured') dataclass

Bases: BufferedCapabilityBridge, Generic[AgentDepsT]

PrepareToolsMode(*, id, name, prepare_func, description=None, plan_mode=False, plan_tools=False) dataclass

Bases: Generic[AgentDepsT]

PrepareOutputToolsBridge(*, metadata_key='prepare_output_tools', default_mode_id, modes, mode_config_key='prepare_output_tools_mode') dataclass

Bases: BufferedCapabilityBridge, Generic[AgentDepsT]

PrepareOutputToolsMode(*, id, name, prepare_func, description=None) dataclass

Bases: Generic[AgentDepsT]

ThinkingBridge(*, config_id='thinking', config_name='Thinking Effort', config_description='Session-local thinking/reasoning effort.') dataclass

HookBridge(metadata_key='hooks', hide_all=False, record_event_stream=True, record_model_requests=True, record_node_lifecycle=True, record_deferred_tool_calls=True, record_output_processing=True, record_output_validation=True, record_prepare_output_tools=True, record_prepare_tools=True, record_run_lifecycle=True, record_tool_execution=True, record_tool_validation=True) dataclass

ExternalHookEventBridge(*, metadata_key='external_hooks', projection_map=HookProjectionMap(), emission_mode='paired') dataclass

EventEmissionMode = Literal['paired', 'start_only'] module-attribute

HistoryProcessorBridge(metadata_key='history_processors', processor_names=list()) dataclass

ThreadExecutorBridge(*, executor, metadata_key='thread_executor') dataclass

ImageGenerationBridge(*, builtin=True, local=None, fallback_model=None, background=None, input_fidelity=None, moderation=None, output_compression=None, output_format=None, quality=None, size=None, aspect_ratio=None, tool_names=_DEFAULT_IMAGE_GENERATION_TOOL_NAMES, metadata_key='image_generation') dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

SetToolMetadataBridge(*, tools='all', metadata_key=None, **metadata) dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/bridges/capability_support.py
179
180
181
182
183
184
185
186
187
188
def __init__(
    self,
    *,
    tools: ToolSelector[AgentDepsT] = "all",
    metadata_key: str | None = None,
    **metadata: JsonValue,
) -> None:
    self.tools = tools
    self.metadata_key = metadata_key
    self.metadata = dict(metadata)

IncludeToolReturnSchemasBridge(tools='all', metadata_key=None) dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

ToolsetBridge(*, toolset, metadata_key='toolset') dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

PrefixToolsBridge(*, wrapped, prefix, metadata_key='prefix_tools') dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

WebSearchBridge(*, builtin=True, local=None, search_context_size=None, user_location=None, blocked_domains=None, allowed_domains=None, max_uses=None, tool_names=_DEFAULT_WEB_SEARCH_TOOL_NAMES, metadata_key='web_search') dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

WebFetchBridge(*, builtin=True, local=None, allowed_domains=None, blocked_domains=None, max_uses=None, enable_citations=None, max_content_tokens=None, tool_names=_DEFAULT_WEB_FETCH_TOOL_NAMES, metadata_key='web_fetch') dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

McpCapabilityBridge(*, url, builtin=True, local=None, id=None, authorization_token=None, headers=None, allowed_tools=None, description=None, tool_name_prefixes=_DEFAULT_MCP_TOOL_NAME_PREFIXES, metadata_key='mcp_capability') dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

SessionMcpBridge(*, metadata_key='session_mcp', include_instructions=True, include_return_schema=None, cache_tools=True, cache_resources=True, cache_prompts=True, tool_error_behavior='retry', max_retries=None, allowed_tools=None, tool_name_prefixes=frozenset(), toolset_id_prefix='acp-session-mcp', advertise_http=True, advertise_sse=True) dataclass

Bases: CapabilityBridge

Attach ACP client-provided MCP servers to the Pydantic AI agent run.

ACP clients may pass MCP server definitions during session/new, session/load, session/fork, or session/resume. The adapter persists those definitions on AcpSessionContext.mcp_servers; this bridge turns them into a Pydantic AI MCPToolset capability for the active session.

OpenAICompactionBridge(*, message_count_threshold=None, trigger=None, instructions=None, metadata_key='openai_compaction') dataclass

Bases: BufferedCapabilityBridge, Generic[AgentDepsT]

AnthropicCompactionBridge(*, token_threshold=150000, instructions=None, pause_after_compaction=False, metadata_key='anthropic_compaction') dataclass

Bases: CapabilityBridge, Generic[AgentDepsT]

McpBridge(*, metadata_key='mcp', approval_policy_scope='tool', config_options=list(), servers=list(), tools=list()) dataclass

McpServerDefinition(*, server_id, name, transport, url=None, description=None, tool_prefix=None) dataclass

McpToolDefinition(*, tool_name, server_id, kind='execute') dataclass

Hook Introspection Helpers

RegisteredHookInfo(*, event_id, hook_name, tool_filters) dataclass

list_agent_hooks(agent)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/runtime/hook_introspection.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def list_agent_hooks(agent: PydanticAgent[Any, Any]) -> list[RegisteredHookInfo]:
    resolved_root_capability = _root_capability(agent)
    if resolved_root_capability is None:
        return []
    hook_infos: list[RegisteredHookInfo] = []
    for hooks in _iter_hooks(resolved_root_capability):
        registry = hook_registry(hooks)
        if registry is None:
            continue
        for registry_key, entries in registry.items():
            event_id = _INTERNAL_EVENT_NAMES.get(registry_key, registry_key)
            for entry in entries:
                func = entry_func(entry)
                if not callable(func):
                    continue
                if getattr(func, "__module__", "") == _SKIPPED_HOOK_MODULE:
                    continue
                hook_infos.append(
                    RegisteredHookInfo(
                        event_id=event_id,
                        hook_name=getattr(func, "__name__", "") or event_id,
                        tool_filters=_tool_filters(entry),
                    ),
                )
    return sorted(
        hook_infos,
        key=lambda hook_info: (
            hook_info.event_id,
            hook_info.hook_name,
            hook_info.tool_filters,
        ),
    )

Projection Classes

FileSystemProjectionMap(*, write_tool_names=frozenset(), read_tool_names=frozenset(), bash_tool_names=frozenset(), search_tool_names=frozenset(), default_write_tool=None, default_read_tool=None, default_bash_tool=None, default_search_tool=None, path_arg=None, content_arg=None, old_text_arg=None, command_arg=None, terminal_id_arg=None, search_path_arg=None, search_pattern_arg=None, render_search_results_as_tree=False, hide_dot_directories_in_tree=True, tree_root_label=None) dataclass

WebToolProjectionMap(*, search_tool_names=_DEFAULT_SEARCH_TOOL_NAMES, fetch_tool_names=_DEFAULT_FETCH_TOOL_NAMES) dataclass

BuiltinToolProjectionMap(*, web_projection_map=WebToolProjectionMap(), image_generation_tool_names=_DEFAULT_IMAGE_GENERATION_TOOL_NAMES, mcp_tool_name_prefixes=_DEFAULT_MCP_TOOL_NAME_PREFIXES) dataclass

CompositeProjectionMap(*, maps) dataclass

ProjectionAwareToolClassifier(*, base_classifier, projection_maps) dataclass

Approval Presentation

PermissionRequestContext(*, session, tool_call, raw_input, cwd, classifier, projection_map=None) dataclass

PermissionToolCallBuilder

Bases: Protocol

DefaultPermissionToolCallBuilder(*, status='pending') dataclass

NativeApprovalBridge(*, enable_persistent_choices=False, tool_call_builder=DefaultPermissionToolCallBuilder(), policy_store=SessionMetadataApprovalPolicyStore(), option_set=PermissionOptionSet()) dataclass

ProjectionAwareApprovalBridge

Bases: Protocol

supports_projection_aware_approval_bridge(value)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def supports_projection_aware_approval_bridge(
    value: object,
) -> TypeIs[ProjectionAwareApprovalBridge]:
    resolver = getattr(value, "resolve_deferred_approvals", None)
    if not callable(resolver):
        return False
    try:
        resolver_signature = signature(resolver)
    except (TypeError, ValueError):
        return False
    parameters = resolver_signature.parameters
    if "projection_map" in parameters:
        return True
    return any(parameter.kind is Parameter.VAR_KEYWORD for parameter in parameters.values())

Slash Commands

SlashCommandRequest(*, name, argument, raw_prompt, session, agent) dataclass

SlashCommandResult(*, text=None, updates=(), stop_reason='end_turn', handled=True, refresh_session_surface=True) dataclass

SlashCommandProvider

Bases: Protocol

StaticSlashCommand(*, command, handler) dataclass

StaticSlashCommandProvider(*, commands) dataclass

SlashCommandHandler = Callable[[SlashCommandRequest], SlashCommandResult | None | Awaitable[SlashCommandResult | None]] module-attribute

Projection Helpers

truncate_text(text, *, limit, marker=DEFAULT_TEXT_TRUNCATION_MARKER)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/_projection_text.py
20
21
22
23
24
25
26
27
28
29
30
31
32
def truncate_text(
    text: str,
    *,
    limit: int,
    marker: str = DEFAULT_TEXT_TRUNCATION_MARKER,
) -> str:
    if limit <= 0:
        return ""
    if len(text) <= limit:
        return text
    if limit <= len(marker):
        return f"{text[:limit]}{marker}"
    return f"{text[: limit - len(marker)]}{marker}"

truncate_lines(lines, *, max_lines, truncation_line='... [truncated]')

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/_projection_text.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def truncate_lines(
    lines: Sequence[str],
    *,
    max_lines: int,
    truncation_line: str = "... [truncated]",
) -> list[str]:
    if max_lines <= 0:
        return []
    materialized = list(lines)
    if len(materialized) <= max_lines:
        return materialized
    if max_lines == 1:
        return [truncation_line]
    return [*materialized[: max_lines - 1], truncation_line]

single_line_summary(text, *, limit)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/_projection_text.py
51
52
53
54
55
56
57
58
59
def single_line_summary(
    text: str,
    *,
    limit: int,
) -> str:
    normalized = " ".join(text.split())
    if len(normalized) <= limit:
        return normalized
    return f"{normalized[:limit].rstrip()}..."

format_code_block(text, *, language=None, limit=None)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/_projection_text.py
62
63
64
65
66
67
68
69
70
71
def format_code_block(
    text: str,
    *,
    language: str | None = None,
    limit: int | None = None,
) -> str:
    body = truncate_text(text, limit=limit) if limit is not None else text
    if language is None:
        return f"```\n{body}\n```"
    return f"```{language}\n{body}\n```"

format_diff_preview(path, old_text, new_text, *, context_lines=3, max_lines=40, include_path_header=True, include_diff_headers=False)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/_projection_text.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def format_diff_preview(
    path: str | Path,
    old_text: str,
    new_text: str,
    *,
    context_lines: int = 3,
    max_lines: int = 40,
    include_path_header: bool = True,
    include_diff_headers: bool = False,
) -> str:
    diff_lines = list(
        unified_diff(
            old_text.strip().splitlines(),
            new_text.strip().splitlines(),
            lineterm="",
            n=context_lines,
        ),
    )
    if not include_diff_headers:
        diff_lines = [
            line
            for line in diff_lines
            if not line.startswith("--- ") and not line.startswith("+++ ")
        ]
    if not diff_lines:
        diff_lines = ["(no visible changes)"]
    body_lines: list[str] = []
    if include_path_header:
        body_lines.append(f"# {path}")
    body_lines.extend(truncate_lines(diff_lines, max_lines=max_lines))
    return "\n".join(body_lines)

format_terminal_status(*, exit_code, signal)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/_projection_text.py
107
108
109
110
111
112
113
114
115
116
117
118
def format_terminal_status(
    *,
    exit_code: int | None,
    signal: str | None,
) -> str:
    if signal is not None:
        return f"cancelled ({signal})"
    if exit_code is None:
        return "running"
    if exit_code == 0:
        return "ok (0)"
    return f"fail ({exit_code})"

caution_for_path(path, *, session_cwd, workspace_root=None, access_policy=None)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/_projection_risk.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def caution_for_path(
    path: str | Path,
    *,
    session_cwd: Path,
    workspace_root: Path | None = None,
    access_policy: HostAccessPolicy | None = None,
) -> str | None:
    policy = access_policy or HostAccessPolicy()
    evaluation = policy.evaluate_path(
        path,
        session_cwd=session_cwd,
        workspace_root=workspace_root,
    )
    if not evaluation.has_risks:
        return None
    return evaluation.message

caution_for_command(command, *, args=None, cwd=None, session_cwd, workspace_root=None, access_policy=None)

Source code in packages/adapters/pydantic-acp/src/pydantic_acp/_projection_risk.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def caution_for_command(
    command: str,
    *,
    args: Sequence[str] | None = None,
    cwd: str | Path | None = None,
    session_cwd: Path,
    workspace_root: Path | None = None,
    access_policy: HostAccessPolicy | None = None,
) -> str | None:
    policy = access_policy or HostAccessPolicy()
    evaluation = policy.evaluate_command(
        command,
        args=args,
        cwd=cwd,
        session_cwd=session_cwd,
        workspace_root=workspace_root,
    )
    if not evaluation.has_risks:
        return None
    return evaluation.message

Host Backend Classes

ClientHostContext(*, client, session, filesystem, terminal, access_policy=None, workspace_root=None) dataclass

ClientFilesystemBackend(*, client, session, access_policy=None, workspace_root=None) dataclass

ClientTerminalBackend(*, client, session, access_policy=None, workspace_root=None) dataclass

Testing Helpers

BlackBoxHarness(*, adapter, client=RecordingACPClient(), last_session_id=None) dataclass

RecordingACPClient(*, updates=list(), permission_option_ids=list(), permission_option_names=list(), permission_responses=list(), read_calls=list(), write_calls=list(), create_calls=list(), output_calls=list(), release_calls=list(), wait_calls=list(), kill_calls=list(), write_response=WriteTextFileResponse(), release_response=ReleaseTerminalResponse(), kill_response=KillTerminalResponse(), wait_response=(lambda: WaitForTerminalExitResponse(exit_code=0))(), terminal_output_response=(lambda: TerminalOutputResponse(output='terminal-output', truncated=False))()) dataclass