Skip to content

Commit eb9a1d9

Browse files
authored
Merge pull request #136 from TransformerOptimus/feat/bench-runner-tunable-policy
Bench-runner v0.1.7: expose LLM + tool policy knobs as CLI / env tunables
2 parents 6e1048e + 76a8a1f commit eb9a1d9

5 files changed

Lines changed: 173 additions & 42 deletions

File tree

crates/agent/src/tool/codebase_graph.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ use crate::context_engine::ContextEngineApi;
77
use crate::error::ToolError;
88
use super::{Tool, ToolContext, ToolResult};
99

10-
/// Max rows rendered per graph section (callers / deps / related) under bench policy.
11-
const GRAPH_SECTION_CAP: usize = 50;
12-
1310
pub struct CodebaseGraphTool {
1411
context_engine: Arc<dyn ContextEngineApi>,
1512
}
@@ -77,12 +74,9 @@ impl Tool for CodebaseGraphTool {
7774
}
7875

7976
// Bench policy: cap each rendered section so a high-degree node doesn't dump
80-
// hundreds of rows into the context. No-op (unbounded) under the app policy.
81-
let section_cap = if ctx.policy.codebase_result_caps {
82-
GRAPH_SECTION_CAP
83-
} else {
84-
usize::MAX
85-
};
77+
// hundreds of rows into the context. No-op (unbounded) under the app policy
78+
// when `codebase_graph_section_cap` is None.
79+
let section_cap = ctx.policy.codebase_graph_section_cap.unwrap_or(usize::MAX);
8680

