workdir: separate identity from worker session
This commit is contained in:
+11
-11
@@ -5,7 +5,7 @@ use async_trait::async_trait;
|
||||
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use workdir::{CommandHandle, CommandOutputRequest, CommandRequest, WorkdirHandle};
|
||||
use workdir::{CommandHandle, CommandOutputRequest, CommandRequest, WorkdirSessionHandle};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 120;
|
||||
const MAX_TIMEOUT_SECS: u64 = 600;
|
||||
@@ -19,18 +19,18 @@ struct BashParams {
|
||||
}
|
||||
|
||||
pub(crate) struct BashTool {
|
||||
workdir: WorkdirHandle,
|
||||
session: WorkdirSessionHandle,
|
||||
}
|
||||
|
||||
struct CommandGuard {
|
||||
workdir: WorkdirHandle,
|
||||
session: WorkdirSessionHandle,
|
||||
handle: Option<CommandHandle>,
|
||||
}
|
||||
|
||||
impl Drop for CommandGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
let workdir = self.workdir.clone();
|
||||
let workdir = self.session.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = workdir.cancel_command(handle).await;
|
||||
});
|
||||
@@ -53,7 +53,7 @@ impl Tool for BashTool {
|
||||
.clamp(1, MAX_TIMEOUT_SECS);
|
||||
let cmd_summary = truncate_for_summary(¶ms.command);
|
||||
let handle = self
|
||||
.workdir
|
||||
.session
|
||||
.start_command(CommandRequest {
|
||||
command: params.command,
|
||||
timeout_secs,
|
||||
@@ -62,11 +62,11 @@ impl Tool for BashTool {
|
||||
.await
|
||||
.map_err(crate::ToolsError::from)?;
|
||||
let mut guard = CommandGuard {
|
||||
workdir: self.workdir.clone(),
|
||||
session: self.session.clone(),
|
||||
handle: Some(handle.clone()),
|
||||
};
|
||||
let output = self
|
||||
.workdir
|
||||
.session
|
||||
.command_output(CommandOutputRequest {
|
||||
handle,
|
||||
cursor: 0,
|
||||
@@ -90,7 +90,7 @@ impl Tool for BashTool {
|
||||
None
|
||||
} else if output.truncated {
|
||||
Some(format!(
|
||||
"[showing bounded Workdir command output; additional output was truncated]\n{}",
|
||||
"[showing bounded WorkdirSession command output; additional output was truncated]\n{}",
|
||||
output.content
|
||||
))
|
||||
} else {
|
||||
@@ -110,14 +110,14 @@ fn truncate_for_summary(command: &str) -> String {
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn bash_tool(workdir: WorkdirHandle, _output_dir: PathBuf) -> ToolDefinition {
|
||||
pub fn bash_tool(session: WorkdirSessionHandle, _output_dir: PathBuf) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(BashParams);
|
||||
let meta = ToolMeta::new("Bash")
|
||||
.description("Execute a shell command in the bound Workdir. Process start, bounded output, timeout and cancellation are owned by the Workdir provider. This is not a sandbox.")
|
||||
.description("Execute a shell command in the bound Workdir. Process start, bounded output, timeout and cancellation are owned by the WorkdirSession provider. This is not a sandbox.")
|
||||
.input_schema(serde_json::to_value(schema).expect("Bash schema serialization"));
|
||||
let tool: Arc<dyn Tool> = Arc::new(BashTool {
|
||||
workdir: workdir.clone(),
|
||||
session: session.clone(),
|
||||
});
|
||||
(meta, tool)
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde::Deserialize;
|
||||
|
||||
use crate::error::ToolsError;
|
||||
use crate::tracker::Tracker;
|
||||
use workdir::{EditRequest, WorkdirHandle, WorkdirPath};
|
||||
use workdir::{EditRequest, WorkdirPath, WorkdirSessionHandle};
|
||||
|
||||
const DESCRIPTION: &str = "Replace a substring in an existing file. By default \
|
||||
`old_string` must be unique in the file; set `replace_all: true` to replace \
|
||||
@@ -30,7 +30,7 @@ pub(crate) struct EditParams {
|
||||
}
|
||||
|
||||
pub(crate) struct EditTool {
|
||||
workdir: WorkdirHandle,
|
||||
session: WorkdirSessionHandle,
|
||||
tracker: Tracker,
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ impl Tool for EditTool {
|
||||
let _mutation_permit = self.tracker.acquire_mutation(&mutation_key, &ctx).await;
|
||||
let expected_hash = self.tracker.expected_workdir_hash(&path)?;
|
||||
let result = self
|
||||
.workdir
|
||||
.session
|
||||
.edit(EditRequest {
|
||||
path: path.clone(),
|
||||
old_string: params.old_string.clone(),
|
||||
@@ -115,7 +115,7 @@ fn make_preview(text: &str, needle: &str) -> String {
|
||||
}
|
||||
|
||||
/// Factory for the `Edit` tool.
|
||||
pub fn edit_tool(workdir: WorkdirHandle, tracker: Tracker) -> ToolDefinition {
|
||||
pub fn edit_tool(session: WorkdirSessionHandle, tracker: Tracker) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(EditParams);
|
||||
let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
|
||||
@@ -123,7 +123,7 @@ pub fn edit_tool(workdir: WorkdirHandle, tracker: Tracker) -> ToolDefinition {
|
||||
.description(DESCRIPTION)
|
||||
.input_schema(schema_value);
|
||||
let tool: Arc<dyn Tool> = Arc::new(EditTool {
|
||||
workdir: workdir.clone(),
|
||||
session: session.clone(),
|
||||
tracker: tracker.clone(),
|
||||
});
|
||||
(meta, tool)
|
||||
@@ -137,16 +137,16 @@ mod tests {
|
||||
use manifest::Scope;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn setup() -> (TempDir, WorkdirHandle, Tracker) {
|
||||
fn setup() -> (TempDir, WorkdirSessionHandle, Tracker) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let fs: WorkdirHandle = Arc::new(workdir::LocalWorkdir::new(
|
||||
let fs: WorkdirSessionHandle = Arc::new(workdir::LocalWorkdirSession::new(
|
||||
Scope::writable(dir.path()).unwrap(),
|
||||
dir.path().to_path_buf(),
|
||||
));
|
||||
(dir, fs, Tracker::new())
|
||||
}
|
||||
|
||||
async fn read_first(fs: &WorkdirHandle, tracker: &Tracker, file: &std::path::Path) {
|
||||
async fn read_first(fs: &WorkdirSessionHandle, tracker: &Tracker, file: &std::path::Path) {
|
||||
let def = read_tool(fs.clone(), tracker.clone());
|
||||
let (_, reader) = def();
|
||||
let inp = serde_json::json!({ "file_path": file.file_name().unwrap().to_str().unwrap() });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Error types for builtin tools.
|
||||
//!
|
||||
//! `ToolsError` keeps tool-specific policy failures separate from Workdir
|
||||
//! `ToolsError` keeps tool-specific policy failures separate from WorkdirSession
|
||||
//! operation failures. Filesystem, search, and command errors originate in
|
||||
//! `workdir` and remain transparent here.
|
||||
|
||||
@@ -11,7 +11,7 @@ use llm_engine::tool::ToolError;
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ToolsError {
|
||||
#[error(transparent)]
|
||||
Workdir(#[from] workdir::WorkdirError),
|
||||
WorkdirSession(#[from] workdir::WorkdirError),
|
||||
|
||||
#[error("file has not been read in this session; read it first: {}", .0.display())]
|
||||
NotRead(PathBuf),
|
||||
@@ -35,12 +35,12 @@ pub enum ToolsError {
|
||||
impl From<ToolsError> for ToolError {
|
||||
fn from(err: ToolsError) -> Self {
|
||||
match &err {
|
||||
ToolsError::Workdir(
|
||||
ToolsError::WorkdirSession(
|
||||
workdir::WorkdirError::NotFound(_)
|
||||
| workdir::WorkdirError::Io { .. }
|
||||
| workdir::WorkdirError::Unavailable(_),
|
||||
) => ToolError::ExecutionFailed(err.to_string()),
|
||||
ToolsError::Workdir(_)
|
||||
ToolsError::WorkdirSession(_)
|
||||
| ToolsError::NotRead(_)
|
||||
| ToolsError::ExternallyModified(_)
|
||||
| ToolsError::StringNotFound { .. }
|
||||
|
||||
@@ -4,7 +4,7 @@ use async_trait::async_trait;
|
||||
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use workdir::{GlobRequest, WorkdirHandle, WorkdirPath};
|
||||
use workdir::{GlobRequest, WorkdirPath, WorkdirSessionHandle};
|
||||
|
||||
use crate::ToolsError;
|
||||
|
||||
@@ -20,7 +20,7 @@ struct GlobParams {
|
||||
}
|
||||
|
||||
struct GlobTool {
|
||||
workdir: WorkdirHandle,
|
||||
session: WorkdirSessionHandle,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -39,7 +39,7 @@ impl Tool for GlobTool {
|
||||
let pattern = params.pattern;
|
||||
tracing::debug!(%pattern, %path, "Glob");
|
||||
let result = self
|
||||
.workdir
|
||||
.session
|
||||
.glob(GlobRequest {
|
||||
pattern: pattern.clone(),
|
||||
path,
|
||||
@@ -73,14 +73,14 @@ impl Tool for GlobTool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn glob_tool(workdir: WorkdirHandle) -> ToolDefinition {
|
||||
pub fn glob_tool(session: WorkdirSessionHandle) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(GlobParams);
|
||||
let meta = ToolMeta::new("Glob")
|
||||
.description("Find files matching a glob pattern inside the bound Workdir. Results are sorted and capped at 1000 entries. Paths are Workdir-relative.")
|
||||
.input_schema(serde_json::to_value(schema).expect("Glob schema serialization"));
|
||||
let tool: Arc<dyn Tool> = Arc::new(GlobTool {
|
||||
workdir: workdir.clone(),
|
||||
session: session.clone(),
|
||||
});
|
||||
(meta, tool)
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ use async_trait::async_trait;
|
||||
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use workdir::{GrepOutputMode, GrepRequest, WorkdirHandle, WorkdirPath};
|
||||
use workdir::{GrepOutputMode, GrepRequest, WorkdirPath, WorkdirSessionHandle};
|
||||
|
||||
use crate::ToolsError;
|
||||
|
||||
@@ -48,7 +48,7 @@ struct GrepParams {
|
||||
}
|
||||
|
||||
struct GrepTool {
|
||||
workdir: WorkdirHandle,
|
||||
session: WorkdirSessionHandle,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -75,7 +75,7 @@ impl Tool for GrepTool {
|
||||
.unwrap_or((params.before.unwrap_or(0), params.after.unwrap_or(0)));
|
||||
let head_limit = params.head_limit.unwrap_or(DEFAULT_HEAD_LIMIT);
|
||||
let result = self
|
||||
.workdir
|
||||
.session
|
||||
.grep(GrepRequest {
|
||||
pattern: params.pattern,
|
||||
path,
|
||||
@@ -124,14 +124,14 @@ impl Tool for GrepTool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grep_tool(workdir: WorkdirHandle) -> ToolDefinition {
|
||||
pub fn grep_tool(session: WorkdirSessionHandle) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(GrepParams);
|
||||
let meta = ToolMeta::new("Grep")
|
||||
.description("Search Workdir file contents with a regex. Glob/Grep traversal executes inside the Workdir provider. Results are bounded and Workdir-relative.")
|
||||
.description("Search Workdir file contents with a regex. Glob/Grep traversal executes inside the WorkdirSession provider. Results are bounded and Workdir-relative.")
|
||||
.input_schema(serde_json::to_value(schema).expect("Grep schema serialization"));
|
||||
let tool: Arc<dyn Tool> = Arc::new(GrepTool {
|
||||
workdir: workdir.clone(),
|
||||
session: session.clone(),
|
||||
});
|
||||
(meta, tool)
|
||||
})
|
||||
|
||||
+24
-23
@@ -1,10 +1,11 @@
|
||||
//! Built-in tools for the Yoi LLM agent.
|
||||
//!
|
||||
//! Read / Write / Edit / Glob / Grep / Bash operate through a host-owned
|
||||
//! [`workdir::Workdir`] handle. This crate owns tool schemas, rendering, and
|
||||
//! read-before-edit tracking; it does not own Workdir identity or lifecycle.
|
||||
//! [`workdir::WorkdirSession`] handle. This crate owns tool schemas, rendering, and
|
||||
//! read-before-edit tracking; it does not own Workdir identity/materialization or
|
||||
//! WorkdirSession lifecycle.
|
||||
//!
|
||||
//! Bash is intentionally not sandboxed. The Workdir supplies its initial cwd
|
||||
//! Bash is intentionally not sandboxed. The WorkdirSession supplies its initial cwd
|
||||
//! and command capability, while the Runtime process and OS user remain the
|
||||
//! trusted execution boundary.
|
||||
|
||||
@@ -29,34 +30,34 @@ pub use tracker::Tracker;
|
||||
pub use web::{web_fetch_tool, web_search_tool};
|
||||
pub use write::write_tool;
|
||||
|
||||
/// Build the local filesystem/command tool surface implemented by a Workdir.
|
||||
/// Build the local filesystem/command tool surface implemented by a WorkdirSession.
|
||||
/// Profile/manifest policy may narrow this set further in the Engine.
|
||||
pub fn core_builtin_tools(
|
||||
workdir: workdir::WorkdirHandle,
|
||||
session: workdir::WorkdirSessionHandle,
|
||||
tracker: Tracker,
|
||||
bash_output_dir: std::path::PathBuf,
|
||||
) -> Vec<llm_engine::tool::ToolDefinition> {
|
||||
use workdir::WorkdirCapability;
|
||||
use workdir::WorkdirSessionCapability;
|
||||
|
||||
let capabilities = workdir.capabilities();
|
||||
let capabilities = session.capabilities();
|
||||
let mut tools = Vec::with_capacity(6);
|
||||
if capabilities.supports(WorkdirCapability::Read) {
|
||||
tools.push(read_tool(workdir.clone(), tracker.clone()));
|
||||
if capabilities.supports(WorkdirSessionCapability::Read) {
|
||||
tools.push(read_tool(session.clone(), tracker.clone()));
|
||||
}
|
||||
if capabilities.supports(WorkdirCapability::Write) {
|
||||
tools.push(write_tool(workdir.clone(), tracker.clone()));
|
||||
if capabilities.supports(WorkdirSessionCapability::Write) {
|
||||
tools.push(write_tool(session.clone(), tracker.clone()));
|
||||
}
|
||||
if capabilities.supports(WorkdirCapability::Edit) {
|
||||
tools.push(edit_tool(workdir.clone(), tracker));
|
||||
if capabilities.supports(WorkdirSessionCapability::Edit) {
|
||||
tools.push(edit_tool(session.clone(), tracker));
|
||||
}
|
||||
if capabilities.supports(WorkdirCapability::Glob) {
|
||||
tools.push(glob_tool(workdir.clone()));
|
||||
if capabilities.supports(WorkdirSessionCapability::Glob) {
|
||||
tools.push(glob_tool(session.clone()));
|
||||
}
|
||||
if capabilities.supports(WorkdirCapability::Grep) {
|
||||
tools.push(grep_tool(workdir.clone()));
|
||||
if capabilities.supports(WorkdirSessionCapability::Grep) {
|
||||
tools.push(grep_tool(session.clone()));
|
||||
}
|
||||
if capabilities.supports(WorkdirCapability::Command) {
|
||||
tools.push(bash_tool(workdir, bash_output_dir));
|
||||
if capabilities.supports(WorkdirSessionCapability::Command) {
|
||||
tools.push(bash_tool(session, bash_output_dir));
|
||||
}
|
||||
tools
|
||||
}
|
||||
@@ -76,18 +77,18 @@ mod workdir_tool_tests {
|
||||
use manifest::{Scope, SharedScope};
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use workdir::{LocalWorkdir, WorkdirCapabilities, WorkdirHandle};
|
||||
use workdir::{LocalWorkdirSession, WorkdirSessionCapabilities, WorkdirSessionHandle};
|
||||
|
||||
#[test]
|
||||
fn read_only_workdir_exposes_only_observation_tools() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let workdir: WorkdirHandle = Arc::new(LocalWorkdir::materialized(
|
||||
let session: WorkdirSessionHandle = Arc::new(LocalWorkdirSession::materialized(
|
||||
dir.path().to_path_buf(),
|
||||
dir.path().to_path_buf(),
|
||||
SharedScope::new(Scope::writable(dir.path()).unwrap()),
|
||||
WorkdirCapabilities::READ_ONLY,
|
||||
WorkdirSessionCapabilities::READ_ONLY,
|
||||
));
|
||||
let names = core_builtin_tools(workdir, Tracker::new(), dir.path().join("output"))
|
||||
let names = core_builtin_tools(session, Tracker::new(), dir.path().join("output"))
|
||||
.into_iter()
|
||||
.map(|definition| definition().0.name)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -8,7 +8,7 @@ use serde::Deserialize;
|
||||
|
||||
use crate::error::ToolsError;
|
||||
use crate::tracker::Tracker;
|
||||
use workdir::{ReadRequest, WorkdirHandle, WorkdirPath};
|
||||
use workdir::{ReadRequest, WorkdirPath, WorkdirSessionHandle};
|
||||
|
||||
const DESCRIPTION: &str = "Read a text file from the local filesystem. \
|
||||
Supports offset/limit for large files. Returns line-numbered output (1-based). \
|
||||
@@ -31,7 +31,7 @@ pub(crate) struct ReadParams {
|
||||
}
|
||||
|
||||
pub(crate) struct ReadTool {
|
||||
workdir: WorkdirHandle,
|
||||
session: WorkdirSessionHandle,
|
||||
tracker: Tracker,
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ impl Tool for ReadTool {
|
||||
tracing::debug!(path = %path, offset, limit, "Read");
|
||||
|
||||
let result = self
|
||||
.workdir
|
||||
.session
|
||||
.read(ReadRequest {
|
||||
path: path.clone(),
|
||||
offset,
|
||||
@@ -144,7 +144,7 @@ fn render_numbered(text: &str, offset: usize, limit: usize) -> Rendered {
|
||||
}
|
||||
|
||||
/// Factory for the `Read` tool.
|
||||
pub fn read_tool(workdir: WorkdirHandle, tracker: Tracker) -> ToolDefinition {
|
||||
pub fn read_tool(session: WorkdirSessionHandle, tracker: Tracker) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(ReadParams);
|
||||
let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
|
||||
@@ -152,7 +152,7 @@ pub fn read_tool(workdir: WorkdirHandle, tracker: Tracker) -> ToolDefinition {
|
||||
.description(DESCRIPTION)
|
||||
.input_schema(schema_value);
|
||||
let tool: Arc<dyn Tool> = Arc::new(ReadTool {
|
||||
workdir: workdir.clone(),
|
||||
session: session.clone(),
|
||||
tracker: tracker.clone(),
|
||||
});
|
||||
(meta, tool)
|
||||
@@ -164,15 +164,15 @@ mod tests {
|
||||
use super::*;
|
||||
use manifest::Scope;
|
||||
use tempfile::TempDir;
|
||||
use workdir::LocalWorkdir;
|
||||
use workdir::LocalWorkdirSession;
|
||||
|
||||
fn setup() -> (TempDir, WorkdirHandle, Tracker) {
|
||||
fn setup() -> (TempDir, WorkdirSessionHandle, Tracker) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let workdir: WorkdirHandle = Arc::new(LocalWorkdir::new(
|
||||
let session: WorkdirSessionHandle = Arc::new(LocalWorkdirSession::new(
|
||||
Scope::writable(dir.path()).unwrap(),
|
||||
dir.path().to_path_buf(),
|
||||
));
|
||||
(dir, workdir, Tracker::new())
|
||||
(dir, session, Tracker::new())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
//! A `Tracker` is **Worker-process scoped**: the Worker layer creates a fresh
|
||||
//! instance at the start of each Worker run (including resume) and discards
|
||||
//! it when the process exits — it is not persisted, so a resumed
|
||||
//! conversation starts with an empty read/edit history. The local Workdir
|
||||
//! conversation starts with an empty read/edit history. The local WorkdirSession
|
||||
//! scope boundary is likewise Worker-process scoped (derived from the
|
||||
//! manifest). The two are orthogonal and the Worker wires them together
|
||||
//! when registering builtin tools.
|
||||
@@ -31,15 +31,15 @@
|
||||
//! # use std::sync::Arc;
|
||||
//! # use manifest::Scope;
|
||||
//! # use tools::{Tracker, core_builtin_tools};
|
||||
//! # use workdir::{LocalWorkdir, WorkdirHandle};
|
||||
//! # use workdir::{LocalWorkdirSession, WorkdirSessionHandle};
|
||||
//! let scope = Scope::writable("/workspace").unwrap();
|
||||
//! let workdir: WorkdirHandle = Arc::new(LocalWorkdir::new(
|
||||
//! let session: WorkdirSessionHandle = Arc::new(LocalWorkdirSession::new(
|
||||
//! scope,
|
||||
//! PathBuf::from("/workspace"),
|
||||
//! ));
|
||||
//! let tracker = Tracker::new(); // session lifetime
|
||||
//! let bash_outputs = PathBuf::from("/run/yoi/bash-output");
|
||||
//! let defs = core_builtin_tools(workdir, tracker, bash_outputs);
|
||||
//! let defs = core_builtin_tools(session, tracker, bash_outputs);
|
||||
//! ```
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
+10
-10
@@ -9,7 +9,7 @@ use serde::Deserialize;
|
||||
|
||||
use crate::error::ToolsError;
|
||||
use crate::tracker::Tracker;
|
||||
use workdir::{StatRequest, WorkdirError, WorkdirHandle, WorkdirPath, WriteRequest};
|
||||
use workdir::{StatRequest, WorkdirError, WorkdirPath, WorkdirSessionHandle, WriteRequest};
|
||||
|
||||
const DESCRIPTION: &str = "Create a new file or overwrite an existing one with \
|
||||
the given content. Missing parent directories within scope are created \
|
||||
@@ -25,7 +25,7 @@ pub(crate) struct WriteParams {
|
||||
}
|
||||
|
||||
pub(crate) struct WriteTool {
|
||||
workdir: WorkdirHandle,
|
||||
session: WorkdirSessionHandle,
|
||||
tracker: Tracker,
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@ impl Tool for WriteTool {
|
||||
|
||||
let mutation_key = PathBuf::from(path.as_str());
|
||||
let _mutation_permit = self.tracker.acquire_mutation(&mutation_key, &ctx).await;
|
||||
let expected_hash = match self.workdir.stat(StatRequest { path: path.clone() }).await {
|
||||
let expected_hash = match self.session.stat(StatRequest { path: path.clone() }).await {
|
||||
Ok(_) => Some(self.tracker.expected_workdir_hash(&path)?),
|
||||
Err(WorkdirError::NotFound(_)) => None,
|
||||
Err(error) => return Err(ToolsError::from(error).into()),
|
||||
};
|
||||
|
||||
let outcome = self
|
||||
.workdir
|
||||
.session
|
||||
.write(WriteRequest {
|
||||
path: path.clone(),
|
||||
content: params.content.as_bytes().to_vec(),
|
||||
@@ -81,7 +81,7 @@ impl Tool for WriteTool {
|
||||
}
|
||||
|
||||
/// Factory for the `Write` tool.
|
||||
pub fn write_tool(workdir: WorkdirHandle, tracker: Tracker) -> ToolDefinition {
|
||||
pub fn write_tool(session: WorkdirSessionHandle, tracker: Tracker) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(WriteParams);
|
||||
let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
|
||||
@@ -89,7 +89,7 @@ pub fn write_tool(workdir: WorkdirHandle, tracker: Tracker) -> ToolDefinition {
|
||||
.description(DESCRIPTION)
|
||||
.input_schema(schema_value);
|
||||
let tool: Arc<dyn Tool> = Arc::new(WriteTool {
|
||||
workdir: workdir.clone(),
|
||||
session: session.clone(),
|
||||
tracker: tracker.clone(),
|
||||
});
|
||||
(meta, tool)
|
||||
@@ -102,15 +102,15 @@ mod tests {
|
||||
use crate::read::read_tool;
|
||||
use manifest::Scope;
|
||||
use tempfile::TempDir;
|
||||
use workdir::LocalWorkdir;
|
||||
use workdir::LocalWorkdirSession;
|
||||
|
||||
fn setup() -> (TempDir, WorkdirHandle, Tracker) {
|
||||
fn setup() -> (TempDir, WorkdirSessionHandle, Tracker) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let workdir: WorkdirHandle = Arc::new(LocalWorkdir::new(
|
||||
let session: WorkdirSessionHandle = Arc::new(LocalWorkdirSession::new(
|
||||
Scope::writable(dir.path()).unwrap(),
|
||||
dir.path().to_path_buf(),
|
||||
));
|
||||
(dir, workdir, Tracker::new())
|
||||
(dir, session, Tracker::new())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -7,7 +7,7 @@ use manifest::{Permission, Scope, ScopeConfig, ScopeRule};
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tools::{Tracker, core_builtin_tools};
|
||||
use workdir::{LocalWorkdir, WorkdirHandle};
|
||||
use workdir::{LocalWorkdirSession, WorkdirSessionHandle};
|
||||
|
||||
struct Registry {
|
||||
entries: Vec<(llm_engine::tool::ToolMeta, Arc<dyn Tool>)>,
|
||||
@@ -42,7 +42,8 @@ fn setup() -> (TempDir, TempDir, Registry) {
|
||||
recursive: true,
|
||||
});
|
||||
let scope = Scope::from_config(&config).unwrap();
|
||||
let fs: WorkdirHandle = std::sync::Arc::new(LocalWorkdir::new(scope, dir.path().to_path_buf()));
|
||||
let fs: WorkdirSessionHandle =
|
||||
std::sync::Arc::new(LocalWorkdirSession::new(scope, dir.path().to_path_buf()));
|
||||
let tracker = Tracker::new();
|
||||
let reg = Registry::new(core_builtin_tools(fs, tracker, spill.path().to_path_buf()));
|
||||
(dir, spill, reg)
|
||||
|
||||
@@ -12,7 +12,7 @@ use manifest::{Permission, Scope, ScopeConfig, ScopeRule};
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tools::{Tracker, core_builtin_tools};
|
||||
use workdir::{LocalWorkdir, WorkdirHandle};
|
||||
use workdir::{LocalWorkdirSession, WorkdirSessionHandle};
|
||||
|
||||
fn scope_with_spill(workspace: &Path, spill: &Path) -> Scope {
|
||||
let base = Scope::writable(workspace).unwrap();
|
||||
@@ -55,7 +55,8 @@ fn setup() -> (TempDir, TempDir, Registry) {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let spill = TempDir::new().unwrap();
|
||||
let scope = scope_with_spill(dir.path(), spill.path());
|
||||
let fs: WorkdirHandle = Arc::new(LocalWorkdir::new(scope, dir.path().to_path_buf()));
|
||||
let fs: WorkdirSessionHandle =
|
||||
Arc::new(LocalWorkdirSession::new(scope, dir.path().to_path_buf()));
|
||||
let tracker = Tracker::new();
|
||||
let reg = Registry::new(core_builtin_tools(fs, tracker, spill.path().to_path_buf()));
|
||||
(dir, spill, reg)
|
||||
@@ -198,7 +199,7 @@ async fn absolute_path_is_rejected() {
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
// Absolute paths are rejected at the logical Workdir boundary.
|
||||
// Absolute paths are rejected at the logical WorkdirSession boundary.
|
||||
let msg = format!("{err}");
|
||||
assert!(msg.contains("invalid Workdir path"), "unexpected: {msg}");
|
||||
}
|
||||
@@ -224,7 +225,7 @@ async fn write_to_existing_without_read_fails() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn shared_workdir_across_tools() {
|
||||
// The key invariant: all builtin tools share the same Workdir instance,
|
||||
// The key invariant: all builtin tools share the same WorkdirSession instance,
|
||||
// so read-history set by Read is visible to Edit and Write.
|
||||
let (dir, _spill, reg) = setup();
|
||||
let file = dir.path().join("shared.txt");
|
||||
@@ -239,7 +240,7 @@ async fn shared_workdir_across_tools() {
|
||||
json!({ "file_path": file.file_name().unwrap().to_str().unwrap() }),
|
||||
)
|
||||
.await;
|
||||
// Write via Write tool — must succeed because the shared Workdir has the read
|
||||
// Write via Write tool — must succeed because the shared WorkdirSession has the read
|
||||
call(
|
||||
&write,
|
||||
json!({
|
||||
@@ -301,7 +302,8 @@ async fn tracker_recent_files_tracks_read_write_edit() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let spill = TempDir::new().unwrap();
|
||||
let scope = scope_with_spill(dir.path(), spill.path());
|
||||
let fs: WorkdirHandle = Arc::new(LocalWorkdir::new(scope, dir.path().to_path_buf()));
|
||||
let fs: WorkdirSessionHandle =
|
||||
Arc::new(LocalWorkdirSession::new(scope, dir.path().to_path_buf()));
|
||||
let tracker = Tracker::new();
|
||||
let reg = Registry::new(core_builtin_tools(
|
||||
fs,
|
||||
@@ -346,7 +348,7 @@ async fn tracker_recent_files_tracks_read_write_edit() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn bash_inherits_workdir_cwd() {
|
||||
// The Bash tool starts at the Workdir's pwd. Without any `cd`, its
|
||||
// The Bash tool starts at the WorkdirSession's pwd. Without any `cd`, its
|
||||
// `pwd` should canonicalize to the workspace root we set up.
|
||||
let (dir, _spill, reg) = setup();
|
||||
let bash = reg.get("Bash");
|
||||
@@ -363,7 +365,7 @@ async fn bash_provider_output_does_not_expose_internal_paths() {
|
||||
let bash = reg.get("Bash");
|
||||
let out = call(&bash, json!({ "command": "printf 'x%.0s' {1..20480}" })).await;
|
||||
let body = out.content.unwrap();
|
||||
assert!(body.contains("bounded Workdir command output"));
|
||||
assert!(body.contains("bounded WorkdirSession command output"));
|
||||
assert!(!body.contains(spill.path().to_str().unwrap()));
|
||||
assert_eq!(std::fs::read_dir(spill.path()).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user