mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 12:14:24 +00:00
fix: deterministic prompt ordering for LLM cache hit (#719)
Sort all non-deterministic map iterations that affect system prompt and tool definitions sent to LLM APIs. Go map iteration order is random, causing prompt prefix to change every turn — breaking Anthropic/OpenAI prompt caching (cache by exact prefix match). Fixed 5 sources of non-deterministic ordering: - Registry.List(): sort canonical tool names - Registry.ProviderDefs(): sort tools + aliases before building defs - PolicyEngine.FilterTools(): sort alias iteration (single Aliases() call) - buildMCPToolsInlineSection(): sort MCP tool names in system prompt - GetAgentContextFiles/GetUserContextFiles: ORDER BY file_name (PG+SQLite) Based on PR #718 by @therichardngai-code with additional fixes: - Context files from DB now deterministic (ORDER BY file_name) - FilterTools() calls registry.Aliases() once instead of 3 times
This commit is contained in:
1 parent
2801f0a978
commit
e88686b13a
5 files changed
+48
-13
No files matched your search
@@ -55,7 +55,14 @@ func buildMCPToolsInlineSection(descs map[string]string) []string {
|
||||
mcpOptionalParamInstruction,
|
||||
"",
|
||||
}
|
||||
for name, desc := range descs {
|
||||
// Sort MCP tool names for deterministic ordering — critical for prompt caching.
|
||||
sortedNames := make([]string, 0, len(descs))
|
||||
for name := range descs {
|
||||
sortedNames = append(sortedNames, name)
|
||||
}
|
||||
slices.Sort(sortedNames)
|
||||
for _, name := range sortedNames {
|
||||
desc := descs[name]
|
||||
if len(desc) > mcpToolDescMaxLen {
|
||||
desc = desc[:mcpToolDescMaxLen] + "…"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func (s *PGAgentStore) GetAgentContextFiles(ctx context.Context, agentID uuid.UU
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
"SELECT agent_id, file_name, content FROM agent_context_files WHERE agent_id = $1"+tClause,
|
||||
"SELECT agent_id, file_name, content FROM agent_context_files WHERE agent_id = $1"+tClause+" ORDER BY file_name",
|
||||
append([]any{agentID}, tArgs...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -87,7 +87,7 @@ func (s *PGAgentStore) GetUserContextFiles(ctx context.Context, agentID uuid.UUI
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
"SELECT agent_id, user_id, file_name, content FROM user_context_files WHERE agent_id = $1 AND user_id = $2"+tClause,
|
||||
"SELECT agent_id, user_id, file_name, content FROM user_context_files WHERE agent_id = $1 AND user_id = $2"+tClause+" ORDER BY file_name",
|
||||
append([]any{agentID, userID}, tArgs...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -27,7 +27,7 @@ func (s *SQLiteAgentStore) GetAgentContextFiles(ctx context.Context, agentID uui
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
"SELECT agent_id, file_name, content FROM agent_context_files WHERE agent_id = ?"+tClause,
|
||||
"SELECT agent_id, file_name, content FROM agent_context_files WHERE agent_id = ?"+tClause+" ORDER BY file_name",
|
||||
append([]any{agentID}, tArgs...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -97,7 +97,7 @@ func (s *SQLiteAgentStore) GetUserContextFiles(ctx context.Context, agentID uuid
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx,
|
||||
"SELECT agent_id, user_id, file_name, content FROM user_context_files WHERE agent_id = ? AND user_id = ?"+tClause,
|
||||
"SELECT agent_id, user_id, file_name, content FROM user_context_files WHERE agent_id = ? AND user_id = ?"+tClause+" ORDER BY file_name",
|
||||
append([]any{agentID, userID}, tArgs...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -130,8 +130,16 @@ func (pe *PolicyEngine) FilterTools(
|
||||
}
|
||||
}
|
||||
|
||||
// Add registry aliases for allowed canonical tools
|
||||
for alias, canonical := range registry.Aliases() {
|
||||
// Add registry aliases for allowed canonical tools.
|
||||
// Sort alias names for deterministic ordering (prompt caching).
|
||||
aliasMap := registry.Aliases()
|
||||
aliasList := make([]string, 0, len(aliasMap))
|
||||
for alias := range aliasMap {
|
||||
aliasList = append(aliasList, alias)
|
||||
}
|
||||
slices.Sort(aliasList)
|
||||
for _, alias := range aliasList {
|
||||
canonical := aliasMap[alias]
|
||||
if !allowedSet[canonical] {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -222,18 +223,34 @@ func safeExecute(tool Tool, ctx context.Context, args map[string]any) (result *R
|
||||
|
||||
// ProviderDefs returns tool definitions for LLM provider APIs.
|
||||
// Includes alias definitions (same params/description, alias name).
|
||||
// Results are sorted by tool name for deterministic ordering (prompt caching).
|
||||
func (r *Registry) ProviderDefs() []providers.ToolDefinition {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
defs := make([]providers.ToolDefinition, 0, len(r.tools)+len(r.aliases))
|
||||
for name, tool := range r.tools {
|
||||
if r.disabled[name] {
|
||||
continue
|
||||
// Sort canonical tool names for deterministic ordering.
|
||||
sortedNames := make([]string, 0, len(r.tools))
|
||||
for name := range r.tools {
|
||||
if !r.disabled[name] {
|
||||
sortedNames = append(sortedNames, name)
|
||||
}
|
||||
defs = append(defs, ToProviderDef(tool))
|
||||
}
|
||||
for alias, canonical := range r.aliases {
|
||||
slices.Sort(sortedNames)
|
||||
|
||||
defs := make([]providers.ToolDefinition, 0, len(sortedNames)+len(r.aliases))
|
||||
for _, name := range sortedNames {
|
||||
defs = append(defs, ToProviderDef(r.tools[name]))
|
||||
}
|
||||
|
||||
// Sort alias names for deterministic ordering.
|
||||
sortedAliases := make([]string, 0, len(r.aliases))
|
||||
for alias := range r.aliases {
|
||||
sortedAliases = append(sortedAliases, alias)
|
||||
}
|
||||
slices.Sort(sortedAliases)
|
||||
|
||||
for _, alias := range sortedAliases {
|
||||
canonical := r.aliases[alias]
|
||||
if r.disabled[canonical] {
|
||||
continue
|
||||
}
|
||||
@@ -254,6 +271,8 @@ func (r *Registry) ProviderDefs() []providers.ToolDefinition {
|
||||
}
|
||||
|
||||
// List returns all registered canonical tool names (excludes aliases).
|
||||
// Results are sorted lexicographically for deterministic ordering — critical
|
||||
// for LLM prompt caching (Anthropic/OpenAI cache by exact prefix match).
|
||||
func (r *Registry) List() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
@@ -263,6 +282,7 @@ func (r *Registry) List() []string {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
slices.Sort(names)
|
||||
return names
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user