Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 19 additions & 4 deletions crates/buzz-acp/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ pub enum AcpError {
#[error("Protocol error: {0}")]
Protocol(String),

#[error("Agent reported error: {0}")]
AgentError(String),
#[error("Agent reported error (code {code}): {message}")]
AgentError { code: i64, message: String },
}

/// ACP client that owns an agent subprocess and communicates over its stdio.
Expand Down Expand Up @@ -863,7 +863,13 @@ impl AcpClient {
if let Some(id) = msg.get("id") {
if *id == serde_json::json!(expected_id) && msg.get("method").is_none() {
if let Some(error) = msg.get("error") {
return Err(AcpError::AgentError(error.to_string()));
let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(-32000);
let message = error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error")
.to_string();
return Err(AcpError::AgentError { code, message });
}
return Ok(msg["result"].clone());
}
Expand Down Expand Up @@ -1185,7 +1191,16 @@ impl AcpClient {
let _ = ack_tx
.send(crate::pool::SteerAck::PromptCompletedNeutral);
}
return Err(AcpError::AgentError(error.to_string()));
let code = error
.get("code")
.and_then(|c| c.as_i64())
.unwrap_or(-32000);
let message = error
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error")
.to_string();
return Err(AcpError::AgentError { code, message });
}
if let Some((_, ack_tx)) = pending_steer.take() {
let _ =
Expand Down
28 changes: 20 additions & 8 deletions crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2782,16 +2782,20 @@ fn handle_prompt_result(
PromptSource::Channel(ch) => Some(*ch),
PromptSource::Heartbeat => None,
};
let emit_turn_error = |error_msg: &str| {
let emit_turn_error = |error_msg: &str, error_code: Option<i64>| {
if let Some(ref observer) = observer {
let mut payload = serde_json::json!({
"outcome": outcome_label,
"error": error_msg,
});
if let Some(code) = error_code {
payload["code"] = serde_json::json!(code);
}
observer.emit(
"turn_error",
Some(agent_index),
&observer::context_for(channel_id, None, None),
serde_json::json!({
"outcome": outcome_label,
"error": error_msg,
}),
payload,
);
}
};
Expand Down Expand Up @@ -2819,7 +2823,7 @@ fn handle_prompt_result(
"exited" => "Agent process exited unexpectedly",
_ => "Agent session timed out due to inactivity",
};
emit_turn_error(death_message);
emit_turn_error(death_message, None);

let index = result.agent.index;
let slot_history = &mut crash_history[index];
Expand Down Expand Up @@ -2880,7 +2884,11 @@ fn handle_prompt_result(
error = %e,
"transport/protocol error — respawning agent"
);
emit_turn_error(&e.to_string());
let error_code = match &e {
acp::AcpError::AgentError { code, .. } => Some(*code),
_ => None,
};
emit_turn_error(&e.to_string(), error_code);

let index = result.agent.index;
let slot_history = &mut crash_history[index];
Expand All @@ -2906,7 +2914,11 @@ fn handle_prompt_result(
error = %e,
"agent_returned (application error — pipe intact)"
);
emit_turn_error(&e.to_string());
let error_code = match &e {
acp::AcpError::AgentError { code, .. } => Some(*code),
_ => None,
};
emit_turn_error(&e.to_string(), error_code);
pool.return_agent(result.agent);
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/buzz-acp/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1884,7 +1884,7 @@ pub async fn run_prompt_task(
// AgentError means the agent caught a problem before mutating
// session state (e.g. bad LLM response). The session is healthy —
// don't invalidate it. Other errors may have corrupted state.
if !matches!(e, AcpError::AgentError(_)) {
if !matches!(e, AcpError::AgentError { .. }) {
agent.state.invalidate(&source);
}
let usage = agent.acp.take_turn_usage();
Expand Down
9 changes: 9 additions & 0 deletions crates/buzz-agent/src/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ impl Llm {
// is centralized and never needs to be repeated in each provider arm.
result.map_err(|e| match e {
AgentError::Llm(s) => AgentError::Llm(format!("({effective_model}) {s}")),
AgentError::LlmModelNotFound(s) => {
AgentError::LlmModelNotFound(format!("({effective_model}) {s}"))
}
other => other,
})
}
Expand Down Expand Up @@ -1071,6 +1074,12 @@ where
backoff_with_jitter(attempt).await;
continue;
}
if status == 404 {
return Err(AgentError::LlmModelNotFound(format!(
"{status}: {}",
read_error_body(resp).await
)));
}
if !status.is_success() {
return Err(AgentError::Llm(format!(
"{status}: {}",
Expand Down
3 changes: 3 additions & 0 deletions crates/buzz-agent/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub enum AgentError {
InvalidParams(String),
Llm(String),
LlmAuth(String),
LlmModelNotFound(String),
Mcp(String),
Cancelled,
}
Expand All @@ -235,6 +236,7 @@ impl std::fmt::Display for AgentError {
Self::InvalidParams(s) => write!(f, "invalid params: {s}"),
Self::Llm(s) => write!(f, "llm: {s}"),
Self::LlmAuth(s) => write!(f, "llm auth: {s}"),
Self::LlmModelNotFound(s) => write!(f, "llm model not found: {s}"),
Self::Mcp(s) => write!(f, "mcp: {s}"),
Self::Cancelled => write!(f, "cancelled"),
}
Expand All @@ -248,6 +250,7 @@ impl AgentError {
match self {
Self::InvalidParams(_) => -32602,
Self::LlmAuth(_) => -32001,
Self::LlmModelNotFound(_) => -32002,
_ => -32000,
}
}
Expand Down
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default defineConfig({
"**/activity-scope-label-screenshots.spec.ts",
"**/local-archive-screenshots.spec.ts",
"**/agent-readiness-screenshots.spec.ts",
"**/agent-error-state-screenshots.spec.ts",
"**/edit-agent.spec.ts",
"**/pubkey-display-screenshots.spec.ts",
"**/file-attachment.spec.ts",
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ mod tests {
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: RespondTo::OwnerOnly,
respond_to_allowlist: vec![],
display_name: None,
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/commands/agent_settings.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use tauri::{AppHandle, Manager};
use tauri::{AppHandle, Manager, State};

use crate::{
app_state::AppState,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/agents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ pub async fn create_managed_agent(
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: input.respond_to,
respond_to_allowlist: respond_to_allowlist.clone(),
display_name: None,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/commands/personas/inbound_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ fn local_agent() -> ManagedAgentRecord {
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: crate::managed_agents::RespondTo::OwnerOnly,
respond_to_allowlist: vec![],
display_name: None,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/agent_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ mod tests {
last_stopped_at: Some("2025-01-03T00:00:00Z".to_string()),
last_exit_code: Some(0),
last_error: Some("some runtime error".to_string()),
last_error_code: None,
respond_to: RespondTo::Allowlist,
respond_to_allowlist: vec!["79be667e".to_string()],
// Unified-model fields carry real values so the exclusion test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ fn test_record() -> ManagedAgentRecord {
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: crate::managed_agents::types::RespondTo::OwnerOnly,
respond_to_allowlist: vec![],
display_name: None,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/discovery/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ fn record_with(
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: Default::default(),
respond_to_allowlist: vec![],
display_name: None,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/nest/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ fn make_agent(name: &str, persona_id: Option<&str>) -> ManagedAgentRecord {
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: RespondTo::default(),
respond_to_allowlist: vec![],
env_vars: std::collections::BTreeMap::new(),
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,7 @@ mod tests {
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: Default::default(),
respond_to_allowlist: vec![],
display_name: None,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/relay_mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ mod tests {
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: RespondTo::OwnerOnly,
respond_to_allowlist: vec![],
relay_mesh: None,
Expand Down
21 changes: 17 additions & 4 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,7 @@ pub fn sync_managed_agent_processes(
if let Some(record) = records.iter_mut().find(|record| record.pubkey == *pubkey) {
record.updated_at = now_iso();
record.last_error = Some(format!("failed to inspect process state: {error}"));
record.last_error_code = None;
}
changed = true;
exited.push(pubkey.clone());
Expand All @@ -1214,13 +1215,20 @@ pub fn sync_managed_agent_processes(
record.runtime_pid = None;
record.last_stopped_at = Some(now_iso());
record.last_exit_code = status.code();
record.last_error = if status.success() {
let log_err = if status.success() {
None
} else {
super::meaningful_agent_error_from_log(&runtime.log_path)
.unwrap_or_else(|| format!("harness exited with status {status}"))
.into()
Some(
super::meaningful_agent_error_from_log(&runtime.log_path).unwrap_or_else(
|| super::storage::AgentLogError {
message: format!("harness exited with status {status}"),
code: None,
},
),
)
};
record.last_error = log_err.as_ref().map(|e| e.message.clone());
record.last_error_code = log_err.as_ref().and_then(|e| e.code);
}

changed = true;
Expand Down Expand Up @@ -1409,6 +1417,7 @@ pub fn build_managed_agent_summary(
last_stopped_at: record.last_stopped_at.clone(),
last_exit_code: record.last_exit_code,
last_error: record.last_error.clone(),
last_error_code: record.last_error_code,
start_on_app_launch: record.start_on_app_launch,
auto_restart_on_config_change: record.auto_restart_on_config_change,
log_path,
Expand Down Expand Up @@ -1927,6 +1936,7 @@ pub fn start_managed_agent_process(
{
record.updated_at = now_iso();
record.last_error = None;
record.last_error_code = None;
return Ok(());
}

Expand All @@ -1942,6 +1952,7 @@ pub fn start_managed_agent_process(
record.last_stopped_at = None;
record.last_exit_code = None;
record.last_error = None;
record.last_error_code = None;

runtimes.insert(record.pubkey.clone(), process);
Ok(())
Expand All @@ -1964,6 +1975,7 @@ pub fn stop_managed_agent_process(
record.last_stopped_at = Some(now);
record.last_exit_code = None;
record.last_error = None;
record.last_error_code = None;
}
super::remove_agent_pid_file(app, &record.pubkey);
return Ok(());
Expand Down Expand Up @@ -1998,6 +2010,7 @@ pub fn stop_managed_agent_process(
record.last_stopped_at = Some(now);
record.last_exit_code = status.code();
record.last_error = None;
record.last_error_code = None;

super::remove_agent_pid_file(app, &record.pubkey);

Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/runtime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ fn fixture(
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to,
respond_to_allowlist: allowlist,
display_name: None,
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/src/managed_agents/spawn_hash/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ fn record() -> ManagedAgentRecord {
last_stopped_at: None,
last_exit_code: None,
last_error: None,
last_error_code: None,
respond_to: Default::default(),
respond_to_allowlist: vec![],
display_name: None,
Expand Down
40 changes: 27 additions & 13 deletions desktop/src-tauri/src/managed_agents/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,14 +532,30 @@ fn bytecount_newlines(buf: &[u8]) -> usize {
buf.iter().filter(|&&b| b == b'\n').count()
}

pub fn meaningful_agent_error_from_log(path: &Path) -> Option<String> {
pub struct AgentLogError {
pub message: String,
pub code: Option<i64>,
}

pub fn meaningful_agent_error_from_log(path: &Path) -> Option<AgentLogError> {
let tail = read_log_tail(path, 200).ok()?;
tail.lines().rev().map(str::trim).find_map(|line| {
if line.starts_with("Agent reported error:") {
return Some(line.to_string());
// New format: "Agent reported error (code -32002): ..."
if let Some(rest) = line.strip_prefix("Agent reported error (code ") {
if let Some(paren_end) = rest.find("): ") {
let code = rest[..paren_end].parse::<i64>().ok();
return Some(AgentLogError {
message: line.to_string(),
code,
});
}
}
if line.starts_with("llm auth:") {
return Some(format!("Agent reported error: {line}"));
// Legacy format (older buzz-acp builds): "Agent reported error: ..."
if line.starts_with("Agent reported error:") {
return Some(AgentLogError {
message: line.to_string(),
code: None,
});
}
None
})
Expand Down Expand Up @@ -857,20 +873,18 @@ mod tests {

#[test]
fn meaningful_agent_error_from_log_promotes_wrapped_llm_auth() {
let file = write_log("noise\nAgent reported error: llm auth: denied\n");
assert_eq!(
super::meaningful_agent_error_from_log(file.path()).as_deref(),
Some("Agent reported error: llm auth: denied")
let file = write_log(
"noise\nAgent reported error (code -32001): llm auth: 401 unauthorized: ...\n",
);
let result = super::meaningful_agent_error_from_log(file.path()).unwrap();
assert!(result.message.contains("llm auth"));
assert_eq!(result.code, Some(-32001));
}

#[test]
fn meaningful_agent_error_from_log_promotes_unwrapped_llm_auth() {
let file = write_log("noise\nllm auth: denied\n");
assert_eq!(
super::meaningful_agent_error_from_log(file.path()).as_deref(),
Some("Agent reported error: llm auth: denied")
);
assert!(super::meaningful_agent_error_from_log(file.path()).is_none());
}

#[test]
Expand Down
Loading
Loading