diff --git a/CHANGELOG.md b/CHANGELOG.md index e558807..c9b2970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -139,6 +139,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). #### 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) - 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) diff --git a/__tests__/explore-output-budget.test.ts b/__tests__/explore-output-budget.test.ts index 9d9a0b3..efe302f 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 2162e00..2b5920b 100644 --- a/__tests__/mcp-tool-annotations.test.ts +++ b/__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); 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!); expectExploreAlwaysLoad(got); }); diff --git a/scripts/agent-eval/probe-suite-envelope.mjs b/scripts/agent-eval/probe-suite-envelope.mjs index dd04aad..aaf24d9 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/server-instructions.ts b/src/mcp/server-instructions.ts index 2b2d4f8..f7e6e02 100644 --- a/src/mcp/server-instructions.ts +++ b/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. - **"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. -- **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\`). ## Anti-patterns diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 8ef64a4..dd57d8a 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; } @@ -1686,7 +1689,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; @@ -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.`] : []; - // 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 }