216 lines
5.6 KiB
Go
216 lines
5.6 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) {
|
|
topic := "session." + sessID + ".agent." + agentID + ".state"
|
|
session.PublishEvent(topic, state)
|
|
})
|
|
ag.SetOnProcStart(func(agentID string, pid int, tool, cmd string) {
|
|
topic := "session." + sessID + ".agent." + agentID + ".proc.start"
|
|
payload := fmt.Sprintf("%d\t%s\t%s", pid, tool, cmd)
|
|
session.PublishEvent(topic, payload)
|
|
})
|
|
}
|
|
|
|
// ctlCmd is one control verb: its name, a one-line description for the
|
|
// self-describing help listing, and its handler.
|
|
type ctlCmd struct {
|
|
name string
|
|
desc string
|
|
fn func([]string) ([]byte, error)
|
|
}
|
|
|
|
// dispatch routes a ctl write to a command by its first word. Reading the ctl
|
|
// file (empty write) or writing "help" returns the command listing. An unknown
|
|
// verb returns an error listing the valid verbs.
|
|
func dispatch(cmds []ctlCmd, data []byte) ([]byte, error) {
|
|
input := strings.TrimSpace(string(data))
|
|
if input == "" || input == "help" {
|
|
var sb strings.Builder
|
|
for _, c := range cmds {
|
|
fmt.Fprintf(&sb, "%-16s %s\n", c.name, c.desc)
|
|
}
|
|
return []byte(sb.String()), 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...)
|
|
}
|
|
for _, c := range cmds {
|
|
if c.name == command {
|
|
return c.fn(args)
|
|
}
|
|
}
|
|
names := make([]string, len(cmds))
|
|
for i, c := range cmds {
|
|
names[i] = c.name
|
|
}
|
|
return nil, fmt.Errorf("unknown ctl command %q; valid: %s", command, strings.Join(names, ", "))
|
|
}
|
|
|
|
// 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", "agent":
|
|
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
|
|
}
|