diff --git a/docs/getting-started.md b/docs/getting-started.md index 2c4e6159..fc28d8f3 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -240,7 +240,7 @@ Right now, you wait for the complete response before seeing anything. Let's make Update `index.ts`: ```typescript -import { CopilotClient, SessionEvent } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient(); const session = await client.createSession({ @@ -249,13 +249,11 @@ const session = await client.createSession({ }); // Listen for response chunks -session.on((event: SessionEvent) => { - if (event.type === "assistant.message_delta") { - process.stdout.write(event.data.deltaContent); - } - if (event.type === "session.idle") { - console.log(); // New line when done - } +session.on("assistant.message_delta", (event) => { + process.stdout.write(event.data.deltaContent); +}); +session.on("session.idle", () => { + console.log(); // New line when done }); 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 Update `index.ts`: ```typescript -import { CopilotClient, defineTool, SessionEvent } from "@github/copilot-sdk"; +import { CopilotClient, defineTool } from "@github/copilot-sdk"; // Define a tool that Copilot can call const getWeather = defineTool("get_weather", { @@ -430,10 +428,8 @@ const session = await client.createSession({ tools: [getWeather], }); -session.on((event: SessionEvent) => { - if (event.type === "assistant.message_delta") { - process.stdout.write(event.data.deltaContent); - } +session.on("assistant.message_delta", (event) => { + process.stdout.write(event.data.deltaContent); }); await session.sendAndWait({ @@ -650,7 +646,7 @@ Let's put it all together into a useful interactive assistant: Node.js / TypeScript ```typescript -import { CopilotClient, defineTool, SessionEvent } from "@github/copilot-sdk"; +import { CopilotClient, defineTool } from "@github/copilot-sdk"; import * as readline from "readline"; const getWeather = defineTool("get_weather", { @@ -677,10 +673,8 @@ const session = await client.createSession({ tools: [getWeather], }); -session.on((event: SessionEvent) => { - if (event.type === "assistant.message_delta") { - process.stdout.write(event.data.deltaContent); - } +session.on("assistant.message_delta", (event) => { + process.stdout.write(event.data.deltaContent); }); const rl = readline.createInterface({ diff --git a/nodejs/README.md b/nodejs/README.md index b6e7aa47..e8951243 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -24,14 +24,13 @@ const session = await client.createSession({ model: "gpt-5", }); -// Wait for response using session.idle event +// Wait for response using typed event handlers const done = new Promise((resolve) => { - session.on((event) => { - if (event.type === "assistant.message") { - console.log(event.data.content); - } else if (event.type === "session.idle") { - resolve(); - } + session.on("assistant.message", (event) => { + console.log(event.data.content); + }); + session.on("session.idle", () => { + resolve(); }); }); @@ -159,13 +158,34 @@ Send a message and wait until the session becomes idle. Returns the final assistant message event, or undefined if none was received. +##### `on(eventType: string, handler: TypedSessionEventHandler): () => void` + +Subscribe to a specific event type. The handler receives properly typed events. + +```typescript +// Listen for specific event types with full type inference +session.on("assistant.message", (event) => { + console.log(event.data.content); // TypeScript knows about event.data.content +}); + +session.on("session.idle", () => { + console.log("Session is idle"); +}); + +// Listen to streaming events +session.on("assistant.message_delta", (event) => { + process.stdout.write(event.data.deltaContent); +}); +``` + ##### `on(handler: SessionEventHandler): () => void` -Subscribe to session events. Returns an unsubscribe function. +Subscribe to all session events. Returns an unsubscribe function. ```typescript const unsubscribe = session.on((event) => { - console.log(event); + // Handle any event type + console.log(event.type, event); }); // Later... @@ -231,27 +251,33 @@ const session = await client.createSession({ streaming: true, }); -// Wait for completion using session.idle event +// Wait for completion using typed event handlers const done = new Promise((resolve) => { - session.on((event) => { - if (event.type === "assistant.message_delta") { - // Streaming message chunk - print incrementally - process.stdout.write(event.data.deltaContent); - } else if (event.type === "assistant.reasoning_delta") { - // Streaming reasoning chunk (if model supports reasoning) - process.stdout.write(event.data.deltaContent); - } else if (event.type === "assistant.message") { - // Final message - complete content - console.log("\n--- Final message ---"); - console.log(event.data.content); - } else if (event.type === "assistant.reasoning") { - // Final reasoning content (if model supports reasoning) - console.log("--- Reasoning ---"); - console.log(event.data.content); - } else if (event.type === "session.idle") { - // Session finished processing - resolve(); - } + session.on("assistant.message_delta", (event) => { + // Streaming message chunk - print incrementally + process.stdout.write(event.data.deltaContent); + }); + + session.on("assistant.reasoning_delta", (event) => { + // Streaming reasoning chunk (if model supports reasoning) + process.stdout.write(event.data.deltaContent); + }); + + session.on("assistant.message", (event) => { + // Final message - complete content + console.log("\n--- Final message ---"); + console.log(event.data.content); + }); + + session.on("assistant.reasoning", (event) => { + // Final reasoning content (if model supports reasoning) + console.log("--- Reasoning ---"); + console.log(event.data.content); + }); + + session.on("session.idle", () => { + // Session finished processing + resolve(); }); }); diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 014a9b43..1a973d0f 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -33,6 +33,8 @@ export type { SessionConfig, SessionEvent, SessionEventHandler, + SessionEventPayload, + SessionEventType, SessionMetadata, SystemMessageAppendConfig, SystemMessageConfig, @@ -41,5 +43,6 @@ export type { ToolHandler, ToolInvocation, ToolResultObject, + TypedSessionEventHandler, ZodSchema, } from "./types.js"; diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 22f745cf..ba6b42d2 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -15,9 +15,12 @@ import type { PermissionRequestResult, SessionEvent, SessionEventHandler, + SessionEventPayload, + SessionEventType, SessionHooks, Tool, ToolHandler, + TypedSessionEventHandler, UserInputHandler, UserInputRequest, UserInputResponse, @@ -53,6 +56,8 @@ export type AssistantMessageEvent = Extract = new Set(); + private typedEventHandlers: Map void>> = + new Map(); private toolHandlers: Map = new Map(); private permissionHandler?: PermissionHandler; private userInputHandler?: UserInputHandler; @@ -190,7 +195,27 @@ export class CopilotSession { * Events include assistant messages, tool executions, errors, and session state changes. * Multiple handlers can be registered and will all receive events. * - * @param handler - A callback function that receives session events + * @param eventType - The specific event type to listen for (e.g., "assistant.message", "session.idle") + * @param handler - A callback function that receives events of the specified type + * @returns A function that, when called, unsubscribes the handler + * + * @example + * ```typescript + * // Listen for a specific event type + * const unsubscribe = session.on("assistant.message", (event) => { + * console.log("Assistant:", event.data.content); + * }); + * + * // Later, to stop receiving events: + * unsubscribe(); + * ``` + */ + on(eventType: K, handler: TypedSessionEventHandler): () => void; + + /** + * Subscribes to all events from this session. + * + * @param handler - A callback function that receives all session events * @returns A function that, when called, unsubscribes the handler * * @example @@ -210,10 +235,34 @@ export class CopilotSession { * unsubscribe(); * ``` */ - on(handler: SessionEventHandler): () => void { - this.eventHandlers.add(handler); + on(handler: SessionEventHandler): () => void; + + on( + eventTypeOrHandler: K | SessionEventHandler, + handler?: TypedSessionEventHandler + ): () => void { + // Overload 1: on(eventType, handler) - typed event subscription + if (typeof eventTypeOrHandler === "string" && handler) { + const eventType = eventTypeOrHandler; + if (!this.typedEventHandlers.has(eventType)) { + this.typedEventHandlers.set(eventType, new Set()); + } + // Cast is safe: handler receives the correctly typed event at dispatch time + const storedHandler = handler as (event: SessionEvent) => void; + this.typedEventHandlers.get(eventType)!.add(storedHandler); + return () => { + const handlers = this.typedEventHandlers.get(eventType); + if (handlers) { + handlers.delete(storedHandler); + } + }; + } + + // Overload 2: on(handler) - wildcard subscription + const wildcardHandler = eventTypeOrHandler as SessionEventHandler; + this.eventHandlers.add(wildcardHandler); return () => { - this.eventHandlers.delete(handler); + this.eventHandlers.delete(wildcardHandler); }; } @@ -224,6 +273,19 @@ export class CopilotSession { * @internal This method is for internal use by the SDK. */ _dispatchEvent(event: SessionEvent): void { + // Dispatch to typed handlers for this specific event type + const typedHandlers = this.typedEventHandlers.get(event.type); + if (typedHandlers) { + for (const handler of typedHandlers) { + try { + handler(event as SessionEventPayload); + } catch (_error) { + // Handler error + } + } + } + + // Dispatch to wildcard handlers for (const handler of this.eventHandlers) { try { handler(event); @@ -441,6 +503,7 @@ export class CopilotSession { sessionId: this.sessionId, }); this.eventHandlers.clear(); + this.typedEventHandlers.clear(); this.toolHandlers.clear(); this.permissionHandler = undefined; } diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 697e4a9f..93fa1d7f 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -807,7 +807,24 @@ export interface MessageOptions { } /** - * Event handler callback type + * All possible event type strings from SessionEvent + */ +export type SessionEventType = SessionEvent["type"]; + +/** + * Extract the specific event payload for a given event type + */ +export type SessionEventPayload = Extract; + +/** + * Event handler for a specific event type + */ +export type TypedSessionEventHandler = ( + event: SessionEventPayload +) => void; + +/** + * Event handler callback type (for all events) */ export type SessionEventHandler = (event: SessionEvent) => void;