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
+45 -4
View File
@@ -1,22 +1,34 @@
//! Buffered newline framing shared by stdin and endpoint connections.
#[cfg(target_os = "wasi")]
#[cfg(any(target_os = "wasi", test))]
#[derive(Default)]
pub(crate) struct Lines {
bytes: Vec<u8>,
scanned: usize,
}
#[cfg(target_os = "wasi")]
#[cfg(any(target_os = "wasi", test))]
impl Lines {
fn frame(&mut self) -> Option<Vec<u8>> {
if let Some(end) = self.bytes[self.scanned..].iter().position(|b| *b == b'\n') {
let end = self.scanned + end;
self.scanned = 0;
return Some(self.bytes.drain(..=end).collect());
}
self.scanned = self.bytes.len();
None
}
#[cfg(target_os = "wasi")]
pub(crate) async fn next(
&mut self,
input: &mut impl wstd::io::AsyncRead,
) -> std::io::Result<Option<Vec<u8>>> {
loop {
if let Some(end) = self.bytes.iter().position(|b| *b == b'\n') {
return Ok(Some(self.bytes.drain(..=end).collect()));
if let Some(line) = self.frame() {
return Ok(Some(line));
}
let mut buffer = [0; 8192];
let count = input.read(&mut buffer).await?;
if count == 0 {
self.scanned = 0;
return Ok(if self.bytes.is_empty() {
None
} else {
@@ -26,4 +38,33 @@ impl Lines {
self.bytes.extend_from_slice(&buffer[..count]);
}
}
#[cfg(target_os = "wasi")]
pub(crate) async fn rpc(
&mut self,
input: &mut impl wstd::io::AsyncRead,
) -> std::io::Result<Option<serde_json::Value>> {
while let Some(bytes) = self.next(input).await? {
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes)
&& value.is_object()
&& value["jsonrpc"] == "2.0"
{
return Ok(Some(value));
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
#[test]
fn incomplete_frames_retain_the_scanned_length() {
let mut lines = super::Lines::default();
lines.bytes.extend_from_slice(b"first");
assert!(lines.frame().is_none());
assert_eq!(lines.scanned, 5);
lines.bytes.extend_from_slice(b"\nsecond\n");
assert_eq!(lines.frame().unwrap(), b"first\n");
assert_eq!(lines.frame().unwrap(), b"second\n");
assert!(lines.frame().is_none());
}
}