use super::*;
pub(crate) struct SessionTopology {
pub(crate) nodes: Vec<SubagentNode>,
pub(crate) workflow_runs: Vec<WorkflowRun>,
}
pub fn run_agents(args: &AgentsArgs) -> Result<()> {
if let Some(msg) = args.span_flag_error() {
bail!(msg);
}
let session_files = path::resolve_targets_with_session_list(
&args.paths,
args.sessions_from.as_deref(),
false.into(),
path::Caller::Other,
)?;
let time_window = TimeWindow::from_args(args.since.as_deref(), args.until.as_deref())?;
let want_returned = args.returned_message || args.agent.is_some();
let want_files = args.with_files || args.agent.is_some();
let topos: Vec<SessionTopology> = session_files
.par_iter()
.map(|sf| topology_for_session(sf, want_files))
.collect::<Result<Vec<_>>>()?;
let mut nodes: Vec<SubagentNode> = Vec::new();
let mut workflow_runs: Vec<WorkflowRun> = Vec::new();
for t in topos {
nodes.extend(t.nodes);
workflow_runs.extend(t.workflow_runs);
}
if let Some(want_id) = args.agent.as_deref() {
nodes.retain(|n| n.agent_id == want_id);
if nodes.is_empty() {
bail!(
"no subagent matched id `{want_id}` in scope. List valid ids first with \
`csift agents @<uuid>` (or `csift agents <project-path>`) and read \
the `agent_id` column / JSON field, then pass one to `--agent`."
);
}
} else {
nodes.retain(|n| kind_allowed(n.kind, &args.kinds));
nodes.retain(|n| window_admits(n, &time_window, args.order_by));
}
nodes.sort_by(|a, b| {
(
&a.parent_session_id,
a.trigger_utc.as_deref().unwrap_or(""),
&a.agent_id,
)
.cmp(&(
&b.parent_session_id,
b.trigger_utc.as_deref().unwrap_or(""),
&b.agent_id,
))
});
augment_unmanifested_runs(&nodes, &mut workflow_runs);
let view = View {
want_returned,
want_files,
single_node: args.agent.is_some(),
};
match args.format {
OutputFormat::Text => render_text(&nodes, &workflow_runs, args, &view),
OutputFormat::Json => render_json(&nodes, &workflow_runs, &view)?,
}
Ok(())
}
pub(crate) fn augment_unmanifested_runs(
nodes: &[SubagentNode],
workflow_runs: &mut Vec<WorkflowRun>,
) {
use std::collections::BTreeSet;
let known: BTreeSet<&str> = workflow_runs.iter().map(|r| r.run_id.as_str()).collect();
let mut missing: Vec<String> = Vec::new();
let mut seen: BTreeSet<&str> = BTreeSet::new();
for n in nodes {
if let Some(wf) = n.workflow_id.as_deref() {
if !known.contains(wf) && seen.insert(wf) {
missing.push(wf.to_string());
}
}
}
for run_id in missing {
workflow_runs.push(WorkflowRun {
run_id,
task_id: None,
workflow_name: None,
status: None,
agent_count: None,
duration_ms: None,
total_tokens: None,
total_tool_calls: None,
default_model: None,
started_utc: None,
});
}
}
pub(crate) struct View {
pub(crate) want_returned: bool,
pub(crate) want_files: bool,
pub(crate) single_node: bool,
}
pub(crate) fn topology_for_session(
session_jsonl: &Path,
with_files: bool,
) -> Result<SessionTopology> {
let nodes = build_topology(session_jsonl, with_files)?;
let workflow_runs = if nodes.is_empty() {
Vec::new()
} else {
discover_workflow_runs(session_jsonl)?
};
Ok(SessionTopology {
nodes,
workflow_runs,
})
}
pub(crate) fn kind_allowed(kind: SubagentKind, want: &[AgentKindFilter]) -> bool {
if want.is_empty() {
return true;
}
want.iter().any(|w| match w {
AgentKindFilter::BuiltinTask => kind == SubagentKind::BuiltinTask,
AgentKindFilter::Workflow => kind == SubagentKind::Workflow,
AgentKindFilter::Teammate => kind == SubagentKind::Teammate,
})
}
pub(crate) fn window_admits(node: &SubagentNode, window: &TimeWindow, axis: AgentTimeAxis) -> bool {
if window.is_unbounded() {
return true;
}
let ts = match axis {
AgentTimeAxis::Trigger => node.trigger_utc.as_deref(),
AgentTimeAxis::Start => node.started_utc.as_deref(),
AgentTimeAxis::Completion => node.last_activity_utc.as_deref(),
};
window.contains(ts)
}
pub(crate) fn any_teammate(nodes: &[SubagentNode]) -> bool {
nodes
.iter()
.any(|n| n.kind == SubagentKind::Teammate || any_teammate(&n.children))
}
pub(crate) const TEAMMATE_CONTROL_HINT_L1: &str = "note: teammate rows are in-process Agent subagents — address one BY NAME (the `(@name)` shown) \
via SendMessage to steer it, and `message:{\"type\":\"shutdown_request\"}` to terminate it.";
pub(crate) const TEAMMATE_CONTROL_HINT_L2: &str =
" A teammate is NOT a background task (TaskStop / a `task_id` will not find it) and has no \
separate OS process (it shares the orchestrator PID — `pkill` won't help).";
pub(crate) const TEAMMATE_CONTROL_HINT_JSON: &str =
"in-process teammate: SendMessage to `name` to steer; \
message {type:\"shutdown_request\"} terminates. Not a TaskStop background task; shares the \
orchestrator PID (no separate process to kill).";
pub(crate) fn axis_label(axis: AgentTimeAxis) -> &'static str {
match axis {
AgentTimeAxis::Trigger => "trigger",
AgentTimeAxis::Start => "start",
AgentTimeAxis::Completion => "completion",
}
}