# Plans, Thinking, And Approvals These are the three ACP-specific workflows most teams care about once the adapter is already running: - plan state - reasoning effort - approval-gated tools ## Native ACP Plan State When `plan_provider` is not configured and the current mode supports native planning, the adapter manages ACP plan state directly. ### Plan access tools This is available when the session supports native plan state: - `acp_get_plan` ### Plan write tool This is available when the current plan mode uses tool-based plan generation: - `acp_set_plan` ### Plan progress tools These are available when the current mode also supports plan progress: - `acp_update_plan_entry` - `acp_mark_plan_done` `acp_get_plan` returns numbered entries, and those numbers are intentionally **1-based**. ## Projection-aware Approval Prompts `NativeApprovalBridge` can render permission requests through a `PermissionToolCallBuilder`. The default builder uses configured projection maps for permission cards, so a projected file-write tool can show the same structured diff before approval that the transcript later shows after execution. Customize permission rendering by passing a builder directly to `NativeApprovalBridge`: ```python from pydantic_acp import NativeApprovalBridge approval_bridge = NativeApprovalBridge(tool_call_builder=my_builder) ``` The builder is intentionally owned by `NativeApprovalBridge`; it is not an `AdapterConfig` field. Custom approval bridges can keep their own permission presentation strategy. ## Persistent Approval Policies `NativeApprovalBridge(enable_persistent_choices=True)` adds always-allow and always-deny options. By default, remembered policies are stored in session metadata under `approval_policies`. Use `ApprovalPolicyStore` to keep remembered approval policy in host storage instead: ```python from pydantic_acp import NativeApprovalBridge approval_bridge = NativeApprovalBridge( enable_persistent_choices=True, policy_store=my_policy_store, ) ``` Use `PermissionOptionSet` to change display names without changing option ids or ACP permission kinds. ## How Native Plan State Is Enabled Mark one `PrepareToolsMode` as `plan_mode=True`: ```python from pydantic_acp import PrepareToolsBridge, PrepareToolsMode PrepareToolsBridge( default_mode_id="ask", modes=[ PrepareToolsMode( id="plan", name="Plan", description="Draft ACP plan state.", prepare_func=plan_tools, plan_mode=True, ), PrepareToolsMode( id="agent", name="Agent", description="Execute the plan with the full tool surface.", prepare_func=agent_tools, plan_tools=True, ), ], ) ``` This pattern gives you: - `plan` mode for drafting or revising the plan - `agent` mode for working through plan entries without losing access to plan progress tools ## Choosing How Plan Mode Records The Plan `PrepareToolsBridge` exposes a session-local `plan_generation_type` select option whenever one mode is marked with `plan_mode=True`. The supported values are: - `structured` - `tools` `structured` is the default. - `structured` - plan mode expects a `TaskPlan` structured result - `acp_set_plan` stays hidden - `tools` - plan mode keeps the agent's normal output type - `acp_set_plan` is exposed so the model can write plan state explicitly ## TaskPlan When `plan_generation_type="structured"` and native plan state is active in plan mode, the adapter supports `TaskPlan` structured output. That lets a model return a structured plan directly instead of mutating ACP plan state through a write tool. The adapter then: 1. stores the plan on the session 2. emits an ACP plan update 3. keeps the plan state available for subsequent prompts ## Persisting Native Plans If you want native plan updates mirrored to your own storage, use `native_plan_persistence_provider`. That provider is called whenever the native ACP plan state changes. A common use case is writing the current session plan into a workspace file while keeping ACP session state as the source of truth. The adapter wiring is direct: ```python from pathlib import Path from acp.schema import PlanEntry from pydantic_ai import Agent from pydantic_acp import AdapterConfig, FileSessionStore, run_acp class WorkspaceNativePlanPersistenceProvider: def persist_plan_state( self, session, agent, entries: list[PlanEntry], plan_markdown: str | None, ) -> None: del agent output_path = session.cwd / ".acpkit" / "plan.md" output_path.parent.mkdir(parents=True, exist_ok=True) numbered_entries = "\n".join( f"{index}. [{entry.status}] {entry.content}" for index, entry in enumerate(entries, start=1) ) output_path.write_text( "\n\n".join(part for part in (plan_markdown, numbered_entries) if part), encoding="utf-8", ) agent = Agent("openai:gpt-5", name="plan-agent") run_acp( agent=agent, config=AdapterConfig( session_store=FileSessionStore(root=Path(".acp-sessions")), native_plan_persistence_provider=WorkspaceNativePlanPersistenceProvider(), ), ) ``` ## How Plan State Renders In ACP Native plan state has two ACP-visible surfaces: - `AgentPlanUpdate` - the adapter emits this whenever native plan state changes - ACP clients can render the current plan directly from session updates - `acp_get_plan` - the model can read back the current numbered plan state as text - this is the stable read surface for tool-driven plan workflows That means you do not need a separate rendering bridge just to make native plan state visible. If ACP owns the plan, the adapter already emits the update stream and read tool surface. ## Adding Plan-Specific Guidance If your integration wants to keep ACP Kit's built-in native plan tool guidance but add product-specific planning instructions, use `AdapterConfig(native_plan_additional_instructions=...)`. This appends extra guidance to the native plan summary returned by `acp_get_plan`. It does not replace the adapter's built-in guidance about 1-based plan entry numbers or the native ACP plan tool contract. Use this for guidance such as: - keep plans short - avoid status churn for trivial same-turn tasks - use `in_progress` only for multi-turn work Use your own agent `instructions=` or session-aware factory when the guidance needs to be fully dynamic or not tied specifically to native plan state. ## Thinking Effort `ThinkingBridge` makes Pydantic AI’s `Thinking` capability visible to ACP clients as session-local state. The bridge contributes: - a select config option - session metadata describing the current and supported values - per-run `ModelSettings` derived from `Thinking(...)` That means ACP clients can offer a reasoning-effort UI without the adapter hard-coding provider-specific settings. ## Approval Flow Approval support comes from `ApprovalBridge`, with `NativeApprovalBridge` as the default practical implementation. Approval-gated tools typically look like this: ```python from pydantic_ai import Agent from pydantic_ai.exceptions import ApprovalRequired from pydantic_ai.tools import RunContext agent = Agent("openai:gpt-5", name="approval-agent") @agent.tool def delete_file(ctx: RunContext[None], path: str) -> str: if not ctx.tool_call_approved: raise ApprovalRequired() return f"Deleted: {path}" ``` With: ```python from pydantic_acp import AdapterConfig, NativeApprovalBridge config = AdapterConfig( approval_bridge=NativeApprovalBridge(enable_persistent_choices=True), ) ``` The bridge handles: - deferred approval loops - remembered approval policies - ACP permission option rendering - stable tool-call updates before and after approval ## Cancellation And Approval Cancellation also matters here. If a user stops a run while the adapter is waiting on approval: - the approval request is marked as cancelled - the session history stays valid - the transcript receives a cancellation message instead of being left half-open That behavior is important for editor-style “stop” controls. ## Choosing Between Native Plans And PlanProvider Use native plan state when: - the ACP session should own the active plan - the model should manipulate the plan through ACP-native semantics - you want plan mode and agent mode to collaborate on the same session plan Use `PlanProvider` when: - the host already owns the plan - you are reflecting product-level planning state into ACP - ACP should observe the plan, not be the source of truth