Skip to content

Commit f3514a0

Browse files
authored
feat(nodejs): add typed event filtering to session.on()
Add overloaded on() method that accepts an event type string as the first argument, enabling type-safe event subscriptions: session.on('assistant.message', (event) => { // event is typed as SessionEventPayload<'assistant.message'> console.log(event.data.content); }); The original on(handler) signature remains supported for wildcard subscriptions. Changes: - Add SessionEventType, SessionEventPayload, TypedSessionEventHandler types - Update CopilotSession.on() with typed overload - Update _dispatchEvent() to dispatch to typed handlers - Export new types from index.ts - Update documentation with new usage patterns
1 parent a124990 commit f3514a0

File tree

5 files changed

+155
-52
lines changed

5 files changed

+155
-52
lines changed

docs/getting-started.md

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ Right now, you wait for the complete response before seeing anything. Let's make
240240
Update `index.ts`:
241241

242242
```typescript
243-
import { CopilotClient, SessionEvent } from "@github/copilot-sdk";
243+
import { CopilotClient } from "@github/copilot-sdk";
244244

245245
const client = new CopilotClient();
246246
const session = await client.createSession({
@@ -249,13 +249,11 @@ const session = await client.createSession({
249249
});
250250

251251
// Listen for response chunks
252-
session.on((event: SessionEvent) => {
253-
if (event.type === "assistant.message_delta") {
254-
process.stdout.write(event.data.deltaContent);
255-
}
256-
if (event.type === "session.idle") {
257-
console.log(); // New line when done
258-
}
252+
session.on("assistant.message_delta", (event) => {
253+
process.stdout.write(event.data.deltaContent);
254+
});
255+
session.on("session.idle", () => {
256+
console.log(); // New line when done
259257
});
260258

261259
await session.sendAndWait({ prompt: "Tell me a short joke" });
@@ -401,7 +399,7 @@ Now for the powerful part. Let's give Copilot the ability to call your code by d
401399
Update `index.ts`:
402400

403401
```typescript
404-
import { CopilotClient, defineTool, SessionEvent } from "@github/copilot-sdk";
402+
import { CopilotClient, defineTool } from "@github/copilot-sdk";
405403

406404
// Define a tool that Copilot can call
407405
const getWeather = defineTool("get_weather", {
@@ -430,10 +428,8 @@ const session = await client.createSession({
430428
tools: [getWeather],
431429
});
432430

433-
session.on((event: SessionEvent) => {
434-
if (event.type === "assistant.message_delta") {
435-
process.stdout.write(event.data.deltaContent);
436-
}
431+
session.on("assistant.message_delta", (event) => {
432+
process.stdout.write(event.data.deltaContent);
437433
});
438434

439435
await session.sendAndWait({
@@ -650,7 +646,7 @@ Let's put it all together into a useful interactive assistant:
650646
<summary><strong>Node.js / TypeScript</strong></summary>
651647

652648
```typescript
653-
import { CopilotClient, defineTool, SessionEvent } from "@github/copilot-sdk";
649+
import { CopilotClient, defineTool } from "@github/copilot-sdk";
654650
import * as readline from "readline";
655651

656652
const getWeather = defineTool("get_weather", {
@@ -677,10 +673,8 @@ const session = await client.createSession({
677673
tools: [getWeather],
678674
});
679675

680-
session.on((event: SessionEvent) => {
681-
if (event.type === "assistant.message_delta") {
682-
process.stdout.write(event.data.deltaContent);
683-
}
676+
session.on("assistant.message_delta", (event) => {
677+
process.stdout.write(event.data.deltaContent);
684678
});
685679

686680
const rl = readline.createInterface({

nodejs/README.md

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,13 @@ const session = await client.createSession({
2424
model: "gpt-5",
2525
});
2626

27-
// Wait for response using session.idle event
27+
// Wait for response using typed event handlers
2828
const done = new Promise<void>((resolve) => {
29-
session.on((event) => {
30-
if (event.type === "assistant.message") {
31-
console.log(event.data.content);
32-
} else if (event.type === "session.idle") {
33-
resolve();
34-
}
29+
session.on("assistant.message", (event) => {
30+
console.log(event.data.content);
31+
});
32+
session.on("session.idle", () => {
33+
resolve();
3534
});
3635
});
3736

@@ -159,13 +158,34 @@ Send a message and wait until the session becomes idle.
159158

160159
Returns the final assistant message event, or undefined if none was received.
161160

161+
##### `on(eventType: string, handler: TypedSessionEventHandler): () => void`
162+
163+
Subscribe to a specific event type. The handler receives properly typed events.
164+
165+
```typescript
166+
// Listen for specific event types with full type inference
167+
session.on("assistant.message", (event) => {
168+
console.log(event.data.content); // TypeScript knows about event.data.content
169+
});
170+
171+
session.on("session.idle", () => {
172+
console.log("Session is idle");
173+
});
174+
175+
// Listen to streaming events
176+
session.on("assistant.message_delta", (event) => {
177+
process.stdout.write(event.data.deltaContent);
178+
});
179+
```
180+
162181
##### `on(handler: SessionEventHandler): () => void`
163182

164-
Subscribe to session events. Returns an unsubscribe function.
183+
Subscribe to all session events. Returns an unsubscribe function.
165184

166185
```typescript
167186
const unsubscribe = session.on((event) => {
168-
console.log(event);
187+
// Handle any event type
188+
console.log(event.type, event);
169189
});
170190

171191
// Later...
@@ -231,27 +251,33 @@ const session = await client.createSession({
231251
streaming: true,
232252
});
233253

234-
// Wait for completion using session.idle event
254+
// Wait for completion using typed event handlers
235255
const done = new Promise<void>((resolve) => {
236-
session.on((event) => {
237-
if (event.type === "assistant.message_delta") {
238-
// Streaming message chunk - print incrementally
239-
process.stdout.write(event.data.deltaContent);
240-
} else if (event.type === "assistant.reasoning_delta") {
241-
// Streaming reasoning chunk (if model supports reasoning)
242-
process.stdout.write(event.data.deltaContent);
243-
} else if (event.type === "assistant.message") {
244-
// Final message - complete content
245-
console.log("\n--- Final message ---");
246-
console.log(event.data.content);
247-
} else if (event.type === "assistant.reasoning") {
248-
// Final reasoning content (if model supports reasoning)
249-
console.log("--- Reasoning ---");
250-
console.log(event.data.content);
251-
} else if (event.type === "session.idle") {
252-
// Session finished processing
253-
resolve();
254-
}
256+
session.on("assistant.message_delta", (event) => {
257+
// Streaming message chunk - print incrementally
258+
process.stdout.write(event.data.deltaContent);
259+
});
260+
261+
session.on("assistant.reasoning_delta", (event) => {
262+
// Streaming reasoning chunk (if model supports reasoning)
263+
process.stdout.write(event.data.deltaContent);
264+
});
265+
266+
session.on("assistant.message", (event) => {
267+
// Final message - complete content
268+
console.log("\n--- Final message ---");
269+
console.log(event.data.content);
270+
});
271+
272+
session.on("assistant.reasoning", (event) => {
273+
// Final reasoning content (if model supports reasoning)
274+
console.log("--- Reasoning ---");
275+
console.log(event.data.content);
276+
});
277+
278+
session.on("session.idle", () => {
279+
// Session finished processing
280+
resolve();
255281
});
256282
});
257283

nodejs/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export type {
3333
SessionConfig,
3434
SessionEvent,
3535
SessionEventHandler,
36+
SessionEventPayload,
37+
SessionEventType,
3638
SessionMetadata,
3739
SystemMessageAppendConfig,
3840
SystemMessageConfig,
@@ -41,5 +43,6 @@ export type {
4143
ToolHandler,
4244
ToolInvocation,
4345
ToolResultObject,
46+
TypedSessionEventHandler,
4447
ZodSchema,
4548
} from "./types.js";

nodejs/src/session.ts

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import type {
1515
PermissionRequestResult,
1616
SessionEvent,
1717
SessionEventHandler,
18+
SessionEventPayload,
19+
SessionEventType,
1820
SessionHooks,
1921
Tool,
2022
ToolHandler,
23+
TypedSessionEventHandler,
2124
UserInputHandler,
2225
UserInputRequest,
2326
UserInputResponse,
@@ -53,6 +56,8 @@ export type AssistantMessageEvent = Extract<SessionEvent, { type: "assistant.mes
5356
*/
5457
export class CopilotSession {
5558
private eventHandlers: Set<SessionEventHandler> = new Set();
59+
private typedEventHandlers: Map<SessionEventType, Set<(event: SessionEvent) => void>> =
60+
new Map();
5661
private toolHandlers: Map<string, ToolHandler> = new Map();
5762
private permissionHandler?: PermissionHandler;
5863
private userInputHandler?: UserInputHandler;
@@ -190,7 +195,27 @@ export class CopilotSession {
190195
* Events include assistant messages, tool executions, errors, and session state changes.
191196
* Multiple handlers can be registered and will all receive events.
192197
*
193-
* @param handler - A callback function that receives session events
198+
* @param eventType - The specific event type to listen for (e.g., "assistant.message", "session.idle")
199+
* @param handler - A callback function that receives events of the specified type
200+
* @returns A function that, when called, unsubscribes the handler
201+
*
202+
* @example
203+
* ```typescript
204+
* // Listen for a specific event type
205+
* const unsubscribe = session.on("assistant.message", (event) => {
206+
* console.log("Assistant:", event.data.content);
207+
* });
208+
*
209+
* // Later, to stop receiving events:
210+
* unsubscribe();
211+
* ```
212+
*/
213+
on<K extends SessionEventType>(eventType: K, handler: TypedSessionEventHandler<K>): () => void;
214+
215+
/**
216+
* Subscribes to all events from this session.
217+
*
218+
* @param handler - A callback function that receives all session events
194219
* @returns A function that, when called, unsubscribes the handler
195220
*
196221
* @example
@@ -210,10 +235,34 @@ export class CopilotSession {
210235
* unsubscribe();
211236
* ```
212237
*/
213-
on(handler: SessionEventHandler): () => void {
214-
this.eventHandlers.add(handler);
238+
on(handler: SessionEventHandler): () => void;
239+
240+
on<K extends SessionEventType>(
241+
eventTypeOrHandler: K | SessionEventHandler,
242+
handler?: TypedSessionEventHandler<K>
243+
): () => void {
244+
// Overload 1: on(eventType, handler) - typed event subscription
245+
if (typeof eventTypeOrHandler === "string" && handler) {
246+
const eventType = eventTypeOrHandler;
247+
if (!this.typedEventHandlers.has(eventType)) {
248+
this.typedEventHandlers.set(eventType, new Set());
249+
}
250+
// Cast is safe: handler receives the correctly typed event at dispatch time
251+
const storedHandler = handler as (event: SessionEvent) => void;
252+
this.typedEventHandlers.get(eventType)!.add(storedHandler);
253+
return () => {
254+
const handlers = this.typedEventHandlers.get(eventType);
255+
if (handlers) {
256+
handlers.delete(storedHandler);
257+
}
258+
};
259+
}
260+
261+
// Overload 2: on(handler) - wildcard subscription
262+
const wildcardHandler = eventTypeOrHandler as SessionEventHandler;
263+
this.eventHandlers.add(wildcardHandler);
215264
return () => {
216-
this.eventHandlers.delete(handler);
265+
this.eventHandlers.delete(wildcardHandler);
217266
};
218267
}
219268

@@ -224,6 +273,19 @@ export class CopilotSession {
224273
* @internal This method is for internal use by the SDK.
225274
*/
226275
_dispatchEvent(event: SessionEvent): void {
276+
// Dispatch to typed handlers for this specific event type
277+
const typedHandlers = this.typedEventHandlers.get(event.type);
278+
if (typedHandlers) {
279+
for (const handler of typedHandlers) {
280+
try {
281+
handler(event as SessionEventPayload<typeof event.type>);
282+
} catch (_error) {
283+
// Handler error
284+
}
285+
}
286+
}
287+
288+
// Dispatch to wildcard handlers
227289
for (const handler of this.eventHandlers) {
228290
try {
229291
handler(event);
@@ -441,6 +503,7 @@ export class CopilotSession {
441503
sessionId: this.sessionId,
442504
});
443505
this.eventHandlers.clear();
506+
this.typedEventHandlers.clear();
444507
this.toolHandlers.clear();
445508
this.permissionHandler = undefined;
446509
}

nodejs/src/types.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,24 @@ export interface MessageOptions {
807807
}
808808

809809
/**
810-
* Event handler callback type
810+
* All possible event type strings from SessionEvent
811+
*/
812+
export type SessionEventType = SessionEvent["type"];
813+
814+
/**
815+
* Extract the specific event payload for a given event type
816+
*/
817+
export type SessionEventPayload<T extends SessionEventType> = Extract<SessionEvent, { type: T }>;
818+
819+
/**
820+
* Event handler for a specific event type
821+
*/
822+
export type TypedSessionEventHandler<T extends SessionEventType> = (
823+
event: SessionEventPayload<T>
824+
) => void;
825+
826+
/**
827+
* Event handler callback type (for all events)
811828
*/
812829
export type SessionEventHandler = (event: SessionEvent) => void;
813830

0 commit comments

Comments
 (0)