theway_core/executor.rs
1//! Execution-environment abstraction.
2//!
3//! Tool execution is decoupled from the agent runtime: tools are defined against the
4//! [`ToolExecutor`] interface, so the same harness, session, snapshot and command
5//! surfaces run with a **local** executor (local editing mode, the default) or a
6//! **remote sandbox** executor without client-visible changes. The trait lives in
7//! `theway-core` so tool definitions compile against it directly and wasm/embedded
8//! consumers can provide their own executors; the daemon kernel
9//! (`theway-daemon`) supplies the reference `LocalExecutor` and the sandbox stub.
10//!
11//! The trait is a *seam*, not an implementation: core defines no local fs/process
12//! behavior here. All methods are async and the trait is object-safe with
13//! `Send + Sync` bounds, so executors can be shared as `Arc<dyn ToolExecutor>`.
14
15use std::path::Path;
16use std::time::Duration;
17
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20use strum::Display;
21
22/// Which execution environment a [`ToolExecutor`] dispatches tool calls to.
23///
24/// Callers (daemons, tests) branch on this to distinguish local editing mode from
25/// remote-sandbox execution; tool *definitions* never depend on it.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display)]
27#[serde(rename_all = "snake_case")]
28#[strum(serialize_all = "snake_case")]
29pub enum ExecutorKind {
30 /// Local filesystem and process table (the default editing mode).
31 Local,
32 /// Remote sandbox environment (stub until a real backend such as e2b lands).
33 Sandbox,
34}
35
36/// Captured result of a command executed through [`ToolExecutor::run_command`]
37/// or [`ToolExecutor::git`].
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct CommandOutput {
40 /// Decoded standard output of the process.
41 pub stdout: String,
42 /// Decoded standard error of the process.
43 pub stderr: String,
44 /// Process exit code (`0` conventionally means success).
45 pub exit_code: i32,
46}
47
48impl CommandOutput {
49 /// `true` when the process exited with code `0`.
50 pub fn success(&self) -> bool {
51 self.exit_code == 0
52 }
53}
54
55/// Errors surfaced by [`ToolExecutor`] implementations.
56#[derive(Debug, thiserror::Error)]
57pub enum ExecutorError {
58 /// The executor kind does not support the requested operation — e.g. any call
59 /// routed to the sandbox stub before a real sandbox backend exists. Always a
60 /// prompt, explicit failure (never a hang).
61 #[error("unsupported executor kind: {0}")]
62 UnsupportedKind(ExecutorKind),
63 /// Any other executor-side failure (I/O, process spawn, timeout) reported with a
64 /// human-readable message.
65 #[error("executor error: {0}")]
66 Other(String),
67}
68
69/// Convenience result alias for [`ToolExecutor`] methods.
70pub type Result<T, E = ExecutorError> = std::result::Result<T, E>;
71
72/// Execution environment that tools dispatch their effects through.
73///
74/// Implementations: `LocalExecutor` (local filesystem + process table) and the
75/// sandbox stub (fails with [`ExecutorError::UnsupportedKind`]) in the daemon
76/// kernel (`theway-daemon`); tests and embedded consumers may provide their own.
77#[async_trait]
78pub trait ToolExecutor: Send + Sync {
79 /// Reports which execution environment this executor dispatches to, so callers
80 /// can distinguish local from sandbox execution.
81 async fn kind(&self) -> ExecutorKind;
82
83 /// Reads a file's content as UTF-8 text.
84 async fn read_file(&self, path: &Path) -> Result<String>;
85
86 /// Writes `content` to `path` (creating or truncating the file).
87 async fn write_file(&self, path: &Path, content: &str) -> Result<()>;
88
89 /// Runs a command with working directory `cwd`, argument vector `argv` and a
90 /// wall-clock `timeout`; returns the captured output.
91 async fn run_command(
92 &self,
93 cwd: &Path,
94 argv: &[String],
95 timeout: Duration,
96 ) -> Result<CommandOutput>;
97
98 /// Lists directory entries at `path`, returning entry names.
99 async fn list_dir(&self, path: &Path) -> Result<Vec<String>>;
100
101 /// Searches for regex `pattern` under `path`, returning matching lines.
102 async fn grep(&self, pattern: &str, path: &Path) -> Result<Vec<String>>;
103
104 /// Finds files matching `glob` under `path`, returning matching paths.
105 async fn find(&self, glob: &str, path: &Path) -> Result<Vec<String>>;
106
107 /// Runs a git invocation with `args` in the repository context of the executor.
108 async fn git(&self, args: &[String]) -> Result<CommandOutput>;
109}
110
111#[cfg(test)]
112tests_bridge_macro::tests_bridge!("executor");