Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions __tests__/explore-output-budget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 () => {
Expand Down
7 changes: 4 additions & 3 deletions __tests__/mcp-tool-annotations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!);
});
});
2 changes: 1 addition & 1 deletion scripts/agent-eval/probe-suite-envelope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 11 additions & 4 deletions src/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down