package fs // Shared utilities for the 9P namespace handlers. import ( "bytes" "fmt" "os" "strings" "sync" "ollie/cmd/olliesrv/internal/agent" "ollie/cmd/olliesrv/internal/backend" "ollie/cmd/olliesrv/internal/session" "ollie/toolsrv/protocol" "ollie/virtfs" ) // wireAgentEvents sets the state change callback on an agent. func wireAgentEvents(sessID string, ag *agent.Agent) { ag.SetOnStateChange(func(agentID, state string) { session.PublishEvent("session."+sessID+".agent."+agentID+".state", state) }) } // dispatch routes a command to the matching handler based on the first word. func dispatch(handlers map[string]func([]string) ([]byte, error), data []byte) ([]byte, error) { input := strings.TrimSpace(string(data)) if input == "" { return nil, nil } parts := strings.Fields(input) command := parts[0] args := parts[1:] if name, value, ok := strings.Cut(command, "="); ok { command = name args = append([]string{value}, args...) } h, ok := handlers[command] if !ok { return nil, fmt.Errorf("unknown ctl command: %s", command) } return h(args) } // stripMarkers removes [[[...]]] marker lines, source fence delimiter lines, // and ... blocks from raw chat data. Content inside fences // is preserved — only the ``` lines themselves are stripped. func stripMarkers(data []byte) []byte { var out []byte inContext := false for len(data) > 0 { nl := bytes.IndexByte(data, '\n') if nl < 0 { if !inContext { out = append(out, data...) } break } line := data[:nl+1] data = data[nl+1:] trimmed := bytes.TrimRight(line, "\n") // Handle block start/end if bytes.Equal(bytes.TrimSpace(trimmed), []byte("")) { inContext = true continue } if bytes.Equal(bytes.TrimSpace(trimmed), []byte("")) { inContext = false continue } if inContext { continue } if len(trimmed) >= 7 && bytes.HasPrefix(trimmed, []byte("[[[")) && bytes.HasSuffix(trimmed, []byte("]]]")) { continue } stripped := bytes.TrimSpace(trimmed) if bytes.HasPrefix(stripped, []byte("```")) && !bytes.HasPrefix(stripped, []byte("````")) { continue } out = append(out, line...) } return out } // helpSpec stores the spec for help generation. Set during tree build. var helpSpec *virtfs.FsNodeDecl var ( helpOnce sync.Once helpText string helpFn = func() string { helpOnce.Do(func() { if helpSpec != nil { helpText = virtfs.GenerateHelp(*helpSpec) } }) return helpText } ) // agentNewRequest holds parsed results from an agent/new payload. type agentNewRequest struct { Params session.AgentParams Prompt string ParentID string ForkAt int Timeout int // seconds; 0 = use default (600) MaxDepth int // max sub-agent depth; 0 = use default (1) MaxParallel *int // max concurrent children; nil = default (5), -1 = unlimited, 0 = disabled } // parseAgentNewRequest parses agent/new payloads in both formats: // - Newline-separated key=value (new format, supports multi-line values via \n escape) // - Space-separated key=value (legacy format from GUI) // // A bare word (no =) is treated as the profile name (legacy positional arg). func parseAgentNewRequest(data []byte) agentNewRequest { s := strings.TrimSpace(string(data)) if s == "" { return agentNewRequest{Params: session.AgentParams{Profile: "default"}} } var tokens []string if strings.Contains(s, "\n") { tokens = strings.Split(s, "\n") } else { tokens = strings.Fields(s) } r := agentNewRequest{Params: session.AgentParams{Profile: "default"}} for _, tok := range tokens { tok = strings.TrimSpace(tok) if tok == "" { continue } if k, v, ok := strings.Cut(tok, "="); ok { v = protocol.UnescapeValue(v) switch k { case "backend": r.Params.Backend = v case "model": r.Params.Model = v case "name": r.Params.Name = v case "remote": r.Params.Remote = v case "profile": r.Params.Profile = v case "prompt": r.Prompt = v case "parent": r.ParentID = v case "fork_at": fmt.Sscanf(v, "%d", &r.ForkAt) case "timeout": fmt.Sscanf(v, "%d", &r.Timeout) case "max_depth": fmt.Sscanf(v, "%d", &r.MaxDepth) case "max_parallel": var v2 int if _, err := fmt.Sscanf(v, "%d", &v2); err == nil { r.MaxParallel = &v2 } } } else if r.Params.Profile == "default" { r.Params.Profile = tok } } return r } // messagesUpToTurn returns messages up to (and including) the Nth user turn. // Turn 1 = first user message and everything before the second user message. func messagesUpToTurn(msgs []backend.Message, n int) []backend.Message { userCount := 0 for i, m := range msgs { if m.Role == "user" { userCount++ if userCount > n { return msgs[:i] } } } return msgs } // agentCwd returns a reasonable CWD for a new agent in a session. // Uses the first agent's CWD, or falls back to os.Getwd(). func agentCwd(s *session.Session) string { for _, ag := range s.Agents() { if c := ag.Cwd(); c != "" { return c } } wd, _ := os.Getwd() return wd }