swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/phone-opus/src/mcp/fault.rs
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-24 19:09:28 -0400
committermain <main@swarm.moe>2026-03-24 19:09:28 -0400
commit8b090c3d0daf8b336aab9074b0d8aa31a688e232 (patch)
tree175d2f2313ea34038278cbd066f10fae6bf244b3 /crates/phone-opus/src/mcp/fault.rs
parent57205cd29c41afe85c78a3b5c1962bb3d1f1b27c (diff)
downloadphone_opus-8b090c3d0daf8b336aab9074b0d8aa31a688e232.zip
Surface reusable consult context on failures
Diffstat (limited to 'crates/phone-opus/src/mcp/fault.rs')
-rw-r--r--crates/phone-opus/src/mcp/fault.rs82
1 files changed, 80 insertions, 2 deletions
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<ConsultFaultContext>,
+}
+
+#[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<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) downstream_session_id: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) resume_session_id: Option<String>,
+ #[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<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) retry_hint: Option<String>,
+}
+
#[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<Box<FaultContext>>,
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<String, String> = 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,