Files
kandev/AGENTS.md
T
2026-01-14 01:42:12 +00:00

978 lines
24 KiB
Markdown

# 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](ARCHITECTURE.md)
> **For WebSocket API**: See [docs/asyncapi.yaml](docs/asyncapi.yaml)
## Overview
Kandev uses a **WebSocket-only architecture** for all client-backend communication. Agents run in Docker containers with **agentctl** as a sidecar process. The agent communicates via **ACP (Agent Communication Protocol)** - a JSON-RPC 2.0 protocol over stdin/stdout. ACP messages are streamed to clients via WebSocket notifications.
```
Client (WS) Backend (kandev) agentctl (HTTP) Agent Process
│ │ │ │
│ orchestrator.start │ │ │
├────────────────────────►│ POST /api/v1/start │ │
│ ├────────────────────────────►│ spawn auggie --acp │
│ │ ├─────────────────────────►│
│ │ POST /api/v1/acp/call │ initialize │
│ ├────────────────────────────►├─────────────────────────►│
│ │◄────────────────────────────┤◄─────────────────────────┤
│ acp.progress │ WS /api/v1/acp/stream │ session/update │
│◄────────────────────────┤◄────────────────────────────┤◄─────────────────────────┤
│ │ │ │
```
## Architecture
### WebSocket Gateway
All client communication uses a single WebSocket endpoint at `ws://localhost:8080/ws`. The backend:
1. **Receives orchestrator requests** (`orchestrator.start`, `orchestrator.stop`, `orchestrator.prompt`)
2. **Manages agent containers** via Docker and agentctl HTTP API
3. **Streams ACP messages** to subscribed clients as WebSocket notifications
4. **Publishes events** via internal event bus for real-time updates
### agentctl
`agentctl` is an HTTP server that runs inside each agent container as a sidecar process. It:
1. **Manages the agent subprocess** (e.g., `auggie --acp`)
2. **Provides HTTP API** for control operations (start, stop, status)
3. **Relays ACP messages** between the backend and agent's stdin/stdout
4. **Streams output** via WebSocket for real-time monitoring
### Communication Flow
1. Client sends `orchestrator.start` via WebSocket
2. Backend creates container with agentctl as entrypoint
3. agentctl starts HTTP server on port 9999
4. Backend waits for agentctl to be ready (`GET /health`)
5. Backend starts agent process (`POST /api/v1/start`)
6. Backend connects to agentctl WebSocket (`GET /api/v1/acp/stream`)
7. Backend sends ACP messages via HTTP (`POST /api/v1/acp/call`)
8. ACP updates are streamed to subscribed clients as `acp.*` notifications
### agentctl HTTP API (Internal)
These endpoints are used by the backend to communicate with agent containers:
| 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 capabilities
- `session/new` - Create new session
- `session/load` - Resume existing session
- `session/prompt` - Send task/prompt to execute
- `session/cancel` - Cancel current operation
**Agent → Backend (Notifications):**
- `session/update` - Progress updates, content, tool calls, completion
---
## Message Specifications
### 1. Initialize Connection
**Backend → Agent:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "1.0",
"clientInfo": {
"name": "kandev",
"version": "0.1.0"
},
"capabilities": {
"streaming": true
}
}
}
```
**Agent → Backend:**
```json
{
"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:**
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "session/new",
"params": {}
}
```
**Agent → Backend:**
```json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"sessionId": "abc123def456"
}
}
```
### 3. Resume Existing Session
**Backend → Agent:**
```json
{
"jsonrpc": "2.0",
"id": 3,
"method": "session/load",
"params": {
"sessionId": "abc123def456"
}
}
```
**Agent → Backend:**
```json
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"sessionId": "abc123def456",
"loaded": true
}
}
```
### 4. Send Prompt
**Backend → Agent:**
```json
{
"jsonrpc": "2.0",
"id": 4,
"method": "session/prompt",
"params": {
"message": "Fix the null pointer bug in the login function"
}
}
```
**Agent → Backend (immediate response):**
```json
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"accepted": true
}
}
```
### 5. Progress Updates (Notifications)
**Content Update:**
```json
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"type": "content",
"data": {
"text": "I'm analyzing the code in auth/login.go..."
}
}
}
```
**Tool Call:**
```json
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"type": "toolCall",
"data": {
"toolName": "file_edit",
"status": "running",
"args": {
"file": "auth/login.go",
"line": 42
}
}
}
}
```
**Completion:**
```json
{
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"type": "complete",
"data": {
"sessionId": "abc123def456",
"success": true,
"summary": "Fixed null pointer by adding nil check"
}
}
}
```
**Error:**
```json
{
"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):**
```json
{
"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:**
```bash
AUGMENT_SESSION_AUTH # From ~/.augment/session.json
TASK_DESCRIPTION # The task to execute
```
**Optional Environment Variables:**
```bash
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:**
```bash
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:
```dockerfile
# 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:
```dockerfile
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:
```bash
#!/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`:
```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
```bash
# 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:
```bash
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 client communication uses a single WebSocket endpoint: `ws://localhost:8080/ws`
### Message Format
```json
{
"id": "uuid",
"type": "request|response|notification|error",
"action": "action.name",
"payload": { ... },
"timestamp": "2026-01-10T12:00:00Z"
}
```
### Orchestrator Actions
| Action | Description |
|--------|-------------|
| `orchestrator.status` | Get orchestrator status |
| `orchestrator.queue` | Get queued tasks |
| `orchestrator.start` | Start task execution |
| `orchestrator.stop` | Stop running task |
| `orchestrator.prompt` | Send follow-up prompt |
| `orchestrator.complete` | Mark task complete |
| `orchestrator.trigger` | Queue task for execution |
### Start Task Execution
**Action**: `orchestrator.start`
**Request:**
```json
{
"id": "uuid",
"type": "request",
"action": "orchestrator.start",
"payload": {
"task_id": "uuid",
"agent_type": "augment-agent",
"priority": 2
}
}
```
**Response:**
```json
{
"id": "uuid",
"type": "response",
"action": "orchestrator.start",
"payload": {
"success": true,
"task_id": "uuid",
"agent_instance_id": "instance-uuid"
}
}
```
### Send Follow-up Prompt
**Action**: `orchestrator.prompt`
**Request:**
```json
{
"id": "uuid",
"type": "request",
"action": "orchestrator.prompt",
"payload": {
"task_id": "uuid",
"prompt": "Please also add unit tests"
}
}
```
**Response:**
```json
{
"id": "uuid",
"type": "response",
"action": "orchestrator.prompt",
"payload": {
"success": true,
"task_id": "uuid"
}
}
```
### Stop Task Execution
**Action**: `orchestrator.stop`
**Request:**
```json
{
"id": "uuid",
"type": "request",
"action": "orchestrator.stop",
"payload": {
"task_id": "uuid",
"reason": "User requested cancellation",
"force": false
}
}
```
**Response:**
```json
{
"id": "uuid",
"type": "response",
"action": "orchestrator.stop",
"payload": {
"success": true,
"task_id": "uuid"
}
}
```
### Complete Task
**Action**: `orchestrator.complete`
**Request:**
```json
{
"id": "uuid",
"type": "request",
"action": "orchestrator.complete",
"payload": {
"task_id": "uuid"
}
}
```
**Response:**
```json
{
"id": "uuid",
"type": "response",
"action": "orchestrator.complete",
"payload": {
"success": true,
"task_id": "uuid"
}
}
```
### Get Orchestrator Status
**Action**: `orchestrator.status`
**Request:**
```json
{
"id": "uuid",
"type": "request",
"action": "orchestrator.status",
"payload": {}
}
```
**Response:**
```json
{
"id": "uuid",
"type": "response",
"action": "orchestrator.status",
"payload": {
"running": true,
"active_agents": 2,
"queued_tasks": 5,
"max_concurrent": 3
}
}
```
### ACP Notifications
When subscribed to a task via `task.subscribe`, clients receive real-time ACP notifications:
**Progress Notification:**
```json
{
"id": "uuid",
"type": "notification",
"action": "acp.progress",
"payload": {
"task_id": "uuid",
"type": "progress",
"data": {
"progress": 50,
"stage": "executing",
"message": "Processing files..."
},
"timestamp": "2026-01-10T12:00:00Z"
}
}
```
**Log Notification:**
```json
{
"id": "uuid",
"type": "notification",
"action": "acp.log",
"payload": {
"task_id": "uuid",
"type": "log",
"data": {
"level": "info",
"message": "Found 5 files to modify"
},
"timestamp": "2026-01-10T12:00:00Z"
}
}
```
**Task Subscription:**
**Action**: `task.subscribe`
```json
{
"id": "uuid",
"type": "request",
"action": "task.subscribe",
"payload": {
"task_id": "uuid"
}
}
```
---
## Session Resumption
Agents can resume previous sessions for follow-up conversations.
### How It Works
1. First task completes, session ID stored in task metadata
2. Retrieve session ID via WebSocket `task.get` action → `metadata.auggie_session_id`
3. Launch new task with `AUGGIE_SESSION_ID` environment variable
### Example
```javascript
// WebSocket connection
const ws = new WebSocket('ws://localhost:8080/ws');
// Get session ID from completed task
ws.send(JSON.stringify({
id: crypto.randomUUID(),
type: 'request',
action: 'task.get',
payload: { id: previousTaskId }
}));
// Handle response and start with session resumption
ws.onmessage = (event) => {
const response = JSON.parse(event.data);
if (response.action === 'task.get') {
const sessionId = response.payload.metadata.auggie_session_id;
// Create new task with session ID in metadata
ws.send(JSON.stringify({
id: crypto.randomUUID(),
type: 'request',
action: 'task.create',
payload: {
board_id: boardId,
column_id: columnId,
title: 'Follow-up task',
description: 'What changes did you make in the previous task?',
agent_type: 'augment-agent',
metadata: {
auggie_session_id: sessionId
}
}
}));
}
};
```
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:
```json
{
"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 file
- `log` - Log messages (debug, info, warn, error)
- `result` - Task completion (completed, failed, cancelled)
- `error` - Error messages
- `status` - 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:**
```json
{
"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
```json
{
"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
1. **Respond quickly to initialize** - Send capabilities within 1 second
2. **Stream progress frequently** - Update every 1-5 seconds during work
3. **Handle cancellation gracefully** - Clean up and exit when receiving `session/cancel`
4. **Use proper session IDs** - Generate unique, persistent IDs for resumption
5. **Log to stderr** - Use stdout exclusively for ACP messages
6. **Validate messages** - Check for required fields before processing
7. **Set exit codes** - 0 for success, non-zero for errors
### Frontend Data Loading (SSR + WS)
For settings + dashboard data, use a consistent pattern:
1. **SSR fetch in layout/page** → hydrate the store via `StateHydrator`.
2. **Components read from store only** (no local fetches in UI components).
3. **WebSocket events update the store** for real-time sync.
Guidelines:
- Fetch once in the server component (e.g. `apps/web/app/settings/layout.tsx`) and hydrate.
- Map API responses into store-friendly shapes (e.g. agent profiles → `{ id, label, agent_id }`).
- Use WS handlers to patch lists; avoid making HTTP requests inside WS handlers.
- Prefer WS events to include full entity payloads so clients do not need follow-up HTTP fetches.
- Only re-fetch when the server cannot emit the full data and document why.
- Avoid request/response WS calls inside UI components.
- UI components should read from the store only; perform HTTP fetches in hooks or store actions.
## Local Agent Discovery
Kandev checks a bundled discovery config to detect locally installed agents. Each agent entry lists OS-specific installation paths (Linux, Windows, macOS). At runtime, the backend selects the paths for the current OS, expands environment variables and `~`, and marks the agent as available if any configured file or directory exists.
### Message Flow
```
1. Client sends orchestrator.start via WebSocket
2. Backend creates container with agentctl
3. agentctl starts HTTP server on port 9999
4. Backend waits for agentctl health check (GET /health)
5. Backend calls POST /api/v1/start
6. agentctl spawns agent subprocess
7. Backend calls POST /api/v1/acp/call with initialize
8. Agent responds with capabilities
9. Backend calls POST /api/v1/acp/call with session/new
10. Agent responds with sessionId
11. Backend connects to agentctl WS /api/v1/acp/stream
12. Backend calls POST /api/v1/acp/call with session/prompt
13. Agent sends session/update notifications (streamed to client via WebSocket)
14. Agent sends final session/update (type: complete)
15. Client sends orchestrator.complete or backend calls POST /api/v1/stop
```
### Testing Your Agent
**With agentctl:**
```bash
# 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:**
```bash
# 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 point
- `internal/agentctl/` - HTTP server, process manager, ACP relay
**Augment Agent:**
- `dockerfiles/augment-agent/Dockerfile` - Multi-stage build with agentctl
- `dockerfiles/augment-agent/entrypoint.sh` - Container entrypoint
**Backend Integration:**
- `internal/agent/agentctl/client.go` - HTTP client for agentctl
- `internal/agent/lifecycle/manager.go` - Container lifecycle management
---
## Additional Resources
- **System Architecture**: [ARCHITECTURE.md](ARCHITECTURE.md)
- **WebSocket API Specification**: [docs/asyncapi.yaml](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-11