8781
log::info!(
8882
"[codebase_graph] query={:?}, function_name={:?}, file_path={:?}, query_type={:?}",

crates/agent/src/tool/codebase_search.rs

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,13 @@ use crate::context_engine::{ContextEngineApi, SearchResultItem};
1010
use crate::error::ToolError;
1111
use super::{Tool, ToolContext, ToolResult};
1212

13-
/// Default result count when the model omits `limit` (bench policy only).
14-
const DEFAULT_SEARCH_LIMIT: u32 = 20;
15-
/// Max bytes of chunk content rendered per result (bench policy only).
16-
const MAX_CHUNK_BYTES: usize = 2048;
17-
18-
/// Truncate a chunk's content to `MAX_CHUNK_BYTES` on a UTF-8 boundary, appending a
13+
/// Truncate a chunk's content to `max_bytes` on a UTF-8 boundary, appending a
1914
/// marker when clipped. Keeps oversized index payloads from ballooning the context.
20-
fn cap_chunk_content(content: &str) -> String {
21-
if content.len() <= MAX_CHUNK_BYTES {
15+
fn cap_chunk_content(content: &str, max_bytes: usize) -> String {
16+
if content.len() <= max_bytes {
2217
return content.to_string();
2318
}
24-
let mut end = MAX_CHUNK_BYTES;
19+
let mut end = max_bytes;
2520
while !content.is_char_boundary(end) {
2621
end -= 1;
2722
}
@@ -89,12 +84,15 @@ impl Tool for CodebaseSearchTool {
8984
let strategy = args.get("strategy").and_then(|v| v.as_str());
9085
let mut limit = args.get("limit").and_then(|v| v.as_u64().map(|n| n as u32));
9186

92-
// Bench policy: when the model omits `limit`, default to 20 so the engine
93-
// doesn't return a huge result set that balloons the conversation. The model
94-
// can still page by raising the existing `limit` param. No-op under app policy.
95-
let caps = ctx.policy.codebase_result_caps;
96-
if caps && limit.is_none() {
97-
limit = Some(DEFAULT_SEARCH_LIMIT);
87+
// Bench policy: when the model omits `limit`, default to the policy value so
88+
// the engine doesn't return a huge result set that balloons the conversation.
89+
// The model can still page by raising the existing `limit` param. No-op under
90+
// app policy (codebase_search_limit = None).
91+
let policy_limit = ctx.policy.codebase_search_limit;
92+
if let Some(l) = policy_limit {
93+
if limit.is_none() {
94+
limit = Some(l);
95+
}
9896
}
9997

10098
log::info!(
@@ -158,12 +156,12 @@ impl Tool for CodebaseSearchTool {
158156
// Bench policy: cap result count (defensive — the engine may ignore `limit`)
159157
// and cap each chunk's content so oversized payloads don't inflate every
160158
// subsequent turn's context. No-op under the default (app) policy.
161-
if caps {
162-
if let Some(l) = limit {
163-
results.truncate(l as usize);
164-
}
159+
if let Some(l) = policy_limit {
160+
results.truncate(l as usize);
161+
}
162+
if let Some(max_bytes) = ctx.policy.codebase_search_chunk_bytes {
165163
for item in results.iter_mut() {
166-
item.content = cap_chunk_content(&item.content);
164+
item.content = cap_chunk_content(&item.content, max_bytes);
167165
}
168166
}
169167

crates/agent/src/tool/mod.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,16 @@ pub struct ToolPolicy {
9797
pub search_timeout_ms: Option<u64>,
9898
/// Skip the default ignore-dir list in grep/glob. `false` = no extra ignores (app default).
9999
pub search_default_ignores: bool,
100-
/// Apply codebase_search/graph result + content caps. `false` = no caps (app default).
101-
pub codebase_result_caps: bool,
100+
/// Default `limit` for codebase_search when the model omits it; the same value
101+
/// also bounds the final result count. `None` = pass through, no default,
102+
/// no truncation (app default).
103+
pub codebase_search_limit: Option<u32>,
104+
/// Cap per-chunk content bytes in codebase_search results (UTF-8 boundary +
105+
/// `…[truncated]` marker). `None` = no cap (app default).
106+
pub codebase_search_chunk_bytes: Option<usize>,
107+
/// Cap rows rendered per codebase_graph section (callers / deps / related);
108+
/// truncated sections append `… and N more`. `None` = no cap (app default).
109+
pub codebase_graph_section_cap: Option<usize>,
102110
}
103111

104112
impl Default for ToolPolicy {
@@ -108,20 +116,25 @@ impl Default for ToolPolicy {
108116
bash_timeout_ceiling_ms: None,
109117
search_timeout_ms: None,
110118
search_default_ignores: false,
111-
codebase_result_caps: false,
119+
codebase_search_limit: None,
120+
codebase_search_chunk_bytes: None,
121+
codebase_graph_section_cap: None,
112122
}
113123
}
114124
}
115125

116126
impl ToolPolicy {
117127
/// Strict policy for the eval harness: bash clamped to 300s, grep/glob walled at
118-
/// 60s and skipping build/vendor dirs, codebase_* results capped.
128+
/// 60s and skipping build/vendor dirs, codebase_search ≤20 results × ≤2KB/chunk,
129+
/// codebase_graph ≤50 rows per section.
119130
pub fn bench() -> Self {
120131
Self {
121132
bash_timeout_ceiling_ms: Some(300_000),
122133
search_timeout_ms: Some(60_000),
123134
search_default_ignores: true,
124-
codebase_result_caps: true,
135+
codebase_search_limit: Some(20),
136+
codebase_search_chunk_bytes: Some(2048),
137+
codebase_graph_section_cap: Some(50),
125138
}
126139
}
127140
}

crates/bench-runner/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ git-ops = { path = "../git-ops" }
1313
tokio = { version = "1", features = ["full"] }
1414
serde = { version = "1", features = ["derive"] }
1515
serde_json = "1"
16-
clap = { version = "4", features = ["derive"] }
16+
clap = { version = "4", features = ["derive", "env"] }

crates/bench-runner/src/main.rs

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,43 @@ struct Cli {
6868
ce_machine_id: String,
6969
#[arg(long, default_value = "")]
7070
ce_auth_token: String,
71+
72+
// ──────────────── LLM transport / retry tunables ────────────────
73+
// Defaults match the v0.1.6/v0.1.7 frozen-policy values exactly; overrides are
74+
// for tuning the bench binary without rebuilding (e.g. raising the header
75+
// timeout for slow Kimi cold starts). Each accepts a CLI flag or env var.
76+
/// Header-arrival timeout (ms). 0 disables. Default 15000.
77+
#[arg(long, env = "SUPERCODER_LLM_HEADER_TIMEOUT_MS", default_value_t = 15_000)]
78+
llm_header_timeout_ms: u64,
79+
/// Max LLM retry attempts after the initial call. Default 3.
80+
#[arg(long, env = "SUPERCODER_LLM_MAX_RETRIES", default_value_t = 3)]
81+
llm_max_retries: u32,
82+
/// Initial backoff between LLM retries (ms). Default 1000.
83+
#[arg(long, env = "SUPERCODER_LLM_RETRY_INITIAL_MS", default_value_t = 1_000)]
84+
llm_retry_initial_ms: u64,
85+
/// Backoff multiplier between LLM retries. Default 2.0.
86+
#[arg(long, env = "SUPERCODER_LLM_RETRY_MULTIPLIER", default_value_t = 2.0)]
87+
llm_retry_multiplier: f64,
88+
/// Backoff cap between LLM retries (ms). Default 30000.
89+
#[arg(long, env = "SUPERCODER_LLM_RETRY_MAX_MS", default_value_t = 30_000)]
90+
llm_retry_max_ms: u64,
91+
92+
// ──────────────── Tool tunables (bench ToolPolicy overrides) ────
93+
/// Bash timeout ceiling (ms). Default 300000 (5 min).
94+
#[arg(long, env = "SUPERCODER_BASH_TIMEOUT_MS", default_value_t = 300_000)]
95+
bash_timeout_ms: u64,
96+
/// grep/glob wall timeout (ms). Default 60000.
97+
#[arg(long, env = "SUPERCODER_SEARCH_TIMEOUT_MS", default_value_t = 60_000)]
98+
search_timeout_ms: u64,
99+
/// Default limit for codebase_search results when the model omits `limit`. Default 20.
100+
#[arg(long, env = "SUPERCODER_CODEBASE_SEARCH_LIMIT", default_value_t = 20)]
101+
codebase_search_limit: u32,
102+
/// Per-chunk content cap for codebase_search results (bytes). Default 2048.
103+
#[arg(long, env = "SUPERCODER_CODEBASE_SEARCH_CHUNK_BYTES", default_value_t = 2048)]
104+
codebase_search_chunk_bytes: usize,
105+
/// Per-section render cap for codebase_graph. Default 50.
106+
#[arg(long, env = "SUPERCODER_CODEBASE_GRAPH_SECTION_CAP", default_value_t = 50)]
107+
codebase_graph_section_cap: usize,
71108
}
72109

73110
#[derive(Copy, Clone, Debug, ValueEnum)]
@@ -202,19 +239,50 @@ async fn run(cli: &Cli, spec: &TaskSpec) -> RunResult {
202239
thinking: None,
203240
disable_cache_control: false,
204241
// Strict LLM transport policy: HTTP/1.1, no connection pooling, 15s
205-
// header-arrival timeout. Matches opencode's working transport shape (which
206-
// has zero decode deaths on the same router that gave bench-runner 26 on
207-
// long Kimi runs). Part of the frozen harness identity. See LlmPolicy::bench().
208-
policy: agent::llm::LlmPolicy::bench(),
242+
// header-arrival timeout (CLI/env-tunable). Matches opencode's working
243+
// transport shape (which has zero decode deaths on the same router that gave
244+
// bench-runner 26 on long Kimi runs). http1_only + no_pool stay hardcoded
245+
// (they ARE the transport fix); the header timeout is exposed since slow
246+
// upstreams may legitimately exceed it.
247+
policy: {
248+
let mut p = agent::llm::LlmPolicy::bench();
249+
p.header_timeout_ms = if cli.llm_header_timeout_ms == 0 {
250+
None
251+
} else {
252+
Some(cli.llm_header_timeout_ms)
253+
};
254+
p
255+
},
209256
};
210257

211258
let mut config = AgentConfig::new(llm, spec.working_dir.clone());
212259
config.mode = ToolMode::Coding;
213260
config.max_iterations = spec.max_iterations;
261+
262+
// Tune LLM retry behavior (CLI/env). Defaults match RetryConfig::default() (3
263+
// retries, 1s→2s→4s, cap 30s) — set --llm-retry-multiplier higher and
264+
// --llm-retry-max-ms wider when the upstream needs longer recovery windows
265+
// (e.g. proxy circuit-breaker open).
266+
config.retry_config = agent::agent::config::RetryConfig {
267+
max_retries: cli.llm_max_retries,
268+
initial_delay: std::time::Duration::from_millis(cli.llm_retry_initial_ms),
269+
multiplier: cli.llm_retry_multiplier,
270+
max_delay: std::time::Duration::from_millis(cli.llm_retry_max_ms),
271+
};
272+
214273
// Strict tool policy is part of the frozen harness identity: bash timeout ceiling,
215-
// grep/glob ignore-list + wall timeout, codebase_* result caps. Always on (the app
216-
// keeps the permissive default). See ToolPolicy::bench().
217-
config.tool_policy = agent::tool::ToolPolicy::bench();
274+
// grep/glob ignore-list + wall timeout, codebase_* result caps. The per-knob
275+
// values are CLI/env-tunable; defaults match ToolPolicy::bench() exactly. The
276+
// `search_default_ignores` flag stays hardcoded (it's the ignore-list behavior,
277+
// not a tuning value).
278+
config.tool_policy = agent::tool::ToolPolicy {
279+
bash_timeout_ceiling_ms: Some(cli.bash_timeout_ms),
280+
search_timeout_ms: Some(cli.search_timeout_ms),
281+
search_default_ignores: true,
282+
codebase_search_limit: Some(cli.codebase_search_limit),
283+
codebase_search_chunk_bytes: Some(cli.codebase_search_chunk_bytes),
284+
codebase_graph_section_cap: Some(cli.codebase_graph_section_cap),
285+
};
218286
// Everything else (system_prompt, checkpoint_dir, skills, subagents, …) stays at the
219287
// AgentConfig::new defaults of None — headless.
220288

@@ -404,3 +472,61 @@ async fn extract_patch(working_dir: &PathBuf, base_commit: &str) -> Result<Strin
404472
.map_err(|e| e.to_string())?;
405473
Ok(diff.stdout)
406474
}
475+
476+
#[cfg(test)]
477+
mod tests {
478+
use super::Cli;
479+
use clap::Parser;
480+
481+
/// Minimum args required to parse — model is the only required field.
482+
fn parse(args: &[&str]) -> Cli {
483+
let mut full = vec!["bench-runner", "--model", "test"];
484+
full.extend_from_slice(args);
485+
Cli::try_parse_from(full).expect("parse")
486+
}
487+
488+
#[test]
489+
fn cli_defaults_match_frozen_policy() {
490+
// Defaults must NOT change behavior vs the hardcoded v0.1.6 LlmPolicy::bench()
491+
// and ToolPolicy::bench() values. If this test fails the eval team's previous
492+
// runs are not reproducible.
493+
let cli = parse(&[]);
494+
assert_eq!(cli.llm_header_timeout_ms, 15_000);
495+
assert_eq!(cli.llm_max_retries, 3);
496+
assert_eq!(cli.llm_retry_initial_ms, 1_000);
497+
assert_eq!(cli.llm_retry_multiplier, 2.0);
498+
assert_eq!(cli.llm_retry_max_ms, 30_000);
499+
assert_eq!(cli.bash_timeout_ms, 300_000);
500+
assert_eq!(cli.search_timeout_ms, 60_000);
501+
assert_eq!(cli.codebase_search_limit, 20);
502+
assert_eq!(cli.codebase_search_chunk_bytes, 2048);
503+
assert_eq!(cli.codebase_graph_section_cap, 50);
504+
}
505+
506+
#[test]
507+
fn cli_flag_overrides() {
508+
let cli = parse(&[
509+
"--llm-header-timeout-ms", "90000",
510+
"--llm-retry-multiplier", "3.0",
511+
"--llm-retry-max-ms", "60000",
512+
"--bash-timeout-ms", "600000",
513+
"--codebase-search-limit", "10",
514+
]);
515+
assert_eq!(cli.llm_header_timeout_ms, 90_000);
516+
assert_eq!(cli.llm_retry_multiplier, 3.0);
517+
assert_eq!(cli.llm_retry_max_ms, 60_000);
518+
assert_eq!(cli.bash_timeout_ms, 600_000);
519+
assert_eq!(cli.codebase_search_limit, 10);
520+
// Untouched flags keep their defaults
521+
assert_eq!(cli.llm_max_retries, 3);
522+
assert_eq!(cli.codebase_search_chunk_bytes, 2048);
523+
}
524+
525+
#[test]
526+
fn header_timeout_zero_means_disabled() {
527+
// Per the CLI doc: 0 disables the header timeout entirely. The run path
528+
// converts this to LlmPolicy::header_timeout_ms = None.
529+
let cli = parse(&["--llm-header-timeout-ms", "0"]);
530+
assert_eq!(cli.llm_header_timeout_ms, 0);
531+
}
532+
}

0 commit comments

Comments
 (0)