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::invocation_env::InvocationEnv;
9use crate::provider::ProviderRegistry;
10use crate::provider_lifecycle::ProviderLifecycle;
11use crate::session::Session;
12use crate::tool::{ToolCtx, ToolRegistry};
13use crate::value::Value;
14
15#[derive(Clone, Default)]
16pub struct RootInvocation {
17 pub turn_id: Option<TurnId>,
18 pub session: Option<std::sync::Arc<Session>>,
19 pub first_run_id: Option<FlowRunId>,
20 pub env: InvocationEnv,
21}
22
23#[derive(Clone)]
24pub struct Executor {
25 pub tools: ToolRegistry,
26 pub providers: ProviderRegistry,
27 pub events: EventSink,
28 pub tool_ctx: ToolCtx,
29 pub safety: Option<crate::safety::SafetyConfig>,
30 pub source_dir: Option<std::path::PathBuf>,
32}
33
34#[derive(Debug, thiserror::Error)]
35#[error("executor already has a provider lifecycle")]
36pub struct ProviderLifecycleAlreadyAttached;
37
38impl Executor {
39 pub fn new() -> Self {
40 let tools = ToolRegistry::new();
41 crate::tools::register_tier_zero(&tools);
42 Self {
43 tools,
44 providers: ProviderRegistry::new(),
45 events: EventSink::new(),
46 tool_ctx: ToolCtx::new(),
47 safety: None,
48 source_dir: None,
49 }
50 }
51
52 pub fn with_events(events: EventSink) -> Self {
53 let tools = ToolRegistry::new();
54 crate::tools::register_tier_zero(&tools);
55 Self {
56 tools,
57 providers: ProviderRegistry::new(),
58 events,
59 tool_ctx: ToolCtx::new(),
60 safety: None,
61 source_dir: None,
62 }
63 }
64
65 pub fn attach_provider_lifecycle(
66 &mut self,
67 hub: crate::config_hub::ConfigHub,
68 ) -> Result<ProviderLifecycle, ProviderLifecycleAlreadyAttached> {
69 self.providers
70 .attach_provider_lifecycle(hub)
71 .ok_or(ProviderLifecycleAlreadyAttached)
72 }
73
74 pub fn provider_lifecycle(&self) -> Option<ProviderLifecycle> {
75 self.providers.provider_lifecycle()
76 }
77
78 pub fn with_safety(mut self, safety: crate::safety::SafetyConfig) -> Self {
79 self.safety = Some(safety);
80 self
81 }
82
83 pub async fn run(
84 &self,
85 file: &File,
86 flow_name: &str,
87 args: Vec<(String, Value)>,
88 ) -> Result<Value, RuntimeError> {
89 self.run_in_turn(file, flow_name, args, None, None).await
90 }
91
92 pub async fn run_in_turn(
93 &self,
94 file: &File,
95 flow_name: &str,
96 args: Vec<(String, Value)>,
97 turn_id: Option<TurnId>,
98 session: Option<std::sync::Arc<Session>>,
99 ) -> Result<Value, RuntimeError> {
100 self.run_with_invocation(
101 file,
102 flow_name,
103 args,
104 RootInvocation {
105 turn_id,
106 session,
107 ..RootInvocation::default()
108 },
109 )
110 .await
111 }
112
113 pub async fn run_in_turn_with_env(
114 &self,
115 file: &File,
116 flow_name: &str,
117 args: Vec<(String, Value)>,
118 turn_id: Option<TurnId>,
119 session: Option<std::sync::Arc<Session>>,
120 invocation_env: InvocationEnv,
121 ) -> Result<Value, RuntimeError> {
122 self.run_with_invocation(
123 file,
124 flow_name,
125 args,
126 RootInvocation {
127 turn_id,
128 session,
129 env: invocation_env,
130 ..RootInvocation::default()
131 },
132 )
133 .await
134 }
135
136 pub async fn run_in_turn_with_run_id(
137 &self,
138 file: &File,
139 flow_name: &str,
140 args: Vec<(String, Value)>,
141 turn_id: Option<TurnId>,
142 session: Option<std::sync::Arc<Session>>,
143 first_run_id: Option<FlowRunId>,
144 ) -> Result<Value, RuntimeError> {
145 self.run_with_invocation(
146 file,
147 flow_name,
148 args,
149 RootInvocation {
150 turn_id,
151 session,
152 first_run_id,
153 ..RootInvocation::default()
154 },
155 )
156 .await
157 }
158
159 pub async fn run_with_invocation(
160 &self,
161 file: &File,
162 flow_name: &str,
163 args: Vec<(String, Value)>,
164 invocation: RootInvocation,
165 ) -> Result<Value, RuntimeError> {
166 let flows: HashMap<_, _> = file
167 .flows
168 .iter()
169 .map(|f| (f.name.name.clone(), f.clone()))
170 .collect();
171 let mut current = flow_name.to_string();
172 let mut current_args = args;
173 let mut next_run_id = invocation.first_run_id.clone();
174 for _ in 0..5 {
175 let flow = flows
176 .get(¤t)
177 .ok_or_else(|| RuntimeError::UndefinedTool(format!("flow `{current}`")))?;
178 match self
179 .run_flow(flow, current_args, &flows, &invocation, next_run_id.take())
180 .await
181 {
182 Err(RuntimeError::Redirect(target)) => {
183 current = target;
184 current_args = Vec::new();
185 continue;
186 }
187 other => return other,
188 }
189 }
190 Err(RuntimeError::ToolFailed(
191 "redirect chain exceeded max depth (5)".into(),
192 ))
193 }
194
195 async fn run_flow(
196 &self,
197 flow: &FlowDecl,
198 args: Vec<(String, Value)>,
199 flows: &HashMap<String, FlowDecl>,
200 invocation: &RootInvocation,
201 run_id: Option<FlowRunId>,
202 ) -> Result<Value, RuntimeError> {
203 let turn_id = invocation.turn_id.clone();
204 let session = invocation.session.clone();
205 if let Some(session) = session.as_ref() {
206 session.set_tool_output_budget(self.tool_ctx.tool_output_budget);
207 }
208 let run_id = run_id.unwrap_or_else(FlowRunId::now);
209 let task_label = root_task_label(
210 &flow.name.name,
211 &args,
212 session.as_ref().and_then(|session| session.goal()),
213 );
214 let flow_cancel = session
215 .as_ref()
216 .map(|s| s.flow_cancel_token())
217 .unwrap_or_default();
218 let flow_registry = session
219 .as_ref()
220 .map(|session| std::sync::Arc::clone(&session.flow_registry))
221 .or_else(|| self.tool_ctx.flow_registry.clone())
222 .unwrap_or_else(|| std::sync::Arc::new(crate::tools::agent_ctrl::FlowRegistry::new()));
223 let permission_broker = session
228 .as_ref()
229 .map(|session| session.permission_broker())
230 .or_else(|| {
231 self.tool_ctx
232 .permission_broker
233 .clone()
234 .filter(|broker| broker.is_for_registry(&flow_registry))
235 })
236 .unwrap_or_else(|| {
237 crate::permission::PermissionBroker::shared(std::sync::Arc::clone(&flow_registry))
238 });
239 let session_id = session
240 .as_ref()
241 .map(|session| session.id().to_string())
242 .or_else(|| self.tool_ctx.session_id.clone())
243 .unwrap_or_else(|| format!("standalone-{}", uuid::Uuid::now_v7()));
244 let trust = session
245 .as_ref()
246 .map(|session| session.trust_config())
247 .or_else(|| self.tool_ctx.trust.clone())
248 .unwrap_or_default();
249 let workspace_root = self
250 .tool_ctx
251 .workspace
252 .as_ref()
253 .map(|binding| binding.path.clone())
254 .or_else(|| self.tool_ctx.fs_access.workspace.clone());
255 let identity = flow_registry.register_root(
256 session_id.clone(),
257 run_id.clone(),
258 crate::flow_authority::EffectiveAuthority::root(
259 &trust,
260 crate::flow_authority::contract_allows_shell(flow.contract.as_ref()),
261 workspace_root,
262 ),
263 )?;
264 let task_id = self.tool_ctx.task_registry.as_ref().map(|tr| {
265 tr.register_flow_with_run_id(
266 task_label,
267 run_id.0.to_string(),
268 self.tool_ctx
269 .session_id
270 .clone()
271 .unwrap_or_else(|| "anon".into()),
272 flow_cancel.clone(),
273 None,
274 run_id.clone(),
275 )
276 });
277 let _lifecycle_guard = flow_registry.lifecycle_guard(&run_id);
278 self.events.emit(Event::FlowStart {
279 run_id: run_id.clone(),
280 flow_name: flow.name.name.clone(),
281 parent_run_id: None,
282 parent_node_id: None,
283 spawned: false,
284 });
285 if let Some(sess) = session.as_ref() {
286 let _ = sess
287 .stream_tx()
288 .send(crate::stream::StreamFrame::FlowStart {
289 run_id: run_id.0.to_string(),
290 flow_name: flow.name.name.clone(),
291 parent_run_id: None,
292 parent_node_id: None,
293 });
294 }
295 let graph = crate::nodegraph::extract_graph(flow);
296 self.events.emit(Event::FlowGraph {
297 run_id: run_id.clone(),
298 graph: graph.clone(),
299 });
300 if let Some(sess) = session.as_ref() {
301 let _ = sess
302 .stream_tx()
303 .send(crate::stream::StreamFrame::FlowGraph {
304 run_id: run_id.0.to_string(),
305 graph,
306 });
307 }
308 let mut tool_ctx = self
311 .tool_ctx
312 .clone()
313 .with_invocation_env(invocation.env.clone());
314 tool_ctx.model_tool_exposures = Some(Default::default());
315 tool_ctx.flow_registry = Some(std::sync::Arc::clone(&flow_registry));
316 tool_ctx.permission_broker = Some(std::sync::Arc::clone(&permission_broker));
317 tool_ctx.trust = Some(trust);
318 tool_ctx.flow_identity = Some(identity);
319 tool_ctx.flow_run_id = Some(run_id.clone());
320 tool_ctx.session_id = Some(session_id);
321 if let Some(sess) = session.as_ref() {
322 tool_ctx.stream_tx = Some(sess.stream_tx());
323 tool_ctx.session_messages_handle = Some(sess.messages_handle());
324 let root_entry = sess.flow_registry.create_entry(
328 "root".to_string(),
329 sess.goal().unwrap_or_else(|| flow.name.name.clone()),
330 String::new(),
331 run_id.clone(),
332 );
333 tool_ctx.agent_entry = Some(std::sync::Arc::clone(&root_entry));
334 sess.set_current_root("root".to_string());
335 }
336 let exec_fut = exec_flow_with_siblings(
337 flow,
338 args,
339 &self.tools,
340 &tool_ctx,
341 &self.providers,
342 flows,
343 Some(&self.events),
344 turn_id,
345 Some(run_id.clone()),
346 session.clone(),
347 flow_cancel.clone(),
348 self.safety.as_ref(),
349 self.source_dir.clone(),
350 );
351 let result = tokio::select! {
352 biased;
353 _ = flow_cancel.cancelled() => Err(RuntimeError::Cancelled("flow cancelled by user".into())),
354 r = exec_fut => r,
355 };
356 let result = if let Err(RuntimeError::Cancelled(_)) = &result {
357 let suicide = task_id.as_ref().and_then(|id| {
358 self.tool_ctx
359 .task_registry
360 .as_ref()
361 .and_then(|tr| tr.lookup(id))
362 .and_then(|snap| snap.termination)
363 }) == Some(crate::task_registry::TaskTermination::Suicide);
364 if suicide {
365 Err(RuntimeError::Cancelled("flow terminated by suicide".into()))
366 } else {
367 result
368 }
369 } else {
370 result
371 };
372 let status = match &result {
373 Ok(v) => {
374 if let Value::Err(e) = v
375 && matches!(e, RuntimeError::Cancelled(_))
376 {
377 FlowStatus::Cancelled
378 } else {
379 FlowStatus::Ok
380 }
381 }
382 Err(e) => {
383 if matches!(e, RuntimeError::Cancelled(_)) {
384 FlowStatus::Cancelled
385 } else {
386 FlowStatus::Errored {
387 message: e.to_string(),
388 }
389 }
390 }
391 };
392 let cancelled = matches!(status, FlowStatus::Cancelled);
393 let suicide = task_id.as_ref().and_then(|id| {
394 self.tool_ctx
395 .task_registry
396 .as_ref()
397 .and_then(|tr| tr.lookup(id))
398 .and_then(|snap| snap.termination)
399 }) == Some(crate::task_registry::TaskTermination::Suicide);
400 if let (Some(tr), Some(tid)) = (self.tool_ctx.task_registry.as_ref(), &task_id) {
401 let ts = match &status {
402 FlowStatus::Ok => crate::task_registry::TaskStatus::Ok,
403 FlowStatus::Cancelled => crate::task_registry::TaskStatus::Killed,
404 FlowStatus::Errored { .. } => crate::task_registry::TaskStatus::Err,
405 };
406 tr.finish(tid, ts);
407 }
408 drop(_lifecycle_guard);
409 self.events.emit(Event::FlowEnd {
410 run_id: run_id.clone(),
411 flow_name: flow.name.name.clone(),
412 status: status.clone(),
413 });
414 if let Some(sess) = session.as_ref() {
415 let _ = sess.stream_tx().send(crate::stream::StreamFrame::FlowDone {
416 run_id: run_id.0.to_string(),
417 flow_name: flow.name.name.clone(),
418 ok: matches!(status, FlowStatus::Ok),
419 cancelled,
420 suicide,
421 });
422 }
423 result
424 }
425}
426
427impl Default for Executor {
428 fn default() -> Self {
429 Self::new()
430 }
431}
432
433fn root_task_label(
434 flow_name: &str,
435 args: &[(String, Value)],
436 session_goal: Option<String>,
437) -> String {
438 const USER_TEXT_KEYS: &[&str] = &["input", "prompt", "goal", "task", "message", "query"];
439
440 let named_user_text = USER_TEXT_KEYS.iter().find_map(|wanted| {
441 args.iter().find_map(|(name, value)| {
442 (name == wanted)
443 .then_some(value)
444 .and_then(|value| match value {
445 Value::Str(value) => crate::task_registry::normalize_task_label(value),
446 _ => None,
447 })
448 })
449 });
450 named_user_text
451 .or_else(|| {
452 args.iter().find_map(|(_, value)| match value {
453 Value::Str(value) => crate::task_registry::normalize_task_label(value),
454 _ => None,
455 })
456 })
457 .or_else(|| {
458 session_goal
459 .as_deref()
460 .and_then(crate::task_registry::normalize_task_label)
461 })
462 .unwrap_or_else(|| flow_name.to_owned())
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 #[test]
470 fn attached_provider_lifecycle_uses_and_retains_the_executor_registry() {
471 let dir = tempfile::tempdir().unwrap();
472 let mut executor = Executor::new();
473 let lifecycle = executor
474 .attach_provider_lifecycle(crate::config_hub::ConfigHub::from_config_dir(dir.path()))
475 .unwrap();
476
477 assert!(
478 lifecycle
479 .provider_registry()
480 .shares_storage_with(&executor.providers)
481 );
482 executor.tool_ctx = ToolCtx::new();
483 assert!(executor.provider_lifecycle().is_some());
484 let cloned = executor.clone();
485 drop(lifecycle);
486 drop(executor);
487 assert!(cloned.provider_lifecycle().is_some());
488 }
489
490 #[test]
491 fn provider_lifecycle_rejects_repeated_attachment() {
492 let first = tempfile::tempdir().unwrap();
493 let second = tempfile::tempdir().unwrap();
494 let mut executor = Executor::new();
495 executor
496 .attach_provider_lifecycle(crate::config_hub::ConfigHub::from_config_dir(first.path()))
497 .unwrap();
498
499 assert!(matches!(
500 executor.attach_provider_lifecycle(crate::config_hub::ConfigHub::from_config_dir(
501 second.path()
502 )),
503 Err(ProviderLifecycleAlreadyAttached)
504 ));
505 }
506
507 #[test]
508 fn root_task_title_prefers_named_user_text() {
509 let args = vec![
510 ("model".into(), Value::Str("gpt-example".into())),
511 ("input".into(), Value::Str(" 检查 当前项目 ".into())),
512 ];
513
514 assert_eq!(
515 root_task_label("agent", &args, Some("旧目标".into())),
516 "检查 当前项目"
517 );
518 }
519
520 #[test]
521 fn root_task_title_falls_back_to_goal_then_flow_name() {
522 assert_eq!(
523 root_task_label("agent", &[], Some(" 完成 审计 ".into())),
524 "完成 审计"
525 );
526 assert_eq!(root_task_label("agent", &[], None), "agent");
527 }
528
529 #[tokio::test]
530 async fn root_invocation_projects_tool_output_budget_into_session() {
531 let budget = crate::tools::tool_output::ToolOutputBudget {
532 max_lines: 7,
533 max_bytes: 777,
534 max_line_bytes: 111,
535 };
536 let mut executor = Executor::new();
537 executor.tool_ctx.tool_output_budget = budget;
538 let session = std::sync::Arc::new(Session::open_ephemeral());
539 let file = atman_dsl::parse::parse_file(
540 r#"flow start() -> string {
541 return "ok"
542}"#,
543 )
544 .unwrap();
545
546 executor
547 .run_in_turn(&file, "start", Vec::new(), None, Some(session.clone()))
548 .await
549 .unwrap();
550
551 assert_eq!(session.tool_output_budget(), budget);
552 }
553}