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