Skip to main content

defect_agent/session/
context.rs

1//! Environment context: factual information injected into the `# Environment` section of
2//! the system prompt.
3//!
4//! The [`RunningContext`] aggregate holds session-varying parts (access method, cwd),
5//! while platform/version/shell detection results that are invariant for the entire
6//! process are cached with [`OnceLock`] — `os_info::get()` reads files and runs probes,
7//! so recomputing it every turn is not worthwhile.
8
9use std::path::Path;
10use std::sync::OnceLock;
11
12/// How the agent is connected — determines its understanding of the file and command
13/// execution environment.
14///
15/// Note: The `defect` CLI binary itself runs as an ACP server over stdio; all real paths
16/// currently go through [`Frontend::Acp`]. [`Frontend::Cli`] and [`Frontend::Headless`]
17/// are variants reserved for future forms (bare CLI user, server backend service).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Frontend {
20    /// Direct CLI interaction (reserved for a future local CLI user mode).
21    Cli,
22    /// Accessed via the ACP protocol (editor / IDE client).
23    ///
24    /// `fs_delegated` / `shell_delegated` come from the ACP `initialize` handshake
25    /// negotiation:
26    /// `true` means file I/O / command execution is delegated to the client proxy,
27    /// `false` means
28    /// executed locally. The agent uses this to know whether it is facing a local
29    /// environment or a remote proxy.
30    Acp {
31        fs_delegated: bool,
32        shell_delegated: bool,
33    },
34    /// Headless (reserved for a background service on the server).
35    Headless,
36}
37
38impl Frontend {
39    /// Whether the filesystem is delegated to the client proxy. Only true for
40    /// [`Frontend::Acp`] when `fs_delegated = true` is negotiated; all other variants
41    /// read and write directly on the local side.
42    fn fs_delegated(self) -> bool {
43        matches!(
44            self,
45            Self::Acp {
46                fs_delegated: true,
47                ..
48            }
49        )
50    }
51
52    /// A single-line description rendered into the `# Environment` section.
53    fn describe(self) -> String {
54        match self {
55            Self::Cli => "CLI".to_owned(),
56            Self::Acp {
57                fs_delegated,
58                shell_delegated,
59            } => format!(
60                "ACP (fs: {}, shell: {})",
61                delegation(fs_delegated),
62                delegation(shell_delegated),
63            ),
64            Self::Headless => "headless".to_owned(),
65        }
66    }
67}
68
69fn delegation(delegated: bool) -> &'static str {
70    if delegated { "delegated" } else { "local" }
71}
72
73/// Injects runtime environment context into the system prompt.
74///
75/// Static parts (platform, version, shell) are detected and cached internally by this
76/// type; callers only need to provide the session‑varying [`Frontend`] and `cwd`.
77pub struct RunningContext<'a> {
78    pub frontend: Frontend,
79    pub cwd: &'a Path,
80}
81
82impl<'a> RunningContext<'a> {
83    pub fn new(frontend: Frontend, cwd: &'a Path) -> Self {
84        Self { frontend, cwd }
85    }
86
87    /// Renders the body of the `# Environment` section (the title and separator are
88    /// handled by
89    /// [`crate::session::resolve_system_prompt`]).
90    pub fn render(&self) -> String {
91        let mut lines = Vec::with_capacity(6);
92        lines.push(format!("- platform: {}", platform_line()));
93        lines.push(format!("- defect version: {}", env!("CARGO_PKG_VERSION")));
94        lines.push(format!("- frontend: {}", self.frontend.describe()));
95        lines.push(format!("- cwd: {}", self.cwd.display()));
96        lines.push(format!("- shell: {}", shell_line()));
97        // The delegated filesystem (ACP) backchannel only supports text; `read_file` will
98        // fail on images.
99        // Explicitly instruct the model not to use `read_file` on images to avoid
100        // unnecessary error round-trips.
101        if self.frontend.fs_delegated() {
102            lines.push(
103                "- note: the filesystem is delegated and only supports text reads; \
104                 do not use read_file on image or other binary files (it will fail)"
105                    .to_owned(),
106            );
107        }
108        lines.join("\n")
109    }
110}
111
112/// Format: `linux / x86_64 (Ubuntu 22.04)`. OS/arch from compile-time `std` constants,
113/// distro and version from runtime `os_info` detection. Cached for the entire process.
114fn platform_line() -> &'static str {
115    static PLATFORM: OnceLock<String> = OnceLock::new();
116    PLATFORM.get_or_init(|| {
117        let info = os_info::get();
118        format!(
119            "{} / {} ({} {})",
120            std::env::consts::OS,
121            std::env::consts::ARCH,
122            info.os_type(),
123            info.version(),
124        )
125    })
126}
127
128/// Default shell: reads `$SHELL`, falls back to `unknown`. Cached for the process
129/// lifetime.
130fn shell_line() -> &'static str {
131    static SHELL: OnceLock<String> = OnceLock::new();
132    SHELL.get_or_init(|| std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_owned()))
133}
134
135#[cfg(test)]
136mod tests;