fix: handle tool calls with empty arguments

A tool call that carries no arguments streams no argument text, so
`arguments_text` is empty and `from_str("")` fails with `EOF while parsing
a value`, aborting the whole run. The model cycle already guards this, but
two other consumers did not:

- `ConversationOutput` re-parses the streamed text on `ToolCallEnd`; and
- `create_tool_round` stored the empty text verbatim in the
  `arguments_json` column, so re-loading the round (`commit_tool_result`
  and the round loader) then failed on `from_str("")`.

Treat empty argument text as an empty object in the output projection, and
persist `{}` for it so the `arguments_json` column always holds valid JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kevin9327
2026-08-30 19:38:39 +09:00
co-authored by Claude Opus 4.8
parent 76003a907a
commit e673a034df
3 changed files with 78 additions and 2 deletions
+8 -1
View File
@@ -343,7 +343,14 @@ impl ConversationOutput {
let call = calls.get_mut(&index).ok_or_else(|| {
Error::Protocol(format!("unknown completed tool index: {index}"))
})?;
call.arguments = serde_json::from_str(&call.arguments_text)?;
// A tool call with no arguments streams no argument text.
// Treat empty text as an empty object, matching the model
// cycle, instead of failing the run on `from_str("")`.
call.arguments = if call.arguments_text.trim().is_empty() {
serde_json::json!({})
} else {
serde_json::from_str(&call.arguments_text)?
};
}
RunEvent::Usage(usage) => {
if !self.context.compacting {
+8 -1
View File
@@ -101,7 +101,14 @@ impl Store {
.bind(&call.call_id)
.bind(&call.model_call_id)
.bind(&call.name)
.bind(&call.arguments_text)
// A no-argument tool call streams no argument text; persist it as an
// empty object so the `arguments_json` column always holds valid JSON
// and can be re-parsed on load.
.bind(if call.arguments_text.trim().is_empty() {
"{}"
} else {
call.arguments_text.as_str()
})
.execute(&mut *tx)
.await?;
}