1use super::{Agent, AgentFailure, AgentRunTime, 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 let new_session_id = Uuid::new_v4().to_string();
194 let transcript = Transcript::new(&new_session_id);
195 self.transcript_storage
196 .save_transcript(&new_session_id, &transcript)
197 .await?;
198 let sandbox = self.sandbox.clone().open(&new_session_id).await?;
199 sandbox.save().await?;
200 Ok(new_session_id)
201 }
202
203 pub async fn submit(
206 &self,
207 session_id: &str,
208 user_text: impl Into<String>,
209 ) -> Result<Agent, AgentError> {
210 let user_text = user_text.into();
211 let span = tracing::info_span!(
212 "user_input",
213 "gen_ai.operation.name" = "user_input",
214 "gen_ai.conversation.id" = %session_id,
215 "gen_ai.input.messages" = %user_text,
217 "tiny_agent.session.id" = %session_id,
218 );
219 self.submit_inner(session_id, user_text)
220 .instrument(span)
221 .await
222 }
223
224 async fn submit_inner(&self, session_id: &str, user_text: String) -> Result<Agent, AgentError> {
225 let session_lock = self.session_lock(session_id).await;
226 let _guard = session_lock.lock().await;
227 if let Err(e) = self.checkpoint_storage.delete_checkpoint(session_id).await {
228 return Ok(storage_failure(
229 AgentState::new(Some(session_id.to_string())),
230 e,
231 ));
232 }
233 if let Err(e) = self
234 .transcript_storage
235 .append_message(session_id, Message::text(Role::User, user_text.clone()))
236 .await
237 {
238 return Ok(storage_failure(
239 AgentState::new(Some(session_id.to_string())),
240 e,
241 ));
242 }
243 self.messages.send(
244 session_id,
245 crate::messages::MessageKind::UserMessage {
246 text: user_text.clone(),
247 },
248 );
249 observability::user_message(session_id, &user_text);
250 Ok(Agent::Ready(AgentState::new(Some(session_id.to_string()))))
251 }
252
253 async fn session_lock(&self, session_id: &str) -> std::sync::Arc<tokio::sync::Mutex<()>> {
254 let mut locks = self.session_locks.lock().await;
255 locks.retain(|_, lock| lock.strong_count() > 0);
256 if let Some(lock) = locks.get(session_id).and_then(|lock| lock.upgrade()) {
257 return lock;
258 }
259 let lock = std::sync::Arc::new(tokio::sync::Mutex::new(()));
260 locks.insert(session_id.to_string(), std::sync::Arc::downgrade(&lock));
261 lock
262 }
263
264 pub(crate) fn get_session_id(&self, agent: &Agent) -> String {
265 match agent {
266 Agent::Fail(ctx, _)
267 | Agent::Interrupted(ctx)
268 | Agent::Ready(ctx)
269 | Agent::Reasoning(ctx)
270 | Agent::Success(ctx)
271 | Agent::ToolExecuting(ctx, _)
272 | Agent::WaitingForUser(ctx, _) => ctx.session_id.clone(),
273 }
274 }
275
276 pub(crate) async fn interrupt(&self, ctx: &AgentState) -> Result<Agent, AgentError> {
280 let agent = Agent::Interrupted(ctx.clone());
281 if let Err(e) = self
282 .checkpoint_storage
283 .as_ref()
284 .save_checkpoint(&ctx.session_id, &agent)
285 .await
286 {
287 return Ok(storage_failure(ctx.clone(), e));
288 }
289 observability::interrupted(&ctx.session_id);
290 Ok(agent)
291 }
292
293 pub(crate) async fn save_interruption(&self, agent: &Agent) -> Result<(), AgentError> {
294 let session_id = self.get_session_id(agent);
295 self.checkpoint_storage
296 .as_ref()
297 .save_checkpoint(&session_id, agent)
298 .await?;
299 observability::interrupted(&session_id);
300 Ok(())
301 }
302}