From 0b3d04c17c4e4c45f3a1e80f949d2dbe3c0a2d47 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 29 Jul 2026 21:52:56 +0200 Subject: [PATCH] session: remove command dispatch from Submit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Session.Submit now forwards directly to Agent.Submit. Slash commands are agent-level concerns handled by Agent.HandleCommand. Session-level operations (save, resume, kill, rename, cwd) are ctl verbs issued via the ctl file — they are NOT slash commands and do not flow through the prompt path. This eliminates the artificial session/agent command routing and makes the prompt path clean: everything in the prompt goes to the agent. --- agent/commands.go | 8 +- session/commands.go | 201 -------------------------------------------- session/session.go | 8 +- 3 files changed, 5 insertions(+), 212 deletions(-) delete mode 100644 session/commands.go diff --git a/agent/commands.go b/agent/commands.go index cc7f289..5922caf 100644 --- a/agent/commands.go +++ b/agent/commands.go @@ -180,14 +180,10 @@ func (ag *Agent) cmdAgent(_ context.Context, args []string) { ag.emit(infoEvent(fmt.Sprintf("error: agent %q: %v", name, err))) return } - d := ag.newDispatcher() - if d == nil { - ag.emit(infoEvent("error: no dispatcher configured")) - return - } + disp := ag.newDispatcher() env := []string{"OLLIE_SESSION_ID=" + ag.sessionID, "OLLIE_UNAME=" + ag.id} env = append(env, ag.promptEnvExtra...) - rt := BuildRuntime(cfg, d, ag.cwd, env, ag.baseLayers...) + rt := BuildRuntime(cfg, disp, ag.cwd, env, ag.baseLayers...) if rt.CfgBackend != "" { newBe, err := ag.newBackend(rt.CfgBackend) if err != nil { diff --git a/session/commands.go b/session/commands.go deleted file mode 100644 index ff473ec..0000000 --- a/session/commands.go +++ /dev/null @@ -1,201 +0,0 @@ -package session - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - - "ollie/agent" -) - -func (a *Session) 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:] - - // Try agent-level commands first (they access agent internals directly). - if a.r.HandleCommand(ctx, input) { - return true - } - - listFromHandler := func(name string) { - if h := a.listHandlers[name]; h != nil { - for _, item := range h() { - a.emit(agent.Event{Role: "info", Content: " " + item + "\n"}) - } - } - } - - type cmdFn func([]string) - cmds := map[string]cmdFn{ - "/sessions": func(args []string) { - allFlag := len(args) > 0 && args[0] == "-a" - type sessionFile struct { - id string - path string - } - var files []sessionFile - appendSessionFiles := func(dir string) { - entries, err := os.ReadDir(dir) - if err != nil { - return - } - for i := len(entries) - 1; i >= 0; i-- { - e := entries[i] - if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { - continue - } - files = append(files, sessionFile{ - id: strings.TrimSuffix(e.Name(), ".json"), - path: filepath.Join(dir, e.Name()), - }) - } - } - appendSessionFiles(a.sessionsDir) - appendSessionFiles(filepath.Join(a.sessionsDir, "active")) - - cwd := a.CWD() - found := false - for _, file := range files { - data, readErr := os.ReadFile(file.path) - if readErr != nil { - continue - } - var ps agent.PersistedAgent - if json.Unmarshal(data, &ps) != nil { - continue - } - if !allFlag && ps.CWD != "" && ps.CWD != cwd { - continue - } - marker := " " - if file.id == a.id { - marker = "* " - } - goal := "" - for _, msg := range ps.Messages { - if msg.Role == "user" { - goal = msg.Content - break - } - } - if len(goal) > 60 { - goal = goal[:60] + "..." - } - a.emit(agent.Event{Role: "info", Content: marker + fmt.Sprintf("%-24s [%s] %q", file.id, ps.Agent, goal) + "\n"}) - found = true - } - if !found { - a.emit(agent.Event{Role: "info", Content: "no sessions for " + cwd + "\n"}) - } - }, - - "/save": func(args []string) { - if !a.r.HasHistory() { - a.emit(agent.Event{Role: "info", Content: "error: no active session\n"}) - return - } - if len(args) == 0 { - a.emit(agent.Event{Role: "info", Content: "error: /save requires a name\n"}) - return - } - name := args[0] - path := a.sessionsDir + "/" + name + ".json" - if err := a.r.SaveTo(path, name, a.CWD()); err != nil { - a.emit(agent.Event{Role: "info", Content: "error: " + err.Error() + "\n"}) - return - } - a.emit(agent.Event{Role: "info", Content: "saved: " + path + "\n"}) - }, - - "/resume": func(args []string) { - if len(args) == 0 { - a.emit(agent.Event{Role: "info", Content: "error: /resume requires a session id or name\n"}) - return - } - if a.IsRunning() { - a.emit(agent.Event{Role: "info", Content: "error: cannot resume while agent is running\n"}) - return - } - name := args[0] - path := a.sessionsDir + "/" + name + ".json" - data, err := os.ReadFile(path) - if err != nil { - a.emit(agent.Event{Role: "info", Content: fmt.Sprintf("error: %v\n", err)}) - return - } - var ps agent.PersistedAgent - if err := json.Unmarshal(data, &ps); err != nil { - a.emit(agent.Event{Role: "info", Content: fmt.Sprintf("error: %v\n", err)}) - return - } - a.r.Restore(&ps) - a.emit(agent.Event{Role: "info", Content: fmt.Sprintf("resumed session %s (%d messages)\n", name, len(ps.Messages))}) - }, - - "/cwd": func(args []string) { - if len(args) == 0 { - a.emit(agent.Event{Role: "info", Content: "cwd: " + a.CWD() + "\n"}) - return - } - dir := strings.Join(args, " ") - if err := a.SetCWD(dir); err != nil { - a.emit(agent.Event{Role: "info", Content: "error: " + err.Error() + "\n"}) - return - } - a.emit(agent.Event{Role: "info", Content: "cwd: " + dir + "\n"}) - }, - - "/skills": func(args []string) { listFromHandler("skills") }, - "/tools": func(args []string) { listFromHandler("tools") }, - - "/help": func(args []string) { - lines := []string{ - "Available commands:", - " /agents - list available agent configs", - " /sessions [-a] - list saved sessions (current cwd; -a for all)", - " /save - save session as a named checkpoint", - " /resume - resume a saved session", - " /agent [name] - show or switch active agent", - " /backend [type] - show current backend, or switch to ", - " /model [name] - show current model, or switch to ", - " /models - list available models", - " /maxsteps [n] - show or set max tool-call steps (0 = unlimited)", - " /skills - list available skills", - " /tools - list available tools", - " /cwd [path] - show or change working directory", - " /i - inject prompt into the running turn", - " /irw - rewrite the pending inject", - " /compact - summarize conversation and compact context", - " /context - show context size and message breakdown", - " /cost - show last turn and session cost", - " /usage - show token usage and context percentage", - " /history - dump bounded message history", - " /clear - clear session", - " /sp - show rendered system prompt", - " /help - show this help", - } - for _, l := range lines { - a.emit(agent.Event{Role: "info", Content: l + "\n"}) - } - }, - } - - fn, ok := cmds[cmd] - if !ok { - return false - } - a.emit(agent.Event{Role: "info", Content: "\n"}) - fn(args) - return true -} diff --git a/session/session.go b/session/session.go index 8422e3d..1835f70 100644 --- a/session/session.go +++ b/session/session.go @@ -185,15 +185,13 @@ func (a *Session) SetEnv(key, value string) { a.r.SetEnv(key, value) } -// Submit processes user input: dispatches commands, delegates turns to agent. +// Submit forwards user input to the agent for processing. +// Slash commands are handled by the agent; session-level operations +// are issued via the ctl file, not via prompt. func (a *Session) Submit(ctx context.Context, input string) { - a.log.Debug("Submit() input_len=%d running=%v", len(input), a.IsRunning()) if input == "" { return } - if a.handleCommand(ctx, input) { - return - } a.r.Submit(ctx, input) }