package fs import ( "bytes" "context" "fmt" "os" "strconv" "strings" "ollie/agent" "ollie/paths" "ollie/session" ) func requestAgentNew(ctx HandlerCtx, data []byte) ([]byte, error) { args := strings.Fields(string(data)) ag, err := session.CreateAgent(ctx.Session.Name(), args) if err != nil { return nil, fmt.Errorf("create agent: %w", err) } // Create AgentLog for this agent and wire up the event handler al := NewAgentLog("") ctx.Session.SetAgentLog(ag.ID(), al) wireAgentEvents(ctx.Session.ID(), ag, al) return []byte(ag.ID() + "\n"), nil } // agentBindings returns bindings for all agents in a session. // The Blueprint pattern clones the structure and binds ctx.Agent + ctx.AgentLog. func agentBindings(ctx HandlerCtx) ([]Binding, error) { sess := ctx.Session if sess == nil || sess.Core == nil { return nil, nil } var out []Binding for _, ag := range sess.Core.Agents() { a := ag // capture al := sess.AgentLogFor(a.ID()) if al == nil { continue } out = append(out, Binding{ Name: a.Name(), Aliases: []string{a.ID()}, UID: a.ID(), GID: "agent", Applier: func(c HandlerCtx) HandlerCtx { c.Agent = a c.AgentLog = al return c }, Rename: func(newName string) error { a.SetName(newName) session.PersistSession(sess.Name()) return nil }, }) } return out, nil } func writeAgentPrompt(ctx HandlerCtx, data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { return nil } if input == "/invalidate" { ctx.Session.InvalidateModelsCache() return nil } go func() { ctx.Agent.Submit(ctx.Session.Ctx(), input) ctx.AgentLog.EnsureTrailingNewline() }() return nil } func writeAgentFifoIn(ctx HandlerCtx, data []byte) error { input := strings.TrimSpace(string(data)) if input == "" { return nil } ctx.Agent.Queue(input) return nil } func readAgentFifoOut(ctx HandlerCtx) ([]byte, error) { item, ok := ctx.Agent.PopQueue() if !ok { return nil, nil } return []byte(item), nil } func statAgentChat() os.FileInfo { // Note: This needs ctx but Stat doesn't receive it. // We'll need to handle this specially or pass via closure. // For now, return a placeholder - the real stat is done in builder. return &SyntheticFileInfo{Name_: "chat", Mode_: 0444, Size_: 64 * 1024} } func readAgentChat(ctx HandlerCtx) ([]byte, error) { ctx.AgentLog.mu.RLock() defer ctx.AgentLog.mu.RUnlock() data := make([]byte, len(ctx.AgentLog.log)) copy(data, ctx.AgentLog.log) return data, nil } func readAgentChatText(ctx HandlerCtx) ([]byte, error) { ctx.AgentLog.mu.RLock() defer ctx.AgentLog.mu.RUnlock() return stripMarkers(ctx.AgentLog.log), nil } func streamAgentChat(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) { merged, cancel := mergeCtx(ctx, cctx) defer cancel() return streamChat(ctx.AgentLog, merged, base) } func streamAgentChatText(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) { merged, cancel := mergeCtx(ctx, cctx) defer cancel() for { data, nextBase, err := streamChat(ctx.AgentLog, merged, base) if err != nil || len(data) == 0 { return data, nextBase, err } stripped := stripMarkers(data) if len(stripped) > 0 { return stripped, nextBase, nil } // All data was markers/fences — advance base and wait for more. base = nextBase } } // stripMarkers removes [[[...]]] marker lines and source fence delimiter lines // from raw chat data. Content inside fences is preserved — only the ``` lines // themselves are stripped. Partial lines (no trailing \n) are always passed // through — they are streaming content, never markers. func stripMarkers(data []byte) []byte { var out []byte for len(data) > 0 { // Find next newline nl := bytes.IndexByte(data, '\n') if nl < 0 { // Partial line at end — always content, emit it out = append(out, data...) break } line := data[:nl+1] // includes \n data = data[nl+1:] // Drop [[[...]]] marker lines trimmed := bytes.TrimRight(line, "\n") if len(trimmed) >= 7 && bytes.HasPrefix(trimmed, []byte("[[[")) && bytes.HasSuffix(trimmed, []byte("]]]")) { continue } // Drop bare fence delimiter lines (``` or ```lang). // These are the source fences wrapping tool output. // Keep fences with 4+ backticks (````), those are user/assistant markdown. stripped := bytes.TrimSpace(trimmed) if bytes.HasPrefix(stripped, []byte("```")) && !bytes.HasPrefix(stripped, []byte("````")) { continue } out = append(out, line...) } return out } func blockAgentStateWait(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) { merged, cancel := mergeCtx(ctx, cctx) defer cancel() if base == "" { base = ctx.Agent.State() } v, ok := ctx.Agent.WaitChange(merged, agent.WatchState, base) if !ok { st := ctx.Agent.State() return []byte(st + "\n"), st, nil } return []byte(v + "\n"), v, nil } func readAgentLog(ctx HandlerCtx) ([]byte, error) { const maxWindow = 64 * 1024 ctx.AgentLog.mu.RLock() log := ctx.AgentLog.log start := 0 if len(log) > maxWindow { start = len(log) - maxWindow } data := make([]byte, len(log)-start) copy(data, log[start:]) ctx.AgentLog.mu.RUnlock() return data, nil } func readAgentPlan(ctx HandlerCtx) ([]byte, error) { ctx.AgentLog.mu.RLock() data := make([]byte, len(ctx.AgentLog.plan)) copy(data, ctx.AgentLog.plan) ctx.AgentLog.mu.RUnlock() return data, nil } func writeAgentPlan(ctx HandlerCtx, data []byte) error { ctx.AgentLog.mu.Lock() ctx.AgentLog.plan = make([]byte, len(data)) copy(ctx.AgentLog.plan, data) ctx.AgentLog.mu.Unlock() return nil } func readAgentCfg(ctx HandlerCtx) ([]byte, error) { ctx.AgentLog.mu.RLock() defer ctx.AgentLog.mu.RUnlock() p := ctx.Agent.GenParams() var sb strings.Builder fmt.Fprintf(&sb, "name=%s\n", ctx.Session.ID()) fmt.Fprintf(&sb, "backend=%s\n", ctx.Agent.BackendName()) fmt.Fprintf(&sb, "model=%s\n", ctx.Agent.ModelName()) fmt.Fprintf(&sb, "id=%s\n", ctx.Agent.ID()) fmt.Fprintf(&sb, "profile=%s\n", ctx.Agent.Profile()) fmt.Fprintf(&sb, "displayName=%s\n", ctx.Agent.Name()) fmt.Fprintf(&sb, "cwd=%s\n", ctx.Agent.Cwd()) fmt.Fprintf(&sb, "remote=%s\n", ctx.AgentLog.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 } func writeAgentCfg(ctx HandlerCtx, 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]) != "" { ctx.Agent.SetName(strings.TrimSpace(parts[1])) } default: return fmt.Errorf("unknown cfg key: %s", parts[0]) } return nil } func requestAgentCtl(ctx HandlerCtx, data []byte) ([]byte, error) { return rdwrDispatch(agentCtlHandlers)(ctx, data) } var agentCtlHandlers = map[string]rdwrHandler{ "kill": func(ctx HandlerCtx, _ []string) ([]byte, error) { if ctx.Session.Core != nil { ctx.Session.Core.RemoveAgent(ctx.Agent.ID()) session.PublishEvent("session."+ctx.Session.ID()+".agent."+ctx.Agent.ID()+".kill", "") } return []byte("ok\n"), nil }, "stop": func(ctx HandlerCtx, _ []string) ([]byte, error) { ctx.Agent.Interrupt(agent.ErrInterrupted) return []byte("ok\n"), nil }, "compact": func(ctx HandlerCtx, _ []string) ([]byte, error) { if err := ctx.Agent.Compact(ctx.Session.Ctx()); err != nil { return nil, err } return []byte("ok\n"), nil }, "clear": func(ctx HandlerCtx, _ []string) ([]byte, error) { if err := ctx.Agent.Clear(); err != nil { return nil, err } return []byte("ok\n"), nil }, "inject": func(ctx HandlerCtx, args []string) ([]byte, error) { text := strings.Join(args, " ") if text == "" { return nil, fmt.Errorf("inject requires text") } ctx.Agent.InjectRewrite(text) return []byte("ok\n"), nil }, "agent": func(ctx HandlerCtx, args []string) ([]byte, error) { if len(args) == 0 { return []byte(ctx.Agent.Profile() + "\n"), nil } if err := ctx.Agent.SwitchProfile(args[0]); err != nil { return nil, err } return []byte(args[0] + "\n"), nil }, "model": func(ctx HandlerCtx, args []string) ([]byte, error) { if len(args) == 0 { if be := ctx.Agent.Backend(); be != nil { return []byte(be.Model() + "\n"), nil } return nil, nil } if be := ctx.Agent.Backend(); be != nil { be.SetModel(strings.Join(args, " ")) } return []byte(strings.Join(args, " ") + "\n"), nil }, "models": func(ctx HandlerCtx, _ []string) ([]byte, error) { return []byte(ctx.Session.CachedListModels() + "\n"), nil }, "tools": func(ctx HandlerCtx, _ []string) ([]byte, error) { if ctx.Session.ToolsConn() == nil { return []byte("(no tool server)\n"), nil } loaded, err := ctx.Session.ToolsConn().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(ctx HandlerCtx, args []string) ([]byte, error) { if len(args) == 0 { return nil, fmt.Errorf("tool_load requires a tool name") } if err := ctx.Session.LoadTool(args[0], ctx.Agent); err != nil { return nil, err } return []byte(args[0] + "\n"), nil }, "backend": func(ctx HandlerCtx, args []string) ([]byte, error) { if len(args) == 0 { if be := ctx.Agent.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(ctx HandlerCtx, args []string) ([]byte, error) { if len(args) == 0 { return []byte(ctx.Agent.Name() + "\n"), nil } ctx.Agent.SetName(strings.Join(args, " ")) return []byte(strings.Join(args, " ") + "\n"), nil }, "cwd": func(ctx HandlerCtx, args []string) ([]byte, error) { if len(args) == 0 { return []byte(ctx.Agent.Cwd() + "\n"), nil } dir := paths.ExpandHome(strings.Join(args, " ")) if _, err := os.Stat(dir); err != nil { return nil, fmt.Errorf("cwd: %w", err) } ctx.Agent.SetCWD(dir) return []byte(dir + "\n"), nil }, "systemprompt": func(ctx HandlerCtx, _ []string) ([]byte, error) { return []byte(ctx.Agent.SystemPrompt() + "\n"), nil }, "help": func(_ HandlerCtx, _ []string) ([]byte, error) { return []byte(`stop — interrupt running agent compact — compact context clear — clear history inject — inject prompt mid-turn (overwrites pending) agent [name] — show/switch agent profile model [name] — show/switch model models — list available models tools — list loaded tools tool_load — load a tool cwd [path] — show/change working directory name [name] — show/rename agent backend — show backend systemprompt — show rendered system prompt help — this message `), nil }, } func readAgentStats(ctx HandlerCtx) ([]byte, error) { s := "usage=" + ctx.Agent.UsageStr() + "\n" + "cost=" + ctx.Agent.CostStr() + "\n" + "ctxsz=" + ctx.Agent.CtxSz() + "\n" return []byte(s), nil } func readAgentName(ctx HandlerCtx) ([]byte, error) { return []byte(ctx.Agent.Name() + "\n"), nil } func writeAgentName(ctx HandlerCtx, data []byte) error { newName := strings.TrimSpace(string(data)) if newName == "" { return nil } ctx.Agent.SetName(newName) session.PersistSession(ctx.Session.Name()) return nil } func readAgentID(ctx HandlerCtx) ([]byte, error) { return []byte(ctx.Agent.ID() + "\n"), nil } func streamChat(al *AgentLog, cctx context.Context, base string) ([]byte, string, error) { ctx, cancel := context.WithCancel(cctx) defer cancel() context.AfterFunc(ctx, func() { al.chatCond.Broadcast() }) var offset int if base != "" { fmt.Sscanf(base, "%d", &offset) } else { al.mu.RLock() offset = len(al.log) al.mu.RUnlock() } al.mu.RLock() for len(al.log) <= offset { if ctx.Err() != nil { al.mu.RUnlock() return nil, "", nil } al.chatCond.Wait() } data := make([]byte, len(al.log)-offset) copy(data, al.log[offset:]) newOffset := len(al.log) al.mu.RUnlock() return data, fmt.Sprintf("%d", newOffset), nil } // processBindings returns bindings for detached processes. func processBindings(ctx HandlerCtx) ([]Binding, error) { if ctx.Agent == nil { return nil, nil } procs := ctx.Agent.ListDetached() out := make([]Binding, len(procs)) for i, p := range procs { pid := p.PID // capture out[i] = Binding{ Name: strconv.Itoa(p.PID), Applier: func(c HandlerCtx) HandlerCtx { c.Data = pid return c }, } } return out, nil } func readProcess(ctx HandlerCtx) ([]byte, error) { pid := ctx.Data.(int) output, err := ctx.Agent.GetDetachedOutput(pid) if err != nil { return nil, err } return []byte(output), nil } func removeProcess(ctx HandlerCtx) error { pid := ctx.Data.(int) if !ctx.Agent.DismissDetached(pid) { return fmt.Errorf("process %d not found or still running", pid) } return nil }