1const BASELINE: f64 = 100_000.0;
19
20const MAX_SCALE: f64 = 4.0;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct OutputCaps {
26 pub tool_result_chars: usize,
29
30 pub web_body_chars: usize,
33
34 pub shell_output_lines: usize,
37
38 pub grep_matches: usize,
41
42 pub list_entries: usize,
45
46 pub glob_results: usize,
49}
50
51impl OutputCaps {
52 const BASE_TOOL_RESULT_CHARS: usize = 10_000;
54 const BASE_WEB_BODY_CHARS: usize = 15_000;
55 const BASE_SHELL_OUTPUT_LINES: usize = 256;
56 const BASE_GREP_MATCHES: usize = 100;
57 const BASE_LIST_ENTRIES: usize = 200;
58 const BASE_GLOB_RESULTS: usize = 200;
59
60 pub fn for_context(max_context_tokens: usize) -> Self {
74 let factor = (max_context_tokens as f64 / BASELINE).clamp(1.0, MAX_SCALE);
75
76 Self {
77 tool_result_chars: scale(Self::BASE_TOOL_RESULT_CHARS, factor),
78 web_body_chars: scale(Self::BASE_WEB_BODY_CHARS, factor),
79 shell_output_lines: scale(Self::BASE_SHELL_OUTPUT_LINES, factor),
80 grep_matches: scale(Self::BASE_GREP_MATCHES, factor),
81 list_entries: scale(Self::BASE_LIST_ENTRIES, factor),
82 glob_results: scale(Self::BASE_GLOB_RESULTS, factor),
83 }
84 }
85}
86
87impl Default for OutputCaps {
88 fn default() -> Self {
90 Self::for_context(100_000)
91 }
92}
93
94fn scale(base: usize, factor: f64) -> usize {
96 (base as f64 * factor).round() as usize
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn small_context_gets_base_values() {
105 let caps = OutputCaps::for_context(4_096);
106 assert_eq!(caps.tool_result_chars, OutputCaps::BASE_TOOL_RESULT_CHARS);
107 assert_eq!(caps.shell_output_lines, OutputCaps::BASE_SHELL_OUTPUT_LINES);
108 assert_eq!(caps.grep_matches, OutputCaps::BASE_GREP_MATCHES);
109 assert_eq!(caps.list_entries, OutputCaps::BASE_LIST_ENTRIES);
110 }
111
112 #[test]
113 fn baseline_context_gets_base_values() {
114 let caps = OutputCaps::for_context(100_000);
115 assert_eq!(caps.tool_result_chars, 10_000);
116 assert_eq!(caps.web_body_chars, 15_000);
117 assert_eq!(caps.shell_output_lines, 256);
118 assert_eq!(caps.grep_matches, 100);
119 assert_eq!(caps.list_entries, 200);
120 assert_eq!(caps.glob_results, 200);
121 }
122
123 #[test]
124 fn double_context_doubles_caps() {
125 let caps = OutputCaps::for_context(200_000);
126 assert_eq!(caps.tool_result_chars, 20_000);
127 assert_eq!(caps.web_body_chars, 30_000);
128 assert_eq!(caps.shell_output_lines, 512);
129 assert_eq!(caps.grep_matches, 200);
130 assert_eq!(caps.list_entries, 400);
131 assert_eq!(caps.glob_results, 400);
132 }
133
134 #[test]
135 fn million_context_caps_at_4x() {
136 let caps = OutputCaps::for_context(1_000_000);
137 assert_eq!(caps.tool_result_chars, 40_000);
138 assert_eq!(caps.web_body_chars, 60_000);
139 assert_eq!(caps.shell_output_lines, 1024);
140 assert_eq!(caps.grep_matches, 400);
141 assert_eq!(caps.list_entries, 800);
142 assert_eq!(caps.glob_results, 800);
143 }
144
145 #[test]
146 fn default_matches_baseline() {
147 assert_eq!(OutputCaps::default(), OutputCaps::for_context(100_000));
148 }
149
150 #[test]
151 fn intermediate_context_scales_linearly() {
152 let caps = OutputCaps::for_context(150_000);
153 assert_eq!(caps.tool_result_chars, 15_000);
155 assert_eq!(caps.shell_output_lines, 384);
156 }
157}