- Remove all REST endpoints, replace with WebSocket message-based API
- Add WebSocket gateway with Hub and Client for connection management
- Add WebSocket handlers for Task, Agent, and Orchestrator services
- Add task subscription support for real-time ACP notifications
- Update all documentation to reflect WebSocket-first architecture
- Replace docs/openapi.yaml with docs/asyncapi.yaml
- Update e2e tests to use WebSocket via websocat
WebSocket endpoint: ws://localhost:8080/ws
Message format: {id, type, action, payload, timestamp}
Available actions:
- Board: board.list, board.create, board.get, board.update, board.delete
- Column: column.list, column.create, column.get
- Task: task.list, task.create, task.get, task.update, task.delete,
task.move, task.state, task.subscribe, task.unsubscribe
- Agent: agent.list, agent.launch, agent.status, agent.logs, agent.stop,
agent.prompt, agent.cancel, agent.session, agent.types
- Orchestrator: orchestrator.status, orchestrator.queue, orchestrator.trigger,
orchestrator.start, orchestrator.stop, orchestrator.prompt,
orchestrator.complete
- Health: health.check
20 KiB
Kandev Agent Protocol
For Agent Developers: This document explains how to build agents that integrate with Kandev using the Agent Communication Protocol (ACP).
For System Architecture: See ARCHITECTURE.md For WebSocket API: See docs/asyncapi.yaml
Overview
Kandev agents run in Docker containers with agentctl - a sidecar process that manages the AI agent and provides HTTP-based control. The agent communicates via ACP (Agent Communication Protocol) - a JSON-RPC 2.0 protocol over stdin/stdout.
Backend agentctl (HTTP) Agent Process
│ │ │
│ POST /api/v1/start │ │
├────────────────────────────►│ spawn auggie --acp │
│ ├───────────────────────────►│
│ POST /api/v1/acp/call │ initialize │
├────────────────────────────►├───────────────────────────►│
│ │◄───────────────────────────┤
│◄────────────────────────────┤ │
│ WS /api/v1/acp/stream │ session/update │
│◄────────────────────────────┤◄───────────────────────────┤
│ │ │
Architecture
agentctl
agentctl is an HTTP server that runs inside each agent container as a sidecar process. It:
- Manages the agent subprocess (e.g.,
auggie --acp) - Provides HTTP API for control operations (start, stop, status)
- Relays ACP messages between HTTP clients and the agent's stdin/stdout
- Streams output via WebSocket for real-time monitoring
Communication Flow
- Backend creates container with agentctl as entrypoint
- agentctl starts HTTP server on port 9999
- Backend waits for agentctl to be ready (
GET /health) - Backend starts agent process (
POST /api/v1/start) - Backend sends ACP messages via HTTP (
POST /api/v1/acp/call) - Backend receives ACP updates via WebSocket (
GET /api/v1/acp/stream)
agentctl HTTP API
| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Health check |
/api/v1/status |
GET | Get agent status |
/api/v1/start |
POST | Start agent process |
/api/v1/stop |
POST | Stop agent process |
/api/v1/acp/send |
POST | Send ACP message (fire and forget) |
/api/v1/acp/call |
POST | Send ACP request, wait for response |
/api/v1/acp/stream |
GET | WebSocket for ACP message streaming |
/api/v1/output |
GET | Get buffered output |
/api/v1/output/stream |
GET | WebSocket for real-time output |
Environment Variables
| Variable | Default | Description |
|---|---|---|
AGENTCTL_PORT |
9999 | HTTP server port |
AGENTCTL_AGENT_COMMAND |
auggie --acp |
Command to run agent |
AGENTCTL_AUTO_START |
false | Auto-start agent on startup |
AGENTCTL_LOG_LEVEL |
info | Logging level |
Agent Communication Protocol (ACP)
Protocol Basics
- Transport: JSON-RPC 2.0 over stdin/stdout
- Format: Newline-delimited JSON
- Direction: Bidirectional (request/response + notifications)
Message Types
Backend → Agent (Requests):
initialize- Establish connection and capabilitiessession/new- Create new sessionsession/load- Resume existing sessionsession/prompt- Send task/prompt to executesession/cancel- Cancel current operation
Agent → Backend (Notifications):
session/update- Progress updates, content, tool calls, completion
Message Specifications
1. Initialize Connection
Backend → Agent:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "1.0",
"clientInfo": {
"name": "kandev",
"version": "0.1.0"
},
"capabilities": {
"streaming": true
}
}
}
Agent → Backend:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "1.0",
"serverInfo": {
"name": "augment-agent",
"version": "1.0.0"
},
"capabilities": {
"streaming": true,
"sessionResumption": true
}
}
}
2. Create New Session
Backend → Agent:
{
"jsonrpc": "2.0",
"id": 2,
"method": "session/new",
"params": {}
}
Agent → Backend:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"sessionId": "abc123def456"
}
}
3. Resume Existing Session
Backend → Agent:
{
"jsonrpc": "2.0",
"id": 3,
"method": "session/load",
"params": {
"sessionId": "abc123def456"
}
}
Agent → Backend:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"sessionId": "abc123def456",
"loaded": true
}
}
4. Send Prompt
Backend → Agent:
{
"jsonrpc": "2.0",
"id": 4,
"method": "session/prompt",
"params": {
"message": "Fix the null pointer bug in the login function"
}
}
Agent → Backend (immediate response):
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"accepted": true
}
}
5. Progress Updates (Notifications)
Content Update:
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"type": "content",
"data": {
"text": "I'm analyzing the code in auth/login.go..."
}
}
}
Tool Call:
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"type": "toolCall",
"data": {
"toolName": "file_edit",
"status": "running",
"args": {
"file": "auth/login.go",
"line": 42
}
}
}
}
Completion:
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"type": "complete",
"data": {
"sessionId": "abc123def456",
"success": true,
"summary": "Fixed null pointer by adding nil check"
}
}
}
Error:
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"type": "error",
"data": {
"message": "Failed to compile after changes",
"details": "syntax error on line 45"
}
}
}
6. Cancel Operation
Backend → Agent (notification, no response expected):
{
"jsonrpc": "2.0",
"method": "session/cancel",
"params": {
"reason": "User requested cancellation"
}
}
Available Agent Types
Augment Agent (augment-agent)
Primary AI coding agent powered by Auggie CLI with agentctl sidecar.
Docker Image: kandev/augment-agent:latest
Architecture:
Container
├── agentctl (HTTP server on port 9999)
│ └── manages → auggie --acp (subprocess)
└── /workspace (mounted project directory)
Specification:
- Working Directory:
/workspace - Memory: 4096 MB
- CPU: 2.0 cores
- Timeout: 1 hour
- Exposed Port: 9999 (agentctl HTTP API)
- Capabilities: code_generation, code_review, refactoring, testing, shell_execution
Required Environment Variables:
AUGMENT_SESSION_AUTH # From ~/.augment/session.json
TASK_DESCRIPTION # The task to execute
Optional Environment Variables:
AUGGIE_SESSION_ID # Resume previous session
AGENTCTL_AUTO_START # Auto-start agent on container start
Mounted Volumes:
{workspace}→/workspace- Project directory{augment_sessions}→/root/.augment/sessions- Session persistence
Dockerfile: apps/backend/dockerfiles/augment-agent/Dockerfile
Building the Image:
cd apps/backend
docker build -t kandev/augment-agent:latest -f dockerfiles/augment-agent/Dockerfile .
Creating Custom Agents
Custom agents can be created in two ways:
Option A: With agentctl (Recommended)
Use the multi-stage Dockerfile pattern to include agentctl:
# Stage 1: Build agentctl
FROM golang:1.23-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o agentctl ./cmd/agentctl
# Stage 2: Final image
FROM ubuntu:22.04
# Install your agent dependencies
RUN apt-get update && apt-get install -y curl jq git && rm -rf /var/lib/apt/lists/*
# Copy agentctl from builder
COPY --from=builder /build/agentctl /usr/local/bin/agentctl
# Copy your agent script
COPY agent.sh /usr/local/bin/agent
RUN chmod +x /usr/local/bin/agent
WORKDIR /workspace
EXPOSE 9999
# Set agentctl as entrypoint
ENV AGENTCTL_AGENT_COMMAND="/usr/local/bin/agent --acp"
ENTRYPOINT ["/usr/local/bin/agentctl"]
Option B: Standalone ACP Agent
For simpler agents that don't need HTTP control:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl jq git && rm -rf /var/lib/apt/lists/*
COPY agent.sh /usr/local/bin/agent
RUN chmod +x /usr/local/bin/agent
WORKDIR /workspace
ENTRYPOINT ["/usr/local/bin/agent", "--acp"]
Implementing ACP Protocol
Create agent.sh that implements JSON-RPC 2.0:
#!/bin/bash
set -e
# ACP mode - JSON-RPC 2.0
if [ "$1" = "--acp" ]; then
# Read requests from stdin
while IFS= read -r line; do
METHOD=$(echo "$line" | jq -r '.method')
ID=$(echo "$line" | jq -r '.id')
case "$METHOD" in
"initialize")
echo '{"jsonrpc":"2.0","id":'$ID',"result":{"protocolVersion":"1.0","serverInfo":{"name":"your-agent","version":"1.0.0"},"capabilities":{"streaming":true}}}'
;;
"session/new")
SESSION_ID=$(uuidgen)
echo '{"jsonrpc":"2.0","id":'$ID',"result":{"sessionId":"'$SESSION_ID'"}}'
;;
"session/prompt")
MESSAGE=$(echo "$line" | jq -r '.params.message')
# Accept prompt
echo '{"jsonrpc":"2.0","id":'$ID',"result":{"accepted":true}}'
# Send progress
echo '{"jsonrpc":"2.0","method":"session/update","params":{"type":"content","data":{"text":"Processing: '$MESSAGE'"}}}'
# Do work here
sleep 2
# Send completion
echo '{"jsonrpc":"2.0","method":"session/update","params":{"type":"complete","data":{"sessionId":"'$SESSION_ID'","success":true}}}'
;;
"session/cancel")
exit 0
;;
esac
done
fi
Register Agent Type
Add to apps/backend/internal/agent/registry/defaults.go:
{
ID: "your-agent",
Name: "Your Custom Agent",
Description: "Description of what your agent does",
Image: "kandev/your-agent:latest",
WorkingDir: "/workspace",
MemoryMB: 2048,
CPUCores: 1.0,
Timeout: 30 * time.Minute,
Capabilities: []string{"custom_capability"},
Enabled: true,
}
Step 4: Build and Test
# Build image
cd apps/backend/dockerfiles/your-agent
docker build -t kandev/your-agent:latest .
# Test locally
docker run -it --rm \
-v $(pwd):/workspace \
-e TASK_DESCRIPTION="Test task" \
kandev/your-agent:latest --acp
Test by sending JSON-RPC messages via stdin:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1.0"}}' | \
docker run -i --rm kandev/your-agent:latest --acp
WebSocket API Integration
All agent control operations use WebSocket messages with the following structure:
{
"id": "uuid",
"type": "request|response",
"action": "action.name",
"payload": { ... }
}
Launch Agent
Action: agent.launch
Request:
{
"id": "uuid",
"type": "request",
"action": "agent.launch",
"payload": {
"task_id": "uuid",
"agent_type": "augment-agent",
"workspace_path": "/absolute/path/to/project",
"env": {
"AUGMENT_SESSION_AUTH": "{...}",
"TASK_DESCRIPTION": "Fix the bug in auth/login.go"
},
"priority": 1,
"metadata": {}
}
}
Response:
{
"id": "uuid",
"type": "response",
"action": "agent.launch",
"payload": {
"id": "instance-uuid",
"task_id": "uuid",
"agent_type": "augment-agent",
"container_id": "docker-id",
"status": "starting",
"started_at": "2026-01-10T12:00:00Z"
}
}
Send Prompt (Mid-Execution)
Action: agent.prompt
Request:
{
"id": "uuid",
"type": "request",
"action": "agent.prompt",
"payload": {
"instance_id": "instance-uuid",
"message": "Please also add unit tests"
}
}
Response:
{
"id": "uuid",
"type": "response",
"action": "agent.prompt",
"payload": {
"status": "accepted"
}
}
Cancel Agent
Action: agent.cancel
Request:
{
"id": "uuid",
"type": "request",
"action": "agent.cancel",
"payload": {
"instance_id": "instance-uuid",
"reason": "User requested cancellation"
}
}
Response:
{
"id": "uuid",
"type": "response",
"action": "agent.cancel",
"payload": {
"status": "cancelled"
}
}
Get Agent Status
Action: agent.status
Request:
{
"id": "uuid",
"type": "request",
"action": "agent.status",
"payload": {
"instance_id": "instance-uuid"
}
}
Response:
{
"id": "uuid",
"type": "response",
"action": "agent.status",
"payload": {
"id": "instance-uuid",
"status": "running",
"progress": 50,
"started_at": "2026-01-10T12:00:00Z",
"finished_at": null
}
}
Statuses: starting, running, completed, failed, stopped
Get Agent Logs
Action: agent.logs
Request:
{
"id": "uuid",
"type": "request",
"action": "agent.logs",
"payload": {
"instance_id": "instance-uuid",
"tail": 100
}
}
Response:
{
"id": "uuid",
"type": "response",
"action": "agent.logs",
"payload": {
"logs": ["line1", "line2", "..."]
}
}
Get Session Info
Action: agent.session
Request:
{
"id": "uuid",
"type": "request",
"action": "agent.session",
"payload": {
"instance_id": "instance-uuid"
}
}
Response:
{
"id": "uuid",
"type": "response",
"action": "agent.session",
"payload": {
"instance_id": "instance-uuid",
"task_id": "task-uuid",
"session_id": "abc123",
"status": "running"
}
}
Session Resumption
Agents can resume previous sessions for follow-up conversations.
How It Works
- First task completes, session ID stored in task metadata
- Retrieve session ID via WebSocket
task.getaction →metadata.auggie_session_id - Launch new task with
AUGGIE_SESSION_IDenvironment variable
Example
// WebSocket connection
const ws = new WebSocket('ws://localhost:8080/api/v1/ws');
// Get session ID from completed task
ws.send(JSON.stringify({
id: crypto.randomUUID(),
type: 'request',
action: 'task.get',
payload: { task_id: previousTaskId }
}));
// Handle response and launch with resumption
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
if (response.action === 'task.get') {
const sessionId = response.payload.metadata.auggie_session_id;
// Launch agent with session resumption
ws.send(JSON.stringify({
id: crypto.randomUUID(),
type: 'request',
action: 'agent.launch',
payload: {
task_id: newTaskId,
agent_type: 'augment-agent',
workspace_path: '/path/to/project',
env: {
AUGMENT_SESSION_AUTH: JSON.stringify(sessionAuth),
AUGGIE_SESSION_ID: sessionId,
TASK_DESCRIPTION: 'What changes did you make in the previous task?'
}
}
}));
}
};
The agent will load the previous context and continue the conversation.
Legacy Message Format
Prior versions used a simpler JSON format (not JSON-RPC). For compatibility reference:
{
"type": "progress|log|result|error|status|heartbeat",
"agent_id": "uuid",
"task_id": "uuid",
"timestamp": "2026-01-10T12:00:00Z",
"data": { ... }
}
Types:
progress- Progress percentage, current filelog- Log messages (debug, info, warn, error)result- Task completion (completed, failed, cancelled)error- Error messagesstatus- Status changes (started, running, paused, stopped)heartbeat- Keep-alive
Current agents should use JSON-RPC 2.0 (ACP), but the backend may support legacy format for backward compatibility.
Error Handling
JSON-RPC Errors
Error Response:
{
"jsonrpc": "2.0",
"id": 4,
"error": {
"code": -32600,
"message": "Invalid request",
"data": "Missing required parameter: message"
}
}
Standard Error Codes:
-32700- Parse error-32600- Invalid request-32601- Method not found-32602- Invalid params-32603- Internal error
HTTP API Errors
{
"error": "error_code",
"message": "Human readable message",
"details": "Additional context",
"http_status": 400
}
Common Codes:
bad_request(400)not_found(404)validation_error(400)internal_error(500)
Best Practices
For Agent Developers
- Respond quickly to initialize - Send capabilities within 1 second
- Stream progress frequently - Update every 1-5 seconds during work
- Handle cancellation gracefully - Clean up and exit when receiving
session/cancel - Use proper session IDs - Generate unique, persistent IDs for resumption
- Log to stderr - Use stdout exclusively for ACP messages
- Validate messages - Check for required fields before processing
- Set exit codes - 0 for success, non-zero for errors
Message Flow (with agentctl)
1. Backend creates container with agentctl
2. agentctl starts HTTP server on port 9999
3. Backend waits for agentctl health check
4. Backend calls POST /api/v1/start
5. agentctl spawns agent subprocess
6. Backend calls POST /api/v1/acp/call with initialize
7. Agent responds with capabilities
8. Backend calls POST /api/v1/acp/call with session/new
9. Agent responds with sessionId
10. Backend connects to WS /api/v1/acp/stream
11. Backend calls POST /api/v1/acp/call with session/prompt
12. Agent sends session/update notifications (streamed via WebSocket)
13. Agent sends final session/update (type: complete)
14. Backend calls POST /api/v1/stop or container exits
Testing Your Agent
With agentctl:
# Build and run container
docker run -d -p 9999:9999 kandev/your-agent:latest
# Check health
curl http://localhost:9999/health
# Start agent
curl -X POST http://localhost:9999/api/v1/start
# Send ACP message
curl -X POST http://localhost:9999/api/v1/acp/call \
-H "Content-Type: application/json" \
-d '{"method":"initialize","params":{"protocolVersion":"1.0"}}'
Direct ACP testing:
# Test initialize
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1.0"}}' | \
docker run -i kandev/your-agent:latest /usr/local/bin/agent --acp
# Test full flow
(
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1.0"}}'
echo '{"jsonrpc":"2.0","id":2,"method":"session/new","params":{}}'
echo '{"jsonrpc":"2.0","id":3,"method":"session/prompt","params":{"message":"test"}}'
) | docker run -i kandev/your-agent:latest /usr/local/bin/agent --acp
Reference Implementation
See apps/backend/ for complete working examples:
agentctl:
cmd/agentctl/main.go- Entry pointinternal/agentctl/- HTTP server, process manager, ACP relay
Augment Agent:
dockerfiles/augment-agent/Dockerfile- Multi-stage build with agentctldockerfiles/augment-agent/entrypoint.sh- Container entrypoint
Backend Integration:
internal/agent/agentctl/client.go- HTTP client for agentctlinternal/agent/lifecycle/manager.go- Container lifecycle management
Additional Resources
- System Architecture: ARCHITECTURE.md
- WebSocket API Specification: docs/asyncapi.yaml
- Agent Registry:
apps/backend/internal/agent/registry/defaults.go - agentctl Implementation:
apps/backend/internal/agentctl/ - ACP Protocol Types:
apps/backend/pkg/acp/jsonrpc/ - JSON-RPC 2.0 Spec: https://www.jsonrpc.org/specification
Last Updated: 2026-01-10