use super::envelope::emit_json;
fn escape(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}
#[derive(serde::Serialize)]
struct ErrorEnvelope<'a> {
error: bool,
code: i32,
message: &'a str,
error_class: &'a str,
retryable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
suggestion: Option<&'a str>,
#[serde(skip_serializing_if = "<[String]>::is_empty")]
discarded_flags: &'a [String],
#[serde(skip_serializing_if = "Option::is_none")]
agent_surface: Option<serde_json::Map<String, serde_json::Value>>,
}
#[cold]
#[inline(never)]
pub fn emit_error_envelope(
code: i32,
message: &str,
error_class: &str,
retryable: bool,
suggestion: Option<&str>,
discarded_flags: &[String],
) {
let target = crate::agent_surface::target::record(
crate::agent_surface::get(),
crate::agent_surface::universe::get(),
);
let envelope = ErrorEnvelope {
error: true,
code,
message,
error_class,
retryable,
suggestion,
discarded_flags,
agent_surface: target.clone(),
};
if emit_json(&envelope).is_err() {
use std::io::Write;
let escaped = escape(message);
let esc_class = escape(error_class);
let mut line = format!(
r#"{{"error":true,"code":{code},"message":"{escaped}","error_class":"{esc_class}","retryable":{retryable}"#
);
if let Some(s) = suggestion {
line.push_str(&format!(r#","suggestion":"{}""#, escape(s)));
}
if !discarded_flags.is_empty() {
let list = discarded_flags
.iter()
.map(|f| format!(r#""{}""#, escape(f)))
.collect::<Vec<_>>()
.join(",");
line.push_str(&format!(r#","discarded_flags":[{list}]"#));
}
if let Some(map) = &target {
let members = map
.iter()
.map(|(k, v)| {
let text = v.as_str().unwrap_or_default();
format!(r#""{}":"{}""#, escape(k), escape(text))
})
.collect::<Vec<_>>()
.join(",");
line.push_str(&format!(r#","agent_surface":{{{members}}}"#));
}
line.push('}');
let _ = writeln!(std::io::stdout().lock(), "{line}");
}
}
#[cold]
#[inline(never)]
pub fn emit_error_json(code: i32, message: &str) {
emit_error_envelope(code, message, "permanent", false, None, &[]);
}
#[cold]
#[inline(never)]
pub fn emit_error_json_with_suggestion(
code: i32,
message: &str,
error_class: &str,
retryable: bool,
suggestion: Option<&str>,
discarded_flags: &[String],
) {
emit_error_envelope(
code,
message,
error_class,
retryable,
suggestion,
discarded_flags,
);
}
#[cfg(test)]
mod tests {
use super::escape;
#[test]
fn escape_leaves_plain_text_untouched() {
assert_eq!(escape("database is malformed"), "database is malformed");
}
#[test]
fn escape_protects_quotes_and_backslashes() {
assert_eq!(escape(r#"say "hi""#), r#"say \"hi\""#);
assert_eq!(escape(r"C:\path"), r"C:\\path");
}
#[test]
fn escape_orders_backslash_before_quote() {
assert_eq!(escape(r#"\""#), r#"\\\""#);
}
}