package fs // spec.go — the single source of truth for the olliesrv 9P namespace. // Every handler is inline. No archaeological expeditions needed. import ( "context" "encoding/json" "fmt" "os" "os/exec" "os/user" "strconv" "strings" "syscall" "time" "ollie/cmd/olliesrv/internal/agent" "ollie/cmd/olliesrv/internal/backend" "ollie/cmd/olliesrv/internal/metrics" "ollie/cmd/olliesrv/internal/session" "ollie/util" "ollie/virtfs" ) // Type aliases for virtfs types used throughout the package. type ( Tree = virtfs.Tree File = virtfs.File SyntheticFileInfo = virtfs.SyntheticFileInfo ) // Structural permissions. const ( PermChildDir = virtfs.PermChildDir PermIdx = virtfs.PermIdx PermMkdir = virtfs.PermMkdir PermMkdirPrivate = virtfs.PermMkdirPrivate ) var treeSpecUID, treeSpecGID string const asyncWorkLimit = 64 var asyncWorkSlots = make(chan struct{}, asyncWorkLimit) func startAsync(ctx context.Context, fn func()) bool { select { case asyncWorkSlots <- struct{}{}: case <-ctx.Done(): return false default: return false } go func() { defer func() { <-asyncWorkSlots }() fn() }() return true } // runWorkflow executes a workflow script in the background. func runWorkflow(s *session.Session, workflow string) { startAsync(s.Ctx, func() { scriptPath := util.CfgDir() + "/workflows/" + workflow if _, err := os.Stat(scriptPath); err != nil { s.SetGoalStatus("error: workflow not found: " + workflow) return } cwd := s.Cwd() cmd := exec.CommandContext(s.Ctx, scriptPath) cmd.Env = append(os.Environ(), "OLLIE_SESSION_ID="+s.ID, "OLLIE_SESSION_NAME="+s.Name(), "OLLIE_CWD="+cwd, ) cmd.Dir = cwd out, err := cmd.CombinedOutput() if err != nil { msg := strings.TrimSpace(string(out)) if msg == "" { msg = err.Error() } s.SetGoalStatus("error: " + msg) } // Script exited — don't auto-set complete; the conductor agent handles that. }) } // buildTreeSpec constructs the full 9P namespace. func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl { mc := cfg.ModelCache u, _ := user.Current() treeSpecUID = "ollie" treeSpecGID = "agent" if u != nil { treeSpecUID = u.Username } // Event subscription for eventwait (lives for server lifetime). eventRead, eventSignal := session.EventValue(session.SubscribeEvents(cfg.Ctx, "*")) return virtfs.DirNode("/", virtfs.UID(treeSpecUID), virtfs.GID(treeSpecGID), virtfs.FileNode("backends", 0444, virtfs.Doc("Available backend names, one per line"), virtfs.Read(func() ([]byte, error) { return []byte(strings.Join(backend.Backends(), "\n") + "\n"), nil }), ), virtfs.FileNode("help", 0444, virtfs.Doc("Filesystem reference"), virtfs.Read(func() ([]byte, error) { return []byte(helpFn()), nil }), ), virtfs.FileNode("models", 0444, virtfs.Doc("Available models. Format: backendmodel per line."), virtfs.Read(func() ([]byte, error) { if mc != nil { return mc.Get(), nil } return []byte("(no model cache)\n"), nil }), ), virtfs.FileNode("agents", 0444, virtfs.Doc("Available agent profiles, one per line"), virtfs.Read(func() ([]byte, error) { var sb strings.Builder for _, dir := range agent.AgentsDirs() { entries, err := os.ReadDir(dir) if err != nil { continue } for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".json") { sb.WriteString(strings.TrimSuffix(e.Name(), ".json")) sb.WriteByte('\n') } } } return []byte(sb.String()), nil }), ), virtfs.FileNode("workflows", 0444, virtfs.Doc("Available workflows, one per line"), virtfs.Read(func() ([]byte, error) { dir := util.CfgDir() + "/workflows" entries, err := os.ReadDir(dir) if err != nil { return nil, nil } var sb strings.Builder for _, e := range entries { if e.IsDir() { continue } sb.WriteString(e.Name()) sb.WriteByte('\n') } return []byte(sb.String()), nil }), ), virtfs.FileNode("ctl", 0666, virtfs.Doc("Server control. Write: 'invalidate', 'kill'"), virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) { return dispatch(map[string]func([]string) ([]byte, error){ "invalidate": func(_ []string) ([]byte, error) { if mc != nil { mc.Invalidate() } if cfg.Invalidate != nil { cfg.Invalidate() } return []byte("ok\n"), nil }, "kill": func(_ []string) ([]byte, error) { if cfg.Shutdown != nil { cfg.Shutdown() } return []byte("ok\n"), nil }, }, data) }), ), virtfs.FileNode("eventwait", 0444, virtfs.Doc("Blocking read for server events"), virtfs.Read(func() ([]byte, error) { data, _, _ := eventRead() return data, nil }), virtfs.BlockOnce(eventRead, eventSignal), ), virtfs.FileNode("generate", 0666, virtfs.Doc("One-shot LLM generation"), virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) { var req backend.GenerateRequest if err := json.Unmarshal(data, &req); err != nil { req.Prompt = strings.TrimSpace(string(data)) } result, err := backend.Generate(cfg.Ctx, req) if err != nil { return nil, err } return []byte(result + "\n"), nil }), ), virtfs.FileNode("aliases", 0444, virtfs.Doc("Alias table. Format: idpath per line."), virtfs.Read(func() ([]byte, error) { var sb strings.Builder for name, sess := range session.Sessions() { fmt.Fprintf(&sb, "%s\tsession/%s\n", sess.ID, name) for _, ag := range sess.Agents() { fmt.Fprintf(&sb, "%s\tsession/%s/agent/%s\n", ag.ID(), name, ag.Name()) } } return []byte(sb.String()), nil }), ), virtfs.FileNode("stats", 0666, virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) { result, err := metrics.QueryFormat(string(data)) if err != nil { return nil, err } return []byte(result), nil }), ), virtfs.FileNode("metrics", 0444, virtfs.Read(func() ([]byte, error) { a, err := metrics.AggregateAll() if err != nil { return nil, err } return []byte(metrics.Format(a)), nil }), ), virtfs.FileNode("metrics.by-backend-model", 0444, virtfs.Read(func() ([]byte, error) { groups, err := metrics.GroupedAggregate("", "") if err != nil { return nil, err } return []byte(metrics.FormatGroups(groups)), nil }), ), // session/ virtfs.DirNode("session", virtfs.Doc("Session management"), virtfs.FileNode("new", 0666, virtfs.Doc("Create session"), virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) { args := strings.Fields(string(data)) name, remote, workflow, cwd := "", "", "", "" yolo := false for _, arg := range args { if k, v, ok := strings.Cut(arg, "="); ok { switch k { case "name": name = v case "remote": remote = v case "workflow": workflow = v case "cwd": cwd = v case "yolo": yolo = v == "true" } } } if cwd == "" { return nil, fmt.Errorf("cwd is required") } sess, err := session.CreateEmpty(name, remote, yolo) if err != nil { return nil, err } sess.SetCwd(cwd) if workflow != "" { sess.SetWorkflow(workflow) } return []byte(sess.Name() + "\n"), nil }), ), virtfs.FileNode("idx", 0444, virtfs.Doc("Session index"), virtfs.Read(func() ([]byte, error) { return session.BuildIndex(), nil }), ), virtfs.Each("{sname}", func() ([]virtfs.FsNodeDecl, error) { return buildSessionEntries() }), ), ) } // buildSessionEntries produces one FsNodeDecl per session. func buildSessionEntries() ([]virtfs.FsNodeDecl, error) { sessions := session.Sessions() var out []virtfs.FsNodeDecl for name, sess := range sessions { s := sess n := name removeFn := func() error { session.Kill(n) return nil } renameFn := func(newName string) error { return session.Rename(n, newName) } out = append(out, virtfs.FsNodeDecl{ Name: s.Name(), Aliases: []string{s.ID}, Remove: removeFn, Rename: renameFn, Children: buildSessionChildren(s, removeFn, renameFn), }) } return out, nil } // buildSessionChildren returns the file nodes for a single session. func buildSessionChildren( s *session.Session, removeFn func() error, renameFn func(string) error, ) []virtfs.FsNodeDecl { return []virtfs.FsNodeDecl{ virtfs.FileNode("env", 0444, virtfs.Read(func() ([]byte, error) { var sb strings.Builder fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", s.ID) for _, e := range os.Environ() { if strings.HasPrefix(e, "OLLIE_") && !strings.HasPrefix(e, "OLLIE_SESSION_ID=") { sb.WriteString(e) sb.WriteByte('\n') } } return []byte(sb.String()), nil }), ), virtfs.FileNode("paused", 0444, virtfs.Read(func() ([]byte, error) { if s.IsPaused() { return []byte("true\n"), nil } return []byte("false\n"), nil }), ), virtfs.FileNode("goal", 0666, virtfs.Doc("Session goal text. Writing triggers the workflow if not already running."), virtfs.GID("agent"), virtfs.Read(func() ([]byte, error) { text, _ := s.Goal() if text == "" { return nil, nil } return []byte(text + "\n"), nil }), virtfs.Write(func(data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { s.ClearGoal() return nil } _, status := s.Goal() s.SetGoal(input) // Trigger workflow only if not already running. if status == "" || status == "complete" || status == "blocked" || strings.HasPrefix(status, "error") { runWorkflow(s, s.Workflow()) } return nil }), ), virtfs.FileNode("goalstatus", 0666, virtfs.Doc("Goal status. Read/write."), virtfs.GID("agent"), virtfs.Read(func() ([]byte, error) { _, status := s.Goal() return []byte(status + "\n"), nil }), virtfs.Write(func(data []byte) error { status := strings.TrimSpace(string(data)) s.SetGoalStatus(status) return nil }), ), virtfs.FileNode("goalwait", 0444, virtfs.Doc("Blocks until goal status changes."), virtfs.BlockOnce(func() ([]byte, string, error) { _, status := s.Goal() return []byte(status + "\n"), status, nil }, s.GoalSignal), ), virtfs.FileNode("stats", 0444, virtfs.Read(func() ([]byte, error) { a, err := metrics.AggregateSession(s.ID) if err != nil { return nil, err } return []byte(metrics.Format(a)), nil }), ), virtfs.FileNode("metrics", 0444, virtfs.Read(func() ([]byte, error) { a, err := metrics.AggregateSession(s.ID) if err != nil { return nil, err } return []byte(metrics.Format(a)), nil }), ), virtfs.FileNode("metrics.by-backend-model", 0444, virtfs.Read(func() ([]byte, error) { groups, err := metrics.GroupedAggregate(s.ID, "") if err != nil { return nil, err } return []byte(metrics.FormatGroups(groups)), nil }), ), virtfs.FileNode("ctl", 0666, virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) { return dispatch(map[string]func([]string) ([]byte, error){ "kill": func(_ []string) ([]byte, error) { return []byte("ok\n"), removeFn() }, ".": func(_ []string) ([]byte, error) { return []byte("ok\n"), removeFn() }, "save": func(_ []string) ([]byte, error) { s.Save() return []byte("ok\n"), nil }, "invalidate": func(_ []string) ([]byte, error) { s.InvalidateModelsCache() return []byte("ok\n"), nil }, "pause": func(_ []string) ([]byte, error) { if err := s.Pause(); err != nil { return nil, err } return []byte("ok\n"), nil }, "resume": func(_ []string) ([]byte, error) { if err := s.Resume(); err != nil { return nil, err } return []byte("ok\n"), nil }, "run": func(args []string) ([]byte, error) { workflow := s.Workflow() if len(args) > 0 { workflow = args[0] } runWorkflow(s, workflow) return []byte("ok\n"), nil }, }, data) }), ), virtfs.FileNode("name", 0666, virtfs.Read(func() ([]byte, error) { return []byte(s.Name() + "\n"), nil }), virtfs.Write(func(data []byte) error { newName := strings.TrimSpace(string(data)) if newName == "" || newName == s.Name() { return nil } return renameFn(newName) }), ), virtfs.FileNode("id", 0444, virtfs.Read(func() ([]byte, error) { return []byte(s.ID + "\n"), nil }), ), virtfs.DirNode("agent", virtfs.FileNode("new", 0666, virtfs.Doc("Create agent. With prompt=, runs as sub-agent: blocks until done, returns reply."), virtfs.Rdwr(func(ctx context.Context, data []byte) ([]byte, error) { req := parseAgentNewRequest(data) req.Params.ParentID = req.ParentID if req.Prompt == "" { // Persistent agent creation (no sub-agent mode). ag, err := session.CreateAgentWithParams(s.Name(), req.Params) if err != nil { return nil, fmt.Errorf("create agent: %w", err) } wireAgentEvents(s.ID, ag) return []byte(ag.ID() + "\n"), nil } // Sub-agent mode: enforce limits, create, run, destroy. maxDepth := req.MaxDepth if maxDepth <= 0 { maxDepth = 1 } maxParallel := 5 // default if req.MaxParallel != nil { maxParallel = *req.MaxParallel } if maxParallel == 0 { return nil, fmt.Errorf("sub-agent spawning is disabled (max_parallel=0)") } timeout := req.Timeout if timeout <= 0 { timeout = 600 } var parent *agent.Agent if req.ParentID != "" { parent = s.FindAgent(req.ParentID) if parent == nil { return nil, fmt.Errorf("parent agent %q not found", req.ParentID) } } if parent != nil { // Depth check. if parent.Depth()+1 > maxDepth { return nil, fmt.Errorf("sub-agent depth limit exceeded (max %d)", maxDepth) } // Parallelism check. if maxParallel > 0 && parent.ActiveChildren() >= int32(maxParallel) { return nil, fmt.Errorf("sub-agent parallelism limit exceeded (max %d)", maxParallel) } // Inherit context (optionally truncated at fork_at). msgs := parent.Messages() if req.ForkAt > 0 { msgs = messagesUpToTurn(msgs, req.ForkAt) } // Sanitize to remove dangling tool calls msgs = backend.SanitizeMessages(msgs) req.Params.History = agent.RestoreHistoryFromMessages(msgs) parent.IncChildren() defer parent.DecChildren() } ag, err := session.CreateAgentWithParams(s.Name(), req.Params) if err != nil { return nil, fmt.Errorf("create agent: %w", err) } wireAgentEvents(s.ID, ag) if parent != nil { ag.SetDepth(parent.Depth() + 1) } ag.SetEnv("OLLIE_SUBAGENT_DEPTH", fmt.Sprintf("%d", ag.Depth())) // Run with timeout. subCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() session.PublishEvent("session."+s.ID+".agent."+ag.ID()+".new", "") ag.Submit(subCtx, req.Prompt) ag.EnsureTrailingNewline() reply := ag.Reply() s.RemoveAgent(ag.ID()) session.PublishEvent("session."+s.ID+".agent."+ag.ID()+".kill", "") if reply == "" { return []byte("(no reply)\n"), nil } return []byte(reply), nil }), ), virtfs.FileNode("idx", 0444, virtfs.Read(func() ([]byte, error) { return session.BuildAgentIndex(s), nil }), ), virtfs.Each("{aname}", func() ([]virtfs.FsNodeDecl, error) { return buildAgentEntries(s) }), ), } } // buildAgentEntries produces one FsNodeDecl per agent in a session. func buildAgentEntries(s *session.Session) ([]virtfs.FsNodeDecl, error) { agents := s.Agents() var out []virtfs.FsNodeDecl for _, ag := range agents { a := ag wireAgentEvents(s.ID, a) out = append(out, virtfs.FsNodeDecl{ Name: a.Name(), Aliases: []string{a.ID()}, Remove: func() error { s.RemoveAgent(a.ID()) session.PublishEvent("session."+s.ID+".agent."+a.ID()+".kill", "") return nil }, Children: buildAgentChildren(a, s), }) } return out, nil } // buildAgentChildren returns the file nodes for a single agent. func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl { chatStat := func() os.FileInfo { return &SyntheticFileInfo{Name_: "chat", Mode_: 0444, Size_: 64 * 1024} } stats := func(_ []string) ([]byte, error) { u := a.Usage() var sb strings.Builder fmt.Fprintf(&sb, "usage=%s\n", a.UsageStr()) fmt.Fprintf(&sb, "cost=%s\n", a.CostStr()) fmt.Fprintf(&sb, "ctxsz=%s\n", a.CtxSz()) if u != nil { fmt.Fprintf(&sb, "cachedInputTokens=%d\n", u.TotalCachedInputTokens) fmt.Fprintf(&sb, "cacheCreationTokens=%d\n", u.TotalCacheCreationTokens) fmt.Fprintf(&sb, "cacheHitRatio=%g\n", u.CacheHitRatio) } return []byte(sb.String()), nil } return []virtfs.FsNodeDecl{ virtfs.FileNode("prompt", 0666, virtfs.Write(func(data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { return nil } if input == "/invalidate" { s.InvalidateModelsCache() return nil } startAsync(s.Ctx, func() { a.Submit(s.Ctx, input) a.EnsureTrailingNewline() }) return nil }), ), virtfs.FileNode("fifo", 0666, virtfs.Write(func(data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { return nil } if err := a.Queue(input); err != nil { return err } // If the agent is idle, trigger processing. if !a.IsRunning() { if !startAsync(s.Ctx, func() { if next, ok := a.PopQueue(); ok { a.Submit(s.Ctx, next) a.EnsureTrailingNewline() } }) { return fmt.Errorf("async work limit reached") } } return nil }), virtfs.Read(func() ([]byte, error) { item, ok := a.PopQueue() if !ok { return nil, nil } return []byte(item), nil }), ), virtfs.FileNode("feed", 0666, virtfs.Write(func(data []byte) error { if len(data) == 0 { return nil } for _, b := range data { if b != 0 { a.FeedWrite(data) return nil } } return nil }), virtfs.BlockOnce(func() ([]byte, string, error) { return a.Feed.Get(), a.Feed.Hash(), nil }, a.SignalCh), ), virtfs.FileNode("chat", 0444, virtfs.StatOverride(chatStat), virtfs.Read(func() ([]byte, error) { a.ChatMu().RLock() defer a.ChatMu().RUnlock() return stripMarkers(a.ChatLog()), nil }), virtfs.Stream(func(base string) ([]byte, string, error) { data, nextBase, err := a.ChatRead(base) if err != nil || len(data) == 0 { return data, nextBase, err } stripped := stripMarkers(data) if len(stripped) > 0 { return stripped, nextBase, nil } return nil, nextBase, nil }, a.ChatSignal), ), virtfs.FileNode("chat.raw", 0444, virtfs.StatOverride(chatStat), virtfs.Read(func() ([]byte, error) { a.ChatMu().RLock() defer a.ChatMu().RUnlock() log := a.ChatLog() data := make([]byte, len(log)) copy(data, log) return data, nil }), virtfs.Stream(a.ChatRead, a.ChatSignal), ), virtfs.FileNode("statewait", 0444, virtfs.Read(func() ([]byte, error) { return []byte(a.State() + "\n"), nil }), virtfs.BlockOnce(func() ([]byte, string, error) { st := a.State() return []byte(st + "\n"), st, nil }, a.SignalCh), ), virtfs.FileNode("log", 0444, virtfs.Read(func() ([]byte, error) { const maxWindow = 64 * 1024 a.ChatMu().RLock() log := a.ChatLog() start := 0 if len(log) > maxWindow { start = len(log) - maxWindow } data := make([]byte, len(log)-start) copy(data, log[start:]) a.ChatMu().RUnlock() return data, nil }), ), virtfs.FileNode("plan", 0666, virtfs.Read(func() ([]byte, error) { return a.Plan(), nil }), virtfs.Write(func(data []byte) error { a.SetPlan(data) return nil }), ), virtfs.FileNode("cfg", 0666, virtfs.Read(func() ([]byte, error) { p := a.GenParams() var sb strings.Builder fmt.Fprintf(&sb, "name=%s\n", s.ID) fmt.Fprintf(&sb, "backend=%s\n", a.BackendName()) fmt.Fprintf(&sb, "model=%s\n", a.ModelName()) fmt.Fprintf(&sb, "id=%s\n", a.ID()) fmt.Fprintf(&sb, "profile=%s\n", a.Profile()) fmt.Fprintf(&sb, "displayName=%s\n", a.Name()) fmt.Fprintf(&sb, "cwd=%s\n", a.Cwd()) fmt.Fprintf(&sb, "remote=%s\n", s.Remote) fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens) if p.Temperature != nil { fmt.Fprintf(&sb, "temperature=%g\n", *p.Temperature) } if p.TopP != nil { fmt.Fprintf(&sb, "topP=%g\n", *p.TopP) } if p.TopK != nil { fmt.Fprintf(&sb, "topK=%d\n", *p.TopK) } if len(p.Stop) > 0 { fmt.Fprintf(&sb, "stop=%s\n", strings.Join(p.Stop, ",")) } return []byte(sb.String()), nil }), virtfs.Write(func(data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { return nil } parts := strings.SplitN(input, "=", 2) if len(parts) != 2 { return fmt.Errorf("invalid cfg format (expected key=value)") } switch parts[0] { case "name": if strings.TrimSpace(parts[1]) != "" { a.SetName(strings.TrimSpace(parts[1])) } default: return fmt.Errorf("unknown cfg key: %s", parts[0]) } return nil }), ), virtfs.FileNode("ctl", 0666, virtfs.Rdwr(func(_ context.Context, data []byte) ([]byte, error) { return dispatch(map[string]func([]string) ([]byte, error){ "kill": func(_ []string) ([]byte, error) { s.RemoveAgent(a.ID()) session.PublishEvent("session."+s.ID+".agent."+a.ID()+".kill", "") return []byte("ok\n"), nil }, "stop": func(_ []string) ([]byte, error) { a.Interrupt(agent.ErrInterrupted) return []byte("ok\n"), nil }, "compact": func(_ []string) ([]byte, error) { if err := a.Compact(s.Ctx); err != nil { return nil, err } return []byte("ok\n"), nil }, "compactionmodel": func(args []string) ([]byte, error) { model := strings.TrimSpace(strings.Join(args, " ")) if model == "" { return []byte(a.CompactionModel() + "\n"), nil } a.SetCompactionModel(model) return []byte(model + "\n"), nil }, "clear": func(_ []string) ([]byte, error) { if err := a.Clear(); err != nil { return nil, err } return []byte("ok\n"), nil }, "inject": func(args []string) ([]byte, error) { text := strings.Join(args, " ") if text == "" { return nil, fmt.Errorf("inject requires text") } if a.IsRunning() { a.InjectRewrite(text) } else { startAsync(s.Ctx, func() { a.Submit(s.Ctx, text) a.EnsureTrailingNewline() }) } return []byte("ok\n"), nil }, "i": func(args []string) ([]byte, error) { text := strings.Join(args, " ") if text == "" { return nil, fmt.Errorf("inject requires text") } if a.IsRunning() { a.InjectRewrite(text) } else { startAsync(s.Ctx, func() { a.Submit(s.Ctx, text) a.EnsureTrailingNewline() }) } return []byte("ok\n"), nil }, "agent": func(args []string) ([]byte, error) { if len(args) == 0 { return []byte(a.Profile() + "\n"), nil } if err := a.SwitchProfile(args[0]); err != nil { return nil, err } return []byte(args[0] + "\n"), nil }, "model": func(args []string) ([]byte, error) { if len(args) == 0 { if be := a.Backend(); be != nil { return []byte(be.Model() + "\n"), nil } return nil, nil } if be := a.Backend(); be != nil { be.SetModel(strings.Join(args, " ")) } return []byte(strings.Join(args, " ") + "\n"), nil }, "models": func(_ []string) ([]byte, error) { return []byte(s.CachedListModels() + "\n"), nil }, "tools": func(_ []string) ([]byte, error) { ts := a.ToolServer() if ts == nil { return []byte("(no tool server)\n"), nil } loaded, err := ts.ListTools() if err != nil { return nil, err } var sb strings.Builder for _, ti := range loaded { if ti.Description != "" { fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description) } else { sb.WriteString(ti.Name + "\n") } } return []byte(sb.String()), nil }, "tool_load": func(args []string) ([]byte, error) { if len(args) == 0 { return nil, fmt.Errorf("tool_load requires a tool name") } ts := a.ToolServer() if ts == nil { return nil, fmt.Errorf("no tool server") } if err := ts.LoadTool(args[0]); err != nil { return nil, err } if infos, err := ts.ListTools(); err == nil { a.SetToolsPreamble(agent.RenderTools(infos)) } return []byte(args[0] + "\n"), nil }, "tools_all": func(args []string) ([]byte, error) { ts := a.ToolServer() if ts == nil { return nil, fmt.Errorf("no tool server") } return ts.ListAllTools() }, "tool_unload": func(args []string) ([]byte, error) { if len(args) == 0 { return nil, fmt.Errorf("tool_unload requires a tool name") } ts := a.ToolServer() if ts == nil { return nil, fmt.Errorf("no tool server") } if err := ts.UnloadTool(args[0]); err != nil { return nil, err } if infos, err := ts.ListTools(); err == nil { a.SetToolsPreamble(agent.RenderTools(infos)) } return []byte(args[0] + "\n"), nil }, "backend": func(args []string) ([]byte, error) { if len(args) == 0 { if be := a.Backend(); be != nil { return []byte(be.Name() + "\n"), nil } return nil, nil } return nil, fmt.Errorf("backend switching not supported via ctl; use cfg") }, "name": func(args []string) ([]byte, error) { if len(args) == 0 { return []byte(a.Name() + "\n"), nil } a.SetName(strings.Join(args, " ")) return []byte(strings.Join(args, " ") + "\n"), nil }, "cwd": func(args []string) ([]byte, error) { return []byte(a.Cwd() + "\n"), nil }, "proc": func(args []string) ([]byte, error) { ts := a.ToolServer() if ts == nil { return nil, fmt.Errorf("no tool server") } if len(args) == 0 || args[0] == "top" { data, err := ts.ListProcs() if err != nil { return nil, err } if len(data) == 0 { return []byte("(no background processes)\n"), nil } return data, nil } subcmd := args[0] switch subcmd { case "term": if len(args) < 2 { return nil, fmt.Errorf("proc term requires pid") } pid, err := strconv.Atoi(args[1]) if err != nil { return nil, fmt.Errorf("invalid pid: %s", args[1]) } if err := ts.SignalDetached(pid, syscall.SIGTERM); err != nil { return nil, err } return []byte("ok\n"), nil case "kill": if len(args) < 2 { return nil, fmt.Errorf("proc kill requires pid") } pid, err := strconv.Atoi(args[1]) if err != nil { return nil, fmt.Errorf("invalid pid: %s", args[1]) } if err := ts.SignalDetached(pid, syscall.SIGKILL); err != nil { return nil, err } return []byte("ok\n"), nil case "out": if len(args) < 2 { return nil, fmt.Errorf("proc out requires pid") } pid, err := strconv.Atoi(args[1]) if err != nil { return nil, fmt.Errorf("invalid pid: %s", args[1]) } out, err := ts.GetDetachedOutput(pid) if err != nil { return nil, err } return []byte(out), nil case "dismiss": if len(args) < 2 { return nil, fmt.Errorf("proc dismiss requires pid") } pid, err := strconv.Atoi(args[1]) if err != nil { return nil, fmt.Errorf("invalid pid: %s", args[1]) } if !ts.DismissDetached(pid) { return nil, fmt.Errorf("process %d not found", pid) } return []byte("ok\n"), nil default: return nil, fmt.Errorf("unknown proc subcommand: %s (use: top, term , kill , out , dismiss )", subcmd) } }, "systemprompt": func(_ []string) ([]byte, error) { return []byte(a.SystemPrompt() + "\n"), nil }, "stats": stats, "help": func(_ []string) ([]byte, error) { return []byte("stop compact clear inject agent model models tools tool_load tool_unload cwd proc name backend systemprompt stats help\n"), nil }, }, data) }), ), virtfs.FileNode("stats", 0444, virtfs.Read(func() ([]byte, error) { a, err := metrics.AggregateAgent(s.ID, a.ID()) if err != nil { return nil, err } return []byte(metrics.Format(a)), nil }), ), virtfs.FileNode("metrics", 0444, virtfs.Read(func() ([]byte, error) { a, err := metrics.AggregateAgent(s.ID, a.ID()) if err != nil { return nil, err } return []byte(metrics.Format(a)), nil }), ), virtfs.FileNode("metrics.by-backend-model", 0444, virtfs.Read(func() ([]byte, error) { groups, err := metrics.GroupedAggregate(s.ID, a.ID()) if err != nil { return nil, err } return []byte(metrics.FormatGroups(groups)), nil }), ), virtfs.FileNode("name", 0666, virtfs.Read(func() ([]byte, error) { return []byte(a.Name() + "\n"), nil }), virtfs.Write(func(data []byte) error { newName := strings.TrimSpace(string(data)) if newName == "" { return nil } a.SetName(newName) session.PersistSession(s.Name()) return nil }), ), virtfs.FileNode("id", 0444, virtfs.Read(func() ([]byte, error) { return []byte(a.ID() + "\n"), nil }), ), } }