use crate::ai_agents::AiAgentStreamEvent; const LOCALIZED_ERROR_PREFIX: &str = "tolaria:i18n-error:"; const PI_EMPTY_OUTPUT_KEY: &str = "ai.error.pi.emptyOutput"; const PI_EMPTY_OUTPUT_WITH_DIAGNOSTIC_KEY: &str = "ai.error.pi.emptyOutputWithDiagnostic"; #[cfg(test)] pub(crate) fn parse_line( line: Result, emit: &mut F, ) -> Option where F: FnMut(AiAgentStreamEvent), { crate::cli_agent_runtime::parse_ai_agent_json_line(line, emit) } pub(crate) fn dispatch_event(json: &serde_json::Value, emit: &mut F) where F: FnMut(AiAgentStreamEvent), { if json["type"].as_str() == Some("session") { emit_session_event(json, emit); } match json["type"].as_str().unwrap_or_default() { "message_update" => emit_message_update(json, emit), "tool_execution_start" => emit_tool_start(json, emit), "tool_execution_end" => emit_tool_done(json, emit), "error" => emit_error_event(json, emit), _ => {} } } pub(crate) fn session_id(json: &serde_json::Value) -> Option<&str> { json["id"] .as_str() .or_else(|| json["session_id"].as_str()) .or_else(|| json["session"]["id"].as_str()) } pub(crate) fn format_error(stderr_output: String, status: String) -> String { let lower = stderr_output.to_ascii_lowercase(); if is_auth_error(&lower) { return "Pi CLI is not authenticated. Run `pi /login` in your terminal or configure a provider API key, then retry.".into(); } if stderr_output.trim().is_empty() { format!("pi exited with status {status}") } else { stderr_output.lines().take(3).collect::>().join("\n") } } pub(crate) fn format_empty_success(diagnostic_output: &str) -> String { if diagnostic_output.is_empty() { return localized_error(PI_EMPTY_OUTPUT_KEY, serde_json::json!({})); } localized_error( PI_EMPTY_OUTPUT_WITH_DIAGNOSTIC_KEY, serde_json::json!({ "diagnostic_output": diagnostic_output }), ) } fn localized_error(key: &str, values: serde_json::Value) -> String { let payload = serde_json::json!({ "key": key, "values": values, }); format!("{LOCALIZED_ERROR_PREFIX}{payload}") } fn emit_session_event(json: &serde_json::Value, emit: &mut F) where F: FnMut(AiAgentStreamEvent), { if let Some(session_id) = session_id(json) { emit(AiAgentStreamEvent::Init { session_id: session_id.to_string(), }); } } fn emit_message_update(json: &serde_json::Value, emit: &mut F) where F: FnMut(AiAgentStreamEvent), { let event = &json["assistantMessageEvent"]; match event["type"].as_str().unwrap_or_default() { "text_delta" => emit_delta(event, emit, |text| AiAgentStreamEvent::TextDelta { text }), "thinking_delta" => emit_delta(event, emit, |text| AiAgentStreamEvent::ThinkingDelta { text, }), _ => {} } } fn emit_delta( json: &serde_json::Value, emit: &mut F, build: impl FnOnce(String) -> AiAgentStreamEvent, ) where F: FnMut(AiAgentStreamEvent), { if let Some(delta) = json["delta"].as_str() { emit(build(delta.to_string())); } } fn emit_tool_start(json: &serde_json::Value, emit: &mut F) where F: FnMut(AiAgentStreamEvent), { emit(AiAgentStreamEvent::ToolStart { tool_name: tool_name(json), tool_id: tool_id(json), input: json.get("args").map(|args| args.to_string()), }); } fn emit_tool_done(json: &serde_json::Value, emit: &mut F) where F: FnMut(AiAgentStreamEvent), { emit(AiAgentStreamEvent::ToolDone { tool_id: tool_id(json), output: json.get("result").map(|result| result.to_string()), }); } fn emit_error_event(json: &serde_json::Value, emit: &mut F) where F: FnMut(AiAgentStreamEvent), { if let Some(message) = message_value(json) { emit(AiAgentStreamEvent::Error { message: message.to_string(), }); } } fn tool_name(json: &serde_json::Value) -> String { json["toolName"].as_str().unwrap_or("tool").to_string() } fn tool_id(json: &serde_json::Value) -> String { json["toolCallId"].as_str().unwrap_or("tool").to_string() } fn message_value(json: &serde_json::Value) -> Option<&str> { json["message"] .as_str() .or_else(|| json["error"].as_str()) .or_else(|| json["text"].as_str()) } fn is_auth_error(lower: &str) -> bool { [ "auth", "login", "sign in", "api key", "api.key", "provider", "401", ] .iter() .any(|pattern| lower.contains(pattern)) } #[cfg(test)] #[path = "pi_events_tests.rs"] mod tests;