Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export const globalSettingsSchema = z.object({
maxWorkspaceFiles: z.number().optional(),
showRooIgnoredFiles: z.boolean().optional(),
enableSubfolderRules: z.boolean().optional(),
maxReadFileLine: z.number().optional(),
maxImageFileSize: z.number().optional(),
maxTotalImageSize: z.number().optional(),

Expand Down
147 changes: 39 additions & 108 deletions src/core/prompts/tools/native-tools/__tests__/read_file.spec.ts
Original file line number Diff line number Diff line change
@@ -1,122 +1,53 @@
import type OpenAI from "openai"
import { createReadFileTool } from "../read_file"

// Helper type to access function tools
type FunctionTool = OpenAI.Chat.ChatCompletionTool & { type: "function" }

// Helper to get function definition from tool
const getFunctionDef = (tool: OpenAI.Chat.ChatCompletionTool) => (tool as FunctionTool).function
import { createReadFileTool, DEFAULT_LINE_LIMIT } from "../read_file"

describe("createReadFileTool", () => {
describe("single-file-per-call documentation", () => {
it("should indicate single-file-per-call and suggest parallel tool calls", () => {
const tool = createReadFileTool()
const description = getFunctionDef(tool).description

expect(description).toContain("exactly one file per call")
expect(description).toContain("multiple parallel read_file calls")
})
function getToolDesc(options = {}): string {
const tool = createReadFileTool(options) as any
return tool.function.description ?? ""
}

function getToolLimitParamDesc(options = {}): string {
const tool = createReadFileTool(options) as any
return tool.function.parameters.properties.limit.description ?? ""
}

it("uses DEFAULT_LINE_LIMIT in description when maxReadFileLine is undefined", () => {
const desc = getToolDesc()
expect(desc).toContain(`returns up to ${DEFAULT_LINE_LIMIT} lines per file`)
expect(desc).not.toContain("no line limit")
})

describe("indentation mode", () => {
it("should always include indentation mode in description", () => {
const tool = createReadFileTool()
const description = getFunctionDef(tool).description

expect(description).toContain("indentation")
})

it("should always include indentation parameter in schema", () => {
const tool = createReadFileTool()
const schema = getFunctionDef(tool).parameters as any

expect(schema.properties).toHaveProperty("indentation")
})

it("should include mode parameter in schema", () => {
const tool = createReadFileTool()
const schema = getFunctionDef(tool).parameters as any

expect(schema.properties).toHaveProperty("mode")
expect(schema.properties.mode.enum).toContain("slice")
expect(schema.properties.mode.enum).toContain("indentation")
})

it("should include offset and limit parameters in schema", () => {
const tool = createReadFileTool()
const schema = getFunctionDef(tool).parameters as any

expect(schema.properties).toHaveProperty("offset")
expect(schema.properties).toHaveProperty("limit")
})
it("uses DEFAULT_LINE_LIMIT in description when maxReadFileLine is -1", () => {
const desc = getToolDesc({ maxReadFileLine: -1 })
expect(desc).toContain(`returns up to ${DEFAULT_LINE_LIMIT} lines per file`)
})

describe("supportsImages option", () => {
it("should include image format documentation when supportsImages is true", () => {
const tool = createReadFileTool({ supportsImages: true })
const description = getFunctionDef(tool).description

expect(description).toContain(
"Automatically processes and returns image files (PNG, JPG, JPEG, GIF, BMP, SVG, WEBP, ICO, AVIF) for visual analysis",
)
})
it("indicates no limit in description when maxReadFileLine is 0", () => {
const desc = getToolDesc({ maxReadFileLine: 0 })
expect(desc).toContain("no line limit")
expect(desc).not.toContain(`returns up to ${DEFAULT_LINE_LIMIT}`)

it("should not include image format documentation when supportsImages is false", () => {
const tool = createReadFileTool({ supportsImages: false })
const description = getFunctionDef(tool).description

expect(description).not.toContain(
"Automatically processes and returns image files (PNG, JPG, JPEG, GIF, BMP, SVG, WEBP, ICO, AVIF) for visual analysis",
)
expect(description).toContain("may not handle other binary files properly")
})

it("should default supportsImages to false", () => {
const tool = createReadFileTool({})
const description = getFunctionDef(tool).description

expect(description).not.toContain(
"Automatically processes and returns image files (PNG, JPG, JPEG, GIF, BMP, SVG, WEBP, ICO, AVIF) for visual analysis",
)
})

it("should always include PDF and DOCX support in description", () => {
const toolWithImages = createReadFileTool({ supportsImages: true })
const toolWithoutImages = createReadFileTool({ supportsImages: false })

expect(getFunctionDef(toolWithImages).description).toContain(
"Supports text extraction from PDF and DOCX files",
)
expect(getFunctionDef(toolWithoutImages).description).toContain(
"Supports text extraction from PDF and DOCX files",
)
})
const limitDesc = getToolLimitParamDesc({ maxReadFileLine: 0 })
expect(limitDesc).toContain("no limit")
})

describe("tool structure", () => {
it("should have correct tool name", () => {
const tool = createReadFileTool()

expect(getFunctionDef(tool).name).toBe("read_file")
})
it("uses custom limit in description when maxReadFileLine is a positive number", () => {
const desc = getToolDesc({ maxReadFileLine: 500 })
expect(desc).toContain("returns up to 500 lines per file")
// Should not mention DEFAULT_LINE_LIMIT as the line limit (but MAX_LINE_LENGTH=2000 chars is ok)
expect(desc).not.toContain(`up to ${DEFAULT_LINE_LIMIT} lines`)

it("should be a function type tool", () => {
const tool = createReadFileTool()

expect(tool.type).toBe("function")
})

it("should have strict mode enabled", () => {
const tool = createReadFileTool()

expect(getFunctionDef(tool).strict).toBe(true)
})
const limitDesc = getToolLimitParamDesc({ maxReadFileLine: 500 })
expect(limitDesc).toContain("500")
})

it("should require path parameter", () => {
const tool = createReadFileTool()
const schema = getFunctionDef(tool).parameters as any
it("includes image support note when supportsImages is true", () => {
const desc = getToolDesc({ supportsImages: true })
expect(desc).toContain("image files")
})

expect(schema.required).toContain("path")
})
it("does not include image support note when supportsImages is false", () => {
const desc = getToolDesc({ supportsImages: false })
expect(desc).not.toContain("image files")
})
})
5 changes: 4 additions & 1 deletion src/core/prompts/tools/native-tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type { ReadFileToolOptions } from "./read_file"
export interface NativeToolsOptions {
/** Whether the model supports image processing (default: false) */
supportsImages?: boolean
/** Maximum line limit for read_file tool. -1 or undefined = DEFAULT_LINE_LIMIT, 0 = no limit */
maxReadFileLine?: number
}

/**
Expand All @@ -40,10 +42,11 @@ export interface NativeToolsOptions {
* @returns Array of native tool definitions
*/
export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.ChatCompletionTool[] {
const { supportsImages = false } = options
const { supportsImages = false, maxReadFileLine } = options

const readFileOptions: ReadFileToolOptions = {
supportsImages,
maxReadFileLine,
}

return [
Expand Down
26 changes: 23 additions & 3 deletions src/core/prompts/tools/native-tools/read_file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ function getReadFileSupportsNote(supportsImages: boolean): string {
export interface ReadFileToolOptions {
/** Whether the model supports image processing (default: false) */
supportsImages?: boolean
/** Maximum line limit for read_file tool. -1 or undefined = DEFAULT_LINE_LIMIT, 0 = no limit */
maxReadFileLine?: number
}

// ─── Schema Builder ───────────────────────────────────────────────────────────
Expand All @@ -58,7 +60,21 @@ export interface ReadFileToolOptions {
* @returns Native tool definition for read_file
*/
export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Chat.ChatCompletionTool {
const { supportsImages = false } = options
const { supportsImages = false, maxReadFileLine } = options

// Resolve the effective line limit for the tool description.
// -1 or undefined: use DEFAULT_LINE_LIMIT (2000)
// 0: no limit (read entire file)
// positive number: use that value
const hasNoLimit = maxReadFileLine === 0
const effectiveLimit =
maxReadFileLine === undefined || maxReadFileLine === -1
? DEFAULT_LINE_LIMIT
: maxReadFileLine === 0
? undefined // no limit
: maxReadFileLine > 0
? maxReadFileLine
: DEFAULT_LINE_LIMIT

// Build description based on capabilities
const descriptionIntro =
Expand All @@ -70,7 +86,9 @@ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Ch
` PREFER indentation mode when you have a specific line number from search results, error messages, or definition lookups - it guarantees complete, syntactically valid code blocks without mid-function truncation.` +
` IMPORTANT: Indentation mode requires anchor_line to be useful. Without it, only header content (imports) is returned.`

const limitNote = ` By default, returns up to ${DEFAULT_LINE_LIMIT} lines per file. Lines longer than ${MAX_LINE_LENGTH} characters are truncated.`
const limitNote = hasNoLimit
? ` Reads the entire file by default (no line limit). Lines longer than ${MAX_LINE_LENGTH} characters are truncated.`
: ` By default, returns up to ${effectiveLimit} lines per file. Lines longer than ${MAX_LINE_LENGTH} characters are truncated.`

const description =
descriptionIntro +
Expand Down Expand Up @@ -125,7 +143,9 @@ export function createReadFileTool(options: ReadFileToolOptions = {}): OpenAI.Ch
},
limit: {
type: "integer",
description: `Maximum number of lines to return (slice mode, default: ${DEFAULT_LINE_LIMIT})`,
description: hasNoLimit
? `Maximum number of lines to return (slice mode, default: no limit - reads entire file)`
: `Maximum number of lines to return (slice mode, default: ${effectiveLimit})`,
},
indentation: {
type: "object",
Expand Down
4 changes: 4 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1670,6 +1670,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
apiConfiguration,
disabledTools: state?.disabledTools,
modelInfo,
maxReadFileLine: state?.maxReadFileLine,
includeAllToolsWithRestrictions: false,
})
allTools = toolsResult.tools
Expand Down Expand Up @@ -3858,6 +3859,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
apiConfiguration,
disabledTools: state?.disabledTools,
modelInfo,
maxReadFileLine: state?.maxReadFileLine,
includeAllToolsWithRestrictions: false,
})
allTools = toolsResult.tools
Expand Down Expand Up @@ -4072,6 +4074,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
apiConfiguration,
disabledTools: state?.disabledTools,
modelInfo,
maxReadFileLine: state?.maxReadFileLine,
includeAllToolsWithRestrictions: false,
})
contextMgmtTools = toolsResult.tools
Expand Down Expand Up @@ -4236,6 +4239,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
apiConfiguration,
disabledTools: state?.disabledTools,
modelInfo,
maxReadFileLine: state?.maxReadFileLine,
includeAllToolsWithRestrictions: supportsAllowedFunctionNames,
})
allTools = toolsResult.tools
Expand Down
4 changes: 4 additions & 0 deletions src/core/task/build-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ interface BuildToolsOptions {
apiConfiguration: ProviderSettings | undefined
disabledTools?: string[]
modelInfo?: ModelInfo
/** Maximum line limit for read_file tool. -1 or undefined = DEFAULT_LINE_LIMIT, 0 = no limit */
maxReadFileLine?: number
/**
* If true, returns all tools without mode filtering, but also includes
* the list of allowed tool names for use with allowedFunctionNames.
Expand Down Expand Up @@ -89,6 +91,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
apiConfiguration,
disabledTools,
modelInfo,
maxReadFileLine,
includeAllToolsWithRestrictions,
} = options

Expand All @@ -111,6 +114,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
// Build native tools with dynamic read_file tool based on settings.
const nativeTools = getNativeTools({
supportsImages,
maxReadFileLine,
})

// Filter native tools based on mode restrictions.
Expand Down
Loading
Loading