Skip to main content

starweaver_runtime/
run.rs

1//! Runtime run state and result types.
2
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use starweaver_core::{ConversationId, Metadata, RunId};
7use starweaver_model::{ModelMessage, ModelResponse, ToolCallPart, ToolReturnPart};
8use starweaver_usage::Usage;
9
10/// Runtime status for an agent run.
11#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
12#[serde(rename_all = "snake_case")]
13pub enum RunStatus {
14    /// Run is being initialized.
15    Starting,
16    /// Run is actively executing graph nodes.
17    Running,
18    /// Run is waiting on external work.
19    Waiting,
20    /// Run completed successfully.
21    Completed,
22    /// Run failed.
23    Failed,
24    /// Run was cancelled.
25    Cancelled,
26}
27
28/// Checkpointable state owned by the graph loop.
29#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
30pub struct AgentRunState {
31    /// Run identifier.
32    pub run_id: RunId,
33    /// Conversation identifier.
34    pub conversation_id: ConversationId,
35    /// Canonical model history.
36    pub message_history: Vec<ModelMessage>,
37    /// Accumulated usage.
38    pub usage: Usage,
39    /// Completed model/tool loop steps.
40    pub run_step: usize,
41    /// Current status.
42    pub status: RunStatus,
43    /// Final text output.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub output: Option<String>,
46    /// Final structured JSON output.
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub structured_output: Option<Value>,
49    /// Latest model response awaiting classification.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub latest_response: Option<ModelResponse>,
52    /// Tool calls awaiting execution.
53    #[serde(default, skip_serializing_if = "Vec::is_empty")]
54    pub pending_tool_calls: Vec<ToolCallPart>,
55    /// Tool returns awaiting request preparation.
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub pending_tool_returns: Vec<ToolReturnPart>,
58    /// Tool calls that require approval before execution can proceed.
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub pending_approval_tool_returns: Vec<ToolReturnPart>,
61    /// Tool calls deferred to another runtime or durable worker.
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub deferred_tool_returns: Vec<ToolReturnPart>,
64    /// Idle messages ready to redirect finalization.
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    pub idle_messages: Vec<String>,
67    /// Run metadata.
68    #[serde(default, skip_serializing_if = "Metadata::is_empty")]
69    pub metadata: Metadata,
70}
71
72impl AgentRunState {
73    /// Create empty run state.
74    #[must_use]
75    pub fn new(run_id: RunId, conversation_id: ConversationId) -> Self {
76        Self {
77            run_id,
78            conversation_id,
79            message_history: Vec::new(),
80            usage: Usage::default(),
81            run_step: 0,
82            status: RunStatus::Starting,
83            output: None,
84            structured_output: None,
85            latest_response: None,
86            pending_tool_calls: Vec::new(),
87            pending_tool_returns: Vec::new(),
88            pending_approval_tool_returns: Vec::new(),
89            deferred_tool_returns: Vec::new(),
90            idle_messages: Vec::new(),
91            metadata: Metadata::default(),
92        }
93    }
94
95    /// Apply a model response to state.
96    pub fn apply_model_response(&mut self, mut response: ModelResponse) {
97        response.run_id.get_or_insert_with(|| self.run_id.clone());
98        response
99            .conversation_id
100            .get_or_insert_with(|| self.conversation_id.clone());
101        response.timestamp.get_or_insert_with(Utc::now);
102        self.usage.add_assign(&response.usage);
103        self.message_history
104            .push(ModelMessage::Response(response.clone()));
105        self.latest_response = Some(response);
106    }
107
108    /// Replace the latest response after lifecycle hooks mutate it.
109    pub fn replace_latest_response(&mut self, response: ModelResponse) {
110        if let Some(ModelMessage::Response(history_response)) = self.message_history.last_mut() {
111            *history_response = response.clone();
112        }
113        self.latest_response = Some(response);
114    }
115
116    /// Return true when the run is waiting for approval or deferred tool results.
117    #[must_use]
118    pub const fn has_pending_hitl(&self) -> bool {
119        !self.pending_approval_tool_returns.is_empty() || !self.deferred_tool_returns.is_empty()
120    }
121
122    /// Return pending approval-required tool returns.
123    #[must_use]
124    pub fn pending_approvals(&self) -> &[ToolReturnPart] {
125        &self.pending_approval_tool_returns
126    }
127
128    /// Return pending deferred tool returns.
129    #[must_use]
130    pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
131        &self.deferred_tool_returns
132    }
133
134    /// Iterate all pending HITL tool returns in approval-then-deferred order.
135    pub fn pending_hitl_tool_returns(&self) -> impl Iterator<Item = &ToolReturnPart> {
136        self.pending_approval_tool_returns
137            .iter()
138            .chain(self.deferred_tool_returns.iter())
139    }
140}
141
142/// Result returned when an agent run completes.
143#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
144pub struct AgentRunResult {
145    /// Final output text.
146    pub output: String,
147    /// Final checkpointable state.
148    pub state: AgentRunState,
149}
150
151impl AgentRunResult {
152    /// Return true when the final state is waiting for HITL input.
153    #[must_use]
154    pub const fn has_pending_hitl(&self) -> bool {
155        self.state.has_pending_hitl()
156    }
157
158    /// Return pending approval-required tool returns.
159    #[must_use]
160    pub fn pending_approvals(&self) -> &[ToolReturnPart] {
161        self.state.pending_approvals()
162    }
163
164    /// Return pending deferred tool returns.
165    #[must_use]
166    pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
167        self.state.pending_deferred_tools()
168    }
169}