From 91a4e0551fc8d0aca33324f49f55d326beb1ca74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 17 Jan 2026 02:29:40 +0000 Subject: [PATCH] feat: updated agents.md (#28) --- AGENTS.md | 1045 +++++++---------------------------------------------- 1 file changed, 127 insertions(+), 918 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8f419f5e5e..911603f19c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,976 +1,185 @@ -# Kandev Agent Protocol +# Kandev Engineering Guide -> **For Agent Developers**: This document explains how to build agents that integrate with Kandev using the Agent Communication Protocol (ACP). +> **Purpose**: This document captures up-to-date architecture notes, agent protocols, and frontend data-fetching patterns for Kandev. > -> **For System Architecture**: See [ARCHITECTURE.md](ARCHITECTURE.md) -> **For WebSocket API**: See [docs/asyncapi.yaml](docs/asyncapi.yaml) +> **Related**: [ARCHITECTURE.md](ARCHITECTURE.md), [docs/asyncapi.yaml](docs/asyncapi.yaml) -## Overview +## Repo Layout -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. +- `apps/backend/`: Go backend (WebSocket gateway, orchestrator, agent lifecycle, agentctl build). +- `apps/web/`: Next.js frontend (SSR + WS + Zustand store). +- `apps/packages/`: Shared packages/types. +- `apps/landing/`: Marketing/landing site that shares UI components with the web app. +- `apps/backend/dockerfiles/`: Agent container images (e.g., augment-agent). -``` -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 │ - │◄────────────────────────┤◄────────────────────────────┤◄─────────────────────────┤ - │ │ │ │ -``` +## Tooling and Workspace -## Architecture +- **Package manager**: `pnpm` workspace. +- **Workspace root**: repository root. +- **Web app**: `apps/web` (Next.js, client + server components). +- **Backend**: `apps/backend` (Go services, agent lifecycle, ws). +- **UI**: Shadcn-based components are wrapped under `@kandev/ui`. -### 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: +Common commands: ```bash -#!/bin/bash -set -e +pnpm -C apps/web dev +pnpm -C apps/web lint +pnpm -C apps/web typecheck -# 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 +make -C apps/backend test +make -C apps/backend lint ``` -### Register Agent Type +## Backend Architecture (High Level) -Add to `apps/backend/internal/agent/registry/defaults.go`: +- **Transport**: WebSocket-only for client <-> backend. +- **Gateway**: Single WS endpoint at `ws://localhost:8080/ws`. +- **Orchestrator**: Starts/stops/resumes tasks, manages sessions. +- **Agent lifecycle**: + - Supports Docker and standalone. + - Creates agentctl instances for workspace access (shell, git, file ops). + - Starts agent subprocesses explicitly. +- **agentctl**: + - HTTP server for per-agent instance management. + - Uses ACP (JSON-RPC 2.0 over stdin/stdout) to talk to agent process. + - Streams ACP updates and workspace outputs to backend. -```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, -} +### Runtime / agentctl Flow (Simplified) + +``` +Client (WS) Backend Runtime agentctl Agent + | orchestrator.start | | | | + |------------------->| LaunchAgent | Create instance | | + | |--------------------->|------------------->| | + | | StartAgentProcess | Configure/Start |----> agent process | + | | ACP calls | |<--- ACP messages | + |<-------------------| WS notifications | | | ``` -### Step 4: Build and Test +### Protocols -```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 +- **ACP**: JSON-RPC 2.0 over stdin/stdout (agent <-> agentctl <-> backend). +- **WS Message Format**: ```json { "id": "uuid", "type": "request|response|notification|error", "action": "action.name", - "payload": { ... }, + "payload": { "...": "..." }, "timestamp": "2026-01-10T12:00:00Z" } ``` -### Orchestrator Actions +- See `docs/asyncapi.yaml` for WS actions and payloads. -| 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 | +## Frontend Architecture -### Start Task Execution +### Key Principles -**Action**: `orchestrator.start` +- **Store is the source of truth** (Zustand store slices). +- **SSR preloads data** into the store via `StateHydrator`. +- **Components read from store only**; avoid ad-hoc fetches in UI components. +- **Reusable components** preferred; split large pages into smaller components. +- **Data fetching should live where it is required**; avoid leaking fetches into parent controllers. -**Request:** -```json -{ - "id": "uuid", - "type": "request", - "action": "orchestrator.start", - "payload": { - "task_id": "uuid", - "agent_type": "augment-agent", - "priority": 2 - } -} +### Data Fetching and SSR + +- SSR fetch in layout/page, map to store shape, hydrate via `StateHydrator`. +- WebSocket events update store slices for real-time sync. +- Only re-fetch when WS cannot deliver full payloads (document why). + +### Subscription Strategy + +TODO + +**Why This Works** + +- Components declare their dependencies explicitly. +- Subscription cleanup is automatic on unmount. +- WebSocket client guarantees only one real subscription per channel. +- Store updates flow globally, components re-render only on relevant slices. + +### Three-Layer Model + +``` +Layer 1: WebSocket Client (connection + dedup) +Layer 2: Event Handlers (route WS payloads to store) +Layer 3: Components (subscribe + render) ``` -**Response:** -```json -{ - "id": "uuid", - "type": "response", - "action": "orchestrator.start", - "payload": { - "success": true, - "task_id": "uuid", - "agent_instance_id": "instance-uuid" - } -} -``` +### Store Structure -### Send Follow-up Prompt +TODO -**Action**: `orchestrator.prompt` +### Custom Hooks Pattern -**Request:** -```json -{ - "id": "uuid", - "type": "request", - "action": "orchestrator.prompt", - "payload": { - "task_id": "uuid", - "prompt": "Please also add unit tests" - } -} -``` +TODO -**Response:** -```json -{ - "id": "uuid", - "type": "response", - "action": "orchestrator.prompt", - "payload": { - "success": true, - "task_id": "uuid" - } -} -``` +### Page-Level Responsibilities -### Stop Task Execution +TODO -**Action**: `orchestrator.stop` +## Agents and ACP -**Request:** -```json -{ - "id": "uuid", - "type": "request", - "action": "orchestrator.stop", - "payload": { - "task_id": "uuid", - "reason": "User requested cancellation", - "force": false - } -} -``` +### ACP Basics -**Response:** -```json -{ - "id": "uuid", - "type": "response", - "action": "orchestrator.stop", - "payload": { - "success": true, - "task_id": "uuid" - } -} -``` +- **Transport**: JSON-RPC 2.0 over stdin/stdout. +- **Format**: newline-delimited JSON. +- **Direction**: request/response + notifications. -### Complete Task +**Backend → Agent (Requests)** +- `initialize` +- `session/new` +- `session/load` +- `session/prompt` +- `session/cancel` -**Action**: `orchestrator.complete` +**Agent → Backend (Notifications)** +- `session/update` (progress/content/tool calls/complete/error) -**Request:** -```json -{ - "id": "uuid", - "type": "request", - "action": "orchestrator.complete", - "payload": { - "task_id": "uuid" - } -} -``` +### agentctl HTTP (Internal) -**Response:** -```json -{ - "id": "uuid", - "type": "response", - "action": "orchestrator.complete", - "payload": { - "success": true, - "task_id": "uuid" - } -} -``` +- `/health` GET +- `/api/v1/status` GET +- `/api/v1/start` POST +- `/api/v1/stop` POST +- `/api/v1/acp/*` (initialize, session/new, session/load, prompt, stream) +- `/api/v1/shell/*` (status, stream, buffer) +- `/api/v1/workspace/*` (git status, file changes) -### Get Orchestrator Status +### Agent Types -**Action**: `orchestrator.status` +- Augment agent lives at `apps/backend/dockerfiles/augment-agent/`. +- Registry defaults in `apps/backend/internal/agent/registry/defaults.go`. -**Request:** -```json -{ - "id": "uuid", - "type": "request", - "action": "orchestrator.status", - "payload": {} -} -``` +## Frontend UI System (Shadcn) -**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) - ---- +- Prefer `@kandev/ui` components over ad-hoc HTML. +- Reuse components; avoid duplicating UI patterns. +- Split large pages into smaller components; keep layout vs data vs presentation separated. ## Best Practices -### For Agent Developers +- Prefer store-driven state over local fetches. +- Prefer reusable components over duplication; split large pages into smaller child components. +- Keep data fetching scoped to the component that needs it; avoid leaking fetches into parent controllers. +- Avoid WS request/response inside UI components when store actions can handle it. +- Keep SSR fetches minimal and map responses into store-friendly shapes. +- Component subscriptions should be local; WS client should dedupe. +- Keep agents logging to stderr; stdout only for ACP. -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 +## agentctl Server Architecture (Adapter Model) -### Frontend Data Loading (SSR + WS) +The agentctl server now uses a protocol adapter layer to support multiple agent CLIs while keeping a consistent HTTP + WebSocket surface for the backend. -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 +- `server/adapter/` defines the `AgentAdapter` interface and normalized `SessionUpdate` payloads. +- `ACPAdapter` uses the ACP JSON-RPC protocol over stdio (`server/adapter/acp_adapter.go`). +- `CodexAdapter` supports Codex-style JSON-RPC over stdio (`server/adapter/codex_adapter.go`). +- `process.Manager` owns the subprocess, wires stdin/stdout into the selected adapter, and forwards normalized updates to API streams. +- `api/acp.go` exposes protocol-agnostic endpoints (`/api/v1/acp/*`) that delegate to the adapter. +- `instance/` manages per-agent instance servers on dedicated ports (control server creates instances; each instance hosts its own API server). ---