Skip to content
Merged
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
117 changes: 117 additions & 0 deletions docs/roadmap/13-file-summary.md
Original file line number Diff line number Diff line change
@@ -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<GenerateContentResponse>
generateContentStream(request: GenerateContentParameters, userPromptId: string): Promise<AsyncGenerator<...>>
countTokens(request: CountTokensParameters): Promise<CountTokensResponse>
embedContent(request: EmbedContentParameters): Promise<EmbedContentResponse>

// Internal — streaming
private streamGenerator(stream: AsyncIterable<ChatCompletionChunk>): 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)
2 changes: 2 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -930,6 +931,7 @@ export class Config {
registerCoreTool(CheckpointSaveTool, this);
registerCoreTool(CheckpointRestoreTool, this);
registerCoreTool(CheckpointListTool, this);
registerCoreTool(FileSummaryTool);

await registry.discoverAllTools();
return registry;
Expand Down
220 changes: 220 additions & 0 deletions packages/core/src/tools/fileSummary.ts
Original file line number Diff line number Diff line change
@@ -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<string>();

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<FileSummaryParams, ToolResult> {
getDescription(): string {
return this.params.path;
}

async execute(): Promise<ToolResult> {
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<FileSummaryParams, ToolResult> {
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);
}
}
Loading