Skip to content

Commit be16922

Browse files
committed
Fix read subtree bug for sub directories
1 parent 06d1ec9 commit be16922

File tree

2 files changed

+94
-2
lines changed

2 files changed

+94
-2
lines changed

packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,64 @@ describe('handleReadSubtree', () => {
140140
expect(String(errEntry.errorMessage)).toContain('Path not found or ignored')
141141
})
142142

143+
it('includes variables when reading a subdirectory with proper path mapping', async () => {
144+
const fileContext = buildMockFileContext()
145+
const logger = createLogger()
146+
147+
// Test with a deeper nested structure to expose potential path issues
148+
fileContext.fileTree = [
149+
{
150+
name: 'packages',
151+
type: 'directory',
152+
filePath: 'packages',
153+
children: [
154+
{
155+
name: 'backend',
156+
type: 'directory',
157+
filePath: 'packages/backend',
158+
children: [
159+
{
160+
name: 'index.ts',
161+
type: 'file',
162+
filePath: 'packages/backend/index.ts',
163+
lastReadTime: 0,
164+
},
165+
],
166+
},
167+
],
168+
},
169+
]
170+
fileContext.fileTokenScores = {
171+
'packages/backend/index.ts': { myFunction: 5.0, myClass: 3.0 },
172+
}
173+
174+
const toolCall: CodebuffToolCall<'read_subtree'> = {
175+
toolName: 'read_subtree',
176+
toolCallId: 'tc-subdir',
177+
input: { paths: ['packages/backend'], maxTokens: 50000 },
178+
}
179+
180+
const { result } = handleReadSubtree({
181+
previousToolCallFinished: Promise.resolve(),
182+
toolCall,
183+
fileContext,
184+
logger,
185+
})
186+
187+
const output = await result
188+
expect(output[0].type).toBe('json')
189+
const value = output[0].value as any[]
190+
const dirEntry = value.find(
191+
(v) => v.type === 'directory' && v.path === 'packages/backend',
192+
)
193+
expect(dirEntry).toBeTruthy()
194+
expect(typeof dirEntry.printedTree).toBe('string')
195+
196+
// The printedTree should include the variable names from fileTokenScores
197+
expect(dirEntry.printedTree).toContain('myFunction')
198+
expect(dirEntry.printedTree).toContain('myClass')
199+
})
200+
143201
it('honors maxTokens by reducing token count under a tiny budget', async () => {
144202
const fileContext = buildMockFileContext()
145203
const logger = createLogger()

packages/agent-runtime/src/tools/handlers/tool/read-subtree.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import type {
1414

1515
type ToolName = 'read_subtree'
1616

17-
1817
export const handleReadSubtree = ((params: {
1918
previousToolCallFinished: Promise<void>
2019
toolCall: CodebuffToolCall<ToolName>
@@ -31,9 +30,44 @@ export const handleReadSubtree = ((params: {
3130
const allFiles = new Set(getAllFilePaths(fileContext.fileTree))
3231

3332
const buildDirectoryResult = (dirNodes: FileTreeNode[], outPath: string) => {
33+
const subTree = deepClone(dirNodes)
34+
35+
// Remap token scores so keys match the paths built by printFileTreeWithTokens.
36+
// When printFileTreeWithTokens walks a subtree starting from dirNodes,
37+
// it builds paths starting from the node names, not from an empty root.
38+
// So for a node with name 'backend' inside 'packages', the paths will be
39+
// 'backend/file.ts', not 'packages/backend/file.ts'.
40+
const remappedTokenScores: Record<string, Record<string, number>> = {}
41+
const prefix =
42+
outPath === '.' || outPath === '/' || outPath === ''
43+
? ''
44+
: outPath.replace(/\\/g, '/')
45+
46+
for (const [filePath, tokens] of Object.entries(
47+
fileContext.fileTokenScores,
48+
)) {
49+
const normalized = filePath.replace(/\\/g, '/')
50+
if (!prefix || normalized.startsWith(prefix + '/')) {
51+
// Strip the parent path prefix and keep the dirBaseName + remainder
52+
const fullPrefix = prefix
53+
? prefix.split('/').slice(0, -1).join('/')
54+
: ''
55+
const afterParent = fullPrefix
56+
? normalized.startsWith(fullPrefix + '/')
57+
? normalized.slice(fullPrefix.length + 1)
58+
: null
59+
: normalized
60+
61+
if (afterParent && !afterParent.startsWith('../')) {
62+
remappedTokenScores[afterParent] = tokens
63+
}
64+
}
65+
}
66+
3467
const subctx: ProjectFileContext = {
3568
...fileContext,
36-
fileTree: deepClone(dirNodes),
69+
fileTree: subTree,
70+
fileTokenScores: remappedTokenScores,
3771
}
3872
const { printedTree, tokenCount, truncationLevel } =
3973
truncateFileTreeBasedOnTokenBudget({

0 commit comments

Comments
 (0)