Skip to main content

kranz_engine/
digest.rs

1//! Orchestrator context digest (plan §4.8).
2//!
3//! Every injected orchestrator turn is prefixed with a deterministic digest
4//! rendered from [`MissionState`]: goal, contract, milestone/feature table,
5//! recent decisions, open user messages (~1-2k tokens). Because it is a pure
6//! function of state, [`render`] on the same state is byte-identical across
7//! runs — the layout is snapshot-tested.
8//!
9//! Re-seed path (§4.5/§4.8): if the streaming orchestrator session dies and
10//! `--resume` fails, [`render_reseed`] (digest + `plan.json`) is the ENTIRE
11//! context a fresh session needs.
12
13use crate::scrub::truncate_chars;
14use crate::types::{
15    AssertionCheck, FeatureOrigin, FeatureStatus, MilestoneStatus, MissionState, MissionStatus,
16};
17
18/// Max chars for individual titles, statements, commands, and user messages.
19const ITEM_MAX: usize = 160;
20
21/// Max chars for a decision summary line.
22const DECISION_MAX: usize = 200;
23
24/// Max decision lines rendered (oldest dropped; the reducer caps at the same
25/// number, this is defense in depth).
26const MAX_DECISIONS: usize = 10;
27
28/// Render the deterministic mission digest. Layout is stable and
29/// snapshot-tested — change it only together with the tests.
30pub fn render(state: &MissionState) -> String {
31    let mission = &state.mission;
32    let mut out = String::new();
33
34    out.push_str(&format!(
35        "MISSION {} [{}] — {}\n",
36        mission.id,
37        mission_status(mission.status),
38        truncate_chars(&mission.goal, ITEM_MAX)
39    ));
40    out.push_str(&format!(
41        "branch {} (from {}) | tokens in/out {}/{} | cost ${:.2}\n",
42        mission.mission_branch,
43        mission.base_branch,
44        state.totals.input,
45        state.totals.output,
46        state.total_cost_usd
47    ));
48
49    out.push_str("CONTRACT:\n");
50    for assertion in &mission.validation_contract {
51        out.push_str(&format!(
52            "- [{}|{}] {}",
53            assertion.id,
54            check_kind(assertion.check),
55            truncate_chars(&assertion.statement, ITEM_MAX)
56        ));
57        if let Some(command) = &assertion.command {
58            out.push_str(&format!(" :: {}", truncate_chars(command, ITEM_MAX)));
59        }
60        out.push('\n');
61    }
62
63    let repair_cap = state.config.max_fix_cycles_per_milestone;
64    out.push_str(&format!(
65        "REPAIR POLICY: current cap {repair_cap} rounds per milestone, per executor tier. \
66         Current policy supersedes planning/research observations. Exhaustion limits repairs; \
67         it does not justify a waiver or establish that the contract is met.\n"
68    ));
69    out.push_str("MILESTONES:\n");
70    for milestone in &mission.milestones {
71        out.push_str(&format!(
72            "{} [{}] {} (fixCycles {}, repair cap {}, remaining {})\n",
73            milestone.id,
74            milestone_status(milestone.status),
75            truncate_chars(&milestone.title, ITEM_MAX),
76            milestone.fix_cycles,
77            repair_cap,
78            repair_cap.saturating_sub(milestone.fix_cycles)
79        ));
80        for feature in &milestone.features {
81            out.push_str(&format!(
82                "  {} [{}|{}] {} (runs {}, respawns {})\n",
83                feature.id,
84                feature_status(feature.status),
85                feature_origin(feature.origin),
86                truncate_chars(&feature.title, ITEM_MAX),
87                feature.worker_runs.len(),
88                feature.respawns
89            ));
90        }
91    }
92
93    out.push_str("RECENT DECISIONS:\n");
94    let skip = state.recent_decisions.len().saturating_sub(MAX_DECISIONS);
95    for decision in &state.recent_decisions[skip..] {
96        out.push_str(&format!("- {}\n", truncate_chars(decision, DECISION_MAX)));
97    }
98
99    out.push_str("OPEN USER MESSAGES:\n");
100    if state.pending_user_messages.is_empty() {
101        out.push_str("(none)\n");
102    } else {
103        for message in &state.pending_user_messages {
104            out.push_str(&format!("- {}\n", truncate_chars(message, ITEM_MAX)));
105        }
106    }
107
108    out.push_str("You are resuming from durable state; the event log is authoritative.");
109    out
110}
111
112/// The full re-seed context (§4.8): the digest plus the approved plan JSON,
113/// verbatim. Digest + plan.json is the ENTIRE context a fresh orchestrator
114/// session is seeded with.
115pub fn render_reseed(state: &MissionState, plan_json: &str) -> String {
116    format!(
117        "{}\n\nAPPROVED PLAN (plan.json):\n{}",
118        render(state),
119        plan_json
120    )
121}
122
123fn mission_status(status: MissionStatus) -> &'static str {
124    match status {
125        MissionStatus::Planning => "planning",
126        MissionStatus::Approved => "approved",
127        MissionStatus::Running => "running",
128        MissionStatus::Paused => "paused",
129        MissionStatus::Blocked => "blocked",
130        MissionStatus::Validating => "validating",
131        MissionStatus::Complete => "complete",
132        MissionStatus::Failed => "failed",
133        MissionStatus::Abandoned => "abandoned",
134    }
135}
136
137fn milestone_status(status: MilestoneStatus) -> &'static str {
138    match status {
139        MilestoneStatus::Pending => "pending",
140        MilestoneStatus::Active => "active",
141        MilestoneStatus::Validating => "validating",
142        MilestoneStatus::Complete => "complete",
143        MilestoneStatus::Blocked => "blocked",
144    }
145}
146
147fn feature_status(status: FeatureStatus) -> &'static str {
148    match status {
149        FeatureStatus::Pending => "pending",
150        FeatureStatus::Active => "active",
151        FeatureStatus::Complete => "complete",
152        FeatureStatus::Skipped => "skipped",
153        FeatureStatus::Failed => "failed",
154    }
155}
156
157fn feature_origin(origin: FeatureOrigin) -> &'static str {
158    match origin {
159        FeatureOrigin::Plan => "plan",
160        FeatureOrigin::Fix => "fix",
161    }
162}
163
164fn check_kind(check: AssertionCheck) -> &'static str {
165    match check {
166        AssertionCheck::Command => "command",
167        AssertionCheck::AgentJudgement => "judgement",
168        AssertionCheck::PtyScript => "pty-script",
169    }
170}