Skip to main content

tiny_agent/core/
runtime.rs

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    /// 驱动一个 `Agent` 跑到停车点(终态或中断),并负责 **checkpoint 生命周期**:
15    ///
16    /// - `Success` / `Fail` 是终态,运行结束时清掉该会话残留的 checkpoint —— 既回收存储,
17    ///   也避免下次 [`resume`](Self::resume) 读到陈旧快照。
18    /// - `Interrupted` 的 checkpoint 由内层在停车时写入,这里**保留**,留给 `resume`。
19    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    /// 恢复被中断的会话:有 checkpoint 就从中断点接着跑,没有则什么都不做返回 `Ok(None)`。
55    ///
56    /// 这是与 [`submit`](Self::submit) 互补的入口 —— `resume` 续上**被中断的 in-flight 状态**,
57    /// `submit` 表示**用户发了新消息**(会丢弃尚未恢复的中断态)。
58    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    /// 一站式入口:有 checkpoint 则恢复,否则从全新 `Ready` 开始,跑到停车点。
90    /// 适合"按 session_id 拉起一轮"的调用方,无需自己拼装 `Agent`。
91    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            // ToolExecuting 仍有未应答的 tool_use,取消时不能在这里直接丢弃 ——
127            // 必须让它流进 execute_pending_tools,由那里为每个 pending call 补占位结果,
128            // 否则 transcript 会留下孤儿 tool_use。其余状态没有这个负担,可就地停车。
129            if cancellation_token.is_cancelled()
130                && !matches!(agent_state, Agent::ToolExecuting(_, _))
131            {
132                // 已经是 Interrupted 的状态:产生它的那一步(execute_pending_tools 里的
133                // `interrupt()` / `save_interruption`)已经存档并 emit 过 Interrupted 了。
134                // 这里若再 park、再 emit 一遍,取消恰好落在工具执行中时就会冒出**两条**
135                // Interrupted 事件。已停车的状态直接原样返回即可。
136                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                    // 迭代上限:每进入一次推理算一步。超限直接终态失败(不重试)
160                    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    /// 在 `parent_session_id` 名下创建一个子会话:子 transcript 的 `parent` 指向父会话,
197    /// 同时在父会话上登记这条 `subsession`(父 → 子)。用于把子 agent 的独立会话挂回发起它的 turn。
198    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    /// One-shot user turn entrypoint.
226    ///
227    /// When `session_id` is `None`, a new session is created. When it is `Some`,
228    /// the existing transcript is reused; if the transcript is missing, the
229    /// session is initialized with that id before the user message is appended.
230    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    /// 框架对外的"用户说话"入口:把一条用户消息追加进会话历史,
269    /// 返回一个 `Ready` 状态交给 `run` 驱动一轮对话。
270    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.chat span 的 input.messages 上。
281            "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    /// 存档 checkpoint 并返回 `Interrupted`。调用前须确保所有 tool_use 已被应答,
342    /// 因此 checkpoint 不再携带可重跑的 pending calls,resume 时只会从 `Interrupted`
343    /// 走 `Ready → Reasoning`,不会重复执行工具。
344    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}