Add hierarchical workspace instructions
This commit is contained in:
116
src/agent.rs
116
src/agent.rs
@@ -1233,12 +1233,17 @@ pub(crate) struct ToolCall {
|
||||
}
|
||||
|
||||
pub(crate) struct ActiveTools {
|
||||
pub(crate) results: Receiver<String>,
|
||||
pub(crate) results: Receiver<ToolRunResult>,
|
||||
pub(crate) events: Receiver<ToolEvent>,
|
||||
pub(crate) cancel: Arc<AtomicBool>,
|
||||
worker: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
pub(crate) struct ToolRunResult {
|
||||
pub(crate) content: String,
|
||||
pub(crate) touched_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl Drop for ActiveTools {
|
||||
fn drop(&mut self) {
|
||||
self.cancel.store(true, Ordering::Relaxed);
|
||||
@@ -2153,6 +2158,17 @@ impl Tools {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn successful_touch(&self, tool: &ToolSpec, call: &ToolCall) -> Option<PathBuf> {
|
||||
matches!(
|
||||
tool.handler,
|
||||
ToolHandler::Read | ToolHandler::Write | ToolHandler::Edit
|
||||
)
|
||||
.then(|| string(call, "path"))
|
||||
.flatten()
|
||||
.and_then(|path| self.existing_path(path).ok())
|
||||
.filter(|path| path.starts_with(&self.root))
|
||||
}
|
||||
|
||||
fn default_lines(&self) -> usize {
|
||||
match self.context_tokens {
|
||||
..=8192 => 120,
|
||||
@@ -2896,6 +2912,7 @@ pub(crate) fn execute_async(
|
||||
.name("agent-tools".into())
|
||||
.spawn(move || {
|
||||
let mut output = String::new();
|
||||
let mut touched_paths = Vec::new();
|
||||
let mut tools = tools
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
@@ -3024,6 +3041,12 @@ pub(crate) fn execute_async(
|
||||
} else {
|
||||
ToolLifecycle::Completed
|
||||
};
|
||||
if state == ToolLifecycle::Completed
|
||||
&& let Some(path) = tools.successful_touch(tool, call)
|
||||
&& !touched_paths.contains(&path)
|
||||
{
|
||||
touched_paths.push(path);
|
||||
}
|
||||
output.push_str(&result);
|
||||
send_state(&event_sender, index, state, Some(result));
|
||||
if !output.ends_with('\n') {
|
||||
@@ -3035,7 +3058,10 @@ pub(crate) fn execute_async(
|
||||
output.push_str(&format!("Tool warning: {failure}\n"));
|
||||
}
|
||||
}
|
||||
let _ = sender.send(output);
|
||||
let _ = sender.send(ToolRunResult {
|
||||
content: output,
|
||||
touched_paths,
|
||||
});
|
||||
})
|
||||
.expect("agent tool worker must start");
|
||||
ActiveTools {
|
||||
@@ -3050,17 +3076,20 @@ pub(crate) fn error_async(error: String) -> ActiveTools {
|
||||
let cancel = Arc::new(AtomicBool::new(false));
|
||||
let (sender, results) = mpsc::channel();
|
||||
let (_event_sender, events) = mpsc::channel();
|
||||
let _ = sender.send(format!(
|
||||
"{}Retry using the exact tool transport syntax from the system prompt.\n",
|
||||
ToolFailure::new(
|
||||
"<transport>",
|
||||
"malformed_syntax",
|
||||
"$",
|
||||
"complete DSML or GLM tool call",
|
||||
bounded_tool_text(&error, 512),
|
||||
)
|
||||
.render()
|
||||
));
|
||||
let _ = sender.send(ToolRunResult {
|
||||
content: format!(
|
||||
"{}Retry using the exact tool transport syntax from the system prompt.\n",
|
||||
ToolFailure::new(
|
||||
"<transport>",
|
||||
"malformed_syntax",
|
||||
"$",
|
||||
"complete DSML or GLM tool call",
|
||||
bounded_tool_text(&error, 512),
|
||||
)
|
||||
.render()
|
||||
),
|
||||
touched_paths: Vec::new(),
|
||||
});
|
||||
ActiveTools {
|
||||
results,
|
||||
events,
|
||||
@@ -3198,7 +3227,7 @@ pub(crate) fn datetime_context() -> String {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn try_tool_result(active: &ActiveTools) -> Result<Option<String>, String> {
|
||||
pub(crate) fn try_tool_result(active: &ActiveTools) -> Result<Option<ToolRunResult>, String> {
|
||||
match active.results.try_recv() {
|
||||
Ok(result) => Ok(Some(result)),
|
||||
Err(TryRecvError::Empty) => Ok(None),
|
||||
@@ -3897,6 +3926,51 @@ mod tests {
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_structured_file_tools_report_project_paths() {
|
||||
let directory = std::env::temp_dir().join(format!(
|
||||
"ds4-agent-touches-{}",
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
let nested = directory.join("nested");
|
||||
fs::create_dir_all(&nested).unwrap();
|
||||
fs::write(nested.join("read.txt"), "read me").unwrap();
|
||||
let tools = Arc::new(Mutex::new(Tools::new(&directory, 4096).unwrap()));
|
||||
let active = execute_async(
|
||||
tools,
|
||||
vec![
|
||||
call("read", [("path", "nested/read.txt")]),
|
||||
call(
|
||||
"write",
|
||||
[("path", "nested/write.txt"), ("content", "before")],
|
||||
),
|
||||
call(
|
||||
"edit",
|
||||
[
|
||||
("path", "nested/write.txt"),
|
||||
("old_text", "before"),
|
||||
("new_text", "after"),
|
||||
],
|
||||
),
|
||||
call("read", [("path", "nested/missing.txt")]),
|
||||
],
|
||||
ShellApprovalMode::Heuristic,
|
||||
);
|
||||
let result = active.results.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
assert_eq!(
|
||||
result.touched_paths,
|
||||
vec![
|
||||
nested.join("read.txt").canonicalize().unwrap(),
|
||||
nested.join("write.txt").canonicalize().unwrap(),
|
||||
]
|
||||
);
|
||||
assert!(result.content.contains("code=execution_failed"));
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_adapters_preserve_primitives_and_report_recoverable_syntax_errors() {
|
||||
let glm = "<tool_call>bash<arg_key>command</arg_key><arg_value>pwd</arg_value><arg_key>timeout_sec</arg_key><arg_value>3</arg_value></tool_call>";
|
||||
@@ -3919,8 +3993,16 @@ mod tests {
|
||||
|
||||
let active = error_async("incomplete GLM tool call".into());
|
||||
let result = active.results.recv_timeout(Duration::from_secs(1)).unwrap();
|
||||
assert!(result.contains("tool=<transport> code=malformed_syntax"));
|
||||
assert!(result.contains("Retry using the exact tool transport syntax"));
|
||||
assert!(
|
||||
result
|
||||
.content
|
||||
.contains("tool=<transport> code=malformed_syntax")
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
.content
|
||||
.contains("Retry using the exact tool transport syntax")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4578,6 +4660,7 @@ mod tests {
|
||||
.results
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.unwrap()
|
||||
.content
|
||||
.contains("code=policy_denied")
|
||||
);
|
||||
assert!(directory.join("keep.txt").exists());
|
||||
@@ -4605,6 +4688,7 @@ mod tests {
|
||||
.results
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.unwrap()
|
||||
.content
|
||||
.contains("interrupted")
|
||||
);
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user