1use clap::Args;
9use leviath_core::run_archive::RunPoint;
10
11#[derive(Args, Debug)]
13pub struct ContextArgs {
14 pub run_id: String,
16 #[arg(long)]
18 pub json: bool,
19 #[arg(long)]
21 pub full: bool,
22}
23
24pub async fn execute(args: ContextArgs) -> anyhow::Result<()> {
26 let history = crate::runstate::context_history(&args.run_id);
27 if history.is_empty() {
28 anyhow::bail!(
29 "no context history for run '{}' (no readable run.lvr archive)",
30 args.run_id
31 );
32 }
33 let out = render(&args.run_id, &history, args.json, args.full);
34 print!("{out}");
35 Ok(())
36}
37
38fn render(run_id: &str, history: &[RunPoint], json: bool, full: bool) -> String {
40 if json {
41 return format!(
43 "{}\n",
44 serde_json::to_string_pretty(history).expect("RunPoint history always serializes")
45 );
46 }
47 let mut out = String::new();
48 out.push_str(&format!(
49 "Context history for run '{run_id}' ({} point{}):\n\n",
50 history.len(),
51 if history.len() == 1 { "" } else { "s" }
52 ));
53 for (i, point) in history.iter().enumerate() {
54 out.push_str(&format!(
55 "[{}] {} stage={} iter={} status={} tokens={}/{}\n",
56 i + 1,
57 format_time(point.at),
58 point.context.stage_name,
59 point.meta.iteration,
60 point.meta.status,
61 point.context.total_tokens,
62 point.context.max_tokens,
63 ));
64 for region in &point.context.regions {
65 out.push_str(&format!(
66 " region {} ({}) - {} tok, {} entr{}\n",
67 region.name,
68 region.kind,
69 region.current_tokens,
70 region.entries.len(),
71 if region.entries.len() == 1 {
72 "y"
73 } else {
74 "ies"
75 },
76 ));
77 if full {
78 for entry in ®ion.entries {
79 for line in entry.content.lines() {
80 out.push_str(&format!(" {line}\n"));
81 }
82 }
83 }
84 }
85 out.push('\n');
86 }
87 out
88}
89
90fn format_time(secs: i64) -> String {
93 match chrono::DateTime::from_timestamp(secs, 0) {
94 Some(dt) => dt
95 .with_timezone(&chrono::Local)
96 .format("%Y-%m-%d %H:%M:%S")
97 .to_string(),
98 None => secs.to_string(),
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use leviath_core::run_meta::{ContextSnapshot, RegionEntrySnapshot, RegionSnapshot, RunMeta};
106
107 fn point(stage: &str, tokens: usize, entries: Vec<&str>) -> RunPoint {
108 RunPoint {
109 meta: RunMeta::new(
110 "run-x".to_string(),
111 "coder".to_string(),
112 "/agents/coder".to_string(),
113 "task".to_string(),
114 None,
115 "/work".to_string(),
116 2,
117 ),
118 context: ContextSnapshot {
119 stage_name: stage.to_string(),
120 total_tokens: tokens,
121 max_tokens: 1000,
122 regions: vec![RegionSnapshot {
123 name: "conv".to_string(),
124 kind: "clearable".to_string(),
125 current_tokens: tokens,
126 max_tokens: 1000,
127 entries: entries
128 .into_iter()
129 .map(|c| RegionEntrySnapshot {
130 content: c.to_string(),
131 tokens: 1,
132 kind: leviath_core::region::EntryKind::Text,
133 metadata: None,
134 key: None,
135 taint: Default::default(),
136 })
137 .collect(),
138 }],
139 },
140 at: 0,
141 }
142 }
143
144 #[test]
145 fn render_human_lists_each_point_and_region() {
146 let history = vec![
147 point("plan", 1, vec!["hi"]),
148 point("implement", 3, vec!["a", "b"]),
149 ];
150 let out = render("run-x", &history, false, false);
151 assert!(out.contains("Context history for run 'run-x' (2 points)"));
152 assert!(out.contains("[1]") && out.contains("stage=plan"));
153 assert!(out.contains("[2]") && out.contains("stage=implement"));
154 assert!(out.contains("region conv (clearable)"));
155 assert!(!out.contains(" hi"));
157 }
158
159 #[test]
160 fn render_full_includes_entry_contents() {
161 let history = vec![point("plan", 1, vec!["secret line"])];
162 let out = render("run-x", &history, false, true);
163 assert!(out.contains("secret line"));
164 assert!(out.contains("(1 point)"));
166 assert!(out.contains("1 entry"));
167 }
168
169 #[test]
170 fn render_json_is_a_parseable_array() {
171 let history = vec![point("plan", 1, vec!["hi"])];
172 let out = render("run-x", &history, true, false);
173 let parsed: Vec<RunPoint> = serde_json::from_str(&out).unwrap();
174 assert_eq!(parsed.len(), 1);
175 assert_eq!(parsed[0].context.stage_name, "plan");
176 }
177
178 #[test]
179 fn format_time_handles_valid_and_out_of_range() {
180 let formatted = format_time(1_700_000_000);
182 assert!(formatted.contains('-') && formatted.contains(':'));
183 assert_eq!(format_time(i64::MAX), i64::MAX.to_string());
185 }
186
187 #[test]
188 fn execute_prints_history_for_a_run_with_an_archive() {
189 crate::runstate::with_isolated_runs_dir("context-execute-ok", |_d| {
190 use leviath_core::run_archive::{self, RunIdentity, RunRecord};
191 let run_id = "ctx-exec-run";
192 std::fs::create_dir_all(crate::runstate::run_dir(run_id)).unwrap();
193 let mut buf = Vec::new();
194 run_archive::write_archive_start(&mut buf, run_archive::RUN_ARCHIVE_VERSION).unwrap();
195 run_archive::write_record(
196 &mut buf,
197 &RunRecord::Header {
198 identity: RunIdentity {
199 run_id: run_id.to_string(),
200 machine_id: "m".to_string(),
201 world_id: "w".to_string(),
202 created_at: 0,
203 },
204 meta: Box::new(RunMeta::new(
205 run_id.to_string(),
206 "a".to_string(),
207 "/p".to_string(),
208 "t".to_string(),
209 None,
210 "/w".to_string(),
211 1,
212 )),
213 },
214 )
215 .unwrap();
216 run_archive::write_record(
217 &mut buf,
218 &RunRecord::ContextCheckpoint {
219 snapshot: ContextSnapshot {
220 stage_name: "plan".to_string(),
221 total_tokens: 1,
222 max_tokens: 100,
223 regions: vec![],
224 },
225 at: 1,
226 },
227 )
228 .unwrap();
229 std::fs::write(crate::runstate::run_dir(run_id).join("run.lvr"), &buf).unwrap();
230
231 let args = ContextArgs {
233 run_id: run_id.to_string(),
234 json: true,
235 full: false,
236 };
237 let rt = tokio::runtime::Runtime::new().unwrap();
238 assert!(rt.block_on(execute(args)).is_ok());
239 let missing = ContextArgs {
241 run_id: "no-archive-run".to_string(),
242 json: false,
243 full: false,
244 };
245 assert!(rt.block_on(execute(missing)).is_err());
246 });
247 }
248}