fs: unify ctl — all levels use rdwrHandler + rdwrDispatch

Root, session, and agent ctl files all share the same dispatch
mechanism: rdwrHandler map → rdwrDispatch → Request (rdwr).

- ctlHandler/ctlDispatch (write-only) removed
- Root and session ctl upgraded to Request (rdwr), return 'ok'
- systemprompt moved to agent ctl command
- nopRead removed (dead)
This commit is contained in:
Ollie Agent 2026-08-09 02:58:09 +02:00
parent 0025003aa0
commit cbfc08875b
5 changed files with 46 additions and 72 deletions

View File

@ -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"

View File

@ -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:])
}
}

View File

@ -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
}

View File

@ -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),

View File

@ -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 |