From 6c1de81cf9a8aed0baf8d46393bdfa821ad7b5d2 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 18 Aug 2026 06:55:25 +0200 Subject: [PATCH] Bound agent result cache --- cmd/olliesrv/internal/agent/agent.go | 2 +- cmd/olliesrv/internal/agent/loop.go | 53 ++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go index c2b29ec..e1a02ec 100644 --- a/cmd/olliesrv/internal/agent/agent.go +++ b/cmd/olliesrv/internal/agent/agent.go @@ -36,7 +36,7 @@ type Agent struct { closed atomic.Bool actionMu sync.Mutex // serializes action registration with shutdown warnedContext bool - resultCache sync.Map + resultCache resultCache // Execution state — owned by the agent, protected by stateMu. state string // "idle", "thinking", "calling: " diff --git a/cmd/olliesrv/internal/agent/loop.go b/cmd/olliesrv/internal/agent/loop.go index b1ed250..60b142f 100644 --- a/cmd/olliesrv/internal/agent/loop.go +++ b/cmd/olliesrv/internal/agent/loop.go @@ -43,6 +43,56 @@ type cachedResult struct { Size int64 // file size at cache time; -1 if no file } +const ( + resultCacheMaxEntries = 256 + resultCacheMaxBytes = 32 * 1024 * 1024 +) + +type resultCache struct { + mu sync.Mutex + items map[string]cachedResult + bytes int +} + +func (c *resultCache) Load(key string) (cachedResult, bool) { + c.mu.Lock() + defer c.mu.Unlock() + v, ok := c.items[key] + return v, ok +} + +func (c *resultCache) Delete(key string) { + c.mu.Lock() + defer c.mu.Unlock() + if v, ok := c.items[key]; ok { + c.bytes -= len(v.Result) + delete(c.items, key) + } +} + +func (c *resultCache) Store(key string, value cachedResult) { + c.mu.Lock() + defer c.mu.Unlock() + if c.items == nil { + c.items = make(map[string]cachedResult) + } + if old, ok := c.items[key]; ok { + c.bytes -= len(old.Result) + } + if len(value.Result) > resultCacheMaxBytes { + return + } + c.items[key] = value + c.bytes += len(value.Result) + for len(c.items) > resultCacheMaxEntries || c.bytes > resultCacheMaxBytes { + for k, v := range c.items { + delete(c.items, k) + c.bytes -= len(v.Result) + break + } + } +} + // extractFilePath attempts to pull a "path" field from JSON tool args. func extractFilePath(args json.RawMessage) string { var m map[string]json.RawMessage @@ -533,8 +583,7 @@ func (ag *Agent) execOne(ctx context.Context, call backend.ToolCall) (toolResult readSafe := rt.ToolMeta[call.Name].Scope == "read" if readSafe { key := call.Name + "\x00" + string(call.Arguments) - if v, ok := resultCache.Load(key); ok { - entry := v.(cachedResult) + if entry, ok := resultCache.Load(key); ok { if cacheValid(entry, call.Arguments) { ag.emit(Event{Role: "tool", Name: call.Name, Content: entry.Result, OutputFormat: toolOutputFormat(rt, call.Name)}) return toolResult{ToolCallID: call.ID, Name: call.Name, Content: entry.Result}, false