There are two different ACP protocols - be careful not to confuse them:
| Protocol | Purpose | Transport |
|---|---|---|
| OpenCode ACP (Agent Client Protocol) | Editor-to-agent integration | JSON-RPC via stdio |
| ACP (Agent Communication Protocol) | Agent-to-agent interoperability | REST/HTTP |
For Charm Crush ↔ OpenCode communication, use the Agent Communication Protocol (ACP) developed by IBM/BeeAI under Linux Foundation.
┌─────────────────┐ ┌─────────────────┐
│ Charm Crush │ ◄───── HTTP ─────► │ OpenCode │
│ (Agent A) │ │ (Agent B) │
│ :8000 │ │ :8001 │
└─────────────────┘ └─────────────────┘
│ │
│ ┌─────────────┐ │
└──────────►│ Task Queue │◄───────────┘
│ (Redis) │
└─────────────┘
| Concept | Description |
|---|---|
| Agent | Service with registered capabilities |
| Run | Single agent execution with inputs/outputs |
| Message | Multimodal communication unit with parts |
| Session | Conversation continuity across interactions |
| Await | Pause/resume mechanism for interactive tasks |
GET /pingResponse: {} (200 OK)
GET /agents
GET /agents?limit=10&offset=0Response:
{
"agents": [
{
"name": "chat",
"description": "Conversational agent",
"input_content_types": ["text/plain", "image/*"],
"output_content_types": ["text/plain", "application/json"],
"metadata": { "tags": ["Chat"], "capabilities": [...] },
"status": { "avg_run_tokens": 1500, "success_rate": 98.5 }
}
]
}GET /agents/{agent_name}POST /runs
Content-Type: application/jsonRequest Body:
{
"agent_name": "chat",
"session_id": "123e4567-e89b-12d3-a456-426614174000",
"input": [
{
"role": "user",
"parts": [
{
"content_type": "text/plain",
"content": "Help me fix failing tests"
}
]
}
],
"mode": "sync"
}Response:
{
"agent_name": "chat",
"session_id": "123e4567-e89b-12d3-a456-426614174000",
"run_id": "456e7890-e89b-12d3-a456-426614174000",
"status": "completed",
"output": [
{
"role": "agent/chat",
"parts": [
{
"content_type": "text/plain",
"content": "I can help you with that..."
}
]
}
],
"created_at": "2024-01-15T10:30:00Z",
"finished_at": "2024-01-15T10:30:10Z"
}GET /runs/{run_id}POST /runs/{run_id}
Content-Type: application/json
{
"run_id": "456e7890-e89b-12d3-a456-426614174000",
"await_resume": { "response": "Yes, proceed" },
"mode": "sync"
}POST /runs/{run_id}/cancelGET /runs/{run_id}/eventsEvent Types:
run.created,run.in-progress,run.completedrun.awaiting,run.cancelled,run.failedmessage.created,message.part,message.completederror
interface Message {
role: "user" | "agent" | "agent/{agent_name}";
parts: MessagePart[];
created_at?: string;
completed_at?: string;
}
interface MessagePart {
name?: string;
content_type: "text/plain" | "text/markdown" | "application/json" | "image/*";
content: string;
content_encoding?: "plain" | "base64";
metadata?: {
kind: "citation" | "trajectory";
title?: string;
url?: string;
};
}created → in-progress → completed
↓ ↓ ↓
cancelled ← cancelling failed
↑ ↓
↑ awaiting → (resume) → in-progress
Create acp-server.mjs:
// acp-server.mjs - Charm Crush running as ACP server
import { Server } from './acp-sdk/server.js';
const server = new Server({
name: 'crush',
description: 'Charm Crush - CLI AI Assistant'
});
@server.agent()
async function crush_agent(input, context) {
const userMessage = input[0]?.parts?.[0]?.content;
// Process with Crush logic
const result = await processWithCrush(userMessage);
return {
thought: "Processing user request...",
result
};
}
server.run(port=8000);Configure OpenCode to connect to Crush:
// opencode.json
{
"agent": {
"crush-remote": {
"mode": "subagent",
"description": "Charm Crush remote agent",
"endpoint": "http://localhost:8000",
"tools": {
"write": true,
"edit": true,
"bash": true
}
}
}
}Or use the OpenCode Task tool to invoke Crush:
@crush-remote Fix the failing Jest tests in __tests__/# acp_client.py - Generic ACP client
import asyncio
from acp_sdk.client import Client
from acp_sdk.models import Message, MessagePart
async def run_crush_agent(prompt: str) -> str:
async with Client(base_url="http://localhost:8000") as client:
run = await client.run_sync(
agent="crush_agent",
input=[
Message(
role="user",
parts=[
MessagePart(
content=prompt,
content_type="text/plain"
)
]
)
]
)
return run.output[0].parts[0].content
# Usage
result = asyncio.run(run_crush_agent("Fix the failing tests"))
print(result)// In Crush, call OpenCode via HTTP
async function callOpenCode(task) {
const response = await fetch('http://localhost:8001/runs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_name: 'build', // OpenCode's Build agent
input: [{ role: 'user', parts: [{ content: task, content_type: 'text/plain' }] }],
mode: 'async'
})
});
return response.json();
}// In OpenCode prompt or task
{
"agent": "orchestrator",
"tasks": [
{ "agent": "crush-remote", "input": "Analyze the codebase structure" },
{ "agent": "build", "input": "Implement the feature" }
]
}# Python SDK (recommended)
pip install acp-sdk
# Or TypeScript SDK
npm install acp-sdk# Option A: If Crush supports native ACP
crush --protocol acp --port 8000
# Option B: Wrap Crush in ACP server
python acp-server.py &opencode acp --port 8001# Check Crush agent availability
curl http://localhost:8000/agents
# Check OpenCode agent availability
curl http://localhost:8001/agentsFor production multi-agent workflows:
# Using Redis as message broker
import redis
import json
r = redis.Redis(host='localhost', port=6379)
def submit_task(agent, task):
r.lpush('acp_tasks', json.dumps({
'agent': agent,
'task': task,
'timestamp': time.time()
}))
def get_result(task_id):
return r.get(f'result:{task_id}')| Type | Description |
|---|---|
text/plain |
Plain text |
text/markdown |
Markdown formatted |
application/json |
Structured data |
image/* |
Any image format |
*/_ |
Wildcard (any type) |
{
"code": "server_error | invalid_input | not_found",
"message": "Human-readable error description",
"data": {
"additional": "error context"
}
}- Install SDK:
pip install acp-sdk - Run servers:
crush --protocol acp --port 8000andopencode acp --port 8001 - Discover agents:
GET /agents - Execute tasks:
POST /runswithagent_nameandinput - Check status:
GET /runs/{run_id} - Stream events:
GET /runs/{run_id}/events
Both agents communicate via standard HTTP/REST with JSON payloads, enabling flexible inter-agent workflows.