perf(agent): cache the MCP tool list per client (F1)
Fix F1 of plans/2026-07-11-nomos-agent-code-review.md, the last item. buildTools called listToolsFull (a tools/list MCP round-trip) at the start of EVERY chat turn, including every auto-continuation resume — the tool list is static for the lifetime of one MCP connection, changing only when the api process re-registers tools (a restart, which this client already detects and reacts to via reconnectLocked). Re-fetching it every single turn was avoidable network+parsing work on the hot path. mcpClient now caches the parsed tool list after its first fetch, guarded by its own mutex (kept separate from the request-serializing mu so a cache check never contends with an in-flight doRequest call). reconnectLocked clears the cache — an api restart may have changed what's registered, so a stale cache would be wrong, not just slow. fleetSnapshot's get_health_summary call is deliberately left uncached — it's meant to be "as of now." Since each session gets its own client (the per-session pool from the concurrency work), this caches per-task-conversation rather than globally: a task's FIRST turn still pays the round-trip, every turn after reuses the cached list — which is exactly the case that mattered (long-running, heavily-autonomous tasks with many auto-continuation resumes). Verified live via the api's request log: a brand-new session's first turn made 3 MCP calls (initialize, tools/list, get_health_summary); a second turn on the SAME session made exactly 1 (only get_health_summary) — tools/list correctly skipped. This completes the implementation order in plans/2026-07-11-nomos-agent-code-review.md — every A/B/D/E/F finding from the review (excluding C1, explicitly deferred per operator instruction) is now fixed, tested, and verified live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -481,6 +481,18 @@ type mcpClient struct {
|
||||
http *http.Client
|
||||
nextID int
|
||||
mu sync.Mutex // this client is one stateful MCP session; serialize ITS OWN calls
|
||||
|
||||
// toolsCache holds the last tools/list result. The tool list is static
|
||||
// for the lifetime of one MCP connection — it only changes when the api
|
||||
// process (re)registers tools, i.e. on a restart, which this client
|
||||
// already detects and reacts to via reconnectLocked. Without this,
|
||||
// buildTools (called at the start of EVERY chat turn, including every
|
||||
// auto-continuation resume) paid a full tools/list round-trip every
|
||||
// single time for a list that's almost always identical to the last one.
|
||||
// Guarded separately from mu (not reused) so a cache check never
|
||||
// contends with an in-flight doRequest call for a different method.
|
||||
toolsMu sync.Mutex
|
||||
toolsCache []toolDef
|
||||
}
|
||||
|
||||
func newMCPClient(baseURL string) (*mcpClient, error) {
|
||||
@@ -540,6 +552,12 @@ func (c *mcpClient) doRequest(method string, params map[string]any) (*mcpJSONRPC
|
||||
// reconnectLocked re-initializes the MCP session. The caller must hold c.mu.
|
||||
func (c *mcpClient) reconnectLocked() error {
|
||||
c.sessionID = ""
|
||||
// A reconnect means the api process was restarted (or forgot us) — its
|
||||
// tool registration may have changed, so the cached list is no longer
|
||||
// trustworthy.
|
||||
c.toolsMu.Lock()
|
||||
c.toolsCache = nil
|
||||
c.toolsMu.Unlock()
|
||||
resp, err := c.send("initialize", map[string]any{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]any{},
|
||||
|
||||
Reference in New Issue
Block a user