Skip to content
Open
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
25 changes: 20 additions & 5 deletions crates/buzz-acp/src/acp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,15 @@ pub enum AcpError {
#[error("Agent did not stop within {0:?} after cancellation")]
CancelDrainTimeout(std::time::Duration),

#[error("Request timeout — agent did not respond within {0:?}")]
Timeout(std::time::Duration),
#[error("Request timeout — agent did not respond to {method} within {elapsed:?}")]
Timeout {
/// The JSON-RPC method that went unanswered. Without it a caller
/// cannot tell a slow `session/new` from a slow `session/prompt`,
/// and sessions are created lazily inside the prompt task — so the
/// two are indistinguishable from the outside (see #4098).
method: &'static str,
elapsed: std::time::Duration,
},

#[error("Write timeout — agent stopped reading stdin (blocked for {0:?})")]
WriteTimeout(std::time::Duration),
Expand Down Expand Up @@ -1075,7 +1082,7 @@ impl AcpClient {
/// if they don't, the agent is likely stuck and we must not block forever.
async fn send_request(
&mut self,
method: &str,
method: &'static str,
params: serde_json::Value,
) -> Result<serde_json::Value, AcpError> {
let id = self.next_id;
Expand All @@ -1096,12 +1103,20 @@ impl AcpClient {
let timeout = Self::REQUEST_TIMEOUT;
match tokio::time::timeout(timeout, self.write_ndjson(&msg)).await {
Ok(result) => result?,
Err(_) => return Err(AcpError::Timeout(timeout)),
Err(_) => {
return Err(AcpError::Timeout {
method,
elapsed: timeout,
})
}
}

match tokio::time::timeout(timeout, self.read_until_response(id)).await {
Ok(result) => result,
Err(_) => Err(AcpError::Timeout(timeout)),
Err(_) => Err(AcpError::Timeout {
method,
elapsed: timeout,
}),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3470,7 +3470,7 @@ fn handle_prompt_result(
e,
acp::AcpError::Io(_)
| acp::AcpError::WriteTimeout(_)
| acp::AcpError::Timeout(_)
| acp::AcpError::Timeout { .. }
| acp::AcpError::Protocol(_)
);
let error_code = match &e {
Expand Down
21 changes: 17 additions & 4 deletions crates/buzz-acp/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,13 @@ async fn apply_model_switch(
}
ModelSwitchMethod::SetModel { .. } => "set_model".to_string(),
};
// The RPC this switch actually sends, for the outer-timeout error below.
// `method_label` above is prose for humans; naming the wrong method in an
// error would be worse than naming none.
let rpc_method = match method {
ModelSwitchMethod::ConfigOption { .. } => "session/set_config_option",
ModelSwitchMethod::SetModel { .. } => "session/set_model",
};

let result = tokio::time::timeout(MODEL_SWITCH_TIMEOUT, async {
match method {
Expand Down Expand Up @@ -1065,7 +1072,7 @@ async fn apply_model_switch(
// so the caller can respawn the agent instead of reusing a poisoned one.
Ok(Err(e @ AcpError::Io(_)))
| Ok(Err(e @ AcpError::WriteTimeout(_)))
| Ok(Err(e @ AcpError::Timeout(_)))
| Ok(Err(e @ AcpError::Timeout { .. }))
| Ok(Err(e @ AcpError::Protocol(_)))
| Ok(Err(e @ AcpError::AgentExited)) => {
tracing::error!(
Expand All @@ -1088,7 +1095,10 @@ async fn apply_model_switch(
target: "pool::model",
"model set via {method_label} timed out ({MODEL_SWITCH_TIMEOUT:?}) — treating as fatal"
);
return Err(AcpError::Timeout(MODEL_SWITCH_TIMEOUT));
return Err(AcpError::Timeout {
method: rpc_method,
elapsed: MODEL_SWITCH_TIMEOUT,
});
}
}
Ok(())
Expand Down Expand Up @@ -1141,7 +1151,7 @@ async fn apply_permission_mode(
// so the caller can respawn the agent.
Ok(Err(e @ AcpError::Io(_)))
| Ok(Err(e @ AcpError::WriteTimeout(_)))
| Ok(Err(e @ AcpError::Timeout(_)))
| Ok(Err(e @ AcpError::Timeout { .. }))
| Ok(Err(e @ AcpError::Protocol(_)))
| Ok(Err(e @ AcpError::AgentExited)) => {
tracing::error!(
Expand All @@ -1163,7 +1173,10 @@ async fn apply_permission_mode(
target: "pool::permission",
"permission mode set timed out ({PERMISSION_MODE_TIMEOUT:?}) — treating as fatal"
);
return Err(AcpError::Timeout(PERMISSION_MODE_TIMEOUT));
return Err(AcpError::Timeout {
method: "session/set_config_option",
elapsed: PERMISSION_MODE_TIMEOUT,
});
}
}
Ok(())
Expand Down