use std::sync::Arc;
use zagens_core::engine::{
BudgetPolicy, ContextCompiler, ContextLayer, ContextSource, RenderedBlock, SourceId,
};
use zagens_core::session::Session;
#[derive(Debug, Clone, Default)]
pub struct ContextCompilerStateSnapshot {
pub static_base_text: String,
pub compaction_text: String,
pub cycle_briefings_text: String,
pub working_set_text: String,
pub tool_catalog_est_tokens: u32,
pub step_idx: u32,
pub scratchpad_reminder_est_tokens: u32,
}
pub const SCRATCHPAD_REMINDER_TOKEN_ESTIMATE: u32 = 80;
#[must_use]
pub fn scratchpad_reminder_est_tokens(
config: &zagens_core::scratchpad::ScratchpadConfig,
step: &zagens_core::engine::scratchpad_state::ScratchpadStepState,
) -> u32 {
if config.enabled
&& config.remind_enabled
&& step.scratchpad_writes_this_step == 0
&& step.readonly_tool_successes >= config.remind_after_readonly_tools
{
SCRATCHPAD_REMINDER_TOKEN_ESTIMATE
} else {
0
}
}
pub const TOOL_CATALOG_BUDGET_TOKENS: u32 = 12_000;
impl ContextCompilerStateSnapshot {
#[must_use]
pub fn from_session(session: &Session, step_idx: u32) -> Self {
let tpl = crate::prompts::COMPACT_TEMPLATE;
let full_text: String = match session.system_prompt.as_ref() {
None => String::new(),
Some(crate::models::SystemPrompt::Text(t)) => t.clone(),
Some(crate::models::SystemPrompt::Blocks(blocks)) => blocks
.iter()
.map(|b| b.text.as_str())
.collect::<Vec<_>>()
.join("\n\n---\n\n"),
};
let (static_base_text, compaction_text) = if let Some(pos) = full_text.find(tpl) {
let split = pos + tpl.len();
(
full_text[..split].to_string(),
full_text[split..].to_string(),
)
} else {
(full_text, String::new())
};
let cycle_briefings_text = render_cycle_briefings(&session.cycle_briefings);
let working_set_text = working_set_turn_meta(session, &session.workspace);
Self {
static_base_text,
compaction_text,
cycle_briefings_text,
working_set_text,
tool_catalog_est_tokens: TOOL_CATALOG_BUDGET_TOKENS,
step_idx,
scratchpad_reminder_est_tokens: 0,
}
}
}
#[must_use]
pub fn build_compiler_from_snapshot(snapshot: &ContextCompilerStateSnapshot) -> ContextCompiler {
let static_text = snapshot.static_base_text.clone();
let compaction_text = snapshot.compaction_text.clone();
let cycle_text = snapshot.cycle_briefings_text.clone();
let working_set_text = snapshot.working_set_text.clone();
let tool_catalog_tokens = snapshot.tool_catalog_est_tokens;
let scratchpad_reminder_tokens = snapshot.scratchpad_reminder_est_tokens;
ContextCompiler::new()
.register(ContextSource {
id: SourceId("system.static"),
layer: ContextLayer::StaticPrefix,
priority: 255,
budget: BudgetPolicy::Fixed(8192),
render: Arc::new(move |_| {
if static_text.is_empty() {
vec![]
} else {
vec![RenderedBlock::new(static_text.clone())]
}
}),
})
.register(ContextSource {
id: SourceId("tools.catalog"),
layer: ContextLayer::StaticPrefix,
priority: 254,
budget: BudgetPolicy::Fixed(tool_catalog_tokens),
render: Arc::new(move |_| {
vec![RenderedBlock::placeholder(tool_catalog_tokens)]
}),
})
.register(ContextSource {
id: SourceId("memory.compaction"),
layer: ContextLayer::SemiStatic,
priority: 200,
budget: BudgetPolicy::Elastic { min: 0, max: 4000 },
render: Arc::new(move |_| {
if compaction_text.is_empty() {
vec![]
} else {
vec![RenderedBlock::new(compaction_text.clone())]
}
}),
})
.register(ContextSource {
id: SourceId("memory.cycle"),
layer: ContextLayer::SemiStatic,
priority: 170,
budget: BudgetPolicy::Elastic { min: 0, max: 3000 },
render: Arc::new(move |_| {
if cycle_text.is_empty() {
vec![]
} else {
vec![RenderedBlock::new(cycle_text.clone())]
}
}),
})
.register(ContextSource {
id: SourceId("working_set"),
layer: ContextLayer::Volatile,
priority: 160,
budget: BudgetPolicy::Elastic { min: 0, max: 1500 },
render: Arc::new(move |_| {
if working_set_text.is_empty() {
vec![]
} else {
vec![RenderedBlock::new(working_set_text.clone())]
}
}),
})
.register(ContextSource {
id: SourceId("scratchpad.reminder"),
layer: ContextLayer::Volatile,
priority: 140,
budget: BudgetPolicy::Elastic { min: 0, max: 800 },
render: Arc::new(move |_| {
if scratchpad_reminder_tokens > 0 {
vec![RenderedBlock::placeholder(scratchpad_reminder_tokens)]
} else {
vec![]
}
}),
})
.register(ContextSource {
id: SourceId("steer"),
layer: ContextLayer::Volatile,
priority: 100,
budget: BudgetPolicy::Elastic { min: 0, max: 2000 },
render: Arc::new(|_| vec![]),
})
}
#[must_use]
pub fn assemble_system_text_for_v2(snapshot: &ContextCompilerStateSnapshot) -> String {
format!("{}{}", snapshot.static_base_text, snapshot.compaction_text)
}
pub fn working_set_turn_meta(session: &Session, workspace: &std::path::Path) -> String {
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let ws_summary = session
.working_set
.summary_block(workspace)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
match ws_summary {
Some(ws) => format!("Current local date: {today}\n{ws}"),
None => format!("Current local date: {today}"),
}
}
pub fn render_cycle_briefings(briefings: &[zagens_core::cycle::CycleBriefing]) -> String {
briefings
.iter()
.filter(|b| !b.briefing_text.trim().is_empty())
.map(|b| {
format!(
"[CYCLE BRIEFING — cycle {} at {}]\n{}",
b.cycle,
b.timestamp.to_rfc3339(),
b.briefing_text.trim()
)
})
.collect::<Vec<_>>()
.join("\n\n")
}
#[cfg(test)]
mod tests {
use super::*;
use zagens_core::engine::ContextProjection;
#[test]
fn build_compiler_from_snapshot_registers_expected_sources() {
let marker = crate::prompts::COMPACT_TEMPLATE;
let snapshot = ContextCompilerStateSnapshot {
static_base_text: format!("static base\n\n{marker}"),
compaction_text: "after-marker".into(),
cycle_briefings_text: String::new(),
working_set_text: "Current local date: 2099-01-01".into(),
tool_catalog_est_tokens: TOOL_CATALOG_BUDGET_TOKENS,
scratchpad_reminder_est_tokens: 0,
step_idx: 0,
};
let compiler = build_compiler_from_snapshot(&snapshot);
assert_eq!(
compiler.source_count(),
7,
"system.static + tools.catalog + memory.compaction + memory.cycle + working_set + scratchpad.reminder + steer"
);
}
#[test]
fn snapshot_static_text_matches_marker_boundary() {
let marker = crate::prompts::COMPACT_TEMPLATE;
let base = "base content";
let extra = "compaction content";
let snapshot = ContextCompilerStateSnapshot {
static_base_text: format!("{base}\n\n{marker}"),
compaction_text: extra.to_string(),
cycle_briefings_text: String::new(),
working_set_text: String::new(),
tool_catalog_est_tokens: TOOL_CATALOG_BUDGET_TOKENS,
scratchpad_reminder_est_tokens: 0,
step_idx: 0,
};
let compiler = build_compiler_from_snapshot(&snapshot);
let session = crate::core::session::Session::new(
"test".to_string(),
std::path::PathBuf::from("/tmp"),
false,
false,
std::path::PathBuf::from("/tmp/notes.txt"),
std::path::PathBuf::from("/tmp/mcp.json"),
);
let proj = ContextProjection::from_session(&session, 0);
let ctx = compiler.compile(&proj);
let static_src = ctx
.contributions
.iter()
.find(|c| c.source_id.0 == "system.static")
.expect("system.static source missing");
let compaction_src = ctx
.contributions
.iter()
.find(|c| c.source_id.0 == "memory.compaction");
assert!(
static_src.token_count > 0,
"system.static must produce tokens"
);
if !extra.is_empty() {
let comp_count = compaction_src.map(|c| c.token_count).unwrap_or(0);
assert!(
comp_count > 0,
"memory.compaction must produce tokens for dynamic content"
);
}
}
#[test]
fn render_cycle_briefings_empty_when_no_briefings() {
let text = render_cycle_briefings(&[]);
assert!(text.is_empty());
}
#[test]
fn render_cycle_briefings_includes_cycle_number_and_text() {
use chrono::Utc;
use zagens_core::cycle::CycleBriefing;
let briefings = vec![
CycleBriefing {
cycle: 1,
timestamp: Utc::now(),
briefing_text: "Decisions: chose A.".into(),
token_estimate: 10,
},
CycleBriefing {
cycle: 2,
timestamp: Utc::now(),
briefing_text: "Completed phase 1.".into(),
token_estimate: 12,
},
];
let text = render_cycle_briefings(&briefings);
assert!(text.contains("cycle 1"), "must reference cycle 1");
assert!(text.contains("cycle 2"), "must reference cycle 2");
assert!(text.contains("Decisions: chose A."));
assert!(text.contains("Completed phase 1."));
}
#[test]
fn snapshot_from_session_splits_at_compact_template() {
use std::path::PathBuf;
let marker = crate::prompts::COMPACT_TEMPLATE;
let workspace = PathBuf::from("/tmp");
let mut session = Session::new(
"test-model".into(),
workspace.clone(),
false,
false,
PathBuf::from("/tmp/notes.txt"),
PathBuf::from("/tmp/mcp.json"),
);
let full_text = format!("base text\n\n{marker}\nvolatile section");
session.system_prompt = Some(crate::models::SystemPrompt::Text(full_text.clone()));
let snapshot = ContextCompilerStateSnapshot::from_session(&session, 0);
assert!(
snapshot.static_base_text.contains(marker),
"static_base_text must include COMPACT_TEMPLATE"
);
assert_eq!(
snapshot.compaction_text, "\nvolatile section",
"compaction_text must be text after COMPACT_TEMPLATE"
);
let reassembled = format!("{}{}", snapshot.static_base_text, snapshot.compaction_text);
assert_eq!(reassembled, full_text);
}
}