Skip to content

Commit e0f5b23

Browse files
bugerclaude
andauthored
fix: improve agent prompts, extract path autofix, and Gemini v3 upgrade (#492)
* fix: add anti-bash-for-code-exploration guidance and support promptType+customPrompt together - Update bash tool description to explicitly forbid code exploration (grep, cat, find, etc.) - Add anti-bash instruction #7 to commonInstructions in getSystemMessage() - Add anti-bash guidance to Claude and Codex native system prompts - Support both promptType and customPrompt simultaneously: predefined prompt as base + custom wrapped in <custom-instructions> tag - When only customPrompt is set (no promptType), don't append commonInstructions since custom prompts are self-contained Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: upgrade @ai-sdk/google to v3 for Gemini thought_signature support Gemini 3.x models require thought_signature to be round-tripped in function call conversation history. @ai-sdk/google v2 stripped these out, causing 400 errors. v3.0.0+ preserves them correctly. Fixes: "Function call is missing a thought_signature in functionCall parts" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: auto-fix extract paths when model uses wrong workspace subdirectory When search returns relative paths (e.g., "gateway/file.go") and the model constructs wrong absolute paths by prepending the workspace root instead of the project subdirectory (e.g., /workspace/gateway/file.go instead of /workspace/tyk/gateway/file.go), the extract tool now auto-corrects by trying each allowedFolder as an alternative base path. This was causing extract to fail 3x in a row, making models fall back to bash cat for file reading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add unit tests for extract path auto-fix 8 tests covering the workspace subdirectory path correction: - Missing project subdirectory fix - Line number and symbol suffix preservation - Nested paths, multiple targets, multiple allowedFolders - No-op for correct paths and unfixable paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: strengthen search delegate prompt against redundant searches Based on real trace analysis showing delegates making 29 searches for one term with case/path variations. Key additions: - "When to use exact=true" section for known symbol lookups - BAD examples from real traces: case variations (limitDRL/LimitDRL), snake_case variations, same query on different paths, full function signature searches - GOOD examples: exact=true for symbol names like ForwardMessage - Clarify that changing path does NOT bypass dedup - "Do NOT search full signatures, just use exact=true with method name" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a8b43ef commit e0f5b23

File tree

3 files changed

+243
-9
lines changed

3 files changed

+243
-9
lines changed

npm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
"dependencies": {
7676
"@ai-sdk/amazon-bedrock": "^1.0.8",
7777
"@ai-sdk/anthropic": "^2.0.8",
78-
"@ai-sdk/google": "^2.0.14",
78+
"@ai-sdk/google": "^3.0.37",
7979
"@ai-sdk/openai": "^2.0.10",
8080
"@anthropic-ai/claude-agent-sdk": "^0.1.46",
8181
"@modelcontextprotocol/sdk": "^1.0.0",

npm/src/tools/vercel.js

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,19 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
144144
'- listFiles: Understand directory structure to find where relevant code might live.',
145145
'',
146146
'CRITICAL - How probe search works (do NOT ignore):',
147-
'- By default (exact=false), probe ALREADY handles stemming, case-insensitive matching, and camelCase/snake_case splitting.',
147+
'- By default (exact=false), probe ALREADY handles stemming, case-insensitive matching, and camelCase/snake_case splitting automatically.',
148148
'- Searching "allowed_ips" ALREADY matches "AllowedIPs", "allowedIps", "allowed_ips", etc. Do NOT manually try case/style variations.',
149149
'- Searching "getUserData" ALREADY matches "get", "user", "data" and their variations.',
150-
'- NEVER repeat the same search query — you will get the same results.',
150+
'- NEVER repeat the same search query — you will get the same results. Changing the path does NOT change this.',
151151
'- NEVER search trivial variations of the same keyword (e.g., AllowedIPs then allowedIps then allowed_ips). This is wasteful — probe handles it.',
152-
'- If a search returns no results, the term likely does not exist in that path. Try a genuinely DIFFERENT keyword or concept, not a variation.',
153-
'- If 2-3 consecutive searches return no results for a concept, STOP searching for it and move on.',
152+
'- If a search returns no results, the term likely does not exist. Try a genuinely DIFFERENT keyword or concept, not a variation.',
153+
'- If 2-3 searches return no results for a concept, STOP searching for it and move on. Do NOT keep retrying.',
154+
'',
155+
'When to use exact=true:',
156+
'- Use exact=true when searching for a KNOWN symbol name (function, type, variable, struct).',
157+
'- exact=true matches the literal string only — no stemming, no splitting.',
158+
'- This is ideal for precise lookups: exact=true "ForwardMessage", exact=true "SessionLimiter", exact=true "ThrottleRetryLimit".',
159+
'- Do NOT use exact=true for exploratory/conceptual queries — use the default for those.',
154160
'',
155161
'GOOD search strategy (do this):',
156162
' Query: "How does authentication work and how are sessions managed?"',
@@ -159,10 +165,18 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
159165
' → search "allowlist middleware" (one search, probe handles IP/ip/Ip variations)',
160166
' Query: "How does BM25 scoring work with SIMD optimization?"',
161167
' → search "BM25 scoring" → search "SIMD optimization" (two different concepts)',
168+
' Query: "Find ForwardMessage and SessionLimiter functions"',
169+
' → search exact=true "ForwardMessage" → search exact=true "SessionLimiter" (known symbols, use exact)',
170+
' Query: "Find ThrottleRetryLimit usage"',
171+
' → search exact=true "ThrottleRetryLimit" (one search, if no results the symbol does not exist — stop)',
162172
'',
163173
'BAD search strategy (never do this):',
164-
' → search "AllowedIPs" → search "allowedIps" → search "allowed_ips" (WRONG: these are trivial case variations, probe handles them)',
165-
' → search "CIDR" → search "cidr" → search "Cidr" → search "*cidr*" (WRONG: same keyword repeated with variations)',
174+
' → search "AllowedIPs" → search "allowedIps" → search "allowed_ips" (WRONG: case/style variations, probe handles them)',
175+
' → search "limitDRL" → search "LimitDRL" (WRONG: case variation of same term)',
176+
' → search "throttle_retry_limit" after searching "ThrottleRetryLimit" (WRONG: snake_case variation, probe handles it)',
177+
' → search "ThrottleRetryLimit" path=tyk → search "ThrottleRetryLimit" path=gateway → search "ThrottleRetryLimit" path=apidef (WRONG: same query on different paths hoping for different results)',
178+
' → search "func (k *RateLimitAndQuotaCheck) handleRateLimitFailure" (WRONG: do not search full function signatures, just use exact=true "handleRateLimitFailure")',
179+
' → search "ForwardMessage" → search "ForwardMessage" → search "ForwardMessage" (WRONG: repeating the exact same query)',
166180
' → search "error handling" → search "error handling" → search "error handling" (WRONG: repeating exact same query)',
167181
'',
168182
'Keyword tips:',
@@ -171,12 +185,13 @@ function buildSearchDelegateTask({ searchQuery, searchPath, exact, language, all
171185
'- To bypass stopword filtering: wrap terms in quotes ("return", "struct") or set exact=true. Both disable stemming and splitting too.',
172186
'- Multiple words without operators use OR logic: foo bar = foo OR bar. Use AND explicitly if you need both: foo AND bar.',
173187
'- camelCase terms are split: getUserData becomes "get", "user", "data" — so one search covers all naming styles.',
188+
'- Do NOT search for full function signatures like "func (r *Type) Method(args)". Just search for the method name with exact=true.',
174189
'',
175190
'Strategy:',
176191
'1. Analyze the query - identify key concepts, entities, and relationships',
177-
'2. Run ONE focused search per concept with the most natural keyword. Trust probe to handle variations.',
192+
'2. Run ONE focused search per concept. For known symbol names use exact=true. For concepts use default (exact=false).',
178193
'3. If a search returns results, use extract to verify relevance',
179-
'4. Only try a different keyword if the first one returned irrelevant results (not if it returned no results — that means the concept is absent)',
194+
'4. If a search returns NO results, the term does not exist in the codebase. Do NOT retry with variations, different paths, or longer strings. Move on.',
180195
'5. Combine all relevant targets in your final response',
181196
'',
182197
`Query: ${searchQuery}`,
@@ -570,6 +585,50 @@ export const extractTool = (options = {}) => {
570585
// Resolve relative paths in targets against cwd
571586
extractFiles = parsedTargets.map(target => resolveTargetPath(target, effectiveCwd));
572587

588+
// Auto-fix: if resolved paths don't exist, try allowedFolders subdirs
589+
// Handles when search returns relative paths (e.g., "gateway/file.go") and
590+
// model constructs wrong absolute paths (e.g., /workspace/gateway/file.go
591+
// instead of /workspace/tyk/gateway/file.go)
592+
if (options.allowedFolders && options.allowedFolders.length > 0) {
593+
extractFiles = extractFiles.map(target => {
594+
const { filePart, suffix } = splitTargetSuffix(target);
595+
if (existsSync(filePart)) return target;
596+
597+
// Try resolving the relative tail against each allowedFolder
598+
const cwdPrefix = (effectiveCwd.endsWith('/') ? effectiveCwd : effectiveCwd + '/');
599+
const relativePart = filePart.startsWith(cwdPrefix)
600+
? filePart.slice(cwdPrefix.length)
601+
: null;
602+
603+
if (relativePart) {
604+
for (const folder of options.allowedFolders) {
605+
const candidate = folder + '/' + relativePart;
606+
if (existsSync(candidate)) {
607+
if (debug) console.error(`[extract] Auto-fixed path: ${filePart}${candidate}`);
608+
return candidate + suffix;
609+
}
610+
}
611+
}
612+
613+
// Try stripping workspace prefix and resolving against allowedFolders
614+
// e.g., /tmp/visor-workspaces/abc/gateway/file.go → try each folder + gateway/file.go
615+
for (const folder of options.allowedFolders) {
616+
const folderPrefix = folder.endsWith('/') ? folder : folder + '/';
617+
const wsParent = folderPrefix.replace(/[^/]+\/$/, '');
618+
if (filePart.startsWith(wsParent)) {
619+
const tail = filePart.slice(wsParent.length);
620+
const candidate = folderPrefix + tail;
621+
if (candidate !== filePart && existsSync(candidate)) {
622+
if (debug) console.error(`[extract] Auto-fixed path via workspace: ${filePart}${candidate}`);
623+
return candidate + suffix;
624+
}
625+
}
626+
}
627+
628+
return target;
629+
});
630+
}
631+
573632
// Apply format mapping for outline-xml to xml
574633
let effectiveFormat = format;
575634
if (outline && format === 'outline-xml') {
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* Tests for extract tool path auto-fix when model uses wrong workspace subdirectory.
3+
*
4+
* Scenario: search returns relative paths like "gateway/file.go", model constructs
5+
* wrong absolute path /workspace/gateway/file.go instead of /workspace/tyk/gateway/file.go.
6+
* The autofix should find the file in the correct allowedFolder subdirectory.
7+
*/
8+
9+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
10+
import { join } from 'path';
11+
import { tmpdir } from 'os';
12+
13+
// Inline splitTargetSuffix to avoid circular import from vercel.js
14+
function splitTargetSuffix(target) {
15+
const searchStart = (target.length > 2 && target[1] === ':' && /[a-zA-Z]/.test(target[0])) ? 2 : 0;
16+
const colonIdx = target.indexOf(':', searchStart);
17+
const hashIdx = target.indexOf('#');
18+
if (colonIdx !== -1 && (hashIdx === -1 || colonIdx < hashIdx)) {
19+
return { filePart: target.substring(0, colonIdx), suffix: target.substring(colonIdx) };
20+
} else if (hashIdx !== -1) {
21+
return { filePart: target.substring(0, hashIdx), suffix: target.substring(hashIdx) };
22+
}
23+
return { filePart: target, suffix: '' };
24+
}
25+
26+
// Reproduce the autofix logic from vercel.js extractTool for unit testing
27+
function autoFixExtractPaths(extractFiles, effectiveCwd, allowedFolders, debug = false) {
28+
if (!allowedFolders || allowedFolders.length === 0) return extractFiles;
29+
30+
return extractFiles.map(target => {
31+
const { filePart, suffix } = splitTargetSuffix(target);
32+
if (existsSync(filePart)) return target;
33+
34+
const cwdPrefix = (effectiveCwd.endsWith('/') ? effectiveCwd : effectiveCwd + '/');
35+
const relativePart = filePart.startsWith(cwdPrefix)
36+
? filePart.slice(cwdPrefix.length)
37+
: null;
38+
39+
if (relativePart) {
40+
for (const folder of allowedFolders) {
41+
const candidate = folder + '/' + relativePart;
42+
if (existsSync(candidate)) {
43+
return candidate + suffix;
44+
}
45+
}
46+
}
47+
48+
for (const folder of allowedFolders) {
49+
const folderPrefix = folder.endsWith('/') ? folder : folder + '/';
50+
const wsParent = folderPrefix.replace(/[^/]+\/$/, '');
51+
if (filePart.startsWith(wsParent)) {
52+
const tail = filePart.slice(wsParent.length);
53+
const candidate = folderPrefix + tail;
54+
if (candidate !== filePart && existsSync(candidate)) {
55+
return candidate + suffix;
56+
}
57+
}
58+
}
59+
60+
return target;
61+
});
62+
}
63+
64+
describe('Extract path auto-fix', () => {
65+
let workspace;
66+
let projectDir;
67+
68+
beforeAll(() => {
69+
// Create temp workspace: /tmp/xxx/ (workspace root)
70+
// /tmp/xxx/tyk/gateway/mw_rate_limiting.go
71+
// /tmp/xxx/tyk/internal/rate/limiter.go
72+
workspace = mkdtempSync(join(tmpdir(), 'probe-test-ws-'));
73+
projectDir = join(workspace, 'tyk');
74+
mkdirSync(join(projectDir, 'gateway'), { recursive: true });
75+
mkdirSync(join(projectDir, 'internal', 'rate'), { recursive: true });
76+
writeFileSync(join(projectDir, 'gateway', 'mw_rate_limiting.go'), 'package gateway');
77+
writeFileSync(join(projectDir, 'gateway', 'session_manager.go'), 'package gateway');
78+
writeFileSync(join(projectDir, 'internal', 'rate', 'limiter.go'), 'package rate');
79+
});
80+
81+
afterAll(() => {
82+
rmSync(workspace, { recursive: true, force: true });
83+
});
84+
85+
test('should fix path missing project subdirectory', () => {
86+
// Model constructs: /workspace/gateway/session_manager.go (wrong)
87+
// Correct: /workspace/tyk/gateway/session_manager.go
88+
const wrongPath = join(workspace, 'gateway', 'session_manager.go');
89+
const result = autoFixExtractPaths(
90+
[wrongPath],
91+
workspace,
92+
[projectDir]
93+
);
94+
expect(result[0]).toBe(join(projectDir, 'gateway', 'session_manager.go'));
95+
});
96+
97+
test('should fix path with line number suffix', () => {
98+
const wrongPath = join(workspace, 'gateway', 'session_manager.go') + ':50-500';
99+
const result = autoFixExtractPaths(
100+
[wrongPath],
101+
workspace,
102+
[projectDir]
103+
);
104+
expect(result[0]).toBe(join(projectDir, 'gateway', 'session_manager.go') + ':50-500');
105+
});
106+
107+
test('should fix path with symbol suffix', () => {
108+
const wrongPath = join(workspace, 'gateway', 'session_manager.go') + '#ForwardMessage';
109+
const result = autoFixExtractPaths(
110+
[wrongPath],
111+
workspace,
112+
[projectDir]
113+
);
114+
expect(result[0]).toBe(join(projectDir, 'gateway', 'session_manager.go') + '#ForwardMessage');
115+
});
116+
117+
test('should fix nested paths', () => {
118+
const wrongPath = join(workspace, 'internal', 'rate', 'limiter.go');
119+
const result = autoFixExtractPaths(
120+
[wrongPath],
121+
workspace,
122+
[projectDir]
123+
);
124+
expect(result[0]).toBe(join(projectDir, 'internal', 'rate', 'limiter.go'));
125+
});
126+
127+
test('should not modify correct paths', () => {
128+
const correctPath = join(projectDir, 'gateway', 'mw_rate_limiting.go');
129+
const result = autoFixExtractPaths(
130+
[correctPath],
131+
workspace,
132+
[projectDir]
133+
);
134+
expect(result[0]).toBe(correctPath);
135+
});
136+
137+
test('should handle multiple targets with mixed correct/wrong paths', () => {
138+
const correct = join(projectDir, 'gateway', 'mw_rate_limiting.go');
139+
const wrong = join(workspace, 'gateway', 'session_manager.go');
140+
const result = autoFixExtractPaths(
141+
[correct, wrong],
142+
workspace,
143+
[projectDir]
144+
);
145+
expect(result[0]).toBe(correct);
146+
expect(result[1]).toBe(join(projectDir, 'gateway', 'session_manager.go'));
147+
});
148+
149+
test('should keep path unchanged when file not found anywhere', () => {
150+
const nonexistent = join(workspace, 'nonexistent', 'file.go');
151+
const result = autoFixExtractPaths(
152+
[nonexistent],
153+
workspace,
154+
[projectDir]
155+
);
156+
expect(result[0]).toBe(nonexistent);
157+
});
158+
159+
test('should try multiple allowedFolders', () => {
160+
// Create a second project
161+
const project2 = join(workspace, 'analytics');
162+
mkdirSync(join(project2, 'api'), { recursive: true });
163+
writeFileSync(join(project2, 'api', 'handler.go'), 'package api');
164+
165+
const wrongPath = join(workspace, 'api', 'handler.go');
166+
const result = autoFixExtractPaths(
167+
[wrongPath],
168+
workspace,
169+
[projectDir, project2]
170+
);
171+
expect(result[0]).toBe(join(project2, 'api', 'handler.go'));
172+
173+
rmSync(project2, { recursive: true, force: true });
174+
});
175+
});

0 commit comments

Comments
 (0)