huskies: merge 1186 story compact chat command: distill session context deterministically, then reset with a seed

This commit is contained in:
Huskies Agent
2026-07-17 12:05:04 +00:00
parent b241661941
commit 043c77f077
29 changed files with 1051 additions and 6 deletions
@@ -6,6 +6,7 @@ mod stream;
mod tests;
use super::parse::{parse_assistant_message, parse_tool_results};
use crate::agents::TokenUsage;
use crate::llm::types::Message;
use crate::slog;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -16,6 +17,7 @@ use stream::handle_stream_event;
/// 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.
#[allow(clippy::too_many_arguments)]
pub(super) fn process_json_event(
json: &serde_json::Value,
token_tx: &tokio::sync::mpsc::UnboundedSender<String>,
@@ -23,6 +25,7 @@ pub(super) fn process_json_event(
activity_tx: &tokio::sync::mpsc::UnboundedSender<String>,
msg_tx: &std::sync::mpsc::Sender<Message>,
sid_tx: &mut Option<tokio::sync::oneshot::Sender<String>>,
usage_tx: &mut Option<tokio::sync::oneshot::Sender<TokenUsage>>,
auth_failed: &AtomicBool,
) -> bool {
let event_type = match json.get("type").and_then(|t| t.as_str()) {
@@ -80,7 +83,14 @@ pub(super) fn process_json_event(
}
false
}
"result" => true,
"result" => {
if let Some(tx) = usage_tx.take()
&& let Some(usage) = TokenUsage::from_result_event(json)
{
let _ = tx.send(usage);
}
true
}
// system, rate_limit_event, and unknown types are no-ops
_ => false,
}
@@ -202,10 +202,68 @@ fn process_json_event_result_returns_true() {
&act_tx,
&msg_tx,
&mut sid_tx_opt,
&mut None,
&AtomicBool::new(false),
));
}
#[test]
fn process_json_event_result_captures_usage() {
let (tok_tx, _tok_rx, thi_tx, _thi_rx, act_tx, _act_rx, msg_tx, _msg_rx) = make_channels();
let mut sid_tx = None::<tokio::sync::oneshot::Sender<String>>;
let (usage_tx, mut usage_rx) = tokio::sync::oneshot::channel::<crate::agents::TokenUsage>();
let mut usage_tx_opt = Some(usage_tx);
let json = json!({
"type": "result",
"subtype": "success",
"total_cost_usd": 0.42,
"usage": {
"input_tokens": 10,
"output_tokens": 20,
"cache_creation_input_tokens": 100,
"cache_read_input_tokens": 60000
}
});
assert!(process_json_event(
&json,
&tok_tx,
&thi_tx,
&act_tx,
&msg_tx,
&mut sid_tx,
&mut usage_tx_opt,
&AtomicBool::new(false),
));
assert!(usage_tx_opt.is_none(), "usage_tx should be consumed");
let usage = usage_rx.try_recv().unwrap();
assert_eq!(usage.cache_read_input_tokens, 60000);
}
#[test]
fn process_json_event_result_without_usage_sends_nothing() {
let (tok_tx, _tok_rx, thi_tx, _thi_rx, act_tx, _act_rx, msg_tx, _msg_rx) = make_channels();
let mut sid_tx = None::<tokio::sync::oneshot::Sender<String>>;
let (usage_tx, mut usage_rx) = tokio::sync::oneshot::channel::<crate::agents::TokenUsage>();
let mut usage_tx_opt = Some(usage_tx);
let json = json!({"type": "result", "subtype": "success"});
assert!(process_json_event(
&json,
&tok_tx,
&thi_tx,
&act_tx,
&msg_tx,
&mut sid_tx,
&mut usage_tx_opt,
&AtomicBool::new(false),
));
// The result event carried no "usage" field, so the sender is dropped
// without sending — the receiver observes a closed channel, not a value.
assert!(
usage_rx.try_recv().is_err(),
"no usage field means nothing sent"
);
}
#[test]
fn process_json_event_system_returns_false() {
let (tok_tx, _tok_rx, thi_tx, _thi_rx, act_tx, _act_rx, msg_tx, _msg_rx) = make_channels();
@@ -218,6 +276,7 @@ fn process_json_event_system_returns_false() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
}
@@ -234,6 +293,7 @@ fn process_json_event_rate_limit_returns_false() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
}
@@ -250,6 +310,7 @@ fn process_json_event_unknown_type_returns_false() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
}
@@ -266,6 +327,7 @@ fn process_json_event_no_type_returns_false() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
}
@@ -283,6 +345,7 @@ fn process_json_event_captures_session_id() {
&act_tx,
&msg_tx,
&mut sid_tx_opt,
&mut None,
&AtomicBool::new(false),
);
// sid_tx should have been consumed
@@ -304,6 +367,7 @@ fn process_json_event_preserves_sid_tx_if_no_session_id() {
&act_tx,
&msg_tx,
&mut sid_tx_opt,
&mut None,
&AtomicBool::new(false),
);
// sid_tx should still be present since no session_id in event
@@ -329,6 +393,7 @@ fn process_json_event_stream_event_forwards_token() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(tok_tx);
@@ -364,6 +429,7 @@ fn process_json_event_stream_event_tool_use_fires_activity() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(act_tx);
@@ -397,6 +463,7 @@ fn process_json_event_assistant_with_tool_use_fires_activity() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(act_tx);
@@ -430,6 +497,7 @@ fn process_json_event_assistant_with_multiple_tool_uses_fires_all_activities() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(act_tx);
@@ -460,6 +528,7 @@ fn process_json_event_assistant_text_only_no_activity() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(act_tx);
@@ -490,6 +559,7 @@ fn process_json_event_assistant_event_parses_message() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(msg_tx);
@@ -515,6 +585,7 @@ fn process_json_event_user_event_parses_tool_results() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(msg_tx);
@@ -539,6 +610,7 @@ fn process_json_event_assistant_without_content_array_is_noop() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(msg_tx);
@@ -558,6 +630,7 @@ fn process_json_event_user_without_content_array_is_noop() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&AtomicBool::new(false),
));
drop(msg_tx);
@@ -584,6 +657,7 @@ fn process_json_event_detects_authentication_failed() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&auth_failed,
));
assert!(auth_failed.load(Ordering::Relaxed));
@@ -607,6 +681,7 @@ fn process_json_event_no_auth_failed_for_normal_events() {
&act_tx,
&msg_tx,
&mut sid_tx,
&mut None,
&auth_failed,
));
assert!(!auth_failed.load(Ordering::Relaxed));