From ac2fd14da36c266b63efbea50140bb1856b9579e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Tue, 11 Aug 2026 19:17:30 +0800 Subject: [PATCH 1/2] feat(agent): capture full Reference content and ChatStartedPayload fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audited the agent SSE wire against the SDK types and closed the gaps where the SDK dropped data the server actually sends: - Reference: the real payload nests the human-readable fields under a `content` object (source, description, published_at, source_url, source_logo, kind, …) and carries `type`/`id`/`original_index` at the top level. The SDK modeled only a flat {index,title,url}, so title/url came back empty for real references and everything else was lost. Add original_index, ref_type (wire "type"), id, and content (raw JSON, since the field set varies by ref_type). This fixes references everywhere they appear: ConversationResponse.references, and the message / node_tool_use_finished / workflow_finished outputs. - ChatStartedPayload: add chat_id / error / error_message (present on the wire, mirroring ChatFinishedPayload). Mirrored across every binding (Python, Node.js, Java, C); the C header is regenerated by cbindgen. content is surfaced as raw JSON — a serde_json::Value in Rust/Python/Node, a JSON string in C (content_json) and Java (getContent), following the existing NodeToolUseOutputs.data precedent. --- c/csrc/include/longbridge.h | 28 +++++++++ c/src/agent_context/types.rs | 61 ++++++++++++++++++- .../longbridge/agent/ChatStartedEvent.java | 33 +++++++++- .../java/com/longbridge/agent/Reference.java | 52 +++++++++++++++- java/src/types/classes.rs | 4 +- nodejs/index.d.ts | 14 +++++ nodejs/src/agent/types.rs | 22 +++++++ python/pysrc/longbridge/openapi.pyi | 15 +++++ python/src/agent/types.rs | 22 +++++++ rust/src/agent/stream.rs | 3 + rust/src/agent/types.rs | 56 ++++++++++++++++- 11 files changed, 300 insertions(+), 10 deletions(-) diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index b62e7183b..627fc9c0c 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -1915,10 +1915,22 @@ typedef struct lb_chat_started_payload_t { * Conversation identifier */ const char *chat_uid; + /** + * Numeric conversation identifier + */ + int64_t chat_id; /** * Message ID of this round */ const char *message_id; + /** + * Error code; empty when absent + */ + const char *error; + /** + * Error message; empty when absent + */ + const char *error_message; } lb_chat_started_payload_t; /** @@ -2096,6 +2108,18 @@ typedef struct lb_reference_t { * Reference index */ int32_t index; + /** + * Original reference index as provided by the source + */ + int32_t original_index; + /** + * Reference type (wire field `type`) + */ + const char *ref_type; + /** + * Reference identifier + */ + const char *id; /** * Reference title */ @@ -2104,6 +2128,10 @@ typedef struct lb_reference_t { * Reference URL */ const char *url; + /** + * Full nested reference payload, as a JSON string; empty when absent + */ + const char *content_json; } lb_reference_t; /** diff --git a/c/src/agent_context/types.rs b/c/src/agent_context/types.rs index 53fe8b157..a1bbe246b 100644 --- a/c/src/agent_context/types.rs +++ b/c/src/agent_context/types.rs @@ -253,26 +253,53 @@ pub struct CGetAgentsOptions { pub struct CReference { /// Reference index pub index: i32, + /// Original reference index as provided by the source + pub original_index: i32, + /// Reference type (wire field `type`) + pub ref_type: *const c_char, + /// Reference identifier + pub id: *const c_char, /// Reference title pub title: *const c_char, /// Reference URL pub url: *const c_char, + /// Full nested reference payload, as a JSON string; empty when absent + pub content_json: *const c_char, } #[derive(Debug)] pub(crate) struct CReferenceOwned { index: i32, + original_index: i32, + ref_type: CString, + id: CString, title: CString, url: CString, + content_json: CString, } impl From for CReferenceOwned { fn from(v: Reference) -> Self { - let Reference { index, title, url } = v; + let Reference { + index, + original_index, + ref_type, + id, + title, + url, + content, + } = v; Self { index, + original_index, + ref_type: ref_type.into(), + id: id.into(), title: title.into(), url: url.into(), + content_json: content + .map(|v| serde_json::to_string(&v).unwrap_or_default()) + .unwrap_or_default() + .into(), } } } @@ -281,11 +308,23 @@ impl ToFFI for CReferenceOwned { type FFIType = CReference; fn to_ffi_type(&self) -> Self::FFIType { - let CReferenceOwned { index, title, url } = self; + let CReferenceOwned { + index, + original_index, + ref_type, + id, + title, + url, + content_json, + } = self; CReference { index: *index, + original_index: *original_index, + ref_type: ref_type.to_ffi_type(), + id: id.to_ffi_type(), title: title.to_ffi_type(), url: url.to_ffi_type(), + content_json: content_json.to_ffi_type(), } } } @@ -593,25 +632,40 @@ impl ToFFI for CConversationResponseOwned { pub struct CChatStartedPayload { /// Conversation identifier pub chat_uid: *const c_char, + /// Numeric conversation identifier + pub chat_id: i64, /// Message ID of this round pub message_id: *const c_char, + /// Error code; empty when absent + pub error: *const c_char, + /// Error message; empty when absent + pub error_message: *const c_char, } #[derive(Debug)] pub(crate) struct CChatStartedPayloadOwned { chat_uid: CString, + chat_id: i64, message_id: CString, + error: CString, + error_message: CString, } impl From for CChatStartedPayloadOwned { fn from(v: ChatStartedPayload) -> Self { let ChatStartedPayload { chat_uid, + chat_id, message_id, + error, + error_message, } = v; Self { chat_uid: chat_uid.into(), + chat_id, message_id: message_id.into(), + error: error.into(), + error_message: error_message.into(), } } } @@ -622,7 +676,10 @@ impl ToFFI for CChatStartedPayloadOwned { fn to_ffi_type(&self) -> Self::FFIType { CChatStartedPayload { chat_uid: self.chat_uid.to_ffi_type(), + chat_id: self.chat_id, message_id: self.message_id.to_ffi_type(), + error: self.error.to_ffi_type(), + error_message: self.error_message.to_ffi_type(), } } } diff --git a/java/javasrc/src/main/java/com/longbridge/agent/ChatStartedEvent.java b/java/javasrc/src/main/java/com/longbridge/agent/ChatStartedEvent.java index 896d4d06d..babe43ac8 100644 --- a/java/javasrc/src/main/java/com/longbridge/agent/ChatStartedEvent.java +++ b/java/javasrc/src/main/java/com/longbridge/agent/ChatStartedEvent.java @@ -6,6 +6,9 @@ public final class ChatStartedEvent extends ConversationStreamEvent { private String chatUid; private String messageId; + private long chatId; + private String error; + private String errorMessage; /** * Returns the conversation identifier. @@ -25,8 +28,36 @@ public String getMessageId() { return messageId; } + /** + * Returns the ID of the owning conversation. + * + * @return owning conversation ID + */ + public long getChatId() { + return chatId; + } + + /** + * Returns the error detail; empty at start. + * + * @return error detail + */ + public String getError() { + return error; + } + + /** + * Returns the user-facing error message; empty at start. + * + * @return user-facing error message + */ + public String getErrorMessage() { + return errorMessage; + } + @Override public String toString() { - return "ChatStartedEvent [chatUid=" + chatUid + ", messageId=" + messageId + "]"; + return "ChatStartedEvent [chatUid=" + chatUid + ", messageId=" + messageId + ", chatId=" + chatId + + ", error=" + error + ", errorMessage=" + errorMessage + "]"; } } diff --git a/java/javasrc/src/main/java/com/longbridge/agent/Reference.java b/java/javasrc/src/main/java/com/longbridge/agent/Reference.java index 35ca58a57..5a89df314 100644 --- a/java/javasrc/src/main/java/com/longbridge/agent/Reference.java +++ b/java/javasrc/src/main/java/com/longbridge/agent/Reference.java @@ -5,8 +5,12 @@ */ public class Reference { private int index; + private int originalIndex; + private String refType; + private String id; private String title; private String url; + private String content; /** * Returns the reference index. @@ -18,7 +22,35 @@ public int getIndex() { } /** - * Returns the reference title. + * Returns the original index in the source list, before any reranking. + * + * @return original reference index + */ + public int getOriginalIndex() { + return originalIndex; + } + + /** + * Returns the reference kind, e.g. {@code "NewsArticle"}. + * + * @return reference kind + */ + public String getRefType() { + return refType; + } + + /** + * Returns the reference id. + * + * @return reference id + */ + public String getId() { + return id; + } + + /** + * Returns the reference title. Often empty at the top level — the + * human-readable title usually lives in {@link #getContent}. * * @return reference title */ @@ -27,7 +59,8 @@ public String getTitle() { } /** - * Returns the reference URL. + * Returns the reference URL. Often empty at the top level — see + * {@link #getContent}. * * @return reference URL */ @@ -35,8 +68,21 @@ public String getUrl() { return url; } + /** + * Returns the full reference payload as sent by the server ({@code + * source}, {@code description}, {@code published_at}, {@code + * source_url}, {@code source_logo}, {@code kind}, …), as JSON text. Kept + * as raw JSON because the field set varies by {@link #getRefType}. + * + * @return full reference payload (JSON text), or {@code null} + */ + public String getContent() { + return content; + } + @Override public String toString() { - return "Reference [index=" + index + ", title=" + title + ", url=" + url + "]"; + return "Reference [index=" + index + ", originalIndex=" + originalIndex + ", refType=" + refType + ", id=" + + id + ", title=" + title + ", url=" + url + ", content=" + content + "]"; } } diff --git a/java/src/types/classes.rs b/java/src/types/classes.rs index 522a485ab..389c37632 100644 --- a/java/src/types/classes.rs +++ b/java/src/types/classes.rs @@ -2953,7 +2953,7 @@ impl_java_class!( impl_java_class!( "com/longbridge/agent/Reference", longbridge::agent::Reference, - [index, title, url] + [index, original_index, ref_type, id, title, url, content] ); impl_java_class!( @@ -3000,7 +3000,7 @@ impl_java_class!( impl_java_class!( "com/longbridge/agent/ChatStartedEvent", longbridge::agent::ChatStartedPayload, - [chat_uid, message_id] + [chat_uid, message_id, chat_id, error, error_message] ); // JNI-side view of `longbridge::agent::WorkflowStartedInputs`, the `inputs` diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index f66ef7129..74e718c93 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -3849,6 +3849,12 @@ export interface ChatStartedPayload { chatUid: string /** Message ID of this round */ messageId: string + /** ID of the owning conversation */ + chatId: number + /** Error detail; empty at start */ + error: string + /** User-facing error message; empty at start */ + errorMessage: string } /** @@ -5998,10 +6004,18 @@ export interface RecentBuybacks { export interface Reference { /** Reference index */ index: number + /** Original index in the source list, before any reranking */ + originalIndex: number + /** Reference kind, e.g. `"NewsArticle"` */ + refType: string + /** Reference id */ + id: string /** Reference title */ title: string /** Reference URL */ url: string + /** Full reference payload as sent by the server; kept as raw JSON because the field set varies by reference `refType` */ + content?: any } /** Parameters for replacing an attached order */ diff --git a/nodejs/src/agent/types.rs b/nodejs/src/agent/types.rs index d67b07b08..2efbd3b4e 100644 --- a/nodejs/src/agent/types.rs +++ b/nodejs/src/agent/types.rs @@ -126,17 +126,30 @@ impl From for ConversationStatus { pub struct Reference { /// Reference index pub index: i32, + /// Original index in the source list, before any reranking + pub original_index: i32, + /// Reference kind, e.g. `"NewsArticle"` + pub ref_type: String, + /// Reference id + pub id: String, /// Reference title pub title: String, /// Reference URL pub url: String, + /// Full reference payload as sent by the server; kept as raw JSON + /// because the field set varies by reference `ref_type` + pub content: Option, } impl From for Reference { fn from(v: lb::Reference) -> Self { Self { index: v.index, + original_index: v.original_index, + ref_type: v.ref_type, + id: v.id, title: v.title, url: v.url, + content: v.content, } } } @@ -275,12 +288,21 @@ pub struct ChatStartedPayload { pub chat_uid: String, /// Message ID of this round pub message_id: String, + /// ID of the owning conversation + pub chat_id: i64, + /// Error detail; empty at start + pub error: String, + /// User-facing error message; empty at start + pub error_message: String, } impl From for ChatStartedPayload { fn from(v: lb::ChatStartedPayload) -> Self { Self { chat_uid: v.chat_uid, message_id: v.message_id, + chat_id: v.chat_id, + error: v.error, + error_message: v.error_message, } } } diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index 70e5d1353..ccf0138a4 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -13420,10 +13420,19 @@ class Reference: index: int """Reference index""" + original_index: int + """Original index in the source list, before any reranking""" + ref_type: str + """Reference kind, e.g. ``"NewsArticle"``""" + id: str + """Reference id""" title: str """Reference title""" url: str """Reference URL""" + content: Any | None + """Full reference payload as sent by the server. Kept as raw JSON + because the field set varies by reference ``ref_type``""" class QuestionOption: """One option of a Question.""" @@ -13497,6 +13506,12 @@ class ChatStartedPayload: """Conversation identifier""" message_id: str """Message ID of this round""" + chat_id: int + """ID of the owning conversation""" + error: str + """Error detail; empty at start""" + error_message: str + """User-facing error message; empty at start""" class MessagePayload: """ diff --git a/python/src/agent/types.rs b/python/src/agent/types.rs index 0f0774c45..b054bf3a8 100644 --- a/python/src/agent/types.rs +++ b/python/src/agent/types.rs @@ -118,16 +118,29 @@ pub(crate) enum ConversationStatus { #[derive(Debug, Clone)] pub(crate) struct Reference { pub index: i32, + /// Original index in the source list, before any reranking + pub original_index: i32, + /// Reference kind, e.g. `"NewsArticle"` + pub ref_type: String, + /// Reference id + pub id: String, pub title: String, pub url: String, + /// Full reference payload as sent by the server. Kept as raw JSON + /// because the field set varies by reference `ref_type`. + pub content: Option, } impl From for Reference { fn from(v: longbridge::agent::Reference) -> Self { Self { index: v.index, + original_index: v.original_index, + ref_type: v.ref_type, + id: v.id, title: v.title, url: v.url, + content: v.content.map(JsonValue), } } } @@ -249,6 +262,12 @@ impl From for ConversationResponse { pub(crate) struct ChatStartedPayload { pub chat_uid: String, pub message_id: String, + /// ID of the owning conversation + pub chat_id: i64, + /// Error detail; empty at start + pub error: String, + /// User-facing error message; empty at start + pub error_message: String, } impl From for ChatStartedPayload { @@ -256,6 +275,9 @@ impl From for ChatStartedPayload { Self { chat_uid: v.chat_uid, message_id: v.message_id, + chat_id: v.chat_id, + error: v.error, + error_message: v.error_message, } } } diff --git a/rust/src/agent/stream.rs b/rust/src/agent/stream.rs index 810549288..65fe78a46 100644 --- a/rust/src/agent/stream.rs +++ b/rust/src/agent/stream.rs @@ -181,6 +181,9 @@ mod tests { Ok(ConversationStreamEvent::ChatStarted(ChatStartedPayload { chat_uid: "ct_1".to_string(), message_id: "1".to_string(), + chat_id: 0, + error: String::new(), + error_message: String::new(), })), Ok(ConversationStreamEvent::HumanInteractionRequired( interrupt_resp, diff --git a/rust/src/agent/types.rs b/rust/src/agent/types.rs index 6e4224952..576824b9d 100644 --- a/rust/src/agent/types.rs +++ b/rust/src/agent/types.rs @@ -143,12 +143,28 @@ pub struct Reference { /// Reference index #[serde(default)] pub index: i32, - /// Reference title + /// Original index in the source list, before any reranking + #[serde(default)] + pub original_index: i32, + /// Reference kind, e.g. `"NewsArticle"` + #[serde(default, rename = "type")] + pub ref_type: String, + /// Reference id + #[serde(default)] + pub id: String, + /// Reference title. Often empty at the top level — the human-readable + /// title usually lives in [`content`](Self::content). #[serde(default)] pub title: String, - /// Reference URL + /// Reference URL. Often empty at the top level — see + /// [`content`](Self::content). #[serde(default)] pub url: String, + /// Full reference payload as sent by the server (`source`, `description`, + /// `published_at`, `source_url`, `source_logo`, `kind`, …). Kept as raw + /// JSON because the field set varies by reference [`ref_type`](Self::ref_type). + #[serde(default)] + pub content: Option, } /// One question the Agent needs you to answer @@ -300,6 +316,15 @@ pub struct ChatStartedPayload { /// which is a quoted string) — accept either. #[serde(deserialize_with = "crate::serde_utils::deserialize_string_or_int_as_string")] pub message_id: String, + /// ID of the owning conversation + #[serde(default)] + pub chat_id: i64, + /// Error detail; empty at start + #[serde(default)] + pub error: String, + /// User-facing error message; empty at start + #[serde(default)] + pub error_message: String, } /// Payload of a `message` SSE event — an incremental text chunk. This is the @@ -1161,6 +1186,33 @@ mod tests { assert_eq!(payload.outputs.references.as_ref().unwrap().len(), 1); } + #[test] + fn deserialize_reference_with_nested_content() { + // The real wire reference nests the human-readable fields under + // `content` and carries `type`/`id`/`original_index` at the top + // level; only `index` overlaps the old flat shape. + 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":""}}"#; + let r: Reference = serde_json::from_str(json).unwrap(); + assert_eq!(r.index, 1); + assert_eq!(r.original_index, 10); + assert_eq!(r.ref_type, "NewsArticle"); + assert_eq!(r.id, "295354885"); + let content = r.content.expect("content"); + assert_eq!(content["source"], "智通财经"); + assert_eq!(content["published_at"], "2026-08-10T03:45:02Z"); + } + + #[test] + fn deserialize_reference_flat_shape_still_works() { + // The docs' example uses a flat {index,title,url}; new fields default. + let r: Reference = serde_json::from_str(r#"{"index":1,"title":"t","url":"u"}"#).unwrap(); + assert_eq!(r.index, 1); + assert_eq!(r.title, "t"); + assert_eq!(r.url, "u"); + assert!(r.content.is_none()); + assert_eq!(r.ref_type, ""); + } + #[test] fn deserialize_plan_changed_payload_picks_up_sibling_tool_name() { let mut payload: PlanChangedPayload = From 2a72feed864a80ee5c1daf211dce9be770603343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E7=AB=A0=E6=B4=AA?= Date: Wed, 12 Aug 2026 09:50:19 +0800 Subject: [PATCH 2/2] chore: rustfmt (wrap long doc comment) --- rust/src/agent/types.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/src/agent/types.rs b/rust/src/agent/types.rs index 576824b9d..be06e3f17 100644 --- a/rust/src/agent/types.rs +++ b/rust/src/agent/types.rs @@ -162,7 +162,8 @@ pub struct Reference { pub url: String, /// Full reference payload as sent by the server (`source`, `description`, /// `published_at`, `source_url`, `source_logo`, `kind`, …). Kept as raw - /// JSON because the field set varies by reference [`ref_type`](Self::ref_type). + /// JSON because the field set varies by reference + /// [`ref_type`](Self::ref_type). #[serde(default)] pub content: Option, }