Skip to main content

kaish_kernel/tools/
context.rs

1//! Execution context for tools.
2
3use std::collections::HashMap;
4use std::path::{Component, PathBuf};
5use std::sync::Arc;
6
7use crate::ast::Value;
8use crate::backend::{KernelBackend, LocalBackend};
9use crate::dispatch::PipelinePosition;
10use crate::interpreter::Scope;
11use crate::scheduler::{JobManager, PipeReader, PipeWriter, StderrStream};
12use crate::tools::ToolRegistry;
13use crate::vfs::VfsRouter;
14
15use super::traits::ToolSchema;
16
17/// Output context determines how command output should be formatted.
18///
19/// Different contexts prefer different output formats:
20/// - **Interactive** — Pretty columns, colors, traditional tree (TTY/REPL)
21/// - **Piped** — Raw output for pipeline processing
22/// - **Model** — Token-efficient compact formats (MCP server / agent context)
23/// - **Script** — Non-interactive script execution
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum OutputContext {
26    /// Interactive TTY/REPL - use human-friendly format with colors.
27    #[default]
28    Interactive,
29    /// Output to another command - use raw output for pipes.
30    Piped,
31    /// MCP server / agent context - use token-efficient model format.
32    Model,
33    /// Non-interactive script - use raw output.
34    Script,
35}
36
37/// Execution context passed to tools.
38///
39/// Provides access to the backend (for file operations and tool dispatch),
40/// scope, and other kernel state.
41pub struct ExecContext {
42    /// Kernel backend for I/O operations.
43    ///
44    /// This is the preferred way to access filesystem operations.
45    /// Use `backend.read()`, `backend.write()`, etc.
46    pub backend: Arc<dyn KernelBackend>,
47    /// Variable scope.
48    pub scope: Scope,
49    /// Current working directory (VFS path).
50    pub cwd: PathBuf,
51    /// Previous working directory (for `cd -`).
52    pub prev_cwd: Option<PathBuf>,
53    /// Standard input for the tool (from pipeline).
54    pub stdin: Option<String>,
55    /// Structured data from pipeline (pre-parsed JSON from previous command).
56    /// Tools can check this before parsing stdin to avoid redundant JSON parsing.
57    pub stdin_data: Option<Value>,
58    /// Streaming pipe input (set when this command is in a concurrent pipeline).
59    pub pipe_stdin: Option<PipeReader>,
60    /// Streaming pipe output (set when this command is in a concurrent pipeline).
61    pub pipe_stdout: Option<PipeWriter>,
62    /// Tool schemas for help command.
63    pub tool_schemas: Vec<ToolSchema>,
64    /// Tool registry reference (for tools that need to inspect available tools).
65    pub tools: Option<Arc<ToolRegistry>>,
66    /// Job manager for background jobs (optional).
67    pub job_manager: Option<Arc<JobManager>>,
68    /// Kernel stderr stream for real-time error output from pipeline stages.
69    ///
70    /// When set, pipeline stages write stderr here instead of buffering in
71    /// `ExecResult.err`. This allows stderr from all stages to stream to
72    /// the terminal (or other sink) concurrently, matching bash behavior.
73    pub stderr: Option<StderrStream>,
74    /// Position of this command within a pipeline (for stdio decisions).
75    pub pipeline_position: PipelinePosition,
76    /// Whether we're running in interactive (REPL) mode.
77    pub interactive: bool,
78    /// Command aliases (name → expansion string).
79    pub aliases: HashMap<String, String>,
80    /// Terminal state for job control (interactive mode, Unix only).
81    #[cfg(unix)]
82    pub terminal_state: Option<std::sync::Arc<crate::terminal::TerminalState>>,
83}
84
85impl ExecContext {
86    /// Create a new execution context with a VFS (uses LocalBackend without tools).
87    ///
88    /// This constructor is for backward compatibility and tests that don't need tool dispatch.
89    /// For full tool support, use `with_vfs_and_tools`.
90    pub fn new(vfs: Arc<VfsRouter>) -> Self {
91        Self {
92            backend: Arc::new(LocalBackend::new(vfs)),
93            scope: Scope::new(),
94            cwd: PathBuf::from("/"),
95            prev_cwd: None,
96            stdin: None,
97            stdin_data: None,
98            pipe_stdin: None,
99            pipe_stdout: None,
100            stderr: None,
101            tool_schemas: Vec::new(),
102            tools: None,
103            job_manager: None,
104            pipeline_position: PipelinePosition::Only,
105            interactive: false,
106            aliases: HashMap::new(),
107            #[cfg(unix)]
108            terminal_state: None,
109        }
110    }
111
112    /// Create a new execution context with VFS and tool registry.
113    ///
114    /// This is the preferred constructor for full kaish operation where
115    /// tools need to be dispatched through the backend.
116    pub fn with_vfs_and_tools(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>) -> Self {
117        Self {
118            backend: Arc::new(LocalBackend::with_tools(vfs, tools.clone())),
119            scope: Scope::new(),
120            cwd: PathBuf::from("/"),
121            prev_cwd: None,
122            stdin: None,
123            stdin_data: None,
124            pipe_stdin: None,
125            pipe_stdout: None,
126            stderr: None,
127            tool_schemas: Vec::new(),
128            tools: Some(tools),
129            job_manager: None,
130            pipeline_position: PipelinePosition::Only,
131            interactive: false,
132            aliases: HashMap::new(),
133            #[cfg(unix)]
134            terminal_state: None,
135        }
136    }
137
138    /// Create a new execution context with a custom backend.
139    pub fn with_backend(backend: Arc<dyn KernelBackend>) -> Self {
140        Self {
141            backend,
142            scope: Scope::new(),
143            cwd: PathBuf::from("/"),
144            prev_cwd: None,
145            stdin: None,
146            stdin_data: None,
147            pipe_stdin: None,
148            pipe_stdout: None,
149            stderr: None,
150            tool_schemas: Vec::new(),
151            tools: None,
152            job_manager: None,
153            pipeline_position: PipelinePosition::Only,
154            interactive: false,
155            aliases: HashMap::new(),
156            #[cfg(unix)]
157            terminal_state: None,
158        }
159    }
160
161    /// Create a context with VFS, tools, and a specific scope.
162    pub fn with_vfs_tools_and_scope(vfs: Arc<VfsRouter>, tools: Arc<ToolRegistry>, scope: Scope) -> Self {
163        Self {
164            backend: Arc::new(LocalBackend::with_tools(vfs, tools.clone())),
165            scope,
166            cwd: PathBuf::from("/"),
167            prev_cwd: None,
168            stdin: None,
169            stdin_data: None,
170            pipe_stdin: None,
171            pipe_stdout: None,
172            stderr: None,
173            tool_schemas: Vec::new(),
174            tools: Some(tools),
175            job_manager: None,
176            pipeline_position: PipelinePosition::Only,
177            interactive: false,
178            aliases: HashMap::new(),
179            #[cfg(unix)]
180            terminal_state: None,
181        }
182    }
183
184    /// Create a context with a specific scope (uses LocalBackend without tools).
185    ///
186    /// For tests that don't need tool dispatch. For full tool support,
187    /// use `with_vfs_tools_and_scope`.
188    pub fn with_scope(vfs: Arc<VfsRouter>, scope: Scope) -> Self {
189        Self {
190            backend: Arc::new(LocalBackend::new(vfs)),
191            scope,
192            cwd: PathBuf::from("/"),
193            prev_cwd: None,
194            stdin: None,
195            stdin_data: None,
196            pipe_stdin: None,
197            pipe_stdout: None,
198            stderr: None,
199            tool_schemas: Vec::new(),
200            tools: None,
201            job_manager: None,
202            pipeline_position: PipelinePosition::Only,
203            interactive: false,
204            aliases: HashMap::new(),
205            #[cfg(unix)]
206            terminal_state: None,
207        }
208    }
209
210    /// Create a context with a custom backend and scope.
211    pub fn with_backend_and_scope(backend: Arc<dyn KernelBackend>, scope: Scope) -> Self {
212        Self {
213            backend,
214            scope,
215            cwd: PathBuf::from("/"),
216            prev_cwd: None,
217            stdin: None,
218            stdin_data: None,
219            pipe_stdin: None,
220            pipe_stdout: None,
221            stderr: None,
222            tool_schemas: Vec::new(),
223            tools: None,
224            job_manager: None,
225            pipeline_position: PipelinePosition::Only,
226            interactive: false,
227            aliases: HashMap::new(),
228            #[cfg(unix)]
229            terminal_state: None,
230        }
231    }
232
233    /// Set the available tool schemas (for help command).
234    pub fn set_tool_schemas(&mut self, schemas: Vec<ToolSchema>) {
235        self.tool_schemas = schemas;
236    }
237
238    /// Set the tool registry reference.
239    pub fn set_tools(&mut self, tools: Arc<ToolRegistry>) {
240        self.tools = Some(tools);
241    }
242
243    /// Set the job manager for background job tracking.
244    pub fn set_job_manager(&mut self, manager: Arc<JobManager>) {
245        self.job_manager = Some(manager);
246    }
247
248    /// Set stdin for this execution.
249    pub fn set_stdin(&mut self, stdin: String) {
250        self.stdin = Some(stdin);
251    }
252
253    /// Get stdin, consuming it.
254    pub fn take_stdin(&mut self) -> Option<String> {
255        self.stdin.take()
256    }
257
258    /// Set both text stdin and structured data.
259    ///
260    /// Use this when passing output through a pipeline where the previous
261    /// command produced structured data (e.g., JSON from MCP tools).
262    pub fn set_stdin_with_data(&mut self, text: String, data: Option<Value>) {
263        self.stdin = Some(text);
264        self.stdin_data = data;
265    }
266
267    /// Take structured data if available, consuming it.
268    ///
269    /// Tools can use this to avoid re-parsing JSON that was already parsed
270    /// by a previous command in the pipeline.
271    pub fn take_stdin_data(&mut self) -> Option<Value> {
272        self.stdin_data.take()
273    }
274
275    /// Resolve a path relative to cwd, normalizing `.` and `..` components.
276    pub fn resolve_path(&self, path: &str) -> PathBuf {
277        let raw = if path.starts_with('/') {
278            PathBuf::from(path)
279        } else {
280            self.cwd.join(path)
281        };
282        normalize_path(&raw)
283    }
284
285    /// Change the current working directory.
286    ///
287    /// Saves the old directory for `cd -` support.
288    pub fn set_cwd(&mut self, path: PathBuf) {
289        self.prev_cwd = Some(self.cwd.clone());
290        self.cwd = path;
291    }
292
293    /// Get the previous working directory (for `cd -`).
294    pub fn get_prev_cwd(&self) -> Option<&PathBuf> {
295        self.prev_cwd.as_ref()
296    }
297
298    /// Read all stdin (pipe or buffered string) into a String.
299    ///
300    /// Prefers pipe_stdin if set (streaming pipeline), otherwise falls back
301    /// to the buffered stdin string. Consumes the source.
302    pub async fn read_stdin_to_string(&mut self) -> Option<String> {
303        if let Some(mut reader) = self.pipe_stdin.take() {
304            use tokio::io::AsyncReadExt;
305            let mut buf = Vec::new();
306            reader.read_to_end(&mut buf).await.ok()?;
307            Some(String::from_utf8_lossy(&buf).into_owned())
308        } else {
309            self.stdin.take()
310        }
311    }
312
313    /// Create a child context for a pipeline stage.
314    ///
315    /// Shares backend, tools, job_manager, aliases, cwd, and scope
316    /// but has independent stdin/stdout pipes.
317    pub fn child_for_pipeline(&self) -> Self {
318        Self {
319            backend: self.backend.clone(),
320            scope: self.scope.clone(),
321            cwd: self.cwd.clone(),
322            prev_cwd: self.prev_cwd.clone(),
323            stdin: None,
324            stdin_data: None,
325            pipe_stdin: None,
326            pipe_stdout: None,
327            stderr: self.stderr.clone(),
328            tool_schemas: self.tool_schemas.clone(),
329            tools: self.tools.clone(),
330            job_manager: self.job_manager.clone(),
331            pipeline_position: PipelinePosition::Only,
332            interactive: self.interactive,
333            aliases: self.aliases.clone(),
334            #[cfg(unix)]
335            terminal_state: self.terminal_state.clone(),
336        }
337    }
338
339    /// Expand a glob pattern to matching file paths.
340    ///
341    /// Returns the matched paths (absolute). Used by builtins that accept glob
342    /// patterns in their path arguments (ls, cat, head, tail, wc, etc.).
343    pub async fn expand_glob(&self, pattern: &str) -> Result<Vec<PathBuf>, String> {
344        use crate::backend_walker_fs::BackendWalkerFs;
345        use crate::walker::{EntryTypes, FileWalker, GlobPath, WalkOptions};
346
347        let glob = GlobPath::new(pattern).map_err(|e| format!("invalid pattern: {}", e))?;
348
349        let root = if glob.is_anchored() {
350            self.resolve_path("/")
351        } else {
352            self.resolve_path(".")
353        };
354
355        let options = WalkOptions {
356            entry_types: EntryTypes::all(),
357            ..WalkOptions::default()
358        };
359
360        let fs = BackendWalkerFs(self.backend.as_ref());
361        let walker = FileWalker::new(&fs, &root)
362            .with_pattern(glob)
363            .with_options(options);
364
365        walker.collect().await.map_err(|e| e.to_string())
366    }
367}
368
369/// Normalize a path by resolving `.` and `..` components lexically (no filesystem access).
370fn normalize_path(path: &std::path::Path) -> PathBuf {
371    let mut parts: Vec<Component> = Vec::new();
372    for component in path.components() {
373        match component {
374            Component::CurDir => {} // skip `.`
375            Component::ParentDir => {
376                // Pop the last normal component, but don't pop past root
377                if let Some(Component::Normal(_)) = parts.last() {
378                    parts.pop();
379                } else {
380                    parts.push(component);
381                }
382            }
383            _ => parts.push(component),
384        }
385    }
386    if parts.is_empty() {
387        PathBuf::from("/")
388    } else {
389        parts.iter().collect()
390    }
391}