Skip to main content

longbridge/agent/
types.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5/// Answers keyed by `tool_call_id`, each value being a map of question text to
6/// answer, used as the request body of
7/// [`crate::AgentContext::continue_conversation`] and
8/// [`crate::AgentContext::continue_conversation_streamed`].
9pub type AnswersByToolCall = HashMap<String, HashMap<String, String>>;
10
11/// A Workspace the current account belongs to
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Workspace {
14    /// Workspace ID
15    pub id: String,
16    /// Workspace name
17    pub name: String,
18    /// Creation time, Unix timestamp in seconds
19    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
20    pub created_at: i64,
21    /// Last updated time, Unix timestamp in seconds
22    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
23    pub updated_at: i64,
24}
25
26/// Response for [`crate::AgentContext::workspaces`]
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct WorkspacesResponse {
29    /// Workspaces the current account belongs to
30    pub workspaces: Vec<Workspace>,
31}
32
33/// An Agent in a Workspace
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Agent {
36    /// Agent UID, used as the path parameter of
37    /// [`crate::AgentContext::conversation`]
38    pub uid: String,
39    /// Agent name
40    pub name: String,
41    /// Agent description
42    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
43    pub description: String,
44    /// Agent mode, e.g. `chat`
45    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
46    pub mode: String,
47    /// Icon URL
48    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
49    pub icon: String,
50    /// Whether published; only published Agents can start conversations
51    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
52    pub is_published: bool,
53    /// Publish time, Unix timestamp in seconds; 0 if unpublished
54    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
55    pub published_at: i64,
56    /// Creation time, Unix timestamp in seconds
57    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
58    pub created_at: i64,
59    /// Last updated time, Unix timestamp in seconds
60    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
61    pub updated_at: i64,
62}
63
64/// Response for [`crate::AgentContext::agents`]
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct AgentsResponse {
67    /// Agent list
68    pub agents: Vec<Agent>,
69    /// Total number of matching Agents
70    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
71    pub total: i32,
72}
73
74/// Options for [`crate::AgentContext::agents`]
75#[derive(Debug, Serialize, Default, Clone)]
76pub struct GetAgentsOptions {
77    #[serde(skip_serializing_if = "Option::is_none")]
78    page: Option<i32>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    limit: Option<i32>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    name: Option<String>,
83}
84
85impl GetAgentsOptions {
86    /// Create a new `GetAgentsOptions`
87    #[inline]
88    pub fn new() -> Self {
89        Default::default()
90    }
91
92    /// Set the page number, starts at 1
93    #[inline]
94    #[must_use]
95    pub fn page(self, page: i32) -> Self {
96        Self {
97            page: Some(page),
98            ..self
99        }
100    }
101
102    /// Set the page size
103    #[inline]
104    #[must_use]
105    pub fn limit(self, limit: i32) -> Self {
106        Self {
107            limit: Some(limit),
108            ..self
109        }
110    }
111
112    /// Fuzzy search by Agent name
113    #[inline]
114    #[must_use]
115    pub fn name(self, name: impl Into<String>) -> Self {
116        Self {
117            name: Some(name.into()),
118            ..self
119        }
120    }
121}
122
123/// Final run status of a conversation
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "snake_case")]
126pub enum ConversationStatus {
127    /// The run completed successfully
128    Succeeded,
129    /// The run is paused, waiting for
130    /// [`crate::AgentContext::continue_conversation`]
131    Interrupted,
132    /// The run failed
133    Failed,
134    /// The run was stopped
135    Stopped,
136}
137
138/// A source referenced by the answer
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Reference {
141    /// Reference index
142    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
143    pub index: i32,
144    /// Original index in the source list, before any reranking
145    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
146    pub original_index: i32,
147    /// Reference kind, e.g. `"NewsArticle"`
148    #[serde(
149        default,
150        deserialize_with = "crate::serde_utils::null_as_default",
151        rename = "type"
152    )]
153    pub ref_type: String,
154    /// Reference id
155    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
156    pub id: String,
157    /// Reference title. Often empty at the top level — the human-readable
158    /// title usually lives in [`content`](Self::content).
159    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
160    pub title: String,
161    /// Reference URL. Often empty at the top level — see
162    /// [`content`](Self::content).
163    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
164    pub url: String,
165    /// Full reference payload as sent by the server (`source`, `description`,
166    /// `published_at`, `source_url`, `source_logo`, `kind`, …). Kept as raw
167    /// JSON because the field set varies by reference
168    /// [`ref_type`](Self::ref_type).
169    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
170    pub content: Option<serde_json::Value>,
171}
172
173/// One question the Agent needs you to answer
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct Question {
176    /// Question text
177    pub question: String,
178    /// Options; empty means free-form answer
179    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
180    pub options: Vec<QuestionOption>,
181    /// Whether multiple options may be selected
182    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
183    pub multi_select: bool,
184}
185
186/// One option of a [`Question`]
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct QuestionOption {
189    /// Short UI label for the option.
190    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
191    pub label: String,
192    /// Option text
193    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
194    pub description: String,
195}
196
197/// Present when a conversation run is interrupted, waiting for
198/// [`crate::AgentContext::continue_conversation`]
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct Interrupt {
201    /// ID of the node that triggered the interrupt
202    pub node_id: String,
203    /// Tool call ID of this inquiry; used as the answer key when continuing
204    pub tool_call_id: String,
205    /// Questions you need to answer
206    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
207    pub questions: Vec<Question>,
208    /// Full interaction descriptors used to render and answer the pause.
209    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
210    pub interactions: Vec<HumanInteraction>,
211    /// ID of the paused message
212    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
213    pub message_id: i64,
214    /// ID of the owning conversation
215    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
216    pub chat_id: i64,
217}
218
219/// A single interaction requested while an Agent workflow is paused.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct HumanInteraction {
222    /// Tool call that requested the interaction.
223    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
224    pub tool_call_id: String,
225    /// Stable key expected by `answers_by_tool_call`.
226    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
227    pub interrupt_id: String,
228    /// Interaction type such as `ask_human` or `trade_password`.
229    #[serde(
230        default,
231        deserialize_with = "crate::serde_utils::null_as_default",
232        rename = "type"
233    )]
234    pub interaction_type: String,
235    /// Human-readable tool name.
236    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
237    pub tool_name: String,
238    /// Questions and answer options presented to the user.
239    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
240    pub questions: Vec<Question>,
241    /// Original tool arguments, retained for host-specific UI rendering.
242    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
243    pub tool_args: serde_json::Value,
244}
245
246/// Present when a conversation run failed
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct AgentError {
249    /// Error code
250    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
251    pub code: i32,
252    /// Error message
253    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
254    pub message: String,
255}
256
257/// Response for [`crate::AgentContext::conversation`],
258/// [`crate::AgentContext::continue_conversation`], and the final result of the
259/// streamed counterparts
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ConversationResponse {
262    /// Conversation identifier, used for follow-up questions and
263    /// troubleshooting
264    pub chat_uid: String,
265    /// Message ID of this round (as a string). Accepts a raw JSON number too,
266    /// defensively — see [`ChatStartedPayload::message_id`].
267    #[serde(deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string")]
268    pub message_id: String,
269    /// Final run status
270    pub status: ConversationStatus,
271    /// Final answer text; valid when `status` is `succeeded`
272    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
273    pub answer: String,
274    /// Sources referenced by the answer
275    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
276    pub references: Option<Vec<Reference>>,
277    /// Suggested follow-up questions ("you might also ask"); present when the
278    /// run produced them
279    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
280    pub further_questions: Option<Vec<String>>,
281    /// Run duration in seconds
282    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
283    pub elapsed_time: f64,
284    /// Present only when `status` is `interrupted`
285    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
286    pub interrupt: Option<Interrupt>,
287    /// Present only when the run failed
288    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
289    pub error: Option<AgentError>,
290}
291
292impl ConversationResponse {
293    /// Build a [`ConversationResponse`] from a streamed conversation's parts —
294    /// `chat_uid`/`message_id` captured from an earlier `chat_started` event
295    /// (`None` if it was never observed) and the `workflow_finished` payload.
296    pub(crate) fn from_stream_parts(
297        started: Option<(String, String)>,
298        payload: WorkflowFinishedPayload,
299    ) -> Self {
300        let (chat_uid, message_id) = started.unwrap_or_default();
301        let error = (payload.status == ConversationStatus::Failed).then_some(AgentError {
302            code: payload.error_code,
303            message: payload.error_message,
304        });
305        Self {
306            chat_uid,
307            message_id,
308            status: payload.status,
309            answer: payload.outputs.answer.unwrap_or_default(),
310            references: payload.outputs.references,
311            further_questions: payload.outputs.further_questions,
312            elapsed_time: payload.elapsed_time,
313            interrupt: None,
314            error,
315        }
316    }
317
318    /// Build a [`ConversationResponse`] from a streamed conversation's parts —
319    /// `chat_uid`/`message_id` captured from an earlier `chat_started` event,
320    /// and a `human_interaction_required` event's [`Interrupt`] payload.
321    ///
322    /// Unlike the succeeded/failed/stopped cases, an interrupted run doesn't
323    /// emit `workflow_finished` at all — `human_interaction_required` is the
324    /// terminal event of the stream instead, so this plays the same role
325    /// [`Self::from_stream_parts`] plays for the other outcomes.
326    pub(crate) fn from_stream_interrupt(
327        started: Option<(String, String)>,
328        interrupt: Interrupt,
329    ) -> Self {
330        let (chat_uid, message_id) = started.unwrap_or_default();
331        Self {
332            chat_uid,
333            message_id,
334            status: ConversationStatus::Interrupted,
335            answer: String::new(),
336            references: None,
337            further_questions: None,
338            elapsed_time: 0.0,
339            interrupt: Some(interrupt),
340            error: None,
341        }
342    }
343}
344
345/// Payload of a `chat_started` SSE event
346#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct ChatStartedPayload {
348    /// Conversation identifier
349    pub chat_uid: String,
350    /// Message ID of this round. The docs' SSE example shows this as a raw JSON
351    /// number here (unlike the blocking response's top-level `message_id`,
352    /// which is a quoted string) — accept either.
353    #[serde(deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string")]
354    pub message_id: String,
355    /// ID of the owning conversation
356    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
357    pub chat_id: i64,
358    /// Error detail; empty at start
359    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
360    pub error: String,
361    /// User-facing error message; empty at start
362    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
363    pub error_message: String,
364}
365
366/// Payload of a `message` SSE event — an incremental text chunk. This is the
367/// highest-frequency event; concatenate `text` fragments in arrival order.
368#[derive(Debug, Clone, Default, Serialize, Deserialize)]
369pub struct MessagePayload {
370    /// Incremental text fragment
371    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
372    pub text: String,
373    /// `answer` — final answer text; `think` — reasoning process; `process`
374    /// — stage progress description
375    #[serde(
376        default,
377        deserialize_with = "crate::serde_utils::null_as_default",
378        rename = "type"
379    )]
380    pub message_type: String,
381    /// Identifier of the stream segment this fragment belongs to. Fragments
382    /// with the same `key` form one continuous block — group by `key` when
383    /// rendering
384    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
385    pub key: String,
386    /// Time this segment started, Unix timestamp in seconds
387    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
388    pub started_at: i64,
389    /// Stage identifier; only present when `message_type` is `"process"`
390    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
391    pub stage: String,
392    /// Stage title while running; only present when `message_type` is
393    /// `"process"`
394    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
395    pub stage_title: String,
396    /// Stage title after it finishes; only present when `message_type` is
397    /// `"process"`
398    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
399    pub stage_finished_title: String,
400    /// Extra payload attached to the fragment; usually absent
401    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
402    pub outputs: Option<serde_json::Value>,
403}
404
405/// `outputs` of a `workflow_finished` SSE event
406#[derive(Debug, Clone, Default, Serialize, Deserialize)]
407pub struct WorkflowOutputs {
408    /// Final answer text; present when the run succeeded
409    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
410    pub answer: Option<String>,
411    /// Sources referenced by the answer
412    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
413    pub references: Option<Vec<Reference>>,
414    /// Suggested follow-up questions ("you might also ask"); present when the
415    /// run produced them
416    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
417    pub further_questions: Option<Vec<String>>,
418}
419
420/// Payload of a `workflow_finished` SSE event. `status` is never
421/// `interrupted` here — an interrupted run doesn't emit `workflow_finished`
422/// at all; see [`ConversationStreamEvent::HumanInteractionRequired`].
423#[derive(Debug, Clone, Serialize, Deserialize)]
424pub struct WorkflowFinishedPayload {
425    /// Final run status: `succeeded` / `failed` / `stopped`
426    pub status: ConversationStatus,
427    /// Run duration in seconds
428    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
429    pub elapsed_time: f64,
430    /// Run outputs
431    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
432    pub outputs: WorkflowOutputs,
433    /// Localized error description; only present when `status` is `failed`
434    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
435    pub error: String,
436    /// Error code; only present when `status` is `failed`
437    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
438    pub error_code: i32,
439    /// User-facing error message; only present on failure
440    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
441    pub error_message: String,
442    /// Extra error context (e.g. `workflow_run_id`); may be omitted
443    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
444    pub error_args: Option<serde_json::Value>,
445    /// Process stages the run went through; for display only
446    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
447    pub process_data: Vec<serde_json::Value>,
448}
449
450/// `inputs` of a `workflow_started` SSE event
451#[derive(Debug, Clone, Default, Serialize, Deserialize)]
452pub struct WorkflowStartedInputs {
453    /// ID of the owning conversation
454    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
455    pub chat_id: i64,
456    /// Conversation identifier
457    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
458    pub chat_uid: String,
459    /// Message ID of this round (observed as a raw JSON number; accepts a
460    /// string too, see [`ChatStartedPayload::message_id`])
461    #[serde(
462        default,
463        deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string"
464    )]
465    pub message_id: String,
466    /// The question that was asked
467    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
468    pub query: String,
469}
470
471/// Payload of a `workflow_started` SSE event, observed right after
472/// `chat_started`
473#[derive(Debug, Clone, Default, Serialize, Deserialize)]
474pub struct WorkflowStartedPayload {
475    /// Whether this run's answer was served from a cache
476    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
477    pub hit_cache: bool,
478    /// Echoes the run's inputs
479    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
480    pub inputs: WorkflowStartedInputs,
481    /// Unix timestamp in seconds
482    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
483    pub started_at: i64,
484    /// Internal workflow run ID
485    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
486    pub workflow_id: i64,
487}
488
489/// Payload of a `chat_finished` SSE event, observed once all `message` events
490/// for this round have been sent, shortly before `workflow_finished`
491#[derive(Debug, Clone, Default, Serialize, Deserialize)]
492pub struct ChatFinishedPayload {
493    /// ID of the owning conversation
494    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
495    pub chat_id: i64,
496    /// Conversation identifier
497    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
498    pub chat_uid: String,
499    /// Message ID of this round (observed as a raw JSON number; accepts a
500    /// string too, see [`ChatStartedPayload::message_id`])
501    #[serde(
502        default,
503        deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string"
504    )]
505    pub message_id: String,
506    /// Error detail; empty on success
507    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
508    pub error: String,
509    /// User-facing error message; empty on success
510    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
511    pub error_message: String,
512}
513
514/// Payload of a `chat_title_updated` SSE event — the server auto-generates a
515/// short title for the conversation as a UI convenience. Can arrive before
516/// *or* after `workflow_finished`; not tied to the run's outcome.
517#[derive(Debug, Clone, Default, Serialize, Deserialize)]
518pub struct ChatTitleUpdatedPayload {
519    /// ID of the owning conversation
520    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
521    pub chat_id: i64,
522    /// Conversation identifier
523    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
524    pub chat_uid: String,
525    /// Where the title came from, e.g. `"ai_generated"`
526    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
527    pub source: String,
528    /// The new (possibly truncated) title
529    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
530    pub title: String,
531    /// Unix timestamp in seconds
532    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
533    pub updated_at: i64,
534}
535
536/// Payload of a `thinking_started` SSE event — the Agent has entered the
537/// reasoning phase (analyzing the question, planning tool calls). Between
538/// this and [`ConversationStreamEvent::ThinkingFinished`], `Message` events
539/// with `message_type == "think"` and tool-call events may arrive.
540#[derive(Debug, Clone, Default, Serialize, Deserialize)]
541pub struct ThinkingStartedPayload {
542    /// Start time, Unix timestamp in seconds
543    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
544    pub started_at: i64,
545}
546
547/// Payload of a `thinking_finished` SSE event — the reasoning phase is over;
548/// answer text (`Message` with `message_type == "answer"`) follows.
549#[derive(Debug, Clone, Default, Serialize, Deserialize)]
550pub struct ThinkingFinishedPayload {
551    /// Finish time, Unix timestamp in seconds
552    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
553    pub finished_at: i64,
554    /// Reasoning duration in seconds
555    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
556    pub elapsed_time: i32,
557}
558
559/// Payload of a `node_tool_use_started` SSE event — an ordinary tool call has
560/// started. Match it to its `NodeToolUseFinished` counterpart by
561/// `tool_use_id`.
562#[derive(Debug, Clone, Default, Serialize, Deserialize)]
563pub struct NodeToolUseStartedPayload {
564    /// Unique ID of this call; matches the finished event
565    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
566    pub tool_use_id: String,
567    /// Localized display name of the tool
568    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
569    pub tool_name: String,
570    /// Locale-stable tool identifier; use this for logic keyed on the tool
571    /// kind
572    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
573    pub tool_func_name: String,
574    /// Call arguments as a JSON string
575    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
576    pub tool_args: String,
577    /// Progress text suitable for direct display, e.g. `"Searching the
578    /// web…"`
579    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
580    pub tips: String,
581    /// Short tags accompanying `tips`; may be omitted
582    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
583    pub tip_chips: Vec<String>,
584    /// Round number. Calls in the same round (same `iteration`) run in
585    /// parallel
586    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
587    pub iteration: i32,
588    /// Start time, Unix timestamp in seconds
589    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
590    pub started_at: i64,
591}
592
593/// `outputs` of a [`NodeToolUseFinishedPayload`] — only carries fields meant
594/// for display
595#[derive(Debug, Clone, Default, Serialize, Deserialize)]
596pub struct NodeToolUseOutputs {
597    /// Sources referenced by the tool result
598    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
599    pub references: Option<Vec<Reference>>,
600    /// Domains of the referenced sources
601    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
602    pub reference_domains: Option<Vec<String>>,
603    /// The query the tool executed
604    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
605    pub query: Option<String>,
606    /// Raw response text of the tool
607    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
608    pub text: Option<String>,
609    /// Parsed request arguments
610    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
611    pub tool_args: Option<serde_json::Value>,
612    /// Structured result; present only for selected tools
613    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
614    pub data: Option<serde_json::Value>,
615}
616
617/// Payload of a `node_tool_use_finished` SSE event — the tool call has
618/// ended.
619#[derive(Debug, Clone, Default, Serialize, Deserialize)]
620pub struct NodeToolUseFinishedPayload {
621    /// Matches the `tool_use_id` of the started event
622    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
623    pub tool_use_id: String,
624    /// `succeeded` / `failed`
625    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
626    pub status: String,
627    /// Error description on failure
628    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
629    pub error: String,
630    /// Call duration in seconds
631    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
632    pub elapsed_time: f64,
633    /// Start time, Unix timestamp in seconds
634    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
635    pub started_at: i64,
636    /// Localized display name
637    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
638    pub tool_name: String,
639    /// Locale-stable tool identifier
640    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
641    pub tool_func_name: String,
642    /// Call arguments as a JSON string
643    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
644    pub tool_args: String,
645    /// Tool category
646    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
647    pub tool_type: String,
648    /// Progress text
649    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
650    pub tips: String,
651    /// Short tags; may be omitted
652    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
653    pub tip_chips: Vec<String>,
654    /// Round number
655    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
656    pub iteration: i32,
657    /// `true` if the call happened during the thinking phase
658    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
659    pub is_thinking: bool,
660    /// Filtered call results, for display
661    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
662    pub outputs: NodeToolUseOutputs,
663}
664
665/// Payload of a `subagent_started` SSE event. When the Agent spawns a
666/// subagent to work on a sub-task, the subagent's lifecycle is reported with
667/// this dedicated event family instead of `node_tool_use_*`.
668#[derive(Debug, Clone, Default, Serialize, Deserialize)]
669pub struct SubagentStartedPayload {
670    /// ID of the node that spawned the subagent
671    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
672    pub node_id: String,
673    /// Unique ID of this spawn; matches the finished event
674    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
675    pub tool_use_id: String,
676    /// Start time, Unix timestamp in seconds
677    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
678    pub started_at: i64,
679    /// Goal assigned to the subagent
680    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
681    pub goal: String,
682    /// Full task prompt given to the subagent
683    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
684    pub prompt: String,
685    /// Subagent identifier; may be omitted
686    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
687    pub subagent_id: String,
688    /// Tools granted to the subagent; may be omitted
689    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
690    pub tools: Vec<serde_json::Value>,
691}
692
693/// Payload of a `subagent_progress` SSE event, emitted every time the
694/// subagent calls one of its own tools. Use it to render a live timeline
695/// inside the subagent card.
696#[derive(Debug, Clone, Default, Serialize, Deserialize)]
697pub struct SubagentProgressPayload {
698    /// ID of the node that spawned the subagent
699    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
700    pub node_id: String,
701    /// `tool_use_id` of the owning `SubagentStarted` event
702    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
703    pub parent_tool_call_id: String,
704    /// Name of the tool the subagent called
705    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
706    pub subagent_tool_name: String,
707    /// Arguments of that call, as a JSON string
708    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
709    pub subagent_tool_args: String,
710    /// Status of that call: `running` / `succeeded` / `failed`
711    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
712    pub subagent_status: String,
713    /// Duration of that call in milliseconds
714    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
715    pub subagent_duration_ms: i64,
716    /// The subagent's internal round number
717    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
718    pub subagent_iteration: i32,
719    /// Start time, Unix timestamp in seconds
720    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
721    pub started_at: i64,
722}
723
724/// `outputs` of a [`SubagentFinishedPayload`]
725#[derive(Debug, Clone, Default, Serialize, Deserialize)]
726pub struct SubagentOutputs {
727    /// The goal that was assigned to the subagent
728    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
729    pub goal: Option<String>,
730    /// The subagent's result
731    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
732    pub result: Option<String>,
733    /// Timeline of tool calls the subagent made
734    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
735    pub subagent_tools: Option<Vec<serde_json::Value>>,
736}
737
738/// Payload of a `subagent_finished` SSE event
739#[derive(Debug, Clone, Default, Serialize, Deserialize)]
740pub struct SubagentFinishedPayload {
741    /// ID of the node that spawned the subagent
742    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
743    pub node_id: String,
744    /// Matches the `tool_use_id` of `SubagentStarted`
745    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
746    pub tool_use_id: String,
747    /// `succeeded` / `failed`
748    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
749    pub status: String,
750    /// Start time, Unix timestamp in seconds
751    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
752    pub started_at: i64,
753    /// Total subagent duration in seconds
754    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
755    pub elapsed_time: f64,
756    /// Error description on failure
757    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
758    pub error: String,
759    /// Subagent result: `goal`, `result`, and the timeline of tool calls it
760    /// made
761    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
762    pub outputs: SubagentOutputs,
763}
764
765/// Payload of an `agent_tool_started` SSE event. When the Agent delegates to
766/// another Agent as a tool, that inner run is reported with the
767/// `agent_tool_*` family — the shape mirrors the subagent events.
768#[derive(Debug, Clone, Default, Serialize, Deserialize)]
769pub struct AgentToolStartedPayload {
770    /// ID of the calling node
771    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
772    pub node_id: String,
773    /// Unique ID of this call; matches the finished event
774    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
775    pub tool_use_id: String,
776    /// Identifier of the Agent being called
777    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
778    pub agent_tool_name: String,
779    /// Display title; may be omitted
780    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
781    pub title: String,
782    /// Start time, Unix timestamp in seconds
783    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
784    pub started_at: i64,
785    /// Call arguments as a JSON string
786    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
787    pub tool_args: String,
788    /// Localized display name
789    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
790    pub tool_name: String,
791    /// Progress text; may be omitted
792    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
793    pub tips: String,
794    /// Short tags; may be omitted
795    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
796    pub tip_chips: Vec<String>,
797    /// `true` if called during the thinking phase
798    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
799    pub is_thinking: bool,
800}
801
802/// Payload of an `agent_tool_progress` SSE event, emitted for each inner
803/// tool call the delegated Agent makes.
804#[derive(Debug, Clone, Default, Serialize, Deserialize)]
805pub struct AgentToolProgressPayload {
806    /// ID of the calling node
807    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
808    pub node_id: String,
809    /// `tool_use_id` of the owning `AgentToolStarted` event
810    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
811    pub parent_tool_call_id: String,
812    /// Identifier of the Agent being called
813    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
814    pub agent_tool_name: String,
815    /// Name of the inner tool the delegated Agent called
816    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
817    pub inner_tool_name: String,
818    /// Arguments of that inner call, as a JSON string
819    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
820    pub inner_tool_args: String,
821    /// Status of the inner call: `running` / `succeeded` / `failed`
822    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
823    pub status: String,
824    /// Duration of the inner call in milliseconds
825    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
826    pub duration_ms: i64,
827    /// Start time, Unix timestamp in seconds
828    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
829    pub started_at: i64,
830    /// `true` if during the thinking phase
831    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
832    pub is_thinking: bool,
833}
834
835/// Payload of an `agent_tool_finished` SSE event
836#[derive(Debug, Clone, Default, Serialize, Deserialize)]
837pub struct AgentToolFinishedPayload {
838    /// ID of the calling node
839    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
840    pub node_id: String,
841    /// Matches the `tool_use_id` of `AgentToolStarted`
842    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
843    pub tool_use_id: String,
844    /// Identifier of the Agent being called
845    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
846    pub agent_tool_name: String,
847    /// `succeeded` / `failed`
848    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
849    pub status: String,
850    /// Start time, Unix timestamp in seconds
851    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
852    pub started_at: i64,
853    /// Total duration in seconds
854    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
855    pub elapsed_time: f64,
856    /// Error description on failure
857    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
858    pub error: String,
859    /// Call arguments as a JSON string
860    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
861    pub tool_args: String,
862    /// Result of the delegated Agent
863    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
864    pub outputs: Option<serde_json::Value>,
865    /// Tool category
866    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
867    pub tool_type: String,
868    /// Progress text; may be omitted
869    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
870    pub tips: String,
871    /// Short tags; may be omitted
872    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
873    pub tip_chips: Vec<String>,
874    /// `true` if during the thinking phase
875    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
876    pub is_thinking: bool,
877}
878
879/// Payload of a `query_masked` SSE event — sensitive content in the user
880/// query was masked before processing. Display `masked_query` instead of the
881/// original query.
882#[derive(Debug, Clone, Default, Serialize, Deserialize)]
883pub struct QueryMaskedPayload {
884    /// The original user query
885    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
886    pub raw_query: String,
887    /// The masked query
888    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
889    pub masked_query: String,
890}
891
892/// Payload of a `plan_changed` SSE event — the Agent created or updated its
893/// task plan.
894#[derive(Debug, Clone, Default, Serialize, Deserialize)]
895pub struct PlanChangedPayload {
896    /// ID of the planning node
897    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
898    pub node_id: String,
899    /// Time of the change, Unix timestamp in seconds
900    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
901    pub started_at: i64,
902    /// The current plan content
903    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
904    pub outputs: Option<serde_json::Value>,
905    /// Identifies the planning tool. Carried as a top-level sibling of
906    /// `data` in the raw SSE envelope rather than inside `data` itself.
907    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
908    pub tool_name: String,
909}
910
911/// Payload of a `context_compress_started` SSE event, marking the start of a
912/// context-compression pass triggered by a long conversation. Unlike other
913/// events, the timestamp here is an RFC 3339 string.
914#[derive(Debug, Clone, Default, Serialize, Deserialize)]
915pub struct ContextCompressStartedPayload {
916    /// Start time, RFC 3339
917    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
918    pub started_at: String,
919    /// Compression input summary
920    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
921    pub inputs: Option<serde_json::Value>,
922}
923
924/// Payload of a `context_compress_finished` SSE event. Unlike other events,
925/// the timestamp here is an RFC 3339 string.
926#[derive(Debug, Clone, Default, Serialize, Deserialize)]
927pub struct ContextCompressFinishedPayload {
928    /// Finish time, RFC 3339
929    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
930    pub created_at: String,
931    /// Compression input summary
932    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
933    pub inputs: Option<serde_json::Value>,
934    /// Compression result summary
935    #[serde(default, deserialize_with = "crate::serde_utils::null_as_default")]
936    pub outputs: Option<serde_json::Value>,
937}
938
939/// One event observed while streaming
940/// [`crate::AgentContext::conversation_streamed`]
941/// or [`crate::AgentContext::continue_conversation_streamed`].
942///
943/// A run always begins with `ChatStarted` and ends with `ChatFinished`. What
944/// happens in between depends on the outcome:
945///
946/// - Succeeded: `ChatStarted` → `WorkflowStarted` → `ThinkingStarted` →
947///   `Message` (`message_type == "think"`) … → `NodeToolUseStarted` /
948///   `NodeToolUseFinished` … → `ThinkingFinished` → `Message` (`message_type ==
949///   "answer"`) … → `WorkflowFinished` (`status == "succeeded"`) →
950///   `ChatFinished`
951/// - Interrupted (the Agent needs your input; resume via
952///   [`crate::AgentContext::continue_conversation_streamed`]): `ChatStarted` →
953///   `WorkflowStarted` → … → `HumanInteractionRequired` → `ChatFinished`. An
954///   interrupted run does **not** emit `WorkflowFinished`, and resuming it does
955///   **not** emit `WorkflowStarted` again.
956/// - Failed: `ChatStarted` → `WorkflowStarted` → … → `WorkflowFinished`
957///   (`status == "failed"`) → `ChatFinished`
958///
959/// For a plain question-and-answer integration you only need to handle four
960/// variants — everything else is optional progress display: `Message` with
961/// `message_type == "answer"` (append `text` to the answer being displayed),
962/// `HumanInteractionRequired` (show the questions and call
963/// `continue_conversation`/`continue_conversation_streamed` with the
964/// answers), `WorkflowFinished` (read the final outcome), and `ChatFinished`
965/// (the stream is over).
966#[derive(Debug, Clone)]
967pub enum ConversationStreamEvent {
968    /// The run has started
969    ChatStarted(ChatStartedPayload),
970    /// Observed right after `ChatStarted` on every run seen so far, see
971    /// [`WorkflowStartedPayload`]'s docs. Not emitted when resuming an
972    /// interrupted run.
973    WorkflowStarted(WorkflowStartedPayload),
974    /// An incremental piece of the answer
975    Message(MessagePayload),
976    /// A heartbeat with no payload, observed at arbitrary points in the
977    /// stream (including in between `Message` chunks)
978    Ping,
979    /// The Agent has entered the reasoning phase
980    ThinkingStarted(ThinkingStartedPayload),
981    /// The reasoning phase is over
982    ThinkingFinished(ThinkingFinishedPayload),
983    /// An ordinary tool call has started
984    NodeToolUseStarted(NodeToolUseStartedPayload),
985    /// An ordinary tool call has ended
986    NodeToolUseFinished(NodeToolUseFinishedPayload),
987    /// The Agent has spawned a subagent to work on a sub-task
988    SubagentStarted(SubagentStartedPayload),
989    /// The subagent has called one of its own tools
990    SubagentProgress(SubagentProgressPayload),
991    /// The subagent has finished its sub-task
992    SubagentFinished(SubagentFinishedPayload),
993    /// The Agent has delegated to another Agent as a tool
994    AgentToolStarted(AgentToolStartedPayload),
995    /// The delegated Agent has called one of its own tools
996    AgentToolProgress(AgentToolProgressPayload),
997    /// The delegated Agent's run has finished
998    AgentToolFinished(AgentToolFinishedPayload),
999    /// The run is paused: the Agent needs more information or confirmation
1000    /// from you, carrying the interrupt to resume from via
1001    /// [`crate::AgentContext::continue_conversation_streamed`]. Unlike
1002    /// `WorkflowFinished`, this is emitted instead of (never alongside)
1003    /// `WorkflowFinished` for the same run.
1004    HumanInteractionRequired(ConversationResponse),
1005    /// Sensitive content in the user query was masked before processing
1006    QueryMasked(QueryMaskedPayload),
1007    /// The Agent created or updated its task plan
1008    PlanChanged(PlanChangedPayload),
1009    /// A context-compression pass has started (long conversations trigger
1010    /// this)
1011    ContextCompressStarted(ContextCompressStartedPayload),
1012    /// The context-compression pass has finished
1013    ContextCompressFinished(ContextCompressFinishedPayload),
1014    /// Observed once all `Message` events for this round have been sent, see
1015    /// [`ChatFinishedPayload`]'s docs
1016    ChatFinished(ChatFinishedPayload),
1017    /// The run finished successfully, with a failure, or stopped by the
1018    /// user, carrying the run's outcome. Never emitted for an interrupted
1019    /// run — see [`ConversationStreamEvent::HumanInteractionRequired`] for
1020    /// that case. Not necessarily the last event of the stream — the server
1021    /// may still emit a few more housekeeping events (e.g.
1022    /// [`ConversationStreamEvent::ChatTitleUpdated`]) before actually
1023    /// closing the connection.
1024    WorkflowFinished(ConversationResponse),
1025    /// The server auto-generating a short title for the conversation, see
1026    /// [`ChatTitleUpdatedPayload`]'s docs. Can arrive before *or* after
1027    /// [`ConversationStreamEvent::WorkflowFinished`].
1028    ChatTitleUpdated(ChatTitleUpdatedPayload),
1029    /// An event type not recognized by this SDK version, carried as raw JSON
1030    /// so callers aren't broken by future additions to the API. `event` is
1031    /// the SSE envelope's discriminator string, so callers can at least tell
1032    /// these apart instead of getting an opaque blob.
1033    Other {
1034        /// The SSE envelope's `event` field (the event type name)
1035        event: String,
1036        /// The SSE envelope's `data` field
1037        data: serde_json::Value,
1038    },
1039}
1040
1041#[cfg(test)]
1042mod tests {
1043    use super::*;
1044
1045    #[test]
1046    fn workflow_finished_tolerates_null_outputs() {
1047        // A resumed/continued conversation can send `"outputs": null`; that
1048        // must deserialize to the default rather than erroring,
1049        // otherwise the whole event stream fails mid-turn.
1050        let payload: WorkflowFinishedPayload =
1051            serde_json::from_str(r#"{"status":"succeeded","outputs":null}"#).unwrap();
1052        assert!(payload.outputs.answer.is_none());
1053
1054        // A missing key must keep working too.
1055        let payload: WorkflowFinishedPayload =
1056            serde_json::from_str(r#"{"status":"succeeded"}"#).unwrap();
1057        assert!(payload.outputs.answer.is_none());
1058    }
1059
1060    #[test]
1061    fn streamed_vec_fields_tolerate_null_sequences() {
1062        // The server can send an explicit `null` for list-typed fields; that
1063        // must deserialize to an empty list, not error with
1064        // "invalid type: null, expected a sequence" and abort the stream.
1065        let payload: WorkflowFinishedPayload =
1066            serde_json::from_str(r#"{"status":"succeeded","process_data":null}"#).unwrap();
1067        assert!(payload.process_data.is_empty());
1068
1069        let payload: NodeToolUseStartedPayload =
1070            serde_json::from_str(r#"{"tip_chips":null}"#).unwrap();
1071        assert!(payload.tip_chips.is_empty());
1072
1073        let payload: SubagentStartedPayload = serde_json::from_str(r#"{"tools":null}"#).unwrap();
1074        assert!(payload.tools.is_empty());
1075    }
1076
1077    // The `data` payload of the "Run succeeded" example from
1078    // https://open.longbridge.com/en/docs/ai/chat/conversation
1079    const SUCCEEDED_JSON: &str = r#"{
1080        "chat_uid": "ct_9f2c1a5b",
1081        "message_id": "42",
1082        "status": "succeeded",
1083        "answer": "Tesla (TSLA.US) recently...",
1084        "references": [
1085            { "index": 1, "title": "...", "url": "..." }
1086        ],
1087        "elapsed_time": 3.21
1088    }"#;
1089
1090    // The `data` payload of the "Run interrupted" example from the same page.
1091    const INTERRUPTED_JSON: &str = r#"{
1092        "chat_uid": "ct_9f2c1a5b",
1093        "message_id": "43",
1094        "status": "interrupted",
1095        "answer": "",
1096        "references": null,
1097        "elapsed_time": 1.05,
1098        "interrupt": {
1099            "node_id": "n_ask_human",
1100            "tool_call_id": "call_abc123",
1101            "questions": [
1102                {
1103                    "question": "Which time range would you like to check?",
1104                    "options": [
1105                        { "description": "Past week" },
1106                        { "description": "Past month" }
1107                    ],
1108                    "multi_select": false
1109                }
1110            ],
1111            "message_id": 43,
1112            "chat_id": 1001
1113        }
1114    }"#;
1115
1116    #[test]
1117    fn deserialize_succeeded_conversation_response() {
1118        let resp: ConversationResponse = serde_json::from_str(SUCCEEDED_JSON).unwrap();
1119        assert_eq!(resp.chat_uid, "ct_9f2c1a5b");
1120        assert_eq!(resp.message_id, "42");
1121        assert_eq!(resp.status, ConversationStatus::Succeeded);
1122        assert_eq!(resp.answer, "Tesla (TSLA.US) recently...");
1123        assert_eq!(resp.references.as_ref().unwrap().len(), 1);
1124        assert_eq!(resp.references.as_ref().unwrap()[0].index, 1);
1125        assert!((resp.elapsed_time - 3.21).abs() < f64::EPSILON);
1126        assert!(resp.interrupt.is_none());
1127        assert!(resp.error.is_none());
1128    }
1129
1130    #[test]
1131    fn deserialize_interrupted_conversation_response() {
1132        let resp: ConversationResponse = serde_json::from_str(INTERRUPTED_JSON).unwrap();
1133        assert_eq!(resp.status, ConversationStatus::Interrupted);
1134        let interrupt = resp.interrupt.expect("interrupt");
1135        assert_eq!(interrupt.node_id, "n_ask_human");
1136        assert_eq!(interrupt.tool_call_id, "call_abc123");
1137        assert_eq!(interrupt.message_id, 43);
1138        assert_eq!(interrupt.chat_id, 1001);
1139        assert_eq!(interrupt.questions.len(), 1);
1140        assert_eq!(interrupt.questions[0].options.len(), 2);
1141        assert!(!interrupt.questions[0].multi_select);
1142    }
1143
1144    #[test]
1145    fn deserialize_chat_started_payload_with_numeric_message_id() {
1146        // The SSE example's `chat_started` event encodes `message_id` as a raw
1147        // JSON number, unlike the blocking response's quoted string.
1148        let json = r#"{"chat_uid":"ct_9f2c1a5b","message_id":42}"#;
1149        let payload: ChatStartedPayload = serde_json::from_str(json).unwrap();
1150        assert_eq!(payload.chat_uid, "ct_9f2c1a5b");
1151        assert_eq!(payload.message_id, "42");
1152    }
1153
1154    #[test]
1155    fn deserialize_message_payload() {
1156        let json = r#"{"text":"Tesla"}"#;
1157        let payload: MessagePayload = serde_json::from_str(json).unwrap();
1158        assert_eq!(payload.text, "Tesla");
1159    }
1160
1161    #[test]
1162    fn deserialize_message_payload_with_full_fields() {
1163        // https://github.com/longbridge/developers/pull/1176
1164        let json =
1165            r#"{"text":"Tesla","type":"answer","key":"n_llm_1:answer","started_at":1752048000}"#;
1166        let payload: MessagePayload = serde_json::from_str(json).unwrap();
1167        assert_eq!(payload.text, "Tesla");
1168        assert_eq!(payload.message_type, "answer");
1169        assert_eq!(payload.key, "n_llm_1:answer");
1170        assert_eq!(payload.started_at, 1752048000);
1171    }
1172
1173    #[test]
1174    fn deserialize_workflow_finished_payload() {
1175        let json = r#"{"status":"succeeded","elapsed_time":3.21,"outputs":{"answer":"Tesla (TSLA.US) recently...","further_questions":["What is Tesla's P/E?","How did Q3 deliveries look?"]}}"#;
1176        let payload: WorkflowFinishedPayload = serde_json::from_str(json).unwrap();
1177        assert_eq!(payload.status, ConversationStatus::Succeeded);
1178        assert!((payload.elapsed_time - 3.21).abs() < f64::EPSILON);
1179        assert_eq!(
1180            payload.outputs.answer.as_deref(),
1181            Some("Tesla (TSLA.US) recently...")
1182        );
1183        assert_eq!(
1184            payload.outputs.further_questions.as_deref(),
1185            Some(
1186                [
1187                    "What is Tesla's P/E?".to_string(),
1188                    "How did Q3 deliveries look?".to_string(),
1189                ]
1190                .as_slice()
1191            )
1192        );
1193
1194        let resp = ConversationResponse::from_stream_parts(
1195            Some(("ct_9f2c1a5b".to_string(), "42".to_string())),
1196            payload,
1197        );
1198        assert_eq!(resp.chat_uid, "ct_9f2c1a5b");
1199        assert_eq!(resp.message_id, "42");
1200        assert_eq!(resp.answer, "Tesla (TSLA.US) recently...");
1201        // Follow-up questions thread through the folded response.
1202        assert_eq!(resp.further_questions.as_ref().unwrap().len(), 2);
1203        assert!(resp.interrupt.is_none());
1204        assert!(resp.error.is_none());
1205    }
1206
1207    #[test]
1208    fn deserialize_workflow_finished_payload_with_failure() {
1209        // Error info is top-level on the event, not nested under `outputs`
1210        // (unlike the blocking response's `ConversationResponse.error`).
1211        let json = r#"{"status":"failed","elapsed_time":0.8,"error":"upstream timeout","error_code":500,"error_message":"Something went wrong, please try again"}"#;
1212        let payload: WorkflowFinishedPayload = serde_json::from_str(json).unwrap();
1213        assert_eq!(payload.status, ConversationStatus::Failed);
1214        assert_eq!(payload.error, "upstream timeout");
1215        assert_eq!(payload.error_code, 500);
1216        assert_eq!(
1217            payload.error_message,
1218            "Something went wrong, please try again"
1219        );
1220
1221        let resp = ConversationResponse::from_stream_parts(None, payload);
1222        assert_eq!(resp.status, ConversationStatus::Failed);
1223        let error = resp.error.expect("error");
1224        assert_eq!(error.code, 500);
1225        assert_eq!(error.message, "Something went wrong, please try again");
1226    }
1227
1228    #[test]
1229    fn conversation_response_from_stream_interrupt() {
1230        // An interrupted run never emits `workflow_finished` — the
1231        // `human_interaction_required` event is the terminal one instead,
1232        // and carries an `Interrupt` shaped identically to the blocking
1233        // response's `interrupt` field.
1234        let json = r#"{"node_id":"n_ask_human","tool_call_id":"call_abc123","questions":[{"question":"Which time range would you like to check?","options":[{"description":"Past week"},{"description":"Past month"}],"multi_select":false}],"message_id":43,"chat_id":1001}"#;
1235        let interrupt: Interrupt = serde_json::from_str(json).unwrap();
1236
1237        let resp = ConversationResponse::from_stream_interrupt(
1238            Some(("ct_9f2c1a5b".to_string(), "43".to_string())),
1239            interrupt,
1240        );
1241        assert_eq!(resp.chat_uid, "ct_9f2c1a5b");
1242        assert_eq!(resp.message_id, "43");
1243        assert_eq!(resp.status, ConversationStatus::Interrupted);
1244        let interrupt = resp.interrupt.expect("interrupt");
1245        assert_eq!(interrupt.node_id, "n_ask_human");
1246        assert_eq!(interrupt.questions.len(), 1);
1247    }
1248
1249    #[test]
1250    fn deserialize_node_tool_use_finished_payload() {
1251        let json = r#"{"tool_use_id":"call_abc123","status":"succeeded","elapsed_time":1.42,"tool_name":"Web Search","tool_func_name":"web_search","tool_args":"{\"query\":\"TSLA stock news\"}","tool_type":"builtin","tips":"Searched the web","iteration":1,"is_thinking":true,"outputs":{"query":"TSLA stock news","references":[{"index":1,"title":"...","url":"..."}]}}"#;
1252        let payload: NodeToolUseFinishedPayload = serde_json::from_str(json).unwrap();
1253        assert_eq!(payload.tool_use_id, "call_abc123");
1254        assert_eq!(payload.status, "succeeded");
1255        assert_eq!(payload.tool_func_name, "web_search");
1256        assert!(payload.is_thinking);
1257        assert_eq!(payload.outputs.query.as_deref(), Some("TSLA stock news"));
1258        assert_eq!(payload.outputs.references.as_ref().unwrap().len(), 1);
1259    }
1260
1261    #[test]
1262    fn deserialize_reference_with_nested_content() {
1263        // The real wire reference nests the human-readable fields under
1264        // `content` and carries `type`/`id`/`original_index` at the top
1265        // level; only `index` overlaps the old flat shape.
1266        let json = r#"{"type":"NewsArticle","id":"295354885","index":1,"original_index":10,"content":{"source":"智通财经","description":"Jefferies cut Tesla's target.","published_at":"2026-08-10T03:45:02Z","source_url":"https://example.com/a","title":""}}"#;
1267        let r: Reference = serde_json::from_str(json).unwrap();
1268        assert_eq!(r.index, 1);
1269        assert_eq!(r.original_index, 10);
1270        assert_eq!(r.ref_type, "NewsArticle");
1271        assert_eq!(r.id, "295354885");
1272        let content = r.content.expect("content");
1273        assert_eq!(content["source"], "智通财经");
1274        assert_eq!(content["published_at"], "2026-08-10T03:45:02Z");
1275    }
1276
1277    #[test]
1278    fn deserialize_reference_flat_shape_still_works() {
1279        // The docs' example uses a flat {index,title,url}; new fields default.
1280        let r: Reference = serde_json::from_str(r#"{"index":1,"title":"t","url":"u"}"#).unwrap();
1281        assert_eq!(r.index, 1);
1282        assert_eq!(r.title, "t");
1283        assert_eq!(r.url, "u");
1284        assert!(r.content.is_none());
1285        assert_eq!(r.ref_type, "");
1286    }
1287
1288    #[test]
1289    fn deserialize_plan_changed_payload_picks_up_sibling_tool_name() {
1290        let mut payload: PlanChangedPayload =
1291            serde_json::from_str(r#"{"node_id":"n_plan","started_at":1752048000}"#).unwrap();
1292        // `tool_name` lives outside `data` in the raw envelope; simulated
1293        // here the same way `map_conversation_event` fills it in.
1294        payload.tool_name = "planner".to_string();
1295        assert_eq!(payload.node_id, "n_plan");
1296        assert_eq!(payload.tool_name, "planner");
1297    }
1298
1299    #[test]
1300    fn deserialize_workspaces_response() {
1301        let json = r#"{
1302            "workspaces": [
1303                { "id": "1001", "name": "My Workspace", "created_at": 1742000000, "updated_at": 1742001000 }
1304            ]
1305        }"#;
1306        let resp: WorkspacesResponse = serde_json::from_str(json).unwrap();
1307        assert_eq!(resp.workspaces.len(), 1);
1308        assert_eq!(resp.workspaces[0].id, "1001");
1309    }
1310
1311    #[test]
1312    fn deserialize_agents_response() {
1313        let json = r#"{
1314            "agents": [
1315                {
1316                    "uid": "ag_7d3f9b2c",
1317                    "name": "US Stock Analyst",
1318                    "description": "Answers US stock questions with market and fundamental data",
1319                    "mode": "chat",
1320                    "icon": "https://cdn.longbridge.com/icons/agent.png",
1321                    "is_published": true,
1322                    "published_at": 1742000000,
1323                    "created_at": 1741000000,
1324                    "updated_at": 1742001000
1325                }
1326            ],
1327            "total": 12
1328        }"#;
1329        let resp: AgentsResponse = serde_json::from_str(json).unwrap();
1330        assert_eq!(resp.total, 12);
1331        assert_eq!(resp.agents[0].uid, "ag_7d3f9b2c");
1332        assert!(resp.agents[0].is_published);
1333    }
1334
1335    #[test]
1336    fn deserialize_interrupt_treats_null_questions_as_empty() {
1337        let interrupt: Interrupt = serde_json::from_str(
1338            r#"{"node_id":"approval","tool_call_id":"call-1","questions":null}"#,
1339        )
1340        .unwrap();
1341        assert!(interrupt.questions.is_empty());
1342    }
1343
1344    #[test]
1345    fn deserialize_question_treats_null_options_as_empty() {
1346        let question: Question =
1347            serde_json::from_str(r#"{"question":"Continue?","options":null}"#).unwrap();
1348        assert!(question.options.is_empty());
1349    }
1350
1351    #[test]
1352    fn deserialize_human_interaction_preserves_labels_and_answer_keys() {
1353        let interrupt: Interrupt = serde_json::from_str(
1354            r#"{
1355            "node_id":"ask",
1356            "tool_call_id":"call-1",
1357            "questions":[],
1358            "interactions":[{
1359                "tool_call_id":"call-1",
1360                "interrupt_id":"call-1",
1361                "type":"ask_human",
1362                "tool_name":"AskHuman",
1363                "questions":[{
1364                    "question":"从哪个方向开始?",
1365                    "options":[{"label":"看行情","description":"比较当前价格"}],
1366                    "multi_select":false
1367                }],
1368                "tool_args":{}
1369            }]
1370        }"#,
1371        )
1372        .unwrap();
1373        assert_eq!(interrupt.interactions[0].interrupt_id, "call-1");
1374        assert_eq!(
1375            interrupt.interactions[0].questions[0].options[0].label,
1376            "看行情"
1377        );
1378        assert_eq!(
1379            interrupt.interactions[0].questions[0].options[0].description,
1380            "比较当前价格"
1381        );
1382    }
1383}