// Package lifecycle manages agent instance lifecycles including tracking, // state transitions, and cleanup. package lifecycle import ( "os" "github.com/kandev/kandev/internal/agent/agents" ) // CommandBuilder builds agent commands from agent configuration. // Delegates to the Agent interface's BuildCommand method. type CommandBuilder struct{} // NewCommandBuilder creates a new CommandBuilder func NewCommandBuilder() *CommandBuilder { return &CommandBuilder{} } // BuildCommand builds a Command from agent config and options. Delegates to // the Agent.BuildCommand method and then appends the user-configured CLI // flag tokens so every agent participates in the cli_flags feature without // each BuildCommand needing to remember to opt in. func (cb *CommandBuilder) BuildCommand(ag agents.Agent, opts agents.CommandOptions) agents.Command { cmd := ag.BuildCommand(opts) if len(opts.CLIFlagTokens) > 0 { cmd = cmd.With().Flag(opts.CLIFlagTokens...).Build() } return prependCommandPrefix(cmd, opts.CommandPrefixTokens) } // prependCommandPrefix wraps the built command in a launcher prefix so the // agent subprocess runs under a sandbox (e.g. ["greywall", "--"] turns // "npx -y " into "greywall -- npx -y "). Returns cmd unchanged when // there is no prefix. func prependCommandPrefix(cmd agents.Command, prefixTokens []string) agents.Command { if len(prefixTokens) == 0 { return cmd } args := make([]string, 0, len(prefixTokens)+len(cmd.Args())) args = append(args, prefixTokens...) args = append(args, cmd.Args()...) return agents.NewCommand(args...) } // BuildCommandArgs builds the executable argv for an agent launch. func (cb *CommandBuilder) BuildCommandArgs(ag agents.Agent, opts agents.CommandOptions) []string { return cb.BuildCommand(ag, opts).Args() } // BuildContinueCommandArgs builds argv for a one-shot follow-up command. func (cb *CommandBuilder) BuildContinueCommandArgs(ag agents.Agent, opts agents.CommandOptions) []string { sessionCfg := ag.Runtime().SessionConfig if sessionCfg.ContinueSessionCmd.IsEmpty() { return nil } // Start from the continue command base and apply the same flags as BuildCommand cmd := sessionCfg.ContinueSessionCmd.With(). Model(ag.Runtime().ModelFlag, opts.Model). Settings(ag.PermissionSettings(), opts.PermissionValues). Flag(opts.CLIFlagTokens...). Build() cmd = prependCommandPrefix(cmd, opts.CommandPrefixTokens) return cmd.Args() } // ExpandSessionDir resolves the host-side directory that should be bind- // mounted into the container at SessionDirTarget. The path is the kandev- // managed per-container session dir (~/.kandev/agent-sessions// // ) — isolated from the user's actual ~/ so the host's stale // state DBs and session caches stay out of the container. // // Returns empty string if no session directory is configured or if kandev // home / instance ID are unavailable. Production callers always supply a // resolved kandev home, so the empty-string path only fires in tests. func (cb *CommandBuilder) ExpandSessionDir(ag agents.Agent, kandevHomeDir, instanceID string) string { template := ag.Runtime().SessionConfig.SessionDirTemplate if template == "" { return "" } path := SessionDirHostPath(kandevHomeDir, instanceID, template) if path == "" { return "" } _ = os.MkdirAll(path, 0o755) return path } // GetSessionDirTarget returns the container path for session directory mount. // Returns empty string if no session directory is configured. func (cb *CommandBuilder) GetSessionDirTarget(ag agents.Agent) string { return ag.Runtime().SessionConfig.SessionDirTarget }