828 lines
20 KiB
Go
828 lines
20 KiB
Go
package fs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"ollie/agent"
|
|
"ollie/backend"
|
|
"ollie/elevate"
|
|
"ollie/fsedsl"
|
|
"ollie/paths"
|
|
"ollie/session"
|
|
"ollie/toolsrv"
|
|
)
|
|
|
|
// wireAgentStateEvents sets up the agent to publish state change events to the event bus.
|
|
func wireAgentStateEvents(sessID string, ag *agent.Agent) {
|
|
ag.SetOnStateChange(func(agentID, state string) {
|
|
session.PublishEvent("session."+sessID+".agent."+agentID+".state", state)
|
|
})
|
|
}
|
|
|
|
func readBackends(_ HandlerCtx) ([]byte, error) {
|
|
return []byte(strings.Join(backend.Backends(), "\n") + "\n"), nil
|
|
}
|
|
|
|
var helpFn func() string
|
|
|
|
func readHelp(_ HandlerCtx) ([]byte, error) {
|
|
if helpFn == nil {
|
|
return []byte("help not initialized\n"), nil
|
|
}
|
|
return []byte(helpFn()), nil
|
|
}
|
|
|
|
func init() {
|
|
helpFn = func() string {
|
|
helpOnce.Do(func() {
|
|
helpText = fsedsl.GenerateHelp(treeSpec)
|
|
})
|
|
return helpText
|
|
}
|
|
}
|
|
|
|
var (
|
|
helpOnce sync.Once
|
|
helpText string
|
|
)
|
|
|
|
func readModels(ctx HandlerCtx) ([]byte, error) {
|
|
if ctx.Models != nil {
|
|
return ctx.Models.Get(), nil
|
|
}
|
|
return []byte("(no model cache)\n"), nil
|
|
}
|
|
|
|
func readAgents(_ HandlerCtx) ([]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
|
|
}
|
|
|
|
func readTools(_ HandlerCtx) ([]byte, error) {
|
|
tools := toolsrv.DiscoverTools()
|
|
if len(tools) == 0 {
|
|
return []byte("(no tools found)\n"), nil
|
|
}
|
|
var sb strings.Builder
|
|
for _, ti := range tools {
|
|
fmt.Fprintf(&sb, "%-24s %s\n", ti.Name, ti.Description)
|
|
}
|
|
return []byte(sb.String()), nil
|
|
}
|
|
|
|
func writeRootCtl(ctx HandlerCtx, data []byte) error {
|
|
cmd := strings.TrimSpace(string(data))
|
|
switch cmd {
|
|
case "invalidate":
|
|
if ctx.Models != nil {
|
|
ctx.Models.Invalidate()
|
|
}
|
|
if ctx.Invalidate != nil {
|
|
ctx.Invalidate()
|
|
}
|
|
return nil
|
|
case "kill":
|
|
if ctx.Shutdown != nil {
|
|
ctx.Shutdown()
|
|
}
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("unknown ctl command: %s (valid: invalidate, kill)", cmd)
|
|
}
|
|
}
|
|
|
|
func readEventwait(_ HandlerCtx) ([]byte, error) {
|
|
return nil, fmt.Errorf("eventwait: use blocking read")
|
|
}
|
|
|
|
func blockEventwait(_ HandlerCtx, cctx context.Context, base string) ([]byte, string, error) {
|
|
return session.WaitEvent(cctx, base)
|
|
}
|
|
|
|
func requestComplete(_ HandlerCtx, data []byte) ([]byte, error) {
|
|
var req struct {
|
|
CWD string `json:"cwd"`
|
|
FilePath string `json:"file"`
|
|
Prefix string `json:"prefix"`
|
|
Suffix string `json:"suffix"`
|
|
ExtraContext string `json:"context"`
|
|
}
|
|
if err := json.Unmarshal(data, &req); err != nil {
|
|
req.Prefix = strings.TrimSpace(string(data))
|
|
}
|
|
result, err := agent.Complete(req.CWD, req.FilePath, req.Prefix, req.Suffix, req.ExtraContext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(result + "\n"), nil
|
|
}
|
|
|
|
func requestGenerate(_ HandlerCtx, 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(context.Background(), req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(result + "\n"), nil
|
|
}
|
|
|
|
func requestRoute(_ HandlerCtx, data []byte) ([]byte, error) {
|
|
var req backend.RouteRequest
|
|
if err := json.Unmarshal(data, &req); err != nil {
|
|
req.Task = strings.TrimSpace(string(data))
|
|
}
|
|
result, err := backend.Route(context.Background(), req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(fmt.Sprintf("backend=%s model=%s\n", result.Backend, result.Model)), nil
|
|
}
|
|
|
|
func readAliases(_ HandlerCtx) ([]byte, error) {
|
|
var sb strings.Builder
|
|
for name, sess := range session.Sessions() {
|
|
sid := sess.ID()
|
|
fmt.Fprintf(&sb, "%s\tsession/%s\n", sid, 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
|
|
}
|
|
|
|
var errNoElevate = fmt.Errorf("elevate broker not available")
|
|
|
|
func readElevatePolicy(ctx HandlerCtx) ([]byte, error) {
|
|
if ctx.Elevate == nil {
|
|
return nil, errNoElevate
|
|
}
|
|
p := ctx.Elevate.GlobalPolicy().Global()
|
|
return p.Marshal()
|
|
}
|
|
|
|
func writeElevatePolicy(ctx HandlerCtx, data []byte) error {
|
|
if ctx.Elevate == nil {
|
|
return errNoElevate
|
|
}
|
|
var p elevate.Policy
|
|
if err := elevate.ParsePolicy(data, &p); err != nil {
|
|
return err
|
|
}
|
|
return ctx.Elevate.GlobalPolicy().SetGlobal(p)
|
|
}
|
|
|
|
// elevateBindings returns bindings for pending elevation requests.
|
|
func elevateBindings(ctx HandlerCtx) ([]Binding, error) {
|
|
if ctx.Elevate == nil {
|
|
return nil, errNoElevate
|
|
}
|
|
pending := ctx.Elevate.Pending()
|
|
out := make([]Binding, len(pending))
|
|
for i, r := range pending {
|
|
req := r // capture
|
|
out[i] = Binding{
|
|
Name: r.ID,
|
|
Applier: func(c HandlerCtx) HandlerCtx {
|
|
c.Data = req
|
|
return c
|
|
},
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func readElevateRequest(ctx HandlerCtx) ([]byte, error) {
|
|
req := ctx.Data.(*elevate.Request)
|
|
return []byte(req.Summary() + "\n"), nil
|
|
}
|
|
|
|
func writeElevateRequest(ctx HandlerCtx, data []byte) error {
|
|
req := ctx.Data.(*elevate.Request)
|
|
cmd := strings.TrimSpace(string(data))
|
|
var res elevate.Resolution
|
|
switch cmd {
|
|
case "approve":
|
|
res = elevate.ResolveApprove
|
|
case "deny":
|
|
res = elevate.ResolveDeny
|
|
case "persist":
|
|
res = elevate.ResolvePersist
|
|
default:
|
|
return fmt.Errorf("unknown resolution: %s (use approve/deny/persist)", cmd)
|
|
}
|
|
if !ctx.Elevate.Resolve(req.ID, res) {
|
|
return fmt.Errorf("request already resolved or timed out")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readSessionIdx(_ HandlerCtx) ([]byte, error) {
|
|
return session.BuildIndex(), nil
|
|
}
|
|
|
|
func readAgentIdx(ctx HandlerCtx) ([]byte, error) {
|
|
return session.BuildAgentIndex(ctx.Session.Core), nil
|
|
}
|
|
|
|
func requestSessionNew(_ HandlerCtx, data []byte) ([]byte, error) {
|
|
args := strings.Fields(string(data))
|
|
name := ""
|
|
remote := ""
|
|
for _, arg := range args {
|
|
if k, v, ok := strings.Cut(arg, "="); ok {
|
|
switch k {
|
|
case "name":
|
|
name = v
|
|
case "remote":
|
|
remote = v
|
|
}
|
|
}
|
|
}
|
|
sess, err := session.CreateEmpty(name, remote)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(sess.Name() + "\n"), nil
|
|
}
|
|
|
|
// sessionBindings returns bindings for all sessions.
|
|
// The Blueprint pattern clones the structure and binds ctx.Session.
|
|
func sessionBindings(_ HandlerCtx) ([]Binding, error) {
|
|
rs := globalRootState
|
|
sessions := session.Sessions()
|
|
|
|
out := make([]Binding, 0, len(sessions))
|
|
for name, sess := range sessions {
|
|
// Get or create cached SessionNode wrapper
|
|
node := rs.GetOrCreateNode(sess)
|
|
n := name // capture
|
|
out = append(out, Binding{
|
|
Name: sess.Name(),
|
|
Aliases: []string{sess.ID()},
|
|
UID: sess.Uname(),
|
|
GID: "agent",
|
|
Applier: func(c HandlerCtx) HandlerCtx {
|
|
c.Session = node
|
|
c.Remove = func() {
|
|
session.Kill(n)
|
|
rs.RemoveNode(n)
|
|
}
|
|
c.Rename = func(newName string) error {
|
|
if err := session.Rename(n, newName); err != nil {
|
|
return err
|
|
}
|
|
rs.RenameNode(n, newName)
|
|
return nil
|
|
}
|
|
return c
|
|
},
|
|
Remove: func() {
|
|
session.Kill(n)
|
|
rs.RemoveNode(n)
|
|
},
|
|
Rename: func(newName string) error {
|
|
if err := session.Rename(n, newName); err != nil {
|
|
return err
|
|
}
|
|
rs.RenameNode(n, newName)
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func readSessionEnv(ctx HandlerCtx) ([]byte, error) {
|
|
s := ctx.Session
|
|
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
|
|
}
|
|
|
|
func readSessionPaused(ctx HandlerCtx) ([]byte, error) {
|
|
if ctx.Session.IsPaused() {
|
|
return []byte("true\n"), nil
|
|
}
|
|
return []byte("false\n"), nil
|
|
}
|
|
|
|
func readSessionConnected(ctx HandlerCtx) ([]byte, error) {
|
|
if ctx.Session.IsConnected() {
|
|
return []byte("true\n"), nil
|
|
}
|
|
return []byte("false\n"), nil
|
|
}
|
|
|
|
func writeSessionCtl(ctx HandlerCtx, data []byte) error {
|
|
input := strings.TrimSpace(string(data))
|
|
if input == "" {
|
|
return nil
|
|
}
|
|
cmd := strings.Fields(input)
|
|
switch cmd[0] {
|
|
case "kill", ".":
|
|
ctx.Remove()
|
|
case "save":
|
|
if ctx.Session.Core != nil {
|
|
ctx.Session.Core.SaveSession("")
|
|
}
|
|
case "invalidate":
|
|
ctx.Session.InvalidateModelsCache()
|
|
case "pause":
|
|
return ctx.Session.Pause()
|
|
case "resume":
|
|
return ctx.Session.Resume()
|
|
default:
|
|
return fmt.Errorf("unknown session ctl command: %s", cmd[0])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readSessionName(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Session.Name() + "\n"), nil
|
|
}
|
|
|
|
func writeSessionName(ctx HandlerCtx, data []byte) error {
|
|
newName := strings.TrimSpace(string(data))
|
|
if newName == "" {
|
|
return nil
|
|
}
|
|
oldName := ctx.Session.Name()
|
|
if newName != oldName {
|
|
return ctx.Rename(newName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readSessionID(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Session.ID() + "\n"), nil
|
|
}
|
|
|
|
func readSessionElevate(ctx HandlerCtx) ([]byte, error) {
|
|
if ctx.Elevate == nil {
|
|
return nil, fmt.Errorf("elevate broker not available")
|
|
}
|
|
p := ctx.Elevate.SessionPolicy(ctx.Session.ID())
|
|
return p.Marshal()
|
|
}
|
|
|
|
func writeSessionElevate(ctx HandlerCtx, data []byte) error {
|
|
if ctx.Elevate == nil {
|
|
return fmt.Errorf("elevate broker not available")
|
|
}
|
|
var p elevate.Policy
|
|
if err := elevate.ParsePolicy(data, &p); err != nil {
|
|
return err
|
|
}
|
|
ctx.Elevate.SetSessionPolicy(ctx.Session.ID(), p)
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
ag.SetOutput(NewEventHandler(al))
|
|
wireAgentStateEvents(ctx.Session.ID(), ag)
|
|
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
|
|
}
|
|
ctx.AgentLog.mu.Lock()
|
|
ctx.AgentLog.prevPrompt = []byte(input)
|
|
ctx.AgentLog.mu.Unlock()
|
|
go func() {
|
|
ctx.Agent.Submit(ctx.Session.Ctx(), input)
|
|
ctx.AgentLog.EnsureTrailingNewline()
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func readAgentPromptPrev(ctx HandlerCtx) ([]byte, error) {
|
|
ctx.AgentLog.mu.RLock()
|
|
data := make([]byte, len(ctx.AgentLog.prevPrompt))
|
|
copy(data, ctx.AgentLog.prevPrompt)
|
|
ctx.AgentLog.mu.RUnlock()
|
|
return data, 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 streamAgentChat(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) {
|
|
return streamChat(ctx.AgentLog, cctx, base)
|
|
}
|
|
|
|
func readAgentConnection(ctx HandlerCtx) ([]byte, error) {
|
|
if ctx.Session.ToolsConn() == nil {
|
|
return nil, fmt.Errorf("agent tool server unavailable")
|
|
}
|
|
if _, err := ctx.Session.ToolsConn().ListTools(); err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte("connected\n"), nil
|
|
}
|
|
|
|
func readAgentState(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Agent.State() + "\n"), nil
|
|
}
|
|
|
|
func blockAgentStateWait(ctx HandlerCtx, cctx context.Context, base string) ([]byte, string, error) {
|
|
waitCtx, cancel := context.WithCancel(cctx)
|
|
defer cancel()
|
|
context.AfterFunc(ctx.Session.Ctx(), cancel)
|
|
if base == "" {
|
|
base = ctx.Agent.State()
|
|
}
|
|
v, ok := ctx.Agent.WaitChange(waitCtx, 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 readAgentOffset(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(fmt.Sprintf("%d\n", ctx.AgentLog.ChatOffset)), 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)
|
|
fmt.Fprintf(&sb, "temperature=%v\n", p.Temperature)
|
|
fmt.Fprintf(&sb, "topP=%v\n", p.TopP)
|
|
fmt.Fprintf(&sb, "topK=%v\n", p.TopK)
|
|
fmt.Fprintf(&sb, "stop=%v\n", 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 writeAgentCtl(ctx HandlerCtx, data []byte) error {
|
|
input := strings.TrimSpace(string(data))
|
|
if input == "" {
|
|
return nil
|
|
}
|
|
cmd := strings.Fields(input)
|
|
if len(cmd) == 0 {
|
|
return nil
|
|
}
|
|
switch cmd[0] {
|
|
case "kill":
|
|
if ctx.Session.Core != nil {
|
|
ctx.Session.Core.RemoveAgent(ctx.Agent.ID())
|
|
session.PublishEvent("session."+ctx.Session.ID()+".agent."+ctx.Agent.ID()+".kill", "")
|
|
}
|
|
case "stop":
|
|
ctx.Agent.Interrupt(agent.ErrInterrupted)
|
|
case "compact":
|
|
ctx.Agent.HandleCommand(ctx.Session.Ctx(), "/compact")
|
|
case "clear":
|
|
ctx.Agent.HandleCommand(ctx.Session.Ctx(), "/clear")
|
|
case "model":
|
|
if len(cmd) > 1 {
|
|
if be := ctx.Agent.Backend(); be != nil {
|
|
be.SetModel(strings.Join(cmd[1:], " "))
|
|
}
|
|
}
|
|
case "name":
|
|
if len(cmd) > 1 {
|
|
ctx.Agent.SetName(strings.Join(cmd[1:], " "))
|
|
}
|
|
case "cwd":
|
|
if len(cmd) > 1 {
|
|
ctx.Agent.SetCWD(strings.Join(cmd[1:], " "))
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown agent ctl command: %s", cmd[0])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readAgentCwd(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Agent.Cwd() + "\n"), nil
|
|
}
|
|
|
|
func writeAgentCwd(ctx HandlerCtx, data []byte) error {
|
|
input := strings.TrimSpace(string(data))
|
|
if input == "" {
|
|
return nil
|
|
}
|
|
input = paths.ExpandHome(input)
|
|
if _, err := os.Stat(input); err != nil {
|
|
return fmt.Errorf("cwd: %w", err)
|
|
}
|
|
ctx.Agent.SetCWD(input)
|
|
return nil
|
|
}
|
|
|
|
func readAgentUsage(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Agent.UsageStr() + "\n"), nil
|
|
}
|
|
|
|
func readAgentCost(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Agent.CostStr() + "\n"), nil
|
|
}
|
|
|
|
func readAgentCtxsz(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Agent.CtxSz() + "\n"), nil
|
|
}
|
|
|
|
func readAgentModels(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Session.CachedListModels() + "\n"), nil
|
|
}
|
|
|
|
func readAgentSystemprompt(ctx HandlerCtx) ([]byte, error) {
|
|
return []byte(ctx.Agent.SystemPrompt() + "\n"), nil
|
|
}
|
|
|
|
func readAgentContext(ctx HandlerCtx) ([]byte, error) {
|
|
var sb strings.Builder
|
|
enc := json.NewEncoder(&sb)
|
|
enc.SetEscapeHTML(false)
|
|
for _, m := range ctx.Agent.Context() {
|
|
enc.Encode(m)
|
|
}
|
|
return []byte(sb.String()), nil
|
|
}
|
|
|
|
func readAgentTail(_ HandlerCtx) ([]byte, error) {
|
|
return []byte("#!/bin/sh\nexec tail -f \"$(dirname \"$0\")/chat\"\n"), nil
|
|
}
|
|
|
|
func readAgentTools(ctx HandlerCtx) ([]byte, error) {
|
|
if ctx.Session.ToolsConn() == nil {
|
|
return []byte("(no tool server)\n"), nil
|
|
}
|
|
loaded, err := ctx.Session.ToolsConn().ListTools()
|
|
if err != nil {
|
|
return []byte(fmt.Sprintf("(error: %v)\n", err)), nil
|
|
}
|
|
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
|
|
}
|
|
|
|
func writeAgentTools(ctx HandlerCtx, data []byte) error {
|
|
return ctx.Session.LoadTool(string(data), ctx.Agent)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func nopRead(_ HandlerCtx) ([]byte, error) {
|
|
return nil, nil
|
|
}
|