Files
kandev/apps/backend/docs/add_agent.md
T
2026-02-04 23:52:26 +00:00

17 KiB

Adding New Agents to Kandev

This guide explains how to add support for new AI coding agents (like Gemini, OpenCode, Amp, etc.) to the Kandev backend.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                         Agent Architecture                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  agents.json          Registry              Adapter Factory         │
│  ┌──────────┐        ┌──────────┐          ┌──────────────┐        │
│  │ Agent    │───────▶│ Registry │─────────▶│ NewAdapter() │        │
│  │ Config   │        │          │          │              │        │
│  └──────────┘        └──────────┘          └──────┬───────┘        │
│                                                    │                │
│                                                    ▼                │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Protocol Adapters                         │   │
│  ├─────────────┬─────────────┬─────────────┬─────────────┐     │   │
│  │ ACP Adapter │Codex Adapter│Claude Adapter│ New Adapter │     │   │
│  │ (JSON-RPC)  │ (JSON-RPC)  │(stream-json) │  (???)      │     │   │
│  └─────────────┴─────────────┴─────────────┴─────────────┘     │   │
│                           │                                     │   │
│                           ▼                                     │   │
│                    AgentEvent (normalized)                      │   │
│                           │                                     │   │
│                           ▼                                     │   │
│                    Lifecycle Manager                            │   │
│                                                                 │   │
└─────────────────────────────────────────────────────────────────────┘

All agents communicate through a protocol adapter that normalizes their output into AgentEvent structs. This allows the rest of the system to work uniformly regardless of the underlying agent.

Files to Modify/Create

1. Protocol Definition

File: pkg/agent/protocol.go

Add a new protocol constant:

const (
    ProtocolACP       Protocol = "acp"
    ProtocolCodex     Protocol = "codex"
    ProtocolClaudeCode Protocol = "claude-code"
    ProtocolGemini    Protocol = "gemini"      // New
    ProtocolOpenCode  Protocol = "opencode"    // New
)

func (p Protocol) IsValid() bool {
    switch p {
    case ProtocolACP, ProtocolREST, ProtocolMCP, ProtocolCodex,
         ProtocolClaudeCode, ProtocolGemini, ProtocolOpenCode:
        return true
    }
    return false
}

2. Protocol Types (if needed)

File: pkg/<agentname>/types.go

If the agent uses a custom protocol, create a types package:

package gemini

// Message types specific to this agent's protocol
type CLIMessage struct {
    Type    string `json:"type"`
    // ... agent-specific fields
}

// Request/response types for control flow
type ControlRequest struct {
    // ...
}

3. Protocol Client (if needed)

File: pkg/<agentname>/client.go

For agents with custom protocols over stdin/stdout:

package gemini

type Client struct {
    stdin  io.Writer
    stdout io.Reader
    // ...
}

func NewClient(stdin io.Writer, stdout io.Reader) *Client
func (c *Client) Start(ctx context.Context)
func (c *Client) SendMessage(msg *Message) error
func (c *Client) SetMessageHandler(handler MessageHandler)

4. Protocol Adapter

File: internal/agentctl/server/adapter/<agentname>_adapter.go

Implement the AgentAdapter interface:

package adapter

type GeminiAdapter struct {
    cfg       *Config
    logger    *logger.Logger
    stdin     io.Writer
    stdout    io.Reader
    client    *gemini.Client
    updatesCh chan AgentEvent
    // ...
}

// Required interface methods:
func (a *GeminiAdapter) PrepareEnvironment() error
func (a *GeminiAdapter) Connect(stdin io.Writer, stdout io.Reader) error
func (a *GeminiAdapter) Initialize(ctx context.Context) error
func (a *GeminiAdapter) Prompt(ctx context.Context, message string) error
func (a *GeminiAdapter) Cancel(ctx context.Context) error
func (a *GeminiAdapter) NewSession(ctx context.Context, mcpServers []types.McpServer) (string, error)
func (a *GeminiAdapter) LoadSession(ctx context.Context, sessionID string) error
func (a *GeminiAdapter) Updates() <-chan AgentEvent
func (a *GeminiAdapter) Close() error
func (a *GeminiAdapter) GetAgentInfo() *AgentInfo
func (a *GeminiAdapter) SetPermissionHandler(handler PermissionHandler)
func (a *GeminiAdapter) RespondToPermission(ctx context.Context, requestID string, response *PermissionResponse) error

5. Adapter Factory

File: internal/agentctl/server/adapter/factory.go

Register the new adapter:

func NewAdapter(protocol agent.Protocol, cfg *Config, log *logger.Logger) (AgentAdapter, error) {
    switch protocol {
    case agent.ProtocolACP:
        return NewACPAdapter(cfg, log), nil
    case agent.ProtocolCodex:
        return NewCodexAdapter(cfg, log), nil
    case agent.ProtocolClaudeCode:
        return NewClaudeCodeAdapter(cfg, log), nil
    case agent.ProtocolGemini:
        return NewGeminiAdapter(cfg, log), nil  // New
    default:
        return nil, fmt.Errorf("unsupported protocol: %s", protocol)
    }
}

