From 686a560e846c2dc7736de49133672548554b25b6 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 11 Aug 2026 10:59:31 +0200 Subject: [PATCH] agent: toolsrv is sole owner of tool state Remove all cached tool state from agent: - Remove toolRegistryRevision from Runtime - Remove wireToolsChanged (OnToolsChanged callback) - Remove toolsNeedRefresh (revision-based cache invalidation) - Remove refreshToolListing (redundant with RefreshTools) Tools are now fetched fresh from toolsrv at the start of every turn. The preamble tools section is also rebuilt at turn start. No local caching means tool_load/unload take effect immediately on the next turn and /tools always shows the live state from toolsrv. --- cmd/olliesrv/internal/agent/agent.go | 47 +++-------------------- cmd/olliesrv/internal/agent/agent_test.go | 3 +- cmd/olliesrv/internal/agent/runtime.go | 6 +-- cmd/olliesrv/internal/agent/turn.go | 6 +-- 4 files changed, 12 insertions(+), 50 deletions(-) diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go index 815191c..d3ba5d8 100644 --- a/cmd/olliesrv/internal/agent/agent.go +++ b/cmd/olliesrv/internal/agent/agent.go @@ -328,7 +328,6 @@ func (ag *Agent) SwitchProfile(name string) error { ag.runtime = rt ag.profile = name ag.history = nil - ag.wireToolsChanged() ag.save() ag.flush() ag.notifyChange() @@ -434,49 +433,14 @@ func (ag *Agent) SetCWD(dir string) { ag.notifyChange() } -// refreshToolListing replaces the tools section in the preamble with an -// updated listing. Called by the tool server when tools are added/removed. -func (ag *Agent) refreshToolListing() { - if ag.runtime == nil || ag.runtime.Preamble == nil { - return - } - if ag.runtime.ToolServer != nil { - if infos, err := ag.runtime.ToolServer.ListTools(); err == nil { - ag.runtime.Preamble.Set(SectionTools, renderTools(infos)) - return - } - } - ag.runtime.Preamble.Set(SectionTools, "") -} - // RefreshTools forces a refresh of the tool listing in the agent's preamble. -// Call this after loading tools via ctl to update the agent's view. func (ag *Agent) RefreshTools() { - ag.refreshToolListing() -} - -// wireToolsChanged sets the OnToolsChanged callback on the tool server -// so the preamble stays in sync when tools are added/removed from disk. -func (ag *Agent) wireToolsChanged() { - if ag.runtime == nil || ag.runtime.ToolServer == nil { + if ag.runtime == nil || ag.runtime.Preamble == nil || ag.runtime.ToolServer == nil { return } - ag.runtime.ToolServer.SetOnToolsChanged(ag.refreshToolListing) -} - -// toolsNeedRefresh reports whether the tool registry has changed since the -// last time Tools was populated. If the tool server doesn't support revision -// tracking (e.g. remote Conn returns 0), this always returns true (safe fallback). -func (ag *Agent) toolsNeedRefresh() bool { - if ag.runtime.ToolServer == nil { - return true + if infos, err := ag.runtime.ToolServer.ListTools(); err == nil { + ag.runtime.Preamble.Set(SectionTools, renderTools(infos)) } - rev := ag.runtime.ToolServer.ToolRegistryRevision() - if rev == 0 || rev != ag.runtime.toolRegistryRevision { - ag.runtime.toolRegistryRevision = rev - return true - } - return false } // CtxSz returns a human-readable context size string. @@ -663,8 +627,8 @@ type AgentParams struct { } // NewAgent constructs an Agent from the given configuration. -// Post-construction wiring (SetSessionEnv, wireToolsChanged) is handled -// internally — no additional calls are required after construction. +// Post-construction wiring (SetSessionEnv) is handled internally — +// no additional calls are required after construction. func NewAgent(cfg AgentParams) *Agent { ag := &Agent{ history: cfg.History, @@ -687,7 +651,6 @@ func NewAgent(cfg AgentParams) *Agent { state: "idle", } ag.signalCh = make(chan struct{}) - ag.wireToolsChanged() ag.SetSessionEnv(cfg.SessionID) return ag } diff --git a/cmd/olliesrv/internal/agent/agent_test.go b/cmd/olliesrv/internal/agent/agent_test.go index 693b944..cc49acc 100644 --- a/cmd/olliesrv/internal/agent/agent_test.go +++ b/cmd/olliesrv/internal/agent/agent_test.go @@ -48,7 +48,8 @@ func TestRefreshToolListingEmpty(t *testing.T) { ag := &Agent{runtime: &Runtime{Preamble: p}} - ag.refreshToolListing() + // With nil ToolServer, RefreshTools is a no-op. Test the preamble Set directly. + p.Set(SectionTools, "") preamble := ag.runtime.PreambleString() diff --git a/cmd/olliesrv/internal/agent/runtime.go b/cmd/olliesrv/internal/agent/runtime.go index f9b9388..b38c5a0 100644 --- a/cmd/olliesrv/internal/agent/runtime.go +++ b/cmd/olliesrv/internal/agent/runtime.go @@ -131,10 +131,8 @@ type Runtime struct { Exec toolExecutor GenParams backend.GenerationParams MaxSteps int - // toolRegistryRevision tracks the last-seen tool registry revision. - toolRegistryRevision uint64 - CompactionModel string - Messages []string + CompactionModel string + Messages []string } // PreambleString returns the fully rendered system prompt. diff --git a/cmd/olliesrv/internal/agent/turn.go b/cmd/olliesrv/internal/agent/turn.go index 20d7322..f9821b6 100644 --- a/cmd/olliesrv/internal/agent/turn.go +++ b/cmd/olliesrv/internal/agent/turn.go @@ -99,9 +99,8 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string { ag.log.Debug("turn: start input=%s session=%s", auditTruncate(input), ag.sessionID) - // Refresh tool definitions only when the registry has changed (tool_load/unload). - // This avoids ListTools IPC + allocation on every turn in the common case. - if ag.toolsNeedRefresh() { + // Fetch tools from toolsrv (the sole owner of tool state). + if ag.runtime.ToolServer != nil { ti, listErr := ag.runtime.ToolServer.ListTools() if listErr != nil { ag.log.Error("tool server unavailable: %v", listErr) @@ -115,6 +114,7 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string { meta[info.Name] = info } ag.runtime.ToolMeta = meta + ag.runtime.Preamble.Set(SectionTools, renderTools(ti)) } turnCtx := TurnCtx{