Skip to content

Latest commit

 

History

History
243 lines (181 loc) · 11 KB

File metadata and controls

243 lines (181 loc) · 11 KB

Adding a New Agent CLI Integration

This guide explains how to add support for a new coding agent CLI to Kandev.

Agents are defined entirely in Go. The backend discovers them through a registry, and the frontend picks them up automatically via the API - no frontend changes are needed.

For a clean reference implementation, see gemini.go (ACP protocol). For a CLI-only TUI agent, see the TUIAgent declarative type in tui_agent.go.


CLI-Only Agent (Passthrough Only) - Recommended for TUI tools

If an agent has a CLI but no structured protocol (no JSON-RPC, no streaming API), use the TUIAgent declarative type. The agent runs in a terminal session and input/output passes through a WebSocket - users interact with the full agent TUI directly.

This is the simplest path. A complete agent definition is ~20 lines.

Step 1: Create the agent file

Create apps/backend/internal/agent/agents/{agent_id}.go:

package agents

func NewClaude() *TUIAgent {
    return NewTUIAgent(TUIAgentConfig{
        AgentID:   "claude",
        AgentName: "Claude",
        Command:   "claude",
        Desc:      "Claude Code - an agentic coding tool by Anthropic.",

        // Optional: let users select a model in profile settings.
        // The {model} placeholder is replaced with the user's chosen model at launch.
        // Omit this if the CLI doesn't accept a model flag.
        ModelFlag: NewParam("--model", "{model}"),
    })
}

Logos are optional - the UI shows a blank placeholder when none is provided. To add them, embed SVGs and pass LogoLight / LogoDark:

package agents

import _ "embed"

//go:embed logos/claude_light.svg
var claudeLogoLight []byte

//go:embed logos/claude_dark.svg
var claudeLogoDark []byte

func NewClaude() *TUIAgent {
    return NewTUIAgent(TUIAgentConfig{
        AgentID:   "claude",
        AgentName: "Claude",
        Command:   "claude",
        Desc:      "Claude Code - an agentic coding tool by Anthropic.",
        ModelFlag: NewParam("--model", "{model}"),
        LogoLight: claudeLogoLight,
        LogoDark:  claudeLogoDark,
    })
}

TUIAgentConfig fields (all optional fields have sensible defaults):

Field Required Description
AgentID Yes Unique lowercase identifier (e.g. "claude")
AgentName Yes Display name (e.g. "Claude")
Command Yes Binary name to run (e.g. "claude")
Desc Yes Description shown in the UI
ModelFlag No Model CLI flag, e.g. NewParam("--model", "{model}"). Lets users pick a model in profile settings. Omit if the CLI doesn't support model selection.
CommandArgs No Extra args appended after Command (e.g. []string{"--debug"})
WaitForTerm No When true, delays process start until the terminal WebSocket connects and sends its first resize event. Use this for TUI apps that need accurate terminal dimensions on startup (e.g. ncurses-based UIs). Most agents don't need this. Defaults to false.
LogoLight / LogoDark No Embedded SVG bytes for light/dark themes. The UI shows a blank placeholder if omitted.
DetectOpts No Custom detection options. Defaults to WithCommand(Command) which checks if the binary is in $PATH.

Step 2: Register the agent

Add your agent to LoadDefaults() in apps/backend/internal/agent/registry/registry.go:

func (r *Registry) LoadDefaults() {
    all := []agents.Agent{
        // ... existing agents ...
        agents.NewMyAgent(), // add here
    }
    // ...
}

Step 3: Build and test

make -C apps/backend build && make -C apps/backend test && make -C apps/backend lint

Note: passthrough-only agents provide terminal interaction only. There are no structured chat messages, tool call visibility, or progress tracking in the chat UI.


Adding a New Agent (Full Integration)

For agents with a structured protocol (JSON-RPC, streaming JSON, etc.), implement the full Agent interface.

Step 1: Create the agent definition file

Create apps/backend/internal/agent/agents/{agent_id}.go.

This file implements the Agent interface. See the interface definition in agent.go for the full contract.

Embed StandardPassthrough to get CLI passthrough support for free:

type MyAgent struct {
    StandardPassthrough
}

func NewMyAgent() *MyAgent {
    return &MyAgent{
        StandardPassthrough: StandardPassthrough{
            PermSettings: myAgentPermSettings,
            Cfg: PassthroughConfig{
                Supported:      true,
                Label:          "CLI Passthrough",
                Description:    "Show terminal directly instead of chat interface",
                PassthroughCmd: NewCommand("myagent"),
                ModelFlag:      NewParam("--model", "{model}"),
                PromptFlag:     NewParam("--prompt", "{prompt}"),
                IdleTimeout:    3 * time.Second,
                BufferMaxBytes: DefaultBufferMaxBytes,
            },
        },
    }
}

