use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::sync::OnceLock;
use claudius::Effort;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const RAW_PROTOCOL_VERSION: u32 = 1;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawRequestEnvelope {
pub protocol_version: u32,
pub request_id: String,
#[serde(flatten)]
pub request: RawRequest,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum RawRequest {
UserTurn {
text: String,
},
PromptResponse {
prompt_id: String,
response: String,
},
ShowAgent,
ListAgents,
SwitchAgent {
agent: String,
},
Compact,
Clear,
SetModel {
model: String,
},
SetSystemPrompt {
prompt: Option<String>,
},
SetMaxTokens {
max_tokens: u32,
},
SetTemperature {
temperature: Option<f32>,
},
SetTopP {
top_p: Option<f32>,
},
SetTopK {
top_k: Option<u32>,
},
AddStopSequence {
sequence: String,
},
ClearStopSequences,
ListStopSequences,
SetThinkingBudget {
tokens: Option<u32>,
},
SetThinkingAdaptive,
SetEffort {
effort: Option<Effort>,
},
SetSpend {
dollars: Option<f64>,
},
SetCaching {
enabled: bool,
},
SaveTranscript {
path: String,
},
LoadTranscript {
path: String,
},
Stats,
ShowConfig,
Shutdown,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RawServerMessage {
Hello(RawHello),
Event(RawEventEnvelope),
Prompt(RawPrompt),
Result(RawResultEnvelope),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawHello {
pub protocol_version: u32,
pub session_id: String,
pub session_dir: String,
pub workspace_root: String,
pub current_agent: String,
pub model: String,
pub resumed: bool,
pub startup_confirmation_required: bool,
pub sandbox_available: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawEventEnvelope {
pub protocol_version: u32,
pub request_id: String,
#[serde(flatten)]
pub event: RawEvent,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum RawEvent {
AgentStart {
label: Option<String>,
depth: usize,
},
AgentFinish {
label: Option<String>,
depth: usize,
stop_reason: Option<String>,
},
AssistantTextDelta {
label: Option<String>,
depth: usize,
text: String,
},
ThinkingDelta {
label: Option<String>,
depth: usize,
text: String,
},
Info {
label: Option<String>,
depth: usize,
message: String,
},
Error {
label: Option<String>,
depth: usize,
message: String,
},
ToolUseStart {
label: Option<String>,
depth: usize,
name: String,
tool_use_id: String,
},
ToolInputDelta {
label: Option<String>,
depth: usize,
partial_json: String,
},
ToolUseEnd {
label: Option<String>,
depth: usize,
},
ToolResultStart {
label: Option<String>,
depth: usize,
tool_use_id: String,
is_error: bool,
},
ToolResultTextDelta {
label: Option<String>,
depth: usize,
text: String,
},
ToolResultEnd {
label: Option<String>,
depth: usize,
},
ResponseFinish {
label: Option<String>,
depth: usize,
},
Interrupted {
label: Option<String>,
depth: usize,
},
ToolOutput {
tool_name: String,
tool_use_id: String,
stream: String,
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
data_b64: Option<String>,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawPrompt {
pub protocol_version: u32,
pub request_id: String,
pub prompt_id: String,
pub kind: String,
pub message: String,
pub choices: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawResultEnvelope {
pub protocol_version: u32,
pub request_id: String,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<RawServerError>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RawServerError {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ToolOutputEvent {
pub request_id: String,
pub tool_name: String,
pub tool_use_id: String,
pub stream: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_b64: Option<String>,
}
pub trait ToolOutputObserver: Send + Sync {
fn on_tool_output(&self, event: &ToolOutputEvent);
}
pub struct ToolOutputObserverRegistration {
previous: Option<Arc<dyn ToolOutputObserver>>,
}
impl Drop for ToolOutputObserverRegistration {
fn drop(&mut self) {
set_active_tool_output_observer(self.previous.take());
}
}
pub fn install_tool_output_observer(
observer: Option<Arc<dyn ToolOutputObserver>>,
) -> ToolOutputObserverRegistration {
let previous = set_active_tool_output_observer(observer);
ToolOutputObserverRegistration { previous }
}
pub(crate) fn notify_tool_output_observer(event: &ToolOutputEvent) {
let observer = active_tool_output_observer()
.lock()
.expect("tool output observer lock poisoned")
.clone();
if let Some(observer) = observer {
observer.on_tool_output(event);
}
}
pub(crate) fn has_active_tool_output_observer() -> bool {
active_tool_output_observer()
.lock()
.expect("tool output observer lock poisoned")
.is_some()
}
fn active_tool_output_observer() -> &'static StdMutex<Option<Arc<dyn ToolOutputObserver>>> {
static ACTIVE: OnceLock<StdMutex<Option<Arc<dyn ToolOutputObserver>>>> = OnceLock::new();
ACTIVE.get_or_init(|| StdMutex::new(None))
}
fn set_active_tool_output_observer(
observer: Option<Arc<dyn ToolOutputObserver>>,
) -> Option<Arc<dyn ToolOutputObserver>> {
let mut slot = active_tool_output_observer()
.lock()
.expect("tool output observer lock poisoned");
std::mem::replace(&mut *slot, observer)
}