Skip to main content

behest_runtime/
run.rs

1//! Run lifecycle types.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use uuid::Uuid;
7
8use behest_provider::{ModelName, ProviderId, ToolChoice};
9
10pub use behest_core::id::RunId;
11
12/// Status of an agent run.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14pub enum RunStatus {
15    /// Run has been created but not yet started.
16    Pending,
17    /// Run is loading or validating session state.
18    SessionLoaded,
19    /// Run is building context from adapters.
20    BuildingContext,
21    /// Run is calling the model provider.
22    CallingModel,
23    /// Run is waiting for tool execution.
24    WaitingForTools,
25    /// Run is persisting results.
26    Persisting,
27    /// Run completed successfully.
28    Completed,
29    /// Run failed with an error.
30    Failed,
31    /// Run was cancelled.
32    Cancelled,
33}
34
35impl RunStatus {
36    /// Returns true if the run is in a terminal state.
37    #[must_use]
38    pub fn is_terminal(&self) -> bool {
39        matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
40    }
41}
42
43/// Request to start a new agent run.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct RunRequest {
46    /// Optional session ID. If None, a new session will be created.
47    pub session_id: Option<Uuid>,
48    /// Optional pre-allocated run ID. If None, a new RunId will be generated.
49    pub run_id: Option<RunId>,
50    /// Provider to use for model calls.
51    pub provider: ProviderId,
52    /// Model to use for generation.
53    pub model: ModelName,
54    /// User input message.
55    pub input: String,
56    /// Optional metadata for the run.
57    pub metadata: Value,
58    /// Tool choice strategy.
59    pub tool_choice: ToolChoice,
60    /// Optional client-provided idempotency key.
61    pub client_request_id: Option<String>,
62}
63
64impl RunRequest {
65    /// Creates a new run request.
66    #[must_use]
67    pub fn new(provider: ProviderId, model: ModelName, input: impl Into<String>) -> Self {
68        Self {
69            session_id: None,
70            run_id: None,
71            provider,
72            model,
73            input: input.into(),
74            metadata: Value::Null,
75            tool_choice: ToolChoice::Auto,
76            client_request_id: None,
77        }
78    }
79
80    /// Sets the session ID.
81    #[must_use]
82    pub fn with_session_id(mut self, session_id: Uuid) -> Self {
83        self.session_id = Some(session_id);
84        self
85    }
86
87    /// Sets the metadata.
88    #[must_use]
89    pub fn with_metadata(mut self, metadata: Value) -> Self {
90        self.metadata = metadata;
91        self
92    }
93
94    /// Sets the tool choice strategy.
95    #[must_use]
96    pub fn with_tool_choice(mut self, tool_choice: ToolChoice) -> Self {
97        self.tool_choice = tool_choice;
98        self
99    }
100
101    /// Sets a pre-allocated run ID.
102    #[must_use]
103    pub fn with_run_id(mut self, run_id: RunId) -> Self {
104        self.run_id = Some(run_id);
105        self
106    }
107
108    /// Sets the client-provided idempotency key.
109    #[must_use]
110    pub fn with_client_request_id(mut self, id: String) -> Self {
111        self.client_request_id = Some(id);
112        self
113    }
114}
115
116/// Persistent record of an agent run.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct RunRecord {
119    /// Unique run identifier.
120    pub id: RunId,
121    /// Session this run belongs to.
122    pub session_id: Uuid,
123    /// Current status of the run.
124    pub status: RunStatus,
125    /// Provider used for model calls.
126    pub provider: ProviderId,
127    /// Model used for generation.
128    pub model: ModelName,
129    /// Run metadata.
130    pub metadata: Value,
131    /// Optional client-provided idempotency key.
132    pub client_request_id: Option<String>,
133    /// When the run was created.
134    pub created_at: DateTime<Utc>,
135    /// When the run was last updated.
136    pub updated_at: DateTime<Utc>,
137}
138
139impl RunRecord {
140    /// Creates a new run record.
141    #[must_use]
142    pub fn new(
143        id: RunId,
144        session_id: Uuid,
145        provider: ProviderId,
146        model: ModelName,
147        metadata: Value,
148        client_request_id: Option<String>,
149    ) -> Self {
150        let now = Utc::now();
151        Self {
152            id,
153            session_id,
154            status: RunStatus::Pending,
155            provider,
156            model,
157            metadata,
158            client_request_id,
159            created_at: now,
160            updated_at: now,
161        }
162    }
163
164    /// Updates the status and timestamp.
165    pub fn update_status(&mut self, status: RunStatus) {
166        self.status = status;
167        self.updated_at = Utc::now();
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn run_id_generation() {
177        let id1 = RunId::new();
178        let id2 = RunId::new();
179        assert_ne!(id1, id2);
180    }
181
182    #[test]
183    fn run_status_terminal() {
184        assert!(!RunStatus::Pending.is_terminal());
185        assert!(!RunStatus::CallingModel.is_terminal());
186        assert!(RunStatus::Completed.is_terminal());
187        assert!(RunStatus::Failed.is_terminal());
188        assert!(RunStatus::Cancelled.is_terminal());
189    }
190
191    #[test]
192    fn run_request_builder() {
193        let provider = ProviderId::new("test");
194        let model = ModelName::new("gpt-4");
195        let request = RunRequest::new(provider.clone(), model.clone(), "hello")
196            .with_metadata(Value::String("meta".to_string()))
197            .with_tool_choice(ToolChoice::Required);
198
199        assert_eq!(request.provider, provider);
200        assert_eq!(request.model, model);
201        assert_eq!(request.input, "hello");
202        assert_eq!(request.metadata, Value::String("meta".to_string()));
203        assert!(matches!(request.tool_choice, ToolChoice::Required));
204    }
205}