/ tools / FileReadTool / limits.ts
limits.ts
 1  /**
 2   * Read tool output limits.  Two caps apply to text reads:
 3   *
 4   *   | limit         | default | checks                    | cost          | on overflow     |
 5   *   |---------------|---------|---------------------------|---------------|-----------------|
 6   *   | maxSizeBytes  | 256 KB  | TOTAL FILE SIZE (not out) | 1 stat        | throws pre-read |
 7   *   | maxTokens     | 25000   | actual output tokens      | API roundtrip | throws post-read|
 8   *
 9   * Known mismatch: maxSizeBytes gates on total file size, not the slice.
10   * Tested truncating instead of throwing for explicit-limit reads that
11   * exceed the byte cap (#21841, Mar 2026).  Reverted: tool error rate
12   * dropped but mean tokens rose — the throw path yields a ~100-byte error
13   * tool-result while truncation yields ~25K tokens of content at the cap.
14   */
15  import memoize from 'lodash-es/memoize.js'
16  import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
17  import { MAX_OUTPUT_SIZE } from 'src/utils/file.js'
18  export const DEFAULT_MAX_OUTPUT_TOKENS = 25000
19  
20  /**
21   * Env var override for max output tokens. Returns undefined when unset/invalid
22   * so the caller can fall through to the next precedence tier.
23   */
24  function getEnvMaxTokens(): number | undefined {
25    const override = process.env.CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS
26    if (override) {
27      const parsed = parseInt(override, 10)
28      if (!isNaN(parsed) && parsed > 0) {
29        return parsed
30      }
31    }
32    return undefined
33  }
34  
35  export type FileReadingLimits = {
36    maxTokens: number
37    maxSizeBytes: number
38    includeMaxSizeInPrompt?: boolean
39    targetedRangeNudge?: boolean
40  }
41  
42  /**
43   * Default limits for Read tool when the ToolUseContext doesn't supply an
44   * override. Memoized so the GrowthBook value is fixed at first call — avoids
45   * the cap changing mid-session as the flag refreshes in the background.
46   *
47   * Precedence for maxTokens: env var > GrowthBook > DEFAULT_MAX_OUTPUT_TOKENS.
48   * (Env var is a user-set override, should beat experiment infrastructure.)
49   *
50   * Defensive: each field is individually validated; invalid values fall
51   * through to the hardcoded defaults (no route to cap=0).
52   */
53  export const getDefaultFileReadingLimits = memoize((): FileReadingLimits => {
54    const override =
55      getFeatureValue_CACHED_MAY_BE_STALE<Partial<FileReadingLimits> | null>(
56        'tengu_amber_wren',
57        {},
58      )
59  
60    const maxSizeBytes =
61      typeof override?.maxSizeBytes === 'number' &&
62      Number.isFinite(override.maxSizeBytes) &&
63      override.maxSizeBytes > 0
64        ? override.maxSizeBytes
65        : MAX_OUTPUT_SIZE
66  
67    const envMaxTokens = getEnvMaxTokens()
68    const maxTokens =
69      envMaxTokens ??
70      (typeof override?.maxTokens === 'number' &&
71      Number.isFinite(override.maxTokens) &&
72      override.maxTokens > 0
73        ? override.maxTokens
74        : DEFAULT_MAX_OUTPUT_TOKENS)
75  
76    const includeMaxSizeInPrompt =
77      typeof override?.includeMaxSizeInPrompt === 'boolean'
78        ? override.includeMaxSizeInPrompt
79        : undefined
80  
81    const targetedRangeNudge =
82      typeof override?.targetedRangeNudge === 'boolean'
83        ? override.targetedRangeNudge
84        : undefined
85  
86    return {
87      maxSizeBytes,
88      maxTokens,
89      includeMaxSizeInPrompt,
90      targetedRangeNudge,
91    }
92  })