Loading...
Loading...
Drive external CLI coding agents (Claude Code, Codex, OpenCode) as first-class AG2 Agents over the Agent Client Protocol (ACP): point an Agent at a ClaudeCodeConfig / CodexConfig / OpenCodeConfig preset from ag2.acp and ask()/run() it like any agent, with its thinking, tool calls, plans and permission prompts externalized onto AG2's event stream. Use when orchestrating, observing, or gating CLI coding agents from Python — permission_policy ask/auto/deny HITL gating, fs_root confinement, turn timeouts, in-process testing via fake_acp_config. To expose an AG2 agent to other systems instead, see ag2-a2a or ag2-mcp; for approval plumbing see ag2-hitl.
npx skill4agent add ag2ai/ag2-skills ag2-acpAgentACPConfigAgentfs_rootag2-a2aag2-mcppermission_policy="ask"ag2-hitlpip install "ag2[acp]"acpagent-client-protocolPATH| Agent | Adapter install | Auth |
|---|---|---|
| Claude Code | | |
| Codex | | |
| OpenCode | | |
ClaudeCodeConfig(command=["npx", "-y", "@agentclientprotocol/claude-agent-acp"])from ag2.acp import ...ACPConfigClaudeCodeConfigCodexConfigOpenCodeConfigimport asyncio
from ag2 import Agent
from ag2.acp import ClaudeCodeConfig
async def main():
config = ClaudeCodeConfig(cwd="/path/to/repo") # workspace root
agent = Agent("coder", config=config)
try:
reply = await agent.ask("Refactor the auth module and add tests")
print(reply.body)
finally:
await config.aclose() # tear down the CLI subprocess
asyncio.run(main())ask()run()ClaudeCodeConfig()claude-agent-acpANTHROPIC_MODELCodexConfig()codex-acpMODEL_PROVIDEROpenCodeConfig()opencode acpopencode.json"model": "provider/model"The presets'field is response metadata only — it is not sent to the agent. Pick the model through each adapter's own mechanism (env var / config file) as above.model
from ag2 import Agent
from ag2.acp import ClaudeCodeConfig
from ag2.acp.events import ACPPlan
from ag2.events import ModelMessageChunk, ModelReasoning
from ag2.events.tool_events import BuiltinToolCallEvent
agent = Agent("coder", config=ClaudeCodeConfig(cwd="/path/to/repo"))
def observe(event):
if isinstance(event, ModelReasoning):
print("thinking:", event.content)
elif isinstance(event, ModelMessageChunk):
print(event.content, end="")
elif isinstance(event, BuiltinToolCallEvent):
print(f"tool: {event.name}({event.arguments})") # arguments = JSON string of the tool input
elif isinstance(event, ACPPlan):
for step in event.entries:
print(f" [{step.status}] {step.content}")
async with agent.run("Add a healthcheck endpoint") as run:
run.stream.subscribe(observe)
reply = await run.result()| ACP update | AG2 event |
|---|---|
| agent message chunk | |
| thinking chunk | |
| tool call / tool result | |
| plan | |
| mode change | |
| available commands | |
ag2.acp.eventsag2.eventspermission_policy| Policy | Behavior |
|---|---|
| Route to the agent's |
| Approve automatically (headless orchestration) |
| Reject automatically |
agent = Agent(
"coder",
config=ClaudeCodeConfig(cwd="/repo", permission_policy="auto"), # fully autonomous
)Pitfall:with no input route available (no"ask", no interactive context) denies the request — a headless run with the default policy will quietly reject every sensitive action. For unattended runs sethitl_hookexplicitly.permission_policy="auto"
ACPConfig| Field | Default | Purpose |
|---|---|---|
| preset per agent | Executable + args launching the agent in ACP mode |
| | Workspace root for the session |
| | Extra env vars, merged over a trimmed base env ( |
| | Response metadata only — see "Choosing an adapter" |
| | |
| | Root for mediated |
| | Advertise the ACP terminal capability |
| | Extra workspace roots |
| | Subprocess spawn + handshake timeout (s) |
| | Per-prompt-turn timeout (s); on expiry the turn is cancelled and the reply body is whatever streamed so far |
| | Grace period (s) after a timed-out turn signals |
fs_rootconfig.copy(**overrides)await config.aclose()ag2.acp.testing.fake_acp_configACPConfigACPTurnsession/updateAgent.runimport asyncio
from acp import schema
from ag2 import Agent
from ag2.acp.testing import ACPTurn, fake_acp_config
def text(t):
return schema.TextContentBlock(type="text", text=t)
async def main():
config = fake_acp_config(
ACPTurn(updates=[
schema.AgentThoughtChunk(session_update="agent_thought_chunk", content=text("planning")),
schema.AgentMessageChunk(session_update="agent_message_chunk", content=text("done")),
]),
permission_policy="auto", # overrides forward to ACPConfig
)
agent = Agent("coder", config=config)
try:
reply = await agent.ask("hello")
assert reply.body == "done"
finally:
await config.aclose()
asyncio.run(main())ACPTurn(hang=True)turn_timeoutacppip install "ag2[acp]"from ag2.acp import ...acpenv=export ANTHROPIC_API_KEY=...env={"ANTHROPIC_API_KEY": ...}CLAUDE_CONFIG_DIRHOMEclaude-code-acp@zed-industries/claude-code-acpclaude-agent-acp@agentclientprotocol/claude-agent-acpClaudeCodeConfig"ask"permission_policy="auto"model=ANTHROPIC_MODELMODEL_PROVIDERopencode.jsonPATHstartup_timeoutnpx -ytools=[...]ag2/acp/config.pyACPConfigag2/acp/client.pybridge.pysession.pyag2/acp/mappers.pyag2/acp/permissions.pypermission_policyag2/acp/events.pyACPPlanACPModeChangeACPAvailableCommandsag2/acp/testing.pyfake_acp_configACPTurn