Skip to content

Commit c07c30b

Browse files
committed
feat: add agent event types and SSE event emitter for improved event handling
1 parent 0df901d commit c07c30b

7 files changed

Lines changed: 590 additions & 253 deletions

File tree

agentEvents.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { ToolCallEvent } from "./agent/toolCallEvents.js";
2+
3+
export type AgentEvent =
4+
| {
5+
type: "turn-started";
6+
messageId: string;
7+
}
8+
| {
9+
type: "text-delta";
10+
delta: string;
11+
}
12+
| {
13+
type: "reasoning-delta";
14+
delta: string;
15+
}
16+
| {
17+
type: "tool-call";
18+
data: ToolCallEvent;
19+
}
20+
| {
21+
type: "transcript";
22+
text: string;
23+
language?: string;
24+
}
25+
| {
26+
type: "response";
27+
text: string;
28+
sessionId: string;
29+
turnId: string;
30+
}
31+
| {
32+
type: "speech-response";
33+
transcript: { text: string; language?: string };
34+
response: { text: string };
35+
sessionId: string;
36+
turnId: string;
37+
}
38+
| {
39+
type: "audio-start";
40+
mimeType: string;
41+
format: string;
42+
sampleRate: number;
43+
channelCount: number;
44+
bitsPerSample: number;
45+
}
46+
| {
47+
type: "audio-delta";
48+
value: Uint8Array;
49+
}
50+
| {
51+
type: "audio-done";
52+
}
53+
| {
54+
type: "error";
55+
error: string;
56+
}
57+
| {
58+
type: "finish";
59+
};
60+
61+
export type AgentEventEmitter = (event: AgentEvent) => void | Promise<void>;

agentResponseEvents.ts

Lines changed: 1 addition & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,206 +1 @@
1-
import { randomUUID } from "crypto";
2-
3-
import type { ToolCallEvent } from "./agent/toolCallEvents.js";
4-
5-
type AgentEventStreamResponse = {
6-
writeHead: (statusCode: number, headers: Record<string, string>) => void;
7-
write: (chunk: string) => unknown;
8-
end: () => unknown;
9-
writableEnded: boolean;
10-
destroyed: boolean;
11-
};
12-
13-
type AgentEventStreamOptions = {
14-
vercelAiUiMessageStream?: boolean;
15-
closeActiveBlockOnToolStart?: boolean;
16-
};
17-
18-
export function createAgentEventStream(
19-
res: AgentEventStreamResponse,
20-
options: AgentEventStreamOptions = {},
21-
) {
22-
let isStreamClosed = false;
23-
let activeBlock: { type: "text" | "reasoning"; id: string } | null = null;
24-
25-
res.writeHead(200, {
26-
"Content-Type": "text/event-stream",
27-
"Cache-Control": "no-cache",
28-
"Connection": "keep-alive",
29-
...(options.vercelAiUiMessageStream
30-
? { "x-vercel-ai-ui-message-stream": "v1" }
31-
: {}),
32-
});
33-
34-
const stream = {
35-
send(obj: unknown) {
36-
if (isStreamClosed || res.writableEnded || res.destroyed) {
37-
return;
38-
}
39-
40-
res.write(`data: ${JSON.stringify(obj)}\n\n`);
41-
},
42-
43-
endActiveBlock() {
44-
if (!activeBlock) {
45-
return;
46-
}
47-
48-
stream.send({
49-
type: `${activeBlock.type}-end`,
50-
id: activeBlock.id,
51-
});
52-
53-
activeBlock = null;
54-
},
55-
56-
startBlock(type: "text" | "reasoning") {
57-
if (activeBlock?.type === type) {
58-
return activeBlock.id;
59-
}
60-
61-
stream.endActiveBlock();
62-
63-
const id = randomUUID();
64-
activeBlock = { type, id };
65-
66-
stream.send({
67-
type: `${type}-start`,
68-
id,
69-
});
70-
71-
return id;
72-
},
73-
74-
start(messageId: string) {
75-
stream.send({
76-
type: "start",
77-
messageId,
78-
});
79-
},
80-
81-
textDelta(delta: string) {
82-
const textId = stream.startBlock("text");
83-
stream.send({
84-
type: "text-delta",
85-
id: textId,
86-
delta,
87-
});
88-
},
89-
90-
reasoningDelta(delta: string) {
91-
const reasoningId = stream.startBlock("reasoning");
92-
stream.send({
93-
type: "reasoning-delta",
94-
id: reasoningId,
95-
delta,
96-
});
97-
},
98-
99-
toolCall(event: ToolCallEvent) {
100-
if (options.closeActiveBlockOnToolStart && event.phase === "start") {
101-
stream.endActiveBlock();
102-
}
103-
104-
stream.send({
105-
type: "data-tool-call",
106-
data: event,
107-
});
108-
},
109-
110-
transcript(text: string, language?: string) {
111-
stream.send({
112-
type: "transcript",
113-
data: {
114-
text,
115-
language,
116-
},
117-
});
118-
},
119-
120-
response(text: string, sessionId: string, turnId: string) {
121-
stream.send({
122-
type: "response",
123-
data: {
124-
text,
125-
sessionId,
126-
turnId,
127-
},
128-
});
129-
},
130-
131-
speechResponse(
132-
transcript: { text: string; language?: string },
133-
response: { text: string },
134-
sessionId: string,
135-
turnId: string,
136-
) {
137-
stream.send({
138-
type: "speech-response",
139-
data: {
140-
transcript,
141-
response,
142-
sessionId,
143-
turnId,
144-
},
145-
});
146-
},
147-
148-
audioStart(
149-
mimeType: string,
150-
format: string,
151-
sampleRate: number,
152-
channelCount: number,
153-
bitsPerSample: number,
154-
) {
155-
stream.send({
156-
type: "audio-start",
157-
data: {
158-
mimeType,
159-
format,
160-
sampleRate,
161-
channelCount,
162-
bitsPerSample,
163-
},
164-
});
165-
},
166-
167-
audioDelta(value: Uint8Array) {
168-
stream.send({
169-
type: "audio-delta",
170-
data: {
171-
base64: Buffer.from(value).toString("base64"),
172-
},
173-
});
174-
},
175-
176-
audioDone() {
177-
stream.send({
178-
type: "audio-done",
179-
});
180-
},
181-
182-
error(error: string) {
183-
stream.send({
184-
type: "error",
185-
error,
186-
});
187-
},
188-
189-
end() {
190-
if (isStreamClosed || res.writableEnded || res.destroyed) {
191-
return;
192-
}
193-
194-
stream.endActiveBlock();
195-
stream.send({
196-
type: "finish",
197-
});
198-
199-
res.write("data: [DONE]\n\n");
200-
isStreamClosed = true;
201-
res.end();
202-
},
203-
};
204-
205-
return stream;
206-
}
1+
export { createSseEventEmitter } from "./surfaces/web-sse/createSseEventEmitter.js";

0 commit comments

Comments
 (0)