Skip to main content

lash_remote_protocol/
turn_result.rs

1//! Turn result envelopes: the turn result itself, outcomes, stops, assistant
2//! output, usage/execution summaries, tool-call summaries, issues, and causal
3//! references.
4
5use std::collections::HashMap;
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use crate::ensure_protocol_version;
11use crate::llm::{RemoteLlmTerminalReason, RemoteProviderFailureKind};
12use crate::registry_errors::{RemoteProtocolError, require_non_empty};
13use crate::turn_control::RemoteTurnCancellationEvidence;
14use crate::usage_activity::{RemoteTokenLedgerEntry, RemoteTurnActivity, RemoteUsage};
15
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
17pub struct RemoteTurnResult {
18    pub protocol_version: u32,
19    pub session_id: String,
20    pub turn_id: String,
21    pub status: RemoteTurnStatus,
22    pub outcome: RemoteTurnOutcome,
23    /// Present exactly when `status == cancelled`.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub cancellation: Option<RemoteTurnCancellationEvidence>,
26    pub assistant_output: RemoteAssistantOutput,
27    #[serde(default)]
28    pub usage: RemoteTurnUsageSummary,
29    #[serde(default)]
30    pub execution: RemoteExecutionSummary,
31    #[serde(default, skip_serializing_if = "Vec::is_empty")]
32    pub tool_calls: Vec<RemoteToolCallSummary>,
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub issues: Vec<RemoteTurnIssue>,
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub activities: Vec<RemoteTurnActivity>,
37    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
38    pub metadata: HashMap<String, serde_json::Value>,
39}
40
41impl RemoteTurnResult {
42    pub fn validate(&self) -> Result<(), RemoteProtocolError> {
43        ensure_protocol_version(self.protocol_version)?;
44        require_non_empty("RemoteTurnResult", "session_id", &self.session_id)?;
45        require_non_empty("RemoteTurnResult", "turn_id", &self.turn_id)?;
46        if (self.status == RemoteTurnStatus::Cancelled) != self.cancellation.is_some() {
47            return Err(RemoteProtocolError::InvalidEnvelope {
48                type_name: "RemoteTurnResult",
49                message: "cancellation evidence must be present if and only if status is cancelled"
50                    .to_string(),
51            });
52        }
53        if let Some(cancellation) = self.cancellation.as_ref() {
54            cancellation.validate()?;
55        }
56        for activity in &self.activities {
57            if activity.protocol_version != self.protocol_version {
58                return Err(RemoteProtocolError::MismatchedNestedProtocolVersion {
59                    parent: "RemoteTurnResult",
60                    child: "activities",
61                    parent_version: self.protocol_version,
62                    child_version: activity.protocol_version,
63                });
64            }
65            activity.validate()?;
66        }
67        Ok(())
68    }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72#[serde(tag = "type", rename_all = "snake_case")]
73pub enum RemoteCausalRef {
74    Turn {
75        session_id: String,
76        turn_id: String,
77    },
78    Effect {
79        session_id: String,
80        #[serde(default, skip_serializing_if = "Option::is_none")]
81        turn_id: Option<String>,
82        effect_id: String,
83    },
84    ToolCall {
85        session_id: String,
86        call_id: String,
87    },
88    Process {
89        process_id: String,
90    },
91    ProcessEvent {
92        process_id: String,
93        sequence: u64,
94    },
95    TriggerOccurrence {
96        occurrence_id: String,
97        #[serde(default, skip_serializing_if = "Option::is_none")]
98        subscription_id: Option<String>,
99        #[serde(default, skip_serializing_if = "Option::is_none")]
100        subscription_incarnation: Option<String>,
101        #[serde(default, skip_serializing_if = "Option::is_none")]
102        subscription_revision: Option<u64>,
103    },
104    SessionNode {
105        session_id: String,
106        node_id: String,
107    },
108}
109
110#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
111#[serde(rename_all = "snake_case")]
112pub enum RemoteTurnStatus {
113    #[default]
114    Completed,
115    Failed,
116    Cancelled,
117    InProgress,
118}
119
120#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
121#[serde(tag = "type", rename_all = "snake_case")]
122pub enum RemoteTurnOutcome {
123    Finished { finish: RemoteTurnFinish },
124    AgentFrameSwitch { frame_id: String, task: String },
125    Stopped { stop: RemoteTurnStop },
126}
127
128impl Default for RemoteTurnOutcome {
129    fn default() -> Self {
130        Self::Stopped {
131            stop: RemoteTurnStop::Incomplete,
132        }
133    }
134}
135
136#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
137#[serde(tag = "type", rename_all = "snake_case")]
138pub enum RemoteTurnFinish {
139    AssistantMessage {
140        text: String,
141    },
142    FinalValue {
143        value: serde_json::Value,
144    },
145    ToolValue {
146        tool_name: String,
147        value: serde_json::Value,
148    },
149}
150
151#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
152#[serde(tag = "type", rename_all = "snake_case")]
153pub enum RemoteTurnStop {
154    Cancelled,
155    Incomplete,
156    InvalidInput,
157    MaxTurns,
158    ToolFailure,
159    ProviderError,
160    PluginAbort,
161    RuntimeError,
162    SubmittedError {
163        value: serde_json::Value,
164    },
165    ToolError {
166        tool_name: String,
167        value: serde_json::Value,
168    },
169}
170
171#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
172pub struct RemoteAssistantOutput {
173    #[serde(default)]
174    pub safe_text: String,
175    #[serde(default)]
176    pub raw_text: String,
177    #[serde(default)]
178    pub state: RemoteAssistantOutputState,
179}
180
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
182#[serde(rename_all = "snake_case")]
183pub enum RemoteAssistantOutputState {
184    #[default]
185    Usable,
186    EmptyOutput,
187    TracebackOnly,
188    RecoveredFromError,
189}
190
191#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
192pub struct RemoteTurnUsageSummary {
193    #[serde(default)]
194    pub parent: RemoteUsage,
195    #[serde(default, skip_serializing_if = "Vec::is_empty")]
196    pub children: Vec<RemoteTokenLedgerEntry>,
197    #[serde(default)]
198    pub total: RemoteUsage,
199}
200
201#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
202pub struct RemoteExecutionSummary {
203    #[serde(default)]
204    pub had_tool_calls: bool,
205    #[serde(default)]
206    pub had_code_execution: bool,
207    /// Wall-clock turn start (epoch milliseconds), measured from turn claim.
208    /// `0` when the producer predates the field.
209    #[serde(default)]
210    pub started_at_ms: u64,
211    /// Whole-turn duration in milliseconds (claim → final commit). `0` when
212    /// the producer predates the field.
213    #[serde(default)]
214    pub duration_ms: u64,
215}
216
217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
218pub struct RemoteToolCallSummary {
219    #[serde(default, skip_serializing_if = "Option::is_none")]
220    pub call_id: Option<String>,
221    pub tool_name: String,
222    #[serde(default)]
223    pub args: serde_json::Value,
224    pub outcome: RemoteToolCallOutcome,
225    pub duration_ms: u64,
226}
227
228#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
229#[serde(tag = "status", content = "payload", rename_all = "snake_case")]
230pub enum RemoteToolCallOutcome {
231    Success(serde_json::Value),
232    Failure(serde_json::Value),
233    Cancelled(serde_json::Value),
234}
235
236#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
237pub struct RemoteTurnIssue {
238    pub kind: String,
239    #[serde(default, skip_serializing_if = "Option::is_none")]
240    pub code: Option<String>,
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub terminal_reason: Option<RemoteLlmTerminalReason>,
243    pub message: String,
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    pub raw: Option<String>,
246    /// Typed retryability signal; `None` when the source did not know.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub retryable: Option<bool>,
249    /// Typed provider-failure classification, present only for classified
250    /// LLM provider/transport failures.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub provider_failure_kind: Option<RemoteProviderFailureKind>,
253}