Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 12 additions & 18 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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" });
Expand Down Expand Up @@ -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", {
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -650,7 +646,7 @@ Let's put it all together into a useful interactive assistant:
<summary><strong>Node.js / TypeScript</strong></summary>

```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", {
Expand All @@ -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({
Expand Down
84 changes: 55 additions & 29 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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();
});
});

Expand Down Expand Up @@ -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...
Expand Down Expand Up @@ -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<void>((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();
});
});

Expand Down
3 changes: 3 additions & 0 deletions nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export type {
SessionConfig,
SessionEvent,
SessionEventHandler,
SessionEventPayload,
SessionEventType,
SessionMetadata,
SystemMessageAppendConfig,
SystemMessageConfig,
Expand All @@ -41,5 +43,6 @@ export type {
ToolHandler,
ToolInvocation,
ToolResultObject,
TypedSessionEventHandler,
ZodSchema,
} from "./types.js";
71 changes: 67 additions & 4 deletions nodejs/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import type {
PermissionRequestResult,
SessionEvent,
SessionEventHandler,
SessionEventPayload,
SessionEventType,
SessionHooks,
Tool,
ToolHandler,
TypedSessionEventHandler,
UserInputHandler,
UserInputRequest,
UserInputResponse,
Expand Down Expand Up @@ -53,6 +56,8 @@ export type AssistantMessageEvent = Extract<SessionEvent, { type: "assistant.mes
*/
export class CopilotSession {
private eventHandlers: Set<SessionEventHandler> = new Set();
private typedEventHandlers: Map<SessionEventType, Set<(event: SessionEvent) => void>> =
new Map();
private toolHandlers: Map<string, ToolHandler> = new Map();
private permissionHandler?: PermissionHandler;
private userInputHandler?: UserInputHandler;
Expand Down Expand Up @@ -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<K extends SessionEventType>(eventType: K, handler: TypedSessionEventHandler<K>): () => 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
Expand All @@ -210,10 +235,34 @@ export class CopilotSession {
* unsubscribe();
* ```
*/
on(handler: SessionEventHandler): () => void {
this.eventHandlers.add(handler);
on(handler: SessionEventHandler): () => void;

on<K extends SessionEventType>(
eventTypeOrHandler: K | SessionEventHandler,
handler?: TypedSessionEventHandler<K>
): () => 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);
};
}

Expand All @@ -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<typeof event.type>);
} catch (_error) {
// Handler error
}
}
}

// Dispatch to wildcard handlers
for (const handler of this.eventHandlers) {
try {
handler(event);
Expand Down Expand Up @@ -441,6 +503,7 @@ export class CopilotSession {
sessionId: this.sessionId,
});
this.eventHandlers.clear();
this.typedEventHandlers.clear();
this.toolHandlers.clear();
this.permissionHandler = undefined;
}
Expand Down
19 changes: 18 additions & 1 deletion nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends SessionEventType> = Extract<SessionEvent, { type: T }>;

/**
* Event handler for a specific event type
*/
export type TypedSessionEventHandler<T extends SessionEventType> = (
event: SessionEventPayload<T>
) => void;

/**
* Event handler callback type (for all events)
*/
export type SessionEventHandler = (event: SessionEvent) => void;

Expand Down
Loading