Skip to main content

longbridge/agent/
types.rs

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