Skip to content

Commit e5f73cd

Browse files
committed
fix(buzz-agent): carry BUZZ_AGENT_LLM_TIMEOUT_SECS onto goose
PR #6115 seats a 660 s client budget for mesh agents in the desktop (`managed_agents/relay_mesh.rs`) because MeshLLM's own backend budget is 600 s and a cold prefill of a large agent prompt can legitimately take ~500 s. On this branch that seed reached nothing: buzz-agent no longer has a request loop, and `project_goose_env` had no projection for the variable, so goose's own default applied instead. goose's default is 600 s (`DEFAULT_PROVIDER_TIMEOUT_SECS`) — just *under* the mesh server's, which is the same wrong-side-of-the-server ordering #6115 diagnosed. So the bug #6115 fixes would have come back on merge, silently and only on shared compute. goose reads the timeout per provider rather than globally, so this projects onto the variable belonging to the configured provider. `relay-mesh` maps to goose's `openai` provider and so reads `OPENAI_TIMEOUT` (`goose/src/providers/openai_def.rs:118`). Providers goose gives no timeout knob — databricks — return `None` rather than getting a variable goose never reads. Two tests. The mesh one asserts the invariant (`> 600`) rather than the literal, and I verified it fails with the projection removed and the test kept, reconstructing that baseline from the file rather than `git stash`. README corrected: it claimed this knob was "no longer read", and its limits table still listed the pre-goose 240 s default. Co-authored-by: Michael Neale <michael.neale@gmail.com> Signed-off-by: Michael Neale <michael.neale@gmail.com>
1 parent 9ca8806 commit e5f73cd

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

crates/buzz-agent/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ Everything is environment variables. No flags, no config files. (We are a subpro
157157
| `BUZZ_AGENT_MAX_TOKEN_RECOVERIES` | `3` | Retries after a successful response is truncated at the output-token limit. `0` disables recovery; the finite value and `BUZZ_AGENT_MAX_ROUNDS` prevent infinite retries. |
158158
| `BUZZ_AGENT_MAX_CONTEXT_TOKENS` | `200000` | Provider context window used by the handoff gate. |
159159
| `BUZZ_AGENT_MAX_HANDOFFS` || **No longer read.** goose's compaction replaced the handoff mechanism it bounded. |
160-
| `BUZZ_AGENT_LLM_TIMEOUT_SECS` | | **No longer read.** goose owns provider transport and its own per-provider timeouts (`OPENAI_TIMEOUT`, `ANTHROPIC_TIMEOUT`, default 600s). |
160+
| `BUZZ_AGENT_LLM_TIMEOUT_SECS` | goose's own (600 s) | Per-request timeout. goose owns provider transport and reads this **per provider**, so buzz projects the value onto the variable belonging to the configured provider — `OPENAI_TIMEOUT` (which covers `relay-mesh` and the other OpenAI-wire providers), `ANTHROPIC_TIMEOUT`, `OLLAMA_TIMEOUT`, `LITELLM_TIMEOUT`. Providers goose gives no timeout knob (databricks) cannot honour it. |
161161
| `BUZZ_AGENT_TOOL_TIMEOUT_SECS` | `660` | Per-tool call timeout in seconds. Projected onto goose's `GOOSE_DEFAULT_EXTENSION_TIMEOUT`. |
162162
| `BUZZ_AGENT_MAX_PARALLEL_TOOLS` || **No longer read.** All of a round's tool calls are dispatched concurrently; there is no cap to configure. |
163163
| `BUZZ_AGENT_MAX_SESSIONS` | unlimited | Max concurrent ACP sessions. Sessions are cheap; default has no cap. |
@@ -333,7 +333,7 @@ The trust boundary is **the operator who launched the agent**. The harness, MCP
333333
| Tool schema bytes | 4 KiB | `MAX_SCHEMA_BYTES` (oversize → replaced with `{}`) |
334334
| Tool calls per turn | 64 | `MAX_TOOL_CALLS_PER_TURN` |
335335
| Loop rounds | 0 (unlimited) | `BUZZ_AGENT_MAX_ROUNDS` |
336-
| LLM read inactivity timeout | 240 s | `BUZZ_AGENT_LLM_TIMEOUT_SECS` |
336+
| LLM request timeout | goose's own (600 s) | `BUZZ_AGENT_LLM_TIMEOUT_SECS` (projected per provider) |
337337
| Tool call timeout | 660 s | `BUZZ_AGENT_TOOL_TIMEOUT_SECS` |
338338

339339
## What This Is NOT

crates/buzz-agent/src/config.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,26 @@ fn goose_provider_name(provider: &str) -> &str {
122122
}
123123
}
124124

125+
/// goose's per-request timeout variable for a Buzz provider id, if it has one.
126+
///
127+
/// goose reads the timeout per provider rather than globally, so the name
128+
/// depends on which provider the agent is configured for. `relay-mesh` and the
129+
/// other OpenAI-wire providers all run through goose's `openai` provider and so
130+
/// read `OPENAI_TIMEOUT` (`goose/src/providers/openai_def.rs:118`).
131+
///
132+
/// Returns `None` for providers whose goose implementation has no timeout knob
133+
/// (databricks among them) — there the buzz value cannot be honoured, and
134+
/// inventing a variable goose never reads would be worse than not setting one.
135+
fn provider_timeout_env_key(provider: &str) -> Option<&'static str> {
136+
match goose_provider_name(provider) {
137+
"openai" => Some("OPENAI_TIMEOUT"),
138+
"anthropic" => Some("ANTHROPIC_TIMEOUT"),
139+
"ollama" => Some("OLLAMA_TIMEOUT"),
140+
"litellm" => Some("LITELLM_TIMEOUT"),
141+
_ => None,
142+
}
143+
}
144+
125145
fn env_str(key: &str) -> Option<String> {
126146
std::env::var(key).ok().filter(|s| !s.trim().is_empty())
127147
}
@@ -263,6 +283,27 @@ impl Config {
263283
.unwrap_or_else(|| DEFAULT_TOOL_RESULT_TEXT_BYTES.to_string()),
264284
);
265285

286+
// Per-LLM-call timeout. goose has no single provider-agnostic name for
287+
// this — each provider reads its own (`OPENAI_TIMEOUT`,
288+
// `ANTHROPIC_TIMEOUT`, ...) — so this projects onto the one belonging
289+
// to the provider actually in use.
290+
//
291+
// This is load-bearing for shared compute, not hygiene. The desktop
292+
// seeds `BUZZ_AGENT_LLM_TIMEOUT_SECS=660` for mesh agents
293+
// (`managed_agents/relay_mesh.rs`, PR #6115) because MeshLLM's own
294+
// backend budget is 600 s and a cold prefill of a large prompt can
295+
// legitimately take ~500 s. Without this projection that seed reaches
296+
// nothing on this branch: goose's default is 600 s, just under the
297+
// server's, so the client would abort a request the mesh is still
298+
// serving — the exact failure #6115 diagnosed and fixed.
299+
if let Some(timeout) = env_str("BUZZ_AGENT_LLM_TIMEOUT_SECS") {
300+
if let Some(key) =
301+
provider_timeout_env_key(&env_str("BUZZ_AGENT_PROVIDER").unwrap_or_default())
302+
{
303+
set_if_absent(key, &timeout);
304+
}
305+
}
306+
266307
// `BUZZ_AGENT_NO_HINTS=1` suppressed AGENTS.md/.goosehints loading.
267308
// goose has no boolean for this, but it takes the *filename list* —
268309
// an empty list finds nothing, which is the same outcome.
@@ -299,6 +340,69 @@ mod tests {
299340
// Env is process-global; these tests set disjoint keys and assert only on
300341
// the pure mapping helpers where possible.
301342

343+
/// The mesh client budget must outlast the mesh server's own.
344+
///
345+
/// The desktop seeds `BUZZ_AGENT_LLM_TIMEOUT_SECS=660` for mesh agents
346+
/// (PR #6115) against MeshLLM's 600 s backend budget. goose reads the
347+
/// timeout per provider, so without the projection under test the seed
348+
/// reaches nothing and goose's own 600 s default applies — under the
349+
/// server's, which is what made Buzz abandon prefills the mesh was still
350+
/// serving. Asserts the invariant (`> 600`) rather than the literal.
351+
#[test]
352+
fn mesh_llm_timeout_outlasts_the_mesh_backend_budget() {
353+
let _guard = env_lock();
354+
for key in [
355+
"BUZZ_AGENT_LLM_TIMEOUT_SECS",
356+
"BUZZ_AGENT_PROVIDER",
357+
"OPENAI_TIMEOUT",
358+
"GOOSE_PROVIDER",
359+
] {
360+
std::env::remove_var(key);
361+
}
362+
363+
std::env::set_var("BUZZ_AGENT_PROVIDER", "relay-mesh");
364+
std::env::set_var("BUZZ_AGENT_LLM_TIMEOUT_SECS", "660");
365+
Config::project_goose_env();
366+
367+
let seated: u64 = std::env::var("OPENAI_TIMEOUT")
368+
.expect("mesh agents must carry a timeout onto goose's openai provider")
369+
.parse()
370+
.expect("timeout must be numeric");
371+
assert!(
372+
seated > 600,
373+
"client budget {seated}s must outlast MeshLLM's 600s backend budget"
374+
);
375+
376+
for key in [
377+
"BUZZ_AGENT_LLM_TIMEOUT_SECS",
378+
"BUZZ_AGENT_PROVIDER",
379+
"OPENAI_TIMEOUT",
380+
"GOOSE_PROVIDER",
381+
] {
382+
std::env::remove_var(key);
383+
}
384+
}
385+
386+
/// A provider goose gives no timeout knob must not get a bogus one.
387+
#[test]
388+
fn provider_without_a_timeout_knob_projects_nothing() {
389+
assert_eq!(
390+
provider_timeout_env_key("relay-mesh"),
391+
Some("OPENAI_TIMEOUT")
392+
);
393+
assert_eq!(
394+
provider_timeout_env_key("openai-compat"),
395+
Some("OPENAI_TIMEOUT")
396+
);
397+
assert_eq!(
398+
provider_timeout_env_key("anthropic"),
399+
Some("ANTHROPIC_TIMEOUT")
400+
);
401+
// goose's databricks providers read no timeout variable; inventing one
402+
// would be a silent no-op dressed up as support.
403+
assert_eq!(provider_timeout_env_key("databricks-v2"), None);
404+
}
405+
302406
/// Every `BUZZ_AGENT_*` knob whose implementation moved to goose must
303407
/// still reach goose under its goose name. A knob that silently stops
304408
/// working is the regression this test exists to prevent.

0 commit comments

Comments
 (0)