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:
parent
0025003aa0
commit
cbfc08875b
|
|
@ -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"
|
||||
|
|
|
|||
19
fs/ctl.go
19
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:])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
12
fs/spec.go
12
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),
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue