Skip to content

Commit e3a440e

Browse files
committed
fix: expose chat input kind to fetch callbacks
1 parent fea9f66 commit e3a440e

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

‎packages/trigger-sdk/src/v3/chat-stop-delivery.test.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createServer, type Server, type ServerResponse } from "node:http";
22
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33
import {
44
TriggerChatTransport,
5+
type ChatTransportEndpointContext,
56
type ChatTransportEvent,
67
type TriggerChatTransportOptions,
78
} from "./chat.js";
@@ -31,6 +32,7 @@ describe("Stop delivery outcomes", () => {
3132
let inputs: InputRequest[];
3233
let outputs: ServerResponse[];
3334
let events: ChatTransportEvent[];
35+
let fetchContexts: ChatTransportEndpointContext[];
3436
let respond: (input: InputRequest) => void;
3537
let inputSeq: number;
3638

@@ -41,6 +43,10 @@ describe("Stop delivery outcomes", () => {
4143
accessToken: () => "test-token",
4244
sessions: { chat: { publicAccessToken: "test-token", lastEventId: "1" } },
4345
onEvent: (event) => events.push(event),
46+
fetch: (url, init, context) => {
47+
fetchContexts.push({ ...context });
48+
return globalThis.fetch(url, init);
49+
},
4450
...options,
4551
});
4652
}
@@ -55,6 +61,7 @@ describe("Stop delivery outcomes", () => {
5561
inputs = [];
5662
outputs = [];
5763
events = [];
64+
fetchContexts = [];
5865
inputSeq = 10;
5966
respond = (input) => appendResponse(input);
6067
server = createServer(async (request, response) => {
@@ -128,6 +135,7 @@ describe("Stop delivery outcomes", () => {
128135
await expect(stop(throwOnError)).resolves.toBe(true);
129136
expect(inputs).toHaveLength(1);
130137
expect(inputs[0]?.kind).toBe("stop");
138+
expect(fetchContexts).toEqual([{ endpoint: "in", chatId: "chat", inputKind: "stop" }]);
131139
expect(sendFailures()).toHaveLength(0);
132140
}
133141
);
@@ -199,6 +207,10 @@ describe("Stop delivery outcomes", () => {
199207
"Bearer test-token",
200208
"Bearer fresh-token",
201209
]);
210+
expect(fetchContexts).toEqual([
211+
{ endpoint: "in", chatId: "chat", inputKind: "stop" },
212+
{ endpoint: "in", chatId: "chat", inputKind: "stop" },
213+
]);
202214
expect(sendFailures()).toHaveLength(0);
203215
}
204216
);
@@ -315,6 +327,29 @@ describe("Stop delivery outcomes", () => {
315327
}
316328
);
317329

330+
it.each(["message", "action"] as const)(
331+
"identifies %s input and consumer Stop without an output input kind",
332+
async (kind) => {
333+
const abort = new AbortController();
334+
const reader = (await send(kind, abort.signal)).getReader();
335+
await vi.waitFor(() => expect(outputs).toHaveLength(1));
336+
expect(fetchContexts).toEqual([
337+
{ endpoint: "in", chatId: "chat", inputKind: "message" },
338+
{ endpoint: "out", chatId: "chat" },
339+
]);
340+
abort.abort();
341+
await expect(reader.read()).resolves.toEqual({ done: true, value: undefined });
342+
await vi.waitFor(() =>
343+
expect(inputs.map((input) => input.kind)).toEqual(["message", "stop"])
344+
);
345+
expect(fetchContexts).toEqual([
346+
{ endpoint: "in", chatId: "chat", inputKind: "message" },
347+
{ endpoint: "out", chatId: "chat" },
348+
{ endpoint: "in", chatId: "chat", inputKind: "stop" },
349+
]);
350+
}
351+
);
352+
318353
describe.each(["message", "action"] as const)("%s cancellation", (kind) => {
319354
it("does not hold a tab claim during cancelled session creation", async () => {
320355
const token = deferred<string>();

‎packages/trigger-sdk/src/v3/chat.ts‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ export type ChatTransportEndpoint = "in" | "out";
190190
export type ChatTransportEndpointContext = {
191191
endpoint: ChatTransportEndpoint;
192192
chatId: string;
193+
/** Input control kind before serialization. Output requests omit this field. */
194+
inputKind?: "message" | "stop";
193195
};
194196

195197
/** Resolver form of `baseURL` — return the base for the given endpoint. */
@@ -1407,7 +1409,8 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
14071409

14081410
const partId = crypto.randomUUID();
14091411
const serializedBody = this.serializeInputChunk({ kind: "stop" });
1410-
const send = (token: string) => this.appendInputChunk(chatId, token, serializedBody, partId);
1412+
const send = (token: string) =>
1413+
this.appendInputChunk(chatId, token, serializedBody, partId, "stop");
14111414
try {
14121415
const inSeq = await this.sendWithEvents(
14131416
chatId,
@@ -2147,9 +2150,10 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
21472150
chatId: string,
21482151
token: string,
21492152
body: string,
2150-
partId?: string
2153+
partId?: string,
2154+
inputKind: "message" | "stop" = "message"
21512155
): Promise<number | undefined> {
2152-
const ctx: ChatTransportEndpointContext = { endpoint: "in", chatId };
2156+
const ctx: ChatTransportEndpointContext = { endpoint: "in", chatId, inputKind };
21532157
const url = `${this.resolveBaseURL(ctx)}/realtime/v1/sessions/${encodeURIComponent(chatId)}/in/append`;
21542158
// extraHeaders first so the fixed headers below win — a transport-wide
21552159
// X-Part-Id must not override the per-append idempotency key.
@@ -2349,7 +2353,9 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
23492353
this.appendInputChunk(
23502354
chatId,
23512355
state.publicAccessToken,
2352-
this.serializeInputChunk({ kind: "stop" })
2356+
this.serializeInputChunk({ kind: "stop" }),
2357+
undefined,
2358+
"stop"
23532359
)
23542360
.then((inSeq) => this.recordStoppedInput(chatId, state, stoppedBoundary, inSeq))
23552361
.catch(() => {});

0 commit comments

Comments
 (0)