Skip to main content

starweaver_context/
run_state.rs

1//! Checkpointable agent run state shared by execution and durability layers.
2
3use chrono::Utc;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use starweaver_core::{ConversationId, Metadata, RunId, RunLifecycle as RunStatus, TaskId};
7use starweaver_model::{ModelMessage, ModelResponse, ToolCallPart, ToolReturnPart};
8use starweaver_usage::Usage;
9
10/// Checkpointable state owned by the agent graph loop.
11#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
12pub struct AgentRunState {
13    /// Run identifier.
14    pub run_id: RunId,
15    /// Conversation identifier.
16    pub conversation_id: ConversationId,
17    /// Canonical model history.
18    pub message_history: Vec<ModelMessage>,
19    /// Accumulated usage.
20    pub usage: Usage,
21    /// Completed model/tool loop steps.
22    pub run_step: usize,
23    /// Current status.
24    pub status: RunStatus,
25    /// Final text output.
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub output: Option<String>,
28    /// Final structured JSON output.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub structured_output: Option<Value>,
31    /// Latest model response awaiting classification.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub latest_response: Option<ModelResponse>,
34    /// Tool calls awaiting execution.
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub pending_tool_calls: Vec<ToolCallPart>,
37    /// Tool returns awaiting request preparation.
38    #[serde(default, skip_serializing_if = "Vec::is_empty")]
39    pub pending_tool_returns: Vec<ToolReturnPart>,
40    /// Tool calls that require approval before execution can proceed.
41    #[serde(default, skip_serializing_if = "Vec::is_empty")]
42    pub pending_approval_tool_returns: Vec<ToolReturnPart>,
43    /// Tool calls deferred to another runtime or durable worker.
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    pub deferred_tool_returns: Vec<ToolReturnPart>,
46    /// Idle messages ready to redirect finalization.
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    pub idle_messages: Vec<String>,
49    /// Parent run identifier when this run is delegated from another run.
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub parent_run_id: Option<RunId>,
52    /// Parent-scoped delegated task identifier when this run executes a lightweight task.
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub parent_task_id: Option<TaskId>,
55    /// Run metadata.
56    #[serde(default, skip_serializing_if = "Metadata::is_empty")]
57    pub metadata: Metadata,
58}
59
60impl AgentRunState {
61    /// Create empty run state.
62    #[must_use]
63    pub fn new(run_id: RunId, conversation_id: ConversationId) -> Self {
64        Self {
65            run_id,
66            conversation_id,
67            message_history: Vec::new(),
68            usage: Usage::default(),
69            run_step: 0,
70            status: RunStatus::Starting,
71            output: None,
72            structured_output: None,
73            latest_response: None,
74            pending_tool_calls: Vec::new(),
75            pending_tool_returns: Vec::new(),
76            pending_approval_tool_returns: Vec::new(),
77            deferred_tool_returns: Vec::new(),
78            idle_messages: Vec::new(),
79            parent_run_id: None,
80            parent_task_id: None,
81            metadata: Metadata::default(),
82        }
83    }
84
85    /// Apply a model response to state.
86    pub fn apply_model_response(&mut self, mut response: ModelResponse) {
87        response.run_id.get_or_insert_with(|| self.run_id.clone());
88        response
89            .conversation_id
90            .get_or_insert_with(|| self.conversation_id.clone());
91        response.timestamp.get_or_insert_with(Utc::now);
92        self.usage.add_assign(&response.usage);
93        self.message_history
94            .push(ModelMessage::Response(response.clone()));
95        self.latest_response = Some(response);
96    }
97
98    /// Replace the latest response after lifecycle hooks mutate it.
99    pub fn replace_latest_response(&mut self, response: ModelResponse) {
100        if let Some(ModelMessage::Response(history_response)) = self.message_history.last_mut() {
101            *history_response = response.clone();
102        }
103        self.latest_response = Some(response);
104    }
105
106    /// Return true when the run is waiting for approval or deferred tool results.
107    #[must_use]
108    pub const fn has_pending_hitl(&self) -> bool {
109        !self.pending_approval_tool_returns.is_empty() || !self.deferred_tool_returns.is_empty()
110    }
111
112    /// Return pending approval-required tool returns.
113    #[must_use]
114    pub fn pending_approvals(&self) -> &[ToolReturnPart] {
115        &self.pending_approval_tool_returns
116    }
117
118    /// Return pending deferred tool returns.
119    #[must_use]
120    pub fn pending_deferred_tools(&self) -> &[ToolReturnPart] {
121        &self.deferred_tool_returns
122    }
123
124    /// Iterate all pending HITL tool returns in approval-then-deferred order.
125    pub fn pending_hitl_tool_returns(&self) -> impl Iterator<Item = &ToolReturnPart> {
126        self.pending_approval_tool_returns
127            .iter()
128            .chain(self.deferred_tool_returns.iter())
129    }
130}