fix: render Ask Fabro session responses

Handle the flattened session SSE event envelopes emitted by the API so assistant deltas are applied to the Ask Fabro runtime instead of being ignored.
This commit is contained in:
Bryan Helmkamp 2026-05-22 12:32:59 -04:00
parent 4190e13a20
commit 5b5e0d83fd
No known key found for this signature in database
3 changed files with 59 additions and 11 deletions

View file

@ -13,7 +13,38 @@ function event(name: string, properties: Record<string, unknown>): SessionStream
} as unknown as SessionStreamEvent;
}
function flattenedEvent(
name: string,
properties: Record<string, unknown>,
): SessionStreamEvent {
return {
seq: 0,
id: "evt_1",
ts: "2026-05-22T16:25:34.940200Z",
run_id: "run_1",
event: name,
properties,
} as unknown as SessionStreamEvent;
}
describe("applyTurnEvent", () => {
test("appends assistant deltas from flattened SSE event envelopes", () => {
const acc = {
activeTextIndex: null,
parts: [],
toolCallIndex: new Map(),
} as Parameters<typeof applyTurnEvent>[0];
expect(
applyTurnEvent(
acc,
flattenedEvent("run.session.assistant_delta", { delta: "Hello" }),
),
).toBe(true);
expect(acc.parts).toEqual([{ type: "text", text: "Hello" }]);
});
test("appends assistant deltas into a single streaming text part", () => {
const acc = {
activeTextIndex: null,

View file

@ -100,17 +100,34 @@ interface NestedRunEvent {
properties?: Record<string, unknown>;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function eventPayload(envelope: SessionStreamEvent): {
eventName: string;
props: Record<string, unknown>;
} {
const raw = envelope as unknown as Record<string, unknown>;
if (typeof raw.event === "string") {
return {
eventName: raw.event,
props: isRecord(raw.properties) ? raw.properties : {},
};
}
const nested = isRecord(raw.event) ? (raw.event as NestedRunEvent) : {};
return {
eventName: nested.event ?? "",
props: isRecord(nested.properties) ? nested.properties : {},
};
}
export function applyTurnEvent(
acc: TurnAccumulator,
envelope: SessionStreamEvent,
): boolean {
// The on-wire SSE envelope is `{ seq, event: { event: "...", properties } }`,
// but the generated OpenAPI `EventEnvelope` type flattens the inner event
// fields. Cast through `unknown` to read the nested runtime shape that the
// server actually emits (matches `session-stream.test.ts`).
const nested = (envelope as unknown as { event?: NestedRunEvent }).event ?? {};
const eventName = nested.event ?? "";
const props: Record<string, unknown> = nested.properties ?? {};
const { eventName, props } = eventPayload(envelope);
if (eventName === "run.session.assistant_delta") {
const delta = typeof props.delta === "string" ? props.delta : "";

View file

@ -41,7 +41,7 @@ describe("session stream helpers", () => {
streamResponse(
[
"id: 3\nevent: run.session.turn.started\n",
'data: {"seq":3,"event":{"event":"run.session.turn.started","properties":{"turn_id":"turn_1"}}}\n\n',
'data: {"seq":3,"id":"evt_1","ts":"2026-05-22T16:25:34.940200Z","run_id":"run_1","event":"run.session.turn.started","properties":{"turn_id":"turn_1"}}\n\n',
],
200,
{ "x-fabro-turn-id": "turn_1" },
@ -65,7 +65,7 @@ describe("session stream helpers", () => {
});
expect(events).toHaveLength(1);
expect(events[0]?.seq).toBe(3);
expect(events[0]?.event.event).toBe("run.session.turn.started");
expect(events[0]?.event).toBe("run.session.turn.started");
});
test("attaches to session events from a run sequence", async () => {
@ -73,7 +73,7 @@ describe("session stream helpers", () => {
const fetchMock = mock(() =>
Promise.resolve(
streamResponse([
'data: {"seq":7,"event":{"event":"run.session.assistant_message","properties":{}}}\n\n',
'data: {"seq":7,"id":"evt_2","ts":"2026-05-22T16:25:34.940200Z","run_id":"run_1","event":"run.session.assistant_message","properties":{}}\n\n',
]),
),
);
@ -96,7 +96,7 @@ describe("session stream helpers", () => {
const fetchMock = mock(() =>
Promise.resolve(
streamResponse([
'data: {"seq":8,"event":{"event":"run.session.assistant_message","properties":{}}}\r\n\r\n',
'data: {"seq":8,"id":"evt_3","ts":"2026-05-22T16:25:34.940200Z","run_id":"run_1","event":"run.session.assistant_message","properties":{}}\r\n\r\n',
]),
),
);