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
+15 -17
View File
@@ -7,6 +7,9 @@ use serde_json::json;
use std::{future::IntoFuture, marker::PhantomData, path::Path, rc::Rc};
/// A verified endpoint address and identity.
///
/// Connect verifies and closes one connection. Each call opens a new connection
/// and repeats the identity handshake before its request. The first call costs three round trips.
#[derive(Clone)]
#[cfg_attr(not(target_os = "wasi"), allow(dead_code))]
pub struct Endpoint {
@@ -20,7 +23,7 @@ impl Endpoint {
if self.interface == name {
Ok(())
} else {
Err(Error::new(-32002, "endpoint interface mismatch"))
Err(Error::new(-32000, "endpoint interface mismatch"))
}
}
/// Build a typed endpoint request.
@@ -35,6 +38,7 @@ impl Endpoint {
}
}
/// A lazy endpoint request with optional progress callbacks.
#[must_use = "await the endpoint call to send it"]
pub struct EndpointCall<T> {
endpoint: Endpoint,
method: String,
@@ -87,7 +91,7 @@ pub async fn listen<T: Interface>(path: impl AsRef<Path>, value: Rc<T>) -> Resul
#[cfg(not(target_os = "wasi"))]
pub async fn listen<T: Interface>(_path: impl AsRef<Path>, _value: Rc<T>) -> Result<()> {
crate::transport::endpoint_name::<T>()?;
Err(Error::new(-32002, "endpoints require WASI"))
Err(Error::new(-32000, "endpoints require WASI"))
}
/// Read an endpoint file and verify its owner before returning a client.
@@ -122,7 +126,7 @@ pub async fn connect(path: impl AsRef<Path>) -> Result<Endpoint> {
/// Connect to an endpoint. Native programs require the WASI runtime.
#[cfg(not(target_os = "wasi"))]
pub async fn connect(_path: impl AsRef<Path>) -> Result<Endpoint> {
Err(Error::new(-32002, "endpoints require WASI"))
Err(Error::new(-32000, "endpoints require WASI"))
}
#[cfg(target_os = "wasi")]
@@ -139,11 +143,10 @@ async fn handshake(endpoint: &Endpoint) -> Result<wstd::net::TcpStream> {
json!({"jsonrpc":"2.0","id":0,"method":"rpc.endpoint","params":{}}),
)
.await?;
let bytes = crate::lines::Lines::default()
.next(&mut stream)
let reply = crate::lines::Lines::default()
.rpc(&mut stream)
.await?
.ok_or_else(|| Error::new(-32002, "endpoint closed"))?;
let reply: Value = serde_json::from_slice(&bytes)?;
.ok_or_else(|| Error::new(-32000, "endpoint closed"))?;
let result = &reply["result"];
let plugin = endpoint
.owner
@@ -154,7 +157,7 @@ async fn handshake(endpoint: &Endpoint) -> Result<wstd::net::TcpStream> {
|| result["interface"] != endpoint.interface
|| result["plugin"].as_str() != plugin
{
return Err(Error::new(-32002, "endpoint identity mismatch"));
return Err(Error::new(-32000, "endpoint identity mismatch"));
}
Ok(stream)
}
@@ -172,8 +175,7 @@ async fn endpoint_request(
json!({"jsonrpc":"2.0","id":1,"method":method,"params":params}),
)
.await?;
while let Some(bytes) = lines.next(&mut stream).await? {
let value: Value = serde_json::from_slice(&bytes)?;
while let Some(value) = lines.rpc(&mut stream).await? {
if value["method"] == "core.notify" {
if let Some(f) = &mut notify {
f(
@@ -188,7 +190,7 @@ async fn endpoint_request(
return Ok(value["result"].clone());
}
}
Err(Error::new(-32002, "endpoint closed before response"))
Err(Error::new(-32000, "endpoint closed before response"))
}
#[cfg(not(target_os = "wasi"))]
async fn endpoint_request(
@@ -197,7 +199,7 @@ async fn endpoint_request(
_params: Value,
_notify: Option<Notify>,
) -> Result<Value> {
Err(Error::new(-32002, "endpoints require WASI"))
Err(Error::new(-32000, "endpoints require WASI"))
}
#[cfg(target_os = "wasi")]
@@ -209,11 +211,7 @@ async fn serve_connection<T: Interface>(
use futures::{FutureExt, StreamExt};
let (mut reader, mut writer) = stream.split();
let mut lines = crate::lines::Lines::default();
while let Some(bytes) = lines.next(&mut reader).await? {
let request: Value = serde_json::from_slice(&bytes)?;
if request["jsonrpc"] != "2.0" {
continue;
}
while let Some(request) = lines.rpc(&mut reader).await? {
let method = request["method"].as_str().unwrap_or_default();
let (tx, mut rx) = futures::channel::mpsc::unbounded();
let ctx = Ctx {