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"
|
SESSION_FILES="env paused connected ctl name id bypass"
|
||||||
|
|
||||||
# Agent-level files (exist under session/{name}/agent/{name}/)
|
# 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() {
|
is_root_path() {
|
||||||
local path="$1"
|
local path="$1"
|
||||||
|
|
|
||||||
19
fs/ctl.go
19
fs/ctl.go
|
|
@ -5,24 +5,23 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ctlHandler is a function that handles a single ctl command.
|
// rdwrHandler is a request-response ctl command handler.
|
||||||
type ctlHandler func(args []string, ctx HandlerCtx) error
|
type rdwrHandler func(ctx HandlerCtx, args []string) ([]byte, error)
|
||||||
|
|
||||||
// ctlDispatch returns a Write handler that dispatches based on the first
|
// rdwrDispatch returns a Request handler that dispatches based on the first
|
||||||
// whitespace-delimited word in the input. The map keys are command names;
|
// whitespace-delimited word in the input.
|
||||||
// values are the handlers. If the command is unknown, an error is returned.
|
func rdwrDispatch(handlers map[string]rdwrHandler) func(HandlerCtx, []byte) ([]byte, error) {
|
||||||
func ctlDispatch(handlers map[string]ctlHandler) func(HandlerCtx, []byte) error {
|
return func(ctx HandlerCtx, data []byte) ([]byte, error) {
|
||||||
return func(ctx HandlerCtx, data []byte) error {
|
|
||||||
input := strings.TrimSpace(string(data))
|
input := strings.TrimSpace(string(data))
|
||||||
if input == "" {
|
if input == "" {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
parts := strings.Fields(input)
|
parts := strings.Fields(input)
|
||||||
cmd := parts[0]
|
cmd := parts[0]
|
||||||
h, ok := handlers[cmd]
|
h, ok := handlers[cmd]
|
||||||
if !ok {
|
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
|
return []byte(sb.String()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRootCtl(ctx HandlerCtx, data []byte) error {
|
func requestRootCtl(ctx HandlerCtx, data []byte) ([]byte, error) {
|
||||||
handlers := map[string]ctlHandler{
|
handlers := map[string]rdwrHandler{
|
||||||
"invalidate": func(_ []string, _ HandlerCtx) error {
|
"invalidate": func(ctx HandlerCtx, _ []string) ([]byte, error) {
|
||||||
if ctx.Models != nil {
|
if ctx.Models != nil {
|
||||||
ctx.Models.Invalidate()
|
ctx.Models.Invalidate()
|
||||||
}
|
}
|
||||||
if ctx.Invalidate != nil {
|
if ctx.Invalidate != nil {
|
||||||
ctx.Invalidate()
|
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 {
|
if ctx.Shutdown != nil {
|
||||||
ctx.Shutdown()
|
ctx.Shutdown()
|
||||||
}
|
}
|
||||||
return nil
|
return []byte("ok\n"), nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return ctlDispatch(handlers)(ctx, data)
|
return rdwrDispatch(handlers)(ctx, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readEventwait(_ HandlerCtx) ([]byte, error) {
|
func readEventwait(_ HandlerCtx) ([]byte, error) {
|
||||||
|
|
@ -320,38 +320,40 @@ func readSessionConnected(ctx HandlerCtx) ([]byte, error) {
|
||||||
return []byte("false\n"), nil
|
return []byte("false\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeSessionCtl(ctx HandlerCtx, data []byte) error {
|
func requestSessionCtl(ctx HandlerCtx, data []byte) ([]byte, error) {
|
||||||
handlers := map[string]ctlHandler{
|
handlers := map[string]rdwrHandler{
|
||||||
"kill": func(_ []string, _ HandlerCtx) error {
|
"kill": func(ctx HandlerCtx, _ []string) ([]byte, error) {
|
||||||
ctx.Remove()
|
ctx.Remove()
|
||||||
return nil
|
return []byte("ok\n"), nil
|
||||||
},
|
},
|
||||||
".": func(_ []string, _ HandlerCtx) error {
|
".": func(ctx HandlerCtx, _ []string) ([]byte, error) {
|
||||||
ctx.Remove()
|
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 {
|
if ctx.Session.Core != nil {
|
||||||
ctx.Session.Core.Save()
|
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()
|
ctx.Session.InvalidateModelsCache()
|
||||||
return nil
|
return []byte("ok\n"), nil
|
||||||
},
|
},
|
||||||
"pause": func(_ []string, _ HandlerCtx) error {
|
"pause": func(ctx HandlerCtx, _ []string) ([]byte, error) {
|
||||||
return ctx.Session.Pause()
|
if err := ctx.Session.Pause(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte("ok\n"), nil
|
||||||
},
|
},
|
||||||
"resume": func(_ []string, _ HandlerCtx) error {
|
"resume": func(ctx HandlerCtx, _ []string) ([]byte, error) {
|
||||||
return ctx.Session.Resume()
|
if err := ctx.Session.Resume(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte("ok\n"), nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := ctlDispatch(handlers)(ctx, data)
|
return rdwrDispatch(handlers)(ctx, data)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("session ctl: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readSessionName(ctx HandlerCtx) ([]byte, error) {
|
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) {
|
func requestAgentCtl(ctx HandlerCtx, data []byte) ([]byte, error) {
|
||||||
input := strings.TrimSpace(string(data))
|
return rdwrDispatch(agentCtlHandlers)(ctx, 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// rdwrHandler is a request-response ctl command handler.
|
|
||||||
type rdwrHandler func(ctx HandlerCtx, args []string) ([]byte, error)
|
|
||||||
|
|
||||||
var agentCtlHandlers = map[string]rdwrHandler{
|
var agentCtlHandlers = map[string]rdwrHandler{
|
||||||
"kill": func(ctx HandlerCtx, _ []string) ([]byte, error) {
|
"kill": func(ctx HandlerCtx, _ []string) ([]byte, error) {
|
||||||
if ctx.Session.Core != nil {
|
if ctx.Session.Core != nil {
|
||||||
|
|
@ -784,6 +771,9 @@ var agentCtlHandlers = map[string]rdwrHandler{
|
||||||
ctx.Agent.SetCWD(dir)
|
ctx.Agent.SetCWD(dir)
|
||||||
return []byte(dir + "\n"), nil
|
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) {
|
func readAgentStats(ctx HandlerCtx) ([]byte, error) {
|
||||||
|
|
@ -793,10 +783,6 @@ func readAgentStats(ctx HandlerCtx) ([]byte, error) {
|
||||||
return []byte(s), nil
|
return []byte(s), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAgentSystemprompt(ctx HandlerCtx) ([]byte, error) {
|
|
||||||
return []byte(ctx.Agent.SystemPrompt() + "\n"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readAgentName(ctx HandlerCtx) ([]byte, error) {
|
func readAgentName(ctx HandlerCtx) ([]byte, error) {
|
||||||
return []byte(ctx.Agent.Name() + "\n"), nil
|
return []byte(ctx.Agent.Name() + "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
@ -883,6 +869,3 @@ func removeProcess(ctx HandlerCtx) error {
|
||||||
return nil
|
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,
|
FileNode("ctl", 0666,
|
||||||
Doc("Server control. Write: 'invalidate' (refresh caches), 'kill' (shutdown server)"),
|
Doc("Server control. Write: 'invalidate' (refresh caches), 'kill' (shutdown server)"),
|
||||||
Write(writeRootCtl),
|
Request(requestRootCtl),
|
||||||
),
|
),
|
||||||
|
|
||||||
FileNode("eventwait", 0444,
|
FileNode("eventwait", 0444,
|
||||||
|
|
@ -176,9 +176,8 @@ FileNode("generate", 0666,
|
||||||
),
|
),
|
||||||
|
|
||||||
FileNode("ctl", 0666,
|
FileNode("ctl", 0666,
|
||||||
Doc("Session control. Write: 'kill' (destroy), 'save', 'invalidate', 'pause', 'resume'."),
|
Doc("Session control: kill, save, invalidate, pause, resume."),
|
||||||
Read(nopRead),
|
Request(requestSessionCtl),
|
||||||
Write(writeSessionCtl),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
FileNode("name", 0666,
|
FileNode("name", 0666,
|
||||||
|
|
@ -274,11 +273,6 @@ FileNode("generate", 0666,
|
||||||
Read(readAgentStats),
|
Read(readAgentStats),
|
||||||
),
|
),
|
||||||
|
|
||||||
FileNode("systemprompt", 0444,
|
|
||||||
Doc("Fully rendered system prompt for this agent."),
|
|
||||||
Read(readAgentSystemprompt),
|
|
||||||
),
|
|
||||||
|
|
||||||
FileNode("name", 0666,
|
FileNode("name", 0666,
|
||||||
Doc("Agent display name. Read: current name. Write: rename agent."),
|
Doc("Agent display name. Read: current name. Write: rename agent."),
|
||||||
Read(readAgentName),
|
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) |
|
| `chat.raw` | read | Conversation (full markup with block markers) |
|
||||||
| `statewait` | read | Blocks until state changes; returns new value |
|
| `statewait` | read | Blocks until state changes; returns new value |
|
||||||
| `cfg` | r/w | Agent config: backend, model, cwd, params (key=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=) |
|
| `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 |
|
| `tools` | r/w | Write tool name to load. Read: list loaded tools |
|
||||||
| `proc/` | dir | Detached background processes |
|
| `proc/` | dir | Detached background processes |
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue