Skip to content

Typed Elicitation

AcpSessionContext.ask_choice() asks the connected ACP client to select one typed application value. It is an ergonomic layer over AcpSessionContext.create_elicitation(); the low-level API remains available for custom ACP form and URL elicitation.

The helper does not introduce another schema system. It compiles choices to ACP's existing ElicitationFormSessionMode, ElicitationSchema, and ElicitationStringPropertySchema types.

Single-Choice Questions

Each ElicitationChoice carries an application value, a user-visible label, an optional description, and whether it is the default:

from pydantic_acp import (
    AcpSessionContext,
    ChoiceElicitationAccepted,
    ChoiceElicitationCancelled,
    ChoiceElicitationDeclined,
    ElicitationChoice,
)


async def choose_target(session: AcpSessionContext) -> str:
    result = await session.ask_choice(
        "Choose a deployment target",
        [
            ElicitationChoice(value="preview", label="Preview"),
            ElicitationChoice(
                value="production",
                label="Production",
                description="Deploy to the production environment.",
                default=True,
            ),
        ],
    )

    if isinstance(result, ChoiceElicitationAccepted):
        return f"Selected: {result.value}"
    if isinstance(result, ChoiceElicitationDeclined):
        return "The user declined to choose."
    if isinstance(result, ChoiceElicitationCancelled):
        return "The choice was cancelled."
    raise AssertionError("unreachable")

The value is not sent as the ACP enum constant. ACP Kit sends opaque string tokens and maps the accepted token back to the original typed value. Values can therefore be enums, dataclasses, identifiers, or other application types; they do not need to be JSON serializable.

Exactly zero or one choice may have default=True. A default is transmitted through the ACP property schema. It does not turn a malformed accepted response into a valid answer: an accepted response must still contain one of the offered tokens.

Result Contract

The result is a discriminated union. Inspect result.status or narrow with the public result classes:

Status Type Meaning
accepted ChoiceElicitationAccepted[T] The user selected a choice; .value contains the typed value.
declined ChoiceElicitationDeclined The user explicitly declined to answer.
cancelled ChoiceElicitationCancelled The interaction was cancelled without an answer.

None is never used to collapse these outcomes. If a client returns an accepted response without a known choice token, ACP Kit raises InvalidElicitationResponseError.

Capability Negotiation And Fallbacks

ask_choice() always checks session.supports_elicitation(mode) before sending the request. It never substitutes ACP permission requests for elicitation. Without form support it raises ElicitationUnsupportedError:

from pydantic_acp import ElicitationChoice, ElicitationUnsupportedError

try:
    result = await session.ask_choice(
        "Continue?",
        [ElicitationChoice(value=True, label="Continue")],
    )
except ElicitationUnsupportedError:
    result = None

When the application has a deliberate non-ACP fallback, pass a synchronous or asynchronous callable that returns the same result union. The fallback runs only when the client lacks form elicitation, and its owner must state whether the outcome is accepted, declined, or cancelled:

from pydantic_acp import ChoiceElicitationDeclined


async def unsupported_client_fallback() -> ChoiceElicitationDeclined:
    return ChoiceElicitationDeclined()


result = await session.ask_choice(
    "Choose a deployment target",
    [
        ElicitationChoice(value="preview", label="Preview", default=True),
        ElicitationChoice(value="production", label="Production"),
    ],
    fallback=unsupported_client_fallback,
)

Return ChoiceElicitationAccepted(value=...) from a fallback only when the fallback itself has an acceptance source whose semantics your application is willing to treat as accepted. ACP Kit does not manufacture acceptance from a decline, cancellation, or exception. Its value must match an offered choice; ACP Kit returns the canonical typed value from that choice and raises InvalidElicitationFallbackError for an impossible selection.

Returning a plain choice value remains temporarily supported for 1.6.0 compatibility, but emits DeprecationWarning. It is subject to the same offered-choice validation. Prefer an explicit result variant: plain-value fallback cannot communicate whether the value came from a user, policy, or configuration. None is treated as a real choice value when it was offered; it is never a decline/cancel sentinel.

Presentation Contract

ACP Kit transmits the question, option labels, schema default, and typed-choice semantics. The client owns visual layout and interaction design. Option descriptions are sent as namespaced option metadata because the published ACP Python SDK 0.11 does not expose its standard per-option description field; clients may ignore that metadata.

The helper intentionally provides no guarantee about radio buttons, menus, dialogs, ordering beyond the schema order, or whether descriptions are visible.

Remote Transport

ACP Python SDK 0.11 keeps the receiving elicitation/create route behind its unstable-protocol opt-in. Enable the flag on the ACP client connection that receives the request from the upstream agent:

from acp import run_agent
from acpremote import TransportOptions, connect_acp

proxy_agent = connect_acp(
    websocket_url,
    options=TransportOptions(use_unstable_protocol=True),
)
await run_agent(proxy_agent)

The upstream/sending agent can use plain run_agent(acp_agent): an agent-side unstable flag is not required merely to call create_elicitation(). A final ACP client connected to the local proxy must likewise support form elicitation and register its receiving unstable route. Route registration and capability negotiation are independent requirements.

The public mirror CLIs expose the same opt-in:

acpkit run --addr ws://remote.example.com:8080/acp/ws --unstable-protocol
acpremote mirror ws://remote.example.com:8080/acp/ws --unstable-protocol

For serve_command(), the relay transports JSON-RPC frames unchanged. The receiving ACP client must register the unstable route; the spawned command does not need an agent-side flag merely to send elicitation.

Low-Level Elicitation

Use create_elicitation() directly for multi-field forms, URL elicitation, or schema shapes other than one typed choice. Check supports_elicitation(mode) first and construct the ACP SDK mode explicitly. ask_choice() is deliberately single-select; it does not infer multi-select behavior from collection values.