6. Registry Protocol Parser

File: internal/agent/registry/registry.go

Add protocol string parsing:

func parseProtocol(p string) agent.Protocol {
    switch p {
    case "acp":
        return agent.ProtocolACP
    case "codex":
        return agent.ProtocolCodex
    case "claude-code":
        return agent.ProtocolClaudeCode
    case "gemini":
        return agent.ProtocolGemini  // New
    default:
        return agent.ProtocolACP
    }
}

7. Agent Configuration

File: internal/agent/registry/agents.json

Add the agent definition:

{
  "id": "gemini",
  "name": "Google Gemini Agent",
  "display_name": "Gemini",
  "description": "Google Gemini CLI-powered coding agent.",
  "enabled": true,
  "image": "",
  "tag": "",
  "cmd": [
    "gemini-cli",
    "--output-format=json",
    "--non-interactive"
  ],
  "working_dir": "{workspace}",
  "required_env": ["GOOGLE_API_KEY"],
  "env": {},
  "mounts": [],
  "resource_limits": {
    "memory_mb": 4096,
    "cpu_cores": 2.0,
    "timeout_seconds": 3600
  },
  "capabilities": [
    "code_generation",
    "code_review",
    "shell_execution"
  ],
  "protocol": "gemini",
  "protocol_config": {},
  "model_flag": "--model {model}",
  "workspace_flag": "--workspace",
  "session_config": {
    "native_session_resume": false,
    "resume_flag": "--resume",
    "can_recover": true,
    "session_dir_template": "{home}/.gemini/sessions",
    "session_dir_target": ""
  },
  "permission_config": {
    "permission_flag": "",
    "tools_requiring_permission": ["shell", "write_file", "edit_file"]
  },
  "permission_settings": {
    "auto_approve": {
      "supported": true,
      "default": false,
      "label": "Auto-approve",
      "description": "Automatically approve tool calls",
      "apply_method": "cli_flag",
      "cli_flag": "--auto-approve"
    }
  },
  "discovery": {
    "supports_mcp": false,
    "mcp_config_path": { "linux": [], "windows": [], "macos": [] },
    "installation_path": {
      "linux": ["~/.gemini/config.json"],
      "windows": ["~/.gemini/config.json"],
      "macos": ["~/.gemini/config.json"]
    },
    "discovery_capabilities": {
      "supports_session_resume": true,
      "supports_shell": false,
      "supports_workspace_only": false
    }
  },
  "model_config": {
    "default_model": "gemini-2.0-flash",
    "available_models": [
      {
        "id": "gemini-2.0-flash",
        "name": "Gemini 2.0 Flash",
        "description": "Fast and efficient model",
        "provider": "google",
        "context_window": 1000000,
        "is_default": true
      },
      {
        "id": "gemini-2.0-pro",
        "name": "Gemini 2.0 Pro",
        "description": "Most capable model",
        "provider": "google",
        "context_window": 2000000,
        "is_default": false
      }
    ]
  }
}

Key Considerations

Execution Modes

Agents can run in two modes:

  1. Docker-based: Set image and tag fields. The agent runs in a container.
  2. Standalone: Set cmd array, leave image empty. The agent runs directly on the host.

The validation in registry.go requires either an image OR a cmd:

if config.Image == "" && len(config.Cmd) == 0 {
    return fmt.Errorf("agent type requires either image (Docker) or cmd (standalone)")
}

Event Types

All adapters must emit normalized AgentEvent structs. Key event types:

Event Type Purpose
message_chunk Streaming text from the agent
reasoning Chain-of-thought/thinking content
tool_call Agent is invoking a tool
tool_update Tool execution result
complete Agent turn finished (triggers state change to REVIEW)
error Error occurred
permission_request Agent needs permission for an action
session_status Session state update (NOT turn completion)
context_window Token usage information

Important: Only emit complete when the agent truly finishes a turn. Don't emit it for initialization messages.

Tool Call Events

When emitting tool calls, set both ToolName and ToolTitle:

a.sendUpdate(AgentEvent{
    Type:        EventTypeToolCall,
    SessionID:   sessionID,
    OperationID: operationID,
    ToolCallID:  toolID,
    ToolName:    "Bash",           // Machine-readable name
    ToolTitle:   "ls -la /tmp",    // Human-readable title for UI
    ToolArgs:    args,
    ToolStatus:  "running",
})

Permission Handling

If the agent requires permission for certain actions:

  1. Set tools_requiring_permission in the agent config
  2. Implement SetPermissionHandler and RespondToPermission in the adapter
  3. Emit permission_request events when the agent needs approval:
a.sendUpdate(AgentEvent{
    Type:              EventTypePermissionRequest,
    SessionID:         sessionID,
    PendingID:         requestID,
    ToolCallID:        toolID,
    PermissionTitle:   "Execute: rm -rf /tmp/test",
    PermissionOptions: []PermissionOption{
        {ID: "allow", Label: "Allow"},
        {ID: "deny", Label: "Deny"},
    },
    ActionType:    "shell",
    ActionDetails: map[string]any{"command": "rm -rf /tmp/test"},
})

Session Management

Configure session handling in session_config:

  • native_session_resume: If true, sessions resume via the adapter's native session loading (works across protocols). If false, use CLI flag or context injection.
  • resume_flag: CLI flag for resuming (e.g., --resume)
  • can_recover: Whether sessions can survive backend restarts
  • session_dir_template: Where session data is stored on host
  • session_dir_target: Mount path inside container (if Docker-based)

Session ID Tracking for Stream-Based Protocols:

For agents that report their session ID via the stream (like Claude Code), you must:

  1. Capture the session ID from the protocol message (e.g., system message in Claude Code)
  2. Emit a session_status event with the session ID in SessionID field
  3. The orchestrator will automatically store this as the resume token

Example from Claude Code adapter:

func (a *ClaudeCodeAdapter) handleSystemMessage(msg *claudecode.CLIMessage) {
    // Update session ID if provided
    if msg.SessionID != "" {
        a.mu.Lock()
        a.sessionID = msg.SessionID
        a.mu.Unlock()
    }

    // Send session status event - this triggers resume token storage
    a.sendUpdate(AgentEvent{
        Type:      EventTypeSessionStatus,
        SessionID: msg.SessionID,  // Agent's native session ID
        Data: map[string]any{
            "session_status": msg.SessionStatus,
            "init":           true,
        },
    })
}

The SessionID field in the event is mapped to ACPSessionID in the event payload, which the orchestrator uses to store the resume token.

Model Configuration

The model_flag field defines how to pass the model to the CLI:

"model_flag": "--model {model}"       // Results in: --model gemini-2.0-flash
"model_flag": "-c model=\"{model}\""  // Results in: -c model="gemini-2.0-flash"

The {model} placeholder is replaced with the selected model ID.

Discovery

The discovery system detects installed agents by checking for files:

"installation_path": {
  "linux": ["~/.gemini/config.json"],
  "windows": ["~/.gemini/config.json"],
  "macos": ["~/.gemini/config.json"]
}

When a file exists at any of these paths, the agent is considered "available" and a default profile is auto-created.

Context Window Tracking

Track token usage and emit context window events:

a.sendUpdate(AgentEvent{
    Type:                   EventTypeContextWindow,
    SessionID:              sessionID,
    ContextWindowSize:      200000,
    ContextWindowUsed:      15000,
    ContextWindowRemaining: 185000,
    ContextEfficiency:      7.5, // percentage
})

Testing Checklist

Before releasing a new agent integration:

  • Agent appears in settings when installed (discovery works)
  • Default profile created with correct model
  • Agent starts successfully with a simple prompt
  • Streaming text appears in real-time
  • Tool calls display with proper titles
  • Tool results are shown correctly
  • Permission requests work (if applicable)
  • Session stays IN_PROGRESS while agent is working
  • Session moves to REVIEW when agent finishes
  • Follow-up prompts work in same session
  • Session resume works after stopping agent
  • Context window tracking shows accurate usage
  • Errors are properly displayed to user
  • Model switching works

Example: Existing Adapters

ACP Adapter (acp_adapter.go)

  • Used by: Auggie
  • Protocol: JSON-RPC over stdin/stdout
  • Features: Full ACP protocol support, MCP server integration

Codex Adapter (codex_adapter.go)

  • Used by: OpenAI Codex
  • Protocol: Custom JSON-RPC (codex app-server)
  • Features: Auto-approval via ACP, session resume

Claude Code Adapter (claude_code_adapter.go)

  • Used by: Claude Code CLI
  • Protocol: stream-json over stdin/stdout
  • Features: Permission prompts via stdio, thinking/reasoning support

Troubleshooting

Agent not appearing in settings

  • Check installation_path paths exist on system
  • Verify enabled: true in agents.json
  • Check backend logs for discovery errors

Protocol not recognized (falls back to ACP)

  • Add protocol to parseProtocol() in registry.go
  • Add to Protocol.IsValid() in pkg/agent/protocol.go

Session moves to REVIEW too quickly

  • Don't emit EventTypeComplete for initialization messages
  • Only emit complete when agent truly finishes turn

Tool titles not showing

  • Set both ToolName and ToolTitle in tool_call events
  • ToolTitle is the human-readable display text

Model flag not working

  • Ensure model_flag includes {model} placeholder
  • Check profile has model set (not empty)

Session resume not working

  • For stream-based protocols: ensure adapter emits session_status event with agent's native session ID
  • Check that resume_flag is set in agent config (e.g., "--resume")
  • Verify native_session_resume is false for CLI-based resume
  • Check executor_running table has the correct resume_token value
  • For Claude Code: the session ID from system message should be stored as resume token