Skip to content

Commit 76a8a1f

Browse files
committed
feat(bench-runner): tunable LLM + tool policy knobs via CLI / env
Add 10 CLI flags (each with SUPERCODER_<NAME> env fallback) for tuning the bench binary without rebuilding. Defaults match the frozen v0.1.6 LlmPolicy:: bench() and ToolPolicy::bench() values exactly — zero behavior change unless overridden. Required by the grid team after kimi-k2p6 hit the hardcoded 15s header timeout on legitimately slow upstream cold starts (20-60s TTFB). LLM transport / retry: --llm-header-timeout-ms (default 15000, 0 disables) --llm-max-retries (default 3) --llm-retry-initial-ms (default 1000) --llm-retry-multiplier (default 2.0) --llm-retry-max-ms (default 30000) Tool policy: --bash-timeout-ms (default 300000) --search-timeout-ms (default 60000) --codebase-search-limit (default 20) --codebase-search-chunk-bytes (default 2048) --codebase-graph-section-cap (default 50) http1_only and no_pool stay hardcoded — they ARE the v0.1.6 transport fix and toggling them risks bringing back the decode disease. For the kimi-k2p6 cells: SUPERCODER_LLM_HEADER_TIMEOUT_MS=90000 SUPERCODER_LLM_RETRY_MULTIPLIER=3 SUPERCODER_LLM_RETRY_MAX_MS=60000 clap 'env' feature added — pure activation of code already in clap; no new transitive deps (Cargo.lock unchanged). R1 invariant intact.
1 parent b632b47 commit 76a8a1f

2 files changed

Lines changed: 134 additions & 8 deletions

File tree

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)