// Package kimi implements the Kimi CLI (Moonshot AI) agent adapter: launching // new non-interactive sessions and resuming sessions when a native Kimi session // id is known. // // Kimi CLI (binary "kimi") is Moonshot AI's terminal-native agentic coding // agent. A new task is run non-interactively with `kimi -p `, which // streams the assistant output to stdout without opening the TUI. Sessions are // resumed by id with `kimi --session `. // // Kimi exposes no native lifecycle/hook system and is not documented as // Claude Code hook-compatible, so this is a Tier C adapter: hook installation // and SessionInfo are intentionally no-ops, and activity is left to the // lifecycle reaper. There is also no documented system-prompt flag, so AO's // system prompt is not injected. Both should be upgraded if/when Kimi adds the // corresponding CLI surface. package kimi import ( "context" "strings" "sync" "github.com/aoagents/agent-orchestrator/backend/internal/adapters" "github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/agentbase" "github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/binaryutil" "github.com/aoagents/agent-orchestrator/backend/internal/ports" ) const adapterID = "kimi" // Plugin is the Kimi CLI agent adapter. It is safe for concurrent use; the // binary path is resolved once and cached under binaryMu. type Plugin struct { agentbase.Base binaryMu sync.Mutex resolvedBinary string } // New returns a ready-to-register Kimi adapter. func New() *Plugin { return &Plugin{} } var _ adapters.Adapter = (*Plugin)(nil) var _ ports.Agent = (*Plugin)(nil) // Manifest returns the adapter's static self-description. func (p *Plugin) Manifest() adapters.Manifest { return adapters.Manifest{ ID: adapterID, Name: "Kimi", Description: "Run Kimi CLI (Moonshot AI) worker sessions.", Version: "0.0.1", Capabilities: []adapters.Capability{ adapters.CapabilityAgent, }, } } // GetLaunchCommand builds the argv to start a new Kimi session: // // kimi -p (non-interactive, default) // kimi [--yolo|--auto] (interactive, no prompt) // // When a prompt is supplied, it is delivered via `-p` (in command), which runs // a single prompt without opening the TUI. Per Kimi docs, `--prompt` cannot be // combined with `--yolo`, `--auto`, or `--plan` -- non-interactive mode already // uses the `auto` permission policy by default, so approval flags would be // rejected at startup. They are only emitted on the (interactive) path with no // prompt. Kimi has no documented system-prompt flag, so cfg.SystemPrompt / // cfg.SystemPromptFile are not injected. func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) (cmd []string, err error) { binary, err := p.kimiBinary(ctx) if err != nil { return nil, err } cmd = []string{binary} if cfg.Prompt != "" { cmd = append(cmd, "-p", cfg.Prompt) return cmd, nil } appendApprovalFlags(&cmd, cfg.Permissions) return cmd, nil } // GetRestoreCommand rebuilds the argv that continues an existing Kimi session // when a native Kimi session id is known: // // kimi --session // // ok is false when no native session id has been captured, so callers fall back // to fresh launch behavior. Per Kimi docs, `--yolo` and `--auto` cannot be // combined with `--session` (or `--continue`) -- resumed sessions inherit the // approval settings of the original session -- so cfg.Permissions is // intentionally ignored here. Kimi has no lifecycle hook for AO to capture the // native session id from yet, so in practice this returns ok=false today. func (p *Plugin) GetRestoreCommand(ctx context.Context, cfg ports.RestoreConfig) (cmd []string, ok bool, err error) { if err := ctx.Err(); err != nil { return nil, false, err } agentSessionID := strings.TrimSpace(cfg.Session.Metadata[ports.MetadataKeyAgentSessionID]) if agentSessionID == "" { return nil, false, nil } binary, err := p.kimiBinary(ctx) if err != nil { return nil, false, err } cmd = []string{binary, "--session", agentSessionID} return cmd, true, nil } // appendApprovalFlags maps AO's permission modes onto Kimi's approval flags // for interactive launches. Per Kimi docs these flags cannot be combined with // `--prompt`, `--session`, or `--continue`, so callers on those paths must // skip this mapping. // // - Default: no flag, deferring to the user's Kimi config/default behavior. // - AcceptEdits / Auto: `--auto` (auto permission mode; approvals handled // automatically). // - BypassPermissions: `-y` (yolo; auto-approve regular tool calls including // file writes and shell execution). func appendApprovalFlags(cmd *[]string, permissions ports.PermissionMode) { switch normalizePermissionMode(permissions) { case ports.PermissionModeDefault: // No flag: defer to the user's Kimi config/default behavior. case ports.PermissionModeAcceptEdits, ports.PermissionModeAuto: *cmd = append(*cmd, "--auto") case ports.PermissionModeBypassPermissions: *cmd = append(*cmd, "-y") } } func normalizePermissionMode(mode ports.PermissionMode) ports.PermissionMode { switch mode { case ports.PermissionModeDefault, ports.PermissionModeAcceptEdits, ports.PermissionModeAuto, ports.PermissionModeBypassPermissions: return mode default: return ports.PermissionModeDefault } } var kimiBinarySpec = binaryutil.BinarySpec{ Label: "kimi", Names: []string{"kimi"}, WinNames: []string{"kimi.cmd", "kimi.exe", "kimi"}, UnixPaths: []string{"/usr/local/bin/kimi", "/opt/homebrew/bin/kimi"}, UnixHomePaths: [][]string{{".local", "bin", "kimi"}, {".cargo", "bin", "kimi"}}, WinPaths: []binaryutil.WinPath{ {Base: binaryutil.WinAppData, Parts: []string{"npm", "kimi.cmd"}}, {Base: binaryutil.WinAppData, Parts: []string{"npm", "kimi.exe"}}, {Base: binaryutil.WinHome, Parts: []string{".local", "bin", "kimi.exe"}}, }, } // ResolveKimiBinary finds the `kimi` binary, searching PATH then common install // locations (the uv tool/curl installer drops it in ~/.local/bin, plus Homebrew // and ~/.cargo/bin). It returns "kimi" as a last resort so callers get the // shell's normal command-not-found behavior if Kimi is absent. func ResolveKimiBinary(ctx context.Context) (string, error) { return binaryutil.ResolveBinary(ctx, kimiBinarySpec) } func (p *Plugin) kimiBinary(ctx context.Context) (string, error) { p.binaryMu.Lock() defer p.binaryMu.Unlock() if p.resolvedBinary != "" { return p.resolvedBinary, nil } binary, err := ResolveKimiBinary(ctx) if err != nil { return "", err } p.resolvedBinary = binary return binary, nil }