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, TaskId};
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    /// Parent run identifier when this run is delegated from another run.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub parent_run_id: Option<RunId>,
70    /// Parent-scoped delegated task identifier when this run executes a lightweight task.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub parent_task_id: Option<TaskId>,
73    /// Run metadata.
74    #[serde(default, skip_serializing_if = "Metadata::is_empty")]
75    pub metadata: Metadata,
76}
77
78impl AgentRunState {
79    /// Create empty run state.
80    #[must_use]
81    pub fn new(run_id: RunId, conversation_id: ConversationId) -> Self {
82        Self {
83            run_id,
84            conversation_id,
85            message_history: Vec::new(),
86            usage: Usage::default(),
87            run_step: 0,
88            status: RunStatus::Starting,
89            output: None,
90            structured_output: None,
91            latest_response: None,
92            pending_tool_calls: Vec::new(),
93            pending_tool_returns: Vec::new(),
94            pending_approval_tool_returns: Vec::new(),
95            deferred_tool_returns: Vec::new(),
96            idle_messages: Vec::new(),
97            parent_run_id: None,
98            parent_task_id: None,
99            metadata: Metadata::default(),
100        }
101    }
102
103    /// Apply a model response to state.
104    pub fn apply_model_response(&mut self, mut response: ModelResponse) {
105        response.run_id.get_or_insert_with(|| self.run_id.clone());
106        response
107            .conversation_id
108            .get_or_insert_with(|| self.conversation_id.clone());
109        response.timestamp.get_or_insert_with(Utc::now);
110        self.usage.add_assign(&response.usage);
111        self.message_history
112            .push(ModelMessage::Response(response.clone()));
113        self.latest_response = Some(response);
114    }
115
116    /// Replace the latest response after lifecycle hooks mutate it.
117    pub fn replace_latest_response(&mut self, response: ModelResponse) {
118        if let Some(ModelMessage::Response(history_response)) = self.message_history.last_mut() {
119            *history_response = response.clone();
120        }
121        self.latest_response = Some(response);
122    }
123
124    /// Return true when the run is waiting for approval or deferred tool results.
125    #[must_use]
126    pub const fn has_pending_hitl(&self) -> bool {
127        !self.pending_approval_tool_returns.is_empty() || !self.deferred_tool_returns.is_empty()
128    }
129
130    /// Return pending approval-required tool returns.
131    #[must_use]
132    pub fn pending_approvals(&self) -> &[ToolReturnPart] {
133        &self.pending_approval_tool_returns
134    }
135
136    /// Return pending deferred tool returns.
137    #[must_use]
138    pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
139        &self.deferred_tool_returns
140    }
141
142    /// Iterate all pending HITL tool returns in approval-then-deferred order.
143    pub fn pending_hitl_tool_returns(&self) -> impl Iterator<Item = &ToolReturnPart> {
144        self.pending_approval_tool_returns
145            .iter()
146            .chain(self.deferred_tool_returns.iter())
147    }
148}
149
150/// Result returned when an agent run completes.
151#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
152pub struct AgentRunResult {
153    /// Final output text.
154    pub output: String,
155    /// Final checkpointable state.
156    pub state: AgentRunState,
157}
158
159impl AgentRunResult {
160    /// Return true when the final state is waiting for HITL input.
161    #[must_use]
162    pub const fn has_pending_hitl(&self) -> bool {
163        self.state.has_pending_hitl()
164    }
165
166    /// Return pending approval-required tool returns.
167    #[must_use]
168    pub fn pending_approvals(&self) -> &[ToolReturnPart] {
169        self.state.pending_approvals()
170    }
171
172    /// Return pending deferred tool returns.
173    #[must_use]
174    pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
175        self.state.pending_deferred_tools()
176    }
177}