olliesrv/fs: eliminate Hctx adapter struct
Handlers now accept their actual dependencies directly: - Root handlers: ModelCache, Broker, context.Context, etc. - Session handlers: *SessionNode, removeFn, renameFn - Agent handlers: *agent.Agent, *AgentLog, *SessionNode Ctl dispatch tables are now per-instance closures built at binding time rather than package-level vars with generic Hctx. Also inlined the intermediate internalBinding type — adapt functions now build fsedsl.Binding directly from raw data. Removed: Hctx struct, rdwrHandler type, rdwrDispatch func, bypassBindings helper, processBindings helper, internalBinding type.
This commit is contained in:
parent
95eb402c68
commit
1474eeaaed
|
|
@ -5,23 +5,17 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// rdwrHandler is a request-response ctl command handler.
|
||||
type rdwrHandler func(ctx Hctx, args []string) ([]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(Hctx, []byte) ([]byte, error) {
|
||||
return func(ctx Hctx, data []byte) ([]byte, error) {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parts := strings.Fields(input)
|
||||
cmd := parts[0]
|
||||
h, ok := handlers[cmd]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown ctl command: %s", cmd)
|
||||
}
|
||||
return h(ctx, parts[1:])
|
||||
// dispatch routes a command to the matching handler based on the first word.
|
||||
func dispatch(handlers map[string]func([]string) ([]byte, error), data []byte) ([]byte, error) {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parts := strings.Fields(input)
|
||||
cmd := parts[0]
|
||||
h, ok := handlers[cmd]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown ctl command: %s", cmd)
|
||||
}
|
||||
return h(parts[1:])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,46 +14,45 @@ import (
|
|||
"ollie/cmd/olliesrv/internal/session"
|
||||
)
|
||||
|
||||
func requestAgentNew(ctx Hctx, data []byte) ([]byte, error) {
|
||||
args := strings.Fields(string(data))
|
||||
ag, err := session.CreateAgent(ctx.Session.Name(), args)
|
||||
func requestAgentNew(s *SessionNode, data []byte) ([]byte, error) {
|
||||
ag, err := session.CreateAgent(s.Name(), strings.Fields(string(data)))
|
||||
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)
|
||||
wireAgentEvents(ctx.Session.ID(), ag, al)
|
||||
s.SetAgentLog(ag.ID(), al)
|
||||
wireAgentEvents(s.ID(), ag, al)
|
||||
return []byte(ag.ID() + "\n"), nil
|
||||
}
|
||||
|
||||
func writeAgentPrompt(ctx Hctx, data []byte) error {
|
||||
func writeAgentPrompt(s *SessionNode, ag *agent.Agent, al *AgentLog, data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
if input == "/invalidate" {
|
||||
ctx.Session.InvalidateModelsCache()
|
||||
s.InvalidateModelsCache()
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
ctx.Agent.Submit(ctx.Session.Ctx(), input)
|
||||
ctx.AgentLog.EnsureTrailingNewline()
|
||||
ag.Submit(s.Ctx(), input)
|
||||
al.EnsureTrailingNewline()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAgentFifoIn(ctx Hctx, data []byte) error {
|
||||
func writeAgentFifoIn(ag *agent.Agent, data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil
|
||||
}
|
||||
ctx.Agent.Queue(input)
|
||||
ag.Queue(input)
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAgentFifoOut(ctx Hctx) ([]byte, error) {
|
||||
item, ok := ctx.Agent.PopQueue()
|
||||
func readAgentFifoOut(ag *agent.Agent) ([]byte, error) {
|
||||
item, ok := ag.PopQueue()
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -61,37 +60,34 @@ func readAgentFifoOut(ctx Hctx) ([]byte, error) {
|
|||
}
|
||||
|
||||
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 Hctx) ([]byte, error) {
|
||||
ctx.AgentLog.mu.RLock()
|
||||
defer ctx.AgentLog.mu.RUnlock()
|
||||
data := make([]byte, len(ctx.AgentLog.log))
|
||||
copy(data, ctx.AgentLog.log)
|
||||
func readAgentChat(al *AgentLog) ([]byte, error) {
|
||||
al.mu.RLock()
|
||||
defer al.mu.RUnlock()
|
||||
data := make([]byte, len(al.log))
|
||||
copy(data, al.log)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readAgentChatText(ctx Hctx) ([]byte, error) {
|
||||
ctx.AgentLog.mu.RLock()
|
||||
defer ctx.AgentLog.mu.RUnlock()
|
||||
return stripMarkers(ctx.AgentLog.log), nil
|
||||
func readAgentChatText(al *AgentLog) ([]byte, error) {
|
||||
al.mu.RLock()
|
||||
defer al.mu.RUnlock()
|
||||
return stripMarkers(al.log), nil
|
||||
}
|
||||
|
||||
func streamAgentChat(ctx Hctx, cctx context.Context, base string) ([]byte, string, error) {
|
||||
merged, cancel := mergeCtx(ctx, cctx)
|
||||
func streamAgentChat(s *SessionNode, al *AgentLog, cctx context.Context, base string) ([]byte, string, error) {
|
||||
merged, cancel := mergeCtx(s.Ctx(), cctx)
|
||||
defer cancel()
|
||||
return streamChat(ctx.AgentLog, merged, base)
|
||||
return streamChat(al, merged, base)
|
||||
}
|
||||
|
||||
func streamAgentChatText(ctx Hctx, cctx context.Context, base string) ([]byte, string, error) {
|
||||
merged, cancel := mergeCtx(ctx, cctx)
|
||||
func streamAgentChatText(s *SessionNode, al *AgentLog, cctx context.Context, base string) ([]byte, string, error) {
|
||||
merged, cancel := mergeCtx(s.Ctx(), cctx)
|
||||
defer cancel()
|
||||
for {
|
||||
data, nextBase, err := streamChat(ctx.AgentLog, merged, base)
|
||||
data, nextBase, err := streamChat(al, merged, base)
|
||||
if err != nil || len(data) == 0 {
|
||||
return data, nextBase, err
|
||||
}
|
||||
|
|
@ -140,63 +136,63 @@ func stripMarkers(data []byte) []byte {
|
|||
return out
|
||||
}
|
||||
|
||||
func blockAgentStateWait(ctx Hctx, cctx context.Context, base string) ([]byte, string, error) {
|
||||
merged, cancel := mergeCtx(ctx, cctx)
|
||||
func blockAgentStateWait(s *SessionNode, ag *agent.Agent, cctx context.Context, base string) ([]byte, string, error) {
|
||||
merged, cancel := mergeCtx(s.Ctx(), cctx)
|
||||
defer cancel()
|
||||
if base == "" {
|
||||
base = ctx.Agent.State()
|
||||
base = ag.State()
|
||||
}
|
||||
v, ok := ctx.Agent.WaitChange(merged, agent.WatchState, base)
|
||||
v, ok := ag.WaitChange(merged, agent.WatchState, base)
|
||||
if !ok {
|
||||
st := ctx.Agent.State()
|
||||
st := ag.State()
|
||||
return []byte(st + "\n"), st, nil
|
||||
}
|
||||
return []byte(v + "\n"), v, nil
|
||||
}
|
||||
|
||||
func readAgentLog(ctx Hctx) ([]byte, error) {
|
||||
func readAgentLog(al *AgentLog) ([]byte, error) {
|
||||
const maxWindow = 64 * 1024
|
||||
ctx.AgentLog.mu.RLock()
|
||||
log := ctx.AgentLog.log
|
||||
al.mu.RLock()
|
||||
log := al.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()
|
||||
al.mu.RUnlock()
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readAgentPlan(ctx Hctx) ([]byte, error) {
|
||||
ctx.AgentLog.mu.RLock()
|
||||
data := make([]byte, len(ctx.AgentLog.plan))
|
||||
copy(data, ctx.AgentLog.plan)
|
||||
ctx.AgentLog.mu.RUnlock()
|
||||
func readAgentPlan(al *AgentLog) ([]byte, error) {
|
||||
al.mu.RLock()
|
||||
data := make([]byte, len(al.plan))
|
||||
copy(data, al.plan)
|
||||
al.mu.RUnlock()
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func writeAgentPlan(ctx Hctx, data []byte) error {
|
||||
ctx.AgentLog.mu.Lock()
|
||||
ctx.AgentLog.plan = make([]byte, len(data))
|
||||
copy(ctx.AgentLog.plan, data)
|
||||
ctx.AgentLog.mu.Unlock()
|
||||
func writeAgentPlan(al *AgentLog, data []byte) error {
|
||||
al.mu.Lock()
|
||||
al.plan = make([]byte, len(data))
|
||||
copy(al.plan, data)
|
||||
al.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAgentCfg(ctx Hctx) ([]byte, error) {
|
||||
ctx.AgentLog.mu.RLock()
|
||||
defer ctx.AgentLog.mu.RUnlock()
|
||||
p := ctx.Agent.GenParams()
|
||||
func readAgentCfg(s *SessionNode, ag *agent.Agent, al *AgentLog) ([]byte, error) {
|
||||
al.mu.RLock()
|
||||
defer al.mu.RUnlock()
|
||||
p := ag.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, "name=%s\n", s.ID())
|
||||
fmt.Fprintf(&sb, "backend=%s\n", ag.BackendName())
|
||||
fmt.Fprintf(&sb, "model=%s\n", ag.ModelName())
|
||||
fmt.Fprintf(&sb, "id=%s\n", ag.ID())
|
||||
fmt.Fprintf(&sb, "profile=%s\n", ag.Profile())
|
||||
fmt.Fprintf(&sb, "displayName=%s\n", ag.Name())
|
||||
fmt.Fprintf(&sb, "cwd=%s\n", ag.Cwd())
|
||||
fmt.Fprintf(&sb, "remote=%s\n", al.remote)
|
||||
fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens)
|
||||
if p.Temperature != nil {
|
||||
fmt.Fprintf(&sb, "temperature=%g\n", *p.Temperature)
|
||||
|
|
@ -213,7 +209,7 @@ func readAgentCfg(ctx Hctx) ([]byte, error) {
|
|||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func writeAgentCfg(ctx Hctx, data []byte) error {
|
||||
func writeAgentCfg(ag *agent.Agent, data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
return nil
|
||||
|
|
@ -225,7 +221,7 @@ func writeAgentCfg(ctx Hctx, data []byte) error {
|
|||
switch parts[0] {
|
||||
case "name":
|
||||
if strings.TrimSpace(parts[1]) != "" {
|
||||
ctx.Agent.SetName(strings.TrimSpace(parts[1]))
|
||||
ag.SetName(strings.TrimSpace(parts[1]))
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown cfg key: %s", parts[0])
|
||||
|
|
@ -233,123 +229,120 @@ func writeAgentCfg(ctx Hctx, data []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func requestAgentCtl(ctx Hctx, data []byte) ([]byte, error) {
|
||||
return rdwrDispatch(agentCtlHandlers)(ctx, data)
|
||||
}
|
||||
|
||||
var agentCtlHandlers = map[string]rdwrHandler{
|
||||
"kill": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
if ctx.Session.Session != nil {
|
||||
ctx.Session.Session.RemoveAgent(ctx.Agent.ID())
|
||||
session.PublishEvent("session."+ctx.Session.ID()+".agent."+ctx.Agent.ID()+".kill", "")
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"stop": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
ctx.Agent.Interrupt(agent.ErrInterrupted)
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"compact": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
if err := ctx.Agent.Compact(ctx.Session.Ctx()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"clear": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
if err := ctx.Agent.Clear(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"inject": func(ctx Hctx, args []string) ([]byte, error) {
|
||||
text := strings.Join(args, " ")
|
||||
if text == "" {
|
||||
return nil, fmt.Errorf("inject requires text")
|
||||
}
|
||||
ctx.Agent.InjectRewrite(text)
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"i": func(ctx Hctx, args []string) ([]byte, error) {
|
||||
text := strings.Join(args, " ")
|
||||
if text == "" {
|
||||
return nil, fmt.Errorf("inject requires text")
|
||||
}
|
||||
ctx.Agent.InjectRewrite(text)
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"agent": func(ctx Hctx, args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return []byte(ctx.Agent.Profile() + "\n"), nil
|
||||
}
|
||||
if err := ctx.Agent.SwitchProfile(args[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(args[0] + "\n"), nil
|
||||
},
|
||||
"model": func(ctx Hctx, args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
if be := ctx.Agent.Backend(); be != nil {
|
||||
return []byte(be.Model() + "\n"), nil
|
||||
func agentCtlHandler(s *SessionNode, ag *agent.Agent) func([]byte) ([]byte, error) {
|
||||
handlers := map[string]func([]string) ([]byte, error){
|
||||
"kill": func(_ []string) ([]byte, error) {
|
||||
if s.Session != nil {
|
||||
s.Session.RemoveAgent(ag.ID())
|
||||
session.PublishEvent("session."+s.ID()+".agent."+ag.ID()+".kill", "")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if be := ctx.Agent.Backend(); be != nil {
|
||||
be.SetModel(strings.Join(args, " "))
|
||||
}
|
||||
return []byte(strings.Join(args, " ") + "\n"), nil
|
||||
},
|
||||
"models": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
return []byte(ctx.Session.CachedListModels() + "\n"), nil
|
||||
},
|
||||
"tools": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
return readAgentTools(ctx)
|
||||
},
|
||||
"tool_load": func(ctx Hctx, args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("tool_load requires a tool name")
|
||||
}
|
||||
ts := ctx.Agent.ToolServer()
|
||||
if ts == nil {
|
||||
return nil, fmt.Errorf("no tool server")
|
||||
}
|
||||
if err := ts.LoadTool(args[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx.Agent.RefreshTools()
|
||||
return []byte(args[0] + "\n"), nil
|
||||
},
|
||||
"backend": func(ctx Hctx, args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
if be := ctx.Agent.Backend(); be != nil {
|
||||
return []byte(be.Name() + "\n"), nil
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"stop": func(_ []string) ([]byte, error) {
|
||||
ag.Interrupt(agent.ErrInterrupted)
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"compact": func(_ []string) ([]byte, error) {
|
||||
if err := ag.Compact(s.Ctx()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("backend switching not supported via ctl; use cfg")
|
||||
},
|
||||
"name": func(ctx Hctx, args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return []byte(ctx.Agent.Name() + "\n"), nil
|
||||
}
|
||||
ctx.Agent.SetName(strings.Join(args, " "))
|
||||
return []byte(strings.Join(args, " ") + "\n"), nil
|
||||
},
|
||||
"cwd": func(ctx Hctx, args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return []byte(ctx.Agent.Cwd() + "\n"), nil
|
||||
}
|
||||
dir := paths.ExpandHome(strings.Join(args, " "))
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
return nil, fmt.Errorf("cwd: %w", err)
|
||||
}
|
||||
ctx.Agent.SetCWD(dir)
|
||||
return []byte(dir + "\n"), nil
|
||||
},
|
||||
"systemprompt": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
return []byte(ctx.Agent.SystemPrompt() + "\n"), nil
|
||||
},
|
||||
"help": func(_ Hctx, _ []string) ([]byte, error) {
|
||||
return []byte(`stop — interrupt running agent
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"clear": func(_ []string) ([]byte, error) {
|
||||
if err := ag.Clear(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"inject": func(args []string) ([]byte, error) {
|
||||
text := strings.Join(args, " ")
|
||||
if text == "" {
|
||||
return nil, fmt.Errorf("inject requires text")
|
||||
}
|
||||
ag.InjectRewrite(text)
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"i": func(args []string) ([]byte, error) {
|
||||
text := strings.Join(args, " ")
|
||||
if text == "" {
|
||||
return nil, fmt.Errorf("inject requires text")
|
||||
}
|
||||
ag.InjectRewrite(text)
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"agent": func(args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return []byte(ag.Profile() + "\n"), nil
|
||||
}
|
||||
if err := ag.SwitchProfile(args[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(args[0] + "\n"), nil
|
||||
},
|
||||
"model": func(args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
if be := ag.Backend(); be != nil {
|
||||
return []byte(be.Model() + "\n"), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
if be := ag.Backend(); be != nil {
|
||||
be.SetModel(strings.Join(args, " "))
|
||||
}
|
||||
return []byte(strings.Join(args, " ") + "\n"), nil
|
||||
},
|
||||
"models": func(_ []string) ([]byte, error) {
|
||||
return []byte(s.CachedListModels() + "\n"), nil
|
||||
},
|
||||
"tools": func(_ []string) ([]byte, error) {
|
||||
return readAgentTools(ag)
|
||||
},
|
||||
"tool_load": func(args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("tool_load requires a tool name")
|
||||
}
|
||||
ts := ag.ToolServer()
|
||||
if ts == nil {
|
||||
return nil, fmt.Errorf("no tool server")
|
||||
}
|
||||
if err := ts.LoadTool(args[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ag.RefreshTools()
|
||||
return []byte(args[0] + "\n"), nil
|
||||
},
|
||||
"backend": func(args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
if be := ag.Backend(); be != nil {
|
||||
return []byte(be.Name() + "\n"), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("backend switching not supported via ctl; use cfg")
|
||||
},
|
||||
"name": func(args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return []byte(ag.Name() + "\n"), nil
|
||||
}
|
||||
ag.SetName(strings.Join(args, " "))
|
||||
return []byte(strings.Join(args, " ") + "\n"), nil
|
||||
},
|
||||
"cwd": func(args []string) ([]byte, error) {
|
||||
if len(args) == 0 {
|
||||
return []byte(ag.Cwd() + "\n"), nil
|
||||
}
|
||||
dir := paths.ExpandHome(strings.Join(args, " "))
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
return nil, fmt.Errorf("cwd: %w", err)
|
||||
}
|
||||
ag.SetCWD(dir)
|
||||
return []byte(dir + "\n"), nil
|
||||
},
|
||||
"systemprompt": func(_ []string) ([]byte, error) {
|
||||
return []byte(ag.SystemPrompt() + "\n"), nil
|
||||
},
|
||||
"help": func(_ []string) ([]byte, error) {
|
||||
return []byte(`stop — interrupt running agent
|
||||
compact — compact context
|
||||
clear — clear history
|
||||
inject <text> — inject prompt mid-turn (overwrites pending)
|
||||
|
|
@ -364,18 +357,22 @@ backend — show backend
|
|||
systemprompt — show rendered system prompt
|
||||
help — this message
|
||||
`), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
return func(data []byte) ([]byte, error) {
|
||||
return dispatch(handlers, data)
|
||||
}
|
||||
}
|
||||
|
||||
func readAgentStats(ctx Hctx) ([]byte, error) {
|
||||
s := "usage=" + ctx.Agent.UsageStr() + "\n" +
|
||||
"cost=" + ctx.Agent.CostStr() + "\n" +
|
||||
"ctxsz=" + ctx.Agent.CtxSz() + "\n"
|
||||
func readAgentStats(ag *agent.Agent) ([]byte, error) {
|
||||
s := "usage=" + ag.UsageStr() + "\n" +
|
||||
"cost=" + ag.CostStr() + "\n" +
|
||||
"ctxsz=" + ag.CtxSz() + "\n"
|
||||
return []byte(s), nil
|
||||
}
|
||||
|
||||
func readAgentTools(ctx Hctx) ([]byte, error) {
|
||||
ts := ctx.Agent.ToolServer()
|
||||
func readAgentTools(ag *agent.Agent) ([]byte, error) {
|
||||
ts := ag.ToolServer()
|
||||
if ts == nil {
|
||||
return []byte("(no tool server)\n"), nil
|
||||
}
|
||||
|
|
@ -394,38 +391,38 @@ func readAgentTools(ctx Hctx) ([]byte, error) {
|
|||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func writeAgentTools(ctx Hctx, data []byte) error {
|
||||
func writeAgentTools(ag *agent.Agent, data []byte) error {
|
||||
name := strings.TrimSpace(string(data))
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
ts := ctx.Agent.ToolServer()
|
||||
ts := ag.ToolServer()
|
||||
if ts == nil {
|
||||
return fmt.Errorf("no tool server")
|
||||
}
|
||||
if err := ts.LoadTool(name); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Agent.RefreshTools()
|
||||
ag.RefreshTools()
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAgentName(ctx Hctx) ([]byte, error) {
|
||||
return []byte(ctx.Agent.Name() + "\n"), nil
|
||||
func readAgentName(ag *agent.Agent) ([]byte, error) {
|
||||
return []byte(ag.Name() + "\n"), nil
|
||||
}
|
||||
|
||||
func writeAgentName(ctx Hctx, data []byte) error {
|
||||
func writeAgentName(s *SessionNode, ag *agent.Agent, data []byte) error {
|
||||
newName := strings.TrimSpace(string(data))
|
||||
if newName == "" {
|
||||
return nil
|
||||
}
|
||||
ctx.Agent.SetName(newName)
|
||||
session.PersistSession(ctx.Session.Name())
|
||||
ag.SetName(newName)
|
||||
session.PersistSession(s.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAgentID(ctx Hctx) ([]byte, error) {
|
||||
return []byte(ctx.Agent.ID() + "\n"), nil
|
||||
func readAgentID(ag *agent.Agent) ([]byte, error) {
|
||||
return []byte(ag.ID() + "\n"), nil
|
||||
}
|
||||
|
||||
func streamChat(al *AgentLog, cctx context.Context, base string) ([]byte, string, error) {
|
||||
|
|
@ -459,34 +456,8 @@ func streamChat(al *AgentLog, cctx context.Context, base string) ([]byte, string
|
|||
return data, fmt.Sprintf("%d", newOffset), nil
|
||||
}
|
||||
|
||||
// processBindings returns bindings for detached processes.
|
||||
func processBindings(ctx Hctx) ([]internalBinding, error) {
|
||||
if ctx.Session == nil {
|
||||
return nil, nil
|
||||
}
|
||||
conn := ctx.Session.DialToolServer()
|
||||
if conn == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer conn.Close()
|
||||
raw := conn.ListDetachedRaw()
|
||||
out := make([]internalBinding, 0, len(raw))
|
||||
for _, r := range raw {
|
||||
if m, ok := r.(map[string]any); ok {
|
||||
if pid, ok := m["pid"].(int); ok {
|
||||
out = append(out, internalBinding{
|
||||
Name: strconv.Itoa(pid),
|
||||
data: pid,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func readProcess(ctx Hctx) ([]byte, error) {
|
||||
pid := ctx.Data.(int)
|
||||
conn := ctx.Session.DialToolServer()
|
||||
func readProcess(s *SessionNode, pid int) ([]byte, error) {
|
||||
conn := s.DialToolServer()
|
||||
if conn == nil {
|
||||
return nil, fmt.Errorf("no tool server")
|
||||
}
|
||||
|
|
@ -498,9 +469,8 @@ func readProcess(ctx Hctx) ([]byte, error) {
|
|||
return []byte(output), nil
|
||||
}
|
||||
|
||||
func readProcessStatus(ctx Hctx) ([]byte, error) {
|
||||
pid := ctx.Data.(int)
|
||||
conn := ctx.Session.DialToolServer()
|
||||
func readProcessStatus(s *SessionNode, pid int) ([]byte, error) {
|
||||
conn := s.DialToolServer()
|
||||
if conn == nil {
|
||||
return nil, fmt.Errorf("no tool server")
|
||||
}
|
||||
|
|
@ -523,9 +493,8 @@ func readProcessStatus(ctx Hctx) ([]byte, error) {
|
|||
return nil, fmt.Errorf("process %d not found", pid)
|
||||
}
|
||||
|
||||
func writeProcessCtl(ctx Hctx, data []byte) error {
|
||||
pid := ctx.Data.(int)
|
||||
conn := ctx.Session.DialToolServer()
|
||||
func writeProcessCtl(s *SessionNode, pid int, data []byte) error {
|
||||
conn := s.DialToolServer()
|
||||
if conn == nil {
|
||||
return fmt.Errorf("no tool server")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func wireAgentEvents(sessID string, ag *agent.Agent, al *AgentLog) {
|
|||
})
|
||||
}
|
||||
|
||||
func readBackends(_ Hctx) ([]byte, error) {
|
||||
func readBackends() ([]byte, error) {
|
||||
return []byte(strings.Join(backend.Backends(), "\n") + "\n"), nil
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ var helpFn func() string
|
|||
// helpSpec stores the spec for help generation. Set during tree build.
|
||||
var helpSpec *fsedsl.FsNodeDecl
|
||||
|
||||
func readHelp(_ Hctx) ([]byte, error) {
|
||||
func readHelp() ([]byte, error) {
|
||||
if helpFn == nil {
|
||||
return []byte("help not initialized\n"), nil
|
||||
}
|
||||
|
|
@ -69,14 +69,14 @@ var (
|
|||
helpText string
|
||||
)
|
||||
|
||||
func readModels(ctx Hctx) ([]byte, error) {
|
||||
if ctx.Models != nil {
|
||||
return ctx.Models.Get(), nil
|
||||
func readModels(mc *ModelCache) ([]byte, error) {
|
||||
if mc != nil {
|
||||
return mc.Get(), nil
|
||||
}
|
||||
return []byte("(no model cache)\n"), nil
|
||||
}
|
||||
|
||||
func readAgents(_ Hctx) ([]byte, error) {
|
||||
func readAgents() ([]byte, error) {
|
||||
var sb strings.Builder
|
||||
for _, dir := range agent.AgentsDirs() {
|
||||
entries, err := os.ReadDir(dir)
|
||||
|
|
@ -93,7 +93,7 @@ func readAgents(_ Hctx) ([]byte, error) {
|
|||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func readTools(_ Hctx) ([]byte, error) {
|
||||
func readTools() ([]byte, error) {
|
||||
tools := toolsrv.DiscoverTools()
|
||||
if len(tools) == 0 {
|
||||
return []byte("(no tools found)\n"), nil
|
||||
|
|
@ -105,37 +105,38 @@ func readTools(_ Hctx) ([]byte, error) {
|
|||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func requestRootCtl(ctx Hctx, data []byte) ([]byte, error) {
|
||||
return rdwrDispatch(rootCtlHandlers)(ctx, data)
|
||||
func rootCtlHandler(mc *ModelCache, invalidate func(), shutdown func()) func([]byte) ([]byte, error) {
|
||||
handlers := map[string]func([]string) ([]byte, error){
|
||||
"invalidate": func(_ []string) ([]byte, error) {
|
||||
if mc != nil {
|
||||
mc.Invalidate()
|
||||
}
|
||||
if invalidate != nil {
|
||||
invalidate()
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"kill": func(_ []string) ([]byte, error) {
|
||||
if shutdown != nil {
|
||||
shutdown()
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
}
|
||||
return func(data []byte) ([]byte, error) {
|
||||
return dispatch(handlers, data)
|
||||
}
|
||||
}
|
||||
|
||||
var rootCtlHandlers = map[string]rdwrHandler{
|
||||
"invalidate": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
if ctx.Models != nil {
|
||||
ctx.Models.Invalidate()
|
||||
}
|
||||
if ctx.Invalidate != nil {
|
||||
ctx.Invalidate()
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"kill": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
if ctx.Shutdown != nil {
|
||||
ctx.Shutdown()
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
}
|
||||
|
||||
func readEventwait(_ Hctx) ([]byte, error) {
|
||||
func readEventwait() ([]byte, error) {
|
||||
return nil, fmt.Errorf("eventwait: use blocking read")
|
||||
}
|
||||
|
||||
func blockEventwait(_ Hctx, cctx context.Context, _ string) ([]byte, string, error) {
|
||||
func blockEventwait(cctx context.Context, _ string) ([]byte, string, error) {
|
||||
return session.WaitEvent(cctx)
|
||||
}
|
||||
|
||||
func requestGenerate(ctx Hctx, data []byte) ([]byte, error) {
|
||||
func requestGenerate(ctx context.Context, data []byte) ([]byte, error) {
|
||||
var req backend.GenerateRequest
|
||||
if err := json.Unmarshal(data, &req); err != nil {
|
||||
req.Prompt = strings.TrimSpace(string(data))
|
||||
|
|
@ -147,7 +148,7 @@ func requestGenerate(ctx Hctx, data []byte) ([]byte, error) {
|
|||
return []byte(result + "\n"), nil
|
||||
}
|
||||
|
||||
func readAliases(_ Hctx) ([]byte, error) {
|
||||
func readAliases() ([]byte, error) {
|
||||
var sb strings.Builder
|
||||
for name, sess := range session.Sessions() {
|
||||
sid := sess.ID
|
||||
|
|
@ -161,48 +162,30 @@ func readAliases(_ Hctx) ([]byte, error) {
|
|||
|
||||
var errNoBypass = fmt.Errorf("bypass broker not available")
|
||||
|
||||
func readBypassPolicy(ctx Hctx) ([]byte, error) {
|
||||
if ctx.Bypass == nil {
|
||||
func readBypassPolicy(b *bypass.Broker) ([]byte, error) {
|
||||
if b == nil {
|
||||
return nil, errNoBypass
|
||||
}
|
||||
p := ctx.Bypass.GlobalPolicy().Global()
|
||||
p := b.GlobalPolicy().Global()
|
||||
return p.Marshal()
|
||||
}
|
||||
|
||||
func writeBypassPolicy(ctx Hctx, data []byte) error {
|
||||
if ctx.Bypass == nil {
|
||||
func writeBypassPolicy(b *bypass.Broker, data []byte) error {
|
||||
if b == nil {
|
||||
return errNoBypass
|
||||
}
|
||||
var p bypass.Policy
|
||||
if err := bypass.ParsePolicy(data, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Bypass.GlobalPolicy().SetGlobal(p)
|
||||
return b.GlobalPolicy().SetGlobal(p)
|
||||
}
|
||||
|
||||
// bypassBindings returns bindings for pending bypass requests.
|
||||
func bypassBindings(ctx Hctx) ([]internalBinding, error) {
|
||||
if ctx.Bypass == nil {
|
||||
return nil, errNoBypass
|
||||
}
|
||||
pending := ctx.Bypass.Pending()
|
||||
out := make([]internalBinding, len(pending))
|
||||
for i, r := range pending {
|
||||
out[i] = internalBinding{
|
||||
Name: r.ID,
|
||||
data: r,
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func readBypassRequest(ctx Hctx) ([]byte, error) {
|
||||
req := ctx.Data.(*bypass.Request)
|
||||
func readBypassRequest(req *bypass.Request) ([]byte, error) {
|
||||
return []byte(req.Summary() + "\n"), nil
|
||||
}
|
||||
|
||||
func writeBypassRequest(ctx Hctx, data []byte) error {
|
||||
req := ctx.Data.(*bypass.Request)
|
||||
func writeBypassRequest(b *bypass.Broker, req *bypass.Request, data []byte) error {
|
||||
cmd := strings.TrimSpace(string(data))
|
||||
var res bypass.Resolution
|
||||
switch cmd {
|
||||
|
|
@ -215,7 +198,7 @@ func writeBypassRequest(ctx Hctx, data []byte) error {
|
|||
default:
|
||||
return fmt.Errorf("unknown resolution: %s (use approve/deny/persist)", cmd)
|
||||
}
|
||||
if !ctx.Bypass.Resolve(req.ID, res) {
|
||||
if !b.Resolve(req.ID, res) {
|
||||
return fmt.Errorf("request already resolved or timed out")
|
||||
}
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ import (
|
|||
"ollie/cmd/olliesrv/internal/session"
|
||||
)
|
||||
|
||||
func readSessionIdx(_ Hctx) ([]byte, error) {
|
||||
func readSessionIdx() ([]byte, error) {
|
||||
return session.BuildIndex(), nil
|
||||
}
|
||||
|
||||
func readAgentIdx(ctx Hctx) ([]byte, error) {
|
||||
return session.BuildAgentIndex(ctx.Session.Session), nil
|
||||
func readAgentIdx(sess *session.Session) ([]byte, error) {
|
||||
return session.BuildAgentIndex(sess), nil
|
||||
}
|
||||
|
||||
func requestSessionNew(_ Hctx, data []byte) ([]byte, error) {
|
||||
func requestSessionNew(data []byte) ([]byte, error) {
|
||||
args := strings.Fields(string(data))
|
||||
name := ""
|
||||
remote := ""
|
||||
|
|
@ -38,9 +38,7 @@ func requestSessionNew(_ Hctx, data []byte) ([]byte, error) {
|
|||
return []byte(sess.Name() + "\n"), nil
|
||||
}
|
||||
|
||||
|
||||
func readSessionEnv(ctx Hctx) ([]byte, error) {
|
||||
s := ctx.Session
|
||||
func readSessionEnv(s *SessionNode) ([]byte, error) {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", s.ID())
|
||||
for _, e := range os.Environ() {
|
||||
|
|
@ -52,93 +50,94 @@ func readSessionEnv(ctx Hctx) ([]byte, error) {
|
|||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func readSessionPaused(ctx Hctx) ([]byte, error) {
|
||||
if ctx.Session.IsPaused() {
|
||||
func readSessionPaused(s *SessionNode) ([]byte, error) {
|
||||
if s.IsPaused() {
|
||||
return []byte("true\n"), nil
|
||||
}
|
||||
return []byte("false\n"), nil
|
||||
}
|
||||
|
||||
func readSessionConnected(ctx Hctx) ([]byte, error) {
|
||||
if ctx.Session.IsConnected() {
|
||||
func readSessionConnected(s *SessionNode) ([]byte, error) {
|
||||
if s.IsConnected() {
|
||||
return []byte("true\n"), nil
|
||||
}
|
||||
return []byte("false\n"), nil
|
||||
}
|
||||
|
||||
func requestSessionCtl(ctx Hctx, data []byte) ([]byte, error) {
|
||||
return rdwrDispatch(sessionCtlHandlers)(ctx, data)
|
||||
func sessionCtlHandler(s *SessionNode, removeFn func(), renameFn func(string) error) func([]byte) ([]byte, error) {
|
||||
handlers := map[string]func([]string) ([]byte, error){
|
||||
"kill": func(_ []string) ([]byte, error) {
|
||||
removeFn()
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
".": func(_ []string) ([]byte, error) {
|
||||
removeFn()
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"save": func(_ []string) ([]byte, error) {
|
||||
if s.Session != nil {
|
||||
s.Session.Save()
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"invalidate": func(_ []string) ([]byte, error) {
|
||||
s.InvalidateModelsCache()
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"pause": func(_ []string) ([]byte, error) {
|
||||
if err := s.Pause(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"resume": func(_ []string) ([]byte, error) {
|
||||
if err := s.Resume(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
}
|
||||
return func(data []byte) ([]byte, error) {
|
||||
return dispatch(handlers, data)
|
||||
}
|
||||
}
|
||||
|
||||
var sessionCtlHandlers = map[string]rdwrHandler{
|
||||
"kill": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
ctx.Remove()
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
".": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
ctx.Remove()
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"save": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
if ctx.Session.Session != nil {
|
||||
ctx.Session.Session.Save()
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"invalidate": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
ctx.Session.InvalidateModelsCache()
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"pause": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
if err := ctx.Session.Pause(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
"resume": func(ctx Hctx, _ []string) ([]byte, error) {
|
||||
if err := ctx.Session.Resume(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte("ok\n"), nil
|
||||
},
|
||||
func readSessionName(s *SessionNode) ([]byte, error) {
|
||||
return []byte(s.Name() + "\n"), nil
|
||||
}
|
||||
|
||||
func readSessionName(ctx Hctx) ([]byte, error) {
|
||||
return []byte(ctx.Session.Name() + "\n"), nil
|
||||
}
|
||||
|
||||
func writeSessionName(ctx Hctx, data []byte) error {
|
||||
func writeSessionName(s *SessionNode, renameFn func(string) error, data []byte) error {
|
||||
newName := strings.TrimSpace(string(data))
|
||||
if newName == "" {
|
||||
return nil
|
||||
}
|
||||
oldName := ctx.Session.Name()
|
||||
oldName := s.Name()
|
||||
if newName != oldName {
|
||||
return ctx.Rename(newName)
|
||||
return renameFn(newName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSessionID(ctx Hctx) ([]byte, error) {
|
||||
return []byte(ctx.Session.ID() + "\n"), nil
|
||||
func readSessionID(s *SessionNode) ([]byte, error) {
|
||||
return []byte(s.ID() + "\n"), nil
|
||||
}
|
||||
|
||||
func readSessionBypass(ctx Hctx) ([]byte, error) {
|
||||
if ctx.Bypass == nil {
|
||||
func readSessionBypass(b *bypass.Broker, s *SessionNode) ([]byte, error) {
|
||||
if b == nil {
|
||||
return nil, fmt.Errorf("bypass broker not available")
|
||||
}
|
||||
p := ctx.Bypass.SessionPolicy(ctx.Session.ID())
|
||||
p := b.SessionPolicy(s.ID())
|
||||
return p.Marshal()
|
||||
}
|
||||
|
||||
func writeSessionBypass(ctx Hctx, data []byte) error {
|
||||
if ctx.Bypass == nil {
|
||||
func writeSessionBypass(b *bypass.Broker, s *SessionNode, data []byte) error {
|
||||
if b == nil {
|
||||
return fmt.Errorf("bypass broker not available")
|
||||
}
|
||||
var p bypass.Policy
|
||||
if err := bypass.ParsePolicy(data, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Bypass.SetSessionPolicy(ctx.Session.ID(), p)
|
||||
b.SetSessionPolicy(s.ID(), p)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,32 +6,14 @@ package fs
|
|||
import (
|
||||
"context"
|
||||
"os/user"
|
||||
"strconv"
|
||||
|
||||
"ollie/cmd/olliesrv/internal/agent"
|
||||
"ollie/cmd/olliesrv/internal/bypass"
|
||||
"ollie/cmd/olliesrv/internal/session"
|
||||
"ollie/fsedsl"
|
||||
olog "ollie/log"
|
||||
)
|
||||
|
||||
// Hctx is an internal adapter — holds scope-level dependencies for handlers
|
||||
// during the incremental migration to pure closures. Will be removed.
|
||||
type Hctx struct {
|
||||
context.Context
|
||||
RootState *RootState
|
||||
Log *olog.Logger
|
||||
Models *ModelCache
|
||||
Bypass *bypass.Broker
|
||||
Invalidate func()
|
||||
Shutdown func()
|
||||
Session *SessionNode
|
||||
Remove func()
|
||||
Rename func(string) error
|
||||
Agent *agent.Agent
|
||||
AgentLog *AgentLog
|
||||
Data any
|
||||
}
|
||||
|
||||
// Type aliases for fsedsl types used throughout the package.
|
||||
type (
|
||||
Tree = fsedsl.Tree
|
||||
|
|
@ -47,46 +29,14 @@ const (
|
|||
PermMkdirPrivate = fsedsl.PermMkdirPrivate
|
||||
)
|
||||
|
||||
// internalBinding is used by old handler functions during migration.
|
||||
// It carries the data needed by the adapter functions in this file.
|
||||
type internalBinding struct {
|
||||
Name string
|
||||
Aliases []string
|
||||
data any
|
||||
}
|
||||
|
||||
var treeSpecUID, treeSpecGID string
|
||||
|
||||
// buildTreeSpec constructs the full 9P namespace as an fsedsl.FsNodeDecl.
|
||||
// All handlers are closures that capture the root context (rs, log, etc.).
|
||||
// All handlers are closures that capture their dependencies at binding time.
|
||||
func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
|
||||
// Root-level Hctx for handlers that need it during migration.
|
||||
rootCtx := func() Hctx {
|
||||
return Hctx{
|
||||
Context: rs.Cfg.Ctx,
|
||||
RootState: rs,
|
||||
Log: rs.Cfg.Log,
|
||||
Models: rs.Cfg.ModelCache,
|
||||
Bypass: rs.Cfg.BypassBroker,
|
||||
Shutdown: rs.Cfg.Shutdown,
|
||||
Invalidate: rs.Cfg.Invalidate,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: session-scoped Hctx
|
||||
sessCtx := func(node *SessionNode) Hctx {
|
||||
ctx := rootCtx()
|
||||
ctx.Session = node
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Helper: agent-scoped Hctx
|
||||
agentCtx := func(node *SessionNode, ag *agent.Agent, al *AgentLog) Hctx {
|
||||
ctx := sessCtx(node)
|
||||
ctx.Agent = ag
|
||||
ctx.AgentLog = al
|
||||
return ctx
|
||||
}
|
||||
cfg := &rs.Cfg
|
||||
mc := cfg.ModelCache
|
||||
broker := cfg.BypassBroker
|
||||
|
||||
u, _ := user.Current()
|
||||
treeSpecUID = "ollie"
|
||||
|
|
@ -101,54 +51,51 @@ func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
|
|||
|
||||
fsedsl.FileNode("backends", 0444,
|
||||
fsedsl.Doc("Available backend names, one per line"),
|
||||
fsedsl.Read(func() ([]byte, error) { return readBackends(rootCtx()) }),
|
||||
fsedsl.Read(readBackends),
|
||||
),
|
||||
fsedsl.FileNode("help", 0444,
|
||||
fsedsl.Doc("Filesystem reference"),
|
||||
fsedsl.Read(func() ([]byte, error) { return readHelp(rootCtx()) }),
|
||||
fsedsl.Read(readHelp),
|
||||
),
|
||||
fsedsl.FileNode("models", 0444,
|
||||
fsedsl.Doc("Available models. Format: backend<tab>model per line."),
|
||||
fsedsl.Read(func() ([]byte, error) { return readModels(rootCtx()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readModels(mc) }),
|
||||
),
|
||||
fsedsl.FileNode("agents", 0444,
|
||||
fsedsl.Doc("Available agent profiles, one per line"),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgents(rootCtx()) }),
|
||||
fsedsl.Read(readAgents),
|
||||
),
|
||||
fsedsl.FileNode("tools", 0444,
|
||||
fsedsl.Doc("Available tools. Format: name<spaces>description per line."),
|
||||
fsedsl.Read(func() ([]byte, error) { return readTools(rootCtx()) }),
|
||||
fsedsl.Read(readTools),
|
||||
),
|
||||
fsedsl.FileNode("ctl", 0666,
|
||||
fsedsl.Doc("Server control. Write: 'invalidate', 'kill'"),
|
||||
fsedsl.Request(func(data []byte) ([]byte, error) { return requestRootCtl(rootCtx(), data) }),
|
||||
fsedsl.Request(rootCtlHandler(mc, cfg.Invalidate, cfg.Shutdown)),
|
||||
),
|
||||
fsedsl.FileNode("eventwait", 0444,
|
||||
fsedsl.Doc("Blocking read for server events"),
|
||||
fsedsl.Read(func() ([]byte, error) { return readEventwait(rootCtx()) }),
|
||||
fsedsl.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) {
|
||||
return blockEventwait(rootCtx(), cctx, base)
|
||||
}),
|
||||
fsedsl.Read(readEventwait),
|
||||
fsedsl.BlockOnce(blockEventwait),
|
||||
),
|
||||
fsedsl.FileNode("generate", 0666,
|
||||
fsedsl.Doc("One-shot LLM generation"),
|
||||
fsedsl.Request(func(data []byte) ([]byte, error) { return requestGenerate(rootCtx(), data) }),
|
||||
fsedsl.Request(func(data []byte) ([]byte, error) { return requestGenerate(cfg.Ctx, data) }),
|
||||
),
|
||||
fsedsl.FileNode("aliases", 0444,
|
||||
fsedsl.Doc("Alias table. Format: id<tab>path per line."),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAliases(rootCtx()) }),
|
||||
fsedsl.Read(readAliases),
|
||||
),
|
||||
|
||||
fsedsl.DirNode("bypass",
|
||||
fsedsl.Doc("Bypass broker for sandboxed command approval"),
|
||||
fsedsl.FileNode("policy", 0666,
|
||||
fsedsl.Doc("Global bypass policy"),
|
||||
fsedsl.Read(func() ([]byte, error) { return readBypassPolicy(rootCtx()) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeBypassPolicy(rootCtx(), data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readBypassPolicy(broker) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeBypassPolicy(broker, data) }),
|
||||
),
|
||||
fsedsl.Each("{reqid}", func() ([]fsedsl.Binding, error) {
|
||||
ctx := rootCtx()
|
||||
return adaptBypassBindings(ctx)
|
||||
return adaptBypassBindings(broker)
|
||||
}),
|
||||
),
|
||||
|
||||
|
|
@ -156,26 +103,21 @@ func buildTreeSpec(rs *RootState) fsedsl.FsNodeDecl {
|
|||
fsedsl.Doc("Session management"),
|
||||
fsedsl.FileNode("new", 0666,
|
||||
fsedsl.Doc("Create session"),
|
||||
fsedsl.Request(func(data []byte) ([]byte, error) { return requestSessionNew(rootCtx(), data) }),
|
||||
fsedsl.Request(requestSessionNew),
|
||||
),
|
||||
fsedsl.FileNode("idx", 0444,
|
||||
fsedsl.Doc("Session index"),
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionIdx(rootCtx()) }),
|
||||
fsedsl.Read(readSessionIdx),
|
||||
),
|
||||
fsedsl.Each("{sname}", func() ([]fsedsl.Binding, error) {
|
||||
return buildSessionBindings(rs, rootCtx, sessCtx, agentCtx)
|
||||
return buildSessionBindings(rs, broker)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// buildSessionBindings produces bindings for each session.
|
||||
func buildSessionBindings(
|
||||
rs *RootState,
|
||||
rootCtx func() Hctx,
|
||||
sessCtx func(*SessionNode) Hctx,
|
||||
agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx,
|
||||
) ([]fsedsl.Binding, error) {
|
||||
func buildSessionBindings(rs *RootState, broker *bypass.Broker) ([]fsedsl.Binding, error) {
|
||||
sessions := session.Sessions()
|
||||
var out []fsedsl.Binding
|
||||
|
||||
|
|
@ -197,11 +139,11 @@ func buildSessionBindings(
|
|||
}
|
||||
|
||||
out = append(out, fsedsl.Binding{
|
||||
Name: sess.Name(),
|
||||
Aliases: []string{sess.ID},
|
||||
Remove: removeFn,
|
||||
Rename: renameFn,
|
||||
Children: buildSessionChildren(s, rs, removeFn, renameFn, rootCtx, sessCtx, agentCtx),
|
||||
Name: sess.Name(),
|
||||
Aliases: []string{sess.ID},
|
||||
Remove: removeFn,
|
||||
Rename: renameFn,
|
||||
Children: buildSessionChildren(s, rs, broker, removeFn, renameFn),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
|
|
@ -211,64 +153,50 @@ func buildSessionBindings(
|
|||
func buildSessionChildren(
|
||||
s *SessionNode,
|
||||
rs *RootState,
|
||||
broker *bypass.Broker,
|
||||
removeFn func(),
|
||||
renameFn func(string) error,
|
||||
rootCtx func() Hctx,
|
||||
sessCtx func(*SessionNode) Hctx,
|
||||
agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx,
|
||||
) []fsedsl.FsNodeDecl {
|
||||
sc := func() Hctx {
|
||||
ctx := sessCtx(s)
|
||||
ctx.Remove = removeFn
|
||||
ctx.Rename = renameFn
|
||||
return ctx
|
||||
}
|
||||
|
||||
return []fsedsl.FsNodeDecl{
|
||||
fsedsl.FileNode("env", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionEnv(sc()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionEnv(s) }),
|
||||
),
|
||||
fsedsl.FileNode("paused", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionPaused(sc()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionPaused(s) }),
|
||||
),
|
||||
fsedsl.FileNode("connected", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionConnected(sc()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionConnected(s) }),
|
||||
),
|
||||
fsedsl.FileNode("ctl", 0666,
|
||||
fsedsl.Request(func(data []byte) ([]byte, error) { return requestSessionCtl(sc(), data) }),
|
||||
fsedsl.Request(sessionCtlHandler(s, removeFn, renameFn)),
|
||||
),
|
||||
fsedsl.FileNode("name", 0666,
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionName(sc()) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeSessionName(sc(), data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionName(s) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeSessionName(s, renameFn, data) }),
|
||||
),
|
||||
fsedsl.FileNode("id", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionID(sc()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionID(s) }),
|
||||
),
|
||||
fsedsl.FileNode("bypass", 0666,
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionBypass(sc()) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeSessionBypass(sc(), data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readSessionBypass(broker, s) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeSessionBypass(broker, s, data) }),
|
||||
),
|
||||
fsedsl.DirNode("agent",
|
||||
fsedsl.FileNode("new", 0666,
|
||||
fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentNew(sc(), data) }),
|
||||
fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentNew(s, data) }),
|
||||
),
|
||||
fsedsl.FileNode("idx", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentIdx(sc()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentIdx(s.Session) }),
|
||||
),
|
||||
fsedsl.Each("{aname}", func() ([]fsedsl.Binding, error) {
|
||||
return buildAgentBindings(s, rs, sessCtx, agentCtx)
|
||||
return buildAgentBindings(s, rs)
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// buildAgentBindings produces bindings for each agent in a session.
|
||||
func buildAgentBindings(
|
||||
s *SessionNode,
|
||||
rs *RootState,
|
||||
sessCtx func(*SessionNode) Hctx,
|
||||
agentCtx func(*SessionNode, *agent.Agent, *AgentLog) Hctx,
|
||||
) ([]fsedsl.Binding, error) {
|
||||
func buildAgentBindings(s *SessionNode, rs *RootState) ([]fsedsl.Binding, error) {
|
||||
agents := s.Session.Agents()
|
||||
var out []fsedsl.Binding
|
||||
|
||||
|
|
@ -281,8 +209,6 @@ func buildAgentBindings(
|
|||
wireAgentEvents(s.Session.ID, a, al)
|
||||
}
|
||||
|
||||
ac := func() Hctx { return agentCtx(s, a, al) }
|
||||
|
||||
out = append(out, fsedsl.Binding{
|
||||
Name: a.Name(),
|
||||
Aliases: []string{a.ID()},
|
||||
|
|
@ -290,34 +216,34 @@ func buildAgentBindings(
|
|||
s.Session.RemoveAgent(a.ID())
|
||||
session.PublishEvent("session."+s.ID()+".agent."+a.ID()+".kill", "")
|
||||
},
|
||||
Children: buildAgentChildren(a, al, s, ac),
|
||||
Children: buildAgentChildren(a, al, s),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// buildAgentChildren returns the file nodes for a single agent.
|
||||
func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode, ac func() Hctx) []fsedsl.FsNodeDecl {
|
||||
func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode) []fsedsl.FsNodeDecl {
|
||||
return []fsedsl.FsNodeDecl{
|
||||
fsedsl.FileNode("prompt", 0666,
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentPrompt(ac(), data) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentPrompt(s, a, al, data) }),
|
||||
),
|
||||
fsedsl.FileNode("fifo", 0666,
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentFifoIn(ac(), data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentFifoOut(ac()) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentFifoIn(a, data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentFifoOut(a) }),
|
||||
),
|
||||
fsedsl.FileNode("chat", 0444,
|
||||
fsedsl.StatOverride(statAgentChat),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentChatText(ac()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentChatText(al) }),
|
||||
fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
|
||||
return streamAgentChatText(ac(), cctx, base)
|
||||
return streamAgentChatText(s, al, cctx, base)
|
||||
}),
|
||||
),
|
||||
fsedsl.FileNode("chat.raw", 0444,
|
||||
fsedsl.StatOverride(statAgentChat),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentChat(ac()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentChat(al) }),
|
||||
fsedsl.Stream(func(cctx context.Context, base string) ([]byte, string, error) {
|
||||
return streamAgentChat(ac(), cctx, base)
|
||||
return streamAgentChat(s, al, cctx, base)
|
||||
}),
|
||||
),
|
||||
fsedsl.FileNode("statewait", 0444,
|
||||
|
|
@ -325,68 +251,63 @@ func buildAgentChildren(a *agent.Agent, al *AgentLog, s *SessionNode, ac func()
|
|||
return []byte(a.State() + "\n"), nil
|
||||
}),
|
||||
fsedsl.BlockOnce(func(cctx context.Context, base string) ([]byte, string, error) {
|
||||
return blockAgentStateWait(ac(), cctx, base)
|
||||
return blockAgentStateWait(s, a, cctx, base)
|
||||
}),
|
||||
),
|
||||
fsedsl.FileNode("log", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentLog(ac()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentLog(al) }),
|
||||
),
|
||||
fsedsl.FileNode("plan", 0666,
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentPlan(ac()) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentPlan(ac(), data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentPlan(al) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentPlan(al, data) }),
|
||||
),
|
||||
fsedsl.FileNode("cfg", 0666,
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentCfg(ac()) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentCfg(ac(), data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentCfg(s, a, al) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentCfg(a, data) }),
|
||||
),
|
||||
fsedsl.FileNode("ctl", 0666,
|
||||
fsedsl.Request(func(data []byte) ([]byte, error) { return requestAgentCtl(ac(), data) }),
|
||||
fsedsl.Request(agentCtlHandler(s, a)),
|
||||
),
|
||||
fsedsl.FileNode("stats", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentStats(ac()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentStats(a) }),
|
||||
),
|
||||
fsedsl.FileNode("tools", 0666,
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentTools(ac()) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentTools(ac(), data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentTools(a) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentTools(a, data) }),
|
||||
),
|
||||
fsedsl.FileNode("name", 0666,
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentName(ac()) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentName(ac(), data) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentName(a) }),
|
||||
fsedsl.Write(func(data []byte) error { return writeAgentName(s, a, data) }),
|
||||
),
|
||||
fsedsl.FileNode("id", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentID(ac()) }),
|
||||
fsedsl.Read(func() ([]byte, error) { return readAgentID(a) }),
|
||||
),
|
||||
fsedsl.DirNode("proc",
|
||||
fsedsl.Each("{pid}", func() ([]fsedsl.Binding, error) {
|
||||
return adaptProcessBindings(ac())
|
||||
return adaptProcessBindings(s)
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// adaptBypassBindings converts old-style bypass bindings to new fsedsl.Binding.
|
||||
func adaptBypassBindings(ctx Hctx) ([]fsedsl.Binding, error) {
|
||||
oldBindings, err := bypassBindings(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// adaptBypassBindings returns bindings for pending bypass requests.
|
||||
func adaptBypassBindings(broker *bypass.Broker) ([]fsedsl.Binding, error) {
|
||||
if broker == nil {
|
||||
return nil, errNoBypass
|
||||
}
|
||||
pending := broker.Pending()
|
||||
var out []fsedsl.Binding
|
||||
for _, ob := range oldBindings {
|
||||
b := ob
|
||||
// Bypass requests are file-like (read/write), not directories
|
||||
for _, r := range pending {
|
||||
req := r
|
||||
out = append(out, fsedsl.Binding{
|
||||
Name: b.Name,
|
||||
Name: req.ID,
|
||||
Children: []fsedsl.FsNodeDecl{
|
||||
fsedsl.FileNode("request", 0666,
|
||||
fsedsl.Read(func() ([]byte, error) {
|
||||
c := ctx
|
||||
c.Data = b.data
|
||||
return readBypassRequest(c)
|
||||
return readBypassRequest(req)
|
||||
}),
|
||||
fsedsl.Write(func(data []byte) error {
|
||||
c := ctx
|
||||
c.Data = b.data
|
||||
return writeBypassRequest(c, data)
|
||||
return writeBypassRequest(broker, req, data)
|
||||
}),
|
||||
),
|
||||
},
|
||||
|
|
@ -395,41 +316,44 @@ func adaptBypassBindings(ctx Hctx) ([]fsedsl.Binding, error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// adaptProcessBindings converts old-style process bindings to new fsedsl.Binding.
|
||||
func adaptProcessBindings(ctx Hctx) ([]fsedsl.Binding, error) {
|
||||
oldBindings, err := processBindings(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// adaptProcessBindings returns bindings for detached processes.
|
||||
func adaptProcessBindings(s *SessionNode) ([]fsedsl.Binding, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
conn := s.DialToolServer()
|
||||
if conn == nil {
|
||||
return nil, nil
|
||||
}
|
||||
defer conn.Close()
|
||||
raw := conn.ListDetachedRaw()
|
||||
var out []fsedsl.Binding
|
||||
for _, ob := range oldBindings {
|
||||
b := ob
|
||||
out = append(out, fsedsl.Binding{
|
||||
Name: b.Name,
|
||||
Children: []fsedsl.FsNodeDecl{
|
||||
fsedsl.FileNode("out", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) {
|
||||
c := ctx
|
||||
c.Data = b.data
|
||||
return readProcess(c)
|
||||
}),
|
||||
),
|
||||
fsedsl.FileNode("ctl", 0222,
|
||||
fsedsl.Write(func(data []byte) error {
|
||||
c := ctx
|
||||
c.Data = b.data
|
||||
return writeProcessCtl(c, data)
|
||||
}),
|
||||
),
|
||||
fsedsl.FileNode("status", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) {
|
||||
c := ctx
|
||||
c.Data = b.data
|
||||
return readProcessStatus(c)
|
||||
}),
|
||||
),
|
||||
},
|
||||
})
|
||||
for _, r := range raw {
|
||||
if m, ok := r.(map[string]any); ok {
|
||||
if id, ok := m["pid"].(int); ok {
|
||||
pid := id
|
||||
out = append(out, fsedsl.Binding{
|
||||
Name: strconv.Itoa(pid),
|
||||
Children: []fsedsl.FsNodeDecl{
|
||||
fsedsl.FileNode("out", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) {
|
||||
return readProcess(s, pid)
|
||||
}),
|
||||
),
|
||||
fsedsl.FileNode("ctl", 0222,
|
||||
fsedsl.Write(func(data []byte) error {
|
||||
return writeProcessCtl(s, pid, data)
|
||||
}),
|
||||
),
|
||||
fsedsl.FileNode("status", 0444,
|
||||
fsedsl.Read(func() ([]byte, error) {
|
||||
return readProcessStatus(s, pid)
|
||||
}),
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue