Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c1397bf
fix(managed-agents): apply Claude Code agent config via ACP/env; resp…
Aug 4, 2026
5191832
fix: effort startup default via ACP + honor CLAUDE_CONFIG_DIR for .cl…
Aug 5, 2026
b5d63b9
fix: add effort_level to test Config initializers; apply rustfmt
Aug 5, 2026
02b541b
fix: Thufir review round — pool-level effort redesign, honest acks, r…
Aug 5, 2026
858d411
fix(buzz-acp): V-1/V-2/V-3 pool lifecycle seam fixes + tests
Aug 5, 2026
54ae346
fix(buzz-acp): close V-2 first-turn window — category trust in pre-di…
Aug 5, 2026
333362a
fix(buzz-acp): generation-based effort commit/rollback + nonce correl…
Aug 5, 2026
dbff640
fix(acp): close F-1/F-2/F-3 pre-Pass-3 blockers
Aug 5, 2026
ffe3d8d
chore: merge origin/main into duncan/claude-config-gaps
Aug 5, 2026
b7c99fb
chore: merge origin/main into duncan/claude-config-gaps (round 2)
Aug 6, 2026
552fcdd
fix(acp): close P2-1/P2-2/P2-3 and P3 B5 effort state-machine findings
Aug 6, 2026
595170b
chore: merge origin/main into duncan/claude-config-gaps (round 3)
Aug 6, 2026
1155310
fix(pool): make P2-3 first-terminal-wins gate cover pool state, not j…
Aug 6, 2026
959f12a
fix(acp): close Thufir Pass 1 blockers — F1/F2/F3 + folded minors
Aug 7, 2026
768b334
fix(pool): close return/report ordering race on Failed path
Aug 7, 2026
012c1c4
fix(runtime): make canonical effort win over user env, locally and re…
Aug 7, 2026
74b9173
Merge remote-tracking branch 'origin/main' into duncan/claude-config-…
Aug 7, 2026
90d7e0d
fix(pool): tri-state effort capability model survives live model swit…
Aug 10, 2026
3cef6cd
Merge remote-tracking branch 'origin/main' into duncan/claude-config-…
Aug 10, 2026
756ed48
fix(pool): reconcile main's ACP session-context tests with effort fields
Aug 10, 2026
4fbbb7d
fix(pool): consume post-switch capability snapshot; settle unsupporte…
Aug 10, 2026
30f302c
fix(acp): close Thufir pass-2 findings — busy-switch Unknown and Reje…
Aug 10, 2026
1b07e41
test(acp): drive busy-switch Unknown regression through production ru…
Aug 11, 2026
679937f
fix(desktop): surface adapter-refused live model switch as failure
Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions crates/buzz-acp/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2115,13 +2115,60 @@ pub fn extract_model_config_options(result: &serde_json::Value) -> Vec<serde_jso
.unwrap_or_default()
}

/// Extract `configOptions` entries relevant to agent capability tracking from a
/// `session/new` result.
///
/// Returns entries with `category == "model"` (for model catalog/validation, existing
/// callers) **and** `category == "thought_level"` (so the pool-level capability cache
/// can populate `valid_values` and the harness `invalid_value` guard runs in production).
///
/// Stored in `AgentModelCapabilities::config_options_raw`; `extract_model_config_options`
/// is kept as a separate helper for callers that only need the model category.
pub fn extract_agent_config_options(result: &serde_json::Value) -> Vec<serde_json::Value> {
result["configOptions"]
.as_array()
.map(|arr| {
arr.iter()
.filter(|opt| {
matches!(
opt.get("category").and_then(|c| c.as_str()),
Some("model") | Some("thought_level")
)
})
.cloned()
.collect()
})
.unwrap_or_default()
}

/// Extract `SessionModelState` (unstable path) from a `session/new` result.
///
/// Returns the `models` object if present: `{ currentModelId, availableModels: [...] }`.
pub fn extract_model_state(result: &serde_json::Value) -> Option<serde_json::Value> {
result.get("models").cloned()
}

/// B5: Extract the `configId` for the `thought_level` category option from a
/// `session/new` result, if the adapter advertised one.
///
/// Claude Code's adapter uses `category: "thought_level"` in its configOptions.
/// The configId is adapter-defined (e.g. `"effort"` on claude-agent-acp) and
/// must not be hardcoded in the harness — this function discovers it at session
/// time so `set_idle_agent_effort` can forward the real id.
pub fn extract_thought_level_config_id(result: &serde_json::Value) -> Option<String> {
let arr = result["configOptions"].as_array()?;
for opt in arr {
if opt.get("category").and_then(|c| c.as_str()) == Some("thought_level") {
let config_id = opt
.get("configId")
.or_else(|| opt.get("id"))
.and_then(|v| v.as_str())?;
return Some(config_id.to_string());
}
}
None
}

/// Match a desired model ID against a fresh `session/new` response.
///
/// Returns the correct ACP method to call, or `None` if no match.
Expand Down
35 changes: 35 additions & 0 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ pub enum PermissionMode {
/// Agent default — permission requests per tool call.
#[value(alias = "default")]
Default,
/// Auto mode — fully autonomous execution; model-gated (requires a model
/// that supports `supportsAutoMode`). Degrades gracefully to `default`
/// when the session's active model does not support it.
#[value(alias = "auto")]
Auto,
/// Auto-approve file edits, still ask for other tools.
#[value(alias = "acceptEdits")]
AcceptEdits,
Expand All @@ -144,6 +149,7 @@ impl PermissionMode {
pub fn as_wire_str(&self) -> &'static str {
match self {
Self::Default => "default",
Self::Auto => "auto",
Self::AcceptEdits => "acceptEdits",
Self::BypassPermissions => "bypassPermissions",
Self::DontAsk => "dontAsk",
Expand Down Expand Up @@ -423,6 +429,14 @@ pub struct CliArgs {
#[arg(long, env = "BUZZ_ACP_MODEL")]
pub model: Option<String>,

/// Persisted effort level value (e.g. "high", "medium", "low") to apply via
/// `session/set_config_option` at the first session creation. The configId is
/// resolved from the adapter's advertised `thought_level` capability — not
/// hardcoded. Non-fatal: if the adapter does not advertise `thought_level`,
/// the value is silently ignored and the persisted effort is not overwritten.
#[arg(long, env = "BUZZ_ACP_EFFORT_LEVEL")]
pub effort_level: Option<String>,

/// Title for the agent's ACP sessions, passed out-of-band in `session/new`
/// `_meta`. Adapters that recognize it name the session after this value;
/// others ignore it. Never enters the prompt.
Expand Down Expand Up @@ -533,6 +547,11 @@ pub struct Config {
pub memory_enabled: bool,
/// Desired LLM model ID. Applied after every `session_new_full()`.
pub model: Option<String>,
/// Persisted effort level value (e.g. "high", "medium", "low"). Resolved into a
/// real `desired_effort` at the first session creation by pairing with the
/// adapter's advertised `thought_level` configId. Non-fatal when absent or
/// when the adapter does not advertise `thought_level`.
pub effort_level: Option<String>,
/// Sanitized session title, sent as `_meta.sessionTitle` on `session/new`.
/// `None` when unset or when the configured value sanitized to empty.
pub session_title: Option<String>,
Expand Down Expand Up @@ -1094,6 +1113,7 @@ impl Config {
typing_enabled: !args.no_typing,
memory_enabled: args.memory && !args.no_memory,
model,
effort_level: args.effort_level,
session_title: args
.session_title
.as_deref()
Expand Down Expand Up @@ -1481,6 +1501,7 @@ mod tests {
agent_owner: None,
no_base_prompt: false,
base_prompt_content: None,
effort_level: None,
}
}

Expand Down Expand Up @@ -2269,6 +2290,7 @@ channels = "ALL"
#[test]
fn test_permission_mode_wire_strings() {
assert_eq!(PermissionMode::Default.as_wire_str(), "default");
assert_eq!(PermissionMode::Auto.as_wire_str(), "auto");
assert_eq!(PermissionMode::AcceptEdits.as_wire_str(), "acceptEdits");
assert_eq!(
PermissionMode::BypassPermissions.as_wire_str(),
Expand All @@ -2281,19 +2303,32 @@ channels = "ALL"
#[test]
fn test_permission_mode_is_default() {
assert!(PermissionMode::Default.is_default());
assert!(!PermissionMode::Auto.is_default());
assert!(!PermissionMode::BypassPermissions.is_default());
assert!(!PermissionMode::AcceptEdits.is_default());
assert!(!PermissionMode::DontAsk.is_default());
assert!(!PermissionMode::Plan.is_default());
}

#[test]
fn test_permission_mode_auto_degrades_to_default_when_unsupported() {
// The wire string is "auto" — the adapter handles graceful downgrade
// to "default" when the active model does not support Auto mode.
// Verify only that the wire string is correct and distinct from "default".
let auto = PermissionMode::Auto;
assert_eq!(auto.as_wire_str(), "auto");
assert_ne!(auto.as_wire_str(), "default");
assert!(!auto.is_default());
}

#[test]
fn test_permission_mode_display() {
assert_eq!(
format!("{}", PermissionMode::BypassPermissions),
"bypassPermissions"
);
assert_eq!(format!("{}", PermissionMode::Default), "default");
assert_eq!(format!("{}", PermissionMode::Auto), "auto");
}

#[test]
Expand Down
Loading
Loading