Support runtime backend switching via ctl

This commit is contained in:
Ollie Agent 2026-08-19 20:06:20 +02:00
parent 5a8f7e394e
commit 19971be343
2 changed files with 40 additions and 10 deletions

View File

@ -7,6 +7,7 @@ import (
"os" "os"
"runtime" "runtime"
"slices" "slices"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -39,17 +40,17 @@ type Agent struct {
resultCache resultCache resultCache resultCache
// Execution state — owned by the agent, protected by stateMu. // Execution state — owned by the agent, protected by stateMu.
state string // "idle", "thinking", "calling: <tool>" state string // "idle", "thinking", "calling: <tool>"
reply string // last assistant response reply string // last assistant response
getCwd func() string // returns session working directory getCwd func() string // returns session working directory
id string // agent identity (unique principal) id string // agent identity (unique principal)
parentID string // immutable ID of the agent that spawned this agent 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) 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 activeChildren atomic.Int32 // number of currently-running child sub-agents
peers map[string]struct{} // peer agent names (same session) peers map[string]struct{} // peer agent names (same session)
peerMu sync.RWMutex peerMu sync.RWMutex
fifo Fifo // prompt queue fifo Fifo // prompt queue
Feed Feed // streaming input gate (change-detecting) Feed Feed // streaming input gate (change-detecting)
toolCallCount atomic.Int64 toolCallCount atomic.Int64
pendingInject atomic.Pointer[string] pendingInject atomic.Pointer[string]
submitMu sync.Mutex // serializes Submit calls (commands + turns) submitMu sync.Mutex // serializes Submit calls (commands + turns)
@ -298,6 +299,31 @@ func (ag *Agent) ModelName() string {
return ag.runtime.Backend.Model() 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. // State returns the agent's current execution state.
func (ag *Agent) State() string { func (ag *Agent) State() string {
ag.stateMu.RLock() ag.stateMu.RLock()

View File

@ -1083,7 +1083,11 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
} }
return nil, nil 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) { "name": func(args []string) ([]byte, error) {
if len(args) == 0 { if len(args) == 0 {