@github/copilot-sdk 1.0.0-beta.1 → 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -43
- package/dist/canvas.d.ts +126 -0
- package/dist/canvas.js +49 -0
- package/dist/cjs/canvas.js +75 -0
- package/dist/cjs/client.js +536 -299
- package/dist/cjs/extension.js +9 -2
- package/dist/cjs/generated/rpc.js +1319 -14
- package/dist/cjs/index.js +22 -7
- package/dist/cjs/session.js +179 -93
- package/dist/cjs/sessionFsProvider.js +34 -0
- package/dist/cjs/toolSet.js +107 -0
- package/dist/cjs/types.js +38 -4
- package/dist/client.d.ts +63 -73
- package/dist/client.js +537 -300
- package/dist/extension.d.ts +3 -1
- package/dist/extension.js +10 -2
- package/dist/generated/rpc.d.ts +10187 -1028
- package/dist/generated/rpc.js +1319 -14
- package/dist/generated/session-events.d.ts +1982 -195
- package/dist/index.d.ts +6 -2
- package/dist/index.js +15 -2
- package/dist/session.d.ts +18 -186
- package/dist/session.js +179 -92
- package/dist/sessionFsProvider.d.ts +29 -2
- package/dist/sessionFsProvider.js +34 -0
- package/dist/toolSet.d.ts +75 -0
- package/dist/toolSet.js +82 -0
- package/dist/types.d.ts +730 -123
- package/dist/types.js +36 -3
- package/docs/agent-author.md +31 -7
- package/docs/examples.md +23 -16
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -18,11 +18,12 @@ import {
|
|
|
18
18
|
registerClientSessionApiHandlers
|
|
19
19
|
} from "./generated/rpc.js";
|
|
20
20
|
import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
|
|
21
|
-
import { CopilotSession
|
|
21
|
+
import { CopilotSession } from "./session.js";
|
|
22
22
|
import { createSessionFsAdapter } from "./sessionFsProvider.js";
|
|
23
23
|
import { getTraceContext } from "./telemetry.js";
|
|
24
|
+
import { ToolSet } from "./toolSet.js";
|
|
24
25
|
import { defaultJoinSessionPermissionHandler } from "./types.js";
|
|
25
|
-
const MIN_PROTOCOL_VERSION =
|
|
26
|
+
const MIN_PROTOCOL_VERSION = 3;
|
|
26
27
|
function isZodSchema(value) {
|
|
27
28
|
return value != null && typeof value === "object" && "toJSONSchema" in value && typeof value.toJSONSchema === "function";
|
|
28
29
|
}
|
|
@@ -33,6 +34,53 @@ function toJsonSchema(parameters) {
|
|
|
33
34
|
}
|
|
34
35
|
return parameters;
|
|
35
36
|
}
|
|
37
|
+
function toWireMcpServers(mcpServers) {
|
|
38
|
+
if (!mcpServers) return void 0;
|
|
39
|
+
return Object.fromEntries(
|
|
40
|
+
Object.entries(mcpServers).map(([name, server]) => {
|
|
41
|
+
if ("workingDirectory" in server) {
|
|
42
|
+
const { workingDirectory, ...rest } = server;
|
|
43
|
+
return [name, { ...rest, cwd: workingDirectory }];
|
|
44
|
+
}
|
|
45
|
+
return [name, server];
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function toWireCustomAgents(agents) {
|
|
50
|
+
if (!agents) return void 0;
|
|
51
|
+
return agents.map((agent) => {
|
|
52
|
+
if (!agent.mcpServers) return agent;
|
|
53
|
+
const { mcpServers, ...rest } = agent;
|
|
54
|
+
return { ...rest, mcpServers: toWireMcpServers(mcpServers) };
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function toWireLargeOutput(config) {
|
|
58
|
+
if (!config) return void 0;
|
|
59
|
+
const { outputDirectory, ...rest } = config;
|
|
60
|
+
const wire = { ...rest };
|
|
61
|
+
if (outputDirectory !== void 0) {
|
|
62
|
+
wire.outputDir = outputDirectory;
|
|
63
|
+
}
|
|
64
|
+
return wire;
|
|
65
|
+
}
|
|
66
|
+
function toolFilterListToArray(value) {
|
|
67
|
+
if (value === void 0) {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
return value instanceof ToolSet ? value.toArray() : value;
|
|
71
|
+
}
|
|
72
|
+
function validateToolFilterList(field, list) {
|
|
73
|
+
if (!list) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const entry of list) {
|
|
77
|
+
if (entry === "*") {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Invalid ${field} entry '*': there is no bare wildcard. Use one or more of \`new ToolSet().addBuiltIn('*')\`, \`.addMcp('*')\`, or \`.addCustom('*')\` to target a specific source.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
36
84
|
function extractTransformCallbacks(systemMessage) {
|
|
37
85
|
if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
|
|
38
86
|
return { wirePayload: systemMessage, transformCallbacks: void 0 };
|
|
@@ -86,12 +134,18 @@ class CopilotClient {
|
|
|
86
134
|
cliProcess = null;
|
|
87
135
|
connection = null;
|
|
88
136
|
socket = null;
|
|
89
|
-
|
|
137
|
+
runtimePort = null;
|
|
90
138
|
actualHost = "localhost";
|
|
91
139
|
state = "disconnected";
|
|
92
140
|
sessions = /* @__PURE__ */ new Map();
|
|
93
141
|
stderrBuffer = "";
|
|
94
142
|
// Captures CLI stderr for error messages
|
|
143
|
+
/** Resolved connection mode chosen in the constructor. */
|
|
144
|
+
connectionConfig;
|
|
145
|
+
/** Resolved path to the runtime executable (only used for child-process kinds). */
|
|
146
|
+
resolvedCliPath;
|
|
147
|
+
/** Resolved environment passed to the spawned runtime. */
|
|
148
|
+
resolvedEnv;
|
|
95
149
|
options;
|
|
96
150
|
isExternalServer = false;
|
|
97
151
|
forceStopping = false;
|
|
@@ -140,85 +194,90 @@ class CopilotClient {
|
|
|
140
194
|
* Creates a new CopilotClient instance.
|
|
141
195
|
*
|
|
142
196
|
* @param options - Configuration options for the client
|
|
143
|
-
* @throws Error if mutually exclusive options are provided (e.g., cliUrl with useStdio or cliPath)
|
|
144
197
|
*
|
|
145
198
|
* @example
|
|
146
199
|
* ```typescript
|
|
147
|
-
* // Default
|
|
200
|
+
* // Default: spawns the bundled runtime over stdio
|
|
148
201
|
* const client = new CopilotClient();
|
|
149
202
|
*
|
|
150
|
-
* // Connect to an existing
|
|
151
|
-
* const client = new CopilotClient({
|
|
203
|
+
* // Connect to an existing runtime
|
|
204
|
+
* const client = new CopilotClient({
|
|
205
|
+
* connection: RuntimeConnection.forUri("localhost:3000"),
|
|
206
|
+
* });
|
|
207
|
+
*
|
|
208
|
+
* // Spawn the runtime over TCP on a chosen port
|
|
209
|
+
* const client = new CopilotClient({
|
|
210
|
+
* connection: RuntimeConnection.forTcp({ port: 9001 }),
|
|
211
|
+
* });
|
|
152
212
|
*
|
|
153
|
-
* //
|
|
213
|
+
* // Use a custom runtime binary
|
|
154
214
|
* const client = new CopilotClient({
|
|
155
|
-
*
|
|
156
|
-
* logLevel: "debug"
|
|
215
|
+
* connection: RuntimeConnection.forStdio({ path: "/usr/local/bin/copilot" }),
|
|
216
|
+
* logLevel: "debug",
|
|
157
217
|
* });
|
|
158
218
|
* ```
|
|
159
219
|
*/
|
|
160
220
|
constructor(options = {}) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
if (options.isChildProcess && (options.cliUrl || options.useStdio === false)) {
|
|
165
|
-
throw new Error(
|
|
166
|
-
"isChildProcess must be used in conjunction with useStdio and not with cliUrl"
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
if (options.cliUrl && (options.gitHubToken || options.useLoggedInUser !== void 0)) {
|
|
221
|
+
const conn = options._internalConnection ?? options.connection ?? { kind: "stdio" };
|
|
222
|
+
if (conn.kind === "uri" && (options.gitHubToken !== void 0 || options.useLoggedInUser !== void 0)) {
|
|
170
223
|
throw new Error(
|
|
171
|
-
"gitHubToken and useLoggedInUser cannot be used with
|
|
224
|
+
"gitHubToken and useLoggedInUser cannot be used with RuntimeConnection.forUri (external server manages its own auth)"
|
|
172
225
|
);
|
|
173
226
|
}
|
|
174
|
-
if (
|
|
175
|
-
if (typeof
|
|
176
|
-
throw new Error("
|
|
177
|
-
}
|
|
178
|
-
if (options.useStdio === true) {
|
|
179
|
-
throw new Error("tcpConnectionToken cannot be used with useStdio: true");
|
|
227
|
+
if (conn.kind === "tcp" && conn.connectionToken !== void 0) {
|
|
228
|
+
if (typeof conn.connectionToken !== "string" || conn.connectionToken.length === 0) {
|
|
229
|
+
throw new Error("connectionToken must be a non-empty string");
|
|
180
230
|
}
|
|
181
231
|
}
|
|
182
|
-
|
|
183
|
-
const sdkSpawnsCli = !willUseStdio && !options.cliUrl && !options.isChildProcess;
|
|
184
|
-
this.effectiveConnectionToken = options.tcpConnectionToken ?? (sdkSpawnsCli ? randomUUID() : void 0);
|
|
232
|
+
this.connectionConfig = conn;
|
|
185
233
|
if (options.sessionFs) {
|
|
186
234
|
this.validateSessionFsConfig(options.sessionFs);
|
|
187
235
|
}
|
|
188
|
-
if (
|
|
189
|
-
const { host, port } = this.parseCliUrl(
|
|
236
|
+
if (conn.kind === "uri") {
|
|
237
|
+
const { host, port } = this.parseCliUrl(conn.url);
|
|
190
238
|
this.actualHost = host;
|
|
191
|
-
this.
|
|
239
|
+
this.runtimePort = port;
|
|
192
240
|
this.isExternalServer = true;
|
|
193
|
-
}
|
|
194
|
-
if (options.isChildProcess) {
|
|
241
|
+
} else if (conn.kind === "parent-process") {
|
|
195
242
|
this.isExternalServer = true;
|
|
196
243
|
}
|
|
244
|
+
if (conn.kind === "tcp") {
|
|
245
|
+
this.effectiveConnectionToken = conn.connectionToken ?? randomUUID();
|
|
246
|
+
} else if (conn.kind === "uri") {
|
|
247
|
+
this.effectiveConnectionToken = conn.connectionToken;
|
|
248
|
+
}
|
|
197
249
|
this.onListModels = options.onListModels;
|
|
198
250
|
this.onGetTraceContext = options.onGetTraceContext;
|
|
199
251
|
this.sessionFsConfig = options.sessionFs ?? null;
|
|
200
252
|
const effectiveEnv = options.env ?? process.env;
|
|
253
|
+
this.resolvedEnv = effectiveEnv;
|
|
254
|
+
this.resolvedCliPath = conn.kind === "stdio" || conn.kind === "tcp" ? conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath() : void 0;
|
|
255
|
+
const connArgs = conn.kind === "stdio" || conn.kind === "tcp" ? conn.args ?? [] : [];
|
|
256
|
+
this.connectionExtraArgs = [...connArgs];
|
|
201
257
|
this.options = {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
cwd: options.cwd ?? process.cwd(),
|
|
205
|
-
port: options.port || 0,
|
|
206
|
-
useStdio: options.cliUrl ? false : options.useStdio ?? true,
|
|
207
|
-
// Default to stdio unless cliUrl is provided
|
|
208
|
-
isChildProcess: options.isChildProcess ?? false,
|
|
209
|
-
cliUrl: options.cliUrl,
|
|
210
|
-
logLevel: options.logLevel || "debug",
|
|
211
|
-
autoStart: options.autoStart ?? true,
|
|
212
|
-
autoRestart: false,
|
|
213
|
-
env: effectiveEnv,
|
|
258
|
+
workingDirectory: options.workingDirectory ?? process.cwd(),
|
|
259
|
+
logLevel: options.logLevel,
|
|
214
260
|
gitHubToken: options.gitHubToken,
|
|
215
|
-
// Default useLoggedInUser to false when gitHubToken is provided, otherwise true
|
|
261
|
+
// Default useLoggedInUser to false when gitHubToken is provided, otherwise true.
|
|
216
262
|
useLoggedInUser: options.useLoggedInUser ?? (options.gitHubToken ? false : true),
|
|
217
263
|
telemetry: options.telemetry,
|
|
218
|
-
|
|
219
|
-
sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0
|
|
264
|
+
baseDirectory: options.baseDirectory,
|
|
265
|
+
sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
|
|
266
|
+
enableRemoteSessions: options.enableRemoteSessions ?? false,
|
|
267
|
+
mode: options.mode ?? "copilot-cli"
|
|
220
268
|
};
|
|
269
|
+
if (this.options.mode === "empty") {
|
|
270
|
+
const hasPersistence = this.options.baseDirectory !== void 0 || this.sessionFsConfig !== null || // External runtimes manage their own persistence layer; the SDK
|
|
271
|
+
// can't enforce it from here.
|
|
272
|
+
conn.kind === "uri" || conn.kind === "parent-process";
|
|
273
|
+
if (!hasPersistence) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
"CopilotClient was created with mode: 'empty' but neither 'baseDirectory' nor 'sessionFs' was set. Empty mode requires an explicit per-session persistence location; pick one."
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
221
279
|
}
|
|
280
|
+
connectionExtraArgs = [];
|
|
222
281
|
/**
|
|
223
282
|
* Parse CLI URL into host and port
|
|
224
283
|
* Supports formats: "host:port", "http://host:port", "https://host:port", or just "port"
|
|
@@ -252,20 +311,37 @@ class CopilotClient {
|
|
|
252
311
|
throw new Error("sessionFs.conventions must be either 'windows' or 'posix'");
|
|
253
312
|
}
|
|
254
313
|
}
|
|
314
|
+
setupSessionFs(session, config) {
|
|
315
|
+
if (!this.sessionFsConfig) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (!config.createSessionFsProvider) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"createSessionFsProvider is required in session config when sessionFs is enabled in client options."
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const provider = config.createSessionFsProvider(session);
|
|
324
|
+
if (this.sessionFsConfig.capabilities?.sqlite && !provider.sqlite) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
"SessionFsConfig declares capabilities.sqlite but the provider does not implement sqlite."
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
session.clientSessionApis.sessionFs = createSessionFsAdapter(provider);
|
|
330
|
+
}
|
|
255
331
|
/**
|
|
256
332
|
* Starts the CLI server and establishes a connection.
|
|
257
333
|
*
|
|
258
334
|
* If connecting to an external server (via cliUrl), only establishes the connection.
|
|
259
335
|
* Otherwise, spawns the CLI server process and then connects.
|
|
260
336
|
*
|
|
261
|
-
* This method is called automatically
|
|
337
|
+
* This method is called automatically the first time you create or resume a session.
|
|
262
338
|
*
|
|
263
339
|
* @returns A promise that resolves when the connection is established
|
|
264
340
|
* @throws Error if the server fails to start or the connection fails
|
|
265
341
|
*
|
|
266
342
|
* @example
|
|
267
343
|
* ```typescript
|
|
268
|
-
* const client = new CopilotClient(
|
|
344
|
+
* const client = new CopilotClient();
|
|
269
345
|
* await client.start();
|
|
270
346
|
* // Now ready to create sessions
|
|
271
347
|
* ```
|
|
@@ -285,7 +361,8 @@ class CopilotClient {
|
|
|
285
361
|
await this.connection.sendRequest("sessionFs.setProvider", {
|
|
286
362
|
initialCwd: this.sessionFsConfig.initialCwd,
|
|
287
363
|
sessionStatePath: this.sessionFsConfig.sessionStatePath,
|
|
288
|
-
conventions: this.sessionFsConfig.conventions
|
|
364
|
+
conventions: this.sessionFsConfig.conventions,
|
|
365
|
+
capabilities: this.sessionFsConfig.capabilities
|
|
289
366
|
});
|
|
290
367
|
}
|
|
291
368
|
this.state = "connected";
|
|
@@ -359,8 +436,15 @@ class CopilotClient {
|
|
|
359
436
|
}
|
|
360
437
|
this.modelsCache = null;
|
|
361
438
|
if (this.socket) {
|
|
439
|
+
const socket = this.socket;
|
|
440
|
+
this.socket = null;
|
|
362
441
|
try {
|
|
363
|
-
|
|
442
|
+
if (!socket.destroyed) {
|
|
443
|
+
await new Promise((resolve) => {
|
|
444
|
+
socket.once("close", () => resolve());
|
|
445
|
+
socket.end();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
364
448
|
} catch (error) {
|
|
365
449
|
errors.push(
|
|
366
450
|
new Error(
|
|
@@ -368,11 +452,18 @@ class CopilotClient {
|
|
|
368
452
|
)
|
|
369
453
|
);
|
|
370
454
|
}
|
|
371
|
-
this.socket = null;
|
|
372
455
|
}
|
|
373
456
|
if (this.cliProcess && !this.isExternalServer) {
|
|
457
|
+
const child = this.cliProcess;
|
|
458
|
+
this.cliProcess = null;
|
|
374
459
|
try {
|
|
375
|
-
|
|
460
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
461
|
+
const exited = new Promise((resolve) => {
|
|
462
|
+
child.once("exit", () => resolve());
|
|
463
|
+
});
|
|
464
|
+
child.kill();
|
|
465
|
+
await exited;
|
|
466
|
+
}
|
|
376
467
|
} catch (error) {
|
|
377
468
|
errors.push(
|
|
378
469
|
new Error(
|
|
@@ -380,18 +471,32 @@ class CopilotClient {
|
|
|
380
471
|
)
|
|
381
472
|
);
|
|
382
473
|
}
|
|
383
|
-
this.cliProcess = null;
|
|
384
474
|
}
|
|
385
475
|
if (this.cliStartTimeout) {
|
|
386
476
|
clearTimeout(this.cliStartTimeout);
|
|
387
477
|
this.cliStartTimeout = null;
|
|
388
478
|
}
|
|
389
479
|
this.state = "disconnected";
|
|
390
|
-
this.
|
|
480
|
+
this.runtimePort = null;
|
|
391
481
|
this.stderrBuffer = "";
|
|
392
482
|
this.processExitPromise = null;
|
|
393
483
|
return errors;
|
|
394
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Alias for {@link stop} that lets `CopilotClient` participate in `await using`
|
|
487
|
+
* blocks for automatic cleanup.
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* await using client = new CopilotClient();
|
|
492
|
+
* const session = await client.createSession({ onPermissionRequest: approveAll });
|
|
493
|
+
* await session.sendAndWait("Hello");
|
|
494
|
+
* // client.stop() is called automatically when the block exits.
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
async [Symbol.asyncDispose]() {
|
|
498
|
+
await this.stop();
|
|
499
|
+
}
|
|
395
500
|
/**
|
|
396
501
|
* Forcefully stops the CLI server without graceful cleanup.
|
|
397
502
|
*
|
|
@@ -448,7 +553,7 @@ class CopilotClient {
|
|
|
448
553
|
this.cliStartTimeout = null;
|
|
449
554
|
}
|
|
450
555
|
this.state = "disconnected";
|
|
451
|
-
this.
|
|
556
|
+
this.runtimePort = null;
|
|
452
557
|
this.stderrBuffer = "";
|
|
453
558
|
this.processExitPromise = null;
|
|
454
559
|
}
|
|
@@ -456,12 +561,11 @@ class CopilotClient {
|
|
|
456
561
|
* Creates a new conversation session with the Copilot CLI.
|
|
457
562
|
*
|
|
458
563
|
* Sessions maintain conversation state, handle events, and manage tool execution.
|
|
459
|
-
* If the client is not connected
|
|
460
|
-
* start the connection.
|
|
564
|
+
* If the client is not connected, this method automatically starts the connection.
|
|
461
565
|
*
|
|
462
566
|
* @param config - Optional configuration for the session
|
|
463
567
|
* @returns A promise that resolves with the created session
|
|
464
|
-
* @throws Error if the client
|
|
568
|
+
* @throws Error if the client fails to start
|
|
465
569
|
*
|
|
466
570
|
* @example
|
|
467
571
|
* ```typescript
|
|
@@ -481,66 +585,189 @@ class CopilotClient {
|
|
|
481
585
|
* });
|
|
482
586
|
* ```
|
|
483
587
|
*/
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
588
|
+
/**
|
|
589
|
+
* Normalizes session-level tool filter options. Converts {@link ToolSet}
|
|
590
|
+
* instances to plain string arrays, rejects misuse (bare `"*"`) and the
|
|
591
|
+
* missing-availableTools case in `mode = "empty"`.
|
|
592
|
+
*
|
|
593
|
+
* The SDK always sends `toolFilterPrecedence: "excluded"` so callers can
|
|
594
|
+
* compose include + exclude lists naturally (e.g. "everything matching X
|
|
595
|
+
* except Y") regardless of mode. Allowlist-precedence is intentionally not
|
|
596
|
+
* exposed — it's available on the runtime side as a CLI-only concession to
|
|
597
|
+
* legacy behavior, but SDK consumers always get the composable semantics.
|
|
598
|
+
*
|
|
599
|
+
* @internal
|
|
600
|
+
*/
|
|
601
|
+
resolveToolFilterOptions(config) {
|
|
602
|
+
const availableTools = toolFilterListToArray(config.availableTools);
|
|
603
|
+
const excludedTools = toolFilterListToArray(config.excludedTools);
|
|
604
|
+
validateToolFilterList("availableTools", availableTools);
|
|
605
|
+
validateToolFilterList("excludedTools", excludedTools);
|
|
606
|
+
if (this.options.mode === "empty") {
|
|
607
|
+
if (availableTools === void 0) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
"CopilotClient is in mode: 'empty' but the session config did not specify 'availableTools'. Empty mode requires every session to explicitly opt into the tools it wants \u2014 e.g. `new ToolSet().addBuiltIn(BuiltInTools.Isolated)`."
|
|
610
|
+
);
|
|
495
611
|
}
|
|
496
612
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
613
|
+
return { availableTools, excludedTools, toolFilterPrecedence: "excluded" };
|
|
614
|
+
}
|
|
615
|
+
/** Mode-specific defaults spread under the caller's config (app values win). */
|
|
616
|
+
configDefaultsForMode() {
|
|
617
|
+
if (this.options.mode === "empty") {
|
|
618
|
+
return {
|
|
619
|
+
enableSessionTelemetry: false,
|
|
620
|
+
mcpOAuthTokenStorage: "in-memory",
|
|
621
|
+
skipEmbeddingRetrieval: true,
|
|
622
|
+
embeddingCacheStorage: "in-memory",
|
|
623
|
+
enableOnDemandInstructionDiscovery: false,
|
|
624
|
+
enableFileHooks: false,
|
|
625
|
+
enableHostGitOperations: false,
|
|
626
|
+
enableSessionStore: false,
|
|
627
|
+
enableSkills: false
|
|
628
|
+
};
|
|
509
629
|
}
|
|
510
|
-
|
|
511
|
-
|
|
630
|
+
return {};
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Returns the systemMessage config to use, adjusted for the current mode.
|
|
634
|
+
* In empty mode we ensure the environment_context section is removed
|
|
635
|
+
* unless the app has already taken control of it. `append` (and
|
|
636
|
+
* unspecified) mode is promoted to `customize` so we can also strip
|
|
637
|
+
* environment_context; the caller's `content` is preserved verbatim
|
|
638
|
+
* because the runtime appends it as additional instructions in both
|
|
639
|
+
* customize and append modes.
|
|
640
|
+
*/
|
|
641
|
+
getSystemMessageConfigForMode(supplied) {
|
|
642
|
+
if (this.options.mode !== "empty") return supplied;
|
|
643
|
+
if (!supplied) {
|
|
644
|
+
return {
|
|
645
|
+
mode: "customize",
|
|
646
|
+
sections: { environment_context: { action: "remove" } }
|
|
647
|
+
};
|
|
512
648
|
}
|
|
513
|
-
|
|
514
|
-
|
|
649
|
+
switch (supplied.mode) {
|
|
650
|
+
case "replace":
|
|
651
|
+
return supplied;
|
|
652
|
+
case "customize":
|
|
653
|
+
if (supplied.sections?.environment_context) return supplied;
|
|
654
|
+
return {
|
|
655
|
+
...supplied,
|
|
656
|
+
sections: {
|
|
657
|
+
...supplied.sections,
|
|
658
|
+
environment_context: { action: "remove" }
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
case "append":
|
|
662
|
+
case void 0:
|
|
663
|
+
return {
|
|
664
|
+
mode: "customize",
|
|
665
|
+
content: supplied.content,
|
|
666
|
+
sections: { environment_context: { action: "remove" } }
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Mode-specific options applied via session.options.update after create/resume.
|
|
672
|
+
*
|
|
673
|
+
* In empty mode, defaults the four overridable feature flags to safe values
|
|
674
|
+
* (caller values from `config` win). `installedPlugins=[]` is unconditional
|
|
675
|
+
* in empty mode — apps that need custom plugins should switch modes.
|
|
676
|
+
*/
|
|
677
|
+
async updateSessionOptionsForMode(session, config) {
|
|
678
|
+
const patch = {};
|
|
679
|
+
if (this.options.mode === "empty") {
|
|
680
|
+
patch.skipCustomInstructions = config.skipCustomInstructions ?? true;
|
|
681
|
+
patch.customAgentsLocalOnly = config.customAgentsLocalOnly ?? true;
|
|
682
|
+
patch.coauthorEnabled = config.coauthorEnabled ?? false;
|
|
683
|
+
patch.manageScheduleEnabled = config.manageScheduleEnabled ?? false;
|
|
684
|
+
patch.installedPlugins = [];
|
|
685
|
+
} else {
|
|
686
|
+
if (config.skipCustomInstructions !== void 0)
|
|
687
|
+
patch.skipCustomInstructions = config.skipCustomInstructions;
|
|
688
|
+
if (config.customAgentsLocalOnly !== void 0)
|
|
689
|
+
patch.customAgentsLocalOnly = config.customAgentsLocalOnly;
|
|
690
|
+
if (config.coauthorEnabled !== void 0)
|
|
691
|
+
patch.coauthorEnabled = config.coauthorEnabled;
|
|
692
|
+
if (config.manageScheduleEnabled !== void 0)
|
|
693
|
+
patch.manageScheduleEnabled = config.manageScheduleEnabled;
|
|
694
|
+
}
|
|
695
|
+
if (Object.keys(patch).length === 0) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
try {
|
|
699
|
+
await session.rpc.options.update(patch);
|
|
700
|
+
} catch (e) {
|
|
701
|
+
try {
|
|
702
|
+
await session.disconnect();
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
throw e;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async createSession(config) {
|
|
709
|
+
if (!this.connection) {
|
|
710
|
+
await this.start();
|
|
515
711
|
}
|
|
712
|
+
config = { ...this.configDefaultsForMode(), ...config };
|
|
713
|
+
config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
|
|
714
|
+
const callerSessionId = config.sessionId;
|
|
715
|
+
const useServerGeneratedId = config.cloud != null && callerSessionId == null;
|
|
716
|
+
const localSessionId = useServerGeneratedId ? void 0 : callerSessionId ?? randomUUID();
|
|
516
717
|
const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
|
|
517
718
|
config.systemMessage
|
|
518
719
|
);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
);
|
|
720
|
+
const initializeSession = (sessionId) => {
|
|
721
|
+
const s = new CopilotSession(
|
|
722
|
+
sessionId,
|
|
723
|
+
this.connection,
|
|
724
|
+
void 0,
|
|
725
|
+
this.onGetTraceContext
|
|
726
|
+
);
|
|
727
|
+
s.registerTools(config.tools);
|
|
728
|
+
s.registerCanvases(config.canvases);
|
|
729
|
+
s.registerCommands(config.commands);
|
|
730
|
+
s.registerPermissionHandler(config.onPermissionRequest);
|
|
731
|
+
if (config.onUserInputRequest) {
|
|
732
|
+
s.registerUserInputHandler(config.onUserInputRequest);
|
|
733
|
+
}
|
|
734
|
+
if (config.onElicitationRequest) {
|
|
735
|
+
s.registerElicitationHandler(config.onElicitationRequest);
|
|
736
|
+
}
|
|
737
|
+
if (config.onExitPlanModeRequest) {
|
|
738
|
+
s.registerExitPlanModeHandler(config.onExitPlanModeRequest);
|
|
739
|
+
}
|
|
740
|
+
if (config.onAutoModeSwitchRequest) {
|
|
741
|
+
s.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
|
|
742
|
+
}
|
|
743
|
+
if (config.hooks) {
|
|
744
|
+
s.registerHooks(config.hooks);
|
|
745
|
+
}
|
|
746
|
+
if (transformCallbacks) {
|
|
747
|
+
s.registerTransformCallbacks(transformCallbacks);
|
|
748
|
+
}
|
|
749
|
+
if (config.onEvent) {
|
|
750
|
+
s.on(config.onEvent);
|
|
535
751
|
}
|
|
752
|
+
this.sessions.set(sessionId, s);
|
|
753
|
+
this.setupSessionFs(s, config);
|
|
754
|
+
return s;
|
|
755
|
+
};
|
|
756
|
+
let session;
|
|
757
|
+
let registeredId;
|
|
758
|
+
if (localSessionId !== void 0) {
|
|
759
|
+
session = initializeSession(localSessionId);
|
|
760
|
+
registeredId = localSessionId;
|
|
536
761
|
}
|
|
762
|
+
const toolFilterOptions = this.resolveToolFilterOptions(config);
|
|
537
763
|
try {
|
|
538
764
|
const response = await this.connection.sendRequest("session.create", {
|
|
539
765
|
...await getTraceContext(this.onGetTraceContext),
|
|
540
766
|
model: config.model,
|
|
541
|
-
sessionId,
|
|
767
|
+
sessionId: localSessionId,
|
|
542
768
|
clientName: config.clientName,
|
|
543
769
|
reasoningEffort: config.reasoningEffort,
|
|
770
|
+
reasoningSummary: config.reasoningSummary,
|
|
544
771
|
tools: config.tools?.map((tool) => ({
|
|
545
772
|
name: tool.name,
|
|
546
773
|
description: tool.description,
|
|
@@ -548,40 +775,81 @@ class CopilotClient {
|
|
|
548
775
|
overridesBuiltInTool: tool.overridesBuiltInTool,
|
|
549
776
|
skipPermission: tool.skipPermission
|
|
550
777
|
})),
|
|
778
|
+
canvases: config.canvases?.map((canvas) => canvas.declaration),
|
|
779
|
+
requestCanvasRenderer: config.requestCanvasRenderer,
|
|
780
|
+
requestExtensions: config.requestExtensions,
|
|
781
|
+
extensionInfo: config.extensionInfo,
|
|
551
782
|
commands: config.commands?.map((cmd) => ({
|
|
552
783
|
name: cmd.name,
|
|
553
784
|
description: cmd.description
|
|
554
785
|
})),
|
|
555
786
|
systemMessage: wireSystemMessage,
|
|
556
|
-
availableTools:
|
|
557
|
-
excludedTools:
|
|
787
|
+
availableTools: toolFilterOptions.availableTools,
|
|
788
|
+
excludedTools: toolFilterOptions.excludedTools,
|
|
789
|
+
toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
|
|
558
790
|
provider: config.provider,
|
|
791
|
+
enableSessionTelemetry: config.enableSessionTelemetry,
|
|
559
792
|
modelCapabilities: config.modelCapabilities,
|
|
560
|
-
|
|
793
|
+
largeOutput: toWireLargeOutput(config.largeOutput),
|
|
794
|
+
requestPermission: !!config.onPermissionRequest,
|
|
561
795
|
requestUserInput: !!config.onUserInputRequest,
|
|
562
796
|
requestElicitation: !!config.onElicitationRequest,
|
|
797
|
+
...config.enableMcpApps ? { requestMcpApps: true } : {},
|
|
798
|
+
requestExitPlanMode: !!config.onExitPlanModeRequest,
|
|
799
|
+
requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
|
|
563
800
|
hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
|
|
564
801
|
workingDirectory: config.workingDirectory,
|
|
565
802
|
streaming: config.streaming,
|
|
566
803
|
includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
|
|
567
|
-
mcpServers: config.mcpServers,
|
|
804
|
+
mcpServers: toWireMcpServers(config.mcpServers),
|
|
805
|
+
mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
|
|
568
806
|
envValueMode: "direct",
|
|
569
|
-
customAgents: config.customAgents,
|
|
807
|
+
customAgents: toWireCustomAgents(config.customAgents),
|
|
570
808
|
defaultAgent: config.defaultAgent,
|
|
571
809
|
agent: config.agent,
|
|
572
|
-
configDir: config.
|
|
810
|
+
configDir: config.configDirectory,
|
|
573
811
|
enableConfigDiscovery: config.enableConfigDiscovery,
|
|
812
|
+
skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
|
|
813
|
+
embeddingCacheStorage: config.embeddingCacheStorage,
|
|
814
|
+
organizationCustomInstructions: config.organizationCustomInstructions,
|
|
815
|
+
enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery,
|
|
816
|
+
enableFileHooks: config.enableFileHooks,
|
|
817
|
+
enableHostGitOperations: config.enableHostGitOperations,
|
|
818
|
+
enableSessionStore: config.enableSessionStore,
|
|
819
|
+
enableSkills: config.enableSkills,
|
|
574
820
|
skillDirectories: config.skillDirectories,
|
|
821
|
+
pluginDirectories: config.pluginDirectories,
|
|
575
822
|
instructionDirectories: config.instructionDirectories,
|
|
576
823
|
disabledSkills: config.disabledSkills,
|
|
577
824
|
infiniteSessions: config.infiniteSessions,
|
|
578
|
-
gitHubToken: config.gitHubToken
|
|
825
|
+
gitHubToken: config.gitHubToken,
|
|
826
|
+
remoteSession: config.remoteSession,
|
|
827
|
+
cloud: config.cloud
|
|
579
828
|
});
|
|
580
|
-
const {
|
|
829
|
+
const {
|
|
830
|
+
sessionId: returnedSessionId,
|
|
831
|
+
workspacePath,
|
|
832
|
+
capabilities
|
|
833
|
+
} = response;
|
|
834
|
+
if (!returnedSessionId) {
|
|
835
|
+
throw new Error("session.create response did not include a sessionId");
|
|
836
|
+
}
|
|
837
|
+
if (localSessionId !== void 0 && localSessionId !== returnedSessionId) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`session.create returned sessionId ${returnedSessionId} but the caller requested ${localSessionId}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
if (session === void 0) {
|
|
843
|
+
session = initializeSession(returnedSessionId);
|
|
844
|
+
registeredId = returnedSessionId;
|
|
845
|
+
}
|
|
581
846
|
session["_workspacePath"] = workspacePath;
|
|
582
847
|
session.setCapabilities(capabilities);
|
|
848
|
+
await this.updateSessionOptionsForMode(session, config);
|
|
583
849
|
} catch (e) {
|
|
584
|
-
|
|
850
|
+
if (registeredId !== void 0) {
|
|
851
|
+
this.sessions.delete(registeredId);
|
|
852
|
+
}
|
|
585
853
|
throw e;
|
|
586
854
|
}
|
|
587
855
|
return session;
|
|
@@ -611,17 +879,8 @@ class CopilotClient {
|
|
|
611
879
|
* ```
|
|
612
880
|
*/
|
|
613
881
|
async resumeSession(sessionId, config) {
|
|
614
|
-
if (!config?.onPermissionRequest) {
|
|
615
|
-
throw new Error(
|
|
616
|
-
"An onPermissionRequest handler is required when resuming a session. For example, to allow all permissions, use { onPermissionRequest: approveAll }."
|
|
617
|
-
);
|
|
618
|
-
}
|
|
619
882
|
if (!this.connection) {
|
|
620
|
-
|
|
621
|
-
await this.start();
|
|
622
|
-
} else {
|
|
623
|
-
throw new Error("Client not connected. Call start() first.");
|
|
624
|
-
}
|
|
883
|
+
await this.start();
|
|
625
884
|
}
|
|
626
885
|
const session = new CopilotSession(
|
|
627
886
|
sessionId,
|
|
@@ -630,6 +889,7 @@ class CopilotClient {
|
|
|
630
889
|
this.onGetTraceContext
|
|
631
890
|
);
|
|
632
891
|
session.registerTools(config.tools);
|
|
892
|
+
session.registerCanvases(config.canvases);
|
|
633
893
|
session.registerCommands(config.commands);
|
|
634
894
|
session.registerPermissionHandler(config.onPermissionRequest);
|
|
635
895
|
if (config.onUserInputRequest) {
|
|
@@ -638,9 +898,17 @@ class CopilotClient {
|
|
|
638
898
|
if (config.onElicitationRequest) {
|
|
639
899
|
session.registerElicitationHandler(config.onElicitationRequest);
|
|
640
900
|
}
|
|
901
|
+
if (config.onExitPlanModeRequest) {
|
|
902
|
+
session.registerExitPlanModeHandler(config.onExitPlanModeRequest);
|
|
903
|
+
}
|
|
904
|
+
if (config.onAutoModeSwitchRequest) {
|
|
905
|
+
session.registerAutoModeSwitchHandler(config.onAutoModeSwitchRequest);
|
|
906
|
+
}
|
|
641
907
|
if (config.hooks) {
|
|
642
908
|
session.registerHooks(config.hooks);
|
|
643
909
|
}
|
|
910
|
+
config = { ...this.configDefaultsForMode(), ...config };
|
|
911
|
+
config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
|
|
644
912
|
const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
|
|
645
913
|
config.systemMessage
|
|
646
914
|
);
|
|
@@ -651,17 +919,8 @@ class CopilotClient {
|
|
|
651
919
|
session.on(config.onEvent);
|
|
652
920
|
}
|
|
653
921
|
this.sessions.set(sessionId, session);
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
session.clientSessionApis.sessionFs = createSessionFsAdapter(
|
|
657
|
-
config.createSessionFsHandler(session)
|
|
658
|
-
);
|
|
659
|
-
} else {
|
|
660
|
-
throw new Error(
|
|
661
|
-
"createSessionFsHandler is required in session config when sessionFs is enabled in client options."
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
922
|
+
this.setupSessionFs(session, config);
|
|
923
|
+
const toolFilterOptions = this.resolveToolFilterOptions(config);
|
|
665
924
|
try {
|
|
666
925
|
const response = await this.connection.sendRequest("session.resume", {
|
|
667
926
|
...await getTraceContext(this.onGetTraceContext),
|
|
@@ -669,9 +928,12 @@ class CopilotClient {
|
|
|
669
928
|
clientName: config.clientName,
|
|
670
929
|
model: config.model,
|
|
671
930
|
reasoningEffort: config.reasoningEffort,
|
|
931
|
+
reasoningSummary: config.reasoningSummary,
|
|
672
932
|
systemMessage: wireSystemMessage,
|
|
673
|
-
availableTools:
|
|
674
|
-
excludedTools:
|
|
933
|
+
availableTools: toolFilterOptions.availableTools,
|
|
934
|
+
excludedTools: toolFilterOptions.excludedTools,
|
|
935
|
+
toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
|
|
936
|
+
enableSessionTelemetry: config.enableSessionTelemetry,
|
|
675
937
|
tools: config.tools?.map((tool) => ({
|
|
676
938
|
name: tool.name,
|
|
677
939
|
description: tool.description,
|
|
@@ -679,58 +941,65 @@ class CopilotClient {
|
|
|
679
941
|
overridesBuiltInTool: tool.overridesBuiltInTool,
|
|
680
942
|
skipPermission: tool.skipPermission
|
|
681
943
|
})),
|
|
944
|
+
canvases: config.canvases?.map((canvas) => canvas.declaration),
|
|
945
|
+
requestCanvasRenderer: config.requestCanvasRenderer,
|
|
946
|
+
requestExtensions: config.requestExtensions,
|
|
947
|
+
extensionInfo: config.extensionInfo,
|
|
682
948
|
commands: config.commands?.map((cmd) => ({
|
|
683
949
|
name: cmd.name,
|
|
684
950
|
description: cmd.description
|
|
685
951
|
})),
|
|
686
952
|
provider: config.provider,
|
|
687
953
|
modelCapabilities: config.modelCapabilities,
|
|
954
|
+
largeOutput: toWireLargeOutput(config.largeOutput),
|
|
688
955
|
requestPermission: config.onPermissionRequest !== defaultJoinSessionPermissionHandler,
|
|
689
956
|
requestUserInput: !!config.onUserInputRequest,
|
|
690
957
|
requestElicitation: !!config.onElicitationRequest,
|
|
958
|
+
...config.enableMcpApps ? { requestMcpApps: true } : {},
|
|
959
|
+
requestExitPlanMode: !!config.onExitPlanModeRequest,
|
|
960
|
+
requestAutoModeSwitch: !!config.onAutoModeSwitchRequest,
|
|
691
961
|
hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)),
|
|
692
962
|
workingDirectory: config.workingDirectory,
|
|
693
|
-
configDir: config.
|
|
963
|
+
configDir: config.configDirectory,
|
|
694
964
|
enableConfigDiscovery: config.enableConfigDiscovery,
|
|
965
|
+
skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
|
|
966
|
+
embeddingCacheStorage: config.embeddingCacheStorage,
|
|
967
|
+
organizationCustomInstructions: config.organizationCustomInstructions,
|
|
968
|
+
enableOnDemandInstructionDiscovery: config.enableOnDemandInstructionDiscovery,
|
|
969
|
+
enableFileHooks: config.enableFileHooks,
|
|
970
|
+
enableHostGitOperations: config.enableHostGitOperations,
|
|
971
|
+
enableSessionStore: config.enableSessionStore,
|
|
972
|
+
enableSkills: config.enableSkills,
|
|
695
973
|
streaming: config.streaming,
|
|
696
974
|
includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
|
|
697
|
-
mcpServers: config.mcpServers,
|
|
975
|
+
mcpServers: toWireMcpServers(config.mcpServers),
|
|
976
|
+
mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
|
|
698
977
|
envValueMode: "direct",
|
|
699
|
-
customAgents: config.customAgents,
|
|
978
|
+
customAgents: toWireCustomAgents(config.customAgents),
|
|
700
979
|
defaultAgent: config.defaultAgent,
|
|
701
980
|
agent: config.agent,
|
|
702
981
|
skillDirectories: config.skillDirectories,
|
|
982
|
+
pluginDirectories: config.pluginDirectories,
|
|
703
983
|
instructionDirectories: config.instructionDirectories,
|
|
704
984
|
disabledSkills: config.disabledSkills,
|
|
705
985
|
infiniteSessions: config.infiniteSessions,
|
|
706
|
-
|
|
986
|
+
suppressResumeEvent: config.suppressResumeEvent,
|
|
707
987
|
continuePendingWork: config.continuePendingWork,
|
|
708
|
-
gitHubToken: config.gitHubToken
|
|
988
|
+
gitHubToken: config.gitHubToken,
|
|
989
|
+
remoteSession: config.remoteSession,
|
|
990
|
+
openCanvases: config.openCanvases
|
|
709
991
|
});
|
|
710
|
-
const { workspacePath, capabilities } = response;
|
|
992
|
+
const { workspacePath, capabilities, openCanvases } = response;
|
|
711
993
|
session["_workspacePath"] = workspacePath;
|
|
712
994
|
session.setCapabilities(capabilities);
|
|
995
|
+
session.setOpenCanvases(openCanvases ?? []);
|
|
996
|
+
await this.updateSessionOptionsForMode(session, config);
|
|
713
997
|
} catch (e) {
|
|
714
998
|
this.sessions.delete(sessionId);
|
|
715
999
|
throw e;
|
|
716
1000
|
}
|
|
717
1001
|
return session;
|
|
718
1002
|
}
|
|
719
|
-
/**
|
|
720
|
-
* Gets the current connection state of the client.
|
|
721
|
-
*
|
|
722
|
-
* @returns The current connection state: "disconnected", "connecting", "connected", or "error"
|
|
723
|
-
*
|
|
724
|
-
* @example
|
|
725
|
-
* ```typescript
|
|
726
|
-
* if (client.getState() === "connected") {
|
|
727
|
-
* const session = await client.createSession({ onPermissionRequest: approveAll });
|
|
728
|
-
* }
|
|
729
|
-
* ```
|
|
730
|
-
*/
|
|
731
|
-
getState() {
|
|
732
|
-
return this.state;
|
|
733
|
-
}
|
|
734
1003
|
/**
|
|
735
1004
|
* Sends a ping request to the server to verify connectivity.
|
|
736
1005
|
*
|
|
@@ -843,7 +1112,7 @@ class CopilotClient {
|
|
|
843
1112
|
);
|
|
844
1113
|
serverVersion = result.protocolVersion;
|
|
845
1114
|
} catch (err) {
|
|
846
|
-
if (err instanceof ResponseError && err.code === ErrorCodes.MethodNotFound) {
|
|
1115
|
+
if (err instanceof ResponseError && (err.code === ErrorCodes.MethodNotFound || err.message === "Unhandled method connect")) {
|
|
847
1116
|
serverVersion = (await raceAgainstExit(this.ping())).protocolVersion;
|
|
848
1117
|
} else {
|
|
849
1118
|
throw err;
|
|
@@ -932,8 +1201,13 @@ class CopilotClient {
|
|
|
932
1201
|
if (!this.connection) {
|
|
933
1202
|
throw new Error("Client not connected");
|
|
934
1203
|
}
|
|
1204
|
+
let wireFilter;
|
|
1205
|
+
if (filter) {
|
|
1206
|
+
const { workingDirectory, ...rest } = filter;
|
|
1207
|
+
wireFilter = { ...rest, cwd: workingDirectory };
|
|
1208
|
+
}
|
|
935
1209
|
const response = await this.connection.sendRequest("session.list", {
|
|
936
|
-
filter
|
|
1210
|
+
filter: wireFilter
|
|
937
1211
|
});
|
|
938
1212
|
const { sessions } = response;
|
|
939
1213
|
return sessions.map(CopilotClient.toSessionMetadata);
|
|
@@ -968,13 +1242,19 @@ class CopilotClient {
|
|
|
968
1242
|
return CopilotClient.toSessionMetadata(session);
|
|
969
1243
|
}
|
|
970
1244
|
static toSessionMetadata(raw) {
|
|
1245
|
+
const { context } = raw;
|
|
971
1246
|
return {
|
|
972
1247
|
sessionId: raw.sessionId,
|
|
973
1248
|
startTime: new Date(raw.startTime),
|
|
974
1249
|
modifiedTime: new Date(raw.modifiedTime),
|
|
975
1250
|
summary: raw.summary,
|
|
976
1251
|
isRemote: raw.isRemote,
|
|
977
|
-
context:
|
|
1252
|
+
context: context ? {
|
|
1253
|
+
workingDirectory: context.cwd,
|
|
1254
|
+
gitRoot: context.gitRoot,
|
|
1255
|
+
repository: context.repository,
|
|
1256
|
+
branch: context.branch
|
|
1257
|
+
} : void 0
|
|
978
1258
|
};
|
|
979
1259
|
}
|
|
980
1260
|
/**
|
|
@@ -1027,7 +1307,7 @@ class CopilotClient {
|
|
|
1027
1307
|
throw new Error(result.error || "Failed to set foreground session");
|
|
1028
1308
|
}
|
|
1029
1309
|
}
|
|
1030
|
-
|
|
1310
|
+
onLifecycle(eventTypeOrHandler, handler) {
|
|
1031
1311
|
if (typeof eventTypeOrHandler === "string" && handler) {
|
|
1032
1312
|
const eventType = eventTypeOrHandler;
|
|
1033
1313
|
if (!this.typedLifecycleHandlers.has(eventType)) {
|
|
@@ -1054,17 +1334,17 @@ class CopilotClient {
|
|
|
1054
1334
|
async startCLIServer() {
|
|
1055
1335
|
return new Promise((resolve, reject) => {
|
|
1056
1336
|
this.stderrBuffer = "";
|
|
1057
|
-
const args = [
|
|
1058
|
-
|
|
1059
|
-
"--
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
this.options.logLevel
|
|
1063
|
-
];
|
|
1064
|
-
if (this.options.useStdio) {
|
|
1337
|
+
const args = [...this.connectionExtraArgs, "--headless", "--no-auto-update"];
|
|
1338
|
+
if (this.options.logLevel) {
|
|
1339
|
+
args.push("--log-level", this.options.logLevel);
|
|
1340
|
+
}
|
|
1341
|
+
if (this.connectionConfig.kind === "stdio") {
|
|
1065
1342
|
args.push("--stdio");
|
|
1066
|
-
} else if (this.
|
|
1067
|
-
|
|
1343
|
+
} else if (this.connectionConfig.kind === "tcp") {
|
|
1344
|
+
const requestedPort = this.connectionConfig.port ?? 0;
|
|
1345
|
+
if (requestedPort > 0) {
|
|
1346
|
+
args.push("--port", requestedPort.toString());
|
|
1347
|
+
}
|
|
1068
1348
|
}
|
|
1069
1349
|
if (this.options.gitHubToken) {
|
|
1070
1350
|
args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
|
|
@@ -1078,7 +1358,10 @@ class CopilotClient {
|
|
|
1078
1358
|
this.options.sessionIdleTimeoutSeconds.toString()
|
|
1079
1359
|
);
|
|
1080
1360
|
}
|
|
1081
|
-
|
|
1361
|
+
if (this.options.enableRemoteSessions) {
|
|
1362
|
+
args.push("--remote");
|
|
1363
|
+
}
|
|
1364
|
+
const envWithoutNodeDebug = { ...this.resolvedEnv };
|
|
1082
1365
|
delete envWithoutNodeDebug.NODE_DEBUG;
|
|
1083
1366
|
if (this.options.gitHubToken) {
|
|
1084
1367
|
envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
|
|
@@ -1086,12 +1369,15 @@ class CopilotClient {
|
|
|
1086
1369
|
if (this.effectiveConnectionToken) {
|
|
1087
1370
|
envWithoutNodeDebug.COPILOT_CONNECTION_TOKEN = this.effectiveConnectionToken;
|
|
1088
1371
|
}
|
|
1089
|
-
if (this.options.
|
|
1090
|
-
envWithoutNodeDebug.COPILOT_HOME = this.options.
|
|
1372
|
+
if (this.options.baseDirectory) {
|
|
1373
|
+
envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory;
|
|
1091
1374
|
}
|
|
1092
|
-
if (
|
|
1375
|
+
if (this.options.mode === "empty") {
|
|
1376
|
+
envWithoutNodeDebug.COPILOT_DISABLE_KEYTAR = "1";
|
|
1377
|
+
}
|
|
1378
|
+
if (!this.resolvedCliPath) {
|
|
1093
1379
|
throw new Error(
|
|
1094
|
-
"Path to Copilot CLI is required. Please
|
|
1380
|
+
"Path to Copilot CLI is required. Please supply it via `RuntimeConnection.forStdio({ path })` or `RuntimeConnection.forTcp({ path })`, set the COPILOT_CLI_PATH environment variable, or use `RuntimeConnection.forUri(...)` to connect to an already-running runtime."
|
|
1095
1381
|
);
|
|
1096
1382
|
}
|
|
1097
1383
|
if (this.options.telemetry) {
|
|
@@ -1110,31 +1396,31 @@ class CopilotClient {
|
|
|
1110
1396
|
t.captureContent
|
|
1111
1397
|
);
|
|
1112
1398
|
}
|
|
1113
|
-
if (!existsSync(this.
|
|
1399
|
+
if (!existsSync(this.resolvedCliPath)) {
|
|
1114
1400
|
throw new Error(
|
|
1115
|
-
`Copilot CLI not found at ${this.
|
|
1401
|
+
`Copilot CLI not found at ${this.resolvedCliPath}. Ensure @github/copilot is installed.`
|
|
1116
1402
|
);
|
|
1117
1403
|
}
|
|
1118
|
-
const stdioConfig = this.
|
|
1119
|
-
const isJsFile = this.
|
|
1404
|
+
const stdioConfig = this.connectionConfig.kind === "stdio" ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
|
|
1405
|
+
const isJsFile = this.resolvedCliPath.endsWith(".js");
|
|
1120
1406
|
if (isJsFile) {
|
|
1121
|
-
this.cliProcess = spawn(getNodeExecPath(), [this.
|
|
1407
|
+
this.cliProcess = spawn(getNodeExecPath(), [this.resolvedCliPath, ...args], {
|
|
1122
1408
|
stdio: stdioConfig,
|
|
1123
|
-
cwd: this.options.
|
|
1409
|
+
cwd: this.options.workingDirectory,
|
|
1124
1410
|
env: envWithoutNodeDebug,
|
|
1125
1411
|
windowsHide: true
|
|
1126
1412
|
});
|
|
1127
1413
|
} else {
|
|
1128
|
-
this.cliProcess = spawn(this.
|
|
1414
|
+
this.cliProcess = spawn(this.resolvedCliPath, args, {
|
|
1129
1415
|
stdio: stdioConfig,
|
|
1130
|
-
cwd: this.options.
|
|
1416
|
+
cwd: this.options.workingDirectory,
|
|
1131
1417
|
env: envWithoutNodeDebug,
|
|
1132
1418
|
windowsHide: true
|
|
1133
1419
|
});
|
|
1134
1420
|
}
|
|
1135
1421
|
let stdout = "";
|
|
1136
1422
|
let resolved = false;
|
|
1137
|
-
if (this.
|
|
1423
|
+
if (this.connectionConfig.kind === "stdio") {
|
|
1138
1424
|
resolved = true;
|
|
1139
1425
|
resolve();
|
|
1140
1426
|
} else {
|
|
@@ -1142,7 +1428,7 @@ class CopilotClient {
|
|
|
1142
1428
|
stdout += data.toString();
|
|
1143
1429
|
const match = stdout.match(/listening on port (\d+)/i);
|
|
1144
1430
|
if (match && !resolved) {
|
|
1145
|
-
this.
|
|
1431
|
+
this.runtimePort = parseInt(match[1], 10);
|
|
1146
1432
|
resolved = true;
|
|
1147
1433
|
resolve();
|
|
1148
1434
|
}
|
|
@@ -1216,19 +1502,21 @@ stderr: ${stderrOutput}`
|
|
|
1216
1502
|
resolved = true;
|
|
1217
1503
|
reject(new Error("Timeout waiting for CLI server to start"));
|
|
1218
1504
|
}
|
|
1219
|
-
},
|
|
1505
|
+
}, 3e4);
|
|
1220
1506
|
});
|
|
1221
1507
|
}
|
|
1222
1508
|
/**
|
|
1223
1509
|
* Connect to the CLI server (via socket or stdio)
|
|
1224
1510
|
*/
|
|
1225
1511
|
async connectToServer() {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1512
|
+
switch (this.connectionConfig.kind) {
|
|
1513
|
+
case "parent-process":
|
|
1514
|
+
return this.connectToParentProcessViaStdio();
|
|
1515
|
+
case "stdio":
|
|
1516
|
+
return this.connectToChildProcessViaStdio();
|
|
1517
|
+
case "tcp":
|
|
1518
|
+
case "uri":
|
|
1519
|
+
return this.connectViaTcp();
|
|
1232
1520
|
}
|
|
1233
1521
|
}
|
|
1234
1522
|
/**
|
|
@@ -1268,12 +1556,17 @@ stderr: ${stderrOutput}`
|
|
|
1268
1556
|
* Connect to the CLI server via TCP socket
|
|
1269
1557
|
*/
|
|
1270
1558
|
async connectViaTcp() {
|
|
1271
|
-
if (!this.
|
|
1559
|
+
if (!this.runtimePort) {
|
|
1272
1560
|
throw new Error("Server port not available");
|
|
1273
1561
|
}
|
|
1274
1562
|
return new Promise((resolve, reject) => {
|
|
1275
1563
|
this.socket = new Socket();
|
|
1276
|
-
|
|
1564
|
+
const connectionTimeout = setTimeout(() => {
|
|
1565
|
+
this.socket?.destroy();
|
|
1566
|
+
reject(new Error("Timeout connecting to CLI server"));
|
|
1567
|
+
}, 1e4);
|
|
1568
|
+
this.socket.connect(this.runtimePort, this.actualHost, () => {
|
|
1569
|
+
clearTimeout(connectionTimeout);
|
|
1277
1570
|
this.connection = createMessageConnection(
|
|
1278
1571
|
new StreamMessageReader(this.socket),
|
|
1279
1572
|
new StreamMessageWriter(this.socket)
|
|
@@ -1283,6 +1576,7 @@ stderr: ${stderrOutput}`
|
|
|
1283
1576
|
resolve();
|
|
1284
1577
|
});
|
|
1285
1578
|
this.socket.on("error", (error) => {
|
|
1579
|
+
clearTimeout(connectionTimeout);
|
|
1286
1580
|
reject(new Error(`Failed to connect to CLI server: ${error.message}`));
|
|
1287
1581
|
});
|
|
1288
1582
|
});
|
|
@@ -1298,16 +1592,16 @@ stderr: ${stderrOutput}`
|
|
|
1298
1592
|
this.handleSessionLifecycleNotification(notification);
|
|
1299
1593
|
});
|
|
1300
1594
|
this.connection.onRequest(
|
|
1301
|
-
"
|
|
1302
|
-
async (params) => await this.
|
|
1595
|
+
"userInput.request",
|
|
1596
|
+
async (params) => await this.handleUserInputRequest(params)
|
|
1303
1597
|
);
|
|
1304
1598
|
this.connection.onRequest(
|
|
1305
|
-
"
|
|
1306
|
-
async (params) => await this.
|
|
1599
|
+
"exitPlanMode.request",
|
|
1600
|
+
async (params) => await this.handleExitPlanModeRequest(params)
|
|
1307
1601
|
);
|
|
1308
1602
|
this.connection.onRequest(
|
|
1309
|
-
"
|
|
1310
|
-
async (params) => await this.
|
|
1603
|
+
"autoModeSwitch.request",
|
|
1604
|
+
async (params) => await this.handleAutoModeSwitchRequest(params)
|
|
1311
1605
|
);
|
|
1312
1606
|
this.connection.onRequest(
|
|
1313
1607
|
"hooks.invoke",
|
|
@@ -1343,7 +1637,20 @@ stderr: ${stderrOutput}`
|
|
|
1343
1637
|
if (typeof notification !== "object" || !notification || !("type" in notification) || typeof notification.type !== "string" || !("sessionId" in notification) || typeof notification.sessionId !== "string") {
|
|
1344
1638
|
return;
|
|
1345
1639
|
}
|
|
1346
|
-
const
|
|
1640
|
+
const raw = notification;
|
|
1641
|
+
let metadata;
|
|
1642
|
+
if (raw.metadata && raw.metadata.startTime && raw.metadata.modifiedTime) {
|
|
1643
|
+
metadata = {
|
|
1644
|
+
startTime: new Date(raw.metadata.startTime),
|
|
1645
|
+
modifiedTime: new Date(raw.metadata.modifiedTime),
|
|
1646
|
+
summary: raw.metadata.summary
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
const event = {
|
|
1650
|
+
type: raw.type,
|
|
1651
|
+
sessionId: raw.sessionId,
|
|
1652
|
+
metadata
|
|
1653
|
+
};
|
|
1347
1654
|
const typedHandlers = this.typedLifecycleHandlers.get(event.type);
|
|
1348
1655
|
if (typedHandlers) {
|
|
1349
1656
|
for (const handler of typedHandlers) {
|
|
@@ -1375,125 +1682,55 @@ stderr: ${stderrOutput}`
|
|
|
1375
1682
|
});
|
|
1376
1683
|
return result;
|
|
1377
1684
|
}
|
|
1378
|
-
async
|
|
1379
|
-
if (!params || typeof params.sessionId !== "string" || typeof params.
|
|
1380
|
-
throw new Error("Invalid
|
|
1685
|
+
async handleExitPlanModeRequest(params) {
|
|
1686
|
+
if (!params || typeof params.sessionId !== "string" || typeof params.summary !== "string" || !Array.isArray(params.actions) || typeof params.recommendedAction !== "string") {
|
|
1687
|
+
throw new Error("Invalid exit plan mode request payload");
|
|
1381
1688
|
}
|
|
1382
1689
|
const session = this.sessions.get(params.sessionId);
|
|
1383
1690
|
if (!session) {
|
|
1384
1691
|
throw new Error(`Session not found: ${params.sessionId}`);
|
|
1385
1692
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1693
|
+
return await session._handleExitPlanModeRequest({
|
|
1694
|
+
summary: params.summary,
|
|
1695
|
+
planContent: params.planContent,
|
|
1696
|
+
actions: params.actions,
|
|
1697
|
+
recommendedAction: params.recommendedAction
|
|
1698
|
+
});
|
|
1388
1699
|
}
|
|
1389
|
-
async
|
|
1390
|
-
if (!params || typeof params.sessionId !== "string"
|
|
1391
|
-
throw new Error("Invalid
|
|
1700
|
+
async handleAutoModeSwitchRequest(params) {
|
|
1701
|
+
if (!params || typeof params.sessionId !== "string") {
|
|
1702
|
+
throw new Error("Invalid auto mode switch request payload");
|
|
1392
1703
|
}
|
|
1393
1704
|
const session = this.sessions.get(params.sessionId);
|
|
1394
1705
|
if (!session) {
|
|
1395
1706
|
throw new Error(`Session not found: ${params.sessionId}`);
|
|
1396
1707
|
}
|
|
1397
|
-
|
|
1708
|
+
const response = await session._handleAutoModeSwitchRequest({
|
|
1709
|
+
errorCode: params.errorCode,
|
|
1710
|
+
retryAfterSeconds: params.retryAfterSeconds
|
|
1711
|
+
});
|
|
1712
|
+
return { response };
|
|
1398
1713
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
/**
|
|
1403
|
-
* Handles a v2-style tool.call RPC request from the server.
|
|
1404
|
-
* Looks up the session and tool handler, executes it, and returns the result
|
|
1405
|
-
* in the v2 response format.
|
|
1406
|
-
*/
|
|
1407
|
-
async handleToolCallRequestV2(params) {
|
|
1408
|
-
if (!params || typeof params.sessionId !== "string" || typeof params.toolCallId !== "string" || typeof params.toolName !== "string") {
|
|
1409
|
-
throw new Error("Invalid tool call payload");
|
|
1714
|
+
async handleHooksInvoke(params) {
|
|
1715
|
+
if (!params || typeof params.sessionId !== "string" || typeof params.hookType !== "string") {
|
|
1716
|
+
throw new Error("Invalid hooks invoke payload");
|
|
1410
1717
|
}
|
|
1411
1718
|
const session = this.sessions.get(params.sessionId);
|
|
1412
1719
|
if (!session) {
|
|
1413
|
-
throw new Error(`
|
|
1414
|
-
}
|
|
1415
|
-
const handler = session.getToolHandler(params.toolName);
|
|
1416
|
-
if (!handler) {
|
|
1417
|
-
return {
|
|
1418
|
-
result: {
|
|
1419
|
-
textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`,
|
|
1420
|
-
resultType: "failure",
|
|
1421
|
-
error: `tool '${params.toolName}' not supported`,
|
|
1422
|
-
toolTelemetry: {}
|
|
1423
|
-
}
|
|
1424
|
-
};
|
|
1425
|
-
}
|
|
1426
|
-
try {
|
|
1427
|
-
const traceparent = params.traceparent;
|
|
1428
|
-
const tracestate = params.tracestate;
|
|
1429
|
-
const invocation = {
|
|
1430
|
-
sessionId: params.sessionId,
|
|
1431
|
-
toolCallId: params.toolCallId,
|
|
1432
|
-
toolName: params.toolName,
|
|
1433
|
-
arguments: params.arguments,
|
|
1434
|
-
traceparent,
|
|
1435
|
-
tracestate
|
|
1436
|
-
};
|
|
1437
|
-
const result = await handler(params.arguments, invocation);
|
|
1438
|
-
return { result: this.normalizeToolResultV2(result) };
|
|
1439
|
-
} catch (error) {
|
|
1440
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1441
|
-
return {
|
|
1442
|
-
result: {
|
|
1443
|
-
textResultForLlm: "Invoking this tool produced an error. Detailed information is not available.",
|
|
1444
|
-
resultType: "failure",
|
|
1445
|
-
error: message,
|
|
1446
|
-
toolTelemetry: {}
|
|
1447
|
-
}
|
|
1448
|
-
};
|
|
1720
|
+
throw new Error(`Session not found: ${params.sessionId}`);
|
|
1449
1721
|
}
|
|
1722
|
+
const output = await session._handleHooksInvoke(params.hookType, params.input);
|
|
1723
|
+
return { output };
|
|
1450
1724
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
async handlePermissionRequestV2(params) {
|
|
1455
|
-
if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) {
|
|
1456
|
-
throw new Error("Invalid permission request payload");
|
|
1725
|
+
async handleSystemMessageTransform(params) {
|
|
1726
|
+
if (!params || typeof params.sessionId !== "string" || !params.sections || typeof params.sections !== "object") {
|
|
1727
|
+
throw new Error("Invalid systemMessage.transform payload");
|
|
1457
1728
|
}
|
|
1458
1729
|
const session = this.sessions.get(params.sessionId);
|
|
1459
1730
|
if (!session) {
|
|
1460
1731
|
throw new Error(`Session not found: ${params.sessionId}`);
|
|
1461
1732
|
}
|
|
1462
|
-
|
|
1463
|
-
const result = await session._handlePermissionRequestV2(params.permissionRequest);
|
|
1464
|
-
return { result };
|
|
1465
|
-
} catch (error) {
|
|
1466
|
-
if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) {
|
|
1467
|
-
throw error;
|
|
1468
|
-
}
|
|
1469
|
-
return {
|
|
1470
|
-
result: {
|
|
1471
|
-
kind: "user-not-available"
|
|
1472
|
-
}
|
|
1473
|
-
};
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
normalizeToolResultV2(result) {
|
|
1477
|
-
if (result === void 0 || result === null) {
|
|
1478
|
-
return {
|
|
1479
|
-
textResultForLlm: "Tool returned no result",
|
|
1480
|
-
resultType: "failure",
|
|
1481
|
-
error: "tool returned no result",
|
|
1482
|
-
toolTelemetry: {}
|
|
1483
|
-
};
|
|
1484
|
-
}
|
|
1485
|
-
if (this.isToolResultObject(result)) {
|
|
1486
|
-
return result;
|
|
1487
|
-
}
|
|
1488
|
-
const textResult = typeof result === "string" ? result : JSON.stringify(result);
|
|
1489
|
-
return {
|
|
1490
|
-
textResultForLlm: textResult,
|
|
1491
|
-
resultType: "success",
|
|
1492
|
-
toolTelemetry: {}
|
|
1493
|
-
};
|
|
1494
|
-
}
|
|
1495
|
-
isToolResultObject(value) {
|
|
1496
|
-
return typeof value === "object" && value !== null && "textResultForLlm" in value && typeof value.textResultForLlm === "string" && "resultType" in value;
|
|
1733
|
+
return await session._handleSystemMessageTransform(params.sections);
|
|
1497
1734
|
}
|
|
1498
1735
|
}
|
|
1499
1736
|
export {
|