Skip to main content

leviath_runtime/pipeline/
spawn.rs

1//! Spawning an agent: the caller-resolved per-stage inputs
2//! ([`ResolvedStage`]), the per-stage setup derived from them, and the two
3//! spawn entry points.
4
5use super::*;
6
7/// A blueprint stage resolved to a concrete provider, model, and effective tool
8/// set - the per-stage input to [`spawn_agent`]. The caller (CLI / daemon) owns
9/// the model-selection policy (overrides, availability, user defaults) and tool
10/// filtering; the runtime just turns the result into agent data.
11#[derive(Debug)]
12pub struct ResolvedStage {
13    /// The provider to call for this stage.
14    pub provider_name: String,
15    /// The resolved model name.
16    pub model: String,
17    /// The effective tool set for this stage (already filtered).
18    pub tools: Vec<Tool>,
19    /// Where to go if `provider_name` turns out to be unusable, best first.
20    /// See [`crate::pipeline::resolve_stage_candidates`].
21    pub fallbacks: Vec<leviath_core::blueprint::ModelEntry>,
22    /// The output shape resolved for this stage: the blueprint's default, the
23    /// stage's override, and the launching caller's request, combined. Resolved
24    /// caller-side (like the model and tool choices beside it) because only the
25    /// caller knows what was asked for at launch.
26    pub output: Option<leviath_core::output::OutputSpec>,
27}
28
29/// Fallback context window used when a stage's provider isn't registered (so
30/// percentage budgets can't be resolved against a real model). Matches
31/// [`leviath_providers::ModelCapabilities`]'s default `max_context_tokens`.
32pub(crate) const DEFAULT_CONTEXT_WINDOW_TOKENS: usize = 8192;
33
34/// Look up a model's context window (for resolving percentage region budgets)
35/// via the registered [`Providers`]. Falls back to
36/// [`DEFAULT_CONTEXT_WINDOW_TOKENS`] with a warning when the provider isn't
37/// registered - non-fatal, and `min_tokens` floors still protect regions.
38pub(crate) fn context_window_tokens(world: &World, provider_name: &str, model: &str) -> usize {
39    match world
40        .get_resource::<Providers>()
41        .and_then(|p| p.0.get(provider_name))
42    {
43        Some(provider) => provider.max_context_tokens(model),
44        None => {
45            tracing::warn!(
46                provider = provider_name,
47                model,
48                "provider not registered; using default context window for percentage budgets"
49            );
50            DEFAULT_CONTEXT_WINDOW_TOKENS
51        }
52    }
53}
54
55/// Build a stage's [`StageSetup`] from its blueprint definition: inference config
56/// (from the model parameters), tool-result routing, accepts-messages, layout,
57/// and system prompt.
58///
59/// `global_hints` is the caller's config-level toggle for each system-prompt
60/// hint; `agent_hints` the blueprint's agent-level override of the same. Each
61/// one cascades stage → agent → global here.
62pub(crate) fn stage_setup_from(
63    stage: &leviath_core::Stage,
64    global_hints: leviath_core::config::PromptHints,
65    agent_hints: leviath_core::config::PromptHintOverrides,
66    output: Option<leviath_core::output::OutputSpec>,
67) -> StageSetup {
68    let temperature = stage
69        .model
70        .parameters
71        .get("temperature")
72        .and_then(|v| v.as_f64())
73        .map(|t| t as f32);
74    // Every other model parameter (top_p, stop, seed, frequency_penalty, …) is
75    // passed through to the provider verbatim; only temperature/max_output_tokens
76    // are consumed specially above.
77    let extra_params: serde_json::Map<String, serde_json::Value> = stage
78        .model
79        .parameters
80        .iter()
81        .filter(|(k, _)| k.as_str() != "temperature" && k.as_str() != "max_output_tokens")
82        .map(|(k, v)| (k.clone(), v.clone()))
83        .collect();
84    let max_output_tokens = stage
85        .model
86        .parameters
87        .get("max_output_tokens")
88        .and_then(|v| v.as_u64())
89        .map(|t| t as usize);
90    let base_prompt = stage
91        .config
92        .get("system_prompt")
93        .and_then(|v| v.as_str())
94        .map(String::from);
95    // A fan-out stage's single inference IS the "split": fold its `split_prompt`
96    // (which asks for the JSON array of work items) onto any base instructions so
97    // the stage's normal inference produces the work items the split system parses.
98    let system_prompt = match &stage.mode {
99        leviath_core::blueprint::StageMode::FanOut { config }
100            if !config.split_prompt.trim().is_empty() =>
101        {
102            Some(match base_prompt {
103                Some(base) => format!("{base}\n\n{}", config.split_prompt),
104                None => config.split_prompt.clone(),
105            })
106        }
107        _ => base_prompt,
108    };
109    // A stage that must hand something back says so in its own instructions, on
110    // top of the `submit_output` tool description carrying the same shape. Both,
111    // because a format the model has no prior knowledge of - a2ui, a house
112    // schema - is exactly the case where one mention is easy to miss, and there
113    // is no parser downstream to catch a near miss.
114    let system_prompt = match (&output, stage.require_output) {
115        (Some(spec), true) => {
116            let described = leviath_core::describe_spec(spec);
117            let demand = match described.is_empty() {
118                true => format!(
119                    "Before this stage ends you must call `{tool}` with your final answer. It is \
120                     the only thing the caller receives.",
121                    tool = leviath_core::blueprint::SUBMIT_OUTPUT_TOOL
122                ),
123                false => format!(
124                    "Before this stage ends you must call `{tool}` with your final answer. It is \
125                     the only thing the caller receives.\n\n{described}",
126                    tool = leviath_core::blueprint::SUBMIT_OUTPUT_TOOL
127                ),
128            };
129            Some(match system_prompt {
130                Some(base) => format!("{base}\n\n{demand}"),
131                None => demand,
132            })
133        }
134        _ => system_prompt,
135    };
136    // Cascade each hint toggle: stage > agent > global (both default on).
137    let batch_tool_hint = leviath_core::taint::resolve_batch_tool_hint(
138        global_hints.batch_tool,
139        agent_hints.batch_tool,
140        stage.batch_tool_hint,
141    );
142    let shell_hint = leviath_core::taint::resolve_shell_hint(
143        global_hints.shell,
144        agent_hints.shell,
145        stage.shell_hint,
146    );
147    StageSetup {
148        inference_config: InferenceConfig {
149            temperature,
150            max_output_tokens,
151            extra_params,
152            batch_tool_hint,
153            shell_hint,
154            request_timeout_secs: stage.model.request_timeout_secs,
155        },
156        routing: stage.tool_result_routing.clone(),
157        accepts_messages: stage.accepts_messages,
158        context_layout: stage.context_layout.clone(),
159        system_prompt,
160        output,
161    }
162}
163
164/// Spawn a fully-formed agent into `world` from its blueprint, task, and
165/// per-stage resolution, and return its entity. Builds every stage's
166/// `StageInference`/`StageSetup` up front (so transitions are pure component
167/// swaps), seeds the context window, applies the **first** stage's setup (its
168/// layout and system prompt), pre-counts the first stage's visit, and marks the
169/// agent `ReadyToInfer`. Returns `Err` if the first stage's system prompt doesn't fit
170/// its region (the same hard failure the imperative loop raises at stage 0).
171///
172/// `stages` must be aligned with `blueprint.stages` (one [`ResolvedStage`] each).
173///
174/// `global_hints` is the caller's global config toggle for each system-prompt
175/// hint; each is resolved per stage against the blueprint's agent-level and
176/// per-stage override of the same name.
177pub fn spawn_agent(
178    world: &mut World,
179    agent_id: String,
180    blueprint: leviath_core::Blueprint,
181    task: &str,
182    stages: Vec<ResolvedStage>,
183    global_hints: leviath_core::config::PromptHints,
184) -> Result<Entity, String> {
185    let seeds = std::collections::HashMap::from([("task".to_string(), task.to_string())]);
186    // No compiled custom-region scripts on this path: script-backed regions
187    // require the seeded spawn (the CLI resolves and compiles them). A custom
188    // region spawned through here renders its fallback shape. Global nudge
189    // defaults are likewise a seeded-spawn concern (the CLI reads them from
190    // config.toml); agents spawned through here cascade straight from the
191    // blueprint to the built-in defaults.
192    spawn_agent_seeded(
193        world,
194        SeededSpawn {
195            agent_id,
196            blueprint,
197            seeds,
198            stages,
199            global_hints,
200            global_nudge: leviath_core::NudgeConfig::default(),
201            region_scripts: std::collections::HashMap::new(),
202        },
203    )
204}
205
206/// Everything a seeded spawn needs besides the world it spawns into.
207///
208/// The blueprint and its resolved stages travel with the seeds and the global
209/// defaults because all six are the same decision made at different layers:
210/// what this agent starts with. The caller resolves them; this consumes them.
211pub struct SeededSpawn {
212    /// The run id this agent is registered under.
213    pub agent_id: String,
214    /// The blueprint being spawned.
215    pub blueprint: leviath_core::Blueprint,
216    /// Content for named caller-input regions, keyed by region name.
217    pub seeds: std::collections::HashMap<String, String>,
218    /// The blueprint's stages, already resolved against the provider registry.
219    pub stages: Vec<ResolvedStage>,
220    /// Config-level prompt hints, applied where the blueprint says nothing.
221    pub global_hints: leviath_core::config::PromptHints,
222    /// The config-level nudge, likewise.
223    pub global_nudge: leviath_core::NudgeConfig,
224    /// Compiled render hooks, keyed by region name.
225    pub region_scripts: std::collections::HashMap<
226        String,
227        std::sync::Arc<leviath_scripting::region_hook::RegionScript>,
228    >,
229}
230
231/// Like [`spawn_agent`], but seeds the context window from a name→content map
232/// (caller-input regions filled by the CLI/ACP/API, plus blueprint-resolved
233/// seeds) rather than a single task string. `spawn_agent` is the thin wrapper
234/// that seeds only the `task` key.
235///
236/// `global_nudge` is the caller's config-level `[nudge]` defaults, captured on
237/// the agent as a [`crate::pipeline::response::GlobalNudge`] component; each
238/// field is resolved per stage against the blueprint's agent-level and
239/// per-stage nudge settings when an empty response is handled.
240pub fn spawn_agent_seeded(world: &mut World, spawn: SeededSpawn) -> Result<Entity, String> {
241    let SeededSpawn {
242        agent_id,
243        mut blueprint,
244        seeds,
245        stages,
246        global_hints,
247        global_nudge,
248        region_scripts,
249    } = spawn;
250    let seeds = &seeds;
251    // Resolve any percentage region budgets against each stage's model context
252    // window (the only place the model - and hence the window - is known). The
253    // global layout resolves against the entry stage (stage 0); each per-stage
254    // layout resolves against that stage's own model. Absolute layouts resolve to
255    // themselves, so this is a no-op for legacy blueprints.
256    let stage_windows: Vec<usize> = stages
257        .iter()
258        .map(|rs| context_window_tokens(world, &rs.provider_name, &rs.model))
259        .collect();
260    blueprint.context_layout = blueprint.context_layout.resolved(stage_windows[0]);
261    for (i, stage) in blueprint.stages.iter_mut().enumerate() {
262        if let Some(layout) = &stage.context_layout {
263            stage.context_layout = Some(layout.resolved(stage_windows[i]));
264        }
265    }
266    // Validate the resolved (fully-absolute) layouts, now that percentages are
267    // concrete numbers judged against the real model window.
268    blueprint
269        .context_layout
270        .validate()
271        .map_err(|e| e.to_string())?;
272    for stage in &blueprint.stages {
273        if let Some(layout) = &stage.context_layout {
274            layout.validate().map_err(|e| e.to_string())?;
275        }
276    }
277
278    // Kept before `stages` is consumed, so each stage's setup can fold the same
279    // shape into its system prompt that its tool description already carries.
280    let stage_outputs: Vec<Option<leviath_core::output::OutputSpec>> =
281        stages.iter().map(|rs| rs.output.clone()).collect();
282    let stage_infs: Vec<StageInference> = stages
283        .into_iter()
284        .map(|rs| StageInference {
285            provider_name: rs.provider_name,
286            model: rs.model,
287            tools: rs.tools,
288            tool_filter: None, // tools already resolved to the effective set
289            fallbacks: rs.fallbacks,
290            output: rs.output,
291        })
292        .collect();
293    let agent_hints = leviath_core::config::PromptHintOverrides {
294        batch_tool: blueprint.batch_tool_hint,
295        shell: blueprint.shell_hint,
296    };
297    let setups: Vec<StageSetup> = blueprint
298        .stages
299        .iter()
300        .zip(stage_outputs)
301        .map(|(s, output)| stage_setup_from(s, global_hints, agent_hints, output))
302        .collect();
303
304    // Seed the window from the blueprint layout + task, then apply stage 0's
305    // context setup (layout swap + system-prompt injection) just as entering any
306    // later stage would.
307    let mut window = ContextWindow::new(blueprint.context_layout.total_budget_tokens);
308    // Attach compiled custom-region scripts BEFORE seeding, so seed writes
309    // pass through each region's on_write hook like any other entry.
310    window.region_scripts = region_scripts;
311    crate::context_setup::init_window_seeded(&mut window, &blueprint, seeds);
312    // Before stage 0's prompt is injected, so it has somewhere of its own to go
313    // rather than being charged to whichever pinned region came first.
314    let prompts: Vec<Option<String>> = setups.iter().map(|s| s.system_prompt.clone()).collect();
315    crate::context_setup::ensure_stage_instructions_region(&mut window, &prompts);
316    apply_stage_context(&setups[0], &mut window)?;
317
318    let stage0_name = blueprint.stages[0].name.clone();
319    let stage0_inf = stage_infs[0].clone();
320    let setup0 = &setups[0];
321    let stage0_cfg = setup0.inference_config.clone();
322    let stage0_routing = setup0.routing.clone();
323    let accepts_messages = setup0.accepts_messages;
324
325    // Pre-count stage 0's visit: the imperative loop bumps a stage's visit after
326    // it runs and before resolving its transition, so stage 0 must read as
327    // visited once by the time its first transition resolves.
328    let mut visits = VisitCounts::default();
329    *visits.0.entry(stage0_name.clone()).or_insert(0) += 1;
330
331    // Seed the per-stage ledger (names + Pending) so the dashboard shows every
332    // stage's real name from the first persist, not just the active one.
333    let ledger = StageLedger(
334        blueprint
335            .stages
336            .iter()
337            .enumerate()
338            .map(|(i, s)| leviath_core::run_meta::StageRecord::new(s.name.clone(), i))
339            .collect(),
340    );
341
342    // Repetition detection is opt-in per blueprint.
343    let repetition = blueprint
344        .repetition_detection
345        .as_ref()
346        .map(crate::repetition::RepetitionDetector::from_detection_config);
347
348    let entity = world
349        .spawn((
350            AgentBlueprint(blueprint),
351            AgentState {
352                agent_id,
353                current_stage: stage0_name,
354                iteration: 0,
355                status: AgentStatus::Active,
356                spawned_children_ids: vec![],
357                pending_wait: None,
358                accepts_messages,
359            },
360            MessageInbox::default(),
361            StageCursor { index: 0 },
362            StageProgress::default(),
363            StageInferences(stage_infs),
364            StageSetups(setups),
365            visits,
366            window,
367            stage0_inf,
368            stage0_cfg,
369            ReadyToInfer,
370        ))
371        .id();
372    // Inserted after spawn: the bundle above is already at bevy's 15-tuple limit.
373    world.entity_mut(entity).insert((
374        ledger,
375        StageIoBuffer::default(),
376        crate::pipeline::response::GlobalNudge(global_nudge),
377    ));
378    if let Some(detector) = repetition {
379        world.entity_mut(entity).insert(detector);
380    }
381    if let Some(routing) = stage0_routing {
382        world
383            .entity_mut(entity)
384            .insert(crate::components::ToolResultRoutingComponent { routing });
385    }
386    Ok(entity)
387}
388
389#[cfg(test)]
390mod stage_instructions_fit_tests {
391    //! The reported spawn failure, reproduced end to end.
392
393    /// A layout shaped like the reported blueprint: a small `task` region and a
394    /// dedicated `stage_instructions` region with room for a stage prompt.
395    fn layout(window: usize) -> leviath_core::layout::ContextLayout {
396        use leviath_core::layout::{BudgetSpec, ContextLayout, RegionDefinition};
397        let pct = |p: f64| BudgetSpec::Percent {
398            percent: p,
399            min: None,
400            max: None,
401        };
402        let mut task =
403            RegionDefinition::new("task".to_string(), leviath_core::RegionKind::Pinned, 0);
404        task.budget = pct(0.02);
405        let mut instr = RegionDefinition::new(
406            leviath_core::layout::STAGE_INSTRUCTIONS_REGION.to_string(),
407            leviath_core::RegionKind::Pinned,
408            0,
409        );
410        instr.budget = pct(0.03);
411        ContextLayout::new(vec![task, instr], window).resolved(window)
412    }
413
414    /// A ~2.9k-token stage prompt: too big for 2% of a 128k window, comfortable
415    /// in 3%.
416    fn big_prompt() -> String {
417        "word ".repeat(2_600)
418    }
419
420    #[test]
421    fn a_stage_prompt_measured_at_spawn_uses_the_declared_region() {
422        let window_tokens = 128_000;
423        let layout = layout(window_tokens);
424        let task_max = layout
425            .regions
426            .iter()
427            .find(|r| r.name == "task")
428            .expect("task")
429            .max_tokens;
430        let instr_max = layout
431            .regions
432            .iter()
433            .find(|r| r.name == leviath_core::layout::STAGE_INSTRUCTIONS_REGION)
434            .expect("stage_instructions")
435            .max_tokens;
436        let prompt = big_prompt();
437        let tokens = leviath_core::estimate_tokens(&format!("[Stage instructions: {prompt}]"));
438        assert!(
439            tokens > task_max && tokens < instr_max,
440            "the fixture must reproduce the reported shape: {tokens} vs task {task_max} / \
441             stage_instructions {instr_max}"
442        );
443
444        let bp = leviath_core::Blueprint::new(
445            "t".to_string(),
446            "d".to_string(),
447            vec![leviath_core::Stage::new(
448                "work".to_string(),
449                leviath_core::blueprint::ModelConfig::new("p".to_string(), "m".to_string()),
450            )],
451            layout,
452        );
453        let mut window = crate::components::ContextWindow::new(window_tokens);
454        crate::context_setup::init_window_seeded(
455            &mut window,
456            &bp,
457            &std::collections::HashMap::new(),
458        );
459        let setup = crate::pipeline::transition::StageSetup {
460            inference_config: crate::components::InferenceConfig {
461                temperature: None,
462                max_output_tokens: None,
463                extra_params: Default::default(),
464                batch_tool_hint: false,
465                shell_hint: false,
466                request_timeout_secs: None,
467            },
468            routing: None,
469            accepts_messages: true,
470            context_layout: None,
471            system_prompt: Some(prompt),
472            output: None,
473        };
474        crate::pipeline::transition::apply_stage_context(&setup, &mut window)
475            .expect("the prompt fits the region declared for it");
476
477        let instr = window
478            .get_region(leviath_core::layout::STAGE_INSTRUCTIONS_REGION)
479            .expect("region exists");
480        assert!(
481            instr.content.iter().any(|e| e.content.contains("word")),
482            "the prompt landed in stage_instructions"
483        );
484    }
485
486    /// The reported failure itself: a blueprint that declares no
487    /// `stage_instructions` region at all.
488    ///
489    /// Its prompt went to `task` - the first pinned region, sized for a sentence
490    /// from the caller - and on a small window the spawn died with
491    /// `stage system prompt does not fit region 'task'`. The workaround was to
492    /// floor every task region with a `min_tokens` sized for the largest stage
493    /// prompt, coupling an unrelated region to prompt lengths.
494    #[test]
495    fn a_blueprint_that_declares_no_region_still_gets_one() {
496        use leviath_core::layout::{BudgetSpec, ContextLayout, RegionDefinition};
497        let window_tokens = 128_000;
498        let prompt = big_prompt();
499
500        // Only `task`, at 2% - exactly the reported declaration.
501        let mut task =
502            RegionDefinition::new("task".to_string(), leviath_core::RegionKind::Pinned, 0);
503        task.budget = BudgetSpec::Percent {
504            percent: 0.02,
505            min: None,
506            max: None,
507        };
508        let only_task = ContextLayout::new(vec![task], window_tokens).resolved(window_tokens);
509        let bp = leviath_core::Blueprint::new(
510            "t".to_string(),
511            "d".to_string(),
512            vec![leviath_core::Stage::new(
513                "work".to_string(),
514                leviath_core::blueprint::ModelConfig::new("p".to_string(), "m".to_string()),
515            )],
516            only_task,
517        );
518
519        let mut window = crate::components::ContextWindow::new(window_tokens);
520        crate::context_setup::init_window_seeded(
521            &mut window,
522            &bp,
523            &std::collections::HashMap::new(),
524        );
525        let prompts = vec![Some(prompt.clone())];
526        crate::context_setup::ensure_stage_instructions_region(&mut window, &prompts);
527
528        let setup = crate::pipeline::transition::StageSetup {
529            inference_config: crate::components::InferenceConfig {
530                temperature: None,
531                max_output_tokens: None,
532                extra_params: Default::default(),
533                batch_tool_hint: false,
534                shell_hint: false,
535                request_timeout_secs: None,
536            },
537            routing: None,
538            accepts_messages: true,
539            context_layout: None,
540            system_prompt: Some(prompt),
541            output: None,
542        };
543        crate::pipeline::transition::apply_stage_context(&setup, &mut window)
544            .expect("the prompt no longer has to fit the caller's task region");
545
546        let task_region = window.get_region("task").expect("task");
547        assert!(
548            task_region.content.is_empty(),
549            "the task region is left for the caller's task"
550        );
551        let instr = window
552            .get_region(leviath_core::layout::STAGE_INSTRUCTIONS_REGION)
553            .expect("the runtime made one");
554        assert!(instr.content.iter().any(|e| e.content.contains("word")));
555    }
556
557    /// Nothing to hold means no region: an empty pinned region is budget taken
558    /// from the work for nothing.
559    #[test]
560    fn no_region_is_made_when_no_stage_has_a_prompt() {
561        let mut window = crate::components::ContextWindow::new(1_000);
562        crate::context_setup::ensure_stage_instructions_region(&mut window, &[None, None]);
563        assert!(
564            window
565                .get_region(leviath_core::layout::STAGE_INSTRUCTIONS_REGION)
566                .is_none()
567        );
568    }
569
570    /// A declared region is left exactly as the author sized it.
571    #[test]
572    fn a_declared_region_is_not_resized() {
573        let mut window = crate::components::ContextWindow::new(100_000);
574        window.add_region(leviath_core::Region::new(
575            leviath_core::layout::STAGE_INSTRUCTIONS_REGION.to_string(),
576            leviath_core::RegionKind::Pinned,
577            4_242,
578        ));
579        crate::context_setup::ensure_stage_instructions_region(&mut window, &[Some(big_prompt())]);
580        assert_eq!(
581            window
582                .get_region(leviath_core::layout::STAGE_INSTRUCTIONS_REGION)
583                .expect("declared")
584                .max_tokens,
585            4_242
586        );
587    }
588
589    /// A prompt bigger than the window is still a spawn failure - it was always
590    /// going to be. What changes is that the message names the region the prompt
591    /// was going to, rather than the caller's task region.
592    #[test]
593    fn an_impossible_prompt_is_still_refused_and_names_the_right_region() {
594        let mut window = crate::components::ContextWindow::new(1_000);
595        window.add_region(leviath_core::Region::new(
596            "task".to_string(),
597            leviath_core::RegionKind::Pinned,
598            40,
599        ));
600        let prompt = "z".repeat(100_000);
601        crate::context_setup::ensure_stage_instructions_region(
602            &mut window,
603            &[Some(prompt.clone())],
604        );
605        // Capped at a quarter of the window rather than sized to the prompt.
606        assert_eq!(
607            window
608                .get_region(leviath_core::layout::STAGE_INSTRUCTIONS_REGION)
609                .expect("made")
610                .max_tokens,
611            250
612        );
613
614        let setup = crate::pipeline::transition::StageSetup {
615            inference_config: crate::components::InferenceConfig {
616                temperature: None,
617                max_output_tokens: None,
618                extra_params: Default::default(),
619                batch_tool_hint: false,
620                shell_hint: false,
621                request_timeout_secs: None,
622            },
623            routing: None,
624            accepts_messages: true,
625            context_layout: None,
626            system_prompt: Some(prompt),
627            output: None,
628        };
629        let err = crate::pipeline::transition::apply_stage_context(&setup, &mut window)
630            .expect_err("a prompt larger than the window cannot be housed");
631        assert!(
632            err.contains(leviath_core::layout::STAGE_INSTRUCTIONS_REGION),
633            "{err}"
634        );
635    }
636
637    /// Sized for the largest prompt in the blueprint, not the first stage's:
638    /// every stage's instructions pass through the same region.
639    #[test]
640    fn the_region_is_sized_for_the_widest_prompt() {
641        let mut window = crate::components::ContextWindow::new(100_000);
642        let small = "word ".repeat(10);
643        let large = big_prompt();
644        let expected = leviath_core::estimate_tokens(&format!("[Stage instructions: {large}]"));
645        crate::context_setup::ensure_stage_instructions_region(
646            &mut window,
647            &[Some(small), Some(large)],
648        );
649        assert_eq!(
650            window
651                .get_region(leviath_core::layout::STAGE_INSTRUCTIONS_REGION)
652                .expect("made")
653                .max_tokens,
654            expected
655        );
656    }
657
658    /// The reported shape: the stage carries its own `[context.regions]`, which
659    /// does not re-declare `stage_instructions`.
660    #[test]
661    fn a_scoped_stage_layout_still_routes_to_the_declared_region() {
662        use leviath_core::layout::{BudgetSpec, ContextLayout, RegionDefinition};
663        let window_tokens = 128_000;
664        let prompt = big_prompt();
665
666        // The stage narrows what it attends to and says nothing about
667        // stage_instructions - the region is the runtime's to fill.
668        let mut scoped_task =
669            RegionDefinition::new("task".to_string(), leviath_core::RegionKind::Pinned, 0);
670        scoped_task.budget = BudgetSpec::Percent {
671            percent: 0.02,
672            min: None,
673            max: None,
674        };
675        let scoped = ContextLayout::new(vec![scoped_task], window_tokens).resolved(window_tokens);
676
677        let bp = leviath_core::Blueprint::new(
678            "t".to_string(),
679            "d".to_string(),
680            vec![leviath_core::Stage::new(
681                "work".to_string(),
682                leviath_core::blueprint::ModelConfig::new("p".to_string(), "m".to_string()),
683            )],
684            layout(window_tokens),
685        );
686
687        let mut window = crate::components::ContextWindow::new(window_tokens);
688        crate::context_setup::init_window_seeded(
689            &mut window,
690            &bp,
691            &std::collections::HashMap::new(),
692        );
693        let setup = crate::pipeline::transition::StageSetup {
694            inference_config: crate::components::InferenceConfig {
695                temperature: None,
696                max_output_tokens: None,
697                extra_params: Default::default(),
698                batch_tool_hint: false,
699                shell_hint: false,
700                request_timeout_secs: None,
701            },
702            routing: None,
703            accepts_messages: true,
704            context_layout: Some(scoped),
705            system_prompt: Some(prompt),
706            output: None,
707        };
708        crate::pipeline::transition::apply_stage_context(&setup, &mut window)
709            .expect("the prompt fits the region declared for it");
710
711        let instr = window
712            .get_region(leviath_core::layout::STAGE_INSTRUCTIONS_REGION)
713            .expect("carried through the scoped layout");
714        assert!(
715            instr.content.iter().any(|e| e.content.contains("word")),
716            "the prompt landed in stage_instructions, not in the scoped task region"
717        );
718    }
719}