fix(mcp): reword explore guidance as advisory, not a quota

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').
This commit is contained in:
rongbaichuan
2026-08-19 17:32:10 +08:00
committed by rongbaichuan
parent c6aaa20358
commit 1572d90d71
4 changed files with 37 additions and 10 deletions
+21 -2
View File
@@ -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<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 () => {
+4 -3
View File
@@ -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!);
});
});
+1 -1
View File
@@ -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 {
+11 -4
View File
@@ -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
}