Files
huskies/server/src/llm/providers/claude_code/events/mod.rs
T

88 lines
3.3 KiB
Rust
Raw Normal View History

2026-04-29 17:45:59 +00:00
//! Claude Code event handling — dispatches JSON events from the CLI to message channels.
mod stream;
#[cfg(test)]
mod tests;
use super::parse::{parse_assistant_message, parse_tool_results};
use crate::llm::types::Message;
use crate::slog;
use std::sync::atomic::{AtomicBool, Ordering};
use stream::handle_stream_event;
/// Dispatch a top-level JSON event from the Claude Code CLI.
///
/// Routes the event to the appropriate handler based on `type`, emitting tokens,
/// thinking output, activity signals, and parsed messages to their respective channels.
/// Returns `true` only for `"result"` events, which signal that the CLI turn is complete.
pub(super) fn process_json_event(
json: &serde_json::Value,
token_tx: &tokio::sync::mpsc::UnboundedSender<String>,
thinking_tx: &tokio::sync::mpsc::UnboundedSender<String>,
activity_tx: &tokio::sync::mpsc::UnboundedSender<String>,
msg_tx: &std::sync::mpsc::Sender<Message>,
sid_tx: &mut Option<tokio::sync::oneshot::Sender<String>>,
auth_failed: &AtomicBool,
) -> bool {
let event_type = match json.get("type").and_then(|t| t.as_str()) {
Some(t) => t,
None => return false,
};
// Capture session_id from the first event that carries it
if let Some(tx) = sid_tx.take() {
if let Some(sid) = json.get("session_id").and_then(|s| s.as_str()) {
slog!("[pty-debug] CAPTURED session_id: {}", sid);
let _ = tx.send(sid.to_string());
} else {
*sid_tx = Some(tx);
}
}
// Detect authentication_failed at the top level of any event.
if json.get("error").and_then(|e| e.as_str()) == Some("authentication_failed") {
slog!("[pty-debug] Detected authentication_failed error");
auth_failed.store(true, Ordering::Relaxed);
}
match event_type {
"stream_event" => {
if let Some(event) = json.get("event") {
handle_stream_event(event, token_tx, thinking_tx, activity_tx);
}
false
}
"assistant" => {
if let Some(message) = json.get("message")
&& let Some(content) = message.get("content").and_then(|c| c.as_array())
{
// Fire activity signals for tool_use blocks as a fallback path.
// The primary path is via stream_event → content_block_start (real-time),
// but assistant events also carry tool_use blocks and serve as a reliable
// backup if stream_event delivery is delayed or missed.
for block in content {
if block.get("type").and_then(|t| t.as_str()) == Some("tool_use")
&& let Some(name) = block.get("name").and_then(|n| n.as_str())
{
let _ = activity_tx.send(name.to_string());
}
}
parse_assistant_message(content, msg_tx);
}
false
}
"user" => {
if let Some(message) = json.get("message")
&& let Some(content) = message.get("content").and_then(|c| c.as_array())
{
parse_tool_results(content, msg_tx);
}
false
}
"result" => true,
// system, rate_limit_event, and unknown types are no-ops
_ => false,
}
}