Skip to main content

vtcode_core/core/agent/
orient.rs

1//! Orient phase for agent sessions.
2//!
3//! Following the long-running harness pattern: "the agent must orient itself
4//! before it can act." The orient phase reads external artifacts (progress
5//! ledger, harness artifacts, loop memory, git log) so the agent has an
6//! accurate picture of the current state without re-exploring from scratch.
7//!
8//! This is the just-in-time context injection: references and summaries rather
9//! than full content, keeping the context lean while providing grounding.
10
11use std::path::Path;
12
13use super::handoff::HandoffRequest;
14use super::harness_artifacts;
15
16/// Structured orientation context gathered at session start.
17#[derive(Debug, Clone)]
18pub struct OrientationContext {
19    /// Summary of the progress ledger (completion ratio, stalled status, milestones).
20    pub progress_summary: Option<String>,
21    /// Summary of the current spec artifact.
22    pub spec_summary: Option<String>,
23    /// Summary of the current contract artifact.
24    pub contract_summary: Option<String>,
25    /// Summary of the sprint contract (pre-sprint negotiation).
26    pub sprint_contract_summary: Option<String>,
27    /// Summary of the last evaluation.
28    pub evaluation_summary: Option<String>,
29    /// Summary of the last outcome verification.
30    pub outcome_verification_summary: Option<String>,
31    /// Recent git log (last N commits).
32    pub recent_git_log: Option<String>,
33    /// Loop memory notes from previous iterations.
34    pub loop_notes: Option<String>,
35    /// Loop decisions from previous iterations.
36    pub loop_decisions: Option<String>,
37    /// Compaction summary from previous sessions.
38    pub compaction_summary: Option<String>,
39    /// Feature list summary — the persistent artifact the planner creates and
40    /// the evaluator modifies during feedback-driven replanning.
41    pub feature_list_summary: Option<String>,
42    /// Context reset manifest — present when the previous session triggered a
43    /// context reset (stall or compaction). Signals that this session starts
44    /// from a clean context and should reorient from artifacts only.
45    pub context_reset_manifest: Option<String>,
46    /// Handoff from a previous agent, if any.
47    pub handoff: Option<HandoffRequest>,
48}
49
50impl OrientationContext {
51    /// Render the orientation context as a section for the system prompt.
52    ///
53    /// Returns `None` if there is no meaningful context to inject (fresh session).
54    pub fn to_prompt_section(&self) -> Option<String> {
55        let mut parts = Vec::new();
56
57        if let Some(progress) = &self.progress_summary {
58            parts.push(format!("### Progress\n{progress}"));
59        }
60        if let Some(spec) = &self.spec_summary {
61            parts.push(format!("### Spec\n{spec}"));
62        }
63        if let Some(contract) = &self.contract_summary {
64            parts.push(format!("### Contract\n{contract}"));
65        }
66        if let Some(sprint) = &self.sprint_contract_summary {
67            parts.push(format!("### Sprint Contract\n{sprint}"));
68        }
69        if let Some(eval) = &self.evaluation_summary {
70            parts.push(format!("### Last Evaluation\n{eval}"));
71        }
72        if let Some(outcome) = &self.outcome_verification_summary {
73            parts.push(format!("### Outcome Verification\n{outcome}"));
74        }
75        if let Some(git) = &self.recent_git_log {
76            parts.push(format!("### Recent Git Log\n{git}"));
77        }
78        if let Some(notes) = &self.loop_notes {
79            parts.push(format!("### Loop Notes\n{notes}"));
80        }
81        if let Some(decisions) = &self.loop_decisions {
82            parts.push(format!("### Loop Decisions\n{decisions}"));
83        }
84        if let Some(compaction) = &self.compaction_summary {
85            parts.push(format!("### Previous Session Summary\n{compaction}"));
86        }
87        if let Some(feature_list) = &self.feature_list_summary {
88            parts.push(format!("### Feature List\n{feature_list}"));
89        }
90        if let Some(reset) = &self.context_reset_manifest {
91            parts.push(format!(
92                "### Context Reset\n\
93                 This session starts from a clean context. Previous conversation \
94                 history was deliberately discarded to clear noise and bad \
95                 assumptions. Reorient from the artifacts below.\n\n{reset}"
96            ));
97        }
98        if let Some(handoff) = &self.handoff {
99            parts.push(handoff.to_handoff_prompt());
100        }
101
102        if parts.is_empty() {
103            return None;
104        }
105
106        Some(format!("[Orientation Context]\n\n{}", parts.join("\n\n")))
107    }
108}
109
110/// Gather orientation context from the workspace.
111///
112/// Reads all available external artifacts and returns a structured summary
113/// that can be injected into the system prompt. Each read is best-effort:
114/// a missing artifact is `None`, not an error.
115pub fn gather_orientation(workspace_root: &Path, session_id: &str) -> OrientationContext {
116    use crate::loop_memory::{LoopMemoryStore, MarkdownLoopMemory};
117
118    let progress_summary = vtcode_memory::progress::load_progress(workspace_root, session_id)
119        .ok()
120        .flatten()
121        .map(|ledger| {
122            format!(
123                "Goal: {} | Completion: {:.0}% | Confidence: {:.2} | {}",
124                ledger.goal,
125                ledger.completion_ratio() * 100.0,
126                ledger.confidence,
127                if ledger.is_stalled() { "STALLED" } else { "on track" },
128            )
129        });
130
131    // Workspace-global task artifacts outlive their session. A leftover fixture
132    // must not describe this session's orientation.
133    let artifact_cutoff = harness_artifacts::session_artifact_cutoff(workspace_root, session_id);
134    let spec_summary = harness_artifacts::read_spec_summary_fresh(workspace_root, artifact_cutoff);
135    let contract_summary = harness_artifacts::read_contract_summary_fresh(workspace_root, artifact_cutoff);
136    let sprint_contract_summary =
137        harness_artifacts::read_sprint_contract_summary_fresh(workspace_root, artifact_cutoff);
138    let evaluation_summary = harness_artifacts::read_evaluation_summary_fresh(workspace_root, artifact_cutoff);
139    let outcome_verification_summary =
140        harness_artifacts::read_outcome_verification_summary_fresh(workspace_root, artifact_cutoff);
141    let feature_list_summary = harness_artifacts::read_feature_list_summary_fresh(workspace_root, artifact_cutoff);
142
143    let recent_git_log = gather_recent_git_log(workspace_root);
144
145    let memory = MarkdownLoopMemory::new(workspace_root);
146    let loop_notes = memory.read_notes().ok().filter(|s| !s.is_empty());
147    let loop_decisions = memory.read_decisions().ok().filter(|s| !s.is_empty());
148
149    // Read compaction summary from persistent artifacts.
150    // This follows the context engineering principle: "the summary should be
151    // written into a persistent artifact, such as progress.md, so that later
152    // sessions can read it."
153    let compaction_summary = read_compaction_summary(workspace_root);
154
155    // Read context reset manifest if a reset was triggered by the previous
156    // session. This signals that the current session starts from a clean
157    // context and should reorient from artifacts only.
158    let context_reset_manifest = read_context_reset_manifest(workspace_root);
159
160    OrientationContext {
161        progress_summary,
162        spec_summary,
163        contract_summary,
164        sprint_contract_summary,
165        evaluation_summary,
166        outcome_verification_summary,
167        recent_git_log,
168        loop_notes,
169        loop_decisions,
170        compaction_summary,
171        feature_list_summary,
172        context_reset_manifest,
173        handoff: None,
174    }
175}
176
177/// Gather the last 5 git commits as a compact summary.
178fn gather_recent_git_log(workspace_root: &Path) -> Option<String> {
179    let output = std::process::Command::new("git")
180        .args(["log", "--oneline", "-5", "--no-decorate"])
181        .current_dir(workspace_root)
182        .output()
183        .ok()?;
184
185    if !output.status.success() {
186        return None;
187    }
188
189    let log = String::from_utf8_lossy(&output.stdout).into_owned();
190    if log.trim().is_empty() { None } else { Some(log) }
191}
192
193/// Read compaction summary from persistent artifacts.
194///
195/// Reads `memories/compaction_summary.md` written by the compaction checkpoint.
196/// This allows later sessions to orient from previous session's compaction summary.
197fn read_compaction_summary(workspace_root: &Path) -> Option<String> {
198    let path = workspace_root.join("memories").join("compaction_summary.md");
199    let content = std::fs::read_to_string(&path).ok()?;
200    if content.trim().is_empty() { None } else { Some(content) }
201}
202
203/// Read the context reset manifest if a reset was triggered by the previous
204/// session. Returns the markdown content of the manifest, or `None` if no
205/// reset occurred.
206fn read_context_reset_manifest(workspace_root: &Path) -> Option<String> {
207    crate::core::agent::context_reset::read_manifest_orientation(workspace_root)
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn empty_context_returns_none() {
216        let ctx = OrientationContext {
217            progress_summary: None,
218            spec_summary: None,
219            contract_summary: None,
220            sprint_contract_summary: None,
221            evaluation_summary: None,
222            outcome_verification_summary: None,
223            recent_git_log: None,
224            loop_notes: None,
225            loop_decisions: None,
226            compaction_summary: None,
227            feature_list_summary: None,
228            context_reset_manifest: None,
229            handoff: None,
230        };
231        assert!(ctx.to_prompt_section().is_none());
232    }
233
234    #[test]
235    fn single_field_produces_section() {
236        let ctx = OrientationContext {
237            progress_summary: Some("Goal: test | Completion: 50%".to_string()),
238            spec_summary: None,
239            contract_summary: None,
240            sprint_contract_summary: None,
241            evaluation_summary: None,
242            outcome_verification_summary: None,
243            recent_git_log: None,
244            loop_notes: None,
245            loop_decisions: None,
246            compaction_summary: None,
247            feature_list_summary: None,
248            context_reset_manifest: None,
249            handoff: None,
250        };
251        let section = ctx.to_prompt_section().expect("should have section");
252        assert!(section.contains("[Orientation Context]"));
253        assert!(section.contains("### Progress"));
254        assert!(section.contains("Goal: test"));
255    }
256
257    #[test]
258    fn multiple_fields_rendered() {
259        let ctx = OrientationContext {
260            progress_summary: Some("progress".to_string()),
261            spec_summary: Some("spec".to_string()),
262            contract_summary: None,
263            sprint_contract_summary: None,
264            evaluation_summary: None,
265            outcome_verification_summary: None,
266            recent_git_log: Some("abc123 commit".to_string()),
267            loop_notes: None,
268            loop_decisions: None,
269            compaction_summary: None,
270            feature_list_summary: Some("auth, api".to_string()),
271            context_reset_manifest: None,
272            handoff: None,
273        };
274        let section = ctx.to_prompt_section().expect("should have section");
275        assert!(section.contains("### Progress"));
276        assert!(section.contains("### Spec"));
277        assert!(section.contains("### Recent Git Log"));
278        assert!(section.contains("### Feature List"));
279        // contract should be absent
280        assert!(!section.contains("### Contract"));
281    }
282
283    #[test]
284    fn context_reset_manifest_rendered() {
285        let ctx = OrientationContext {
286            progress_summary: Some("Goal: test | Completion: 50%".to_string()),
287            spec_summary: None,
288            contract_summary: None,
289            sprint_contract_summary: None,
290            evaluation_summary: None,
291            outcome_verification_summary: None,
292            recent_git_log: None,
293            loop_notes: None,
294            loop_decisions: None,
295            compaction_summary: None,
296            feature_list_summary: None,
297            context_reset_manifest: Some("# Context Reset Manifest\n\n**Trigger:** stall".to_string()),
298            handoff: None,
299        };
300        let section = ctx.to_prompt_section().expect("should have section");
301        assert!(section.contains("### Context Reset"));
302        assert!(section.contains("clean context"));
303        assert!(section.contains("**Trigger:** stall"));
304    }
305}