Skip to main content

tiny_agent/core/
runtime.rs

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    /// 驱动一个 `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        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    /// 框架对外的"用户说话"入口:把一条用户消息追加进会话历史,
204    /// 返回一个 `Ready` 状态交给 `run` 驱动一轮对话。
205    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.chat span 的 input.messages 上。
216            "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    /// 存档 checkpoint 并返回 `Interrupted`。调用前须确保所有 tool_use 已被应答,
277    /// 因此 checkpoint 不再携带可重跑的 pending calls,resume 时只会从 `Interrupted`
278    /// 走 `Ready → Reasoning`,不会重复执行工具。
279    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}