Explorar o código

fix(mcp): make explore guidance advisory (#1504) (#1805)

Apply upstream commit 1572d90d7174f0c65019e768c8f65728fae668c5 from
PR #1570 by @rongbc.

Clarify that suggested explore call counts are advisory and extra calls
remain available. Sync the MCP initialization guidance and eval probe,
retain the upstream regression tests, close the footer's bold formatting,
and credit the fix under Unreleased in the changelog.

Validation: npm run build; focused Vitest (3 files, 53 tests); source and
dist wording greps; git diff --check.

Fixes #1504

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
Colby Mchenry hai 6 horas
pai
achega
e79d5df448

+ 2 - 0
CHANGELOG.md

@@ -139,6 +139,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
 #### MCP / indexing
 #### MCP / indexing
 
 
+- `codegraph_explore` now makes clear that suggested call counts are advisory, so agents keep exploring when an answer is incomplete; thanks @rongbc. (#1504, #1570)
+
 - C++ functions following anonymous namespaces containing raw-string templates are now indexed correctly, even when template text resembles an unfinished macro call. (#1505)
 - C++ functions following anonymous namespaces containing raw-string templates are now indexed correctly, even when template text resembles an unfinished macro call. (#1505)
 
 
 - Indexing now warns when parser errors leave a file with no symbols, including C++ raw strings with 16-character delimiters, so missing code is no longer silent. (#1522)
 - Indexing now warns when parser errors leave a file with no symbols, including C++ raw strings with 16-character delimiters, so missing code is no longer silent. (#1522)

+ 21 - 2
__tests__/explore-output-budget.test.ts

@@ -6,7 +6,7 @@
  * grep+Read. These tests pin the per-tier budget shape so future tuning
  * grep+Read. These tests pin the per-tier budget shape so future tuning
  * doesn't silently drift the small-project case back into bloat.
  * 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 fs from 'fs';
 import * as path from 'path';
 import * as path from 'path';
 import * as os from 'os';
 import * as os from 'os';
@@ -198,7 +198,26 @@ describe('codegraph_explore output respects the adaptive budget', () => {
     const text = result.content?.[0]?.text ?? '';
     const text = result.content?.[0]?.text ?? '';
     expect(text).not.toContain('### Additional relevant files');
     expect(text).not.toContain('### Additional relevant files');
     expect(text).not.toContain('Complete source code is included above');
     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<CodeGraph['getStats']>);
+    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 () => {
   it('still includes the Relationships section — it is the cheapest structural signal', async () => {

+ 4 - 3
__tests__/mcp-tool-annotations.test.ts

@@ -108,11 +108,12 @@ describe('Live tool surface keeps annotations with a project open (#1018)', () =
     expect(got.length).toBeGreaterThan(0);
     expect(got.length).toBeGreaterThan(0);
     for (const tool of got) expectReadOnly(tool);
     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');
     const explore = got.find((t) => t.name === 'codegraph_explore');
     expect(explore).toBeDefined();
     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!);
     expectReadOnly(explore!);
     expectExploreAlwaysLoad(got);
     expectExploreAlwaysLoad(got);
   });
   });

+ 1 - 1
scripts/agent-eval/probe-suite-envelope.mjs

@@ -85,7 +85,7 @@ try {
       epilogueCut: text.includes('omitted for size'),
       epilogueCut: text.includes('omitted for size'),
       sectionCut: text.includes('output truncated to budget'),
       sectionCut: text.includes('output truncated to budget'),
       notShown: text.includes('Not shown above'),
       notShown: text.includes('Not shown above'),
-      budgetNote: text.includes('**Explore budget:'),
+      budgetNote: text.includes('advisory only, NOT a quota'),
     });
     });
   }
   }
 } finally {
 } finally {

+ 1 - 1
src/mcp/server-instructions.ts

@@ -54,7 +54,7 @@ calls; a grep/read exploration is dozens.
 - **Almost any question — "how does X work", architecture, a bug, "what/where is X", or surveying an area** → \`codegraph_explore\` with a natural-language question or the relevant names. ONE capped call returns the verbatim source grouped by file; most often the ONLY call you need.
 - **Almost any question — "how does X work", architecture, a bug, "what/where is X", or surveying an area** → \`codegraph_explore\` with a natural-language question or the relevant names. ONE capped call returns the verbatim source grouped by file; most often the ONLY call you need.
 - **"How does X reach/become Y? / the flow / the path from X to Y"** → \`codegraph_explore\`, naming the symbols that span the flow (e.g. \`mutateElement renderScene\`) — it surfaces the call path among them, riding dynamic-dispatch hops, and returns their source.
 - **"How does X reach/become Y? / the flow / the path from X to Y"** → \`codegraph_explore\`, naming the symbols that span the flow (e.g. \`mutateElement renderScene\`) — it surfaces the call path among them, riding dynamic-dispatch hops, and returns their source.
 - **Reading or editing a file/symbol you can name** → put its name or file path in the \`codegraph_explore\` query — it returns that current line-numbered source (safe to \`Edit\` from) with the call path and blast radius attached, so you don't Read it separately. For an overloaded name it returns every matching definition's body in one call.
 - **Reading or editing a file/symbol you can name** → put its name or file path in the \`codegraph_explore\` query — it returns that current line-numbered source (safe to \`Edit\` from) with the call path and blast radius attached, so you don't Read it separately. For an overloaded name it returns every matching definition's body in one call.
-- **Need more?** Call \`codegraph_explore\` again with more specific names — treat the source it returns as already Read.
+- **Need more?** Call \`codegraph_explore\` again with more specific names — treat the source it returns as already Read. Suggested call counts are advisory only, NOT a quota; extra calls are never rejected or rate-limited.
 - Qualified symbol names accept dots, \`::\`, or slashes, including containers whose names contain dots (for example, \`AppWeb.Format.group\`).
 - Qualified symbol names accept dots, \`::\`, or slashes, including containers whose names contain dots (for example, \`AppWeb.Format.group\`).
 
 
 ## Anti-patterns
 ## Anti-patterns

+ 11 - 4
src/mcp/tools.ts

@@ -207,7 +207,10 @@ export interface ExploreOutputBudget {
   includeAdditionalFiles: boolean;
   includeAdditionalFiles: boolean;
   /** Include the "Complete source code is included above…" reminder. */
   /** Include the "Complete source code is included above…" reminder. */
   includeCompletenessSignal: boolean;
   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;
   includeBudgetNote: boolean;
 }
 }
 
 
@@ -1686,7 +1689,7 @@ export class ToolHandler {
         if (tool.name === 'codegraph_explore') {
         if (tool.name === 'codegraph_explore') {
           return {
           return {
             ...tool,
             ...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;
         return tool;
@@ -5941,13 +5944,17 @@ export class ToolHandler {
         ? ['', `> Some file sections were trimmed for size. Elided symbols are named inside gap markers as \`name (file:line)\` and preferred in the file header — run another \`codegraph_explore\` (or \`codegraph_node\`) with those exact names for their source.`]
         ? ['', `> Some file sections were trimmed for size. Elided symbols are named inside gap markers as \`name (file:line)\` and preferred in the file header — run another \`codegraph_explore\` (or \`codegraph_node\`) with those exact names for their source.`]
         : [];
         : [];
 
 
-    // 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[] = [];
     let budgetBlock: string[] = [];
     if (budget.includeBudgetNote) {
     if (budget.includeBudgetNote) {
       try {
       try {
         const stats = cg.getStats();
         const stats = cg.getStats();
         const callBudget = getExploreBudget(stats.fileCount);
         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 {
       } catch {
         // Stats unavailable — skip budget note
         // Stats unavailable — skip budget note
       }
       }