From 19971be343c7f164000eae53bee2c0b0d5b32cea Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Wed, 19 Aug 2026 20:06:20 +0200 Subject: [PATCH] Support runtime backend switching via ctl --- cmd/olliesrv/internal/agent/agent.go | 44 ++++++++++++++++++++++------ cmd/olliesrv/internal/fs/spec.go | 6 +++- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go index c23d717..30bb26d 100644 --- a/cmd/olliesrv/internal/agent/agent.go +++ b/cmd/olliesrv/internal/agent/agent.go @@ -7,6 +7,7 @@ import ( "os" "runtime" "slices" + "strings" "sync" "sync/atomic" @@ -39,17 +40,17 @@ type Agent struct { resultCache resultCache // Execution state — owned by the agent, protected by stateMu. - state string // "idle", "thinking", "calling: " - reply string // last assistant response - getCwd func() string // returns session working directory - id string // agent identity (unique principal) - parentID string // immutable ID of the agent that spawned this agent - depth int // sub-agent depth (0=top-level, 1=sub-agent, 2=sub-sub-agent) - activeChildren atomic.Int32 // number of currently-running child sub-agents + state string // "idle", "thinking", "calling: " + reply string // last assistant response + getCwd func() string // returns session working directory + id string // agent identity (unique principal) + parentID string // immutable ID of the agent that spawned this agent + depth int // sub-agent depth (0=top-level, 1=sub-agent, 2=sub-sub-agent) + activeChildren atomic.Int32 // number of currently-running child sub-agents peers map[string]struct{} // peer agent names (same session) peerMu sync.RWMutex - fifo Fifo // prompt queue - Feed Feed // streaming input gate (change-detecting) + fifo Fifo // prompt queue + Feed Feed // streaming input gate (change-detecting) toolCallCount atomic.Int64 pendingInject atomic.Pointer[string] submitMu sync.Mutex // serializes Submit calls (commands + turns) @@ -298,6 +299,31 @@ func (ag *Agent) ModelName() string { return ag.runtime.Backend.Model() } +// SwitchBackend replaces the active backend by name. The new backend is fully +// constructed before the runtime is changed, so a failed switch leaves the +// current backend usable. Backend changes are only safe between turns. +func (ag *Agent) SwitchBackend(name string) error { + name = strings.TrimSpace(name) + if name == "" { + return fmt.Errorf("backend requires a name") + } + if ag.IsRunning() { + return fmt.Errorf("cannot switch backend while agent is running") + } + if ag.newBackend == nil { + return fmt.Errorf("backend switching unavailable") + } + be, err := ag.newBackend(name) + if err != nil { + return fmt.Errorf("backend %q: %w", name, err) + } + ag.runtime.Backend = be + ag.save() + ag.flush() + ag.notifyChange() + return nil +} + // State returns the agent's current execution state. func (ag *Agent) State() string { ag.stateMu.RLock() diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go index a60c824..9ce5fce 100644 --- a/cmd/olliesrv/internal/fs/spec.go +++ b/cmd/olliesrv/internal/fs/spec.go @@ -1083,7 +1083,11 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl } return nil, nil } - return nil, fmt.Errorf("backend switching not supported via ctl; use cfg") + name := strings.Join(args, " ") + if err := a.SwitchBackend(name); err != nil { + return nil, err + } + return []byte(name + "\n"), nil }, "name": func(args []string) ([]byte, error) { if len(args) == 0 {