From 8b090c3d0daf8b336aab9074b0d8aa31a688e232 Mon Sep 17 00:00:00 2001 From: main Date: Tue, 24 Mar 2026 19:09:28 -0400 Subject: Surface reusable consult context on failures --- crates/phone-opus/src/mcp/fault.rs | 82 +++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) (limited to 'crates/phone-opus/src/mcp/fault.rs') diff --git a/crates/phone-opus/src/mcp/fault.rs b/crates/phone-opus/src/mcp/fault.rs index 5b23f79..4d438e3 100644 --- a/crates/phone-opus/src/mcp/fault.rs +++ b/crates/phone-opus/src/mcp/fault.rs @@ -1,7 +1,33 @@ +use std::collections::BTreeMap; + use libmcp::{Fault, FaultClass, FaultCode, Generation, RecoveryDirective, ToolErrorDetail}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub(crate) struct FaultContext { + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) consult: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub(crate) struct ConsultFaultContext { + pub(crate) cwd: String, + pub(crate) context_mode: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) reused_session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) downstream_session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) resume_session_id: Option, + #[serde(default, skip_serializing_if = "is_false")] + pub(crate) quota_limited: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) quota_reset_hint: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) retry_hint: Option, +} + #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub(crate) enum FaultStage { Host, @@ -18,6 +44,8 @@ pub(crate) struct FaultRecord { pub(crate) stage: FaultStage, pub(crate) operation: String, pub(crate) jsonrpc_code: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) context: Option>, pub(crate) retryable: bool, pub(crate) retried: bool, } @@ -153,15 +181,59 @@ impl FaultRecord { self } + pub(crate) fn with_context(mut self, context: FaultContext) -> Self { + self.context = Some(Box::new(context)); + self + } + pub(crate) fn message(&self) -> &str { self.fault.detail.as_str() } + fn rendered_message(&self) -> String { + let mut lines = vec![self.message().to_owned()]; + let Some(consult) = self + .context + .as_ref() + .and_then(|context| context.consult.as_ref()) + else { + return lines.join("\n"); + }; + let mut fields: BTreeMap = BTreeMap::from([ + ("cwd".to_owned(), consult.cwd.clone()), + ("context_mode".to_owned(), consult.context_mode.clone()), + ]); + if let Some(session_id) = consult.reused_session_id.as_ref() { + let _ = fields.insert("reused_session".to_owned(), session_id.clone()); + } + if let Some(session_id) = consult.downstream_session_id.as_ref() { + let _ = fields.insert("downstream_session".to_owned(), session_id.clone()); + } + if let Some(session_id) = consult.resume_session_id.as_ref() { + let _ = fields.insert("resume_session".to_owned(), session_id.clone()); + } + if consult.quota_limited { + let _ = fields.insert("quota_limited".to_owned(), "true".to_owned()); + } + if let Some(reset_hint) = consult.quota_reset_hint.as_ref() { + let _ = fields.insert("quota_reset".to_owned(), reset_hint.clone()); + } + if let Some(retry_hint) = consult.retry_hint.as_ref() { + let _ = fields.insert("retry_hint".to_owned(), retry_hint.clone()); + } + lines.extend( + fields + .into_iter() + .map(|(key, value)| format!("{key}: {value}")), + ); + lines.join("\n") + } + pub(crate) fn error_detail(&self) -> ToolErrorDetail { ToolErrorDetail { code: Some(self.jsonrpc_code), kind: Some(self.fault.code.as_str().to_owned()), - message: Some(self.message().to_owned()), + message: Some(self.rendered_message()), } } @@ -174,10 +246,11 @@ impl FaultRecord { } pub(crate) fn into_tool_result(self) -> Value { + let rendered_message = self.rendered_message(); json!({ "content": [{ "type": "text", - "text": self.message(), + "text": rendered_message, }], "structuredContent": self, "isError": true, @@ -201,11 +274,16 @@ impl FaultRecord { stage, operation: operation.into(), jsonrpc_code, + context: None, retried: false, } } } +const fn is_false(value: &bool) -> bool { + !*value +} + fn fault_code(code: &'static str) -> FaultCode { match FaultCode::try_new(code.to_owned()) { Ok(value) => value, -- cgit v1.2.3