Add an Agent CLI
Add a new ACP adapter or agent CLI integration to Kandev.
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 lintNote: 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 or bridge command for structured protocol mode. It must start an ACP-speaking process; include the CLI's ACP flag when one is required.Runtime()returns configuration used when running the agent in Docker or as a standalone process. Full integrations setProtocol: agent.ProtocolACP; the adapter factory only accepts ACP.IsInstalled()uses theDetect()helper withWithFileExists()to check for known installation paths. SetSupportsMCPandMCPConfigPathson the result.- MCP servers in passthrough — set
MCPStrategyonPassthroughConfigso 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-configflag),mcpconfig.CodexStrategy{}(repeated-c mcp_servers.…overrides),mcpconfig.CursorStrategy{}(project-local.cursor/mcp.json),mcpconfig.PiStrategy{}(project-local.pi/mcp.json), ormcpconfig.OpenCodeStrategy{}(temp JSON +OPENCODE_CONFIGenv var). To add a strategy for a CLI with a different mechanism, implementmcpconfig.PassthroughMCPStrategy. See ADR 0014, ADR 0020, andinternal/agent/mcpconfig/passthrough.go. - MCP project files in protocol mode — set
RuntimeConfig.ProjectMCPStrategyonly when the structured protocol adapter does not pass ACPsession/newMCP servers through to the underlying CLI. Pi uses this to write.pi/mcp.jsonbefore the subprocess starts. - Models and modes are discovered from the ACP server during host-utility probing. Agent definitions do not maintain a separate static model list.
- 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.svgEmbed them in your agent file:
//go:embed logos/myagent_light.svg
var myagentLogoLight []byte
//go:embed logos/myagent_dark.svg
var myagentLogoDark []byteStep 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: Understand ACP, REST, and MCP
These names describe separate boundaries; they are not interchangeable agent runtime options:
| Boundary | Role in Kandev |
|---|---|
| ACP | The only structured agent runtime protocol accepted by the agentctl adapter factory. Set Runtime().Protocol to agent.ProtocolACP. ACP adapters can wrap CLIs whose internals use other transports. |
| REST | Local HTTP control APIs exposed by agentctl for operations such as Git, processes, and workspace state. REST is not selected as an agent Protocol. |
| MCP | The tool protocol used to give agents Kandev and profile-configured tools. MCP servers are passed through ACP session/new when supported or materialized by MCPStrategy / ProjectMCPStrategy. MCP is not an agent runtime adapter. |
To integrate a structured agent, provide or reuse an ACP server/bridge and configure agent.ProtocolACP. The adapter factory in factory.go rejects non-ACP runtime protocols. If a CLI has no ACP server or bridge, integrate it as a passthrough-only TUIAgent instead of adding a protocol constant.
Step 5: Build and test
make -C apps/backend build && make -C apps/backend test && make -C apps/backend lintStart 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 |
| Change model or mode discovery | Update the ACP server/bridge capability response; Kandev learns these during host-utility probing |
| Update permissions | PermissionSettings() variable and PassthroughConfig |
| Update Docker image | Runtime() - change Image/Tag fields |
| Change structured runtime | Keep Runtime().Protocol on ACP and update the ACP server/bridge command |
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 | Read-only reference for the ACP runtime protocol value |
apps/backend/internal/agentctl/server/adapter/factory.go | Read-only reference for ACP adapter construction |
apps/backend/internal/agentctl/server/adapter/transport/acp/adapter.go | ACP transport implementation and bridge behavior |
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) |