feat: refresh autoLoad tools on agent profile switch

When the 'agent <profile>' ctl command switches profiles, the tool
registry now clears old tools and loads the new profile's autoLoad
list. Previously, switching profiles left the old tools loaded.

Changes:
- registry: add ClearAgent(agentID) to remove all tools for an agent
- server/proc: add ClearAgent wrapper and 'clear <agentID>' ctl command
- toolclient: add ClearTools() method to ToolsrvConn
- agent: SwitchProfile now returns *AgentConfig for tool reload
- fs/spec: agent ctl handler clears and reloads tools after switch
This commit is contained in:
Levi Neely 2026-08-21 10:57:59 +02:00
parent e0ac54c0fb
commit 8960fb73fd
6 changed files with 58 additions and 10 deletions

View File

@ -489,21 +489,22 @@ func (ag *Agent) Clear() error {
}
// SwitchProfile loads a new agent profile by name, replacing the runtime
// and clearing history. Returns an error if the agent is running or the
// profile cannot be loaded.
func (ag *Agent) SwitchProfile(name string) error {
// and clearing history. Returns the loaded config on success for the caller
// to reload tools. Returns an error if the agent is running or the profile
// cannot be loaded.
func (ag *Agent) SwitchProfile(name string) (*AgentConfig, error) {
if ag.IsRunning() {
return fmt.Errorf("cannot switch agent while agent is running")
return nil, fmt.Errorf("cannot switch agent while agent is running")
}
cfgPath := AgentConfigPath(ag.agentsDir, name)
f, err := os.Open(cfgPath)
if err != nil {
return fmt.Errorf("agent %q: %w", name, err)
return nil, fmt.Errorf("agent %q: %w", name, err)
}
cfg, err := Load(f)
f.Close()
if err != nil {
return fmt.Errorf("agent %q: %w", name, err)
return nil, fmt.Errorf("agent %q: %w", name, err)
}
disp := ag.newToolServer()
if disp != nil && ag.id != "" {
@ -514,7 +515,7 @@ func (ag *Agent) SwitchProfile(name string) error {
if cfg.Backend != "" {
newBe, err := ag.newBackend(cfg.Backend)
if err != nil {
return fmt.Errorf("backend %q: %w", cfg.Backend, err)
return nil, fmt.Errorf("backend %q: %w", cfg.Backend, err)
}
if cfg.Model != "" {
newBe.SetModel(cfg.Model)
@ -532,7 +533,7 @@ func (ag *Agent) SwitchProfile(name string) error {
ag.save()
ag.flush()
ag.notifyChange()
return nil
return cfg, nil
}
// Event is a typed output event emitted during an agent turn or in response

View File

@ -1002,9 +1002,15 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
if len(args) == 0 {
return []byte(a.Profile() + "\n"), nil
}
if err := a.SwitchProfile(args[0]); err != nil {
cfg, err := a.SwitchProfile(args[0])
if err != nil {
return nil, err
}
// Reload tools for the new profile
if ts := a.ToolServer(); ts != nil {
ts.ClearTools()
session.LoadAutoLoadTools(cfg, ts, s.ID, a.ID(), nil)
}
return []byte(args[0] + "\n"), nil
},
"model": func(args []string) ([]byte, error) {

View File

@ -275,6 +275,17 @@ func (c *ToolsrvConn) UnloadTool(name string) error {
return err
}
// ClearTools removes all tools for this agent.
func (c *ToolsrvConn) ClearTools() error {
fid, err := c.fsys.Open("ctl", plan9.OWRITE)
if err != nil {
return err
}
defer fid.Close()
_, err = fid.Write([]byte("clear " + c.agentID + "\n"))
return err
}
// Ping checks if the connection is alive.
func (c *ToolsrvConn) Ping() error {
_, err := c.fsys.Stat("info")

View File

@ -80,6 +80,14 @@ func (r *Registry) Unload(agentID, name string) error {
return nil
}
// ClearAgent removes all tools for an agent.
func (r *Registry) ClearAgent(agentID string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.agents, agentID)
delete(r.revisions, agentID)
}
// Loaded returns all loaded tools for an agent.
func (r *Registry) Loaded(agentID string) []protocol.ToolInfo {
r.mu.RLock()

View File

@ -218,6 +218,22 @@ func (st *State) UnloadTool(agentID, name string) error {
return nil
}
// ClearAgent removes all tools for an agent's registry.
func (st *State) ClearAgent(agentID string) {
st.mu.RLock()
reg := st.registry
st.mu.RUnlock()
if reg == nil {
return
}
reg.ClearAgent(agentID)
if st.OnToolsChanged != nil {
st.OnToolsChanged()
}
}
// --- Process Management ---
// allocID allocates a new process ID.
@ -646,6 +662,12 @@ func (st *State) HandleCtl(input string) error {
return fmt.Errorf("unload requires agent ID and tool name")
}
return st.UnloadTool(parts[1], parts[2])
case "clear":
if len(parts) < 2 {
return fmt.Errorf("clear requires agent ID")
}
st.ClearAgent(parts[1])
return nil
case "env":
if len(parts) < 2 {
return fmt.Errorf("env requires KEY=VALUE")

View File

@ -117,7 +117,7 @@ func randomToken() (string, error) {
func Spec(srv *Server) virtfs.FsNodeDecl {
return virtfs.DirNode("/",
virtfs.FileNode("ctl", 0222,
virtfs.Doc("Control: write 'load <agentID> <tool>' or 'unload <agentID> <tool>'"),
virtfs.Doc("Control: load <agentID> <tool>, unload <agentID> <tool>, clear <agentID>"),
virtfs.Write(func(data []byte) error {
return srv.Fs.HandleCtl(strings.TrimSpace(string(data)))
}),