use serde_json::{Map, Value};
use super::tool_error::failure_category_for_error_code;
use super::{
ParsedLegacyOutput, ToolEnvelopeContext, parse_legacy_output, tool_error_retryable_heuristic,
};
pub const CRABMATE_TOOL_ENVELOPE_VERSION_V1: u32 = 1;
#[derive(Debug, Clone, PartialEq)]
pub struct NormalizedToolEnvelope {
pub envelope_version: u32,
pub name: String,
pub summary: String,
pub output: String,
pub ok: bool,
pub exit_code: Option<i32>,
pub error_code: Option<String>,
pub failure_category: Option<String>,
pub retryable: Option<bool>,
pub tool_call_id: Option<String>,
pub execution_mode: Option<String>,
pub parallel_batch_id: Option<String>,
pub output_truncated: bool,
pub output_original_chars: Option<u64>,
pub output_kept_head_chars: Option<u64>,
pub output_kept_tail_chars: Option<u64>,
pub structured_payload: Option<Value>,
}
impl NormalizedToolEnvelope {
pub fn from_tool_run(
tool_name: &str,
summary: String,
parsed: &ParsedLegacyOutput,
raw_output: &str,
envelope_ctx: Option<&ToolEnvelopeContext<'_>>,
structured_payload: Option<Value>,
) -> Self {
let retryable = if parsed.ok {
None
} else {
Some(tool_error_retryable_heuristic(parsed.error_code.as_deref()))
};
let (tool_call_id, execution_mode, parallel_batch_id) = match envelope_ctx {
Some(c) => (
Some(c.tool_call_id.to_string()),
Some(c.execution_mode.to_string()),
c.parallel_batch_id.map(|s| s.to_string()),
),
None => (None, None, None),
};
let failure_category = if parsed.ok {
None
} else {
parsed
.error_code
.as_deref()
.map(failure_category_for_error_code)
.map(|c| c.as_str().to_string())
};
Self {
envelope_version: CRABMATE_TOOL_ENVELOPE_VERSION_V1,
name: tool_name.to_string(),
summary,
output: raw_output.to_string(),
ok: parsed.ok,
exit_code: parsed.exit_code,
error_code: parsed.error_code.clone(),
failure_category,
retryable,
tool_call_id,
execution_mode,
parallel_batch_id,
output_truncated: false,
output_original_chars: None,
output_kept_head_chars: None,
output_kept_tail_chars: None,
structured_payload,
}
}
fn from_crabmate_object(ct: &Map<String, Value>) -> Option<Self> {
let envelope_version = ct
.get("v")
.and_then(|x| x.as_u64())
.map(|u| u as u32)
.unwrap_or(CRABMATE_TOOL_ENVELOPE_VERSION_V1);
let name = ct.get("name")?.as_str()?.to_string();
let summary = ct
.get("summary")
.and_then(|x| x.as_str())
.unwrap_or("")
.to_string();
let output = ct.get("output")?.as_str()?.to_string();
let ok = ct
.get("ok")
.and_then(|x| x.as_bool())
.unwrap_or_else(|| parse_legacy_output(name.as_str(), output.as_str()).ok);
let exit_code = ct
.get("exit_code")
.and_then(|x| x.as_i64())
.map(|i| i as i32);
let error_code = ct
.get("error_code")
.and_then(|x| x.as_str())
.map(String::from);
let failure_category_stored = ct
.get("failure_category")
.and_then(|x| x.as_str())
.map(String::from);
let retryable = ct.get("retryable").and_then(|x| x.as_bool());
let tool_call_id = ct
.get("tool_call_id")
.and_then(|x| x.as_str())
.map(String::from);
let execution_mode = ct
.get("execution_mode")
.and_then(|x| x.as_str())
.map(String::from);
let parallel_batch_id = ct
.get("parallel_batch_id")
.and_then(|x| x.as_str())
.map(String::from);
let output_truncated = ct.get("output_truncated").and_then(|x| x.as_bool()) == Some(true);
let output_original_chars = ct.get("output_original_chars").and_then(|x| x.as_u64());
let output_kept_head_chars = ct.get("output_kept_head_chars").and_then(|x| x.as_u64());
let output_kept_tail_chars = ct.get("output_kept_tail_chars").and_then(|x| x.as_u64());
let structured_payload = ct.get("structured_payload").cloned();
let failure_category = failure_category_stored.or_else(|| {
if ok {
return None;
}
error_code
.as_deref()
.map(failure_category_for_error_code)
.map(|c| c.as_str().to_string())
});
Some(Self {
envelope_version,
name,
summary,
output,
ok,
exit_code,
error_code,
failure_category,
retryable,
tool_call_id,
execution_mode,
parallel_batch_id,
output_truncated,
output_original_chars,
output_kept_head_chars,
output_kept_tail_chars,
structured_payload,
})
}
pub fn parse_tool_message_content(content: &str) -> Option<Self> {
let t = content.trim();
let v: Value = serde_json::from_str(t).ok()?;
let ct = v.get("crabmate_tool")?.as_object()?;
Self::from_crabmate_object(ct)
}
pub fn to_crabmate_tool_map(&self) -> Map<String, Value> {
let mut ct = Map::new();
ct.insert("v".into(), Value::from(self.envelope_version));
ct.insert("name".into(), Value::String(self.name.clone()));
ct.insert("summary".into(), Value::String(self.summary.clone()));
ct.insert("ok".into(), Value::Bool(self.ok));
ct.insert("output".into(), Value::String(self.output.clone()));
insert_opt(&mut ct, "exit_code", &self.exit_code);
insert_opt(&mut ct, "error_code", &self.error_code);
insert_opt(&mut ct, "failure_category", &self.failure_category);
insert_opt(&mut ct, "retryable", &self.retryable);
insert_opt(&mut ct, "tool_call_id", &self.tool_call_id);
insert_opt(&mut ct, "execution_mode", &self.execution_mode);
insert_opt(&mut ct, "parallel_batch_id", &self.parallel_batch_id);
if self.output_truncated {
ct.insert("output_truncated".into(), Value::Bool(true));
}
insert_opt(&mut ct, "output_original_chars", &self.output_original_chars);
insert_opt(&mut ct, "output_kept_head_chars", &self.output_kept_head_chars);
insert_opt(&mut ct, "output_kept_tail_chars", &self.output_kept_tail_chars);
insert_opt(&mut ct, "structured_payload", &self.structured_payload);
ct
}
pub fn encode_to_message_line(&self) -> String {
let mut root = Map::new();
root.insert(
"crabmate_tool".into(),
Value::Object(self.to_crabmate_tool_map()),
);
serde_json::to_string(&Value::Object(root)).unwrap_or_else(|_| self.output.clone())
}
}
pub fn normalize_tool_message_content(content: &str) -> Option<NormalizedToolEnvelope> {
NormalizedToolEnvelope::parse_tool_message_content(content)
}
fn insert_opt<T: Clone + Into<Value>>(ct: &mut Map<String, Value>, key: &str, v: &Option<T>) {
if let Some(v) = v.as_ref() {
ct.insert(key.to_string(), (*v).clone().into());
}
}