package agent import ( "context" "encoding/json" "fmt" "os" "slices" "strconv" "strings" ) // HandleCommand processes agent-level slash commands. Returns true if the // input was recognized as a command, false otherwise. // The emit function is used to send output events. sessionID and agentsDir // are passed from the session to avoid the agent needing session-level knowledge. func (ag *Agent) HandleCommand(ctx context.Context, input string) bool { if !strings.HasPrefix(input, "/") { return false } parts := strings.Fields(input) if len(parts) == 0 { return false } cmd := parts[0] args := parts[1:] switch cmd { case "/i": ag.cmdInject(ctx, args) case "/irw": ag.cmdInjectRewrite(ctx, args) case "/backend": ag.cmdBackend(ctx, args) case "/models": ag.cmdModels(ctx, args) case "/model": ag.cmdModel(ctx, args) case "/maxsteps": ag.cmdMaxSteps(ctx, args) case "/agents": ag.cmdAgents(ctx, args) case "/agent": ag.cmdAgent(ctx, args) case "/compact": ag.cmdCompact(ctx, args) case "/context": ag.cmdContext(ctx, args) case "/cost": ag.cmdCost(ctx, args) case "/usage": ag.cmdUsage(ctx, args) case "/history": ag.cmdHistory(ctx, args) case "/clear": ag.cmdClear(ctx, args) case "/sp": ag.cmdSP(ctx, args) default: return false } return true } func (ag *Agent) cmdBackend(_ context.Context, args []string) { if len(args) == 0 { ag.emit(infoEvent(ag.runtime.Backend.Name())) return } if ag.IsRunning() { ag.emit(infoEvent("error: cannot switch backend while agent is running")) return } be, err := ag.newBackend(args[0]) if err != nil { ag.emit(infoEvent(fmt.Sprintf("error: failed to switch backend: %v", err))) return } ag.runtime.Backend = be ag.emit(infoEvent(fmt.Sprintf("switched backend to: %s (model: %s)", be.Name(), be.Model()))) } func (ag *Agent) cmdModels(ctx context.Context, args []string) { models := ag.runtime.Backend.Models(ctx) if len(models) == 0 { ag.emit(infoEvent("no models available")) return } slices.Sort(models) current := ag.runtime.Backend.Model() for _, m := range models { marker := " " if m == current { marker = "* " } ag.emit(infoEvent(marker + m)) } } func (ag *Agent) cmdModel(_ context.Context, args []string) { if len(args) == 0 { ag.emit(infoEvent(ag.runtime.Backend.Model())) return } ag.runtime.Backend.SetModel(args[0]) ag.emit(infoEvent("switched model to: " + args[0])) } func (ag *Agent) cmdMaxSteps(_ context.Context, args []string) { if len(args) == 0 { if ag.runtime.MaxSteps == 0 { ag.emit(infoEvent("maxsteps: unlimited")) } else { ag.emit(infoEvent(fmt.Sprintf("maxsteps: %d", ag.runtime.MaxSteps))) } return } n, err := strconv.Atoi(args[0]) if err != nil || n < 0 { ag.emit(infoEvent("error: maxsteps requires a non-negative integer (0 = unlimited)")) return } ag.runtime.MaxSteps = n if n == 0 { ag.emit(infoEvent("maxsteps: unlimited")) } else { ag.emit(infoEvent(fmt.Sprintf("maxsteps: %d", n))) } } func (ag *Agent) cmdAgents(_ context.Context, _ []string) { seen := make(map[string]bool) found := false for _, dir := range AgentsDirs() { entries, err := os.ReadDir(dir) if err != nil { continue } for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { continue } name := strings.TrimSuffix(e.Name(), ".json") if seen[name] { continue } seen[name] = true marker := " " if name == ag.agentName { marker = "* " } ag.emit(infoEvent(marker + name)) found = true } } if !found { ag.emit(infoEvent("no agents found")) } } func (ag *Agent) cmdAgent(_ context.Context, args []string) { if len(args) == 0 { ag.emit(infoEvent("active agent: " + ag.agentName)) return } if ag.IsRunning() { ag.emit(infoEvent("error: cannot switch agent while agent is running")) return } name := args[0] cfgPath := AgentConfigPath(ag.agentsDir, name) f, err := os.Open(cfgPath) if err != nil { ag.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err))) return } cfg, err := Load(f) f.Close() if err != nil { ag.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err))) return } disp := ag.newToolServer() env := []string{"OLLIE_SESSION_ID=" + ag.sessionID, "OLLIE_UNAME=" + ag.id} env = append(env, ag.promptEnvExtra...) rt := BuildRuntime(cfg, disp, ag.cwd, env, ag.baseLayers...) if rt.CfgBackend != "" { newBe, err := ag.newBackend(rt.CfgBackend) if err != nil { ag.emit(infoEvent(fmt.Sprintf("error: backend %q: %v", rt.CfgBackend, err))) return } if rt.CfgModel != "" { newBe.SetModel(rt.CfgModel) } rt.Backend = newBe } else { rt.Backend = ag.runtime.Backend if rt.CfgModel != "" { rt.Backend.SetModel(rt.CfgModel) } } ag.runtime = rt ag.agentName = name ag.history = nil ag.wireToolsChanged() ag.notifyChange() for _, msg := range rt.Messages { ag.emit(infoEvent(msg)) } ag.emit(infoEvent("agent: " + name)) } func (ag *Agent) cmdCompact(ctx context.Context, _ []string) { if ag.IsRunning() { ag.emit(infoEvent("error: cannot compact while agent is running")) return } if ag.history == nil { ag.emit(infoEvent("nothing to compact")) return } ag.SetState("compacting") n, err := ag.runCompact(ctx, "manual") ag.SetState("idle") if err != nil { ag.emit(infoEvent("compact error: " + err.Error())) return } if n == 0 { ag.emit(infoEvent("nothing to compact")) return } ag.emit(infoEvent(fmt.Sprintf("compacted %d messages", n))) ag.saveSession() } func (ag *Agent) cmdContext(ctx context.Context, _ []string) { if ag.history == nil { ag.emit(infoEvent("no active session")) return } ctxLen := ag.runtime.Backend.ContextLength(ctx) if ctxLen <= 0 { ctxLen = defaultContextLength } estimated := ag.history.estimateTokens() pct := estimated * 100 / ctxLen ag.emit(infoEvent(fmt.Sprintf("~%d / %d tokens (%d%%)", estimated, ctxLen, pct))) ag.emit(infoEvent(strings.TrimRight(ag.history.contextDebug(), "\n"))) } func (ag *Agent) cmdCost(_ context.Context, _ []string) { if ag.history == nil { ag.emit(infoEvent("no active session")) return } ag.emit(infoEvent(fmt.Sprintf("last=$%.4f session=$%.4f", ag.history.LastTurnCostUSD, ag.history.SessionCostUSD))) } func (ag *Agent) cmdUsage(ctx context.Context, _ []string) { if ag.history == nil { ag.emit(infoEvent("no active session")) return } ctxLen := ag.runtime.Backend.ContextLength(ctx) if ctxLen <= 0 { ctxLen = defaultContextLength } estimated := ag.history.estimateTokens() pct := estimated * 100 / ctxLen usageStr := fmt.Sprintf("~%d / %d tokens (%d%%) | %d in, %d out, %d requests", estimated, ctxLen, pct, ag.history.TotalInputTokens, ag.history.TotalOutputTokens, ag.history.TotalRequests) if ag.history.Estimated { usageStr += " [estimated]" } ag.emit(infoEvent(usageStr)) } func (ag *Agent) cmdHistory(_ context.Context, _ []string) { if ag.history == nil { ag.emit(infoEvent("no active session")) return } for _, msg := range ag.history.history() { preview := msg.Content if len(preview) > 200 { preview = preview[:200] + "..." } ag.emit(infoEvent(fmt.Sprintf("[%s] %s", msg.Role, preview))) } } func (ag *Agent) cmdClear(_ context.Context, _ []string) { if ag.IsRunning() { ag.emit(infoEvent("error: cannot clear while agent is running")) return } ag.history = nil ag.emit(infoEvent("cleared")) } func (ag *Agent) cmdSP(_ context.Context, _ []string) { ag.emit(infoEvent(ag.runtime.Preamble)) } func (ag *Agent) cmdInject(_ context.Context, args []string) { prompt := strings.Join(args, " ") if prompt == "" { ag.emit(infoEvent("error: /i requires a prompt")) return } if ag.IsRunning() { ag.Inject(prompt) } else { go ag.Submit(context.Background(), prompt) } } func (ag *Agent) cmdInjectRewrite(_ context.Context, args []string) { prompt := strings.Join(args, " ") if prompt == "" { ag.emit(infoEvent("error: /irw requires a prompt")) return } ag.InjectRewrite(prompt) } // Inject queues a prompt for mid-turn injection. func (ag *Agent) Inject(prompt string) { if !ag.pendingInject.CompareAndSwap(nil, &prompt) { ag.fifo.Push(prompt) return } ag.emit(Event{Role: "info", Content: "\n"}) ag.emit(Event{Role: "user", Content: prompt}) } // InjectRewrite replaces the pending inject. func (ag *Agent) InjectRewrite(prompt string) { ag.pendingInject.Store(&prompt) ag.emit(Event{Role: "info", Content: "\n"}) ag.emit(Event{Role: "user", Content: prompt}) } // CompactionSnapshot returns the pre-compaction snapshot for external persistence. // Returns nil if no history exists. func (ag *Agent) CompactionSnapshot() json.RawMessage { if ag.history == nil { return nil } snap := ag.history.PreCompactionSnapshot() data, err := json.Marshal(snap) if err != nil { return nil } return data }