| //! T0: the small always-loaded index an agent starts with. | |
| //! | |
| //! Under 2k tokens by construction. It names where to look, not what is true: the latest | |
| //! handoffs, the standing decisions, and pointers. Everything else is one `get` away. | |
| use dm_core::approx_tokens; | |
| use dm_index::Hit; | |
| /// The token budget T0 is held under. | |
| pub const BUDGET_TOKENS: usize = 2000; | |
| /// What goes into a T0, before trimming. | |
| pub struct Inputs<'a> { | |
| /// The domain this T0 is for. | |
| pub domain: &'a str, | |
| /// Who generated it, for the header. | |
| pub generated_by: &'a str, | |
| /// Open handoffs, newest first. | |
| pub handoffs: &'a [Hit], | |
| /// Standing decisions, newest first. | |
| pub decisions: &'a [Hit], | |
| /// Conventions and methods, newest first. | |
| pub conventions: &'a [Hit], | |
| /// How many active entries the domain holds. | |
| pub total_active: u64, | |
| /// How many are past their review date. | |
| pub due_for_review: usize, | |
| /// How many admitted tasks are waiting. | |
| pub tasks_waiting: usize, | |
| /// The policy digest: the L0 core in full, the other layers' rules in force by id, | |
| /// and the exceptions in force, one line each. | |
| pub policy_digest: &'a [String], | |
| } | |
| fn line(h: &Hit) -> String { | |
| let summary = if h.summary.is_empty() { | |
| String::new() | |
| } else { | |
| format!(": {}", h.summary) | |
| }; | |
| // the decay clock, where the reader sees it | |
| let due = if h.review_due { ", review due" } else { "" }; | |
| format!( | |
| "- `{}` {}{} ({}{due})\n", | |
| h.id, h.title, summary, h.trust_tier | |
| ) | |
| } | |
| /// Render a T0 under [`BUDGET_TOKENS`]. Sections are trimmed from the bottom, oldest | |
| /// decisions first, then conventions, then handoffs, so the newest handoff is the last | |
| /// thing to go. | |
| #[must_use] | |
| pub fn render(inputs: &Inputs<'_>) -> String { | |
| let mut handoffs = inputs.handoffs.len(); | |
| let mut decisions = inputs.decisions.len(); | |
| let mut conventions = inputs.conventions.len(); | |
| loop { | |
| let text = render_with(inputs, handoffs, decisions, conventions); | |
| if approx_tokens(&text) <= BUDGET_TOKENS { | |
| return text; | |
| } | |
| if decisions > 0 { | |
| decisions -= 1; | |
| } else if conventions > 0 { | |
| conventions -= 1; | |
| } else if handoffs > 1 { | |
| handoffs -= 1; | |
| } else { | |
| return text; | |
| } | |
| } | |
| } | |
| fn render_with( | |
| inputs: &Inputs<'_>, | |
| handoffs: usize, | |
| decisions: usize, | |
| conventions: usize, | |
| ) -> String { | |
| let mut out = String::new(); | |
| out.push_str(&format!( | |
| "# {} · T0 · generated by {}\n\n", | |
| inputs.domain, inputs.generated_by | |
| )); | |
| out.push_str("## Now\n\n"); | |
| if inputs.handoffs.is_empty() { | |
| out.push_str("No open handoffs. Start from the ledger.\n"); | |
| } else { | |
| for h in &inputs.handoffs[..handoffs] { | |
| out.push_str(&line(h)); | |
| } | |
| if handoffs < inputs.handoffs.len() { | |
| out.push_str(&format!( | |
| "- {} older handoffs, via search\n", | |
| inputs.handoffs.len() - handoffs | |
| )); | |
| } | |
| } | |
| out.push_str("\n## Standing decisions (full text via get)\n\n"); | |
| if inputs.decisions.is_empty() { | |
| out.push_str("None recorded yet.\n"); | |
| } else { | |
| for h in &inputs.decisions[..decisions] { | |
| out.push_str(&line(h)); | |
| } | |
| if decisions < inputs.decisions.len() { | |
| out.push_str(&format!( | |
| "- {} more, via search\n", | |
| inputs.decisions.len() - decisions | |
| )); | |
| } | |
| } | |
| if !inputs.conventions.is_empty() { | |
| out.push_str("\n## Conventions and methods\n\n"); | |
| for h in &inputs.conventions[..conventions] { | |
| out.push_str(&line(h)); | |
| } | |
| if conventions < inputs.conventions.len() { | |
| out.push_str(&format!( | |
| "- {} more, via search\n", | |
| inputs.conventions.len() - conventions | |
| )); | |
| } | |
| } | |
| if !inputs.policy_digest.is_empty() { | |
| out.push_str("\n## Policy digest\n\n"); | |
| for l in inputs.policy_digest { | |
| out.push_str(&format!("- {l}\n")); | |
| } | |
| } | |
| out.push_str("\n## Pointers\n\n"); | |
| out.push_str(&format!( | |
| "- {} active entries in this domain; search before writing a fact that may exist\n", | |
| inputs.total_active | |
| )); | |
| if inputs.due_for_review > 0 { | |
| out.push_str(&format!( | |
| "- {} entries past their review date; treat their facts as unverified\n", | |
| inputs.due_for_review | |
| )); | |
| } | |
| out.push_str(&format!( | |
| "- {} tasks waiting on the ledger; `claim_next` takes the highest-ranked\n", | |
| inputs.tasks_waiting | |
| )); | |
| out.push_str("- when you stop, write a `handoff` entry; the next session starts from it, not from your transcript\n"); | |
| out | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use dm_core::{EntryType, Label, TrustTier}; | |
| fn hit(id: &str, title: &str, summary: &str, kind: EntryType) -> Hit { | |
| Hit { | |
| id: id.into(), | |
| domain: "dom-a".into(), | |
| entry_type: kind, | |
| trust_tier: TrustTier::Raw, | |
| classification: Label::Public, | |
| title: title.into(), | |
| summary: summary.into(), | |
| score: 0.0, | |
| review_due: false, | |
| } | |
| } | |
| fn inputs<'a>(handoffs: &'a [Hit], decisions: &'a [Hit], conventions: &'a [Hit]) -> Inputs<'a> { | |
| Inputs { | |
| domain: "dom-a", | |
| generated_by: "memory 0.1", | |
| handoffs, | |
| decisions, | |
| conventions, | |
| total_active: 42, | |
| due_for_review: 3, | |
| tasks_waiting: 2, | |
| policy_digest: &[], | |
| } | |
| } | |
| #[test] | |
| fn empty_domain_still_orients() { | |
| let t = render(&inputs(&[], &[], &[])); | |
| assert!(t.starts_with("# dom-a · T0")); | |
| assert!(t.contains("No open handoffs")); | |
| assert!(t.contains("None recorded yet")); | |
| assert!(t.contains("42 active entries")); | |
| assert!(t.contains("3 entries past their review date")); | |
| assert!(t.contains("2 tasks waiting")); | |
| assert!(t.contains("write a `handoff` entry")); | |
| assert!( | |
| !t.contains("Conventions and methods"), | |
| "an empty section is left out" | |
| ); | |
| } | |
| #[test] | |
| fn lists_handoffs_decisions_and_conventions() { | |
| let h = [hit( | |
| "mem-a-h1", | |
| "Stopped at step 3", | |
| "Resume there.", | |
| EntryType::Handoff, | |
| )]; | |
| let d = [hit( | |
| "mem-a-d1", | |
| "Retry with backoff", | |
| "", | |
| EntryType::Decision, | |
| )]; | |
| let c = [hit( | |
| "mem-a-c1", | |
| "Commit prefixes", | |
| "feat, fix, docs.", | |
| EntryType::Convention, | |
| )]; | |
| let t = render(&inputs(&h, &d, &c)); | |
| assert!(t.contains("- `mem-a-h1` Stopped at step 3: Resume there. (raw)")); | |
| assert!(t.contains("- `mem-a-d1` Retry with backoff (raw)")); | |
| assert!(t.contains("## Conventions and methods")); | |
| assert!(t.contains("`mem-a-c1`")); | |
| } | |
| #[test] | |
| fn stays_under_budget_and_drops_decisions_first() { | |
| let long = "word ".repeat(60); | |
| let handoffs: Vec<Hit> = (0..5) | |
| .map(|i| hit(&format!("mem-a-h{i}"), "Handoff", &long, EntryType::Handoff)) | |
| .collect(); | |
| let decisions: Vec<Hit> = (0..40) | |
| .map(|i| { | |
| hit( | |
| &format!("mem-a-d{i}"), | |
| "Decision", | |
| &long, | |
| EntryType::Decision, | |
| ) | |
| }) | |
| .collect(); | |
| let t = render(&inputs(&handoffs, &decisions, &[])); | |
| assert!(approx_tokens(&t) <= BUDGET_TOKENS, "{}", approx_tokens(&t)); | |
| assert!(t.contains("more, via search")); | |
| assert!(t.contains("mem-a-h0"), "the newest handoff survives"); | |
| assert!(!t.contains("mem-a-d39"), "the oldest decision goes first"); | |
| } | |
| #[test] | |
| fn a_single_oversized_handoff_is_kept_rather_than_producing_nothing() { | |
| let huge = "word ".repeat(3000); | |
| let handoffs = [hit("mem-a-h0", "Huge", &huge, EntryType::Handoff)]; | |
| let t = render(&inputs(&handoffs, &[], &[])); | |
| assert!(t.contains("mem-a-h0")); | |
| } | |
| } |