1use bevy_ecs::prelude::*;
11use leviath_core::RegionKind;
12use leviath_core::run_meta::{
13 ContextSnapshot, RegionEntrySnapshot, RegionSnapshot, RunMeta, RunStatus, StageRunStatus,
14 WaitMarkers, wait_reason_from,
15};
16
17use crate::components::{AgentState, AgentStatus, ContextWindow};
18
19#[derive(Component, Clone)]
23pub struct RunMetadata {
24 pub run_id: String,
26 pub agent_name: String,
28 pub agent_path: String,
30 pub task: String,
32 pub model: Option<String>,
34 pub workdir: String,
36 pub num_stages: usize,
38 pub started_at: i64,
40 pub parent_run_id: Option<String>,
42 pub metadata: std::collections::HashMap<String, String>,
44 pub callback_url: Option<String>,
46 pub callback_secret: Option<String>,
48 pub title: Option<String>,
50 pub unattended: bool,
59 pub read_paths: Option<leviath_core::run_meta::ReadPathGrantCounts>,
65 pub output_request: Option<leviath_core::output::OutputSpec>,
69}
70
71#[derive(Component, Clone, Copy, Default, Debug, PartialEq, Eq)]
74pub struct TokenTotals {
75 pub prompt_tokens: usize,
77 pub completion_tokens: usize,
79 pub cached_tokens: usize,
81 pub cache_write_tokens: usize,
83 pub tool_calls: usize,
85}
86
87#[derive(Component, Clone, Default, Debug, PartialEq)]
93pub struct RunOutcomeFlags(pub leviath_core::run_meta::RunFlags);
94
95impl RunOutcomeFlags {
96 pub fn for_blueprint(bp: &leviath_core::Blueprint) -> Self {
108 Self(leviath_core::run_meta::RunFlags {
109 no_output_tools: !bp.stages.iter().any(stage_can_modify),
110 ..Default::default()
111 })
112 }
113}
114
115#[derive(Component, Clone, Debug, PartialEq)]
123pub struct FinalOutput(pub leviath_core::output::FinalOutput);
124
125fn stage_can_modify(stage: &leviath_core::Stage) -> bool {
140 stage.available_tools.iter().any(|t| {
141 let canonical = leviath_tools::canonical_tool_name(t);
142 leviath_core::blueprint::MODIFYING_TOOLS.contains(&canonical)
143 || stage
144 .transitions
145 .iter()
146 .flat_map(|edges| edges.values())
147 .filter_map(|edge| edge.gate.as_ref())
148 .any(|gate| {
149 gate.tools
150 .iter()
151 .any(|extra| leviath_tools::canonical_tool_name(extra) == canonical)
152 })
153 })
154}
155
156impl TokenTotals {
157 pub fn add_usage(&mut self, usage: &leviath_providers::TokenUsage) {
159 self.prompt_tokens += usage.prompt_tokens;
160 self.completion_tokens += usage.completion_tokens;
161 self.cached_tokens += usage.cached_tokens;
162 self.cache_write_tokens += usage.cache_write_tokens;
163 }
164}
165
166pub fn is_empty_output(status: &AgentStatus, flags: &leviath_core::run_meta::RunFlags) -> bool {
180 matches!(
181 run_status_from(status),
182 RunStatus::Complete | RunStatus::Error | RunStatus::Cancelled
183 ) && flags.modified_file_count == 0
184 && !flags.produced_output
185 && !flags.no_output_tools
186}
187
188pub fn run_status_from(status: &AgentStatus) -> RunStatus {
190 match status {
191 AgentStatus::Idle | AgentStatus::Active => RunStatus::Running,
192 AgentStatus::Paused => RunStatus::Paused,
193 AgentStatus::Waiting => RunStatus::WaitingInput,
194 AgentStatus::Complete => RunStatus::Complete,
195 AgentStatus::Error { .. } => RunStatus::Error,
196 AgentStatus::Cancelled => RunStatus::Cancelled,
197 }
198}
199
200pub fn stage_status_from(status: &AgentStatus) -> StageRunStatus {
204 match status {
205 AgentStatus::Idle | AgentStatus::Active | AgentStatus::Paused => StageRunStatus::Active,
207 AgentStatus::Waiting => StageRunStatus::WaitingInput,
208 AgentStatus::Complete => StageRunStatus::Complete,
209 AgentStatus::Error { .. } | AgentStatus::Cancelled => StageRunStatus::Error,
210 }
211}
212
213fn region_kind_str(kind: &RegionKind) -> &'static str {
215 match kind {
216 RegionKind::Pinned => "pinned",
217 RegionKind::Temporary => "temporary",
218 RegionKind::Clearable => "clearable",
219 RegionKind::SlidingWindow { .. } => "sliding",
220 RegionKind::Compacting { .. } => "compacting",
221 RegionKind::CompactHistory { .. } => "history",
222 RegionKind::HashMap { .. } => "hashmap",
223 RegionKind::Checklist => "checklist",
224 RegionKind::Custom { .. } => "custom",
225 }
226}
227
228pub fn build_context_snapshot(window: &ContextWindow, stage_name: &str) -> ContextSnapshot {
231 let regions = window
232 .regions
233 .iter()
234 .map(|r| RegionSnapshot {
235 name: r.name.clone(),
236 kind: region_kind_str(&r.kind).to_string(),
237 current_tokens: r.current_tokens,
238 max_tokens: r.max_tokens,
239 entries: r
240 .content
241 .iter()
242 .enumerate()
243 .map(|(i, e)| RegionEntrySnapshot {
244 content: e.content.clone(),
245 tokens: e.tokens,
246 kind: e.kind.clone(),
247 metadata: e.metadata.clone(),
248 key: e.key.clone(),
249 taint: r
253 .taint
254 .as_ref()
255 .and_then(|t| t.entry_taint(i))
256 .unwrap_or_default(),
257 })
258 .collect(),
259 })
260 .collect();
261 ContextSnapshot {
262 stage_name: stage_name.to_string(),
263 total_tokens: window.current_tokens,
264 max_tokens: window.max_tokens,
265 regions,
266 }
267}
268
269pub struct RunMetaSources<'a> {
275 pub md: &'a RunMetadata,
277 pub state: &'a AgentState,
279 pub totals: &'a TokenTotals,
281 pub flags: &'a RunOutcomeFlags,
283 pub final_output: Option<&'a FinalOutput>,
285 pub parked: WaitMarkers,
288}
289
290pub struct RunPosition {
292 pub stage_index: usize,
294 pub now_secs: i64,
296 pub last_progress_at: Option<i64>,
298 pub depth: usize,
300 pub max_child_depth: usize,
302}
303
304pub fn build_run_meta(sources: RunMetaSources<'_>, at: RunPosition) -> RunMeta {
315 let RunMetaSources {
316 md,
317 state,
318 totals,
319 flags,
320 final_output,
321 parked,
322 } = sources;
323 let RunPosition {
324 stage_index,
325 now_secs,
326 last_progress_at,
327 depth,
328 max_child_depth,
329 } = at;
330 let status = run_status_from(&state.status);
331 let mut flags = flags.0.clone();
332 flags.produced_output = final_output.is_some();
335 flags.empty_output = is_empty_output(&state.status, &flags);
336 RunMeta {
337 run_id: md.run_id.clone(),
338 agent_name: md.agent_name.clone(),
339 agent_path: md.agent_path.clone(),
340 task: md.task.clone(),
341 model: md.model.clone(),
342 pid: 0, status,
344 current_stage: state.current_stage.clone(),
345 stage_index,
346 num_stages: md.num_stages,
347 iteration: state.iteration,
348 prompt_tokens: totals.prompt_tokens,
349 completion_tokens: totals.completion_tokens,
350 cached_tokens: totals.cached_tokens,
351 cache_write_tokens: totals.cache_write_tokens,
352 tool_calls: totals.tool_calls,
353 workdir: md.workdir.clone(),
354 started_at: md.started_at,
355 updated_at: now_secs,
356 last_progress_at,
357 error: match &state.status {
358 AgentStatus::Error { message } => Some(message.clone()),
359 _ => None,
360 },
361 title: md.title.clone(),
362 metadata: md.metadata.clone(),
363 callback_url: md.callback_url.clone(),
364 callback_secret: md.callback_secret.clone(),
365 parent_run_id: md.parent_run_id.clone(),
366 children: state.spawned_children_ids.clone(),
368 depth,
369 max_child_depth,
370 flags,
371 yolo: md.unattended,
372 read_paths: md.read_paths,
373 final_output: final_output.map(|o| o.0.descriptor()),
374 waiting_on: wait_reason_from(
379 matches!(state.status, AgentStatus::Waiting | AgentStatus::Paused),
380 &parked,
381 ),
382 output_request: md.output_request.clone(),
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use leviath_core::Region;
390 use leviath_core::run_meta::WaitReason;
391 use leviath_providers::TokenUsage;
392
393 fn state(status: AgentStatus) -> AgentState {
394 AgentState {
395 agent_id: "a".to_string(),
396 current_stage: "plan".to_string(),
397 iteration: 4,
398 status,
399 spawned_children_ids: vec![],
400 pending_wait: None,
401 accepts_messages: true,
402 }
403 }
404
405 fn metadata() -> RunMetadata {
406 RunMetadata {
407 run_id: "run-1".to_string(),
408 agent_name: "coder".to_string(),
409 agent_path: "/agents/coder".to_string(),
410 task: "do it".to_string(),
411 model: Some("anthropic/claude".to_string()),
412 workdir: "/work".to_string(),
413 num_stages: 3,
414 started_at: 1000,
415 parent_run_id: Some("parent".to_string()),
416 metadata: std::collections::HashMap::from([("k".to_string(), "v".to_string())]),
417 callback_url: Some("http://cb".to_string()),
418 callback_secret: Some("sekret".to_string()),
419 title: Some("Do It".to_string()),
420 unattended: false,
421 read_paths: None,
422 output_request: None,
423 }
424 }
425
426 fn stage_with(tools: &[&str], gate_tools: Option<&[&str]>) -> leviath_core::Stage {
430 let mut stage = leviath_core::Stage::new(
431 "s".to_string(),
432 leviath_core::blueprint::ModelConfig::new("p".to_string(), "m".to_string()),
433 );
434 stage.available_tools = tools.iter().map(|t| (*t).to_string()).collect();
435 stage.transitions = gate_tools.map(|extra| {
436 let gate = (!extra.is_empty()).then(|| leviath_core::blueprint::TransitionGate {
437 require_modifications: true,
438 tools: extra.iter().map(|t| (*t).to_string()).collect(),
439 ..Default::default()
440 });
441 std::collections::HashMap::from([(
442 "next".to_string(),
443 leviath_core::blueprint::TransitionEdge {
444 target: "next".to_string(),
445 condition: leviath_core::blueprint::TransitionCondition::Always,
446 hint: None,
447 transform: leviath_core::blueprint::EdgeTransform::Direct,
448 gate,
449 stuck: None,
450 },
451 )])
452 });
453 stage
454 }
455
456 fn blueprint_of(stages: Vec<leviath_core::Stage>) -> leviath_core::Blueprint {
457 leviath_core::Blueprint::new(
458 "bp".to_string(),
459 "d".to_string(),
460 stages,
461 leviath_core::ContextLayout::new(vec![], 1000),
462 )
463 }
464
465 fn no_output_tools(stages: Vec<leviath_core::Stage>) -> bool {
466 RunOutcomeFlags::for_blueprint(&blueprint_of(stages))
467 .0
468 .no_output_tools
469 }
470
471 #[test]
472 fn for_blueprint_asks_whether_any_stage_could_have_written() {
473 assert!(no_output_tools(vec![]));
475 assert!(no_output_tools(vec![stage_with(
478 &["read_file", "spawn_agent", "context_write"],
479 None
480 )]));
481 assert!(no_output_tools(vec![stage_with(&["bash"], None)]));
485 assert!(!no_output_tools(vec![stage_with(&["write_file"], None)]));
487 assert!(!no_output_tools(vec![stage_with(&["edit_file"], None)]));
488 assert!(!no_output_tools(vec![
490 stage_with(&["read_file"], None),
491 stage_with(&["write_file"], None),
492 ]));
493 }
494
495 #[test]
496 fn for_blueprint_honors_a_gate_declaring_its_own_write_tool() {
497 assert!(!no_output_tools(vec![stage_with(
500 &["mcp__fs__put"],
501 Some(&["mcp__fs__put"])
502 )]));
503 assert!(no_output_tools(vec![stage_with(
505 &["read_file"],
506 Some(&["mcp__fs__put"])
507 )]));
508 assert!(no_output_tools(vec![stage_with(&["read_file"], Some(&[]))]));
510 assert!(no_output_tools(vec![stage_with(
512 &["mcp__fs__put"],
513 Some(&["mcp__other__put"])
514 )]));
515 }
516
517 #[test]
518 fn is_empty_output_needs_a_stopped_run_that_could_have_written() {
519 let nothing = leviath_core::run_meta::RunFlags::default();
520 assert!(!is_empty_output(&AgentStatus::Active, ¬hing));
522 assert!(!is_empty_output(&AgentStatus::Idle, ¬hing));
523 assert!(!is_empty_output(&AgentStatus::Paused, ¬hing));
524 assert!(!is_empty_output(&AgentStatus::Waiting, ¬hing));
525 for status in [
527 AgentStatus::Complete,
528 AgentStatus::Cancelled,
529 AgentStatus::Error {
530 message: "x".to_string(),
531 },
532 ] {
533 assert!(is_empty_output(&status, ¬hing));
534 }
535 let mut wrote = leviath_core::run_meta::RunFlags::default();
537 wrote.record_modification("src/a.rs");
538 assert!(!is_empty_output(&AgentStatus::Complete, &wrote));
539 let incapable = leviath_core::run_meta::RunFlags {
541 no_output_tools: true,
542 ..Default::default()
543 };
544 assert!(!is_empty_output(&AgentStatus::Complete, &incapable));
545 }
546
547 #[test]
548 fn status_mapping_covers_all_variants() {
549 assert_eq!(run_status_from(&AgentStatus::Idle), RunStatus::Running);
550 assert_eq!(run_status_from(&AgentStatus::Active), RunStatus::Running);
551 assert_eq!(run_status_from(&AgentStatus::Paused), RunStatus::Paused);
552 assert_eq!(
553 run_status_from(&AgentStatus::Waiting),
554 RunStatus::WaitingInput
555 );
556 assert_eq!(run_status_from(&AgentStatus::Complete), RunStatus::Complete);
557 assert_eq!(
558 run_status_from(&AgentStatus::Error {
559 message: "x".to_string()
560 }),
561 RunStatus::Error
562 );
563 assert_eq!(
564 run_status_from(&AgentStatus::Cancelled),
565 RunStatus::Cancelled
566 );
567 }
568
569 #[test]
570 fn stage_status_mapping_covers_all_variants() {
571 use leviath_core::run_meta::StageRunStatus;
572 assert_eq!(
573 stage_status_from(&AgentStatus::Idle),
574 StageRunStatus::Active
575 );
576 assert_eq!(
577 stage_status_from(&AgentStatus::Active),
578 StageRunStatus::Active
579 );
580 assert_eq!(
581 stage_status_from(&AgentStatus::Paused),
582 StageRunStatus::Active
583 );
584 assert_eq!(
585 stage_status_from(&AgentStatus::Waiting),
586 StageRunStatus::WaitingInput
587 );
588 assert_eq!(
589 stage_status_from(&AgentStatus::Complete),
590 StageRunStatus::Complete
591 );
592 assert_eq!(
593 stage_status_from(&AgentStatus::Error {
594 message: "x".to_string()
595 }),
596 StageRunStatus::Error
597 );
598 assert_eq!(
599 stage_status_from(&AgentStatus::Cancelled),
600 StageRunStatus::Error
601 );
602 }
603
604 #[test]
605 fn token_totals_accumulate() {
606 let mut t = TokenTotals::default();
607 t.add_usage(&TokenUsage {
608 prompt_tokens: 10,
609 completion_tokens: 5,
610 total_tokens: 15,
611 cached_tokens: 2,
612 cache_write_tokens: 1,
613 });
614 t.add_usage(&TokenUsage {
615 prompt_tokens: 3,
616 completion_tokens: 4,
617 total_tokens: 7,
618 cached_tokens: 0,
619 cache_write_tokens: 0,
620 });
621 t.tool_calls = 6;
622 assert_eq!(t.prompt_tokens, 13);
623 assert_eq!(t.completion_tokens, 9);
624 assert_eq!(t.cached_tokens, 2);
625 assert_eq!(t.cache_write_tokens, 1);
626 }
627
628 #[test]
634 fn each_parking_marker_names_its_own_reason() {
635 let cases = [
636 (
637 WaitMarkers {
638 gate_prompt: true,
639 ..Default::default()
640 },
641 WaitReason::TaintGate,
642 ),
643 (
644 WaitMarkers {
645 interaction_point: true,
646 ..Default::default()
647 },
648 WaitReason::InteractionPoint,
649 ),
650 (
651 WaitMarkers {
652 fan_out_outstanding: Some(3),
653 ..Default::default()
654 },
655 WaitReason::FanOutWorkers { outstanding: 3 },
656 ),
657 (
658 WaitMarkers {
659 children_outstanding: Some(2),
660 ..Default::default()
661 },
662 WaitReason::Children { outstanding: 2 },
663 ),
664 ];
665 for (markers, expected) in cases {
666 assert_eq!(
667 wait_reason_from(true, &markers),
668 Some(expected.clone()),
669 "{markers:?}"
670 );
671 assert_eq!(wait_reason_from(false, &markers), None, "{markers:?}");
674 }
675 }
676
677 #[test]
680 fn a_fan_out_parent_is_never_reported_as_waiting_on_a_person() {
681 let reason = wait_reason_from(
682 true,
683 &WaitMarkers {
684 fan_out_outstanding: Some(8),
685 ..Default::default()
686 },
687 )
688 .expect("a parked parent has a reason");
689 assert_eq!(reason, WaitReason::FanOutWorkers { outstanding: 8 });
690 assert!(
691 !reason.needs_a_person(),
692 "its workers are still going; nobody is needed"
693 );
694 }
695
696 #[test]
698 fn build_run_meta_records_why_a_run_is_parked() {
699 let meta = build_run_meta(
700 RunMetaSources {
701 md: &metadata(),
702 state: &state(AgentStatus::Waiting),
703 totals: &TokenTotals::default(),
704 flags: &RunOutcomeFlags::default(),
705 final_output: None,
706 parked: WaitMarkers {
707 children_outstanding: Some(2),
708 ..Default::default()
709 },
710 },
711 RunPosition {
712 stage_index: 0,
713 now_secs: 0,
714 last_progress_at: None,
715 depth: 0,
716 max_child_depth: 0,
717 },
718 );
719 assert_eq!(meta.status, RunStatus::WaitingInput);
720 assert_eq!(
721 meta.waiting_on,
722 Some(WaitReason::Children { outstanding: 2 })
723 );
724 }
725
726 #[test]
727 fn build_run_meta_fills_dynamic_and_static_fields() {
728 let md = metadata();
729 let totals = TokenTotals {
730 prompt_tokens: 100,
731 completion_tokens: 50,
732 cached_tokens: 10,
733 cache_write_tokens: 5,
734 tool_calls: 7,
735 };
736 let mut st = state(AgentStatus::Active);
737 st.spawned_children_ids = vec!["child-a".to_string(), "child-b".to_string()];
738 let meta = build_run_meta(
739 RunMetaSources {
740 md: &md,
741 state: &st,
742 totals: &totals,
743 flags: &RunOutcomeFlags::default(),
744 final_output: None,
745 parked: WaitMarkers::default(),
746 },
747 RunPosition {
748 stage_index: 1,
749 now_secs: 2000,
750 last_progress_at: Some(1900),
751 depth: 1,
752 max_child_depth: 4,
753 },
754 );
755
756 assert_eq!(meta.run_id, "run-1");
757 assert_eq!(meta.status, RunStatus::Running);
758 assert_eq!(meta.current_stage, "plan");
759 assert_eq!(meta.stage_index, 1);
760 assert_eq!(meta.iteration, 4);
761 assert_eq!(meta.prompt_tokens, 100);
762 assert_eq!(meta.tool_calls, 7);
763 assert_eq!(meta.updated_at, 2000);
764 assert_eq!(meta.last_progress_at, Some(1900));
767 assert_eq!(meta.parent_run_id.as_deref(), Some("parent"));
768 assert_eq!(meta.callback_url.as_deref(), Some("http://cb"));
769 assert_eq!(meta.callback_secret.as_deref(), Some("sekret"));
770 assert!(meta.error.is_none());
771 assert_eq!(
773 meta.children,
774 vec!["child-a".to_string(), "child-b".to_string()]
775 );
776 assert_eq!(meta.depth, 1);
777 assert_eq!(meta.max_child_depth, 4);
778 assert!(!meta.yolo);
780 }
781
782 #[test]
785 fn build_run_meta_records_an_unattended_run() {
786 let mut md = metadata();
787 md.unattended = true;
788 let meta = build_run_meta(
789 RunMetaSources {
790 md: &md,
791 state: &state(AgentStatus::Active),
792 totals: &TokenTotals::default(),
793 flags: &RunOutcomeFlags::default(),
794 final_output: None,
795 parked: WaitMarkers::default(),
796 },
797 RunPosition {
798 stage_index: 1,
799 now_secs: 2000,
800 last_progress_at: None,
801 depth: 1,
802 max_child_depth: 4,
803 },
804 );
805 assert!(meta.yolo);
806 }
807
808 #[test]
809 fn build_run_meta_flags_empty_output_only_once_the_run_has_stopped() {
810 let mut flags = RunOutcomeFlags::default();
811 flags.0.gates_forced = 2;
812 let running = build_run_meta(
814 RunMetaSources {
815 md: &metadata(),
816 state: &state(AgentStatus::Active),
817 totals: &TokenTotals::default(),
818 flags: &flags,
819 final_output: None,
820 parked: WaitMarkers::default(),
821 },
822 RunPosition {
823 stage_index: 0,
824 now_secs: 1000,
825 last_progress_at: None,
826 depth: 0,
827 max_child_depth: 0,
828 },
829 );
830 assert!(!running.flags.empty_output);
831 assert_eq!(running.flags.gates_forced, 2);
832
833 for status in [
835 AgentStatus::Complete,
836 AgentStatus::Cancelled,
837 AgentStatus::Error {
838 message: "x".to_string(),
839 },
840 ] {
841 let meta = build_run_meta(
842 RunMetaSources {
843 md: &metadata(),
844 state: &state(status),
845 totals: &TokenTotals::default(),
846 flags: &flags,
847 final_output: None,
848 parked: WaitMarkers::default(),
849 },
850 RunPosition {
851 stage_index: 0,
852 now_secs: 1000,
853 last_progress_at: None,
854 depth: 0,
855 max_child_depth: 0,
856 },
857 );
858 assert!(meta.flags.empty_output);
859 }
860
861 let mut wrote = RunOutcomeFlags::default();
863 wrote.0.record_modification("src/a.rs");
864 let meta = build_run_meta(
865 RunMetaSources {
866 md: &metadata(),
867 state: &state(AgentStatus::Complete),
868 totals: &TokenTotals::default(),
869 flags: &wrote,
870 final_output: None,
871 parked: WaitMarkers::default(),
872 },
873 RunPosition {
874 stage_index: 0,
875 now_secs: 1000,
876 last_progress_at: None,
877 depth: 0,
878 max_child_depth: 0,
879 },
880 );
881 assert!(!meta.flags.empty_output);
882 assert_eq!(meta.flags.modified_files, vec!["src/a.rs".to_string()]);
883
884 let mut incapable = RunOutcomeFlags::default();
887 incapable.0.no_output_tools = true;
888 let meta = build_run_meta(
889 RunMetaSources {
890 md: &metadata(),
891 state: &state(AgentStatus::Complete),
892 totals: &TokenTotals::default(),
893 flags: &incapable,
894 final_output: None,
895 parked: WaitMarkers::default(),
896 },
897 RunPosition {
898 stage_index: 0,
899 now_secs: 1000,
900 last_progress_at: None,
901 depth: 0,
902 max_child_depth: 0,
903 },
904 );
905 assert!(!meta.flags.empty_output);
906 assert!(meta.flags.no_output_tools);
907 }
908
909 #[test]
910 fn build_run_meta_carries_error_message() {
911 let meta = build_run_meta(
912 RunMetaSources {
913 md: &metadata(),
914 state: &state(AgentStatus::Error {
915 message: "boom".to_string(),
916 }),
917 totals: &TokenTotals::default(),
918 flags: &RunOutcomeFlags::default(),
919 final_output: None,
920 parked: WaitMarkers::default(),
921 },
922 RunPosition {
923 stage_index: 2,
924 now_secs: 3000,
925 last_progress_at: None,
926 depth: 0,
927 max_child_depth: 0,
928 },
929 );
930 assert_eq!(meta.status, RunStatus::Error);
931 assert_eq!(meta.error.as_deref(), Some("boom"));
932 }
933
934 #[test]
940 fn build_run_meta_carries_a_submitted_output_and_clears_the_empty_verdict() {
941 let submitted = FinalOutput(leviath_core::output::FinalOutput::new(
942 "Renamed two helpers and updated their callers.",
943 Some("markdown".to_string()),
944 "summary".to_string(),
945 1234,
946 ));
947 let meta = build_run_meta(
948 RunMetaSources {
949 md: &metadata(),
950 state: &state(AgentStatus::Complete),
951 totals: &TokenTotals::default(),
952 flags: &RunOutcomeFlags::default(),
953 final_output: Some(&submitted),
954 parked: WaitMarkers::default(),
955 },
956 RunPosition {
957 stage_index: 0,
958 now_secs: 1000,
959 last_progress_at: None,
960 depth: 0,
961 max_child_depth: 0,
962 },
963 );
964 let carried = meta.final_output.expect("the submission reached meta.json");
965 assert_eq!(
968 carried.bytes,
969 "Renamed two helpers and updated their callers.".len()
970 );
971 assert_eq!(carried.format.as_deref(), Some("markdown"));
972 assert_eq!(carried.stage, "summary");
973 assert!(meta.flags.produced_output);
974 assert!(!meta.flags.empty_output);
976 }
977
978 #[test]
981 fn a_run_that_submits_nothing_is_still_judged_empty() {
982 let meta = build_run_meta(
983 RunMetaSources {
984 md: &metadata(),
985 state: &state(AgentStatus::Complete),
986 totals: &TokenTotals::default(),
987 flags: &RunOutcomeFlags::default(),
988 final_output: None,
989 parked: WaitMarkers::default(),
990 },
991 RunPosition {
992 stage_index: 0,
993 now_secs: 1000,
994 last_progress_at: None,
995 depth: 0,
996 max_child_depth: 0,
997 },
998 );
999 assert!(meta.final_output.is_none());
1000 assert!(!meta.flags.produced_output);
1001 assert!(meta.flags.empty_output);
1002 }
1003
1004 #[test]
1005 fn context_snapshot_captures_all_region_kinds() {
1006 let mut w = ContextWindow::new(1000);
1007 w.add_region(Region::new("pin".to_string(), RegionKind::Pinned, 100));
1008 w.add_region(Region::new("tmp".to_string(), RegionKind::Temporary, 100));
1009 w.add_region(Region::new("clr".to_string(), RegionKind::Clearable, 100));
1010 w.add_region(Region::new(
1011 "slide".to_string(),
1012 RegionKind::SlidingWindow {
1013 max_items: 5,
1014 eviction_strategy: leviath_core::EvictionStrategy::PerItem,
1015 },
1016 100,
1017 ));
1018 w.add_region(Region::new(
1019 "comp".to_string(),
1020 RegionKind::Compacting {
1021 threshold_tokens: 5,
1022 },
1023 100,
1024 ));
1025 w.add_region(Region::new(
1026 "hist".to_string(),
1027 RegionKind::CompactHistory {
1028 source_region: "comp".to_string(),
1029 },
1030 100,
1031 ));
1032 w.add_region(Region::new(
1033 "map".to_string(),
1034 RegionKind::HashMap { max_entries: None },
1035 100,
1036 ));
1037 w.add_region(Region::new(
1038 "brain".to_string(),
1039 RegionKind::Custom {
1040 script: "b.rhai".to_string(),
1041 persistent: false,
1042 },
1043 100,
1044 ));
1045 w.add_region(Region::new("todos".to_string(), RegionKind::Checklist, 100));
1046 let _ = w.add_to_region("pin", "hello".to_string(), 3);
1047 w.current_tokens = w.calculate_tokens();
1048
1049 let snap = build_context_snapshot(&w, "plan");
1050
1051 assert_eq!(snap.stage_name, "plan");
1052 let kinds: Vec<&str> = snap.regions.iter().map(|r| r.kind.as_str()).collect();
1053 assert_eq!(
1054 kinds,
1055 vec![
1056 "pinned",
1057 "temporary",
1058 "clearable",
1059 "sliding",
1060 "compacting",
1061 "history",
1062 "hashmap",
1063 "custom",
1064 "checklist"
1065 ]
1066 );
1067 let pin = snap.regions.iter().find(|r| r.name == "pin").unwrap();
1069 assert_eq!(pin.entries.len(), 1);
1070 assert_eq!(pin.entries[0].content, "hello");
1071 }
1072}