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:
+42
-11
@@ -33,7 +33,7 @@ impl Agent {
|
||||
let _guard = lock(HISTORY).await?;
|
||||
std::fs::create_dir_all("/state/agent")?;
|
||||
let cfg: Config = serde_json::from_slice(&std::fs::read("/config/agent.json")?)?;
|
||||
let mut entries = load_history()?;
|
||||
let mut entries = load_history(HISTORY)?;
|
||||
repair_tools(&mut entries)?;
|
||||
let current = entries.len();
|
||||
append(&mut entries, Entry::User { text: p.text })?;
|
||||
@@ -52,12 +52,12 @@ impl Agent {
|
||||
&& let Some(next) = next_turn_start(&entries, start + 1, current)
|
||||
{
|
||||
start = next;
|
||||
std::fs::write(SKIP, start.to_string())?;
|
||||
continue;
|
||||
}
|
||||
return Err(Error::new(-32603, error));
|
||||
}
|
||||
};
|
||||
std::fs::write(SKIP, start.to_string())?;
|
||||
let has_tools = response
|
||||
.iter()
|
||||
.any(|entry| matches!(entry, Entry::ToolCall { .. }));
|
||||
@@ -70,7 +70,12 @@ impl Agent {
|
||||
{
|
||||
ctx.notify("text", json!({"text":text}))?
|
||||
}
|
||||
Entry::Assistant { text, .. } => final_text.push_str(text),
|
||||
Entry::Assistant { text, .. } => {
|
||||
if !final_text.is_empty() && !text.is_empty() {
|
||||
final_text.push_str("\n\n");
|
||||
}
|
||||
final_text.push_str(text);
|
||||
}
|
||||
Entry::ToolCall {
|
||||
id,
|
||||
name,
|
||||
@@ -87,14 +92,14 @@ impl Agent {
|
||||
let output = match serde_json::from_str(&arguments) {
|
||||
Ok(args) => tools::run(&name, &args)
|
||||
.await
|
||||
.unwrap_or_else(|error| json!({"error":error.to_string()})),
|
||||
.unwrap_or_else(|error| json!({"error":error})),
|
||||
Err(error) => json!({"error":format!("arguments are not valid JSON: {error}")}),
|
||||
};
|
||||
append(
|
||||
&mut entries,
|
||||
Entry::ToolResult {
|
||||
id,
|
||||
output: output.to_string(),
|
||||
output: tools::limit_output(output.to_string()),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
@@ -103,12 +108,18 @@ impl Agent {
|
||||
}
|
||||
|
||||
/// Read persisted entries in their original order. A new world has no history file.
|
||||
fn load_history() -> Result<Vec<Entry>> {
|
||||
match std::fs::read_to_string(HISTORY) {
|
||||
Ok(text) => text
|
||||
fn load_history(path: &str) -> Result<Vec<Entry>> {
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(text) => Ok(text
|
||||
.lines()
|
||||
.map(|line| serde_json::from_str(line).map_err(Into::into))
|
||||
.collect(),
|
||||
.filter_map(|line| match serde_json::from_str(line) {
|
||||
Ok(entry) => Some(entry),
|
||||
Err(error) => {
|
||||
eprintln!("skip damaged history line: {error}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
@@ -119,7 +130,7 @@ fn append(history: &mut Vec<Entry>, entry: Entry) -> Result<()> {
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(HISTORY)?;
|
||||
writeln!(file, "{}", serde_json::to_string(&entry)?)?;
|
||||
file.write_all(format!("{}\n", serde_json::to_string(&entry)?).as_bytes())?;
|
||||
history.push(entry);
|
||||
Ok(())
|
||||
}
|
||||
@@ -154,3 +165,23 @@ fn repair_tools(history: &mut Vec<Entry>) -> Result<()> {
|
||||
fn main() {
|
||||
sausage_plugin::run(Agent);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn damaged_history_keeps_valid_entries() {
|
||||
let path = if cfg!(target_os = "wasi") {
|
||||
std::path::PathBuf::from("/history-test.jsonl")
|
||||
} else {
|
||||
std::env::temp_dir().join(format!("sausage-history-{}", std::process::id()))
|
||||
};
|
||||
std::fs::write(
|
||||
&path,
|
||||
"{\"kind\":\"user\",\"text\":\"preserved\"}\n{\"kind\":",
|
||||
)
|
||||
.unwrap();
|
||||
let history = super::load_history(path.to_str().unwrap()).unwrap();
|
||||
assert_eq!(history.len(), 1);
|
||||
std::fs::remove_file(path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user