Skip to main content

atman_runtime/
executor.rs

1use std::collections::HashMap;
2
3use atman_dsl::ast::{File, FlowDecl};
4
5use crate::error::RuntimeError;
6use crate::event::{Event, EventSink, FlowRunId, FlowStatus, TurnId};
7use crate::exec::exec_flow_with_siblings;
8use crate::provider::ProviderRegistry;
9use crate::session::Session;
10use crate::tool::{ToolCtx, ToolRegistry};
11use crate::value::Value;
12
13#[derive(Clone)]
14pub struct Executor {
15    pub tools: ToolRegistry,
16    pub providers: ProviderRegistry,
17    pub events: EventSink,
18    pub tool_ctx: ToolCtx,
19    pub safety: Option<crate::safety::SafetyConfig>,
20    /// When set, relative `@` paths are resolved against this directory.
21    pub source_dir: Option<std::path::PathBuf>,
22}
23
24impl Executor {
25    pub fn new() -> Self {
26        Self {
27            tools: ToolRegistry::new(),
28            providers: ProviderRegistry::new(),
29            events: EventSink::new(),
30            tool_ctx: ToolCtx::new(),
31            safety: None,
32            source_dir: None,
33        }
34    }
35
36    pub fn with_events(events: EventSink) -> Self {
37        Self {
38            tools: ToolRegistry::new(),
39            providers: ProviderRegistry::new(),
40            events,
41            tool_ctx: ToolCtx::new(),
42            safety: None,
43            source_dir: None,
44        }
45    }
46
47    pub fn with_safety(mut self, safety: crate::safety::SafetyConfig) -> Self {
48        self.safety = Some(safety);
49        self
50    }
51
52    pub async fn run(
53        &self,
54        file: &File,
55        flow_name: &str,
56        args: Vec<(String, Value)>,
57    ) -> Result<Value, RuntimeError> {
58        self.run_in_turn(file, flow_name, args, None, None).await
59    }
60
61    pub async fn run_in_turn(
62        &self,
63        file: &File,
64        flow_name: &str,
65        args: Vec<(String, Value)>,
66        turn_id: Option<TurnId>,
67        session: Option<std::sync::Arc<Session>>,
68    ) -> Result<Value, RuntimeError> {
69        self.run_in_turn_with_run_id(file, flow_name, args, turn_id, session, None)
70            .await
71    }
72
73    pub async fn run_in_turn_with_run_id(
74        &self,
75        file: &File,
76        flow_name: &str,
77        args: Vec<(String, Value)>,
78        turn_id: Option<TurnId>,
79        session: Option<std::sync::Arc<Session>>,
80        first_run_id: Option<FlowRunId>,
81    ) -> Result<Value, RuntimeError> {
82        let flows: HashMap<_, _> = file
83            .flows
84            .iter()
85            .map(|f| (f.name.name.clone(), f.clone()))
86            .collect();
87        let mut current = flow_name.to_string();
88        let mut current_args = args;
89        let mut next_run_id = first_run_id;
90        for _ in 0..5 {
91            let flow = flows
92                .get(&current)
93                .ok_or_else(|| RuntimeError::UndefinedTool(format!("flow `{current}`")))?;
94            match self
95                .run_flow(
96                    flow,
97                    current_args,
98                    &flows,
99                    turn_id.clone(),
100                    session.clone(),
101                    next_run_id.take(),
102                )
103                .await
104            {
105                Err(RuntimeError::Redirect(target)) => {
106                    current = target;
107                    current_args = Vec::new();
108                    continue;
109                }
110                other => return other,
111            }
112        }
113        Err(RuntimeError::ToolFailed(
114            "redirect chain exceeded max depth (5)".into(),
115        ))
116    }
117
118    async fn run_flow(
119        &self,
120        flow: &FlowDecl,
121        args: Vec<(String, Value)>,
122        flows: &HashMap<String, FlowDecl>,
123        turn_id: Option<TurnId>,
124        session: Option<std::sync::Arc<Session>>,
125        run_id: Option<FlowRunId>,
126    ) -> Result<Value, RuntimeError> {
127        let run_id = run_id.unwrap_or_else(FlowRunId::now);
128        let flow_cancel = session
129            .as_ref()
130            .map(|s| s.flow_cancel_token())
131            .unwrap_or_default();
132        let task_id = self.tool_ctx.task_registry.as_ref().map(|tr| {
133            tr.register(
134                crate::task_registry::TaskKind::Flow,
135                flow.name.name.clone(),
136                run_id.0.to_string(),
137                self.tool_ctx
138                    .session_id
139                    .clone()
140                    .unwrap_or_else(|| "anon".into()),
141                flow_cancel.clone(),
142            )
143        });
144        self.events.emit(Event::FlowStart {
145            run_id: run_id.clone(),
146            flow_name: flow.name.name.clone(),
147            parent_run_id: None,
148            parent_node_id: None,
149            spawned: false,
150        });
151        if let Some(sess) = session.as_ref() {
152            let _ = sess
153                .stream_tx()
154                .send(crate::stream::StreamFrame::FlowStart {
155                    run_id: run_id.0.to_string(),
156                    flow_name: flow.name.name.clone(),
157                    parent_run_id: None,
158                    parent_node_id: None,
159                });
160        }
161        let graph = crate::nodegraph::extract_graph(flow);
162        self.events.emit(Event::FlowGraph {
163            run_id: run_id.clone(),
164            graph: graph.clone(),
165        });
166        if let Some(sess) = session.as_ref() {
167            let _ = sess
168                .stream_tx()
169                .send(crate::stream::StreamFrame::FlowGraph {
170                    run_id: run_id.0.to_string(),
171                    graph,
172                });
173        }
174        // Root's tool_ctx carries session stream_tx so emit sites use
175        // tool_ctx.stream_tx uniformly.
176        let mut tool_ctx = self.tool_ctx.clone();
177        if let Some(sess) = session.as_ref() {
178            tool_ctx.stream_tx = Some(sess.stream_tx());
179            tool_ctx.session_messages_handle = Some(sess.messages_handle());
180            // Register root so flow.output/interject("root") work. Root's llm
181            // context stays on session MessageStream; entry is for output +
182            // interjection addressing.
183            let root_entry = sess.flow_registry.create_entry(
184                "root".to_string(),
185                sess.goal().unwrap_or_else(|| flow.name.name.clone()),
186                String::new(),
187                run_id.clone(),
188            );
189            tool_ctx.agent_entry = Some(std::sync::Arc::clone(&root_entry));
190            sess.set_current_root("root".to_string());
191        }
192        let exec_fut = exec_flow_with_siblings(
193            flow,
194            args,
195            &self.tools,
196            &tool_ctx,
197            &self.providers,
198            flows,
199            Some(&self.events),
200            turn_id,
201            Some(run_id.clone()),
202            session.clone(),
203            flow_cancel.clone(),
204            self.safety.as_ref(),
205            self.source_dir.clone(),
206        );
207        let result = tokio::select! {
208            biased;
209            _ = flow_cancel.cancelled() => Err(RuntimeError::Cancelled("flow cancelled by user".into())),
210            r = exec_fut => r,
211        };
212        let status = match &result {
213            Ok(v) => {
214                if let Value::Err(e) = v
215                    && matches!(e, RuntimeError::Cancelled(_))
216                {
217                    FlowStatus::Cancelled
218                } else {
219                    FlowStatus::Ok
220                }
221            }
222            Err(e) => {
223                if matches!(e, RuntimeError::Cancelled(_)) {
224                    FlowStatus::Cancelled
225                } else {
226                    FlowStatus::Errored {
227                        message: e.to_string(),
228                    }
229                }
230            }
231        };
232        let cancelled = matches!(status, FlowStatus::Cancelled);
233        if let (Some(tr), Some(tid)) = (self.tool_ctx.task_registry.as_ref(), &task_id) {
234            let ts = match &status {
235                FlowStatus::Ok => crate::task_registry::TaskStatus::Ok,
236                FlowStatus::Cancelled => crate::task_registry::TaskStatus::Killed,
237                FlowStatus::Errored { .. } => crate::task_registry::TaskStatus::Err,
238            };
239            tr.finish(tid, ts);
240        }
241        self.events.emit(Event::FlowEnd {
242            run_id: run_id.clone(),
243            flow_name: flow.name.name.clone(),
244            status: status.clone(),
245        });
246        if let Some(sess) = session.as_ref() {
247            let _ = sess.stream_tx().send(crate::stream::StreamFrame::FlowDone {
248                run_id: run_id.0.to_string(),
249                flow_name: flow.name.name.clone(),
250                ok: matches!(status, FlowStatus::Ok),
251                cancelled,
252            });
253        }
254        result
255    }
256}
257
258impl Default for Executor {
259    fn default() -> Self {
260        Self::new()
261    }
262}