Skip to main content

atman_runtime/tools/
agent_ctrl.rs

1use crate::error::RuntimeError;
2use crate::event::{Event, FlowRunId, FlowStatus};
3use crate::git_workspace::{
4    WorkspaceBinding, WorkspaceFinalizeOutcome, WorkspacePolicy, WorkspaceState,
5};
6use crate::message::Message;
7use crate::tool::{ApprovalLevel, BoxFut, Tier, Tool, ToolArgs, ToolCtx, ToolResult};
8use crate::value::Value;
9use std::path::PathBuf;
10use std::sync::{Arc, Mutex};
11
12pub struct AgentSpawn;
13
14#[derive(Debug, Clone)]
15pub enum FlowRunStatus {
16    Running {
17        started_at: chrono::DateTime<chrono::Utc>,
18    },
19    Ok {
20        ended_at: chrono::DateTime<chrono::Utc>,
21        final_text: String,
22    },
23    Err {
24        ended_at: chrono::DateTime<chrono::Utc>,
25        message: String,
26    },
27    Killed {
28        ended_at: chrono::DateTime<chrono::Utc>,
29    },
30}
31
32impl FlowRunStatus {
33    pub fn is_running(&self) -> bool {
34        matches!(self, Self::Running { .. })
35    }
36
37    pub fn kind_str(&self) -> &'static str {
38        match self {
39            Self::Running { .. } => "running",
40            Self::Ok { .. } => "ok",
41            Self::Err { .. } => "err",
42            Self::Killed { .. } => "killed",
43        }
44    }
45}
46
47#[derive(Debug, Clone)]
48pub enum FlowEvent {
49    AssistantDone { text: String },
50    Exited { status: FlowRunStatus },
51}
52
53pub struct FlowEntry {
54    pub handle: String,
55    pub goal: String,
56    pub display_label: String,
57    pub status: Arc<Mutex<FlowRunStatus>>,
58    pub output: Arc<Mutex<String>>,
59    pub cancel: tokio_util::sync::CancellationToken,
60    pub stream_tx: tokio::sync::broadcast::Sender<FlowEvent>,
61    pub messages: Arc<Mutex<Vec<Message>>>,
62    pub iteration: Arc<std::sync::atomic::AtomicU64>,
63    pub child_run_id: FlowRunId,
64    pub model: String,
65    pub started_at: chrono::DateTime<chrono::Utc>,
66    pub compact_lock: Arc<tokio::sync::Mutex<()>>,
67    pub interjection_tx: tokio::sync::broadcast::Sender<crate::injection::Injection>,
68    pub pending_injections: Arc<std::sync::Mutex<Vec<crate::injection::Injection>>>,
69    pub injection_notify: Arc<tokio::sync::Notify>,
70    pub frame_tx: tokio::sync::broadcast::Sender<crate::stream::StreamFrame>,
71    pub workspace: Option<WorkspaceBinding>,
72    pub workspace_state: Arc<Mutex<Option<WorkspaceState>>>,
73    pub cleanup_error: Arc<Mutex<Option<String>>>,
74}
75
76impl crate::watch::Watchable for FlowEntry {
77    fn watch_output(
78        self: Arc<Self>,
79        pattern: String,
80        cancel: tokio_util::sync::CancellationToken,
81    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::watch::WatchResult> + Send>>
82    {
83        let stream_tx = self.stream_tx.clone();
84        let output = self.output.clone();
85        let status = self.status.clone();
86        Box::pin(async move {
87            {
88                let existing = output.lock().unwrap().clone();
89                if existing.find(&pattern).is_some() {
90                    return crate::watch::WatchResult::Matched {
91                        row: None,
92                        col: None,
93                        text: existing,
94                    };
95                }
96            }
97            // Subscribe BEFORE checking status — if the source exits between
98            // subscribe and the status check, the broadcast event arrives via rx.
99            // If it exits before subscribe, the status check catches it.
100            let mut rx = stream_tx.subscribe();
101            {
102                let st = status.lock().unwrap().clone();
103                if !st.is_running() {
104                    if let FlowRunStatus::Ok { final_text, .. } = &st {
105                        if final_text.find(&pattern).is_some() {
106                            return crate::watch::WatchResult::Matched {
107                                row: None,
108                                col: None,
109                                text: final_text.clone(),
110                            };
111                        }
112                    }
113                    return crate::watch::WatchResult::SourceExited;
114                }
115            }
116            loop {
117                tokio::select! {
118                    _ = cancel.cancelled() => return crate::watch::WatchResult::Cancelled,
119                    result = rx.recv() => match result {
120                        Ok(FlowEvent::AssistantDone { text }) => {
121                            if text.find(&pattern).is_some() {
122                                return crate::watch::WatchResult::Matched {
123                                    row: None,
124                                    col: None,
125                                    text,
126                                };
127                            }
128                        }
129                        Ok(FlowEvent::Exited { status }) => {
130                            if let FlowRunStatus::Ok { final_text, .. } = &status {
131                                if final_text.find(&pattern).is_some() {
132                                    return crate::watch::WatchResult::Matched {
133                                        row: None,
134                                        col: None,
135                                        text: final_text.clone(),
136                                    };
137                                }
138                            }
139                            return crate::watch::WatchResult::SourceExited;
140                        }
141                        Err(_) => return crate::watch::WatchResult::SourceExited,
142                    }
143                }
144            }
145        })
146    }
147}
148
149/// Observes flow terminal transitions while the lifecycle arbitration is held, so
150/// subsystems keyed by run liveness can be updated inside the same linearization point.
151pub(crate) trait FlowTerminalObserver: Send + Sync {
152    fn flow_became_terminal(
153        &self,
154        session_id: &str,
155        run_id: &FlowRunId,
156    ) -> Option<Box<dyn FnOnce() + Send>>;
157}
158
159#[derive(Default)]
160pub struct FlowRegistry {
161    entries: Mutex<std::collections::HashMap<String, Arc<FlowEntry>>>,
162    runs: Mutex<std::collections::HashMap<FlowRunId, Arc<crate::flow_authority::FlowIdentity>>>,
163    /// Serializes every identity/execution-state transition. Held around observer
164    /// notification so a run cannot go terminal between a liveness check and a
165    /// decision commit in another subsystem. Lock order: lifecycle -> runs -> identity.
166    lifecycle: Mutex<()>,
167    terminal_observers: Mutex<Vec<std::sync::Weak<dyn FlowTerminalObserver>>>,
168}
169
170pub struct DescendantBlockGuard {
171    registry: Arc<FlowRegistry>,
172    parent_run_id: FlowRunId,
173    child_run_id: FlowRunId,
174}
175
176pub struct FlowLifecycleGuard {
177    registry: Arc<FlowRegistry>,
178    run_id: FlowRunId,
179}
180
181impl Drop for DescendantBlockGuard {
182    fn drop(&mut self) {
183        self.registry
184            .unblock_descendant(&self.parent_run_id, &self.child_run_id);
185    }
186}
187
188impl Drop for FlowLifecycleGuard {
189    fn drop(&mut self) {
190        self.registry.mark_terminal(&self.run_id);
191    }
192}
193
194impl FlowRegistry {
195    pub fn new() -> Self {
196        Self::default()
197    }
198
199    /// Runs `f` under the lifecycle arbitration. Callers must not already hold it,
200    /// and must not acquire it again from inside `f`.
201    pub(crate) fn with_lifecycle_arbitration<T>(&self, f: impl FnOnce() -> T) -> T {
202        let _lifecycle = self.lifecycle.lock().unwrap_or_else(|e| e.into_inner());
203        f()
204    }
205
206    pub(crate) fn register_terminal_observer(
207        &self,
208        observer: std::sync::Weak<dyn FlowTerminalObserver>,
209    ) {
210        let mut observers = self.terminal_observers.lock().unwrap();
211        observers.retain(|existing| existing.strong_count() > 0);
212        observers.push(observer);
213    }
214
215    pub fn register_root(
216        &self,
217        session_id: String,
218        run_id: FlowRunId,
219        authority: crate::flow_authority::EffectiveAuthority,
220    ) -> Result<Arc<crate::flow_authority::FlowIdentity>, RuntimeError> {
221        self.with_lifecycle_arbitration(|| self.register_root_locked(session_id, run_id, authority))
222    }
223
224    fn register_root_locked(
225        &self,
226        session_id: String,
227        run_id: FlowRunId,
228        authority: crate::flow_authority::EffectiveAuthority,
229    ) -> Result<Arc<crate::flow_authority::FlowIdentity>, RuntimeError> {
230        let identity = Arc::new(crate::flow_authority::FlowIdentity {
231            session_id,
232            run_id: run_id.clone(),
233            parent_run_id: None,
234            root_run_id: run_id.clone(),
235            invocation: crate::flow_authority::InvocationKind::Root,
236            effective_authority: authority,
237            execution_state: Mutex::new(crate::flow_authority::FlowExecutionState::Running),
238        });
239        let mut runs = self.runs.lock().unwrap();
240        if runs.contains_key(&run_id) {
241            return Err(RuntimeError::ToolFailed(format!(
242                "flow identity '{run_id}' is already registered"
243            )));
244        }
245        runs.insert(run_id, Arc::clone(&identity));
246        Ok(identity)
247    }
248
249    pub fn register_child(
250        &self,
251        parent_run_id: &FlowRunId,
252        child_run_id: FlowRunId,
253        invocation: crate::flow_authority::InvocationKind,
254        contract_allows_shell: bool,
255        workspace: crate::flow_authority::ChildWorkspaceAuthority,
256    ) -> Result<Arc<crate::flow_authority::FlowIdentity>, RuntimeError> {
257        self.with_lifecycle_arbitration(|| {
258            self.register_child_locked(
259                parent_run_id,
260                child_run_id,
261                invocation,
262                contract_allows_shell,
263                workspace,
264            )
265        })
266    }
267
268    fn register_child_locked(
269        &self,
270        parent_run_id: &FlowRunId,
271        child_run_id: FlowRunId,
272        invocation: crate::flow_authority::InvocationKind,
273        contract_allows_shell: bool,
274        workspace: crate::flow_authority::ChildWorkspaceAuthority,
275    ) -> Result<Arc<crate::flow_authority::FlowIdentity>, RuntimeError> {
276        if invocation == crate::flow_authority::InvocationKind::Root {
277            return Err(RuntimeError::ToolFailed(
278                "child flow identity cannot use root invocation".into(),
279            ));
280        }
281        let mut runs = self.runs.lock().unwrap();
282        if runs.contains_key(&child_run_id) {
283            return Err(RuntimeError::ToolFailed(format!(
284                "flow identity '{child_run_id}' is already registered"
285            )));
286        }
287        let parent = runs.get(parent_run_id).cloned().ok_or_else(|| {
288            RuntimeError::ToolFailed(format!(
289                "parent flow identity '{parent_run_id}' is not registered"
290            ))
291        })?;
292        if matches!(
293            parent.execution_state(),
294            crate::flow_authority::FlowExecutionState::Terminal
295        ) {
296            return Err(RuntimeError::ToolFailed(format!(
297                "parent flow identity '{parent_run_id}' is terminal"
298            )));
299        }
300        let identity = Arc::new(crate::flow_authority::FlowIdentity {
301            session_id: parent.session_id.clone(),
302            run_id: child_run_id.clone(),
303            parent_run_id: Some(parent_run_id.clone()),
304            root_run_id: parent.root_run_id.clone(),
305            invocation,
306            effective_authority: parent
307                .effective_authority
308                .inherited_child(contract_allows_shell, workspace)
309                .map_err(|error| RuntimeError::ToolFailed(format!("child authority: {error}")))?,
310            execution_state: Mutex::new(crate::flow_authority::FlowExecutionState::Running),
311        });
312        runs.insert(child_run_id, Arc::clone(&identity));
313        Ok(identity)
314    }
315
316    pub fn lookup_run(
317        &self,
318        run_id: &FlowRunId,
319    ) -> Option<Arc<crate::flow_authority::FlowIdentity>> {
320        self.runs.lock().unwrap().get(run_id).cloned()
321    }
322
323    pub fn is_strict_ancestor(&self, ancestor: &FlowRunId, descendant: &FlowRunId) -> bool {
324        if ancestor == descendant {
325            return false;
326        }
327        let runs = self.runs.lock().unwrap();
328        let Some(ancestor_identity) = runs.get(ancestor) else {
329            return false;
330        };
331        let Some(mut current) = runs.get(descendant).cloned() else {
332            return false;
333        };
334        if ancestor_identity.session_id != current.session_id {
335            return false;
336        }
337        let mut visited = std::collections::HashSet::new();
338        while let Some(parent_run_id) = current.parent_run_id.as_ref() {
339            if !visited.insert(current.run_id.clone()) {
340                return false;
341            }
342            if parent_run_id == ancestor {
343                return true;
344            }
345            let Some(parent) = runs.get(parent_run_id) else {
346                return false;
347            };
348            if parent.session_id != ancestor_identity.session_id {
349                return false;
350            }
351            current = Arc::clone(parent);
352        }
353        false
354    }
355
356    pub fn strict_ancestors(
357        &self,
358        run_id: &FlowRunId,
359    ) -> Vec<Arc<crate::flow_authority::FlowIdentity>> {
360        let runs = self.runs.lock().unwrap();
361        let Some(start) = runs.get(run_id) else {
362            return Vec::new();
363        };
364        let session_id = start.session_id.clone();
365        let mut current = Arc::clone(start);
366        let mut ancestors = Vec::new();
367        let mut visited = std::collections::HashSet::new();
368        while let Some(parent_run_id) = current.parent_run_id.as_ref() {
369            if !visited.insert(current.run_id.clone()) {
370                return Vec::new();
371            }
372            let Some(parent) = runs.get(parent_run_id) else {
373                return Vec::new();
374            };
375            if parent.session_id != session_id {
376                return Vec::new();
377            }
378            ancestors.push(Arc::clone(parent));
379            current = Arc::clone(parent);
380        }
381        ancestors
382    }
383
384    pub fn execution_state(
385        &self,
386        run_id: &FlowRunId,
387    ) -> Option<crate::flow_authority::FlowExecutionState> {
388        self.lookup_run(run_id)
389            .map(|identity| identity.execution_state())
390    }
391
392    pub fn mark_terminal(&self, run_id: &FlowRunId) {
393        let completions = self.with_lifecycle_arbitration(|| self.mark_terminal_locked(run_id));
394        for completion in completions {
395            completion();
396        }
397    }
398
399    fn mark_terminal_locked(&self, run_id: &FlowRunId) -> Vec<Box<dyn FnOnce() + Send>> {
400        let Some(identity) = self.lookup_run(run_id) else {
401            return Vec::new();
402        };
403        *identity.execution_state.lock().unwrap() =
404            crate::flow_authority::FlowExecutionState::Terminal;
405        let observers: Vec<_> = {
406            let mut observers = self.terminal_observers.lock().unwrap();
407            observers.retain(|existing| existing.strong_count() > 0);
408            observers
409                .iter()
410                .filter_map(std::sync::Weak::upgrade)
411                .collect()
412        };
413        observers
414            .into_iter()
415            .filter_map(|observer| observer.flow_became_terminal(&identity.session_id, run_id))
416            .collect()
417    }
418
419    pub fn lifecycle_guard(self: &Arc<Self>, run_id: &FlowRunId) -> FlowLifecycleGuard {
420        FlowLifecycleGuard {
421            registry: Arc::clone(self),
422            run_id: run_id.clone(),
423        }
424    }
425
426    pub fn block_on_descendant(
427        self: &Arc<Self>,
428        parent_run_id: &FlowRunId,
429        child_run_id: &FlowRunId,
430    ) -> Result<DescendantBlockGuard, RuntimeError> {
431        self.with_lifecycle_arbitration(|| {
432            self.block_on_descendant_locked(parent_run_id, child_run_id)
433        })
434    }
435
436    fn block_on_descendant_locked(
437        self: &Arc<Self>,
438        parent_run_id: &FlowRunId,
439        child_run_id: &FlowRunId,
440    ) -> Result<DescendantBlockGuard, RuntimeError> {
441        if !self.is_strict_ancestor(parent_run_id, child_run_id) {
442            return Err(RuntimeError::ToolFailed(format!(
443                "flow '{parent_run_id}' is not a strict ancestor of '{child_run_id}'"
444            )));
445        }
446        let parent = self.lookup_run(parent_run_id).ok_or_else(|| {
447            RuntimeError::ToolFailed(format!("flow identity '{parent_run_id}' is not registered"))
448        })?;
449        let mut state = parent.execution_state.lock().unwrap();
450        match &mut *state {
451            crate::flow_authority::FlowExecutionState::Running => {
452                *state = crate::flow_authority::FlowExecutionState::BlockedOnDescendants {
453                    child_run_counts: std::collections::HashMap::from([(child_run_id.clone(), 1)]),
454                };
455            }
456            crate::flow_authority::FlowExecutionState::BlockedOnDescendants {
457                child_run_counts,
458            } => {
459                *child_run_counts.entry(child_run_id.clone()).or_default() += 1;
460            }
461            crate::flow_authority::FlowExecutionState::Terminal => {
462                return Err(RuntimeError::ToolFailed(format!(
463                    "flow identity '{parent_run_id}' is terminal"
464                )));
465            }
466        }
467        drop(state);
468        Ok(DescendantBlockGuard {
469            registry: Arc::clone(self),
470            parent_run_id: parent_run_id.clone(),
471            child_run_id: child_run_id.clone(),
472        })
473    }
474
475    fn unblock_descendant(&self, parent_run_id: &FlowRunId, child_run_id: &FlowRunId) {
476        self.with_lifecycle_arbitration(|| {
477            self.unblock_descendant_locked(parent_run_id, child_run_id)
478        });
479    }
480
481    fn unblock_descendant_locked(&self, parent_run_id: &FlowRunId, child_run_id: &FlowRunId) {
482        let Some(parent) = self.lookup_run(parent_run_id) else {
483            return;
484        };
485        let mut state = parent.execution_state.lock().unwrap();
486        let crate::flow_authority::FlowExecutionState::BlockedOnDescendants { child_run_counts } =
487            &mut *state
488        else {
489            return;
490        };
491        if let Some(count) = child_run_counts.get_mut(child_run_id) {
492            *count -= 1;
493            if *count == 0 {
494                child_run_counts.remove(child_run_id);
495            }
496        }
497        if child_run_counts.is_empty() {
498            *state = crate::flow_authority::FlowExecutionState::Running;
499        }
500    }
501
502    pub fn create_entry(
503        &self,
504        handle: String,
505        goal: String,
506        model: String,
507        child_run_id: FlowRunId,
508    ) -> Arc<FlowEntry> {
509        self.create_entry_with_workspace(handle, goal, model, child_run_id, None)
510    }
511
512    pub fn create_entry_with_workspace(
513        &self,
514        handle: String,
515        goal: String,
516        model: String,
517        child_run_id: FlowRunId,
518        workspace: Option<WorkspaceBinding>,
519    ) -> Arc<FlowEntry> {
520        let display_label = goal.clone();
521        self.create_entry_with_workspace_label(
522            handle,
523            goal,
524            display_label,
525            model,
526            child_run_id,
527            workspace,
528        )
529    }
530
531    fn create_entry_with_workspace_label(
532        &self,
533        handle: String,
534        goal: String,
535        display_label: String,
536        model: String,
537        child_run_id: FlowRunId,
538        workspace: Option<WorkspaceBinding>,
539    ) -> Arc<FlowEntry> {
540        let (stream_tx, _) = tokio::sync::broadcast::channel(64);
541        let entry = Arc::new(FlowEntry {
542            handle: handle.clone(),
543            goal,
544            display_label,
545            status: Arc::new(Mutex::new(FlowRunStatus::Running {
546                started_at: chrono::Utc::now(),
547            })),
548            output: Arc::new(Mutex::new(String::new())),
549            cancel: tokio_util::sync::CancellationToken::new(),
550            stream_tx,
551            messages: Arc::new(Mutex::new(Vec::new())),
552            iteration: Arc::new(std::sync::atomic::AtomicU64::new(0)),
553            child_run_id,
554            model,
555            started_at: chrono::Utc::now(),
556            compact_lock: Arc::new(tokio::sync::Mutex::new(())),
557            interjection_tx: tokio::sync::broadcast::channel(32).0,
558            pending_injections: Arc::new(std::sync::Mutex::new(Vec::new())),
559            injection_notify: Arc::new(tokio::sync::Notify::new()),
560            frame_tx: tokio::sync::broadcast::channel(256).0,
561            workspace_state: Arc::new(Mutex::new(
562                workspace.as_ref().map(|_| WorkspaceState::Active),
563            )),
564            workspace,
565            cleanup_error: Arc::new(Mutex::new(None)),
566        });
567        self.entries
568            .lock()
569            .unwrap()
570            .insert(handle, Arc::clone(&entry));
571        entry
572    }
573
574    pub fn lookup(&self, handle: &str) -> Result<Arc<FlowEntry>, RuntimeError> {
575        self.entries
576            .lock()
577            .unwrap()
578            .get(handle)
579            .map(Arc::clone)
580            .ok_or_else(|| RuntimeError::ToolFailed(format!("agent: handle '{handle}' not found")))
581    }
582
583    pub fn remove(&self, handle: &str) {
584        self.entries.lock().unwrap().remove(handle);
585    }
586
587    pub fn is_empty(&self) -> bool {
588        self.entries.lock().unwrap().is_empty()
589    }
590}
591
592impl Tool for AgentSpawn {
593    fn name(&self) -> &str {
594        "flow.spawn"
595    }
596
597    fn tier(&self) -> Tier {
598        Tier::Two
599    }
600
601    fn approval_level(&self, _args: &ToolArgs, _ctx: &ToolCtx) -> ApprovalLevel {
602        ApprovalLevel::Approve
603    }
604
605    fn description(&self) -> Option<&str> {
606        Some(
607            "Spawn a DSL flow as an independent sub-agent with its own message history and \
608             iteration counter. All named args except `flow` and `async` pass through to the \
609             flow as parameters. Use flow.search for bounded discovery and flow.describe for \
610             the selected parameter contract.\n\n\
611             Flow reference syntax: `file@flow_name`\n\
612             - \"subagent.at@subagent\" — run the `subagent` flow in subagent.at\n\
613             - \"subagent@research_loop\" — .at suffix optional\n\
614             - \"subagent.at\" — no @, takes first non-describe flow\n\
615             - \"/abs/path/my.at@main\" — absolute path\n\n\
616             Default flow is `subagent.at` (research/verify/implement/review roles). \
617             Required: `flow`. `version` rejects source changes after discovery. `async` is optional (default true). \
618             Other named args pass through to the flow. \
619             A flow may declare `contract { invocation { user_message: param } }` to seed its child message context. \
620             Use flow.status/flow.output/flow.kill to manage async sub-agents by handle. \
621             Pass only parameters declared by flow.describe. Missing params use flow-defined defaults.",
622        )
623    }
624
625    fn input_schema(&self) -> serde_json::Value {
626        serde_json::json!({
627            "type": "object",
628            "properties": {
629                "flow": {"type": "string", "description": "Flow reference (e.g. \"subagent.at@subagent\")."},
630                "version": {"type": "string", "minLength": 1, "description": "Staleness guard. Pass the non-empty source fingerprint returned by the current flow.search or flow.describe result verbatim. Omit this field when no fingerprint is available; never send an empty or invented value."},
631                "arguments": {
632                    "type": "object",
633                    "additionalProperties": true,
634                    "description": "Target flow parameters as key-value pairs. Use flow.search then flow.describe when the parameter contract is unknown. Example: arguments={\"goal\":\"read Cargo.toml\",\"role\":\"research\"}"
635                },
636                "async": {"type": "boolean", "default": true, "description": "If true (default), run in background and return a handle. If false, block until done."},
637                "inherit_context": {"type": "boolean", "default": false, "description": "If true, seed the sub-agent's context with the parent snapshot through its last complete tool transaction. Every child also receives an explicit handoff.parent context record."},
638                "workspace": {"type": "string", "enum": ["none", "auto", "retain"], "default": "none", "description": "Workspace policy for the child flow."}
639            },
640            "required": ["flow"]
641        })
642    }
643
644    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
645        Box::pin(async move {
646            let is_async = args
647                .named("async")
648                .and_then(|v| {
649                    if let Value::Bool(b) = v {
650                        Some(*b)
651                    } else {
652                        None
653                    }
654                })
655                .unwrap_or(true);
656            if is_async {
657                run_sub_agent_async(args, ctx).await
658            } else {
659                run_sub_agent(args, ctx).await
660            }
661        })
662    }
663}
664
665fn workspace_policy(args: &ToolArgs) -> Result<WorkspacePolicy, RuntimeError> {
666    match args.named("workspace") {
667        None => Ok(WorkspacePolicy::None),
668        Some(Value::Str(value)) => value.parse().map_err(|error| {
669            RuntimeError::ToolFailed(format!("flow.spawn: invalid workspace policy: {error}"))
670        }),
671        Some(_) => Err(RuntimeError::ToolFailed(
672            "flow.spawn: workspace must be one of none, auto, or retain".into(),
673        )),
674    }
675}
676
677fn workspace_session(
678    ctx: &ToolCtx,
679    policy: WorkspacePolicy,
680) -> Result<Option<String>, RuntimeError> {
681    if policy == WorkspacePolicy::None {
682        return Ok(ctx.session_id.clone().filter(|id| !id.is_empty()));
683    }
684    ctx.session_id
685        .clone()
686        .filter(|id| !id.is_empty())
687        .map(Some)
688        .ok_or_else(|| {
689            RuntimeError::ToolFailed(
690                "flow.spawn: workspace auto/retain requires a non-empty session id".into(),
691            )
692        })
693}
694
695fn allocate_workspace(
696    ctx: &ToolCtx,
697    policy: WorkspacePolicy,
698    session_id: Option<&str>,
699    run_id: &FlowRunId,
700) -> Result<Option<WorkspaceBinding>, RuntimeError> {
701    if policy == WorkspacePolicy::None {
702        return Ok(None);
703    }
704    let service = ctx.flow_workspace_service.as_ref().ok_or_else(|| {
705        RuntimeError::ToolFailed("flow.spawn: workspace service is unavailable".into())
706    })?;
707    service
708        .allocate(
709            policy,
710            session_id.expect("validated managed workspace session"),
711            &run_id.0.to_string(),
712            ctx.workspace.as_ref().map(|binding| binding.path.as_path()),
713        )
714        .map_err(|error| RuntimeError::ToolFailed(format!("flow.spawn: {error}")))
715}
716
717struct WorkspaceFinalizeGuard {
718    ctx: ToolCtx,
719    binding: Option<WorkspaceBinding>,
720    session_id: Option<String>,
721    run_id: FlowRunId,
722    state_projection: Option<Arc<Mutex<Option<WorkspaceState>>>>,
723    error_projection: Option<Arc<Mutex<Option<String>>>>,
724}
725
726impl WorkspaceFinalizeGuard {
727    fn new(
728        ctx: &ToolCtx,
729        binding: Option<WorkspaceBinding>,
730        session_id: Option<String>,
731        run_id: FlowRunId,
732    ) -> Self {
733        Self {
734            ctx: ctx.clone(),
735            binding,
736            session_id,
737            run_id,
738            state_projection: None,
739            error_projection: None,
740        }
741    }
742
743    fn with_projections(
744        mut self,
745        state_projection: Arc<Mutex<Option<WorkspaceState>>>,
746        error_projection: Arc<Mutex<Option<String>>>,
747    ) -> Self {
748        self.state_projection = Some(state_projection);
749        self.error_projection = Some(error_projection);
750        self
751    }
752
753    fn binding(&self) -> Option<&WorkspaceBinding> {
754        self.binding.as_ref()
755    }
756}
757
758impl Drop for WorkspaceFinalizeGuard {
759    fn drop(&mut self) {
760        finalize_workspace(
761            &self.ctx,
762            self.binding.as_ref(),
763            self.session_id.as_deref(),
764            &self.run_id,
765            self.state_projection.as_ref(),
766            self.error_projection.as_ref(),
767        );
768    }
769}
770
771fn finalize_workspace(
772    ctx: &ToolCtx,
773    binding: Option<&WorkspaceBinding>,
774    session_id: Option<&str>,
775    run_id: &FlowRunId,
776    state_projection: Option<&Arc<Mutex<Option<WorkspaceState>>>>,
777    error_projection: Option<&Arc<Mutex<Option<String>>>>,
778) {
779    let Some(binding) = binding else {
780        return;
781    };
782    let service = ctx.flow_workspace_service.as_ref();
783    let result = service
784        .ok_or_else(|| "workspace service is unavailable".to_string())
785        .and_then(|service| {
786            service
787                .finalize(
788                    binding,
789                    session_id.unwrap_or_default(),
790                    &run_id.0.to_string(),
791                )
792                .map_err(|error| error.to_string())
793        });
794    let (state, cleanup_error) = match result {
795        Ok(outcome) => {
796            let record = match outcome {
797                WorkspaceFinalizeOutcome::Released(record)
798                | WorkspaceFinalizeOutcome::Dirty(record)
799                | WorkspaceFinalizeOutcome::Retained(record)
800                | WorkspaceFinalizeOutcome::AlreadyReleased(record) => record,
801            };
802            (record.lifecycle_state(), None)
803        }
804        Err(error) => (
805            service
806                .and_then(|service| service.persisted_state(binding))
807                .unwrap_or_else(|| WorkspaceState::Unknown("unknown".into())),
808            Some(error),
809        ),
810    };
811    if let Some(projection) = state_projection {
812        *projection.lock().unwrap() = Some(state.clone());
813    }
814    if let Some(projection) = error_projection {
815        *projection.lock().unwrap() = cleanup_error.clone();
816    }
817    if let Some(sink) = &ctx.events {
818        sink.emit(Event::WorkspaceLifecycle {
819            run_id: run_id.clone(),
820            workspace_id: binding.workspace_id.clone(),
821            path: binding.path.display().to_string(),
822            state: state.as_str().into(),
823            cleanup_error,
824        });
825    }
826}
827
828struct PreparedFlowAgent {
829    path: PathBuf,
830    flow: atman_dsl::ast::FlowDecl,
831    flows: std::collections::HashMap<String, atman_dsl::ast::FlowDecl>,
832}
833
834async fn prepare_flow_agent(
835    flow_ref: &str,
836    expected_version: Option<&str>,
837) -> Result<PreparedFlowAgent, RuntimeError> {
838    let (file_part, flow_name) = match flow_ref.split_once('@') {
839        Some((file, name)) => (file, Some(name)),
840        None => (flow_ref, None),
841    };
842    let (path, source) = read_flow_source(file_part).await?;
843    let actual_version = format!("blake3:{}", blake3::hash(source.as_bytes()).to_hex());
844    if expected_version.is_some_and(|version| version != actual_version) {
845        return Err(RuntimeError::ToolFailed(format!(
846            "flow.spawn: stale version for `{flow_ref}`; search again"
847        )));
848    }
849    let file = atman_dsl::parse::parse_file(&source).map_err(|error| {
850        RuntimeError::ToolFailed(format!("flow.spawn: parse {}: {error}", path.display()))
851    })?;
852    let flow = match flow_name {
853        Some(name) => file.flows.iter().find(|flow| flow.name.name == name),
854        None => file.flows.iter().find(|flow| flow.name.name != "describe"),
855    }
856    .cloned()
857    .ok_or_else(|| {
858        RuntimeError::ToolFailed(format!(
859            "flow.spawn: target flow not found in {}",
860            path.display()
861        ))
862    })?;
863    let flows = file
864        .flows
865        .into_iter()
866        .map(|flow| (flow.name.name.clone(), flow))
867        .collect();
868    Ok(PreparedFlowAgent { path, flow, flows })
869}
870
871fn register_prepared_identity(
872    prepared: &PreparedFlowAgent,
873    ctx: &ToolCtx,
874    child_run_id: &FlowRunId,
875    invocation: crate::flow_authority::InvocationKind,
876    workspace: crate::flow_authority::ChildWorkspaceAuthority,
877) -> Result<Arc<crate::flow_authority::FlowIdentity>, RuntimeError> {
878    let registry = ctx.flow_registry.as_ref().ok_or_else(|| {
879        RuntimeError::ToolFailed("flow.spawn: trusted flow registry is unavailable".into())
880    })?;
881    let parent = ctx.flow_identity.as_ref().ok_or_else(|| {
882        RuntimeError::ToolFailed("flow.spawn: trusted parent flow identity is unavailable".into())
883    })?;
884    registry.register_child(
885        &parent.run_id,
886        child_run_id.clone(),
887        invocation,
888        crate::flow_authority::contract_allows_shell(prepared.flow.contract.as_ref()),
889        workspace,
890    )
891}
892
893fn spawned_workspace_authority(
894    binding: Option<&WorkspaceBinding>,
895) -> crate::flow_authority::ChildWorkspaceAuthority {
896    match binding {
897        Some(binding) => {
898            crate::flow_authority::ChildWorkspaceAuthority::TrustedDelegation(binding.path.clone())
899        }
900        None => crate::flow_authority::ChildWorkspaceAuthority::Inherit,
901    }
902}
903
904async fn run_sub_agent(args: ToolArgs, ctx: &ToolCtx) -> ToolResult {
905    let flow = extract_flow(&args)?.unwrap_or_else(|| "subagent.at".to_string());
906    let version = extract_flow_version(&args)?;
907    let prepared = prepare_flow_agent(&flow, version.as_deref()).await?;
908    let flow_args = resolve_flow_arguments(&prepared.flow, &args)?;
909    let inherit_context = should_inherit_context(&args);
910    let run_id = FlowRunId::now();
911    let policy = workspace_policy(&args)?;
912    let session_id = workspace_session(ctx, policy)?;
913    let workspace_guard = WorkspaceFinalizeGuard::new(
914        ctx,
915        allocate_workspace(ctx, policy, session_id.as_deref(), &run_id)?,
916        session_id,
917        run_id.clone(),
918    );
919    let child_identity = register_prepared_identity(
920        &prepared,
921        ctx,
922        &run_id,
923        crate::flow_authority::InvocationKind::SpawnSync,
924        spawned_workspace_authority(workspace_guard.binding()),
925    )?;
926    let flow_registry = ctx.flow_registry.as_ref().expect("validated flow registry");
927    let _lifecycle_guard = flow_registry.lifecycle_guard(&run_id);
928    let parent_run_id = ctx
929        .flow_identity
930        .as_ref()
931        .expect("validated parent flow identity")
932        .run_id
933        .clone();
934    let _block_guard = flow_registry.block_on_descendant(&parent_run_id, &run_id)?;
935    let mut child_ctx = match workspace_guard.binding().cloned() {
936        Some(binding) => ctx.clone().with_workspace(binding),
937        None => ctx.clone(),
938    };
939    child_ctx.flow_run_id = Some(run_id.clone());
940    child_ctx.flow_identity = Some(child_identity);
941    child_ctx.call_intent = None;
942    let child_messages = Arc::new(Mutex::new(Vec::new()));
943    if inherit_context && let Some(parent) = &ctx.session_messages_handle {
944        *child_messages.lock().unwrap() = inherited_context_snapshot(parent);
945    }
946    run_prepared_flow_agent(
947        prepared,
948        flow_args,
949        &child_ctx,
950        run_id,
951        child_messages,
952        Arc::new(tokio::sync::Mutex::new(())),
953        inherit_context,
954    )
955    .await
956}
957
958async fn run_sub_agent_async(args: ToolArgs, ctx: &ToolCtx) -> ToolResult {
959    // Use the first string-typed argument as a display label for the FlowEntry.
960    let goal = match args.named("arguments") {
961        Some(Value::Struct(fields)) => fields.iter().find_map(|(_, value)| match value {
962            Value::Str(value) => Some(value.clone()),
963            _ => None,
964        }),
965        _ => None,
966    }
967    .unwrap_or_default();
968    let display_label = ctx
969        .call_intent
970        .as_ref()
971        .map(|intent| intent.as_str().to_string())
972        .unwrap_or_else(|| goal.clone());
973    let inherit_context = should_inherit_context(&args);
974    let flow_registry = ctx.flow_registry.clone().ok_or_else(|| {
975        RuntimeError::ToolFailed("flow.spawn: no agent registry available on ctx".into())
976    })?;
977
978    let flow_ref = extract_flow(&args)?.unwrap_or_else(|| "subagent.at".to_string());
979    let version = extract_flow_version(&args)?;
980    let prepared = prepare_flow_agent(&flow_ref, version.as_deref()).await?;
981    let flow_args = resolve_flow_arguments(&prepared.flow, &args)?;
982    let model = flow_args
983        .iter()
984        .find_map(|(name, value)| match (name.as_str(), value) {
985            ("model", Value::Str(model)) => Some(model.clone()),
986            _ => None,
987        })
988        .or_else(|| {
989            prepared
990                .flow
991                .params
992                .iter()
993                .find(|parameter| parameter.name.name == "model")
994                .and_then(|parameter| match parameter.default.as_ref() {
995                    Some(atman_dsl::ast::Expr::Literal(atman_dsl::ast::Literal::Str(model))) => {
996                        Some(model.clone())
997                    }
998                    _ => None,
999                })
1000        })
1001        .unwrap_or_default();
1002
1003    let handle = format!("agent_{}", uuid::Uuid::now_v7().simple());
1004    let child_run_id = FlowRunId::now();
1005    let policy = workspace_policy(&args)?;
1006    let session_id = workspace_session(ctx, policy)?;
1007    let mut workspace_guard = WorkspaceFinalizeGuard::new(
1008        ctx,
1009        allocate_workspace(ctx, policy, session_id.as_deref(), &child_run_id)?,
1010        session_id.clone(),
1011        child_run_id.clone(),
1012    );
1013    let child_identity = register_prepared_identity(
1014        &prepared,
1015        ctx,
1016        &child_run_id,
1017        crate::flow_authority::InvocationKind::SpawnAsync,
1018        spawned_workspace_authority(workspace_guard.binding()),
1019    )?;
1020    let lifecycle_guard = flow_registry.lifecycle_guard(&child_run_id);
1021    let workspace = workspace_guard.binding().cloned();
1022    let entry = flow_registry.create_entry_with_workspace_label(
1023        handle.clone(),
1024        goal,
1025        display_label,
1026        model,
1027        child_run_id.clone(),
1028        workspace.clone(),
1029    );
1030    workspace_guard = workspace_guard.with_projections(
1031        Arc::clone(&entry.workspace_state),
1032        Arc::clone(&entry.cleanup_error),
1033    );
1034    if inherit_context {
1035        if let Some(parent) = &ctx.session_messages_handle {
1036            let snapshot = inherited_context_snapshot(parent);
1037            *entry.messages.lock().unwrap() = snapshot;
1038        }
1039    }
1040
1041    let task_registry = ctx.task_registry.clone();
1042    let task_id = task_registry.as_ref().map(|tr| {
1043        tr.register_flow_with_run_id(
1044            entry.display_label.clone(),
1045            handle.clone(),
1046            session_id.clone().unwrap_or_default(),
1047            entry.cancel.clone(),
1048            workspace
1049                .as_ref()
1050                .map(|binding| binding.workspace_id.clone()),
1051            child_run_id.clone(),
1052        )
1053    });
1054
1055    let entry_clone = Arc::clone(&entry);
1056    let ctx_clone = ctx.clone();
1057    let parent_stream_tx = ctx.stream_tx.clone();
1058    let child_run_id_str = child_run_id.0.to_string();
1059    tokio::spawn(async move {
1060        let _lifecycle_guard = lifecycle_guard;
1061        let _workspace_guard = workspace_guard;
1062        // Send SubAgentStarted so the TUI creates a SubAgentActivity item and
1063        // registers child_run_id in sub_agent_run_ids for frame routing.
1064        if let Some(tx) = &parent_stream_tx {
1065            let _ = tx.send(crate::stream::StreamFrame::SubAgentStarted {
1066                handle: entry_clone.handle.clone(),
1067                goal: entry_clone.display_label.clone(),
1068                child_run_id: child_run_id_str.clone(),
1069                model: entry_clone.model.clone(),
1070            });
1071        }
1072
1073        // Replace ctx.cancel with entry.cancel so flow.kill can actually cancel
1074        // the sub-agent's flow execution. Pass entry into ctx so the DSL runtime
1075        // can write output/messages/iteration synchronously during LLM calls.
1076        // Bind the sub-agent's own compact_lock so it can compact its segment
1077        // independently.
1078        let mut ctx_for_flow = ctx_clone;
1079        ctx_for_flow.cancel = entry_clone.cancel.clone();
1080        ctx_for_flow.call_intent = None;
1081        ctx_for_flow.agent_entry = Some(Arc::clone(&entry_clone));
1082        ctx_for_flow.flow_run_id = Some(child_run_id.clone());
1083        ctx_for_flow.flow_identity = Some(child_identity);
1084        ctx_for_flow.compact_lock_handle = Some(Arc::clone(&entry_clone.compact_lock));
1085        if let Some(binding) = entry_clone.workspace.clone() {
1086            ctx_for_flow = ctx_for_flow.with_workspace(binding);
1087        }
1088
1089        let result = run_prepared_flow_agent(
1090            prepared,
1091            flow_args,
1092            &ctx_for_flow,
1093            child_run_id.clone(),
1094            Arc::clone(&entry_clone.messages),
1095            Arc::clone(&entry_clone.compact_lock),
1096            inherit_context,
1097        )
1098        .await;
1099        let killed = entry_clone.cancel.is_cancelled();
1100        let status = match &result {
1101            _ if killed => FlowRunStatus::Killed {
1102                ended_at: chrono::Utc::now(),
1103            },
1104            Ok(Value::Str(s)) => FlowRunStatus::Ok {
1105                ended_at: chrono::Utc::now(),
1106                final_text: s.clone(),
1107            },
1108            Ok(_) => FlowRunStatus::Ok {
1109                ended_at: chrono::Utc::now(),
1110                final_text: String::new(),
1111            },
1112            Err(e) => FlowRunStatus::Err {
1113                ended_at: chrono::Utc::now(),
1114                message: e.to_string(),
1115            },
1116        };
1117
1118        *entry_clone.status.lock().unwrap() = status.clone();
1119
1120        // Send SubAgentDone so the TUI updates the SubAgentActivity item.
1121        if let Some(tx) = &parent_stream_tx {
1122            let final_text = match &status {
1123                FlowRunStatus::Ok { final_text, .. } => final_text.clone(),
1124                _ => String::new(),
1125            };
1126            let _ = tx.send(crate::stream::StreamFrame::SubAgentDone {
1127                handle: entry_clone.handle.clone(),
1128                status: status.kind_str().to_string(),
1129                final_text,
1130            });
1131        }
1132
1133        let _ = entry_clone.stream_tx.send(FlowEvent::Exited { status });
1134        if let (Some(tr), Some(tid)) = (&task_registry, &task_id) {
1135            let ts = if killed {
1136                crate::task_registry::TaskStatus::Killed
1137            } else {
1138                match result {
1139                    Ok(_) => crate::task_registry::TaskStatus::Ok,
1140                    Err(_) => crate::task_registry::TaskStatus::Err,
1141                }
1142            };
1143            tr.finish(tid, ts);
1144        }
1145    });
1146
1147    let mut fields = vec![
1148        ("handle".into(), Value::Str(handle)),
1149        ("status".into(), Value::Str("running".into())),
1150    ];
1151    if let Some(binding) = workspace {
1152        fields.push(("workspace_id".into(), Value::Str(binding.workspace_id)));
1153        fields.push((
1154            "workspace_path".into(),
1155            Value::Str(binding.path.display().to_string()),
1156        ));
1157        fields.push((
1158            "workspace_state".into(),
1159            Value::Str(WorkspaceState::Active.as_str().into()),
1160        ));
1161    }
1162    Ok(Value::Struct(fields))
1163}
1164
1165pub struct AgentStatus;
1166impl Tool for AgentStatus {
1167    fn name(&self) -> &str {
1168        "flow.status"
1169    }
1170    fn tier(&self) -> Tier {
1171        Tier::Zero
1172    }
1173    fn description(&self) -> Option<&str> {
1174        Some("Check the status of an async sub-agent. Returns handle, status, goal, and timing.")
1175    }
1176    fn input_schema(&self) -> serde_json::Value {
1177        serde_json::json!({
1178            "type": "object",
1179            "properties": {"handle": {"type": "string"}},
1180            "required": ["handle"]
1181        })
1182    }
1183    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
1184        Box::pin(async move {
1185            let handle = extract_string(&args, "handle", 0)?;
1186            let reg = ctx
1187                .flow_registry
1188                .clone()
1189                .ok_or_else(|| RuntimeError::ToolFailed("flow.status: no agent registry".into()))?;
1190            let entry = reg.lookup(&handle)?;
1191            let st = entry.status.lock().unwrap().clone();
1192            let goal = entry.goal.clone();
1193            let mut fields = vec![
1194                ("handle".into(), Value::Str(handle)),
1195                ("status".into(), Value::Str(st.kind_str().into())),
1196                ("goal".into(), Value::Str(goal)),
1197            ];
1198            if let FlowRunStatus::Err { message, .. } = &st {
1199                fields.push(("error".into(), Value::Str(message.clone())));
1200            }
1201            if let Some(binding) = &entry.workspace {
1202                fields.push((
1203                    "workspace_id".into(),
1204                    Value::Str(binding.workspace_id.clone()),
1205                ));
1206                fields.push((
1207                    "workspace_path".into(),
1208                    Value::Str(binding.path.display().to_string()),
1209                ));
1210                if let Some(state) = entry.workspace_state.lock().unwrap().clone() {
1211                    fields.push(("workspace_state".into(), Value::Str(state.as_str().into())));
1212                }
1213                if let Some(error) = entry.cleanup_error.lock().unwrap().clone() {
1214                    fields.push(("cleanup_error".into(), Value::Str(error)));
1215                }
1216            }
1217            Ok(Value::Struct(fields))
1218        })
1219    }
1220}
1221
1222pub struct AgentOutput;
1223impl Tool for AgentOutput {
1224    fn name(&self) -> &str {
1225        "flow.output"
1226    }
1227    fn tier(&self) -> Tier {
1228        Tier::Zero
1229    }
1230    fn description(&self) -> Option<&str> {
1231        Some("Read accumulated assistant text from an async sub-agent.")
1232    }
1233    fn input_schema(&self) -> serde_json::Value {
1234        serde_json::json!({
1235            "type": "object",
1236            "properties": {
1237                "handle": {"type": "string"},
1238                "cursor": {"type": "integer", "default": 0},
1239                "limit": {"type": "integer", "default": 4096}
1240            },
1241            "required": ["handle"]
1242        })
1243    }
1244    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
1245        Box::pin(async move {
1246            let handle = extract_string(&args, "handle", 0)?;
1247            let cursor = args
1248                .named("cursor")
1249                .and_then(|v| {
1250                    if let Value::Int(n) = v {
1251                        Some(*n)
1252                    } else {
1253                        None
1254                    }
1255                })
1256                .unwrap_or(0)
1257                .max(0) as usize;
1258            let limit = args
1259                .named("limit")
1260                .and_then(|v| {
1261                    if let Value::Int(n) = v {
1262                        Some(*n)
1263                    } else {
1264                        None
1265                    }
1266                })
1267                .unwrap_or(4096)
1268                .max(1) as usize;
1269            let reg = ctx
1270                .flow_registry
1271                .clone()
1272                .ok_or_else(|| RuntimeError::ToolFailed("flow.output: no agent registry".into()))?;
1273            let entry = reg.lookup(&handle)?;
1274            let output = entry.output.lock().unwrap().clone();
1275            let chunk = output.chars().skip(cursor).take(limit).collect::<String>();
1276            let next_cursor = cursor + chunk.chars().count();
1277            let eof = next_cursor >= output.chars().count();
1278            Ok(Value::Struct(vec![
1279                ("handle".into(), Value::Str(handle)),
1280                ("chunk".into(), Value::Str(chunk)),
1281                ("cursor".into(), Value::Int(cursor as i64)),
1282                ("next_cursor".into(), Value::Int(next_cursor as i64)),
1283                ("eof".into(), Value::Bool(eof)),
1284            ]))
1285        })
1286    }
1287}
1288
1289pub struct AgentKill;
1290impl Tool for AgentKill {
1291    fn name(&self) -> &str {
1292        "flow.kill"
1293    }
1294    fn tier(&self) -> Tier {
1295        Tier::Four
1296    }
1297    fn description(&self) -> Option<&str> {
1298        Some("Cancel a running async sub-agent by handle.")
1299    }
1300    fn input_schema(&self) -> serde_json::Value {
1301        serde_json::json!({
1302            "type": "object",
1303            "properties": {"handle": {"type": "string"}},
1304            "required": ["handle"]
1305        })
1306    }
1307    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
1308        Box::pin(async move {
1309            let handle = extract_string(&args, "handle", 0)?;
1310            let reg = ctx
1311                .flow_registry
1312                .clone()
1313                .ok_or_else(|| RuntimeError::ToolFailed("flow.kill: no agent registry".into()))?;
1314            let entry = reg.lookup(&handle)?;
1315            entry.cancel.cancel();
1316            Ok(Value::Unit)
1317        })
1318    }
1319}
1320
1321pub struct FlowInterject;
1322impl Tool for FlowInterject {
1323    fn name(&self) -> &str {
1324        "flow.interject"
1325    }
1326    fn tier(&self) -> Tier {
1327        Tier::Two
1328    }
1329    fn approval_level(&self, _args: &ToolArgs, _ctx: &ToolCtx) -> ApprovalLevel {
1330        ApprovalLevel::Approve
1331    }
1332    fn description(&self) -> Option<&str> {
1333        Some(
1334            "Interject a text message into a running FlowRun (root or sub-agent) by handle. \
1335             L1 (nudge): inject text into context, flow continues. \
1336             L2 (course_correct): inject text, cancel current LLM call, flow continues with correction. \
1337             L3 (redirect): cancel current LLM call, redirect to a different flow. \
1338             L4 (hard_stop): kill the FlowRun immediately. \
1339             Use handle \"root\" to interject into the main agent.",
1340        )
1341    }
1342    fn input_schema(&self) -> serde_json::Value {
1343        serde_json::json!({
1344            "type": "object",
1345            "properties": {
1346                "handle": {"type": "string", "description": "Target FlowRun handle (e.g. from flow.spawn return, or \"root\")."},
1347                "text": {"type": "string", "description": "Interjection text."},
1348                "level": {"type": "string", "enum": ["l1_nudge", "l2_course_correct", "l3_redirect", "l4_hard_stop"], "default": "l1_nudge", "description": "Interjection level."},
1349                "redirect_target": {"type": "string", "description": "Required for L3 redirect: the flow to redirect to."}
1350            },
1351            "required": ["handle", "text"]
1352        })
1353    }
1354    fn call<'a>(&'a self, args: ToolArgs, ctx: &'a ToolCtx) -> BoxFut<'a, ToolResult> {
1355        Box::pin(async move {
1356            let handle = extract_string(&args, "handle", 0)?;
1357            let text = extract_string(&args, "text", 1)?;
1358            let level_str = args
1359                .named("level")
1360                .and_then(|v| {
1361                    if let Value::Str(s) = v {
1362                        Some(s.clone())
1363                    } else {
1364                        None
1365                    }
1366                })
1367                .unwrap_or_else(|| "l1_nudge".to_string());
1368            let level = match level_str.as_str() {
1369                "l2_course_correct" => crate::injection::InjectionLevel::L2CourseCorrect,
1370                "l3_redirect" => crate::injection::InjectionLevel::L3Redirect,
1371                "l4_hard_stop" => crate::injection::InjectionLevel::L4HardStop,
1372                _ => crate::injection::InjectionLevel::L1Nudge,
1373            };
1374            let redirect_target = args.named("redirect_target").and_then(|v| {
1375                if let Value::Str(s) = v {
1376                    Some(s.clone())
1377                } else {
1378                    None
1379                }
1380            });
1381            let reg = ctx.flow_registry.clone().ok_or_else(|| {
1382                RuntimeError::ToolFailed("flow.interject: no agent registry".into())
1383            })?;
1384            let entry = reg.lookup(&handle)?;
1385            let inj = crate::injection::Injection::with_level(
1386                crate::event::TurnId::now(),
1387                text,
1388                level,
1389                redirect_target,
1390            );
1391            entry.pending_injections.lock().unwrap().push(inj);
1392            entry.injection_notify.notify_one();
1393            Ok(Value::Unit)
1394        })
1395    }
1396}
1397
1398fn extract_string(args: &ToolArgs, name: &str, pos: usize) -> Result<String, RuntimeError> {
1399    let value = match args.named(name) {
1400        Some(v) => v,
1401        None => args.positional(pos)?,
1402    };
1403    match value {
1404        Value::Str(s) => Ok(s.clone()),
1405        other => Err(RuntimeError::TypeMismatch {
1406            expected: "string".into(),
1407            actual: other.kind_name().into(),
1408        }),
1409    }
1410}
1411
1412async fn run_prepared_flow_agent(
1413    prepared: PreparedFlowAgent,
1414    flow_args: Vec<(String, Value)>,
1415    ctx: &ToolCtx,
1416    run_id: FlowRunId,
1417    child_messages: Arc<Mutex<Vec<Message>>>,
1418    child_compact_lock: Arc<tokio::sync::Mutex<()>>,
1419    inherited_parent_context: bool,
1420) -> ToolResult {
1421    let Some(registry) = ctx.registry.as_ref() else {
1422        return Err(RuntimeError::ToolFailed(
1423            "flow.spawn: no tool registry available on ctx".into(),
1424        ));
1425    };
1426    let Some(providers) = ctx.providers.as_ref() else {
1427        return Err(RuntimeError::ToolFailed(
1428            "flow.spawn: no provider registry available on ctx".into(),
1429        ));
1430    };
1431    let PreparedFlowAgent { path, flow, flows } = prepared;
1432    let initial_prompt = invocation_user_message(&flow, &flow_args)?;
1433    emit_flow_agent_start(ctx, &run_id, &flow.name.name);
1434    let mut child_ctx = sanitize_child_ctx(ctx);
1435    child_ctx.session_messages_handle = Some(child_messages);
1436    child_ctx.compact_lock_handle = Some(child_compact_lock);
1437    child_ctx.context_epoch_handle = Some(Arc::new(std::sync::atomic::AtomicU64::new(0)));
1438    child_ctx.context_prefix_tracker = Some(Arc::new(std::sync::Mutex::new(
1439        crate::context_plan::ContextPrefixTracker::default(),
1440    )));
1441    seed_parent_handoff_context(
1442        &child_ctx,
1443        &flow,
1444        &run_id,
1445        initial_prompt.as_deref(),
1446        inherited_parent_context,
1447    )?;
1448    if let Some(prompt) = initial_prompt {
1449        seed_child_message_context(&child_ctx, prompt)?;
1450    }
1451    let out = crate::exec::exec_flow_with_siblings(
1452        &flow,
1453        flow_args,
1454        registry.as_ref(),
1455        &child_ctx,
1456        providers.as_ref(),
1457        &flows,
1458        child_ctx.events.as_ref(),
1459        child_ctx.turn_id.clone(),
1460        Some(run_id.clone()),
1461        None,
1462        child_ctx.cancel.clone(),
1463        None,
1464        path.parent().map(|p| p.to_path_buf()),
1465    )
1466    .await;
1467    let status = match &out {
1468        Ok(_) => FlowStatus::Ok,
1469        Err(e) => FlowStatus::Errored {
1470            message: e.to_string(),
1471        },
1472    };
1473    mark_terminal_and_emit_child_flow_end(ctx, &run_id, &status);
1474    out
1475}
1476
1477fn resolve_flow_arguments(
1478    flow: &atman_dsl::ast::FlowDecl,
1479    args: &ToolArgs,
1480) -> Result<Vec<(String, Value)>, RuntimeError> {
1481    let mut flow_args = Vec::new();
1482    let mut unknown = Vec::new();
1483    // Extract the structured flow parameters first.
1484    match args.named("arguments") {
1485        Some(Value::Struct(fields)) => {
1486            for (key, value) in fields {
1487                if flow
1488                    .params
1489                    .iter()
1490                    .any(|parameter| parameter.name.name == *key)
1491                {
1492                    flow_args.push((key.clone(), value.clone()));
1493                } else {
1494                    unknown.push(key.clone());
1495                }
1496            }
1497        }
1498        Some(Value::Unit) | None => {}
1499        Some(other) => {
1500            return Err(RuntimeError::ToolFailed(format!(
1501                "flow.spawn: `arguments` must be a struct, got {}",
1502                other.kind_name()
1503            )));
1504        }
1505    }
1506    // Backward compat: top-level named args (pre-arguments schema).
1507    for (key, value) in &args.named {
1508        if key == "flow"
1509            || key == "version"
1510            || key == "async"
1511            || key == "inherit_context"
1512            || key == "arguments"
1513            || key == "workspace"
1514        {
1515            continue;
1516        }
1517        if !flow
1518            .params
1519            .iter()
1520            .any(|parameter| parameter.name.name == *key)
1521        {
1522            unknown.push(key.clone());
1523        } else if flow_args.iter().any(|(name, _)| name == key) {
1524            return Err(RuntimeError::ToolFailed(format!(
1525                "flow.spawn: argument `{key}` was provided twice"
1526            )));
1527        } else {
1528            flow_args.push((key.clone(), value.clone()));
1529        }
1530    }
1531    if !unknown.is_empty() {
1532        unknown.sort();
1533        unknown.dedup();
1534        return Err(RuntimeError::ToolFailed(format!(
1535            "flow.spawn: unknown argument(s) for `{}`: {}",
1536            flow.name.name,
1537            unknown.join(", ")
1538        )));
1539    }
1540    Ok(flow_args)
1541}
1542
1543fn should_inherit_context(args: &ToolArgs) -> bool {
1544    matches!(args.named("inherit_context"), Some(Value::Bool(true)))
1545}
1546
1547fn inherited_context_snapshot(parent: &Arc<Mutex<Vec<Message>>>) -> Vec<Message> {
1548    let mut snapshot = parent.lock().unwrap().clone();
1549    crate::message::retain_complete_tool_pairs(&mut snapshot);
1550    snapshot
1551}
1552
1553fn invocation_user_message(
1554    flow: &atman_dsl::ast::FlowDecl,
1555    flow_args: &[(String, Value)],
1556) -> Result<Option<String>, RuntimeError> {
1557    let Some((_, parameter_value)) = flow.contract.as_ref().and_then(|contract| {
1558        contract
1559            .blocks
1560            .iter()
1561            .find(|block| block.name.name == "invocation")
1562            .and_then(|block| {
1563                block
1564                    .kwargs
1565                    .iter()
1566                    .find(|(name, _)| name.name == "user_message")
1567            })
1568    }) else {
1569        return Ok(None);
1570    };
1571    let atman_dsl::ast::Expr::Ident(parameter_name) = parameter_value else {
1572        return Err(RuntimeError::ToolFailed(
1573            "flow.spawn: invocation user_message must reference a string flow parameter".into(),
1574        ));
1575    };
1576    let parameter_name = parameter_name.name.as_str();
1577    let Some(parameter) = flow
1578        .params
1579        .iter()
1580        .find(|parameter| parameter.name.name == parameter_name)
1581    else {
1582        return Err(RuntimeError::ToolFailed(format!(
1583            "flow.spawn: invocation user_message references unknown parameter `{parameter_name}`"
1584        )));
1585    };
1586    if !matches!(
1587        &parameter.ty,
1588        atman_dsl::ast::TypeExpr::Named(name) if name.name == "string"
1589    ) {
1590        return Err(RuntimeError::ToolFailed(format!(
1591            "flow.spawn: invocation user_message parameter `{parameter_name}` must be a string"
1592        )));
1593    }
1594    match flow_args
1595        .iter()
1596        .find(|(name, _)| name == parameter_name)
1597        .map(|(_, value)| value)
1598    {
1599        Some(Value::Str(value)) => Ok(Some(value.clone())),
1600        Some(value) => Err(RuntimeError::TypeMismatch {
1601            expected: "string invocation user_message".into(),
1602            actual: value.kind_name().into(),
1603        }),
1604        None => match parameter.default.as_ref() {
1605            Some(atman_dsl::ast::Expr::Literal(atman_dsl::ast::Literal::Str(value))) => {
1606                Ok(Some(value.clone()))
1607            }
1608            Some(_) => Err(RuntimeError::ToolFailed(format!(
1609                "flow.spawn: invocation user_message parameter `{parameter_name}` requires a literal string default"
1610            ))),
1611            None => Err(RuntimeError::ToolFailed(format!(
1612                "flow.spawn: invocation user_message parameter `{parameter_name}` was not provided"
1613            ))),
1614        },
1615    }
1616}
1617
1618fn seed_child_message_context(ctx: &ToolCtx, prompt: String) -> Result<(), RuntimeError> {
1619    let turn_id = ctx
1620        .turn_id
1621        .clone()
1622        .unwrap_or_else(crate::event::TurnId::now);
1623    crate::tools::session::append_message_to_context(
1624        ctx,
1625        crate::message::Message::user_text(turn_id, prompt),
1626    )
1627}
1628
1629fn seed_parent_handoff_context(
1630    ctx: &ToolCtx,
1631    flow: &atman_dsl::ast::FlowDecl,
1632    child_run_id: &FlowRunId,
1633    invocation_prompt: Option<&str>,
1634    inherited_parent_context: bool,
1635) -> Result<(), RuntimeError> {
1636    let parent_run_id = ctx
1637        .flow_identity
1638        .as_ref()
1639        .and_then(|identity| identity.parent_run_id.as_ref())
1640        .map(ToString::to_string);
1641    let expected_result = flow
1642        .ret
1643        .as_ref()
1644        .map(super::flow_list::render_type)
1645        .unwrap_or_else(|| "unit".into());
1646    let task_digest = invocation_prompt
1647        .map(|prompt| format!("blake3:{}", blake3::hash(prompt.as_bytes()).to_hex()));
1648    let handoff = serde_json::json!({
1649        "parent_run_id": parent_run_id,
1650        "child_run_id": child_run_id.to_string(),
1651        "flow": flow.name.name,
1652        "task_source": if invocation_prompt.is_some() { "following_user_message" } else { "flow_parameters" },
1653        "task_digest": task_digest,
1654        "parent_context": if inherited_parent_context {
1655            "materialized_window_through_last_complete_tool_transaction"
1656        } else {
1657            "not_inherited"
1658        },
1659        "capability_boundary": "Only tools exposed by the current child model request are callable.",
1660        "task_boundary": "Execute only this delegated flow invocation and return its declared result to the parent.",
1661        "expected_result": expected_result,
1662    });
1663    let spec = crate::context_plan::ContextRecordSpec::new(
1664        "handoff.parent",
1665        crate::context_plan::ContextRecordAuthority::Runtime,
1666        crate::context_plan::ContextRecordRetention::Latest,
1667        crate::context_plan::ContextRecordBody::text(handoff.to_string()),
1668    );
1669    let turn_id = ctx
1670        .turn_id
1671        .clone()
1672        .unwrap_or_else(crate::event::TurnId::now);
1673    let record = {
1674        let messages = ctx.session_messages_handle.as_ref().ok_or_else(|| {
1675            RuntimeError::ToolFailed("flow.spawn: child message context is unavailable".into())
1676        })?;
1677        let messages = messages.lock().unwrap();
1678        crate::context_plan::compile_context_records(&messages, [spec])
1679            .into_iter()
1680            .next()
1681            .ok_or_else(|| {
1682                RuntimeError::ToolFailed(
1683                    "flow.spawn: child handoff record was not materialized".into(),
1684                )
1685            })?
1686    };
1687    crate::tools::session::append_message_to_context(
1688        ctx,
1689        crate::message::Message::context_record(turn_id, record),
1690    )
1691}
1692
1693fn mark_terminal_and_emit_child_flow_end(ctx: &ToolCtx, run_id: &FlowRunId, status: &FlowStatus) {
1694    terminal_then_emit(
1695        || {
1696            if let Some(flow_registry) = &ctx.flow_registry {
1697                flow_registry.mark_terminal(run_id);
1698            }
1699        },
1700        || emit_child_flow_end(ctx, run_id, status),
1701    );
1702}
1703
1704fn terminal_then_emit(mark_terminal: impl FnOnce(), emit: impl FnOnce()) {
1705    mark_terminal();
1706    emit();
1707}
1708
1709fn extract_flow(args: &ToolArgs) -> Result<Option<String>, RuntimeError> {
1710    match args.named("flow") {
1711        Some(Value::Str(s)) if !s.trim().is_empty() => Ok(Some(s.clone())),
1712        Some(Value::Unit) | None => Ok(None),
1713        Some(other) => Err(RuntimeError::TypeMismatch {
1714            expected: "flow string".into(),
1715            actual: other.kind_name().into(),
1716        }),
1717    }
1718}
1719
1720fn extract_flow_version(args: &ToolArgs) -> Result<Option<String>, RuntimeError> {
1721    match args.named("version") {
1722        Some(Value::Str(version)) if !version.trim().is_empty() => Ok(Some(version.clone())),
1723        Some(Value::Str(_)) => Ok(None),
1724        Some(Value::Unit) | None => Ok(None),
1725        Some(other) => Err(RuntimeError::TypeMismatch {
1726            expected: "version string".into(),
1727            actual: other.kind_name().into(),
1728        }),
1729    }
1730}
1731
1732async fn read_flow_source(flow_ref: &str) -> Result<(PathBuf, String), RuntimeError> {
1733    for path in super::flow_source::candidates(flow_ref) {
1734        match tokio::fs::read_to_string(&path).await {
1735            Ok(src) => return Ok((path, src)),
1736            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
1737            Err(e) => {
1738                return Err(RuntimeError::ToolFailed(format!(
1739                    "flow.spawn: read {}: {e}",
1740                    path.display()
1741                )));
1742            }
1743        }
1744    }
1745    Err(RuntimeError::ToolFailed(format!(
1746        "flow.spawn: flow `{flow_ref}` not found"
1747    )))
1748}
1749
1750fn emit_flow_agent_start(ctx: &ToolCtx, run_id: &FlowRunId, flow_name: &str) {
1751    let parent_run_id = ctx
1752        .flow_identity
1753        .as_ref()
1754        .and_then(|identity| identity.parent_run_id.clone())
1755        .or_else(|| {
1756            ctx.flow_run_id
1757                .clone()
1758                .filter(|candidate| candidate != run_id)
1759        });
1760    let parent_node_id = ctx.current_node_id.clone();
1761    if let Some(sink) = &ctx.events {
1762        sink.emit(Event::FlowStart {
1763            run_id: run_id.clone(),
1764            flow_name: flow_name.into(),
1765            parent_run_id: parent_run_id.clone(),
1766            parent_node_id: parent_node_id.clone(),
1767            spawned: true,
1768        });
1769    }
1770    if let Some(tx) = &ctx.stream_tx {
1771        let _ = tx.send(crate::stream::StreamFrame::FlowStart {
1772            run_id: run_id.0.to_string(),
1773            flow_name: flow_name.into(),
1774            parent_run_id: parent_run_id.as_ref().map(|r| r.0.to_string()),
1775            parent_node_id,
1776        });
1777    }
1778}
1779
1780fn emit_child_flow_end(ctx: &ToolCtx, run_id: &FlowRunId, status: &FlowStatus) {
1781    let suicide = ctx.task_registry.as_ref().is_some_and(|registry| {
1782        registry
1783            .list(&crate::task_registry::TaskFilter::default())
1784            .iter()
1785            .any(|snapshot| {
1786                snapshot.flow_run_id.as_ref() == Some(run_id)
1787                    && snapshot.termination == Some(crate::task_registry::TaskTermination::Suicide)
1788            })
1789    });
1790    if let Some(sink) = &ctx.events {
1791        sink.emit(Event::FlowEnd {
1792            run_id: run_id.clone(),
1793            flow_name: "agent.sub".into(),
1794            status: status.clone(),
1795        });
1796    }
1797    if let Some(tx) = &ctx.stream_tx {
1798        let _ = tx.send(crate::stream::StreamFrame::FlowDone {
1799            run_id: run_id.0.to_string(),
1800            flow_name: "agent.sub".into(),
1801            ok: matches!(status, FlowStatus::Ok),
1802            cancelled: matches!(status, FlowStatus::Cancelled),
1803            suicide,
1804        });
1805    }
1806}
1807
1808fn sanitize_child_ctx(parent: &ToolCtx) -> ToolCtx {
1809    let mut c = parent.clone();
1810    c.session_runtime = None;
1811    c.history_segment = crate::tool::HistorySegment::Spawned;
1812    c.session_messages_handle = None;
1813    c.compact_lock_handle = None;
1814    c.context_epoch_handle = None;
1815    c.context_prefix_tracker = None;
1816    c.forms = None;
1817    c.on_memory_recent = None;
1818    c
1819}
1820
1821#[cfg(test)]
1822mod tests {
1823    use super::{
1824        AgentSpawn, FlowRegistry, FlowRunStatus, extract_flow_version, inherited_context_snapshot,
1825        prepare_flow_agent, resolve_flow_arguments, terminal_then_emit,
1826    };
1827    use crate::message::{Message, MessageOrigin, MessagePart, MessageRole};
1828    use crate::permission::PermissionBroker;
1829    use crate::provider::ProviderRegistry;
1830    use crate::tool::{Tier, Tool, ToolArgs, ToolCtx, ToolRegistry};
1831    use crate::{InvocationEnv, Value};
1832    use std::cell::Cell;
1833    use std::sync::Arc;
1834
1835    struct SandboxProbe;
1836
1837    struct SessionTextProbe;
1838
1839    #[test]
1840    fn empty_spawn_version_is_treated_as_omitted() {
1841        for value in ["", " ", "\t\n"] {
1842            let args = ToolArgs {
1843                named: vec![("version".into(), Value::Str(value.into()))],
1844                ..Default::default()
1845            };
1846            assert_eq!(extract_flow_version(&args).unwrap(), None);
1847        }
1848    }
1849
1850    #[tokio::test]
1851    async fn prepared_flow_rejects_a_stale_discovery_version() {
1852        let dir = tempfile::tempdir().unwrap();
1853        let path = dir.path().join("child.at");
1854        let source = "flow child(goal: string) -> string { return goal }";
1855        std::fs::write(&path, source).unwrap();
1856        let flow_ref = format!("{}@child", path.display());
1857        let version = format!("blake3:{}", blake3::hash(source.as_bytes()).to_hex());
1858
1859        prepare_flow_agent(&flow_ref, Some(&version)).await.unwrap();
1860        let error = match prepare_flow_agent(&flow_ref, Some("blake3:stale")).await {
1861            Ok(_) => panic!("stale version should fail"),
1862            Err(error) => error,
1863        };
1864        assert!(error.to_string().contains("stale version"));
1865    }
1866
1867    #[test]
1868    fn flow_arguments_reject_unknown_and_duplicate_fields() {
1869        let file = atman_dsl::parse::parse_file(
1870            "flow child(goal: string, retries: int = 1) -> string { return goal }",
1871        )
1872        .unwrap();
1873        let flow = &file.flows[0];
1874        let unknown = ToolArgs {
1875            named: vec![(
1876                "arguments".into(),
1877                Value::Struct(vec![
1878                    ("goal".into(), Value::Str("work".into())),
1879                    ("typo".into(), Value::Bool(true)),
1880                ]),
1881            )],
1882            ..Default::default()
1883        };
1884        let error = resolve_flow_arguments(flow, &unknown).unwrap_err();
1885        assert!(error.to_string().contains("unknown argument(s)"));
1886        assert!(error.to_string().contains("typo"));
1887
1888        let duplicate = ToolArgs {
1889            named: vec![
1890                (
1891                    "arguments".into(),
1892                    Value::Struct(vec![("goal".into(), Value::Str("nested".into()))]),
1893                ),
1894                ("goal".into(), Value::Str("top-level".into())),
1895            ],
1896            ..Default::default()
1897        };
1898        assert!(
1899            resolve_flow_arguments(flow, &duplicate)
1900                .unwrap_err()
1901                .to_string()
1902                .contains("provided twice")
1903        );
1904    }
1905
1906    #[test]
1907    fn flow_entry_keeps_execution_goal_separate_from_display_label() {
1908        let registry = FlowRegistry::new();
1909        let entry = registry.create_entry_with_workspace_label(
1910            "agent-test".into(),
1911            "Audit the full provider chain".into(),
1912            "Review provider routing".into(),
1913            "smart".into(),
1914            crate::event::FlowRunId::now(),
1915            None,
1916        );
1917        assert_eq!(entry.goal, "Audit the full provider chain");
1918        assert_eq!(entry.display_label, "Review provider routing");
1919    }
1920
1921    #[test]
1922    fn inherited_context_excludes_incomplete_parent_tool_transactions() {
1923        let turn_id = crate::event::TurnId::now();
1924        let parent = Arc::new(std::sync::Mutex::new(vec![
1925            Message::user_text(turn_id.clone(), "delegate work"),
1926            Message {
1927                role: MessageRole::Assistant,
1928                parts: vec![
1929                    MessagePart::Text {
1930                        text: "starting tools".into(),
1931                    },
1932                    MessagePart::ToolUse {
1933                        id: "active-spawn".into(),
1934                        name: "flow.spawn".into(),
1935                        input: serde_json::json!({}),
1936                        intent: None,
1937                    },
1938                    MessagePart::ToolUse {
1939                        id: "complete-read".into(),
1940                        name: "fs.read".into(),
1941                        input: serde_json::json!({"path": "README.md"}),
1942                        intent: None,
1943                    },
1944                ],
1945                turn_id: turn_id.clone(),
1946                origin: MessageOrigin::User,
1947            },
1948            Message {
1949                role: MessageRole::Tool,
1950                parts: vec![MessagePart::ToolResult {
1951                    tool_use_id: "complete-read".into(),
1952                    content: "contents".into(),
1953                    is_error: false,
1954                }],
1955                turn_id,
1956                origin: MessageOrigin::User,
1957            },
1958        ]));
1959
1960        let snapshot = inherited_context_snapshot(&parent);
1961
1962        assert!(snapshot.iter().any(|message| {
1963            message
1964                .parts
1965                .iter()
1966                .any(|part| matches!(part, MessagePart::Text { text } if text == "starting tools"))
1967        }));
1968        assert!(snapshot.iter().any(|message| {
1969            message.parts.iter().any(
1970                |part| matches!(part, MessagePart::ToolUse { id, .. } if id == "complete-read"),
1971            )
1972        }));
1973        assert!(!snapshot.iter().any(|message| {
1974            message
1975                .parts
1976                .iter()
1977                .any(|part| matches!(part, MessagePart::ToolUse { id, .. } if id == "active-spawn"))
1978        }));
1979        assert!(parent.lock().unwrap().iter().any(|message| {
1980            message
1981                .parts
1982                .iter()
1983                .any(|part| matches!(part, MessagePart::ToolUse { id, .. } if id == "active-spawn"))
1984        }));
1985    }
1986
1987    impl Tool for SandboxProbe {
1988        fn name(&self) -> &str {
1989            "sandbox.probe"
1990        }
1991
1992        fn tier(&self) -> Tier {
1993            Tier::Zero
1994        }
1995
1996        fn call<'a>(
1997            &'a self,
1998            _args: ToolArgs,
1999            ctx: &'a ToolCtx,
2000        ) -> crate::tool::BoxFut<'a, crate::tool::ToolResult> {
2001            Box::pin(async move { Ok(Value::Bool(ctx.sandbox.is_some())) })
2002        }
2003    }
2004
2005    impl Tool for SessionTextProbe {
2006        fn name(&self) -> &str {
2007            "session.text"
2008        }
2009
2010        fn tier(&self) -> Tier {
2011            Tier::Zero
2012        }
2013
2014        fn call<'a>(
2015            &'a self,
2016            _args: ToolArgs,
2017            ctx: &'a ToolCtx,
2018        ) -> crate::tool::BoxFut<'a, crate::tool::ToolResult> {
2019            Box::pin(async move {
2020                let text = ctx
2021                    .session_messages_handle
2022                    .as_ref()
2023                    .map(|handle| {
2024                        handle
2025                            .lock()
2026                            .unwrap()
2027                            .iter()
2028                            .filter(|message| {
2029                                !message
2030                                    .parts
2031                                    .iter()
2032                                    .any(|part| matches!(part, MessagePart::ContextRecord(_)))
2033                            })
2034                            .map(crate::message::Message::text_concat)
2035                            .collect::<Vec<_>>()
2036                            .join("|")
2037                    })
2038                    .unwrap_or_default();
2039                Ok(Value::Str(text))
2040            })
2041        }
2042    }
2043
2044    #[test]
2045    fn terminal_transition_happens_before_flow_end_emit() {
2046        let terminal = Cell::new(false);
2047
2048        terminal_then_emit(
2049            || terminal.set(true),
2050            || assert!(terminal.get(), "FlowEnd emitted before terminal transition"),
2051        );
2052    }
2053
2054    #[tokio::test]
2055    async fn sync_and_async_spawned_flows_inherit_invocation_snapshot() {
2056        for is_async in [false, true] {
2057            let dir = tempfile::tempdir().unwrap();
2058            let path = dir.path().join("child.at");
2059            std::fs::write(&path, r#"flow child() -> string { return env("effort") }"#).unwrap();
2060
2061            let registry = Arc::new(FlowRegistry::new());
2062            let root_run_id = crate::event::FlowRunId::now();
2063            let root_identity = registry
2064                .register_root(
2065                    "test-session".into(),
2066                    root_run_id.clone(),
2067                    crate::flow_authority::EffectiveAuthority::root(
2068                        &Default::default(),
2069                        false,
2070                        None,
2071                    ),
2072                )
2073                .unwrap();
2074            let mut ctx = ToolCtx::new()
2075                .with_registry(Arc::new(ToolRegistry::new()))
2076                .with_providers(Arc::new(ProviderRegistry::new()))
2077                .with_flow_registry(Arc::clone(&registry))
2078                .with_invocation_env(InvocationEnv::single("effort", Value::Str("high".into())));
2079            ctx.flow_run_id = Some(root_run_id);
2080            ctx.flow_identity = Some(root_identity);
2081
2082            let result = AgentSpawn
2083                .call(
2084                    ToolArgs {
2085                        positional: Vec::new(),
2086                        named: vec![
2087                            (
2088                                "flow".into(),
2089                                Value::Str(format!("{}@child", path.display())),
2090                            ),
2091                            ("async".into(), Value::Bool(is_async)),
2092                        ],
2093                    },
2094                    &ctx,
2095                )
2096                .await
2097                .unwrap();
2098
2099            if !is_async {
2100                assert!(matches!(result, Value::Str(value) if value == "high"));
2101                continue;
2102            }
2103
2104            let Value::Struct(fields) = result else {
2105                panic!("expected async spawn handle");
2106            };
2107            let handle = fields
2108                .into_iter()
2109                .find_map(|(name, value)| {
2110                    (name == "handle")
2111                        .then_some(value)
2112                        .and_then(|value| match value {
2113                            Value::Str(handle) => Some(handle),
2114                            _ => None,
2115                        })
2116                })
2117                .expect("async spawn handle");
2118            let status = tokio::time::timeout(std::time::Duration::from_secs(2), async {
2119                loop {
2120                    let status = registry
2121                        .lookup(&handle)
2122                        .unwrap()
2123                        .status
2124                        .lock()
2125                        .unwrap()
2126                        .clone();
2127                    if !status.is_running() {
2128                        break status;
2129                    }
2130                    tokio::task::yield_now().await;
2131                }
2132            })
2133            .await
2134            .unwrap();
2135            assert!(matches!(
2136                status,
2137                FlowRunStatus::Ok { final_text, .. } if final_text == "high"
2138            ));
2139        }
2140    }
2141
2142    #[tokio::test]
2143    async fn async_spawn_announces_declared_model() {
2144        for (arguments, expected) in [
2145            (Vec::new(), "smart"),
2146            (
2147                vec![("model".into(), Value::Str("vendor/model".into()))],
2148                "vendor/model",
2149            ),
2150        ] {
2151            let dir = tempfile::tempdir().unwrap();
2152            let path = dir.path().join("child.at");
2153            std::fs::write(
2154                &path,
2155                r#"flow child(model: string = "smart") -> string { return model }"#,
2156            )
2157            .unwrap();
2158
2159            let registry = Arc::new(FlowRegistry::new());
2160            let root_run_id = crate::event::FlowRunId::now();
2161            let root_identity = registry
2162                .register_root(
2163                    "test-session".into(),
2164                    root_run_id.clone(),
2165                    crate::flow_authority::EffectiveAuthority::root(
2166                        &Default::default(),
2167                        false,
2168                        None,
2169                    ),
2170                )
2171                .unwrap();
2172            let (stream_tx, mut stream_rx) = tokio::sync::broadcast::channel(16);
2173            let mut ctx = ToolCtx::new()
2174                .with_registry(Arc::new(ToolRegistry::new()))
2175                .with_providers(Arc::new(ProviderRegistry::new()))
2176                .with_flow_registry(registry)
2177                .with_stream_tx(stream_tx);
2178            ctx.flow_run_id = Some(root_run_id);
2179            ctx.flow_identity = Some(root_identity);
2180
2181            AgentSpawn
2182                .call(
2183                    ToolArgs {
2184                        positional: Vec::new(),
2185                        named: vec![
2186                            (
2187                                "flow".into(),
2188                                Value::Str(format!("{}@child", path.display())),
2189                            ),
2190                            ("async".into(), Value::Bool(true)),
2191                            ("arguments".into(), Value::Struct(arguments)),
2192                        ],
2193                    },
2194                    &ctx,
2195                )
2196                .await
2197                .unwrap();
2198
2199            let announced_model = tokio::time::timeout(std::time::Duration::from_secs(2), async {
2200                loop {
2201                    if let crate::stream::StreamFrame::SubAgentStarted { model, .. } =
2202                        stream_rx.recv().await.unwrap()
2203                    {
2204                        break model;
2205                    }
2206                }
2207            })
2208            .await
2209            .unwrap();
2210            assert_eq!(announced_model, expected);
2211        }
2212    }
2213
2214    #[tokio::test]
2215    async fn flow_spawn_retains_sandbox_for_child_tool_invocations() {
2216        let dir = tempfile::tempdir().unwrap();
2217        let path = dir.path().join("child.at");
2218        std::fs::write(&path, r#"flow child() -> bool { return sandbox.probe() }"#).unwrap();
2219
2220        let tools = Arc::new(ToolRegistry::new());
2221        tools.register(Arc::new(SandboxProbe));
2222        let flows = Arc::new(FlowRegistry::new());
2223        let broker = PermissionBroker::shared(Arc::clone(&flows));
2224        let root_run_id = crate::event::FlowRunId::now();
2225        let trust = crate::trust::TrustConfig::default();
2226        let root_identity = flows
2227            .register_root(
2228                "test-session".into(),
2229                root_run_id.clone(),
2230                crate::flow_authority::EffectiveAuthority::root(&trust, false, None),
2231            )
2232            .unwrap();
2233        let sandbox: Arc<dyn crate::sandbox::Sandbox> =
2234            Arc::new(crate::sandbox::SandboxExec::new(dir.path()));
2235        let mut ctx = ToolCtx::new()
2236            .with_registry(tools)
2237            .with_providers(Arc::new(ProviderRegistry::new()))
2238            .with_flow_registry(Arc::clone(&flows))
2239            .with_permission_broker(broker)
2240            .with_session_id("test-session")
2241            .with_trust(trust)
2242            .with_sandbox(sandbox)
2243            .for_tool_invocation(Tier::Two);
2244        ctx.flow_run_id = Some(root_run_id);
2245        ctx.flow_identity = Some(root_identity);
2246
2247        let result = AgentSpawn
2248            .call(
2249                ToolArgs {
2250                    positional: Vec::new(),
2251                    named: vec![
2252                        (
2253                            "flow".into(),
2254                            Value::Str(format!("{}@child", path.display())),
2255                        ),
2256                        ("async".into(), Value::Bool(false)),
2257                    ],
2258                },
2259                &ctx,
2260            )
2261            .await
2262            .unwrap();
2263
2264        assert!(matches!(result, Value::Bool(true)));
2265    }
2266
2267    #[tokio::test]
2268    async fn flow_spawn_owns_one_isolated_invocation_message() {
2269        let dir = tempfile::tempdir().unwrap();
2270        let path = dir.path().join("child.at");
2271        std::fs::write(
2272            &path,
2273            r#"flow child(user_prompt: string) -> string {
2274    contract { invocation { user_message: user_prompt } }
2275    return session.text()
2276}
2277
2278flow plain(user_prompt: string) -> string {
2279    return session.text()
2280}"#,
2281        )
2282        .unwrap();
2283
2284        let tools = Arc::new(ToolRegistry::new());
2285        tools.register(Arc::new(SessionTextProbe));
2286        let flows = Arc::new(FlowRegistry::new());
2287        let broker = PermissionBroker::shared(Arc::clone(&flows));
2288        let event_session = Arc::new(crate::session::Session::open_ephemeral());
2289        let root_run_id = crate::event::FlowRunId::now();
2290        let trust = crate::trust::TrustConfig::default();
2291        let root_identity = flows
2292            .register_root(
2293                "test-session".into(),
2294                root_run_id.clone(),
2295                crate::flow_authority::EffectiveAuthority::root(&trust, false, None),
2296            )
2297            .unwrap();
2298        let root_entry = flows.create_entry(
2299            "root".into(),
2300            "parent prompt".into(),
2301            String::new(),
2302            root_run_id.clone(),
2303        );
2304        let parent_messages = Arc::new(std::sync::Mutex::new(vec![
2305            crate::message::Message::user_text(crate::event::TurnId::now(), "parent prompt"),
2306        ]));
2307        let mut ctx = ToolCtx::new()
2308            .with_registry(tools)
2309            .with_providers(Arc::new(ProviderRegistry::new()))
2310            .with_flow_registry(Arc::clone(&flows))
2311            .with_permission_broker(broker)
2312            .with_events(event_session.sink().clone())
2313            .with_session_id("test-session")
2314            .with_trust(trust)
2315            .with_agent_entry(Arc::clone(&root_entry))
2316            .with_session_messages_handle(Arc::clone(&parent_messages));
2317        ctx.flow_run_id = Some(root_run_id);
2318        ctx.flow_identity = Some(root_identity);
2319
2320        let result = AgentSpawn
2321            .call(
2322                ToolArgs {
2323                    positional: Vec::new(),
2324                    named: vec![
2325                        (
2326                            "flow".into(),
2327                            Value::Str(format!("{}@child", path.display())),
2328                        ),
2329                        ("async".into(), Value::Bool(false)),
2330                        (
2331                            "arguments".into(),
2332                            Value::Struct(vec![(
2333                                "user_prompt".into(),
2334                                Value::Str("child prompt".into()),
2335                            )]),
2336                        ),
2337                    ],
2338                },
2339                &ctx,
2340            )
2341            .await
2342            .unwrap();
2343
2344        assert!(matches!(result, Value::Str(text) if text == "child prompt"));
2345        assert_eq!(parent_messages.lock().unwrap().len(), 1);
2346        assert!(
2347            root_entry.messages.lock().unwrap().is_empty(),
2348            "sync child must not reuse the parent FlowEntry message segment"
2349        );
2350
2351        let plain_result = AgentSpawn
2352            .call(
2353                ToolArgs {
2354                    positional: Vec::new(),
2355                    named: vec![
2356                        (
2357                            "flow".into(),
2358                            Value::Str(format!("{}@plain", path.display())),
2359                        ),
2360                        ("async".into(), Value::Bool(false)),
2361                        (
2362                            "arguments".into(),
2363                            Value::Struct(vec![(
2364                                "user_prompt".into(),
2365                                Value::Str("not implicit".into()),
2366                            )]),
2367                        ),
2368                    ],
2369                },
2370                &ctx,
2371            )
2372            .await
2373            .unwrap();
2374
2375        assert!(matches!(plain_result, Value::Str(text) if text.is_empty()));
2376
2377        let async_result = AgentSpawn
2378            .call(
2379                ToolArgs {
2380                    positional: Vec::new(),
2381                    named: vec![
2382                        (
2383                            "flow".into(),
2384                            Value::Str(format!("{}@child", path.display())),
2385                        ),
2386                        ("async".into(), Value::Bool(true)),
2387                        (
2388                            "arguments".into(),
2389                            Value::Struct(vec![(
2390                                "user_prompt".into(),
2391                                Value::Str("async child prompt".into()),
2392                            )]),
2393                        ),
2394                    ],
2395                },
2396                &ctx,
2397            )
2398            .await
2399            .unwrap();
2400        let Value::Struct(fields) = async_result else {
2401            panic!("expected async spawn handle");
2402        };
2403        let handle = fields
2404            .into_iter()
2405            .find_map(|(name, value)| match (name.as_str(), value) {
2406                ("handle", Value::Str(handle)) => Some(handle),
2407                _ => None,
2408            })
2409            .expect("async spawn handle");
2410        let entry = flows.lookup(&handle).unwrap();
2411        let status = tokio::time::timeout(std::time::Duration::from_secs(2), async {
2412            loop {
2413                let status = entry.status.lock().unwrap().clone();
2414                if !status.is_running() {
2415                    break status;
2416                }
2417                tokio::task::yield_now().await;
2418            }
2419        })
2420        .await
2421        .unwrap();
2422        assert!(matches!(
2423            status,
2424            FlowRunStatus::Ok { final_text, .. } if final_text == "async child prompt"
2425        ));
2426        let entry_messages = entry.messages.lock().unwrap();
2427        assert_eq!(entry_messages.len(), 2);
2428        assert!(matches!(
2429            entry_messages[0].parts.as_slice(),
2430            [MessagePart::ContextRecord(record)] if record.key() == "handoff.parent"
2431        ));
2432        let MessagePart::ContextRecord(handoff) = &entry_messages[0].parts[0] else {
2433            unreachable!("validated handoff record")
2434        };
2435        let rendered_handoff = handoff.render_for_model();
2436        assert!(rendered_handoff.contains("following_user_message"));
2437        assert!(rendered_handoff.contains("expected_result"));
2438        assert!(!rendered_handoff.contains("async child prompt"));
2439        assert_eq!(entry_messages[1].text_concat(), "async child prompt");
2440        drop(entry_messages);
2441        assert_eq!(parent_messages.lock().unwrap().len(), 1);
2442        assert!(root_entry.messages.lock().unwrap().is_empty());
2443        assert!(
2444            event_session.messages_full().is_empty(),
2445            "spawned handoff records must not project into root history"
2446        );
2447        let invocation_events = event_session
2448            .sink()
2449            .snapshot()
2450            .into_iter()
2451            .filter_map(|event| match event {
2452                crate::event::Event::UserMsg {
2453                    flow_run_id,
2454                    message,
2455                    ..
2456                } => Some((flow_run_id, message.text_concat())),
2457                _ => None,
2458            })
2459            .collect::<Vec<_>>();
2460        assert_eq!(invocation_events.len(), 2);
2461        assert!(
2462            invocation_events
2463                .iter()
2464                .all(|(flow_run_id, _)| flow_run_id.is_some()),
2465            "child invocation events must not project into root history"
2466        );
2467        let handoff_events = event_session
2468            .sink()
2469            .snapshot()
2470            .into_iter()
2471            .filter_map(|event| match event {
2472                crate::event::Event::SystemMsg {
2473                    flow_run_id,
2474                    message,
2475                    ..
2476                } if message.parts.iter().any(
2477                    |part| matches!(part, MessagePart::ContextRecord(record) if record.key() == "handoff.parent"),
2478                ) => Some(flow_run_id),
2479                _ => None,
2480            })
2481            .collect::<Vec<_>>();
2482        assert_eq!(handoff_events.len(), 3);
2483        assert!(handoff_events.iter().all(Option::is_some));
2484    }
2485}