Key implementation notes:

  • BuildCommand() returns the CLI command for structured protocol mode (the agent communicates via stdin/stdout with a protocol adapter). Include the protocol flag here (e.g. --acp, --stream-json).
  • Runtime() returns configuration used when running the agent in Docker or as a standalone process. Set Protocol to tell the adapter factory which transport to use.
  • IsInstalled() uses the Detect() helper with WithFileExists() to check for known installation paths. Set SupportsMCP and MCPConfigPaths on the result.
  • MCP servers in passthrough — set MCPStrategy on PassthroughConfig so the agent receives kandev's own tools server (plus any profile-configured MCP servers) when run in passthrough mode. Omitting it means no MCP servers are injected in passthrough. Pick the strategy that matches how your CLI loads MCP servers: mcpconfig.ClaudeStrategy{} (temp JSON file + --mcp-config flag), mcpconfig.CodexStrategy{} (repeated -c mcp_servers.… overrides), mcpconfig.CursorStrategy{} (project-local .cursor/mcp.json), mcpconfig.PiStrategy{} (project-local .pi/mcp.json), or mcpconfig.OpenCodeStrategy{} (temp JSON + OPENCODE_CONFIG env var). To add a strategy for a CLI with a different mechanism, implement mcpconfig.PassthroughMCPStrategy. See ADR 0014, ADR 0020, and internal/agent/mcpconfig/passthrough.go.
  • MCP project files in protocol mode — set RuntimeConfig.ProjectMCPStrategy only when the structured protocol adapter does not pass ACP session/new MCP servers through to the underlying CLI. Pi uses this to write .pi/mcp.json before the subprocess starts.
  • ListModels() can return a static list or dynamically query the CLI. Use execAndParse() from helpers.go for dynamic model discovery.
  • Use Cmd() builder for constructing commands fluently: Cmd("myagent", "--flag").Model(...).Settings(...).Build().

Step 2: Add logos (optional)

Logos are optional - the UI shows a blank placeholder when none is provided. To add them, place SVGs in:

apps/backend/internal/agent/agents/logos/{agent_id}_light.svg
apps/backend/internal/agent/agents/logos/{agent_id}_dark.svg

Embed them in your agent file:

//go:embed logos/myagent_light.svg
var myagentLogoLight []byte

//go:embed logos/myagent_dark.svg
var myagentLogoDark []byte

Step 3: Register the agent

Add your agent to LoadDefaults() in apps/backend/internal/agent/registry/registry.go:

func (r *Registry) LoadDefaults() {
    all := []agents.Agent{
        // ... existing agents ...
        agents.NewMyAgent(), // add here
    }
    // ...
}

That's it for registration. The frontend discovers agents through the API automatically.

Step 4: Choose a protocol

Check apps/backend/pkg/agent/protocol.go for available protocols:

Protocol Constant Transport Used By
ACP ProtocolACP JSON-RPC 2.0 over stdin/stdout Gemini, Auggie
stream-json ProtocolClaudeCode Streaming JSON over stdin/stdout Claude Code
Codex ProtocolCodex JSON-RPC variant over stdin/stdout Codex
OpenCode ProtocolOpenCode REST/SSE over HTTP OpenCode
Copilot ProtocolCopilot Go SDK (JSON-RPC internally) GitHub Copilot
Amp ProtocolAmp Streaming JSON over stdin/stdout Amp

If an existing protocol fits your agent, reuse it - set Protocol in Runtime(). The adapter factory in factory.go selects the right transport automatically based on this value.

If you need a new protocol, you'll need to:

  1. Add a new Protocol constant in apps/backend/pkg/agent/protocol.go
  2. Create a transport adapter in apps/backend/internal/agentctl/server/adapter/transport/{protocol}/
  3. Add the case to the factory switch in apps/backend/internal/agentctl/server/adapter/factory.go

Step 5: Build and test

make -C apps/backend build && make -C apps/backend test && make -C apps/backend lint

Start the dev server and verify:

  • The agent appears in the agent selector in the UI
  • Agent execution works (start a task, send a prompt)
  • Passthrough mode works (toggle to CLI passthrough in the session)
  • Model listing works

Updating an Existing Agent

Change Where
Update CLI version BuildCommand() and Runtime().Cmd in the agent file
Add/remove models ListModels() - update static list or dynamic command
Update permissions PermissionSettings() variable and PassthroughConfig
Update Docker image Runtime() - change Image/Tag fields
Change protocol Runtime().Protocol in the agent file

File Reference

File When to modify
apps/backend/internal/agent/agents/{agent_id}.go Always - agent definition
apps/backend/internal/agent/agents/logos/{agent_id}_{light,dark}.svg Optional - agent logos (UI shows blank placeholder if omitted)
apps/backend/internal/agent/registry/registry.go Always - register in LoadDefaults()
apps/backend/internal/agent/agents/tui_agent.go Never - provides TUIAgent declarative type (read-only reference)
apps/backend/pkg/agent/protocol.go Only if adding a new protocol
apps/backend/internal/agentctl/server/adapter/factory.go Only if adding a new protocol
apps/backend/internal/agentctl/server/adapter/transport/{proto}/ Only if adding a new protocol
apps/backend/internal/agent/agents/passthrough.go Never - provides StandardPassthrough (read-only reference)
apps/backend/internal/agent/agents/agent.go Never - defines the Agent interface (read-only reference)