fix(agent-sdk): translate snake_case SSE wire shape and recognise run_finished/cancelled/error as terminal

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.
This commit is contained in:
Ishaan Jaffer 2026-05-06 17:15:30 -07:00
parent 2bf50108fe
commit a31ab78bac
No known key found for this signature in database

View file

@ -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<RunEvent>;
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<string, unknown>;
const camel = snakeToCamel(raw) as Record<string, unknown>;
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;
}