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