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    },
100    SessionNode {
101        session_id: String,
102        node_id: String,
103    },
104}
105
106#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
107#[serde(rename_all = "snake_case")]
108pub enum RemoteTurnStatus {
109    #[default]
110    Completed,
111    Failed,
112    Cancelled,
113    InProgress,
114}
115
116#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
117#[serde(tag = "type", rename_all = "snake_case")]
118pub enum RemoteTurnOutcome {
119    Finished { finish: RemoteTurnFinish },
120    AgentFrameSwitch { frame_id: String, task: String },
121    Stopped { stop: RemoteTurnStop },
122}
123
124impl Default for RemoteTurnOutcome {
125    fn default() -> Self {
126        Self::Stopped {
127            stop: RemoteTurnStop::Incomplete,
128        }
129    }
130}
131
132#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
133#[serde(tag = "type", rename_all = "snake_case")]
134pub enum RemoteTurnFinish {
135    AssistantMessage {
136        text: String,
137    },
138    FinalValue {
139        value: serde_json::Value,
140    },
141    ToolValue {
142        tool_name: String,
143        value: serde_json::Value,
144    },
145}
146
147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
148#[serde(tag = "type", rename_all = "snake_case")]
149pub enum RemoteTurnStop {
150    Cancelled,
151    Incomplete,
152    InvalidInput,
153    MaxTurns,
154    ToolFailure,
155    ProviderError,
156    PluginAbort,
157    RuntimeError,
158    SubmittedError {
159        value: serde_json::Value,
160    },
161    ToolError {
162        tool_name: String,
163        value: serde_json::Value,
164    },
165}
166
167#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
168pub struct RemoteAssistantOutput {
169    #[serde(default)]
170    pub safe_text: String,
171    #[serde(default)]
172    pub raw_text: String,
173    #[serde(default)]
174    pub state: RemoteAssistantOutputState,
175}
176
177#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
178#[serde(rename_all = "snake_case")]
179pub enum RemoteAssistantOutputState {
180    #[default]
181    Usable,
182    EmptyOutput,
183    TracebackOnly,
184    RecoveredFromError,
185}
186
187#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
188pub struct RemoteTurnUsageSummary {
189    #[serde(default)]
190    pub parent: RemoteUsage,
191    #[serde(default, skip_serializing_if = "Vec::is_empty")]
192    pub children: Vec<RemoteTokenLedgerEntry>,
193    #[serde(default)]
194    pub total: RemoteUsage,
195}
196
197#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
198pub struct RemoteExecutionSummary {
199    #[serde(default)]
200    pub had_tool_calls: bool,
201    #[serde(default)]
202    pub had_code_execution: bool,
203    /// Wall-clock turn start (epoch milliseconds), measured from turn claim.
204    /// `0` when the producer predates the field.
205    #[serde(default)]
206    pub started_at_ms: u64,
207    /// Whole-turn duration in milliseconds (claim → final commit). `0` when
208    /// the producer predates the field.
209    #[serde(default)]
210    pub duration_ms: u64,
211}
212
213#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
214pub struct RemoteToolCallSummary {
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub call_id: Option<String>,
217    pub tool_name: String,
218    #[serde(default)]
219    pub args: serde_json::Value,
220    pub outcome: RemoteToolCallOutcome,
221    pub duration_ms: u64,
222}
223
224#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
225#[serde(tag = "status", content = "payload", rename_all = "snake_case")]
226pub enum RemoteToolCallOutcome {
227    Success(serde_json::Value),
228    Failure(serde_json::Value),
229    Cancelled(serde_json::Value),
230}
231
232#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
233pub struct RemoteTurnIssue {
234    pub kind: String,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub code: Option<String>,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    pub terminal_reason: Option<RemoteLlmTerminalReason>,
239    pub message: String,
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    pub raw: Option<String>,
242    /// Typed retryability signal; `None` when the source did not know.
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    pub retryable: Option<bool>,
245    /// Typed provider-failure classification, present only for classified
246    /// LLM provider/transport failures.
247    #[serde(default, skip_serializing_if = "Option::is_none")]
248    pub provider_failure_kind: Option<RemoteProviderFailureKind>,
249}