From a31ab78baced0578bb784be564147d6c05df94bc Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 6 May 2026 17:15:30 -0700 Subject: [PATCH] fix(agent-sdk): translate snake_case SSE wire shape and recognise run_finished/cancelled/error as terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend SSE frames are {seq, event_type, payload} but the SDK was reading obj.type / obj.data without any snake-to-camel transform — all events were being dropped. Apply the same snakeToCamel walk used for JSON responses, then map event_type/payload to type/data so the public RunEvent shape stays stable. Also extend the terminal-event check so the stream returns on the actual backend terminal types (run_finished | run_cancelled | run_error) instead of hanging until the keep-alive timeout. --- sdks/typescript-agent-sdk/src/client/sse.ts | 33 +++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/sdks/typescript-agent-sdk/src/client/sse.ts b/sdks/typescript-agent-sdk/src/client/sse.ts index 5dd73af6926..383a6a15a2f 100644 --- a/sdks/typescript-agent-sdk/src/client/sse.ts +++ b/sdks/typescript-agent-sdk/src/client/sse.ts @@ -7,7 +7,7 @@ import { createParser, type EventSourceMessage } from "eventsource-parser"; import { LiteLLMAgentError, type RunEvent } from "../types.js"; -import { request, type ResolvedClient } from "./http.js"; +import { request, snakeToCamel, type ResolvedClient } from "./http.js"; export interface StreamRunOptions { startingSeq?: number; @@ -76,7 +76,17 @@ export async function* streamRunEvents( lastSeq = parsed.seq; sawProgressOnThisConnection = true; yield parsed; - if (parsed.type === "done" || parsed.type === "error") { + // Terminal event types — the backend emits ``run_finished``, + // ``run_cancelled``, and ``run_error`` to mark the end of a run. + // ``done``/``error`` are kept for backwards-compat with older + // wire shapes used in early SDK tests. + if ( + parsed.type === "done" || + parsed.type === "error" || + parsed.type === "run_finished" || + parsed.type === "run_cancelled" || + parsed.type === "run_error" + ) { return; } } @@ -108,15 +118,20 @@ export async function* streamRunEvents( function decodeEvent(msg: EventSourceMessage): RunEvent | null { if (!msg.data) return null; try { - const obj = JSON.parse(msg.data) as Partial; - if (typeof obj.seq !== "number" || typeof obj.type !== "string") { + // Backend SSE wire shape is ``{seq, event_type, payload}`` (snake_case + // keys, ``event_type``/``payload`` instead of ``type``/``data``). Apply + // the same snake_to_camel transform we use for JSON responses, then + // also map the legacy event_type/payload field names so the public + // ``RunEvent`` interface stays stable. + const raw = JSON.parse(msg.data) as Record; + const camel = snakeToCamel(raw) as Record; + const seq = camel.seq; + const type = (camel.type ?? camel.eventType) as string | undefined; + const data = (camel.data ?? camel.payload) ?? null; + if (typeof seq !== "number" || typeof type !== "string") { return null; } - return { - seq: obj.seq, - type: obj.type, - data: obj.data ?? null, - }; + return { seq, type, data }; } catch { return null; }