Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed

- **All languages:** the AI Agent streamed conversation no longer errors mid-run when the server sends an explicit `"outputs": null`. `WorkflowFinishedPayload.outputs`, `NodeToolUseFinishedPayload.outputs`, and `SubagentFinishedPayload.outputs` were annotated `#[serde(default)]`, which only covers a *missing* key, not an explicit `null` — so a `workflow_finished` / `node_tool_use_finished` / `subagent_finished` event carrying `null` outputs failed to deserialize and aborted the whole event stream (`invalid type: null, expected struct WorkflowOutputs`). These fields now map `null` to the type's default
- **All languages:** likewise, list-typed fields on the streamed AI Agent event payloads no longer error on an explicit `null` (`invalid type: null, expected a sequence`). `tip_chips` (on the node / subagent / agent-tool `*_started` payloads), `WorkflowFinishedPayload.process_data`, and `SubagentStartedPayload.tools` were `#[serde(default)]`, which does not accept an explicit `null`; they now deserialize `null` to an empty list

## [4.5.0] - 2026-08-14

Expand Down
29 changes: 23 additions & 6 deletions rust/src/agent/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ pub struct WorkflowFinishedPayload {
#[serde(default)]
pub error_args: Option<serde_json::Value>,
/// Process stages the run went through; for display only
#[serde(default)]
#[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
pub process_data: Vec<serde_json::Value>,
}

Expand Down Expand Up @@ -569,7 +569,7 @@ pub struct NodeToolUseStartedPayload {
#[serde(default)]
pub tips: String,
/// Short tags accompanying `tips`; may be omitted
#[serde(default)]
#[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
pub tip_chips: Vec<String>,
/// Round number. Calls in the same round (same `iteration`) run in
/// parallel
Expand Down Expand Up @@ -639,7 +639,7 @@ pub struct NodeToolUseFinishedPayload {
#[serde(default)]
pub tips: String,
/// Short tags; may be omitted
#[serde(default)]
#[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
pub tip_chips: Vec<String>,
/// Round number
#[serde(default)]
Expand Down Expand Up @@ -676,7 +676,7 @@ pub struct SubagentStartedPayload {
#[serde(default)]
pub subagent_id: String,
/// Tools granted to the subagent; may be omitted
#[serde(default)]
#[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
pub tools: Vec<serde_json::Value>,
}

Expand Down Expand Up @@ -782,7 +782,7 @@ pub struct AgentToolStartedPayload {
#[serde(default)]
pub tips: String,
/// Short tags; may be omitted
#[serde(default)]
#[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
pub tip_chips: Vec<String>,
/// `true` if called during the thinking phase
#[serde(default)]
Expand Down Expand Up @@ -859,7 +859,7 @@ pub struct AgentToolFinishedPayload {
#[serde(default)]
pub tips: String,
/// Short tags; may be omitted
#[serde(default)]
#[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
pub tip_chips: Vec<String>,
/// `true` if during the thinking phase
#[serde(default)]
Expand Down Expand Up @@ -1047,6 +1047,23 @@ mod tests {
assert!(payload.outputs.answer.is_none());
}

#[test]
fn streamed_vec_fields_tolerate_null_sequences() {
// The server can send an explicit `null` for list-typed fields; that
// must deserialize to an empty list, not error with
// "invalid type: null, expected a sequence" and abort the stream.
let payload: WorkflowFinishedPayload =
serde_json::from_str(r#"{"status":"succeeded","process_data":null}"#).unwrap();
assert!(payload.process_data.is_empty());

let payload: NodeToolUseStartedPayload =
serde_json::from_str(r#"{"tip_chips":null}"#).unwrap();
assert!(payload.tip_chips.is_empty());

let payload: SubagentStartedPayload = serde_json::from_str(r#"{"tools":null}"#).unwrap();
assert!(payload.tools.is_empty());
}

// The `data` payload of the "Run succeeded" example from
// https://open.longbridge.com/en/docs/ai/chat/conversation
const SUCCEEDED_JSON: &str = r#"{
Expand Down
Loading