Skip to content

vcoderun/acpkit

ACP Kit

ACP Kit is a Python SDK and CLI for turning an existing agent surface into a truthful ACP server.

  • acpkit is the root CLI and target resolver
  • pydantic-acp adapts pydantic_ai.Agent instances to ACP
  • codex-auth-helper turns a local Codex login into a pydantic-ai Responses model

ACP Kit is not a new agent framework. The core use case is:

  1. keep your current agent surface
  2. expose it through ACP without rewriting the agent
  3. only publish models, modes, plans, approvals, MCP metadata, and host tools that the runtime can actually honor

The core workflow is simple:

  1. build a normal pydantic_ai.Agent
  2. expose it through ACP with run_acp(...) or create_acp_agent(...)
  3. optionally add session stores, approvals, providers, projection maps, and bridges

Installation

# production
uv pip install "acpkit[pydantic]"

# production with acpkit launch support
uv pip install "acpkit[pydantic,launch]"

# development
uv pip install -e ".[dev,docs,pydantic]"

ACP Kit Skill

This repo also ships an acpkit-sdk skill package for Codex.

Use it when you want Codex to help integrate ACP into an existing agent surface, especially for:

  • exposing an existing pydantic_ai.Agent through ACP
  • choosing between run_acp(...), create_acp_agent(...), providers, bridges, and AgentSource
  • wiring plans, approvals, session stores, thinking, MCP metadata, and host-backed tools
  • keeping docs and examples aligned with the real SDK surface

From a checkout of this repo, install the skill with Unix commands:

mkdir -p "$HOME/.codex/skills/acpkit-sdk" \
  && cp -R .agents/skills/acpkit-sdk/. "$HOME/.codex/skills/acpkit-sdk/"

The canonical skill package lives here:

Example prompts:

  • Use $acpkit-sdk to expose my existing pydantic_ai.Agent through ACP.
  • Use $acpkit-sdk to add ACP plans, approvals, and slash-command mode switching to this agent.

CLI

Run a supported agent target through ACP:

acpkit run strong_agent
acpkit run strong_agent:agent
acpkit run strong_agent:agent -p ./examples

acpkit resolves module or module:attribute targets, auto-detects pydantic_ai.Agent instances, and dispatches them to the installed adapter package. If only the module is given, it selects the last defined pydantic_ai.Agent instance in that module.

If the matching adapter extra is not installed, acpkit fails with an install hint such as uv pip install "acpkit[pydantic]".

Launch a target through Toad ACP:

acpkit launch strong_agent
acpkit launch strong_agent:agent -p ./examples

acpkit launch TARGET mirrors the resolved target to:

toad acp "acpkit run TARGET [-p PATH]..."

The command is dispatched through uvx --python 3.14 --from batrachian-toad, so Toad runs in a separate Python 3.14 tool environment and does not replace your project Python.

If the script already starts its own ACP server and should be launched directly, use --command:

acpkit launch -c "python3.11 strong_agent.py"

launch TARGET and launch --command ... are mutually exclusive. -p/--path only applies to TARGET mode.

run_acp

Use run_acp(...) when you want to start an ACP server directly from a Pydantic AI agent:

from pydantic_ai import Agent
from pydantic_acp import run_acp

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

@agent.tool_plain
def get_weather(city: str) -> str:
    return f"Weather in {city}: sunny"

run_acp(agent=agent)

create_acp_agent

Use create_acp_agent(...) when you want the ACP agent object without starting the server immediately:

import asyncio

from acp import run_agent
from pydantic_ai import Agent
from pydantic_acp import AdapterConfig, MemorySessionStore, create_acp_agent

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

acp_agent = create_acp_agent(
    agent=agent,
    config=AdapterConfig(
        agent_name="my-service",
        agent_title="My Service Agent",
        session_store=MemorySessionStore(),
    ),
)

asyncio.run(run_agent(acp_agent))

AdapterConfig

AdapterConfig is the main runtime configuration surface. Common fields include:

  • agent metadata: agent_name, agent_title, agent_version
  • persistence: session_store
  • model selection: allow_model_selection, available_models, models_provider
  • mode and config state: modes_provider, config_options_provider
  • plans: plan_provider, or native plan state via PrepareToolsMode(plan_mode=True)
  • approvals: approval_bridge, approval_state_provider
  • bridges: capability_bridges
  • projection and classification: projection_maps, tool_classifier, enable_generic_tool_projection

