From 8e65ce2bbabfea7424caf8a45aada4ddba9edb15 Mon Sep 17 00:00:00 2001 From: tjdoomer Date: Thu, 2 Apr 2026 17:52:11 +1100 Subject: [PATCH 1/2] docs: add roadmap for file summary tool Structural skeleton extraction (signatures only, no bodies) for 95% token reduction. Tree-sitter preferred, regex fallback. --- docs/roadmap/13-file-summary.md | 117 ++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/roadmap/13-file-summary.md diff --git a/docs/roadmap/13-file-summary.md b/docs/roadmap/13-file-summary.md new file mode 100644 index 00000000000..6305c06ce1c --- /dev/null +++ b/docs/roadmap/13-file-summary.md @@ -0,0 +1,117 @@ +# File Summary Tool + +## Branch: `feature/file-summary` + +## Problem + +`read_file` returns full source code. A 2000-line file uses ~4000 tokens of +context. A 32B model running locally with 32k context can only hold ~8 full +files before context is exhausted. Most of the time, the model only needs to +understand a file's interface — not read every line. + +## What it is + +A tool that returns only the structural skeleton of a file: exports, class +declarations, method signatures, type definitions, and docstrings. No function +bodies. A 2000-line file becomes a ~50-line summary. + +## Example + +``` +read_file_summary({ path: "packages/core/src/core/openaiContentGenerator.ts" }) + +→ // OpenAI provider — wraps the openai SDK for Delta's ContentGenerator interface + // Dependencies: @google/genai types, openai SDK, Config, telemetry loggers + + class OpenAIContentGenerator implements ContentGenerator { + constructor(config: ContentGeneratorConfig, gcConfig: Config) + + // Core interface + generateContent(request: GenerateContentParameters, userPromptId: string): Promise + generateContentStream(request: GenerateContentParameters, userPromptId: string): Promise> + countTokens(request: CountTokensParameters): Promise + embedContent(request: EmbedContentParameters): Promise + + // Internal — streaming + private streamGenerator(stream: AsyncIterable): AsyncGenerator<...> + private filterThinkTags(text: string): string + private convertStreamChunkToGenAIFormat(chunk: ChatCompletionChunk): GenerateContentResponse + + // Internal — format conversion + private convertToOpenAIFormat(request: GenerateContentParameters): {...} + private convertToGenAIFormat(response: OpenAIResponseFormat): GenerateContentResponse + } + + // Types + interface ChatCompletionContentPartTextWithCache extends ChatCompletionContentPartText { ... } + interface OpenAIToolCall { id, type, function: { name, arguments } } + interface OpenAIMessage { role, content, tool_calls?, tool_call_id? } + interface OpenAIUsage { prompt_tokens, completion_tokens, total_tokens } +``` + +1763 lines → 30 lines. ~95% token reduction. + +## Implementation + +### With tree-sitter (preferred, shares infra with repo-map) + +If `feature/repo-map` (branch 8) is merged, reuse its AST extraction layer. +Call `astExtractor.extractSymbols(filePath)` and format as a summary. + +### Without tree-sitter (standalone fallback) + +Regex-based extraction for the top languages: + +- **TypeScript/JavaScript**: Match `export`, `class`, `interface`, `type`, + `function`, `const ... = (` patterns. Extract JSDoc comments. +- **Python**: Match `class`, `def`, decorators. Extract docstrings. +- **Go**: Match `func`, `type`, `struct`, `interface`. +- **Rust**: Match `pub fn`, `struct`, `enum`, `trait`, `impl`. + +Less accurate than tree-sitter but zero dependencies. + +## Tool declaration + +```typescript +{ + name: 'read_file_summary', + description: 'Get a structural summary of a file: exports, class ' + + 'declarations, method signatures, and type definitions. No function ' + + 'bodies. Use this instead of read_file when you need to understand ' + + 'a file\'s interface without reading all the code. ~95% fewer tokens ' + + 'than reading the full file.', + parameters: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Absolute path to the file.', + }, + include_docstrings: { + type: 'boolean', + description: 'Include docstrings/JSDoc comments (default true).', + }, + include_private: { + type: 'boolean', + description: 'Include private/internal members (default false).', + }, + }, + required: ['path'], + }, +} +``` + +## Files to create +- `packages/core/src/tools/fileSummary.ts` — tool implementation +- `packages/core/src/tools/fileSummary.test.ts` +- `packages/core/src/indexing/signatureExtractor.ts` — regex fallback + (or reuse `astExtractor.ts` from repo-map branch) + +## Files to modify +- `packages/core/src/config/config.ts` — register tool + +## Dependencies +- Shares tree-sitter infrastructure with `feature/repo-map` (branch 8) + but can work standalone with regex fallback + +## Effort: 2-3 days (with tree-sitter), 1 day (regex-only fallback) From 2b8a66f49a6225b260cc2284be019cae918690d3 Mon Sep 17 00:00:00 2001 From: tjdoomer Date: Thu, 2 Apr 2026 22:20:59 +1100 Subject: [PATCH 2/2] feat: file summary tool for signatures-only reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regex-based extraction of exports, classes, methods, types, and interfaces — no function bodies. ~95% token reduction vs read_file. Supports TypeScript/JavaScript, Python, and generic fallback. Upgradeable to tree-sitter when repo-map branch lands. --- packages/core/src/config/config.ts | 2 + packages/core/src/tools/fileSummary.ts | 220 +++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 packages/core/src/tools/fileSummary.ts diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 2459037c4e3..a835682625c 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -36,6 +36,7 @@ import { MultiEditTool } from '../tools/multiEdit.js'; import { LintTool } from '../tools/lint.js'; import { RunTestsTool } from '../tools/runTests.js'; import { CheckpointSaveTool, CheckpointRestoreTool, CheckpointListTool } from '../tools/checkpoint.js'; +import { FileSummaryTool } from '../tools/fileSummary.js'; import { DeltaClient } from '../core/client.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { GitService } from '../services/gitService.js'; @@ -930,6 +931,7 @@ export class Config { registerCoreTool(CheckpointSaveTool, this); registerCoreTool(CheckpointRestoreTool, this); registerCoreTool(CheckpointListTool, this); + registerCoreTool(FileSummaryTool); await registry.discoverAllTools(); return registry; diff --git a/packages/core/src/tools/fileSummary.ts b/packages/core/src/tools/fileSummary.ts new file mode 100644 index 00000000000..8301f436b81 --- /dev/null +++ b/packages/core/src/tools/fileSummary.ts @@ -0,0 +1,220 @@ +/** + * File summary tool — returns only the structural skeleton of a file: + * exports, class declarations, method signatures, type definitions. + * No function bodies. A 2000-line file becomes ~50 lines. + * + * Uses regex-based extraction (no tree-sitter dependency). When the + * repo-map branch lands with tree-sitter, this can be upgraded to use + * the shared AST extraction layer for better accuracy. + */ + +import { + BaseDeclarativeTool, + BaseToolInvocation, + Kind, + ToolResult, +} from './tools.js'; +import { FunctionDeclaration } from '@google/genai'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + +interface FileSummaryParams { + path: string; + include_docstrings?: boolean; + include_private?: boolean; +} + +const fileSummarySchemaData: FunctionDeclaration = { + name: 'read_file_summary', + description: + 'Get a structural summary of a file: exports, class declarations, method signatures, and type definitions. No function bodies. Use this instead of read_file when you need to understand a file\'s interface without reading all the code. ~95% fewer tokens than reading the full file.', + parametersJsonSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Absolute path to the file.', + }, + include_docstrings: { + type: 'boolean', + description: 'Include docstrings/JSDoc comments (default true).', + }, + include_private: { + type: 'boolean', + description: 'Include private/internal members (default false).', + }, + }, + required: ['path'], + $schema: 'http://json-schema.org/draft-07/schema#', + }, +}; + +// Regex patterns for extracting structural elements by language +// These intentionally capture signatures without bodies + +function extractTypeScriptSignatures(content: string, includePrivate: boolean): string[] { + const lines: string[] = []; + const patterns = [ + // Imports (first few for dependency context) + /^import\s+.*from\s+['"].*['"]/gm, + // Export declarations + /^export\s+(?:default\s+)?(?:class|interface|type|enum|function|const|let|var|abstract\s+class)\s+\w+[^{]*/gm, + // Class declarations + /^(?:export\s+)?(?:abstract\s+)?class\s+\w+(?:\s+extends\s+\w+)?(?:\s+implements\s+[\w,\s]+)?/gm, + // Interface declarations + /^(?:export\s+)?interface\s+\w+(?:\s+extends\s+[\w,\s]+)?/gm, + // Type declarations + /^(?:export\s+)?type\s+\w+\s*=/gm, + // Enum declarations + /^(?:export\s+)?enum\s+\w+/gm, + // Method signatures (inside classes) + /^\s+(?:(?:public|protected|private|static|async|override|readonly|abstract)\s+)*\w+\s*\([^)]*\)\s*(?::\s*[^{]+)?/gm, + // Function declarations + /^(?:export\s+)?(?:async\s+)?function\s+\w+\s*\([^)]*\)\s*(?::\s*[^{]+)?/gm, + // Const arrow functions + /^(?:export\s+)?const\s+\w+\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*[^=]+)?=>/gm, + ]; + + const seen = new Set(); + + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + let line = match[0].trim(); + + // Skip private members unless requested + if (!includePrivate && /^\s*private\s+/.test(line)) continue; + + // Clean up: remove trailing { and whitespace + line = line.replace(/\s*\{?\s*$/, '').trim(); + + if (line && !seen.has(line)) { + seen.add(line); + lines.push(line); + } + } + } + + return lines; +} + +function extractPythonSignatures(content: string, includePrivate: boolean): string[] { + const lines: string[] = []; + const patterns = [ + // Imports + /^(?:from\s+\S+\s+)?import\s+.+$/gm, + // Class declarations + /^class\s+\w+(?:\(.*?\))?\s*:/gm, + // Function/method definitions + /^(?:\s*)(?:async\s+)?def\s+\w+\s*\([^)]*\)(?:\s*->\s*\S+)?\s*:/gm, + // Decorators + /^\s*@\w+(?:\(.*?\))?$/gm, + ]; + + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + const line = match[0].trimEnd(); + if (!includePrivate && /def\s+__(?!init__|str__|repr__)/.test(line)) continue; + if (!includePrivate && /def\s+_[a-z]/.test(line)) continue; + lines.push(line); + } + } + + return lines; +} + +function extractGenericSignatures(content: string): string[] { + // Fallback: extract lines that look like declarations + const lines: string[] = []; + const patterns = [ + /^(?:pub\s+)?(?:fn|func|function|def|class|struct|enum|trait|impl|interface|type)\s+.+$/gm, + ]; + + for (const pattern of patterns) { + let match; + while ((match = pattern.exec(content)) !== null) { + lines.push(match[0].replace(/\s*\{?\s*$/, '').trim()); + } + } + + return lines; +} + +class FileSummaryInvocation extends BaseToolInvocation { + getDescription(): string { + return this.params.path; + } + + async execute(): Promise { + const filePath = this.params.path; + const includePrivate = this.params.include_private ?? false; + + let content: string; + try { + content = await fs.readFile(filePath, 'utf-8'); + } catch { + return { + llmContent: [{ text: `Error: file not found: ${filePath}` }], + returnDisplay: `Error: file not found: ${filePath}`, + error: { message: `File not found: ${filePath}` }, + }; + } + + const ext = path.extname(filePath).toLowerCase(); + let signatures: string[]; + + switch (ext) { + case '.ts': + case '.tsx': + case '.js': + case '.jsx': + case '.mjs': + case '.cjs': + signatures = extractTypeScriptSignatures(content, includePrivate); + break; + case '.py': + signatures = extractPythonSignatures(content, includePrivate); + break; + default: + signatures = extractGenericSignatures(content); + break; + } + + const totalLines = content.split('\n').length; + + if (signatures.length === 0) { + const msg = `${filePath} (${totalLines} lines) — no structural elements extracted.`; + return { + llmContent: [{ text: msg }], + returnDisplay: msg, + }; + } + + const header = `// ${filePath} (${totalLines} lines → ${signatures.length} signatures)`; + const summary = [header, '', ...signatures].join('\n'); + + return { + llmContent: [{ text: summary }], + returnDisplay: summary, + }; + } +} + +export class FileSummaryTool extends BaseDeclarativeTool { + static readonly Name: string = fileSummarySchemaData.name!; + + constructor() { + super( + FileSummaryTool.Name, + 'File Summary', + fileSummarySchemaData.description!, + Kind.Read, + fileSummarySchemaData.parametersJsonSchema, + ); + } + + protected createInvocation(params: FileSummaryParams) { + return new FileSummaryInvocation(params); + } +}