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:
2026-10-23 02:38:35 +01:00
parent d7b78ed0fd
commit c728429d04
29 changed files with 829 additions and 104 deletions
+102 -5
View File
@@ -11,6 +11,21 @@ struct Process {
output: BufReader<std::process::ChildStdout>,
}
impl Process {
fn finishes(&mut self) {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(4);
loop {
if let Some(status) = self.child.try_wait().unwrap() {
assert!(status.success());
return;
}
if std::time::Instant::now() >= deadline {
self.child.kill().unwrap();
let _ = self.child.wait();
panic!("process survived exit or EOF for four seconds");
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
fn send(&mut self, v: Value) {
writeln!(self.input.as_mut().unwrap(), "{v}").unwrap();
}
@@ -76,6 +91,15 @@ fn protocol() {
let wasm = target.join("wasm32-wasip2/debug/sdk-probe.wasm");
let dir = std::env::temp_dir().join(format!("sausage-sdk-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
for method in ["sleepy_exit", "sleepy"] {
let mut p = start(&wasm, &dir);
p.request(method, json!({}));
assert_eq!(p.read()["result"], Value::Null);
if method == "sleepy" {
drop(p.input.take());
}
p.finishes();
}
let mut p = start(&wasm, &dir);
p.request("rpc.discover", json!({}));
let d = p.read();
@@ -170,7 +194,7 @@ fn protocol() {
p.answer(&outgoing, json!({}));
assert!(p.child.wait().unwrap().success());
let provider_doc = &d["result"];
let imports = root.join("tests/plugins/consumer/imports");
let imports = target.join("imports");
std::fs::create_dir_all(&imports).unwrap();
std::fs::write(
imports.join("probe.json"),
@@ -237,9 +261,82 @@ fn protocol() {
.contains("identity mismatch")
);
assert!(client.child.wait().unwrap().success());
std::fs::write(path, original).unwrap();
drop(provider.input.take());
assert_eq!(provider.read()["params"]["name"], "stopped");
assert!(provider.child.wait().unwrap().success());
std::fs::write(&path, original).unwrap();
// The endpoint server ignores non-RPC lines before a request.
let address = std::fs::read_to_string(&path)
.unwrap()
.split_whitespace()
.next()
.unwrap()
.trim_start_matches("tcp:")
.to_owned();
let mut stream = std::net::TcpStream::connect(address).unwrap();
stream
.set_read_timeout(Some(std::time::Duration::from_secs(4)))
.unwrap();
stream.write_all(b"\nnot-json\n[]\n{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"rpc.endpoint\",\"params\":{}}\n").unwrap();
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert_eq!(
sausage_plugin::serde_json::from_str::<Value>(&line).unwrap()["id"],
99
);
reader
.get_mut()
.write_all(b"{\"jsonrpc\":\"2.0\",\"id\":100,\"method\":\"stop\",\"params\":{}}\n")
.unwrap();
line.clear();
reader.read_line(&mut line).unwrap();
assert_eq!(
sausage_plugin::serde_json::from_str::<Value>(&line).unwrap()["id"],
100
);
provider.finishes();
// The endpoint client ignores non-RPC lines during verification and responses.
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let owner = "probe-00000000-0000-7000-8000-000000000000";
std::fs::write(
&path,
format!("tcp:{} {owner} control\n", listener.local_addr().unwrap()),
)
.unwrap();
let server = std::thread::spawn(move || {
for _ in 0..2 {
let (stream, _) = listener.accept().unwrap();
stream
.set_read_timeout(Some(std::time::Duration::from_secs(4)))
.unwrap();
let mut reader = BufReader::new(stream);
loop {
let mut line = String::new();
if reader.read_line(&mut line).unwrap() == 0 {
break;
}
let request: Value = sausage_plugin::serde_json::from_str(&line).unwrap();
let result = if request["method"] == "rpc.endpoint" {
json!({"owner":owner,"plugin":"probe","interface":"control"})
} else {
json!(7)
};
writeln!(
reader.get_mut(),
"\nnot-json\n[]\n{}",
json!({"jsonrpc":"2.0","id":request["id"],"result":result})
)
.unwrap();
}
}
});
let mut client = start(&consumer, &dir);
client.request("endpoint", json!({}));
assert_eq!(client.read()["result"], 7);
client.finishes();
server.join().unwrap();
assert_eq!(
std::fs::read(root.join("tests/plugins/consumer/imports/probe.json")).unwrap(),
include_bytes!("plugins/consumer/imports/probe.json")
);
std::fs::remove_dir_all(dir).unwrap();
}