From 1572d90d7174f0c65019e768c8f65728fae668c5 Mon Sep 17 00:00:00 2001 From: rongbaichuan Date: Wed, 19 Aug 2026 17:32:10 +0800 Subject: [PATCH] fix(mcp): reword explore guidance as advisory, not a quota MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trailing explore note said 'Explore budget: N calls ... spend your remaining calls ... Synthesize once you've used N' — agents read it as a hard quota, stopped exploring early, and fell back to grep+Read (more tokens, less complete). Reword it as explicit advisory guidance: the server never rejects or rate-limits extra explore calls, and exploring further is encouraged when the response doesn't cover the flow. The tool schema description carried the same quota language ('Budget: make at most N calls for this project') — the higher-salience text the agent reads before ever calling. Reword it identically: advisory only, NOT a quota. Also sync the eval probe string (probe-suite-envelope.mjs) and pin the new wording in tests with anti-regression assertions (medium tier must contain 'NOT a quota' and never 'remaining calls'/'Synthesize once'/'make at most'). --- __tests__/explore-output-budget.test.ts | 23 +++++++++++++++++++-- __tests__/mcp-tool-annotations.test.ts | 7 ++++--- scripts/agent-eval/probe-suite-envelope.mjs | 2 +- src/mcp/tools.ts | 15 ++++++++++---- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/__tests__/explore-output-budget.test.ts b/__tests__/explore-output-budget.test.ts index 9d9a0b3a3..efe302fb7 100644 --- a/__tests__/explore-output-budget.test.ts +++ b/__tests__/explore-output-budget.test.ts @@ -6,7 +6,7 @@ * grep+Read. These tests pin the per-tier budget shape so future tuning * doesn't silently drift the small-project case back into bloat. */ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; @@ -198,7 +198,26 @@ describe('codegraph_explore output respects the adaptive budget', () => { const text = result.content?.[0]?.text ?? ''; expect(text).not.toContain('### Additional relevant files'); expect(text).not.toContain('Complete source code is included above'); - expect(text).not.toContain('Explore budget:'); + expect(text).not.toContain('advisory only, NOT a quota'); + }); + + it('emits advisory-only exploration guidance on medium projects — never quota wording', async () => { + // Medium tier (500–4,999 files) turns the guidance note on. The synthetic + // project is tiny, so fake the stats to land in that tier — the note's + // WORDING is what this test pins. Regression guard: quota phrasing + // ("remaining calls" / "Synthesize once") must never come back — agents + // read it as a hard cap, stop exploring early, and fall back to grep+Read. + const spy = vi.spyOn(cg, 'getStats').mockReturnValue({ fileCount: 1000 } as ReturnType); + try { + const result = await handler.execute('codegraph_explore', { query: 'Session method helper' }); + const text = result.content?.[0]?.text ?? ''; + expect(text).toContain('advisory only, NOT a quota'); + expect(text).toContain('extra calls are never rejected or rate-limited'); + expect(text).not.toContain('remaining calls'); + expect(text).not.toContain('Synthesize once'); + } finally { + spy.mockRestore(); + } }); it('still includes the Relationships section — it is the cheapest structural signal', async () => { diff --git a/__tests__/mcp-tool-annotations.test.ts b/__tests__/mcp-tool-annotations.test.ts index 28dbe2593..66b7ad8b6 100644 --- a/__tests__/mcp-tool-annotations.test.ts +++ b/__tests__/mcp-tool-annotations.test.ts @@ -95,11 +95,12 @@ describe('Live tool surface keeps annotations with a project open (#1018)', () = expect(got.length).toBeGreaterThan(0); for (const tool of got) expectReadOnly(tool); - // explore's description is regenerated with a per-repo budget suffix via - // object spread; the annotation must survive that rewrite. + // explore's description is regenerated with a per-repo advisory-guidance + // suffix via object spread; the annotation must survive that rewrite. const explore = got.find((t) => t.name === 'codegraph_explore'); expect(explore).toBeDefined(); - expect(explore!.description).toMatch(/Budget: make at most/); + expect(explore!.description).toMatch(/advisory only, NOT a quota/); + expect(explore!.description).not.toMatch(/make at most/); expectReadOnly(explore!); }); }); diff --git a/scripts/agent-eval/probe-suite-envelope.mjs b/scripts/agent-eval/probe-suite-envelope.mjs index dd04aad07..aaf24d989 100644 --- a/scripts/agent-eval/probe-suite-envelope.mjs +++ b/scripts/agent-eval/probe-suite-envelope.mjs @@ -85,7 +85,7 @@ try { epilogueCut: text.includes('omitted for size'), sectionCut: text.includes('output truncated to budget'), notShown: text.includes('Not shown above'), - budgetNote: text.includes('**Explore budget:'), + budgetNote: text.includes('advisory only, NOT a quota'), }); } } finally { diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index d1d013514..b6bdcd466 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -207,7 +207,10 @@ export interface ExploreOutputBudget { includeAdditionalFiles: boolean; /** Include the "Complete source code is included above…" reminder. */ includeCompletenessSignal: boolean; - /** Include the explore-budget reminder at the end. */ + /** + * Include the advisory exploration-guidance note at the end. Purely + * advisory — the server NEVER rejects or rate-limits extra explore calls. + */ includeBudgetNote: boolean; } @@ -1490,7 +1493,7 @@ export class ToolHandler { if (tool.name === 'codegraph_explore') { return { ...tool, - description: `${tool.description} Budget: make at most ${budget} calls for this project (${stats.fileCount.toLocaleString()} files indexed).`, + description: `${tool.description} Exploration guidance — advisory only, NOT a quota: ~${budget} focused calls usually cover this project (${stats.fileCount.toLocaleString()} files indexed), and extra calls are never rejected or rate-limited.`, }; } return tool; @@ -5687,13 +5690,17 @@ export class ToolHandler { ? ['', `> Some file sections were trimmed for size. For a specific symbol you still need, run another \`codegraph_explore\` (or \`codegraph_node\`) with its exact name — line-numbered source, cheaper and more complete than Read.`] : []; - // Explore budget note based on project size. + // Advisory exploration-guidance note based on project size. Deliberately + // phrased as guidance, NOT a quota: agents read "budget / remaining calls / + // Synthesize once" as a hard cap and stop exploring early, falling back to + // grep + Read (which costs more tokens). The server never rejects or + // rate-limits extra explore calls, and the note says so explicitly. let budgetBlock: string[] = []; if (budget.includeBudgetNote) { try { const stats = cg.getStats(); const callBudget = getExploreBudget(stats.fileCount); - budgetBlock = ['', `> **Explore budget: ${callBudget} calls for this project (${stats.fileCount.toLocaleString()} files indexed).** Each call covers ~6 files; if your question spans more, spend your remaining calls on the uncovered area BEFORE falling back to Read — another explore is cheaper and more complete than reading those files. Synthesize once you've used ${callBudget}.`]; + budgetBlock = ['', `> **Exploration guidance — advisory only, NOT a quota: this project (~${stats.fileCount.toLocaleString()} files indexed) is usually covered in ≈${callBudget} focused explore calls, and extra calls are never rejected or rate-limited. If the response above does not fully cover your question, run another codegraph_explore on the uncovered symbols — it is cheaper and more complete than Read. Only stop exploring when the response actually covers the flow you asked about.`]; } catch { // Stats unavailable — skip budget note }