-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Expand file tree
/
Copy pathsession.ts
More file actions
288 lines (270 loc) · 9.4 KB
/
session.ts
File metadata and controls
288 lines (270 loc) · 9.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
/**
* Copilot Session - represents a single conversation session with the Copilot CLI.
* @module session
*/
import type { MessageConnection } from "vscode-jsonrpc/node";
import type {
MessageOptions,
PermissionHandler,
PermissionRequest,
PermissionRequestResult,
SessionEvent,
SessionEventHandler,
Tool,
ToolHandler,
} from "./types.js";
/**
* Represents a single conversation session with the Copilot CLI.
*
* A session maintains conversation state, handles events, and manages tool execution.
* Sessions are created via {@link CopilotClient.createSession} or resumed via
* {@link CopilotClient.resumeSession}.
*
* @example
* ```typescript
* const session = await client.createSession({ model: "gpt-4" });
*
* // Subscribe to events
* const unsubscribe = session.on((event) => {
* if (event.type === "assistant.message") {
* console.log(event.data.content);
* }
* });
*
* // Send a message
* await session.send({ prompt: "Hello, world!" });
*
* // Clean up
* unsubscribe();
* await session.destroy();
* ```
*/
export class CopilotSession {
private eventHandlers: Set<SessionEventHandler> = new Set();
private toolHandlers: Map<string, ToolHandler> = new Map();
private permissionHandler?: PermissionHandler;
/**
* Creates a new CopilotSession instance.
*
* @param sessionId - The unique identifier for this session
* @param connection - The JSON-RPC message connection to the Copilot CLI
* @internal This constructor is internal. Use {@link CopilotClient.createSession} to create sessions.
*/
constructor(
public readonly sessionId: string,
private connection: MessageConnection
) {}
/**
* Sends a message to this session and waits for the response.
*
* The message is processed asynchronously. Subscribe to events via {@link on}
* to receive streaming responses and other session events.
*
* @param options - The message options including the prompt and optional attachments
* @returns A promise that resolves with the message ID of the response
* @throws Error if the session has been destroyed or the connection fails
*
* @example
* ```typescript
* const messageId = await session.send({
* prompt: "Explain this code",
* attachments: [{ type: "file", path: "./src/index.ts" }]
* });
* ```
*/
async send(options: MessageOptions): Promise<string> {
const response = await this.connection.sendRequest("session.send", {
sessionId: this.sessionId,
prompt: options.prompt,
attachments: options.attachments,
mode: options.mode,
});
return (response as { messageId: string }).messageId;
}
/**
* Subscribes to events from this session.
*
* 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
* @returns A function that, when called, unsubscribes the handler
*
* @example
* ```typescript
* const unsubscribe = session.on((event) => {
* switch (event.type) {
* case "assistant.message":
* console.log("Assistant:", event.data.content);
* break;
* case "session.error":
* console.error("Error:", event.data.message);
* break;
* }
* });
*
* // Later, to stop receiving events:
* unsubscribe();
* ```
*/
on(handler: SessionEventHandler): () => void {
this.eventHandlers.add(handler);
return () => {
this.eventHandlers.delete(handler);
};
}
/**
* Dispatches an event to all registered handlers.
*
* @param event - The session event to dispatch
* @internal This method is for internal use by the SDK.
*/
_dispatchEvent(event: SessionEvent): void {
for (const handler of this.eventHandlers) {
try {
handler(event);
} catch (_error) {
// Handler error
}
}
}
/**
* Registers custom tool handlers for this session.
*
* Tools allow the assistant to execute custom functions. When the assistant
* invokes a tool, the corresponding handler is called with the tool arguments.
*
* @param tools - An array of tool definitions with their handlers, or undefined to clear all tools
* @internal This method is typically called internally when creating a session with tools.
*/
registerTools(tools?: Tool[]): void {
this.toolHandlers.clear();
if (!tools) {
return;
}
for (const tool of tools) {
this.toolHandlers.set(tool.name, tool.handler);
}
}
/**
* Retrieves a registered tool handler by name.
*
* @param name - The name of the tool to retrieve
* @returns The tool handler if found, or undefined
* @internal This method is for internal use by the SDK.
*/
getToolHandler(name: string): ToolHandler | undefined {
return this.toolHandlers.get(name);
}
/**
* Registers a handler for permission requests.
*
* When the assistant needs permission to perform certain actions (e.g., file operations),
* this handler is called to approve or deny the request.
*
* @param handler - The permission handler function, or undefined to remove the handler
* @internal This method is typically called internally when creating a session.
*/
registerPermissionHandler(handler?: PermissionHandler): void {
this.permissionHandler = handler;
}
/**
* Handles a permission request from the Copilot CLI.
*
* @param request - The permission request data from the CLI
* @returns A promise that resolves with the permission decision
* @internal This method is for internal use by the SDK.
*/
async _handlePermissionRequest(request: unknown): Promise<PermissionRequestResult> {
if (!this.permissionHandler) {
// No handler registered, deny permission
return { kind: "denied-no-approval-rule-and-could-not-request-from-user" };
}
try {
const result = await this.permissionHandler(request as PermissionRequest, {
sessionId: this.sessionId,
});
return result;
} catch (_error) {
// Handler failed, deny permission
return { kind: "denied-no-approval-rule-and-could-not-request-from-user" };
}
}
/**
* Retrieves all events and messages from this session's history.
*
* This returns the complete conversation history including user messages,
* assistant responses, tool executions, and other session events.
*
* @returns A promise that resolves with an array of all session events
* @throws Error if the session has been destroyed or the connection fails
*
* @example
* ```typescript
* const events = await session.getMessages();
* for (const event of events) {
* if (event.type === "assistant.message") {
* console.log("Assistant:", event.data.content);
* }
* }
* ```
*/
async getMessages(): Promise<SessionEvent[]> {
const response = await this.connection.sendRequest("session.getMessages", {
sessionId: this.sessionId,
});
return (response as { events: SessionEvent[] }).events;
}
/**
* Destroys this session and releases all associated resources.
*
* After calling this method, the session can no longer be used. All event
* handlers and tool handlers are cleared. To continue the conversation,
* use {@link CopilotClient.resumeSession} with the session ID.
*
* @returns A promise that resolves when the session is destroyed
* @throws Error if the connection fails
*
* @example
* ```typescript
* // Clean up when done
* await session.destroy();
* ```
*/
async destroy(): Promise<void> {
await this.connection.sendRequest("session.destroy", {
sessionId: this.sessionId,
});
this.eventHandlers.clear();
this.toolHandlers.clear();
this.permissionHandler = undefined;
}
/**
* Aborts the currently processing message in this session.
*
* Use this to cancel a long-running request. The session remains valid
* and can continue to be used for new messages.
*
* @returns A promise that resolves when the abort request is acknowledged
* @throws Error if the session has been destroyed or the connection fails
*
* @example
* ```typescript
* // Start a long-running request
* const messagePromise = session.send({ prompt: "Write a very long story..." });
*
* // Abort after 5 seconds
* setTimeout(async () => {
* await session.abort();
* }, 5000);
* ```
*/
async abort(): Promise<void> {
await this.connection.sendRequest("session.abort", {
sessionId: this.sessionId,
});
}
}