@@ -24,6 +24,7 @@ use std::collections::{
24
24
HashSet ,
25
25
VecDeque ,
26
26
} ;
27
+ use std:: error:: Error ;
27
28
use std:: io:: {
28
29
IsTerminal ,
29
30
Read ,
@@ -66,6 +67,7 @@ use crossterm::{
66
67
terminal,
67
68
} ;
68
69
use eyre:: {
70
+ Chain ,
69
71
ErrReport ,
70
72
Result ,
71
73
bail,
@@ -160,8 +162,11 @@ use crate::mcp_client::{
160
162
PromptGetResult ,
161
163
} ;
162
164
use crate :: platform:: Context ;
163
- use crate :: telemetry:: TelemetryThread ;
164
165
use crate :: telemetry:: core:: ToolUseEventBuilder ;
166
+ use crate :: telemetry:: {
167
+ TelemetryResult ,
168
+ TelemetryThread ,
169
+ } ;
165
170
use crate :: util:: CLI_BINARY_NAME ;
166
171
167
172
/// Help text for the compact command
@@ -843,7 +848,7 @@ impl ChatContext {
843
848
} => {
844
849
let tool_uses_clone = tool_uses. clone ( ) ;
845
850
tokio:: select! {
846
- res = self . handle_input( telemetry, input, tool_uses, pending_tool_index) => res,
851
+ res = self . handle_input( telemetry, database , input, tool_uses, pending_tool_index) => res,
847
852
Ok ( _) = ctrl_c_stream => Err ( ChatError :: Interrupted { tool_uses: tool_uses_clone } )
848
853
}
849
854
} ,
@@ -856,7 +861,7 @@ impl ChatContext {
856
861
} => {
857
862
let tool_uses_clone = tool_uses. clone ( ) ;
858
863
tokio:: select! {
859
- res = self . compact_history( telemetry, tool_uses, pending_tool_index, prompt, show_summary, help) => res,
864
+ res = self . compact_history( telemetry, database , tool_uses, pending_tool_index, prompt, show_summary, help) => res,
860
865
Ok ( _) = ctrl_c_stream => Err ( ChatError :: Interrupted { tool_uses: tool_uses_clone } )
861
866
}
862
867
} ,
@@ -875,19 +880,24 @@ impl ChatContext {
875
880
} ,
876
881
ChatState :: HandleResponseStream ( response) => tokio:: select! {
877
882
res = self . handle_response( database, telemetry, response) => res,
878
- Ok ( _) = ctrl_c_stream => Err ( ChatError :: Interrupted { tool_uses: None } )
883
+ Ok ( _) = ctrl_c_stream => {
884
+ self . send_chat_telemetry( database, telemetry, None , TelemetryResult :: Cancelled , None ) . await ;
885
+
886
+ Err ( ChatError :: Interrupted { tool_uses: None } )
887
+ }
879
888
} ,
880
889
ChatState :: Exit => return Ok ( ( ) ) ,
881
890
} ;
882
891
883
- next_state = Some ( self . handle_state_execution_result ( database, result) . await ?) ;
892
+ next_state = Some ( self . handle_state_execution_result ( telemetry , database, result) . await ?) ;
884
893
}
885
894
}
886
895
887
896
/// Handles the result of processing a [ChatState], returning the next [ChatState] to change
888
897
/// to.
889
898
async fn handle_state_execution_result (
890
899
& mut self ,
900
+ telemetry : & TelemetryThread ,
891
901
database : & mut Database ,
892
902
result : Result < ChatState , ChatError > ,
893
903
) -> Result < ChatState , ChatError > {
@@ -896,6 +906,9 @@ impl ChatContext {
896
906
match result {
897
907
Ok ( state) => Ok ( state) ,
898
908
Err ( e) => {
909
+ self . send_error_telemetry ( database, telemetry, get_error_string ( & e) )
910
+ . await ;
911
+
899
912
macro_rules! print_err {
900
913
( $prepend_msg: expr, $err: expr) => { {
901
914
queue!(
@@ -1027,9 +1040,11 @@ impl ChatContext {
1027
1040
/// model.
1028
1041
///
1029
1042
/// The last two user messages in the history are not included in the compaction process.
1043
+ #[ allow( clippy:: too_many_arguments) ]
1030
1044
async fn compact_history (
1031
1045
& mut self ,
1032
1046
telemetry : & TelemetryThread ,
1047
+ database : & mut Database ,
1033
1048
tool_uses : Option < Vec < QueuedTool > > ,
1034
1049
pending_tool_index : Option < usize > ,
1035
1050
custom_prompt : Option < String > ,
@@ -1085,32 +1100,43 @@ impl ChatContext {
1085
1100
// retry except with less context included.
1086
1101
let response = match response {
1087
1102
Ok ( res) => res,
1088
- Err ( e) => match e {
1089
- crate :: api_client:: ApiClientError :: ContextWindowOverflow => {
1090
- self . conversation_state . clear ( true ) ;
1091
- if self . interactive {
1092
- self . spinner . take ( ) ;
1093
- execute ! (
1094
- self . output,
1095
- terminal:: Clear ( terminal:: ClearType :: CurrentLine ) ,
1096
- cursor:: MoveToColumn ( 0 ) ,
1097
- style:: SetForegroundColor ( Color :: Yellow ) ,
1098
- style:: Print (
1099
- "The context window usage has overflowed. Clearing the conversation history.\n \n "
1100
- ) ,
1101
- style:: SetAttribute ( Attribute :: Reset )
1102
- ) ?;
1103
- }
1104
- return Ok ( ChatState :: PromptUser {
1105
- tool_uses,
1106
- pending_tool_index,
1107
- skip_printing_tools : true ,
1108
- } ) ;
1109
- } ,
1110
- e => return Err ( e. into ( ) ) ,
1103
+ Err ( e) => {
1104
+ self . send_chat_telemetry (
1105
+ database,
1106
+ telemetry,
1107
+ None ,
1108
+ TelemetryResult :: Failed ,
1109
+ Some ( get_error_string ( & e) ) ,
1110
+ )
1111
+ . await ;
1112
+ match e {
1113
+ crate :: api_client:: ApiClientError :: ContextWindowOverflow => {
1114
+ self . conversation_state . clear ( true ) ;
1115
+ if self . interactive {
1116
+ self . spinner . take ( ) ;
1117
+ execute ! (
1118
+ self . output,
1119
+ terminal:: Clear ( terminal:: ClearType :: CurrentLine ) ,
1120
+ cursor:: MoveToColumn ( 0 ) ,
1121
+ style:: SetForegroundColor ( Color :: Yellow ) ,
1122
+ style:: Print (
1123
+ "The context window usage has overflowed. Clearing the conversation history.\n \n "
1124
+ ) ,
1125
+ style:: SetAttribute ( Attribute :: Reset )
1126
+ ) ?;
1127
+ }
1128
+ return Ok ( ChatState :: PromptUser {
1129
+ tool_uses,
1130
+ pending_tool_index,
1131
+ skip_printing_tools : true ,
1132
+ } ) ;
1133
+ } ,
1134
+ e => return Err ( e. into ( ) ) ,
1135
+ }
1111
1136
} ,
1112
1137
} ;
1113
1138
1139
+ let request_id = response. request_id ( ) . map ( |s| s. to_string ( ) ) ;
1114
1140
let summary = {
1115
1141
let mut parser = ResponseParser :: new ( response) ;
1116
1142
loop {
@@ -1123,6 +1149,14 @@ impl ChatContext {
1123
1149
if let Some ( request_id) = & err. request_id {
1124
1150
self . failed_request_ids . push ( request_id. clone ( ) ) ;
1125
1151
} ;
1152
+ self . send_chat_telemetry (
1153
+ database,
1154
+ telemetry,
1155
+ err. request_id . clone ( ) ,
1156
+ TelemetryResult :: Failed ,
1157
+ Some ( get_error_string ( & err) ) ,
1158
+ )
1159
+ . await ;
1126
1160
return Err ( err. into ( ) ) ;
1127
1161
} ,
1128
1162
}
@@ -1138,16 +1172,8 @@ impl ChatContext {
1138
1172
cursor:: Show
1139
1173
) ?;
1140
1174
}
1141
-
1142
- if let Some ( message_id) = self . conversation_state . message_id ( ) {
1143
- telemetry
1144
- . send_chat_added_message (
1145
- self . conversation_state . conversation_id ( ) . to_owned ( ) ,
1146
- message_id. to_owned ( ) ,
1147
- self . conversation_state . context_message_length ( ) ,
1148
- )
1149
- . ok ( ) ;
1150
- }
1175
+ self . send_chat_telemetry ( database, telemetry, request_id, TelemetryResult :: Succeeded , None )
1176
+ . await ;
1151
1177
1152
1178
self . conversation_state . replace_history_with_summary ( summary. clone ( ) ) ;
1153
1179
@@ -1308,6 +1334,7 @@ impl ChatContext {
1308
1334
async fn handle_input (
1309
1335
& mut self ,
1310
1336
telemetry : & TelemetryThread ,
1337
+ database : & mut Database ,
1311
1338
mut user_input : String ,
1312
1339
tool_uses : Option < Vec < QueuedTool > > ,
1313
1340
pending_tool_index : Option < usize > ,
@@ -1440,6 +1467,7 @@ impl ChatContext {
1440
1467
} => {
1441
1468
self . compact_history (
1442
1469
telemetry,
1470
+ database,
1443
1471
Some ( tool_uses) ,
1444
1472
pending_tool_index,
1445
1473
prompt,
@@ -3261,6 +3289,15 @@ impl ChatContext {
3261
3289
self . failed_request_ids . push ( request_id. clone ( ) ) ;
3262
3290
} ;
3263
3291
3292
+ self . send_chat_telemetry (
3293
+ database,
3294
+ telemetry,
3295
+ recv_error. request_id . clone ( ) ,
3296
+ TelemetryResult :: Failed ,
3297
+ Some ( get_error_string ( & recv_error) ) ,
3298
+ )
3299
+ . await ;
3300
+
3264
3301
match recv_error. source {
3265
3302
RecvErrorKind :: StreamTimeout { source, duration } => {
3266
3303
error ! (
@@ -3395,15 +3432,8 @@ impl ChatContext {
3395
3432
}
3396
3433
3397
3434
if ended {
3398
- if let Some ( message_id) = self . conversation_state . message_id ( ) {
3399
- telemetry
3400
- . send_chat_added_message (
3401
- self . conversation_state . conversation_id ( ) . to_owned ( ) ,
3402
- message_id. to_owned ( ) ,
3403
- self . conversation_state . context_message_length ( ) ,
3404
- )
3405
- . ok ( ) ;
3406
- }
3435
+ self . send_chat_telemetry ( database, telemetry, request_id, TelemetryResult :: Succeeded , None )
3436
+ . await ;
3407
3437
3408
3438
if self . interactive
3409
3439
&& database
@@ -3691,6 +3721,41 @@ impl ChatContext {
3691
3721
3692
3722
Ok ( ( ) )
3693
3723
}
3724
+
3725
+ async fn send_chat_telemetry (
3726
+ & self ,
3727
+ database : & Database ,
3728
+ telemetry : & TelemetryThread ,
3729
+ request_id : Option < String > ,
3730
+ result : TelemetryResult ,
3731
+ reason : Option < String > ,
3732
+ ) {
3733
+ telemetry
3734
+ . send_chat_added_message (
3735
+ database,
3736
+ self . conversation_state . conversation_id ( ) . to_owned ( ) ,
3737
+ self . conversation_state . message_id ( ) . map ( |s| s. to_owned ( ) ) ,
3738
+ request_id,
3739
+ self . conversation_state . context_message_length ( ) ,
3740
+ result,
3741
+ reason,
3742
+ )
3743
+ . await
3744
+ . ok ( ) ;
3745
+ }
3746
+
3747
+ async fn send_error_telemetry ( & self , database : & Database , telemetry : & TelemetryThread , reason : String ) {
3748
+ telemetry
3749
+ . send_response_error (
3750
+ database,
3751
+ self . conversation_state . conversation_id ( ) . to_owned ( ) ,
3752
+ self . conversation_state . context_message_length ( ) ,
3753
+ TelemetryResult :: Failed ,
3754
+ Some ( reason) ,
3755
+ )
3756
+ . await
3757
+ . ok ( ) ;
3758
+ }
3694
3759
}
3695
3760
3696
3761
/// Prints hook configuration grouped by trigger: conversation session start or per user message
@@ -3789,6 +3854,22 @@ fn create_stream(model_responses: serde_json::Value) -> StreamingClient {
3789
3854
StreamingClient :: mock ( mock)
3790
3855
}
3791
3856
3857
+ /// Returns surface error + root cause as a string. If there is only one error
3858
+ /// in the chain, return that as a string.
3859
+ fn get_error_string ( error : & ( dyn Error + ' static ) ) -> String {
3860
+ let err_chain = Chain :: new ( error) ;
3861
+
3862
+ if err_chain. len ( ) > 1 {
3863
+ format ! (
3864
+ "'{}' caused by: {}" ,
3865
+ error,
3866
+ err_chain. last( ) . map_or( "UNKNOWN" . to_string( ) , |e| e. to_string( ) )
3867
+ )
3868
+ } else {
3869
+ error. to_string ( )
3870
+ }
3871
+ }
3872
+
3792
3873
#[ cfg( test) ]
3793
3874
mod tests {
3794
3875
use super :: * ;
0 commit comments