sdk, agent: review fixes (T18R)
- The serve loop ends the process after EOF or exit_after_response, also with background tasks or listeners. - One schema generator per direction for the whole document, so types with the same name get distinct names. - The agent gives the complete error (code, message, data) to the model, writes SKIP only after a successful request, caps tool output at 64 KiB, writes each history line in one write, and skips a damaged line. - Endpoints skip blank and non-RPC lines. client_rs does not panic. - #[must_use] on builders and the lock guard; #[schemars] attributes and duplicate hooks are compile errors; raw identifiers; -32000 for local SDK errors; x-sausage-params only for methods with params.
This commit is contained in:
+54
-5
@@ -4,12 +4,29 @@ use crate::llm::Tool;
|
||||
use serde_json::{Value, json};
|
||||
use std::io;
|
||||
|
||||
/// Maximum UTF-8 bytes in a serialized tool result, including its truncation marker.
|
||||
pub const MAX_OUTPUT_BYTES: usize = 65_536;
|
||||
|
||||
/// Bound every tool result before it enters history or a provider request.
|
||||
pub fn limit_output(mut output: String) -> String {
|
||||
const MARKER: &str = "\n[tool output truncated at 65536 bytes]";
|
||||
if output.len() > MAX_OUTPUT_BYTES {
|
||||
let mut end = MAX_OUTPUT_BYTES - MARKER.len();
|
||||
while !output.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
output.truncate(end);
|
||||
output.push_str(MARKER);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
/// Describe the five tools sent to either inference API.
|
||||
pub fn definitions() -> Vec<Tool> {
|
||||
vec![
|
||||
Tool {
|
||||
name: "call",
|
||||
description: "Call a plugin method through the core. Await its result. Set detach to let its process survive this caller.",
|
||||
description: "Call a plugin method through the core. Await its result. Set detach to let its process survive this caller. Output is capped at 65536 bytes with a truncation marker.",
|
||||
parameters: schema(
|
||||
json!({
|
||||
"method": {"type": "string", "description": "Qualified plugin method"},
|
||||
@@ -21,7 +38,7 @@ pub fn definitions() -> Vec<Tool> {
|
||||
},
|
||||
Tool {
|
||||
name: "read_file",
|
||||
description: "Read a text file in the world, including `/proc`. Reads all bytes by default; `offset` and `max_bytes` are optional.",
|
||||
description: "Read a text file in the world, including `/proc`. `offset` and `max_bytes` are optional. Output is capped at 65536 bytes with a truncation marker.",
|
||||
parameters: schema(
|
||||
json!({
|
||||
"path": {"type": "string"},
|
||||
@@ -33,7 +50,7 @@ pub fn definitions() -> Vec<Tool> {
|
||||
},
|
||||
Tool {
|
||||
name: "write_file",
|
||||
description: "Write a text file in the world. Parent folders are created.",
|
||||
description: "Write a text file in the world. Parent folders are created. Output is capped at 65536 bytes with a truncation marker.",
|
||||
parameters: schema(
|
||||
json!({"path": {"type": "string"}, "content": {"type": "string"}}),
|
||||
&["path", "content"],
|
||||
@@ -41,12 +58,12 @@ pub fn definitions() -> Vec<Tool> {
|
||||
},
|
||||
Tool {
|
||||
name: "list_dir",
|
||||
description: "List entries in a world directory. Returns each name, type, and size.",
|
||||
description: "List entries in a world directory. Returns each name, type, and size. Output is capped at 65536 bytes with a truncation marker.",
|
||||
parameters: schema(json!({"path": {"type": "string"}}), &["path"]),
|
||||
},
|
||||
Tool {
|
||||
name: "copy_dir",
|
||||
description: "Copy a directory tree, for example a plugin source to a draft folder. Skips `target/` folders.",
|
||||
description: "Copy a directory tree, for example a plugin source to a draft folder. Skips `target/` folders. Output is capped at 65536 bytes with a truncation marker.",
|
||||
parameters: schema(
|
||||
json!({"from": {"type": "string"}, "to": {"type": "string"}}),
|
||||
&["from", "to"],
|
||||
@@ -128,6 +145,15 @@ fn list_dir(args: &Value) -> io::Result<Value> {
|
||||
|
||||
/// Copy a tree and report its file count.
|
||||
fn copy_dir(args: &Value) -> io::Result<Value> {
|
||||
let from = std::fs::canonicalize(args["from"].as_str().unwrap_or_default())?;
|
||||
let to = args["to"].as_str().unwrap_or_default();
|
||||
std::fs::create_dir_all(to)?;
|
||||
if std::fs::canonicalize(to)?.starts_with(&from) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"destination is inside the source",
|
||||
));
|
||||
}
|
||||
let count = copy_tree(
|
||||
args["from"].as_str().unwrap_or_default(),
|
||||
args["to"].as_str().unwrap_or_default(),
|
||||
@@ -155,3 +181,26 @@ fn copy_tree(from: &str, to: &str) -> io::Result<u64> {
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn tool_output_limit_preserves_utf8_and_marks_truncation() {
|
||||
let limited = super::limit_output("😀".repeat(40_000));
|
||||
assert!(limited.len() <= super::MAX_OUTPUT_BYTES);
|
||||
assert!(limited.ends_with("[tool output truncated at 65536 bytes]"));
|
||||
assert_eq!(super::limit_output("small".into()), "small");
|
||||
}
|
||||
#[test]
|
||||
fn copy_rejects_a_destination_inside_the_source() {
|
||||
let root = if cfg!(target_os = "wasi") {
|
||||
std::path::PathBuf::from("/copy-test")
|
||||
} else {
|
||||
std::env::temp_dir().join(format!("sausage-copy-{}", std::process::id()))
|
||||
};
|
||||
std::fs::create_dir_all(&root).unwrap();
|
||||
let result = super::copy_dir(&serde_json::json!({"from":root,"to":root.join("target")}));
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::InvalidInput);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user