ollie/cmd/olliesrv/internal/fs/support.go

191 lines
4.7 KiB
Go

package fs
// Shared utilities for the 9P namespace handlers.
import (
"bytes"
"fmt"
"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 <context>...</context> 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 <context> block start/end
if bytes.Equal(bytes.TrimSpace(trimmed), []byte("<context>")) {
inContext = true
continue
}
if bytes.Equal(bytes.TrimSpace(trimmed), []byte("</context>")) {
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
}