1use super::{Agent, AgentFailure, AgentRunTime, AgentTurn, FailureKind, storage_failure};
2use crate::{
3 error::AgentError,
4 observability,
5 shared::{Message, Role},
6 state::AgentState,
7 transcript::Transcript,
8};
9use tokio_util::sync::CancellationToken;
10use tracing::Instrument;
11use uuid::Uuid;
12
13impl AgentRunTime {
14 pub async fn run(
20 &self,
21 agent_state: Agent,
22 cancellation_token: CancellationToken,
23 ) -> Result<Agent, AgentError> {
24 let session_id = self.get_session_id(&agent_state);
25 let span = tracing::info_span!(
26 "agent.run",
27 "gen_ai.conversation.id" = %session_id,
28 "tiny_agent.session.id" = %session_id,
29 );
30 async move {
31 let session_lock = self.session_lock(&session_id).await;
32 let _guard = session_lock.lock().await;
33 self.run_unlocked(agent_state, cancellation_token).await
34 }
35 .instrument(span)
36 .await
37 }
38
39 async fn run_unlocked(
40 &self,
41 agent_state: Agent,
42 cancellation_token: CancellationToken,
43 ) -> Result<Agent, AgentError> {
44 let outcome = self.run_inner(agent_state, cancellation_token).await?;
45 if matches!(outcome, Agent::Success(_) | Agent::Fail(_, _)) {
46 let session_id = self.get_session_id(&outcome);
47 if let Err(e) = self.checkpoint_storage.delete_checkpoint(&session_id).await {
48 return Ok(storage_failure(AgentState::new(Some(session_id)), e));
49 }
50 }
51 Ok(outcome)
52 }
53
54 pub async fn resume(
59 &self,
60 session_id: &str,
61 cancellation_token: CancellationToken,
62 ) -> Result<Option<Agent>, AgentError> {
63 let span = tracing::info_span!(
64 "agent.resume",
65 "gen_ai.conversation.id" = %session_id,
66 "tiny_agent.session.id" = %session_id,
67 );
68 async move {
69 let session_lock = self.session_lock(session_id).await;
70 let _guard = session_lock.lock().await;
71 let checkpoint = match self.checkpoint_storage.get_checkpoint(session_id).await {
72 Ok(Some(checkpoint)) => checkpoint,
73 Ok(None) => return Ok(None),
74 Err(e) => {
75 return Ok(Some(storage_failure(
76 AgentState::new(Some(session_id.to_string())),
77 e,
78 )));
79 }
80 };
81 self.run_unlocked(checkpoint, cancellation_token)
82 .await
83 .map(Some)
84 }
85 .instrument(span)
86 .await
87 }
88
89 pub async fn run_session(
92 &self,
93 session_id: &str,
94 cancellation_token: CancellationToken,
95 ) -> Result<Agent, AgentError> {
96 let span = tracing::info_span!(
97 "agent.run_session",
98 "gen_ai.conversation.id" = %session_id,
99 "tiny_agent.session.id" = %session_id,
100 );
101 async move {
102 let session_lock = self.session_lock(session_id).await;
103 let _guard = session_lock.lock().await;
104 let start = match self.checkpoint_storage.get_checkpoint(session_id).await {
105 Ok(Some(checkpoint)) => checkpoint,
106 Ok(None) => Agent::Ready(AgentState::new(Some(session_id.to_string()))),
107 Err(e) => {
108 return Ok(storage_failure(
109 AgentState::new(Some(session_id.to_string())),
110 e,
111 ));
112 }
113 };
114 self.run_unlocked(start, cancellation_token).await
115 }
116 .instrument(span)
117 .await
118 }
119
120 async fn run_inner(
121 &self,
122 mut agent_state: Agent,
123 cancellation_token: CancellationToken,
124 ) -> Result<Agent, AgentError> {
125 loop {
126 if cancellation_token.is_cancelled()
130 && !matches!(agent_state, Agent::ToolExecuting(_, _))
131 {
132 if matches!(agent_state, Agent::Interrupted(_)) {
137 return Ok(agent_state);
138 }
139 let session_id = self.get_session_id(&agent_state);
140 let parked = match agent_state {
141 Agent::Reasoning(ctx) => Agent::Interrupted(ctx),
142 other => other,
143 };
144 if let Err(e) = self
145 .checkpoint_storage
146 .as_ref()
147 .save_checkpoint(&session_id, &parked)
148 .await
149 {
150 return Ok(storage_failure(AgentState::new(Some(session_id)), e));
151 }
152 observability::interrupted(&session_id);
153 return Ok(parked);
154 }
155
156 agent_state = match agent_state {
157 Agent::Interrupted(ctx) => Agent::Ready(ctx),
158 Agent::Ready(mut ctx) => {
159 if ctx.steps_count >= self.max_iter {
161 let failure = AgentFailure::new(
162 FailureKind::MaxIter,
163 format!("exceeded max_iter ({})", self.max_iter),
164 );
165 observability::failed(
166 &ctx.session_id,
167 failure.kind.as_str(),
168 &failure.message,
169 );
170 return Ok(Agent::Fail(ctx, failure));
171 }
172 ctx.steps_count += 1;
173 Agent::Reasoning(ctx)
174 }
175 Agent::Reasoning(ctx) => {
176 self.handle_reasoning(ctx, cancellation_token.clone())
177 .await?
178 }
179 Agent::ToolExecuting(ctx, tool_calls) => {
180 self.execute_pending_tools(&ctx, tool_calls, cancellation_token.clone())
181 .await?
182 }
183 Agent::Fail(ctx, failure) => match self.handle_fail(ctx, failure).await? {
184 ready @ Agent::Ready(_) => ready,
185 terminal => return Ok(terminal),
186 },
187 other => return Ok(other),
188 }
189 }
190 }
191
192 pub async fn create_session(&self) -> Result<String, AgentError> {
193 self.create_session_inner(None).await
194 }
195
196 pub async fn create_subsession(
199 &self,
200 parent_session_id: &str,
201 ) -> Result<String, AgentError> {
202 let child = self
203 .create_session_inner(Some(parent_session_id.to_string()))
204 .await?;
205 self.transcript_storage
206 .add_subsession(parent_session_id, &child)
207 .await?;
208 Ok(child)
209 }
210
211 async fn create_session_inner(&self, parent: Option<String>) -> Result<String, AgentError> {
212 let new_session_id = Uuid::new_v4().to_string();
213 let transcript = match &parent {
214 Some(parent) => Transcript::with_parent(&new_session_id, parent),
215 None => Transcript::new(&new_session_id),
216 };
217 self.transcript_storage
218 .save_transcript(&new_session_id, &transcript)
219 .await?;
220 let sandbox = self.sandbox.clone().open(&new_session_id).await?;
221 sandbox.save().await?;
222 Ok(new_session_id)
223 }
224
225 pub async fn run_turn(
231 &self,
232 session_id: Option<String>,
233 user_text: impl Into<String>,
234 cancellation_token: CancellationToken,
235 ) -> Result<AgentTurn, AgentError> {
236 let session_id = match session_id {
237 Some(session_id) => {
238 self.ensure_session(&session_id).await?;
239 session_id
240 }
241 None => self.create_session().await?,
242 };
243 let ready = self.submit(&session_id, user_text).await?;
244 let outcome = self.run(ready, cancellation_token).await?;
245 Ok(AgentTurn {
246 session_id,
247 outcome,
248 })
249 }
250
251 async fn ensure_session(&self, session_id: &str) -> Result<(), AgentError> {
252 if self
253 .transcript_storage
254 .get_transcript(session_id)
255 .await?
256 .is_some()
257 {
258 return Ok(());
259 }
260 self.transcript_storage
261 .save_transcript(session_id, &Transcript::new(session_id))
262 .await?;
263 let sandbox = self.sandbox.clone().open(session_id).await?;
264 sandbox.save().await?;
265 Ok(())
266 }
267
268 pub async fn submit(
271 &self,
272 session_id: &str,
273 user_text: impl Into<String>,
274 ) -> Result<Agent, AgentError> {
275 let user_text = user_text.into();
276 let span = tracing::info_span!(
277 "user_input",
278 "gen_ai.operation.name" = "user_input",
279 "gen_ai.conversation.id" = %session_id,
280 "gen_ai.input.messages" = %user_text,
282 "tiny_agent.session.id" = %session_id,
283 );
284 self.submit_inner(session_id, user_text)
285 .instrument(span)
286 .await
287 }
288
289 async fn submit_inner(&self, session_id: &str, user_text: String) -> Result<Agent, AgentError> {
290 let session_lock = self.session_lock(session_id).await;
291 let _guard = session_lock.lock().await;
292 if let Err(e) = self.checkpoint_storage.delete_checkpoint(session_id).await {
293 return Ok(storage_failure(
294 AgentState::new(Some(session_id.to_string())),
295 e,
296 ));
297 }
298 if let Err(e) = self
299 .transcript_storage
300 .append_message(session_id, Message::text(Role::User, user_text.clone()))
301 .await
302 {
303 return Ok(storage_failure(
304 AgentState::new(Some(session_id.to_string())),
305 e,
306 ));
307 }
308 self.messages.send(
309 session_id,
310 crate::messages::MessageKind::UserMessage {
311 text: user_text.clone(),
312 },
313 );
314 observability::user_message(session_id, &user_text);
315 Ok(Agent::Ready(AgentState::new(Some(session_id.to_string()))))
316 }
317
318 async fn session_lock(&self, session_id: &str) -> std::sync::Arc<tokio::sync::Mutex<()>> {
319 let mut locks = self.session_locks.lock().await;
320 locks.retain(|_, lock| lock.strong_count() > 0);
321 if let Some(lock) = locks.get(session_id).and_then(|lock| lock.upgrade()) {
322 return lock;
323 }
324 let lock = std::sync::Arc::new(tokio::sync::Mutex::new(()));
325 locks.insert(session_id.to_string(), std::sync::Arc::downgrade(&lock));
326 lock
327 }
328
329 pub(crate) fn get_session_id(&self, agent: &Agent) -> String {
330 match agent {
331 Agent::Fail(ctx, _)
332 | Agent::Interrupted(ctx)
333 | Agent::Ready(ctx)
334 | Agent::Reasoning(ctx)
335 | Agent::Success(ctx)
336 | Agent::ToolExecuting(ctx, _)
337 | Agent::WaitingForUser(ctx, _) => ctx.session_id.clone(),
338 }
339 }
340
341 pub(crate) async fn interrupt(&self, ctx: &AgentState) -> Result<Agent, AgentError> {
345 let agent = Agent::Interrupted(ctx.clone());
346 if let Err(e) = self
347 .checkpoint_storage
348 .as_ref()
349 .save_checkpoint(&ctx.session_id, &agent)
350 .await
351 {
352 return Ok(storage_failure(ctx.clone(), e));
353 }
354 observability::interrupted(&ctx.session_id);
355 Ok(agent)
356 }
357
358 pub(crate) async fn save_interruption(&self, agent: &Agent) -> Result<(), AgentError> {
359 let session_id = self.get_session_id(agent);
360 self.checkpoint_storage
361 .as_ref()
362 .save_checkpoint(&session_id, agent)
363 .await?;
364 observability::interrupted(&session_id);
365 Ok(())
366 }
367}