Skip to main content

kaish_kernel/
dispatch.rs

1//! Command dispatch — the single execution path for all commands.
2//!
3//! The `CommandDispatcher` trait defines how a single command is resolved and
4//! executed. The Kernel implements this trait with the full dispatch chain:
5//! user tools → builtins → .kai scripts → external commands → backend tools.
6//!
7//! `PipelineRunner` calls `dispatcher.dispatch()` for each command in a
8//! pipeline, handling I/O routing (stdin piping, redirects) around each call.
9//!
10//! ```text
11//! Stmt::Command ──┐
12//!                  ├──▶ execute_pipeline() ──▶ PipelineRunner::run(dispatcher, commands, ctx)
13//! Stmt::Pipeline ──┘                                  │
14//!                                               for each command:
15//!                                                 dispatcher.dispatch(cmd, ctx)
16//!                                                     │
17//!                                               ┌─────┼──────────────┐
18//!                                               │     │              │
19//!                                          user_tools builtins  .kai scripts
20//!                                                                external cmds
21//!                                                                backend tools
22//! ```
23
24use std::sync::Arc;
25
26use anyhow::Result;
27use async_trait::async_trait;
28
29use crate::ast::{Command, Value};
30use crate::backend::BackendError;
31use crate::interpreter::{apply_output_format, ExecResult};
32use crate::scheduler::build_tool_args;
33use crate::tools::{extract_output_format, ExecContext, ToolRegistry};
34
35/// Position of a command within a pipeline.
36///
37/// Used by external command execution to decide stdio inheritance:
38/// - `Only` or `Last` in interactive mode → inherit terminal
39/// - `First` or `Middle` → always capture
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum PipelinePosition {
42    /// Single command, no pipe.
43    #[default]
44    Only,
45    /// First command in a pipeline (no stdin from pipe).
46    First,
47    /// Middle of a pipeline (piped stdin, piped stdout).
48    Middle,
49    /// Last command in a pipeline (piped stdin, final output).
50    Last,
51}
52
53/// Trait for dispatching a single command through the full resolution chain.
54///
55/// Implementations handle argument parsing, tool lookup, and execution.
56/// The pipeline runner handles I/O routing (stdin, redirects, piping).
57#[async_trait]
58pub trait CommandDispatcher: Send + Sync {
59    /// Dispatch a single command for execution.
60    ///
61    /// The `ctx` provides stdin (from pipe or redirect), scope, and backend.
62    /// Implementations should handle schema-aware argument parsing and
63    /// output format extraction internally.
64    async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult>;
65}
66
67/// Fallback dispatcher that routes through `backend.call_tool()`.
68///
69/// This provides the same behavior as the old `PipelineRunner` — it dispatches
70/// to builtins via the backend's tool registry. Used for background jobs and
71/// scatter/gather workers until full `Arc<Kernel>` dispatch is wired up.
72///
73/// Limitations compared to the Kernel dispatcher:
74/// - No user-defined tools
75/// - No .kai script resolution
76/// - No external command execution
77/// - No async argument evaluation (command substitution in args won't work)
78pub struct BackendDispatcher {
79    tools: Arc<ToolRegistry>,
80}
81
82impl BackendDispatcher {
83    /// Create a new backend dispatcher with the given tool registry.
84    pub fn new(tools: Arc<ToolRegistry>) -> Self {
85        Self { tools }
86    }
87}
88
89#[async_trait]
90impl CommandDispatcher for BackendDispatcher {
91    async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
92        // Handle built-in true/false
93        match cmd.name.as_str() {
94            "true" => return Ok(ExecResult::success("")),
95            "false" => return Ok(ExecResult::failure(1, "")),
96            _ => {}
97        }
98
99        // Build tool args with schema-aware parsing (sync — no command substitution)
100        let schema = self.tools.get(&cmd.name).map(|t| t.schema());
101        let mut tool_args = build_tool_args(&cmd.args, ctx, schema.as_ref());
102        let output_format = extract_output_format(&mut tool_args, schema.as_ref());
103
104        // Execute via backend
105        let backend = ctx.backend.clone();
106        let result = match backend.call_tool(&cmd.name, tool_args, ctx).await {
107            Ok(tool_result) => {
108                let mut exec = ExecResult::from_output(
109                    tool_result.code as i64,
110                    tool_result.stdout,
111                    tool_result.stderr,
112                );
113                exec.output = tool_result.output;
114                // Restore structured data from ToolResult (preserved through backend roundtrip)
115                if let Some(json_data) = tool_result.data {
116                    exec.data = Some(Value::Json(json_data));
117                }
118                exec
119            }
120            Err(BackendError::ToolNotFound(_)) => {
121                ExecResult::failure(127, format!("command not found: {}", cmd.name))
122            }
123            Err(e) => ExecResult::failure(127, e.to_string()),
124        };
125
126        // Apply output format transform
127        let result = match output_format {
128            Some(format) => apply_output_format(result, format),
129            None => result,
130        };
131
132        Ok(result)
133    }
134}