Files
kandev/apps/backend/internal/agent/lifecycle/command.go
T

132 lines
4.8 KiB
Go

// Package lifecycle manages agent instance lifecycles including tracking,
// state transitions, and cleanup.
package lifecycle
import (
"os"
"strings"
"github.com/kandev/kandev/internal/agent/registry"
)
// CommandBuilder builds agent commands from registry configuration
type CommandBuilder struct{}
// NewCommandBuilder creates a new CommandBuilder
func NewCommandBuilder() *CommandBuilder {
return &CommandBuilder{}
}
// CommandOptions contains options for building a command
type CommandOptions struct {
Model string // Model to use (appended via ModelFlag if set)
SessionID string // Session ID to resume (appended via SessionConfig.ResumeFlag if not NativeSessionResume)
AutoApprove bool // If true, skip permission flags (auto-approve all tool calls)
// PermissionValues contains the values for each permission setting
// Keys are setting names (e.g., "allow_indexing", "auto_approve")
// Values are the boolean state of each setting
PermissionValues map[string]bool
}
// BuildCommand builds a command slice from agent config and options
// Returns the command as a string slice ready for execution
func (cb *CommandBuilder) BuildCommand(agentConfig *registry.AgentTypeConfig, opts CommandOptions) []string {
// Start with base command from config
cmd := make([]string, len(agentConfig.Cmd))
copy(cmd, agentConfig.Cmd)
// Append model flag if agent supports it and model is specified
// ModelFlag supports {model} placeholder, e.g. "--model {model}" or "-c model=\"{model}\""
if agentConfig.ModelFlag != "" && opts.Model != "" {
expanded := strings.ReplaceAll(agentConfig.ModelFlag, "{model}", opts.Model)
// Split on first space to separate flag from value (if combined)
parts := strings.SplitN(expanded, " ", 2)
cmd = append(cmd, parts...)
}
// Append session resume flag if:
// 1. Session ID is provided
// 2. Agent does NOT use native session loading (uses CLI flag instead)
// 3. Agent has a ResumeFlag configured
// 4. Agent supports session recovery (CanRecover is not false)
if opts.SessionID != "" && !agentConfig.SessionConfig.NativeSessionResume && agentConfig.SessionConfig.ResumeFlag != "" && agentConfig.SessionConfig.SupportsRecovery() {
cmd = append(cmd, agentConfig.SessionConfig.ResumeFlag, opts.SessionID)
}
// Add permission flags when AutoApprove is false
// This makes the agent request permission via ACP for tool calls
permConfig := agentConfig.PermissionConfig
if !opts.AutoApprove && permConfig.PermissionFlag != "" && len(permConfig.ToolsRequiringPermission) > 0 {
for _, tool := range permConfig.ToolsRequiringPermission {
cmd = append(cmd, permConfig.PermissionFlag, tool+":ask-user")
}
}
// Apply permission settings that use CLI flags
// Each permission setting can define its own CLI flag via ApplyMethod="cli_flag"
if permSettings := agentConfig.PermissionSettings; permSettings != nil {
for settingName, setting := range permSettings {
// Skip if not supported or not a CLI flag setting
if !setting.Supported || setting.ApplyMethod != "cli_flag" || setting.CLIFlag == "" {
continue
}
// Get the value for this setting from options
value, exists := opts.PermissionValues[settingName]
if !exists {
continue
}
// Only apply if the setting value is true
if value {
if setting.CLIFlagValue != "" {
// Flag with value: "--flag value"
cmd = append(cmd, setting.CLIFlag, setting.CLIFlagValue)
} else {
// Boolean flag or multiple flags: "--flag" or "--flag1 --flag2 arg"
// Split on spaces to handle multiple flags in one setting
parts := strings.Fields(setting.CLIFlag)
cmd = append(cmd, parts...)
}
}
}
}
return cmd
}
// BuildCommandString builds a command as a single string (for standalone mode)
func (cb *CommandBuilder) BuildCommandString(agentConfig *registry.AgentTypeConfig, opts CommandOptions) string {
cmd := cb.BuildCommand(agentConfig, opts)
return strings.Join(cmd, " ")
}
// ExpandSessionDir expands the session directory template from SessionConfig
// Replaces {home} with the user's home directory
// Returns empty string if no session directory is configured
func (cb *CommandBuilder) ExpandSessionDir(agentConfig *registry.AgentTypeConfig) string {
template := agentConfig.SessionConfig.SessionDirTemplate
if template == "" {
return ""
}
homeDir, err := os.UserHomeDir()
if err != nil {
homeDir = "/tmp"
}
result := strings.ReplaceAll(template, "{home}", homeDir)
// Ensure the directory exists
_ = os.MkdirAll(result, 0755)
return result
}
// GetSessionDirTarget returns the container path for session directory mount
// Returns empty string if no session directory is configured
func (cb *CommandBuilder) GetSessionDirTarget(agentConfig *registry.AgentTypeConfig) string {
return agentConfig.SessionConfig.SessionDirTarget
}