1use llm::{ContentBlock, LlmCallPurpose, LlmError, MessageId, ModelIdentity, StopReason, TokenUsage};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
7#[serde(tag = "status", rename_all = "snake_case")]
8pub enum TurnOutcome {
9 Completed,
10 Cancelled,
11 Failed { message_id: MessageId, error: String },
12}
13
14impl TurnOutcome {
15 pub fn failed(error: impl Into<String>) -> Self {
16 Self::Failed { message_id: MessageId::new(), error: error.into() }
17 }
18}
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
22#[serde(tag = "status", rename_all = "snake_case")]
23pub enum LlmCallOutcome {
24 Completed {
25 stop_reason: Option<StopReason>,
26 usage: Option<TokenUsage>,
27 },
28 Failed {
29 error: String,
30 will_retry: bool,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
32 http_status: Option<u16>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 provider_request_id: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 provider_error_code: Option<String>,
37 },
38 Cancelled,
39}
40
41impl LlmCallOutcome {
42 pub fn failed(error: impl Into<String>, will_retry: bool) -> Self {
43 Self::Failed {
44 error: error.into(),
45 will_retry,
46 http_status: None,
47 provider_request_id: None,
48 provider_error_code: None,
49 }
50 }
51
52 pub fn from_llm_error(error: &LlmError, will_retry: bool) -> Self {
53 let Some(provider) = error.provider() else {
54 return Self::failed(error.to_string(), will_retry);
55 };
56 Self::Failed {
57 error: provider.to_string(),
58 will_retry,
59 http_status: provider.http_status,
60 provider_request_id: provider.request_id.clone(),
61 provider_error_code: provider.code.clone(),
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct RetryInfo {
69 pub attempt: u32,
70 pub max_attempts: u32,
71 pub delay_ms: u64,
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
75#[serde(tag = "type", rename_all = "snake_case")]
76pub enum TurnEvent {
77 Started {
78 #[serde(default, skip_serializing_if = "Vec::is_empty")]
79 content: Vec<ContentBlock>,
80 },
81 UserMessageInserted {
82 message_id: MessageId,
83 },
84 UserMessageDiscarded {
85 message_id: MessageId,
86 },
87 RetryScheduled {
88 purpose: LlmCallPurpose,
89 attempt: u32,
90 max_attempts: u32,
91 delay_ms: u64,
92 },
93 LlmCallStarted {
94 purpose: LlmCallPurpose,
95 model: ModelIdentity,
96 display_name: String,
97 attempt: u32,
99 max_attempts: u32,
100 },
101 LlmCallEnded {
102 purpose: LlmCallPurpose,
103 outcome: LlmCallOutcome,
104 },
105 AutoContinue {
106 attempt: u32,
107 max_attempts: u32,
108 message_id: MessageId,
109 content: Vec<ContentBlock>,
110 },
111 Ended {
112 outcome: TurnOutcome,
113 },
114}
115
116impl TurnEvent {
117 pub fn retry_info(&self) -> Option<RetryInfo> {
118 match self {
119 Self::RetryScheduled { attempt, max_attempts, delay_ms, .. } => {
120 Some(RetryInfo { attempt: *attempt, max_attempts: *max_attempts, delay_ms: *delay_ms })
121 }
122 _ => None,
123 }
124 }
125}