1use crate::error::RuntimeError;
2use crate::event::{Event, FlowRunId, FlowStatus};
3use crate::message::Message;
4use crate::tool::{ApprovalLevel, BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
5use crate::value::Value;
6use std::path::PathBuf;
7use std::sync::{Arc, Mutex};
8
9pub struct AgentSpawn;
10
11#[derive(Debug, Clone)]
12pub enum FlowRunStatus {
13 Running {
14 started_at: chrono::DateTime<chrono::Utc>,
15 },
16 Ok {
17 ended_at: chrono::DateTime<chrono::Utc>,
18 final_text: String,
19 },
20 Err {
21 ended_at: chrono::DateTime<chrono::Utc>,
22 message: String,
23 },
24 Killed {
25 ended_at: chrono::DateTime<chrono::Utc>,
26 },
27}
28
29impl FlowRunStatus {
30 pub fn is_running(&self) -> bool {
31 matches!(self, Self::Running { .. })
32 }
33
34 pub fn kind_str(&self) -> &'static str {
35 match self {
36 Self::Running { .. } => "running",
37 Self::Ok { .. } => "ok",
38 Self::Err { .. } => "err",
39 Self::Killed { .. } => "killed",
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
45pub enum FlowEvent {
46 AssistantDone { text: String },
47 Exited { status: FlowRunStatus },
48}
49
50pub struct FlowEntry {
51 pub handle: String,
52 pub goal: String,
53 pub status: Arc<Mutex<FlowRunStatus>>,
54 pub output: Arc<Mutex<String>>,
55 pub cancel: tokio_util::sync::CancellationToken,
56 pub stream_tx: tokio::sync::broadcast::Sender<FlowEvent>,
57 pub messages: Arc<Mutex<Vec<Message>>>,
58 pub iteration: Arc<std::sync::atomic::AtomicU64>,
59 pub child_run_id: FlowRunId,
60 pub model: String,
61 pub started_at: chrono::DateTime<chrono::Utc>,
62 pub compact_lock: Arc<tokio::sync::Mutex<()>>,
63 pub interjection_tx: tokio::sync::broadcast::Sender<crate::injection::Injection>,
64 pub pending_injections: Arc<std::sync::Mutex<Vec<crate::injection::Injection>>>,
65 pub injection_notify: Arc<tokio::sync::Notify>,
66 pub frame_tx: tokio::sync::broadcast::Sender<crate::stream::StreamFrame>,
67}
68
69impl crate::watch::Watchable for FlowEntry {
70 fn watch_output(
71 self: Arc<Self>,
72 pattern: String,
73 cancel: tokio_util::sync::CancellationToken,
74 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::watch::WatchResult> + Send>>
75 {
76 let stream_tx = self.stream_tx.clone();
77 let output = self.output.clone();
78 let status = self.status.clone();
79 Box::pin(async move {
80 {
81 let existing = output.lock().unwrap().clone();
82 if existing.find(&pattern).is_some() {
83 return crate::watch::WatchResult::Matched {
84 row: None,
85 col: None,
86 text: existing,
87 };
88 }
89 }
90 let mut rx = stream_tx.subscribe();
94 {
95 let st = status.lock().unwrap().clone();
96 if !st.is_running() {
97 if let FlowRunStatus::Ok { final_text, .. } = &st {
98 if final_text.find(&pattern).is_some() {
99 return crate::watch::WatchResult::Matched {
100 row: None,
101 col: None,
102 text: final_text.clone(),
103 };
104 }
105 }
106 return crate::watch::WatchResult::SourceExited;
107 }
108 }
109 loop {
110 tokio::select! {
111 _ = cancel.cancelled() => return crate::watch::WatchResult::Cancelled,
112 result = rx.recv() => match result {
113 Ok(FlowEvent::AssistantDone { text }) => {
114 if text.find(&pattern).is_some() {
115 return crate::watch::WatchResult::Matched {
116 row: None,
117 col: None,
118 text,
119 };
120 }
121 }
122 Ok(FlowEvent::Exited { status }) => {
123 if let FlowRunStatus::Ok { final_text, .. } = &status {
124 if final_text.find(&pattern).is_some() {
125 return crate::watch::WatchResult::Matched {
126 row: None,
127 col: None,
128 text: final_text.clone(),
129 };
130 }
131 }
132 return crate::watch::WatchResult::SourceExited;
133 }
134 Err(_) => return crate::watch::WatchResult::SourceExited,
135 }
136 }
137 }
138 })
139 }
140}
141
142#[derive(Default)]
143pub struct FlowRegistry {
144 entries: Mutex<std::collections::HashMap<String, Arc<FlowEntry>>>,
145}
146
147impl FlowRegistry {
148 pub fn new() -> Self {
149 Self::default()
150 }
151
152 pub fn create_entry(
153 &self,
154 handle: String,
155 goal: String,
156 model: String,
157 child_run_id: FlowRunId,
158 ) -> Arc<FlowEntry> {
159 let (stream_tx, _) = tokio::sync::broadcast::channel(64);
160 let entry = Arc::new(FlowEntry {
161 handle: handle.clone(),
162 goal,
163 status: Arc::new(Mutex::new(FlowRunStatus::Running {
164 started_at: chrono::Utc::now(),
165 })),
166 output: Arc::new(Mutex::new(String::new())),
167 cancel: tokio_util::sync::CancellationToken::new(),
168 stream_tx,
169 messages: Arc::new(Mutex::new(Vec::new())),
170 iteration: Arc::new(std::sync::atomic::AtomicU64::new(0)),
171 child_run_id,
172 model,
173 started_at: chrono::Utc::now(),
174 compact_lock: Arc::new(tokio::sync::Mutex::new(())),
175 interjection_tx: tokio::sync::broadcast::channel(32).0,
176 pending_injections: Arc::new(std::sync::Mutex::new(Vec::new())),
177 injection_notify: Arc::new(tokio::sync::Notify::new()),
178 frame_tx: tokio::sync::broadcast::channel(256).0,
179 });
180 self.entries
181 .lock()
182 .unwrap()
183 .insert(handle, Arc::clone(&entry));
184 entry
185 }
186
187 pub fn lookup(&self, handle: &str) -> Result<Arc<FlowEntry>, RuntimeError> {
188 self.entries
189 .lock()
190 .unwrap()
191 .get(handle)
192 .map(Arc::clone)
193 .ok_or_else(|| RuntimeError::ToolFailed(format!("agent: handle '{handle}' not found")))
194 }
195
196 pub fn remove(&self, handle: &str) {
197 self.entries.lock().unwrap().remove(handle);
198 }
199}
200
201impl Tool for AgentSpawn {
202 fn name(&self) -> &str {
203 "flow.spawn"
204 }
205
206 fn tier(&self) -> Tier {
207 Tier::Two
208 }
209
210 fn approval_level(&self, _args: &ToolArgs, _ctx: &ToolCtx) -> ApprovalLevel {
211 ApprovalLevel::Approve
212 }
213
214 fn description(&self) -> Option<&str> {
215 Some(
216 "Spawn a DSL flow as an independent sub-agent with its own message history and \
217 iteration counter. All named args except `flow` and `async` pass through to the \
218 flow as parameters — call flow.list first to discover available flows and their \
219 parameter signatures.\n\n\
220 Flow reference syntax: `file@flow_name`\n\
221 - \"subagent.at@subagent\" — run the `subagent` flow in subagent.at\n\
222 - \"subagent@research_loop\" — .at suffix optional\n\
223 - \"subagent.at\" — no @, takes first non-describe flow\n\
224 - \"/abs/path/my.at@main\" — absolute path\n\n\
225 Default flow is `subagent.at` (research/verify/implement/review roles). \
226 Required: `flow`. `async` is optional (default true). Other named args pass through to the flow. \
227 Use flow.status/flow.output/flow.kill to manage async sub-agents by handle. \
228 Best practice: call flow.list to see available flows and params, then pass \
229 matching named args. Missing params use flow-defined defaults.",
230 )
231 }
232
233 fn input_schema(&self) -> serde_json::Value {
234 serde_json::json!({
235 "type": "object",
236 "properties": {
237 "flow": {"type": "string", "description": "Flow reference (e.g. \"subagent.at@subagent\")."},
238 "arguments": {
239 "type": "object",
240 "additionalProperties": true,
241 "description": "Target flow parameters as key-value pairs. You MUST call flow.list first to discover the flow's description and parameter signatures (names, types, required/optional), then construct this object accordingly. Example: arguments={\"goal\":\"read Cargo.toml\",\"role\":\"research\"}"
242 },
243 "async": {"type": "boolean", "default": true, "description": "If true (default), run in background and return a handle. If false, block until done."},
244 "inherit_context": {"type": "boolean", "default": false, "description": "If true, seed the sub-agent's context with a snapshot of the parent's messages."}
245 },
246 "required": ["flow"]
247 })
248 }
249
250 fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
251 Box::pin(async move {
252 let is_async = args
253 .named("async")
254 .and_then(|v| {
255 if let Value::Bool(b) = v {
256 Some(*b)
257 } else {
258 None
259 }
260 })
261 .unwrap_or(true);
262 if is_async {
263 run_sub_agent_async(args, ctx).await
264 } else {
265 run_sub_agent(args, ctx).await
266 }
267 })
268 }
269}
270
271async fn run_sub_agent(args: ToolArgs, ctx: &ToolCtx) -> ToolResult {
272 let flow = extract_flow(&args)?.unwrap_or_else(|| "subagent.at".to_string());
273 run_flow_agent(&flow, &args, ctx, FlowRunId::now()).await
274}
275
276async fn run_sub_agent_async(args: ToolArgs, ctx: &ToolCtx) -> ToolResult {
277 let arg_fields: Vec<(String, Value)> = match args.named("arguments") {
279 Some(Value::Struct(fields)) => fields.clone(),
280 _ => Vec::new(),
281 };
282 let display_label = arg_fields
284 .iter()
285 .find_map(|(_, v)| {
286 if let Value::Str(s) = v {
287 Some(s.clone())
288 } else {
289 None
290 }
291 })
292 .unwrap_or_default();
293 let inherit_context = args
294 .named("inherit_context")
295 .map(|v| matches!(v, Value::Bool(true)))
296 .unwrap_or(false);
297 let flow_registry = ctx.flow_registry.clone().ok_or_else(|| {
298 RuntimeError::ToolFailed("flow.spawn: no agent registry available on ctx".into())
299 })?;
300
301 let flow_ref = extract_flow(&args)?.unwrap_or_else(|| "subagent.at".to_string());
302
303 let handle = format!("agent_{}", uuid::Uuid::now_v7().simple());
304 let child_run_id = FlowRunId::now();
305 let entry = flow_registry.create_entry(
306 handle.clone(),
307 display_label,
308 String::new(),
309 child_run_id.clone(),
310 );
311 if inherit_context {
312 if let Some(parent) = &ctx.session_messages_handle {
313 let snapshot = parent.lock().unwrap().clone();
314 *entry.messages.lock().unwrap() = snapshot;
315 }
316 }
317
318 let task_registry = ctx.task_registry.clone();
319 let session_id = ctx.session_id.clone().unwrap_or_else(|| "anon".into());
320 let task_id = task_registry.as_ref().map(|tr| {
321 tr.register(
322 crate::task_registry::TaskKind::Flow,
323 entry.goal.clone(),
324 handle.clone(),
325 session_id,
326 entry.cancel.clone(),
327 )
328 });
329
330 let entry_clone = Arc::clone(&entry);
331 let ctx_clone = ctx.clone();
332 let parent_stream_tx = ctx.stream_tx.clone();
333 let child_run_id_str = child_run_id.0.to_string();
334 tokio::spawn(async move {
335 if let Some(tx) = &parent_stream_tx {
338 let _ = tx.send(crate::stream::StreamFrame::SubAgentStarted {
339 handle: entry_clone.handle.clone(),
340 goal: entry_clone.goal.clone(),
341 child_run_id: child_run_id_str.clone(),
342 model: entry_clone.model.clone(),
343 });
344 }
345
346 let mut ctx_for_flow = ctx_clone;
352 ctx_for_flow.cancel = entry_clone.cancel.clone();
353 ctx_for_flow.agent_entry = Some(Arc::clone(&entry_clone));
354 ctx_for_flow.compact_lock_handle = Some(Arc::clone(&entry_clone.compact_lock));
355
356 let result = run_flow_agent(&flow_ref, &args, &ctx_for_flow, child_run_id.clone()).await;
357
358 let status = match &result {
359 Ok(Value::Str(s)) => FlowRunStatus::Ok {
360 ended_at: chrono::Utc::now(),
361 final_text: s.clone(),
362 },
363 Ok(_) => FlowRunStatus::Ok {
364 ended_at: chrono::Utc::now(),
365 final_text: String::new(),
366 },
367 Err(e) => FlowRunStatus::Err {
368 ended_at: chrono::Utc::now(),
369 message: e.to_string(),
370 },
371 };
372 *entry_clone.status.lock().unwrap() = status.clone();
373
374 if let Some(tx) = &parent_stream_tx {
376 let final_text = match &status {
377 FlowRunStatus::Ok { final_text, .. } => final_text.clone(),
378 _ => String::new(),
379 };
380 let _ = tx.send(crate::stream::StreamFrame::SubAgentDone {
381 handle: entry_clone.handle.clone(),
382 status: status.kind_str().to_string(),
383 final_text,
384 });
385 }
386
387 let _ = entry_clone.stream_tx.send(FlowEvent::Exited { status });
388 if let (Some(tr), Some(tid)) = (&task_registry, &task_id) {
389 let ts = match result {
390 Ok(_) => crate::task_registry::TaskStatus::Ok,
391 Err(_) => crate::task_registry::TaskStatus::Err,
392 };
393 tr.finish(tid, ts);
394 }
395 });
396
397 Ok(Value::Struct(vec![
398 ("handle".into(), Value::Str(handle)),
399 ("status".into(), Value::Str("running".into())),
400 ]))
401}
402
403pub struct AgentStatus;
404impl Tool for AgentStatus {
405 fn name(&self) -> &str {
406 "flow.status"
407 }
408 fn tier(&self) -> Tier {
409 Tier::Zero
410 }
411 fn description(&self) -> Option<&str> {
412 Some("Check the status of an async sub-agent. Returns handle, status, goal, and timing.")
413 }
414 fn input_schema(&self) -> serde_json::Value {
415 serde_json::json!({
416 "type": "object",
417 "properties": {"handle": {"type": "string"}},
418 "required": ["handle"]
419 })
420 }
421 fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
422 Box::pin(async move {
423 let handle = extract_string(&args, "handle", 0)?;
424 let reg = ctx
425 .flow_registry
426 .clone()
427 .ok_or_else(|| RuntimeError::ToolFailed("flow.status: no agent registry".into()))?;
428 let entry = reg.lookup(&handle)?;
429 let st = entry.status.lock().unwrap().clone();
430 let goal = entry.goal.clone();
431 let mut fields = vec![
432 ("handle".into(), Value::Str(handle)),
433 ("status".into(), Value::Str(st.kind_str().into())),
434 ("goal".into(), Value::Str(goal)),
435 ];
436 if let FlowRunStatus::Err { message, .. } = &st {
437 fields.push(("error".into(), Value::Str(message.clone())));
438 }
439 Ok(Value::Struct(fields))
440 })
441 }
442}
443
444pub struct AgentOutput;
445impl Tool for AgentOutput {
446 fn name(&self) -> &str {
447 "flow.output"
448 }
449 fn tier(&self) -> Tier {
450 Tier::Zero
451 }
452 fn description(&self) -> Option<&str> {
453 Some("Read accumulated assistant text from an async sub-agent.")
454 }
455 fn input_schema(&self) -> serde_json::Value {
456 serde_json::json!({
457 "type": "object",
458 "properties": {
459 "handle": {"type": "string"},
460 "cursor": {"type": "integer", "default": 0},
461 "limit": {"type": "integer", "default": 4096}
462 },
463 "required": ["handle"]
464 })
465 }
466 fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
467 Box::pin(async move {
468 let handle = extract_string(&args, "handle", 0)?;
469 let cursor = args
470 .named("cursor")
471 .and_then(|v| {
472 if let Value::Int(n) = v {
473 Some(*n)
474 } else {
475 None
476 }
477 })
478 .unwrap_or(0)
479 .max(0) as usize;
480 let limit = args
481 .named("limit")
482 .and_then(|v| {
483 if let Value::Int(n) = v {
484 Some(*n)
485 } else {
486 None
487 }
488 })
489 .unwrap_or(4096)
490 .max(1) as usize;
491 let reg = ctx
492 .flow_registry
493 .clone()
494 .ok_or_else(|| RuntimeError::ToolFailed("flow.output: no agent registry".into()))?;
495 let entry = reg.lookup(&handle)?;
496 let output = entry.output.lock().unwrap().clone();
497 let chunk = output.chars().skip(cursor).take(limit).collect::<String>();
498 let next_cursor = cursor + chunk.chars().count();
499 let eof = next_cursor >= output.chars().count();
500 Ok(Value::Struct(vec![
501 ("handle".into(), Value::Str(handle)),
502 ("chunk".into(), Value::Str(chunk)),
503 ("cursor".into(), Value::Int(cursor as i64)),
504 ("next_cursor".into(), Value::Int(next_cursor as i64)),
505 ("eof".into(), Value::Bool(eof)),
506 ]))
507 })
508 }
509}
510
511pub struct AgentKill;
512impl Tool for AgentKill {
513 fn name(&self) -> &str {
514 "flow.kill"
515 }
516 fn tier(&self) -> Tier {
517 Tier::Four
518 }
519 fn description(&self) -> Option<&str> {
520 Some("Cancel a running async sub-agent by handle.")
521 }
522 fn input_schema(&self) -> serde_json::Value {
523 serde_json::json!({
524 "type": "object",
525 "properties": {"handle": {"type": "string"}},
526 "required": ["handle"]
527 })
528 }
529 fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
530 Box::pin(async move {
531 let handle = extract_string(&args, "handle", 0)?;
532 let reg = ctx
533 .flow_registry
534 .clone()
535 .ok_or_else(|| RuntimeError::ToolFailed("flow.kill: no agent registry".into()))?;
536 let entry = reg.lookup(&handle)?;
537 entry.cancel.cancel();
538 Ok(Value::Unit)
539 })
540 }
541}
542
543pub struct FlowInterject;
544impl Tool for FlowInterject {
545 fn name(&self) -> &str {
546 "flow.interject"
547 }
548 fn tier(&self) -> Tier {
549 Tier::Two
550 }
551 fn approval_level(&self, _args: &ToolArgs, _ctx: &ToolCtx) -> ApprovalLevel {
552 ApprovalLevel::Approve
553 }
554 fn description(&self) -> Option<&str> {
555 Some(
556 "Interject a text message into a running FlowRun (root or sub-agent) by handle. \
557 L1 (nudge): inject text into context, flow continues. \
558 L2 (course_correct): inject text, cancel current LLM call, flow continues with correction. \
559 L3 (redirect): cancel current LLM call, redirect to a different flow. \
560 L4 (hard_stop): kill the FlowRun immediately. \
561 Use handle \"root\" to interject into the main agent.",
562 )
563 }
564 fn input_schema(&self) -> serde_json::Value {
565 serde_json::json!({
566 "type": "object",
567 "properties": {
568 "handle": {"type": "string", "description": "Target FlowRun handle (e.g. from flow.spawn return, or \"root\")."},
569 "text": {"type": "string", "description": "Interjection text."},
570 "level": {"type": "string", "enum": ["l1_nudge", "l2_course_correct", "l3_redirect", "l4_hard_stop"], "default": "l1_nudge", "description": "Interjection level."},
571 "redirect_target": {"type": "string", "description": "Required for L3 redirect: the flow to redirect to."}
572 },
573 "required": ["handle", "text"]
574 })
575 }
576 fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
577 Box::pin(async move {
578 let handle = extract_string(&args, "handle", 0)?;
579 let text = extract_string(&args, "text", 1)?;
580 let level_str = args
581 .named("level")
582 .and_then(|v| {
583 if let Value::Str(s) = v {
584 Some(s.clone())
585 } else {
586 None
587 }
588 })
589 .unwrap_or_else(|| "l1_nudge".to_string());
590 let level = match level_str.as_str() {
591 "l2_course_correct" => crate::injection::InjectionLevel::L2CourseCorrect,
592 "l3_redirect" => crate::injection::InjectionLevel::L3Redirect,
593 "l4_hard_stop" => crate::injection::InjectionLevel::L4HardStop,
594 _ => crate::injection::InjectionLevel::L1Nudge,
595 };
596 let redirect_target = args.named("redirect_target").and_then(|v| {
597 if let Value::Str(s) = v {
598 Some(s.clone())
599 } else {
600 None
601 }
602 });
603 let reg = ctx.flow_registry.clone().ok_or_else(|| {
604 RuntimeError::ToolFailed("flow.interject: no agent registry".into())
605 })?;
606 let entry = reg.lookup(&handle)?;
607 let inj = crate::injection::Injection::with_level(
608 crate::event::TurnId::now(),
609 text,
610 level,
611 redirect_target,
612 );
613 entry.pending_injections.lock().unwrap().push(inj);
614 entry.injection_notify.notify_one();
615 Ok(Value::Unit)
616 })
617 }
618}
619
620fn extract_string(args: &ToolArgs, name: &str, pos: usize) -> Result<String, RuntimeError> {
621 let value = match args.named(name) {
622 Some(v) => v,
623 None => args.positional(pos)?,
624 };
625 match value {
626 Value::Str(s) => Ok(s.clone()),
627 other => Err(RuntimeError::TypeMismatch {
628 expected: "string".into(),
629 actual: other.kind_name().into(),
630 }),
631 }
632}
633
634async fn run_flow_agent(
635 flow_ref: &str,
636 args: &ToolArgs,
637 ctx: &ToolCtx,
638 run_id: FlowRunId,
639) -> ToolResult {
640 let Some(registry) = ctx.registry.as_ref() else {
641 return Err(RuntimeError::ToolFailed(
642 "flow.spawn: no tool registry available on ctx".into(),
643 ));
644 };
645 let Some(providers) = ctx.providers.as_ref() else {
646 return Err(RuntimeError::ToolFailed(
647 "flow.spawn: no provider registry available on ctx".into(),
648 ));
649 };
650 let (file_part, flow_name) = match flow_ref.split_once('@') {
651 Some((f, n)) => (f, Some(n.to_string())),
652 None => (flow_ref, None),
653 };
654 let (path, src) = read_flow_source(file_part).await?;
655 let file = atman_dsl::parse::parse_file(&src).map_err(|e| {
656 RuntimeError::ToolFailed(format!("flow.spawn: parse {}: {e}", path.display()))
657 })?;
658 let flow = match &flow_name {
659 Some(name) => file
660 .flows
661 .iter()
662 .find(|f| f.name.name == *name)
663 .ok_or_else(|| {
664 let available: Vec<&str> =
665 file.flows.iter().map(|f| f.name.name.as_str()).collect();
666 RuntimeError::ToolFailed(format!(
667 "flow.spawn: flow `{name}` not found in {}. available: {}",
668 path.display(),
669 available.join(", ")
670 ))
671 })?,
672 None => file
673 .flows
674 .iter()
675 .find(|f| f.name.name != "describe")
676 .ok_or_else(|| {
677 RuntimeError::ToolFailed(format!(
678 "flow.spawn: no entry flow in {} (all flows are describe())",
679 path.display()
680 ))
681 })?,
682 };
683 let mut flow_args: Vec<(String, Value)> = Vec::new();
686 if let Some(Value::Struct(fields)) = args.named("arguments") {
687 for (key, value) in fields {
688 if key == "flow" || key == "async" || key == "inherit_context" {
689 continue;
690 }
691 if flow.params.iter().any(|p| p.name.name == *key) {
692 flow_args.push((key.clone(), value.clone()));
693 }
694 }
695 }
696 for (key, value) in &args.named {
698 if key == "flow" || key == "async" || key == "inherit_context" || key == "arguments" {
699 continue;
700 }
701 if flow.params.iter().any(|p| p.name.name == *key)
702 && !flow_args.iter().any(|(k, _)| k == key)
703 {
704 flow_args.push((key.clone(), value.clone()));
705 }
706 }
707 let flows = file
708 .flows
709 .iter()
710 .map(|flow| (flow.name.name.clone(), flow.clone()))
711 .collect();
712 emit_flow_agent_start(ctx, &run_id, &flow.name.name);
713 let mut child_ctx = sanitize_child_ctx(ctx);
714 let inherit = args
717 .named("inherit_context")
718 .map(|v| matches!(v, Value::Bool(true)))
719 .unwrap_or(false);
720 child_ctx.session_messages_handle = match &ctx.agent_entry {
721 Some(entry) => Some(std::sync::Arc::clone(&entry.messages)),
722 None => {
723 let handle: std::sync::Arc<std::sync::Mutex<Vec<_>>> =
724 std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
725 if inherit {
726 if let Some(parent) = &ctx.session_messages_handle {
727 *handle.lock().unwrap() = parent.lock().unwrap().clone();
728 }
729 }
730 Some(handle)
731 }
732 };
733 if ctx.agent_entry.is_none() {
736 child_ctx.compact_lock_handle = Some(std::sync::Arc::new(tokio::sync::Mutex::new(())));
737 }
738 let out = crate::exec::exec_flow_with_siblings(
739 flow,
740 flow_args,
741 registry.as_ref(),
742 &child_ctx,
743 providers.as_ref(),
744 &flows,
745 child_ctx.events.as_ref(),
746 child_ctx.turn_id.clone(),
747 Some(run_id.clone()),
748 None,
749 child_ctx.cancel.clone(),
750 None,
751 path.parent().map(|p| p.to_path_buf()),
752 )
753 .await;
754 let status = match &out {
755 Ok(_) => FlowStatus::Ok,
756 Err(e) => FlowStatus::Errored {
757 message: e.to_string(),
758 },
759 };
760 emit_child_flow_end(ctx, &run_id, &status);
761 out
762}
763
764fn extract_flow(args: &ToolArgs) -> Result<Option<String>, RuntimeError> {
765 match args.named("flow") {
766 Some(Value::Str(s)) if !s.trim().is_empty() => Ok(Some(s.clone())),
767 Some(Value::Unit) | None => Ok(None),
768 Some(other) => Err(RuntimeError::TypeMismatch {
769 expected: "flow string".into(),
770 actual: other.kind_name().into(),
771 }),
772 }
773}
774
775async fn read_flow_source(flow_ref: &str) -> Result<(PathBuf, String), RuntimeError> {
776 for path in flow_candidates(flow_ref) {
777 match tokio::fs::read_to_string(&path).await {
778 Ok(src) => return Ok((path, src)),
779 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
780 Err(e) => {
781 return Err(RuntimeError::ToolFailed(format!(
782 "flow.spawn: read {}: {e}",
783 path.display()
784 )));
785 }
786 }
787 }
788 Err(RuntimeError::ToolFailed(format!(
789 "flow.spawn: flow `{flow_ref}` not found"
790 )))
791}
792
793fn flow_candidates(flow_ref: &str) -> Vec<PathBuf> {
794 let path = PathBuf::from(flow_ref);
795 if path.is_absolute() {
796 return vec![path];
797 }
798 let file_name = if flow_ref.ends_with(".at") {
799 flow_ref.to_string()
800 } else {
801 format!("{flow_ref}.at")
802 };
803 let mut out = Vec::new();
804 if let Some(home) = std::env::var_os("HOME") {
805 out.push(
806 PathBuf::from(home)
807 .join(".config")
808 .join("atman")
809 .join("commands")
810 .join(&file_name),
811 );
812 }
813 out.push(PathBuf::from(file_name));
814 out
815}
816
817fn emit_flow_agent_start(ctx: &ToolCtx, run_id: &FlowRunId, flow_name: &str) {
818 let parent_run_id = ctx.flow_run_id.clone();
819 let parent_node_id = ctx.current_node_id.clone();
820 if let Some(sink) = &ctx.events {
821 sink.emit(Event::FlowStart {
822 run_id: run_id.clone(),
823 flow_name: flow_name.into(),
824 parent_run_id: parent_run_id.clone(),
825 parent_node_id: parent_node_id.clone(),
826 spawned: true,
827 });
828 }
829 if let Some(tx) = &ctx.stream_tx {
830 let _ = tx.send(crate::stream::StreamFrame::FlowStart {
831 run_id: run_id.0.to_string(),
832 flow_name: flow_name.into(),
833 parent_run_id: parent_run_id.as_ref().map(|r| r.0.to_string()),
834 parent_node_id,
835 });
836 }
837}
838
839fn emit_child_flow_end(ctx: &ToolCtx, run_id: &FlowRunId, status: &FlowStatus) {
840 if let Some(sink) = &ctx.events {
841 sink.emit(Event::FlowEnd {
842 run_id: run_id.clone(),
843 flow_name: "agent.sub".into(),
844 status: status.clone(),
845 });
846 }
847 if let Some(tx) = &ctx.stream_tx {
848 let _ = tx.send(crate::stream::StreamFrame::FlowDone {
849 run_id: run_id.0.to_string(),
850 flow_name: "agent.sub".into(),
851 ok: matches!(status, FlowStatus::Ok),
852 cancelled: false,
853 });
854 }
855}
856
857fn sanitize_child_ctx(parent: &ToolCtx) -> ToolCtx {
858 let mut c = parent.clone();
859 c.session_runtime = None;
860 c.history_segment = crate::tool::HistorySegment::Spawned;
861 c.session_messages_handle = None;
862 c.compact_lock_handle = None;
863 c.forms = None;
864 c.on_memory_recent = None;
865 c
866}