Configured adapter example:

from pathlib import Path

from pydantic_ai import Agent
from pydantic_acp import (
    AdapterConfig,
    AdapterModel,
    FileSessionStore,
    NativeApprovalBridge,
    run_acp,
)

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

config = AdapterConfig(
    agent_name="configured-agent",
    agent_title="Configured Agent",
    allow_model_selection=True,
    available_models=[
        AdapterModel(
            model_id="fast",
            name="Fast",
            description="Lower-latency responses",
            override="openai:gpt-5-mini",
        ),
        AdapterModel(
            model_id="smart",
            name="Smart",
            description="Higher-quality responses",
            override="openai:gpt-5",
        ),
    ],
    session_store=FileSessionStore(root=Path(".acp-sessions")),
    approval_bridge=NativeApprovalBridge(enable_persistent_choices=True),
)

run_acp(agent=agent, config=config)

Native Plan State

When plan_provider is not configured, the adapter can manage ACP plan state natively. Enable it by marking one PrepareToolsMode with plan_mode=True inside a PrepareToolsBridge:

from pydantic_ai import Agent
from pydantic_ai.tools import RunContext, ToolDefinition
from pydantic_acp import AdapterConfig, PrepareToolsBridge, PrepareToolsMode, run_acp


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


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


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

run_acp(
    agent=agent,
    config=AdapterConfig(
        capability_bridges=[
            PrepareToolsBridge(
                default_mode_id="agent",
                modes=[
                    PrepareToolsMode(
                        id="plan",
                        name="Plan",
                        description="Inspect and write plans.",
                        prepare_func=plan_tools,
                        plan_mode=True,
                    ),
                    PrepareToolsMode(
                        id="agent",
                        name="Agent",
                        description="Full tool surface.",
                        prepare_func=agent_tools,
                    ),
                ],
            ),
        ],
    ),
)

When the session is in plan mode, the adapter:

  • injects acp_get_plan and acp_set_plan as hidden tools on the agent
  • extends output_type with NativePlanGeneration so the agent can emit a structured plan in a single response

NativePlanGeneration fields:

  • plan_entries: list[PlanEntry] — structured plan entries
  • plan_md: str — optional markdown representation

Native plan state and plan_provider are mutually exclusive. See Providers for full details.

Agent Factories

Use a factory or custom AgentSource when agent construction depends on the current session:

from pydantic_ai import Agent
from pydantic_acp import AcpSessionContext, create_acp_agent

def build_agent(session: AcpSessionContext) -> Agent[None, str]:
    return Agent(
        "openai:gpt-5",
        name=f"agent-{session.cwd.name}",
        system_prompt=f"Work inside {session.cwd.name}.",
    )

acp_agent = create_acp_agent(agent_factory=build_agent)

StaticAgentSource accepts an optional deps field to pass typed runtime dependencies alongside a shared agent instance without a factory:

from pydantic_acp import AdapterConfig, create_acp_agent
from pydantic_acp.agent_source import StaticAgentSource
from pydantic_ai import Agent

from myapp.deps import AppDependencies

deps = AppDependencies(db=my_db, cache=my_cache)
agent = Agent("openai:gpt-5", name="deps-agent", deps_type=AppDependencies)

acp_agent = create_acp_agent(
    agent_source=StaticAgentSource(agent=agent, deps=deps),
)

Session Stores

MemorySessionStore is the default. Use FileSessionStore when sessions should survive process restarts:

from pathlib import Path

from pydantic_ai import Agent
from pydantic_acp import AdapterConfig, FileSessionStore, run_acp

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

run_acp(
    agent=agent,
    config=AdapterConfig(
        session_store=FileSessionStore(root=Path(".acp-sessions")),
    ),
)

Session lifecycle support includes create, load, list, fork, resume, close, transcript replay, and message-history replay.

Runtime Controls

The adapter exposes a small ACP control plane alongside normal prompts:

  • /model print the current session model id
  • /model provider:model switch the current session model
  • /tools list registered tools
  • /hooks list registered Hooks callbacks visible on the current agent
  • /mcp-servers list MCP servers extracted from the current agent toolsets and session metadata

Codex-backed model changes must be explicit:

