use super::*;
pub fn gate_requires_children(world: &mut World) {
crate::tick_scope::clear();
use crate::components::SubAgentChildren;
let mut candidates: Vec<(Entity, Vec<Entity>)> = Vec::new();
{
let mut q = world.query_filtered::<(
Entity,
&AgentBlueprint,
&StageCursor,
&SubAgentChildren,
&AgentState,
), With<ResolveTransition>>();
for (e, bp, cursor, children, _) in q.iter(world) {
if bp.0.stages[cursor.index].requires_children {
candidates.push((e, children.children.clone()));
}
}
}
for (entity, children) in candidates {
crate::tick_scope::enter(entity);
let pending = children.iter().any(|&c| {
world
.get::<AgentState>(c)
.is_some_and(|s| !is_terminal_status(&s.status))
});
if pending {
world
.entity_mut(entity)
.remove::<ResolveTransition>()
.insert(WaitingForChildren);
world
.get_mut::<AgentState>(entity)
.expect("held agent has AgentState")
.status = AgentStatus::Waiting;
}
}
crate::tick_scope::clear();
let mut waiting: Vec<(Entity, Vec<Entity>)> = Vec::new();
{
let mut q = world.query_filtered::<
(Entity, Option<&SubAgentChildren>, &AgentState),
With<WaitingForChildren>,
>();
for (e, children, _) in q.iter(world) {
waiting.push((e, children.map(|c| c.children.clone()).unwrap_or_default()));
}
}
for (entity, children) in waiting {
crate::tick_scope::enter(entity);
let all_done = children.iter().all(|&c| {
world
.get::<AgentState>(c)
.is_none_or(|s| is_terminal_status(&s.status))
});
if all_done {
world
.entity_mut(entity)
.remove::<WaitingForChildren>()
.insert(ResolveTransition);
world
.get_mut::<AgentState>(entity)
.expect("waiting agent has AgentState")
.status = AgentStatus::Active;
}
}
}
pub(crate) const DEFAULT_REQUIRED_REENTRY_CAP: usize = 3;
#[derive(Component, Debug, Clone, Copy)]
pub struct RequiredReentries(pub usize);
pub(crate) fn unmet_required_regions(
blueprint: &leviath_core::Blueprint,
stage: &leviath_core::Stage,
window: &ContextWindow,
) -> Vec<(String, Option<String>)> {
let can_write = stage
.available_tools
.iter()
.any(|t| t == "context_write" || t == "context_append");
if !can_write {
return Vec::new();
}
let layout = stage
.context_layout
.as_ref()
.unwrap_or(&blueprint.context_layout);
layout
.regions
.iter()
.filter(|r| r.required)
.filter(|r| {
!matches!(
r.seed,
Some(leviath_core::layout::RegionSeed::CallerInput { .. })
)
})
.filter(|r| {
window
.get_region(&r.name)
.map(|reg| reg.content.is_empty())
.unwrap_or(true)
})
.map(|r| (r.name.clone(), r.required_message.clone()))
.collect()
}
pub(crate) fn inject_required_region_nudges(
window: &mut ContextWindow,
unmet: &[(String, Option<String>)],
) {
const DEFAULT_REQUIRED_MESSAGE: &str = "Required context region '{region}' is still empty. \
You must populate it (e.g. via context_write with region=\"{region}\") before this \
stage can complete.";
for (name, msg) in unmet {
let text = leviath_core::text::interpolate(
msg.as_deref().unwrap_or(DEFAULT_REQUIRED_MESSAGE),
&[("region", name)],
);
crate::pipeline::response::inject_system_nudge(window, &text);
}
}
type ContextRegionQuery = (
Entity,
&'static AgentBlueprint,
&'static StageCursor,
&'static mut ContextWindow,
Option<&'static RequiredReentries>,
Option<&'static StageOutcome>,
Option<&'static mut crate::persistence::RunOutcomeFlags>,
);
pub fn require_context_regions(
mut agents: Query<ContextRegionQuery, With<ResolveTransition>>,
mut commands: Commands,
) {
crate::tick_scope::clear();
for (entity, bp, cursor, mut window, reentries, outcome, flags) in agents.iter_mut() {
crate::tick_scope::enter(entity);
if outcome.is_some() {
continue; }
let stage = &bp.0.stages[cursor.index];
let unmet = unmet_required_regions(&bp.0, stage, &window);
if unmet.is_empty() {
continue;
}
let cap = stage.max_revisits.unwrap_or(DEFAULT_REQUIRED_REENTRY_CAP);
let round = reentries.map_or(0, |r| r.0);
if round >= cap {
let names: Vec<&str> = unmet.iter().map(|(n, _)| n.as_str()).collect();
tracing::warn!(
stage = %stage.name,
regions = ?names,
attempts = cap,
"required context regions still empty after re-run attempts; proceeding"
);
if let Some(mut flags) = flags {
for name in &names {
if !flags
.0
.required_regions_abandoned
.iter()
.any(|seen| seen == name)
{
flags.0.required_regions_abandoned.push((*name).to_string());
}
}
}
continue; }
inject_required_region_nudges(&mut window, &unmet);
commands
.entity(entity)
.remove::<ResolveTransition>()
.insert(ReadyToInfer)
.insert(RequiredReentries(round + 1));
}
}
#[derive(Component, Debug, Clone, Copy)]
pub struct OutputReentries(pub usize);
const MISSING_OUTPUT_NUDGE: &str = "This stage is not finished: you have not called `submit_output`. Whatever you wrote to \
files or to context is not what the caller receives - only the final output is. Call \
`submit_output` now with your answer.";
type FinalOutputQuery = (
Entity,
&'static AgentBlueprint,
&'static StageCursor,
&'static AgentState,
&'static mut ContextWindow,
Option<&'static OutputReentries>,
Option<&'static StageOutcome>,
Option<&'static crate::persistence::FinalOutput>,
Option<&'static mut crate::persistence::RunOutcomeFlags>,
);
pub fn require_final_output(
mut agents: Query<FinalOutputQuery, With<ResolveTransition>>,
mut commands: Commands,
) {
crate::tick_scope::clear();
for (entity, bp, cursor, state, mut window, reentries, outcome, submitted, mut flags) in
agents.iter_mut()
{
crate::tick_scope::enter(entity);
let stage = &bp.0.stages[cursor.index];
if !stage.require_output {
continue;
}
if submitted.is_some_and(|o| o.0.stage == state.current_stage) {
continue;
}
if outcome.is_some() {
tracing::warn!(
stage = %stage.name,
"stage ended without its required final output"
);
if let Some(flags) = flags.as_mut() {
flags.0.output_forced += 1;
}
continue;
}
let cap = leviath_core::blueprint::DEFAULT_OUTPUT_REENTRY_CAP;
let round = reentries.map_or(0, |r| r.0);
if round >= cap {
tracing::warn!(
stage = %stage.name,
attempts = cap,
"stage never produced its required final output; proceeding without one"
);
if let Some(flags) = flags.as_mut() {
flags.0.output_forced += 1;
}
continue; }
crate::pipeline::response::inject_system_nudge(&mut window, MISSING_OUTPUT_NUDGE);
commands
.entity(entity)
.remove::<ResolveTransition>()
.insert(ReadyToInfer)
.insert(OutputReentries(round + 1));
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum GateDecision {
Pass,
Forced,
Block(String),
}