diff --git a/rust/crates/runtime/src/compact.rs b/rust/crates/runtime/src/compact.rs index 9c9f5a8..906d01b 100644 --- a/rust/crates/runtime/src/compact.rs +++ b/rust/crates/runtime/src/compact.rs @@ -577,21 +577,17 @@ mod tests { #[test] fn keeps_previous_compacted_context_when_compacting_again() { - let initial_session = Session { - version: 1, - messages: vec![ - ConversationMessage::user_text("Investigate rust/crates/runtime/src/compact.rs"), - ConversationMessage::assistant(vec![ContentBlock::Text { - text: "I will inspect the compact flow.".to_string(), - }]), - ConversationMessage::user_text( - "Also update rust/crates/runtime/src/conversation.rs", - ), - ConversationMessage::assistant(vec![ContentBlock::Text { - text: "Next: preserve prior summary context during auto compact.".to_string(), - }]), - ], - }; + let mut initial_session = Session::new(); + initial_session.messages = vec![ + ConversationMessage::user_text("Investigate rust/crates/runtime/src/compact.rs"), + ConversationMessage::assistant(vec![ContentBlock::Text { + text: "I will inspect the compact flow.".to_string(), + }]), + ConversationMessage::user_text("Also update rust/crates/runtime/src/conversation.rs"), + ConversationMessage::assistant(vec![ContentBlock::Text { + text: "Next: preserve prior summary context during auto compact.".to_string(), + }]), + ]; let config = CompactionConfig { preserve_recent_messages: 2, max_estimated_tokens: 1, @@ -606,13 +602,9 @@ mod tests { }]), ]); - let second = compact_session( - &Session { - version: 1, - messages: follow_up_messages, - }, - config, - ); + let mut second_session = Session::new(); + second_session.messages = follow_up_messages; + let second = compact_session(&second_session, config); assert!(second .formatted_summary @@ -641,22 +633,20 @@ mod tests { #[test] fn ignores_existing_compacted_summary_when_deciding_to_recompact() { let summary = "Conversation summary:\n- Scope: earlier work preserved.\n- Key timeline:\n - user: large preserved context\n"; - let session = Session { - version: 1, - messages: vec![ - ConversationMessage { - role: MessageRole::System, - blocks: vec![ContentBlock::Text { - text: get_compact_continuation_message(summary, true, true), - }], - usage: None, - }, - ConversationMessage::user_text("tiny"), - ConversationMessage::assistant(vec![ContentBlock::Text { - text: "recent".to_string(), - }]), - ], - }; + let mut session = Session::new(); + session.messages = vec![ + ConversationMessage { + role: MessageRole::System, + blocks: vec![ContentBlock::Text { + text: get_compact_continuation_message(summary, true, true), + }], + usage: None, + }, + ConversationMessage::user_text("tiny"), + ConversationMessage::assistant(vec![ContentBlock::Text { + text: "recent".to_string(), + }]), + ]; assert!(!should_compact( &session, diff --git a/rust/crates/runtime/src/conversation.rs b/rust/crates/runtime/src/conversation.rs index 13c227c..ec3544a 100644 --- a/rust/crates/runtime/src/conversation.rs +++ b/rust/crates/runtime/src/conversation.rs @@ -1,6 +1,7 @@ use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; +use serde_json::{Map, Value}; use telemetry::SessionTracer; use crate::compact::{ @@ -319,13 +320,14 @@ where return Err(error); } }; - let (assistant_message, usage) = match build_assistant_message(events) { - Ok(result) => result, - Err(error) => { - self.record_turn_failed(iterations, &error); - return Err(error); - } - }; + let (assistant_message, usage, turn_prompt_cache_events) = + match build_assistant_message(events) { + Ok(result) => result, + Err(error) => { + self.record_turn_failed(iterations, &error); + return Err(error); + } + }; if let Some(usage) = usage { self.usage_tracker.record(usage); } @@ -397,6 +399,7 @@ where let result_message = match permission_outcome { PermissionOutcome::Allow => { + self.record_tool_started(iterations, &tool_name); let (mut output, mut is_error) = match self.tool_executor.execute(&tool_name, &effective_input) { Ok(output) => (output, false), @@ -439,20 +442,24 @@ where self.session .push_message(result_message.clone()) .map_err(|error| RuntimeError::new(error.to_string()))?; + self.record_tool_finished(iterations, &result_message); tool_results.push(result_message); } } let auto_compaction = self.maybe_auto_compact(); - Ok(TurnSummary { + let summary = TurnSummary { assistant_messages, tool_results, prompt_cache_events, iterations, usage: self.usage_tracker.cumulative_usage(), auto_compaction, - }) + }; + self.record_turn_completed(&summary); + + Ok(summary) } #[must_use] @@ -510,23 +517,112 @@ where }) } - fn record_turn_started(&self, _user_input: &str) {} + fn record_turn_started(&self, user_input: &str) { + let Some(session_tracer) = &self.session_tracer else { + return; + }; + + let mut attributes = Map::new(); + attributes.insert( + "user_input".to_string(), + Value::String(user_input.to_string()), + ); + session_tracer.record("turn_started", attributes); + } fn record_assistant_iteration( &self, - _iteration: usize, - _assistant_message: &ConversationMessage, - _pending_tool_use_count: usize, + iteration: usize, + assistant_message: &ConversationMessage, + pending_tool_use_count: usize, ) { + let Some(session_tracer) = &self.session_tracer else { + return; + }; + + let mut attributes = Map::new(); + attributes.insert("iteration".to_string(), Value::from(iteration as u64)); + attributes.insert( + "assistant_blocks".to_string(), + Value::from(assistant_message.blocks.len() as u64), + ); + attributes.insert( + "pending_tool_use_count".to_string(), + Value::from(pending_tool_use_count as u64), + ); + session_tracer.record("assistant_iteration_completed", attributes); } - fn record_tool_started(&self, _iteration: usize, _tool_name: &str) {} + fn record_tool_started(&self, iteration: usize, tool_name: &str) { + let Some(session_tracer) = &self.session_tracer else { + return; + }; - fn record_tool_finished(&self, _iteration: usize, _result_message: &ConversationMessage) {} + let mut attributes = Map::new(); + attributes.insert("iteration".to_string(), Value::from(iteration as u64)); + attributes.insert( + "tool_name".to_string(), + Value::String(tool_name.to_string()), + ); + session_tracer.record("tool_execution_started", attributes); + } - fn record_turn_completed(&self, _summary: &TurnSummary) {} + fn record_tool_finished(&self, iteration: usize, result_message: &ConversationMessage) { + let Some(session_tracer) = &self.session_tracer else { + return; + }; - fn record_turn_failed(&self, _iteration: usize, _error: &RuntimeError) {} + let Some(ContentBlock::ToolResult { + tool_name, + is_error, + .. + }) = result_message.blocks.first() + else { + return; + }; + + let mut attributes = Map::new(); + attributes.insert("iteration".to_string(), Value::from(iteration as u64)); + attributes.insert("tool_name".to_string(), Value::String(tool_name.clone())); + attributes.insert("is_error".to_string(), Value::Bool(*is_error)); + session_tracer.record("tool_execution_finished", attributes); + } + + fn record_turn_completed(&self, summary: &TurnSummary) { + let Some(session_tracer) = &self.session_tracer else { + return; + }; + + let mut attributes = Map::new(); + attributes.insert( + "iterations".to_string(), + Value::from(summary.iterations as u64), + ); + attributes.insert( + "assistant_messages".to_string(), + Value::from(summary.assistant_messages.len() as u64), + ); + attributes.insert( + "tool_results".to_string(), + Value::from(summary.tool_results.len() as u64), + ); + attributes.insert( + "prompt_cache_events".to_string(), + Value::from(summary.prompt_cache_events.len() as u64), + ); + session_tracer.record("turn_completed", attributes); + } + + fn record_turn_failed(&self, iteration: usize, error: &RuntimeError) { + let Some(session_tracer) = &self.session_tracer else { + return; + }; + + let mut attributes = Map::new(); + attributes.insert("iteration".to_string(), Value::from(iteration as u64)); + attributes.insert("error".to_string(), Value::String(error.to_string())); + session_tracer.record("turn_failed", attributes); + } } #[must_use] @@ -665,8 +761,8 @@ impl ToolExecutor for StaticToolExecutor { mod tests { use super::{ parse_auto_compaction_threshold, ApiClient, ApiRequest, AssistantEvent, - AutoCompactionEvent, ConversationRuntime, RuntimeError, StaticToolExecutor, - DEFAULT_AUTO_COMPACTION_INPUT_TOKENS_THRESHOLD, + AutoCompactionEvent, ConversationRuntime, PromptCacheEvent, RuntimeError, + StaticToolExecutor, DEFAULT_AUTO_COMPACTION_INPUT_TOKENS_THRESHOLD, }; use crate::compact::CompactionConfig; use crate::config::{RuntimeFeatureConfig, RuntimeHookConfig}; @@ -679,7 +775,9 @@ mod tests { use crate::usage::TokenUsage; use std::fs; use std::path::PathBuf; + use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; + use telemetry::{MemoryTelemetrySink, SessionTracer, TelemetryEvent}; struct ScriptedApiClient { call_count: usize, @@ -1217,19 +1315,17 @@ mod tests { } } - let session = Session { - version: 1, - messages: vec![ - crate::session::ConversationMessage::user_text("one"), - crate::session::ConversationMessage::assistant(vec![ContentBlock::Text { - text: "two".to_string(), - }]), - crate::session::ConversationMessage::user_text("three"), - crate::session::ConversationMessage::assistant(vec![ContentBlock::Text { - text: "four".to_string(), - }]), - ], - }; + let mut session = Session::new(); + session.messages = vec![ + crate::session::ConversationMessage::user_text("one"), + crate::session::ConversationMessage::assistant(vec![ContentBlock::Text { + text: "two".to_string(), + }]), + crate::session::ConversationMessage::user_text("three"), + crate::session::ConversationMessage::assistant(vec![ContentBlock::Text { + text: "four".to_string(), + }]), + ]; let mut runtime = ConversationRuntime::new( session, diff --git a/rust/crates/runtime/src/session.rs b/rust/crates/runtime/src/session.rs index f102b3b..f08face 100644 --- a/rust/crates/runtime/src/session.rs +++ b/rust/crates/runtime/src/session.rs @@ -161,8 +161,15 @@ impl Session { let path = path.as_ref(); let contents = fs::read_to_string(path)?; let session = match JsonValue::parse(&contents) { - Ok(value) => Self::from_json(&value)?, + Ok(value) + if value + .as_object() + .is_some_and(|object| object.contains_key("messages")) => + { + Self::from_json(&value)? + } Err(_) => Self::from_jsonl(&contents)?, + Ok(_) => Self::from_jsonl(&contents)?, }; Ok(session.with_persistence_path(path.to_path_buf())) } diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 74a3292..35ad552 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -17,7 +17,7 @@ use std::time::{Duration, Instant, UNIX_EPOCH}; use api::{ resolve_startup_auth_source, AnthropicClient, AuthSource, ContentBlockDelta, InputContentBlock, - InputMessage, JsonlTelemetrySink, MessageRequest, MessageResponse, OutputContentBlock, + InputMessage, MessageRequest, MessageResponse, OutputContentBlock, SessionTracer, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock, }; @@ -3277,8 +3277,7 @@ impl AnthropicRuntimeClient { Ok(Self { runtime: tokio::runtime::Runtime::new()?, client: AnthropicClient::from_auth(resolve_cli_auth_source()?) - .with_base_url(api::read_base_url()) - .with_prompt_cache(PromptCache::new(session_id)), + .with_base_url(api::read_base_url()), model, enable_tools, emit_output, @@ -4037,25 +4036,7 @@ fn response_to_events( Ok(events) } -fn push_prompt_cache_record(client: &AnthropicClient, events: &mut Vec) { - if let Some(event) = client - .take_last_prompt_cache_record() - .and_then(prompt_cache_record_to_runtime_event) - { - events.push(AssistantEvent::PromptCache(event)); - } -} - -fn prompt_cache_record_to_runtime_event(record: PromptCacheRecord) -> Option { - let cache_break = record.cache_break?; - Some(PromptCacheEvent { - unexpected: cache_break.unexpected, - reason: cache_break.reason, - previous_cache_read_input_tokens: cache_break.previous_cache_read_input_tokens, - current_cache_read_input_tokens: cache_break.current_cache_read_input_tokens, - token_drop: cache_break.token_drop, - }) -} +fn push_prompt_cache_record(_client: &AnthropicClient, _events: &mut Vec) {} struct CliToolExecutor { renderer: TerminalRenderer, @@ -4273,19 +4254,18 @@ mod tests { format_permissions_report, format_permissions_switch_report, format_resume_report, format_status_report, format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args, - parse_git_status_branch, parse_git_status_metadata, permission_policy, print_help_to, - push_output_block, render_config_report, render_diff_report, render_memory_report, - render_repl_help, resolve_model_alias, response_to_events, + parse_git_status_branch, parse_git_status_metadata_for, permission_policy, + print_help_to, push_output_block, render_config_report, render_diff_report, + render_memory_report, render_repl_help, resolve_model_alias, response_to_events, resume_supported_slash_commands, run_resume_command, status_context, CliAction, - CliOutputFormat, HookAbortMonitor, InternalPromptProgressEvent, + CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL, create_managed_session_handle, resolve_session_reference, }; use api::{MessageResponse, OutputContentBlock, Usage}; use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission}; use runtime::{ - AssistantEvent, ContentBlock, ConversationMessage, HookAbortSignal, MessageRole, - PermissionMode, Session, + AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode, Session, }; use serde_json::json; use std::fs; @@ -4354,8 +4334,6 @@ mod tests { std::env::set_current_dir(previous).expect("cwd should restore"); result } - use std::sync::mpsc; - #[test] fn defaults_to_repl_when_no_args() { assert_eq!( @@ -4626,7 +4604,12 @@ mod tests { #[test] fn permission_policy_uses_plugin_tool_permissions() { - let policy = permission_policy(PermissionMode::ReadOnly, ®istry_with_plugin_tool()); + let feature_config = runtime::RuntimeFeatureConfig::default(); + let policy = permission_policy( + PermissionMode::ReadOnly, + &feature_config, + ®istry_with_plugin_tool(), + ); let required = policy.required_mode_for("plugin_echo"); assert_eq!(required, PermissionMode::WorkspaceWrite); } @@ -4844,12 +4827,13 @@ mod tests { let _guard = env_lock(); let temp_root = temp_dir(); fs::create_dir_all(&temp_root).expect("root dir"); - let (project_root, branch) = with_current_dir(&temp_root, || { - parse_git_status_metadata(Some( + let (project_root, branch) = parse_git_status_metadata_for( + &temp_root, + Some( "## rcc/cli...origin/rcc/cli M src/main.rs", - )) - }); + ), + ); assert_eq!(branch.as_deref(), Some("rcc/cli")); assert!(project_root.is_none()); fs::remove_dir_all(temp_root).expect("cleanup temp dir"); @@ -5057,7 +5041,7 @@ mod tests { let handle = create_managed_session_handle("session-alpha").expect("jsonl handle"); assert!(handle.path.ends_with("session-alpha.jsonl")); - let legacy_path = workspace.join(".claude/sessions/legacy.json"); + let legacy_path = workspace.join(".claw/sessions/legacy.json"); std::fs::create_dir_all( legacy_path .parent() @@ -5070,7 +5054,10 @@ mod tests { .expect("legacy session should save"); let resolved = resolve_session_reference("legacy").expect("legacy session should resolve"); - assert_eq!(resolved.path, legacy_path); + assert_eq!( + resolved.path.canonicalize().expect("resolved path should exist"), + legacy_path.canonicalize().expect("legacy path should exist") + ); std::env::set_current_dir(previous).expect("restore cwd"); std::fs::remove_dir_all(workspace).expect("workspace should clean up"); @@ -5455,7 +5442,10 @@ mod tests { #[cfg(test)] mod sandbox_report_tests { - use super::format_sandbox_report; + use super::{format_sandbox_report, HookAbortMonitor}; + use runtime::HookAbortSignal; + use std::sync::mpsc; + use std::time::Duration; #[test] fn sandbox_report_renders_expected_fields() {