Skip to content

Commit d396049

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@956da09e97bf1ee5e9055569400110d41358cdef
1 parent 8cf2049 commit d396049

8 files changed

Lines changed: 67 additions & 17 deletions

File tree

‎agents/__tests__/base3-fast.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ describe('the base3-fast root', () => {
6666
})
6767
expect(root.toolNames).toEqual([...plain.toolNames!, 'spawn_agents'])
6868
expect(root.spawnableAgents).toEqual([...BASE3_FAST_SPAWNABLE_AGENTS])
69-
// Pickers and searchers for context, in parallel; the worker for slices.
70-
expect(root.spawnableAgents).toContain('file-picker')
69+
// Searchers for context, in parallel; the worker for slices.
70+
expect(root.spawnableAgents).not.toContain('file-picker')
7171
expect(root.spawnableAgents).toContain('code-searcher')
7272
expect(root.spawnableAgents).toContain(FREEBUFF_BASE3_FAST_WORKER_AGENT_ID)
7373
// The base3 levers the runtime reads are untouched.
@@ -99,7 +99,7 @@ describe('the base3-fast root', () => {
9999
expect(guidance.split(/\s+/).length).toBeLessThan(600)
100100
// Firm about parallel helpers, and about reconciling what they produce.
101101
expect(guidance).toContain('really, really helpful')
102-
expect(guidance).toContain('file-pickers')
102+
expect(guidance).not.toContain('file-picker')
103103
expect(guidance).toContain('code-searchers')
104104
expect(guidance).toContain('coherence is your job')
105105
expect(guidance).toContain('names the files it edited')

‎agents/base3-fast.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import type { SecretAgentDefinition } from './types/secret-agent-definition'
99
/**
1010
* base3-fast: base3's loop with a fan-out on top.
1111
*
12-
* The root is a base3 CLI root plus `spawn_agents` and three things to spawn:
13-
* `file-picker` and `code-searcher` for context, in parallel, and the worker
14-
* below for the one shape of work worth splitting — a large mechanical change
12+
* The root is a base3 CLI root plus `spawn_agents` and two things to spawn:
13+
* `code-searcher` for context, in parallel, and the worker below for the one
14+
* shape of work worth splitting — a large mechanical change
1515
* repeated across many independent files. The root implements everything
1616
* else itself: across five 62-task runs the tasks it split were the ones it
1717
* did worst on and no faster than base3 (docs/benchmarks/base3-fast/). Every
@@ -34,11 +34,11 @@ import type { SecretAgentDefinition } from './types/secret-agent-definition'
3434
* FREEBUFF_FAST_MODE_MODEL_IDS (common/src/constants/freebuff-model-ids.ts).
3535
*/
3636

37-
/** Everything the root may spawn: pickers and searchers for context, the
38-
* worker for slices. The code-searcher is programmatic — a batch of ripgrep
39-
* queries and no model call — so it needs no free-mode allowlist entry. */
37+
/** Everything the root may spawn: searchers for context, the worker for
38+
* slices. The code-searcher is programmatic — a batch of ripgrep queries and
39+
* no model call — so it needs no free-mode allowlist entry. No file-picker
40+
* since 2026-10-16: the 62-task bench without it (2026-10-05) was a wash. */
4041
export const BASE3_FAST_SPAWNABLE_AGENTS = [
41-
'file-picker',
4242
'code-searcher',
4343
FREEBUFF_BASE3_FAST_WORKER_AGENT_ID,
4444
] as const
@@ -77,7 +77,7 @@ export function buildBase3FastGuidance(
7777
7878
You are running in fast mode: besides your own tools you can spawn helpers with spawn_agents, and everything named in one spawn_agents call runs at the same time. Spawning context helpers in parallel is really, really helpful here and strongly encouraged. The notes below are the shape that works, not a script: when the user is asking a question, thinking something through or wants a small change, just answer or make it.
7979
80-
**Context.** When a task needs you to understand code you have not read, start with one spawn_agents call that spawns several file-pickers, each prompted from a different angle (the feature itself, its callers and tests, the conventions it should match), together with code-searchers, each carrying a batch of ripgrep queries for the names and patterns involved — put every query you can already think of into that first round. Then read generously: one read_files call with every path that looked relevant, rather than one file per step. Follow up with another round of code-searchers when the first points somewhere specific; a second round of file-pickers is rarely worth it. Everything you need is in the repository in front of you: do not search the web for this project's own code, history or packages.
80+
**Context.** When a task needs you to understand code you have not read, start with one spawn_agents call that spawns several code-searchers, each carrying a batch of ripgrep queries from a different angle (the feature itself, its callers and tests, the conventions it should match) — put every query you can already think of into that first round. Then read generously: one read_files call with every path that looked relevant, rather than one file per step. Follow up with another round of code-searchers when the first points somewhere specific. Everything you need is in the repository in front of you: do not search the web for this project's own code, history or packages.
8181
8282
**Implementing.** Make the change yourself. A change you hold in your head comes out coherent, and the slow part of any delegation is reading, reconciling and repairing what came back. Workers exist for one shape of work: a large, mechanical change repeated across many independent files, where each slice can be written without seeing the others, such as a rename across a tree or the same migration applied to dozens of modules. Only then spawn one ${workerAgentId} per slice, all in one spawn_agents call, after writing any piece the slices must agree on (a new type, signature or helper) yourself. Give each worker a short assignment that opens with "${BASE3_FAST_WORKER_ASSIGNMENT_OPENING}", then names the files it owns, the shared names it must use, and what belongs to another worker. Never give two workers the same file.
8383
@@ -132,7 +132,7 @@ export function createBase3FastCliRoot(
132132
...root,
133133
displayName: 'Buffy (fast mode)',
134134
spawnerPrompt:
135-
'Fast-mode coding agent: gathers context with parallel file-pickers and code-searchers, implements the change itself, and can split a large mechanical change across parallel prompt-cached workers that report which files they changed',
135+
'Fast-mode coding agent: gathers context with parallel code-searchers, implements the change itself, and can split a large mechanical change across parallel prompt-cached workers that report which files they changed',
136136
toolNames: [...root.toolNames!, 'spawn_agents'],
137137
spawnableAgents: BASE3_FAST_SPAWNABLE_AGENTS.map((id) =>
138138
id === FREEBUFF_BASE3_FAST_WORKER_AGENT_ID ? workerAgentId : id,

‎agents/projects-subagent.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ import type { SecretAgentDefinition } from './types/secret-agent-definition'
2323
*
2424
* At most FREEBUFF_PROJECTS_MAX_LIVE_SUBAGENTS run at once under one root,
2525
* nested ones included (the agent runtime's spawn_agents handler). The helpers
26-
* one of them spawns (file-picker and its file-lister, code-searcher,
27-
* researcher-web) count against that subagent's own MAX_LIVE_SUBAGENTS_PER_ROOT,
26+
* one of them spawns (code-searcher, researcher-web) count against that subagent's own MAX_LIVE_SUBAGENTS_PER_ROOT,
2827
* apart from the root's and its siblings', so they neither starve nor are
2928
* starved by the rest of the tree. Every spawn_agents call is held to
3029
* MAX_SPAWN_AGENTS_PER_CALL. Every agent also has its step budget, and

‎common/src/constants/free-agents.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export const FREEBUFF_DESKTOP_THREAD_AGENT_IDS = [
176176
/**
177177
* The base3-fast root and its worker subagent (agents/base3-fast.ts), one pair
178178
* for the one model that runs fast mode today. The root is a base3 root with
179-
* a fan-out: it spawns file-pickers in parallel, then
179+
* a fan-out: it spawns code-searchers in parallel, then
180180
* splits the implementation across parallel workers that inherit its system
181181
* prompt and history (so their requests are prompt-cache hits), and verifies
182182
* the combined result itself. The worker is pinned to the same model as the
@@ -228,7 +228,7 @@ export const FREEBUFF_WEB_BASE3_AGENT_ID_BY_MODEL: Record<string, string> = {
228228
[FREEBUFF_DEEPSEEK_V4_PRO_MODEL_ID]: 'base3-free-deepseek',
229229
[FREEBUFF_DEEPSEEK_V4_FLASH_MODEL_ID]: 'base3-free-deepseek-flash',
230230
// Fast mode runs the base3-FAST harness (agents/base3-fast.ts): base3's
231-
// loop plus parallel file-pickers and prompt-cached worker
231+
// loop plus parallel code-searchers and prompt-cached worker
232232
// subagents. Registered in the base3 maps because it is the base3 family's
233233
// rollout shape (windowed reads, mechanical compaction, no instructions
234234
// prompt) with a fan-out on top; the no-subagents assertions on this map

‎sdk/CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ All notable changes to the @codebuff/sdk package will be documented in this file
44

55
## [Unreleased]
66

7+
- New `skipSymbolIndex` run option: a fresh session builds the file tree but skips the tree-sitter parse of every project file, leaving `fileTokenScores` and `tokenCallers` empty. Set it when your agents do not use the file-picker, `read_subtree` symbols or `read_files`' `referencedBy`.
8+
79
- `compactRunState` now rewrites a run's history into at most 12k tokens (`MECHANICAL_HISTORY_TOKENS`) of summary plus a working set of file reads, whatever `maxContextLength` allows; it used to fill the budget. The newest read of each file not edited since comes back whole and every other read becomes an outline stub, so expect a much smaller `nextTokens`. The live request and the newest tool results are kept whole on top of the cap.
810

911
- **Security:** `read_url` now connects only to an address it has just validated. Resolution and the private/reserved-address check happen in the connection's own `lookup`, and the host is refused if any answer is blocked, so a hostname can no longer pass the check with one answer and connect with another. Every redirect hop is validated the same way. More non-routable ranges are refused: IPv4-compatible IPv6 (`::a.b.c.d`), 198.18.0.0/15, `fec0::/10` and `64:ff9b:1::/48`. The tool call passes only `url` and `max_chars` through to `readUrl`. Under Bun with a proxy variable set, Bun's `fetch` is still used so the proxy keeps working.

‎sdk/src/__tests__/initial-session-state.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import path from 'path'
55
import { describe, expect, test, beforeEach, spyOn } from 'bun:test'
66
import { z } from 'zod/v4'
77

8+
import { getAllFilePaths } from '@codebuff/common/project-file-tree'
9+
810
import { initialSessionState } from '../run-state'
911

1012
import type { MockStatResult } from '@codebuff/common/testing/mock-types'
@@ -160,6 +162,42 @@ describe('Initial Session State', () => {
160162
expect(readFilePaths.some((p) => p.endsWith('AGENTS.md'))).toBe(true)
161163
})
162164

165+
test('skipSymbolIndex builds the file tree without reading a source file', async () => {
166+
mockFs.readdir = (async (dirPath: string) => {
167+
if (dirPath === '/test-project') return ['src', '.gitignore']
168+
if (dirPath === '/test-project/src') return ['index.ts', 'utils.ts']
169+
return []
170+
}) as CodebuffFileSystem['readdir']
171+
mockFs.stat = (async (filePath: string) =>
172+
({
173+
isDirectory: () => filePath === '/test-project/src',
174+
isFile: () => filePath !== '/test-project/src',
175+
size: 100,
176+
}) as MockStatResult & { size: number }) as CodebuffFileSystem['stat']
177+
const readFilePaths: string[] = []
178+
const originalReadFile = mockFs.readFile
179+
mockFs.readFile = (async (filePath: string, encoding?: BufferEncoding) => {
180+
readFilePaths.push(filePath)
181+
return originalReadFile(filePath, encoding)
182+
}) as CodebuffFileSystem['readFile']
183+
184+
const sessionState = await initialSessionState({
185+
cwd: '/test-project',
186+
knowledgeFiles: {},
187+
skipSymbolIndex: true,
188+
fs: mockFs,
189+
logger: mockLogger,
190+
})
191+
192+
expect(getAllFilePaths(sessionState.fileContext.fileTree).sort()).toEqual(
193+
['.gitignore', 'src/index.ts', 'src/utils.ts'],
194+
)
195+
expect(sessionState.fileContext.fileTokenScores).toEqual({})
196+
expect(sessionState.fileContext.tokenCallers).toEqual({})
197+
// Only the walk's ignore files are read, never a file to parse.
198+
expect(readFilePaths.some((p) => p.endsWith('.ts'))).toBe(false)
199+
})
200+
163201
test('derives knowledgeFiles from projectFiles when not provided', async () => {
164202
const projectFiles = {
165203
'src/index.ts': 'console.log("Hello world");',

‎sdk/src/run-state.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ export type InitialSessionStateOptions = {
116116
/** Precomputed index for exactly these `projectFiles` (see
117117
* ComputedProjectIndex). Ignored when `projectFiles` is absent. */
118118
projectIndex?: ComputedProjectIndex
119+
/** Build the file tree but skip the tree-sitter parse (it reads every
120+
* project file before the first request), leaving `fileTokenScores` and
121+
* `tokenCallers` empty. Only the file-picker's tree prompt, `read_subtree`
122+
* and `read_files`' `referencedBy` read them. */
123+
skipSymbolIndex?: boolean
119124
knowledgeFiles?: Record<string, string>
120125
/** User-provided knowledge files that will be merged with home directory files */
121126
userKnowledgeFiles?: Record<string, string>
@@ -945,7 +950,9 @@ export async function initialSessionState(
945950
discoveredProject,
946951
})
947952
: undefined
948-
if (projectIndex) {
953+
if (projectIndex && params.skipSymbolIndex) {
954+
fileTree = projectIndex.fileTree
955+
} else if (projectIndex) {
949956
const result = await computeProjectIndex(projectIndex)
950957
fileTree = result.fileTree
951958
fileTokenScores = result.fileTokenScores

‎sdk/src/run.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ export type CodebuffClientOptions = {
200200
* `computeProjectIndexFromFiles`). Skips the per-run tree-sitter parse;
201201
* ignored when `projectFiles` is absent. */
202202
projectIndex?: ComputedProjectIndex
203+
/** See `InitialSessionStateOptions.skipSymbolIndex`. */
204+
skipSymbolIndex?: boolean
203205
knowledgeFiles?: Record<string, string>
204206
agentDefinitions?: AgentDefinition[]
205207
maxAgentSteps?: number
@@ -569,6 +571,7 @@ async function runOnce({
569571
includeHomeSkills,
570572
projectFiles,
571573
projectIndex,
574+
skipSymbolIndex,
572575
knowledgeFiles,
573576
agentDefinitions,
574577
maxAgentSteps = MAX_AGENT_STEPS_DEFAULT,
@@ -661,6 +664,7 @@ async function runOnce({
661664
customToolDefinitions,
662665
projectFiles,
663666
projectIndex,
667+
skipSymbolIndex,
664668
maxAgentSteps,
665669
fs,
666670
spawn,

0 commit comments

Comments
 (0)