1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Centralized tool output caps, scaled to the model's context window.
//!
//! All tool output limits live here instead of being scattered across 6+ files.
//! Caps scale linearly from a floor (current defaults) up to a 4× ceiling
//! as the context window grows.
//!
//! Scaling formula:
//! `clamp(base × (ctx / BASELINE), base, base × MAX_SCALE)`
//!
//! | Context window | Scale factor | Effect |
//! |----------------|-------------|-------------------|
//! | 4K | 0.04× | Floor (base) |
//! | 100K | 1.0× | Base (current) |
//! | 200K | 2.0× | 2× current |
//! | 1M | 10.0× | Ceiling (4× base) |
/// Baseline context window for scaling (100K tokens = 1.0× factor).
const BASELINE: f64 = 100_000.0;
/// Maximum scale multiplier (4× base values).
const MAX_SCALE: f64 = 4.0;
/// Pre-computed output caps for all tools in a session.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutputCaps {
/// Max chars for tool results stored in conversation history.
/// Base: 10,000 (was `MAX_TOOL_RESULT_CHARS` in `tool_dispatch.rs`).
pub tool_result_chars: usize,
/// Max chars for web page body content.
/// Base: 15,000 (was `MAX_BODY_CHARS` in `web_fetch.rs`).
pub web_body_chars: usize,
/// Max lines of shell command output.
/// Base: 256 (was `MAX_OUTPUT_LINES` in `shell.rs`).
pub shell_output_lines: usize,
/// Max grep matches returned.
/// Base: 100 (was `MAX_MATCHES` in `grep.rs`).
pub grep_matches: usize,
/// Max directory listing entries.
/// Base: 200 (was `MAX_ENTRIES` in `file_tools.rs`).
pub list_entries: usize,
/// Max glob search results.
/// Base: 200 (was `MAX_RESULTS` in `glob_tool.rs`).
pub glob_results: usize,
}
impl OutputCaps {
// ── Base values (floors) ─────────────────────────────────
const BASE_TOOL_RESULT_CHARS: usize = 10_000;
const BASE_WEB_BODY_CHARS: usize = 15_000;
const BASE_SHELL_OUTPUT_LINES: usize = 256;
const BASE_GREP_MATCHES: usize = 100;
const BASE_LIST_ENTRIES: usize = 200;
const BASE_GLOB_RESULTS: usize = 200;
/// Compute caps scaled to the given context window size (in tokens).
///
/// ```
/// use koda_core::output_caps::OutputCaps;
///
/// // 100K context = 1× (baseline)
/// let caps = OutputCaps::for_context(100_000);
/// assert_eq!(caps.grep_matches, 100);
///
/// // 200K context = 2×
/// let caps = OutputCaps::for_context(200_000);
/// assert_eq!(caps.grep_matches, 200);
/// ```
pub fn for_context(max_context_tokens: usize) -> Self {
let factor = (max_context_tokens as f64 / BASELINE).clamp(1.0, MAX_SCALE);
Self {
tool_result_chars: scale(Self::BASE_TOOL_RESULT_CHARS, factor),
web_body_chars: scale(Self::BASE_WEB_BODY_CHARS, factor),
shell_output_lines: scale(Self::BASE_SHELL_OUTPUT_LINES, factor),
grep_matches: scale(Self::BASE_GREP_MATCHES, factor),
list_entries: scale(Self::BASE_LIST_ENTRIES, factor),
glob_results: scale(Self::BASE_GLOB_RESULTS, factor),
}
}
}
impl Default for OutputCaps {
/// Default caps (100K context baseline — matches legacy hardcoded values).
fn default() -> Self {
Self::for_context(100_000)
}
}
/// Scale a base value by factor, rounding to nearest integer.
fn scale(base: usize, factor: f64) -> usize {
(base as f64 * factor).round() as usize
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn small_context_gets_base_values() {
let caps = OutputCaps::for_context(4_096);
assert_eq!(caps.tool_result_chars, OutputCaps::BASE_TOOL_RESULT_CHARS);
assert_eq!(caps.shell_output_lines, OutputCaps::BASE_SHELL_OUTPUT_LINES);
assert_eq!(caps.grep_matches, OutputCaps::BASE_GREP_MATCHES);
assert_eq!(caps.list_entries, OutputCaps::BASE_LIST_ENTRIES);
}
#[test]
fn baseline_context_gets_base_values() {
let caps = OutputCaps::for_context(100_000);
assert_eq!(caps.tool_result_chars, 10_000);
assert_eq!(caps.web_body_chars, 15_000);
assert_eq!(caps.shell_output_lines, 256);
assert_eq!(caps.grep_matches, 100);
assert_eq!(caps.list_entries, 200);
assert_eq!(caps.glob_results, 200);
}
#[test]
fn double_context_doubles_caps() {
let caps = OutputCaps::for_context(200_000);
assert_eq!(caps.tool_result_chars, 20_000);
assert_eq!(caps.web_body_chars, 30_000);
assert_eq!(caps.shell_output_lines, 512);
assert_eq!(caps.grep_matches, 200);
assert_eq!(caps.list_entries, 400);
assert_eq!(caps.glob_results, 400);
}
#[test]
fn million_context_caps_at_4x() {
let caps = OutputCaps::for_context(1_000_000);
assert_eq!(caps.tool_result_chars, 40_000);
assert_eq!(caps.web_body_chars, 60_000);
assert_eq!(caps.shell_output_lines, 1024);
assert_eq!(caps.grep_matches, 400);
assert_eq!(caps.list_entries, 800);
assert_eq!(caps.glob_results, 800);
}
#[test]
fn default_matches_baseline() {
assert_eq!(OutputCaps::default(), OutputCaps::for_context(100_000));
}
#[test]
fn intermediate_context_scales_linearly() {
let caps = OutputCaps::for_context(150_000);
// 1.5× base
assert_eq!(caps.tool_result_chars, 15_000);
assert_eq!(caps.shell_output_lines, 384);
}
}