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