diff --git a/data/scripts/o b/data/scripts/o index 32e86f6..cdccd8e 100755 --- a/data/scripts/o +++ b/data/scripts/o @@ -33,7 +33,7 @@ ROOT_ENTRIES="session bypass agents backends models help ctl eventwait complete SESSION_FILES="env paused connected ctl name id bypass" # Agent-level files (exist under session/{name}/agent/{name}/) -AGENT_FILES="prompt fifo chat chat.raw statewait log plan cfg ctl stats systemprompt name id proc" +AGENT_FILES="prompt fifo chat chat.raw statewait log plan cfg ctl stats name id proc" is_root_path() { local path="$1" diff --git a/fs/ctl.go b/fs/ctl.go index 0ad4627..ed45c2b 100644 --- a/fs/ctl.go +++ b/fs/ctl.go @@ -5,24 +5,23 @@ import ( "strings" ) -// ctlHandler is a function that handles a single ctl command. -type ctlHandler func(args []string, ctx HandlerCtx) error +// rdwrHandler is a request-response ctl command handler. +type rdwrHandler func(ctx HandlerCtx, args []string) ([]byte, error) -// ctlDispatch returns a Write handler that dispatches based on the first -// whitespace-delimited word in the input. The map keys are command names; -// values are the handlers. If the command is unknown, an error is returned. -func ctlDispatch(handlers map[string]ctlHandler) func(HandlerCtx, []byte) error { - return func(ctx HandlerCtx, data []byte) error { +// rdwrDispatch returns a Request handler that dispatches based on the first +// whitespace-delimited word in the input. +func rdwrDispatch(handlers map[string]rdwrHandler) func(HandlerCtx, []byte) ([]byte, error) { + return func(ctx HandlerCtx, data []byte) ([]byte, error) { input := strings.TrimSpace(string(data)) if input == "" { - return nil + return nil, nil } parts := strings.Fields(input) cmd := parts[0] h, ok := handlers[cmd] if !ok { - return fmt.Errorf("unknown ctl command: %s", cmd) + return nil, fmt.Errorf("unknown ctl command: %s", cmd) } - return h(parts[1:], ctx) + return h(ctx, parts[1:]) } } diff --git a/fs/handlers.go b/fs/handlers.go index 6c37b99..dcd8817 100644 --- a/fs/handlers.go +++ b/fs/handlers.go @@ -97,25 +97,25 @@ func readTools(_ HandlerCtx) ([]byte, error) { return []byte(sb.String()), nil } -func writeRootCtl(ctx HandlerCtx, data []byte) error { - handlers := map[string]ctlHandler{ - "invalidate": func(_ []string, _ HandlerCtx) error { +func requestRootCtl(ctx HandlerCtx, data []byte) ([]byte, error) { + handlers := map[string]rdwrHandler{ + "invalidate": func(ctx HandlerCtx, _ []string) ([]byte, error) { if ctx.Models != nil { ctx.Models.Invalidate() } if ctx.Invalidate != nil { ctx.Invalidate() } - return nil + return []byte("ok\n"), nil }, - "kill": func(_ []string, _ HandlerCtx) error { + "kill": func(ctx HandlerCtx, _ []string) ([]byte, error) { if ctx.Shutdown != nil { ctx.Shutdown() } - return nil + return []byte("ok\n"), nil }, } - return ctlDispatch(handlers)(ctx, data) + return rdwrDispatch(handlers)(ctx, data) } func readEventwait(_ HandlerCtx) ([]byte, error) { @@ -320,38 +320,40 @@ func readSessionConnected(ctx HandlerCtx) ([]byte, error) { return []byte("false\n"), nil } -func writeSessionCtl(ctx HandlerCtx, data []byte) error { - handlers := map[string]ctlHandler{ - "kill": func(_ []string, _ HandlerCtx) error { +func requestSessionCtl(ctx HandlerCtx, data []byte) ([]byte, error) { + handlers := map[string]rdwrHandler{ + "kill": func(ctx HandlerCtx, _ []string) ([]byte, error) { ctx.Remove() - return nil + return []byte("ok\n"), nil }, - ".": func(_ []string, _ HandlerCtx) error { + ".": func(ctx HandlerCtx, _ []string) ([]byte, error) { ctx.Remove() - return nil + return []byte("ok\n"), nil }, - "save": func(_ []string, _ HandlerCtx) error { + "save": func(ctx HandlerCtx, _ []string) ([]byte, error) { if ctx.Session.Core != nil { ctx.Session.Core.Save() } - return nil + return []byte("ok\n"), nil }, - "invalidate": func(_ []string, _ HandlerCtx) error { + "invalidate": func(ctx HandlerCtx, _ []string) ([]byte, error) { ctx.Session.InvalidateModelsCache() - return nil + return []byte("ok\n"), nil }, - "pause": func(_ []string, _ HandlerCtx) error { - return ctx.Session.Pause() + "pause": func(ctx HandlerCtx, _ []string) ([]byte, error) { + if err := ctx.Session.Pause(); err != nil { + return nil, err + } + return []byte("ok\n"), nil }, - "resume": func(_ []string, _ HandlerCtx) error { - return ctx.Session.Resume() + "resume": func(ctx HandlerCtx, _ []string) ([]byte, error) { + if err := ctx.Session.Resume(); err != nil { + return nil, err + } + return []byte("ok\n"), nil }, } - err := ctlDispatch(handlers)(ctx, data) - if err != nil { - return fmt.Errorf("session ctl: %w", err) - } - return nil + return rdwrDispatch(handlers)(ctx, data) } func readSessionName(ctx HandlerCtx) ([]byte, error) { @@ -656,24 +658,9 @@ func writeAgentCfg(ctx HandlerCtx, data []byte) error { } func requestAgentCtl(ctx HandlerCtx, data []byte) ([]byte, error) { - input := strings.TrimSpace(string(data)) - if input == "" { - return nil, nil - } - parts := strings.Fields(input) - cmd := parts[0] - args := parts[1:] - - h, ok := agentCtlHandlers[cmd] - if !ok { - return nil, fmt.Errorf("unknown ctl command: %s", cmd) - } - return h(ctx, args) + return rdwrDispatch(agentCtlHandlers)(ctx, data) } -// rdwrHandler is a request-response ctl command handler. -type rdwrHandler func(ctx HandlerCtx, args []string) ([]byte, error) - var agentCtlHandlers = map[string]rdwrHandler{ "kill": func(ctx HandlerCtx, _ []string) ([]byte, error) { if ctx.Session.Core != nil { @@ -784,6 +771,9 @@ var agentCtlHandlers = map[string]rdwrHandler{ ctx.Agent.SetCWD(dir) return []byte(dir + "\n"), nil }, + "systemprompt": func(ctx HandlerCtx, _ []string) ([]byte, error) { + return []byte(ctx.Agent.SystemPrompt() + "\n"), nil + }, } func readAgentStats(ctx HandlerCtx) ([]byte, error) { @@ -793,10 +783,6 @@ func readAgentStats(ctx HandlerCtx) ([]byte, error) { return []byte(s), nil } -func readAgentSystemprompt(ctx HandlerCtx) ([]byte, error) { - return []byte(ctx.Agent.SystemPrompt() + "\n"), nil -} - func readAgentName(ctx HandlerCtx) ([]byte, error) { return []byte(ctx.Agent.Name() + "\n"), nil } @@ -883,6 +869,3 @@ func removeProcess(ctx HandlerCtx) error { return nil } -func nopRead(_ HandlerCtx) ([]byte, error) { - return nil, nil -} diff --git a/fs/spec.go b/fs/spec.go index dafdae5..6b10b31 100644 --- a/fs/spec.go +++ b/fs/spec.go @@ -108,7 +108,7 @@ var treeSpec = DirNode("/", FileNode("ctl", 0666, Doc("Server control. Write: 'invalidate' (refresh caches), 'kill' (shutdown server)"), - Write(writeRootCtl), + Request(requestRootCtl), ), FileNode("eventwait", 0444, @@ -176,9 +176,8 @@ FileNode("generate", 0666, ), FileNode("ctl", 0666, - Doc("Session control. Write: 'kill' (destroy), 'save', 'invalidate', 'pause', 'resume'."), - Read(nopRead), - Write(writeSessionCtl), + Doc("Session control: kill, save, invalidate, pause, resume."), + Request(requestSessionCtl), ), FileNode("name", 0666, @@ -274,11 +273,6 @@ FileNode("generate", 0666, Read(readAgentStats), ), - FileNode("systemprompt", 0444, - Doc("Fully rendered system prompt for this agent."), - Read(readAgentSystemprompt), - ), - FileNode("name", 0666, Doc("Agent display name. Read: current name. Write: rename agent."), Read(readAgentName), diff --git a/prompts/system_prompt.md b/prompts/system_prompt.md index d758bcc..68a91d2 100644 --- a/prompts/system_prompt.md +++ b/prompts/system_prompt.md @@ -181,10 +181,8 @@ Each agent has its own directory. The `{aname}` is the agent's numeric ID (stabl | `chat.raw` | read | Conversation (full markup with block markers) | | `statewait` | read | Blocks until state changes; returns new value | | `cfg` | r/w | Agent config: backend, model, cwd, params (key=value) | -| `ctl` | rdwr | Control: stop, compact, clear, inject, agent, model, models, tools, tool_load, cwd, name | +| `ctl` | rdwr | Control: stop, compact, clear, inject, agent, model, models, tools, tool_load, cwd, name, systemprompt | | `stats` | read | Agent statistics (usage=, cost=, ctxsz=) | -| `models` | read | Available models for current backend | -| `systemprompt` | read | Fully rendered system prompt | | `tools` | r/w | Write tool name to load. Read: list loaded tools | | `proc/` | dir | Detached background processes |