/model codex:gpt-5.4

Approval Flow

Pydantic AI approval-gated tools are bridged to ACP permission requests:

from pydantic_ai import Agent
from pydantic_ai.exceptions import ApprovalRequired
from pydantic_ai.tools import RunContext
from pydantic_acp import AdapterConfig, NativeApprovalBridge, run_acp

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}"

run_acp(
    agent=agent,
    config=AdapterConfig(
        approval_bridge=NativeApprovalBridge(enable_persistent_choices=True),
    ),
)

Projection Maps

projection_maps lets known tool families render as richer ACP content instead of raw text.

FileSystemProjectionMap

FileSystemProjectionMap can project:

  • read tools into ACP diff previews
  • write tools into ACP file diffs
  • bash tools into command previews and terminal references
from pydantic_acp import FileSystemProjectionMap, run_acp

run_acp(
    agent=agent,
    projection_maps=(
        FileSystemProjectionMap(
            default_read_tool="read_file",
            default_write_tool="write_file",
            default_bash_tool="execute",
        ),
    ),
)

HookProjectionMap

HookProjectionMap controls how existing pydantic_ai.capabilities.Hooks callbacks render into ACP updates:

from pydantic_ai import Agent
from pydantic_ai.capabilities import Hooks
from pydantic_acp import HookProjectionMap, run_acp

hooks = Hooks[None]()

@hooks.on.before_model_request
async def log_request(ctx, request_context):
    del ctx
    return request_context

agent = Agent("openai:gpt-5", capabilities=[hooks])

run_acp(
    agent=agent,
    projection_maps=(
        HookProjectionMap(
            hidden_event_ids=frozenset({"after_model_request"}),
            event_labels={"before_model_request": "Preparing Request"},
        ),
    ),
)

Capability Bridges

Capability bridges extend ACP exposure without coupling the adapter core to one product runtime. Built-in bridges cover:

  • HookBridge
  • PrepareToolsBridge
  • HistoryProcessorBridge
  • McpBridge
  • AgentBridgeBuilder

See Bridges for the full bridge model.

Providers

Providers let the host own session state while the adapter exposes it through ACP:

  • SessionModelsProvider
  • SessionModesProvider
  • ConfigOptionsProvider
  • PlanProvider
  • ApprovalStateProvider

See Providers for full details.

Host Backends

ClientHostContext provides session-scoped access to ACP client-backed filesystem and terminal operations:

from acp.interfaces import Client as AcpClient
from pydantic_ai import Agent
from pydantic_acp import AcpSessionContext, ClientHostContext

def build_agent(client: AcpClient, session: AcpSessionContext) -> Agent[None, str]:
    host = ClientHostContext.from_session(client=client, session=session)
    agent = Agent("openai:gpt-5")

    @agent.tool
    async def read_user_file(ctx, path: str) -> str:
        del ctx
        result = await host.filesystem.read_text_file(path)
        return result.content

    return agent

See Host Backends And Projections for the filesystem and terminal API surface.

Codex Auth Helper

codex-auth-helper reads ~/.codex/auth.json, refreshes tokens when needed, builds a Codex-aware AsyncOpenAI client, and returns a ready-to-use OpenAIResponsesModel.

from pydantic_ai import Agent
from codex_auth_helper import create_codex_responses_model

agent = Agent(create_codex_responses_model("gpt-5.4"))

See Helpers for helper package details.

Examples

Runnable and focused examples live under examples/pydantic:

  • static_agent.py smallest direct run_acp(agent=...) setup
  • factory_agent.py session-aware factory plus session-local model selection
  • providers.py models, modes, config options, plan updates, and approval metadata
  • bridges.py bridge builder, prepare-tools, history processors, and MCP metadata
  • approvals.py native deferred approval flow
  • host_context.py ClientHostContext usage inside a factory-built agent
  • hook_projection.py existing Hooks capability introspection rendered through HookProjectionMap
  • strong_agent.py full-featured workspace agent example combining factories, providers, approvals, bridges, projection maps, ask/plan/agent modes, and host helpers

Development

Canonical local checks:

make tests
make check
make save-coverage

Preview the docs locally:

make serve

Documentation Map

License

Apache 2.0

About

ACP Kit provides a common adapter for Agent Frameworks.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Generated from fswair/python-template