@love-moon/ai-sdk 0.2.39 → 0.2.40
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/dist/external-provider-registry.js +1 -1
- package/dist/providers/claude-agent-sdk-session.d.ts +3 -1
- package/dist/providers/claude-agent-sdk-session.js +23 -4
- package/dist/providers/codex-app-server-session.d.ts +2 -1
- package/dist/providers/codex-app-server-session.js +19 -3
- package/dist/providers/codex-exec-session.d.ts +116 -0
- package/dist/providers/codex-exec-session.js +583 -0
- package/dist/providers/copilot-sdk-session.d.ts +193 -0
- package/dist/providers/copilot-sdk-session.js +1463 -0
- package/dist/providers/kimi-cli-session.d.ts +2 -1
- package/dist/providers/kimi-cli-session.js +14 -2
- package/dist/providers/kimi-print-session.d.ts +125 -0
- package/dist/providers/kimi-print-session.js +633 -0
- package/dist/providers/opencode-sdk-session.d.ts +2 -1
- package/dist/providers/opencode-sdk-session.js +4 -1
- package/dist/session-factory.d.ts +7 -1
- package/dist/session-factory.js +48 -6
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +39 -20
- package/dist/transports/codex-app-server-transport.d.ts +1 -0
- package/dist/transports/codex-app-server-transport.js +10 -5
- package/package.json +3 -2
|
@@ -0,0 +1,1463 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { emitLog, getBoundedEnvInt, loadEnvConfig, normalizeLogger, parseCommandParts, proxyToEnv, sanitizeForLog, withoutCopilotGithubTokenEnv, } from "../shared.js";
|
|
5
|
+
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
6
|
+
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
7
|
+
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
8
|
+
const DEFAULT_CLOSE_TIMEOUT_MS = 5 * 1000;
|
|
9
|
+
const SDK_SEND_AND_WAIT_TIMEOUT_GRACE_MS = 5 * 1000;
|
|
10
|
+
const COPILOT_PROVIDER_VARIANT = "copilot-sdk";
|
|
11
|
+
const LEGACY_COPILOT_CLI_ARGS = new Set(["--allow-all-paths", "--allow-all-tools"]);
|
|
12
|
+
function waitForever() {
|
|
13
|
+
return new Promise(() => { });
|
|
14
|
+
}
|
|
15
|
+
function createTurnError(message, extras = {}) {
|
|
16
|
+
const error = new Error(message);
|
|
17
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
18
|
+
error[key] = value;
|
|
19
|
+
}
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
function resolvePositiveTimeoutMs(value, fallback) {
|
|
23
|
+
const n = Number(value);
|
|
24
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n) : fallback;
|
|
25
|
+
}
|
|
26
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
27
|
+
let timer = null;
|
|
28
|
+
try {
|
|
29
|
+
return await Promise.race([
|
|
30
|
+
promise,
|
|
31
|
+
new Promise((_, reject) => {
|
|
32
|
+
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
if (timer) {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function normalizeCopilotBackend(backend) {
|
|
43
|
+
const normalized = String(backend || "").trim().toLowerCase();
|
|
44
|
+
return normalized || "copilot";
|
|
45
|
+
}
|
|
46
|
+
function normalizeText(value) {
|
|
47
|
+
return typeof value === "string" ? value : "";
|
|
48
|
+
}
|
|
49
|
+
function sanitizeSummary(value, maxLen = 180) {
|
|
50
|
+
return sanitizeForLog(value, maxLen);
|
|
51
|
+
}
|
|
52
|
+
function normalizeReasoningEffort(value) {
|
|
53
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : "";
|
|
54
|
+
if (normalized === "low" || normalized === "medium" || normalized === "high" || normalized === "xhigh") {
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
function normalizeStringList(value) {
|
|
60
|
+
if (!Array.isArray(value)) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const normalized = value
|
|
64
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
65
|
+
.filter(Boolean);
|
|
66
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
67
|
+
}
|
|
68
|
+
function isTruthyPlainObject(value) {
|
|
69
|
+
return value && typeof value === "object" && !Array.isArray(value);
|
|
70
|
+
}
|
|
71
|
+
function normalizeCopilotCliArgs(args) {
|
|
72
|
+
if (!Array.isArray(args)) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
return args.filter((item) => {
|
|
76
|
+
const normalized = typeof item === "string" ? item.trim().toLowerCase() : "";
|
|
77
|
+
return normalized && !LEGACY_COPILOT_CLI_ARGS.has(normalized);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function stripExecutableSuffix(name) {
|
|
81
|
+
return String(name || "")
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/\.(cmd|bat|exe)$/i, "");
|
|
85
|
+
}
|
|
86
|
+
function isDefaultCopilotCommand(command) {
|
|
87
|
+
const normalized = String(command || "").trim();
|
|
88
|
+
if (!normalized || /[\\/]/.test(normalized)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return stripExecutableSuffix(normalized) === "copilot";
|
|
92
|
+
}
|
|
93
|
+
function isEnvironmentAssignment(token) {
|
|
94
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(String(token || "").trim());
|
|
95
|
+
}
|
|
96
|
+
function parseEnvironmentAssignment(token) {
|
|
97
|
+
const normalized = String(token || "");
|
|
98
|
+
const index = normalized.indexOf("=");
|
|
99
|
+
if (index <= 0) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
key: normalized.slice(0, index),
|
|
104
|
+
value: normalized.slice(index + 1),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function isEnvCommand(command) {
|
|
108
|
+
return stripExecutableSuffix(path.basename(String(command || ""))) === "env";
|
|
109
|
+
}
|
|
110
|
+
function isPathLikeCommand(command) {
|
|
111
|
+
const normalized = String(command || "").trim();
|
|
112
|
+
return (normalized.startsWith(".") ||
|
|
113
|
+
normalized.startsWith("/") ||
|
|
114
|
+
normalized.includes("/") ||
|
|
115
|
+
normalized.includes("\\") ||
|
|
116
|
+
/^[A-Za-z]:[\\/]/.test(normalized));
|
|
117
|
+
}
|
|
118
|
+
function resolveExecutablePath(command, env = process.env) {
|
|
119
|
+
const normalized = String(command || "").trim();
|
|
120
|
+
if (!normalized) {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
if (isPathLikeCommand(normalized)) {
|
|
124
|
+
return normalized;
|
|
125
|
+
}
|
|
126
|
+
const pathEnv = typeof env?.PATH === "string" ? env.PATH : process.env.PATH || "";
|
|
127
|
+
const pathExt = process.platform === "win32" && !path.extname(normalized)
|
|
128
|
+
? String(env?.PATHEXT || process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD")
|
|
129
|
+
.split(";")
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
: [""];
|
|
132
|
+
for (const dir of pathEnv.split(path.delimiter)) {
|
|
133
|
+
if (!dir) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
for (const ext of pathExt) {
|
|
137
|
+
const candidate = path.join(dir, `${normalized}${ext}`);
|
|
138
|
+
if (existsSync(candidate)) {
|
|
139
|
+
return candidate;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
function unwrapEnvironmentCommand(command, args) {
|
|
146
|
+
const parts = [command, ...args].filter((item) => typeof item === "string" && item.length > 0);
|
|
147
|
+
const extraEnv = {};
|
|
148
|
+
let index = 0;
|
|
149
|
+
while (index < parts.length && isEnvironmentAssignment(parts[index])) {
|
|
150
|
+
const assignment = parseEnvironmentAssignment(parts[index]);
|
|
151
|
+
if (assignment) {
|
|
152
|
+
extraEnv[assignment.key] = assignment.value;
|
|
153
|
+
}
|
|
154
|
+
index += 1;
|
|
155
|
+
}
|
|
156
|
+
if (index > 0) {
|
|
157
|
+
return {
|
|
158
|
+
command: parts[index] || "",
|
|
159
|
+
args: parts.slice(index + 1),
|
|
160
|
+
env: extraEnv,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (!isEnvCommand(command)) {
|
|
164
|
+
return { command, args, env: extraEnv };
|
|
165
|
+
}
|
|
166
|
+
index = 0;
|
|
167
|
+
while (index < args.length) {
|
|
168
|
+
const token = args[index];
|
|
169
|
+
if (token === "--") {
|
|
170
|
+
index += 1;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (isEnvironmentAssignment(token)) {
|
|
174
|
+
const assignment = parseEnvironmentAssignment(token);
|
|
175
|
+
if (assignment) {
|
|
176
|
+
extraEnv[assignment.key] = assignment.value;
|
|
177
|
+
}
|
|
178
|
+
index += 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (String(token || "").startsWith("-")) {
|
|
182
|
+
return { command, args, env: extraEnv };
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
command: args[index] || "",
|
|
188
|
+
args: args.slice(index + 1),
|
|
189
|
+
env: extraEnv,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function hasOwnEnumerableKeys(value) {
|
|
193
|
+
return value && typeof value === "object" && Object.keys(value).length > 0;
|
|
194
|
+
}
|
|
195
|
+
function resolveCopilotCliLaunch(commandLine, env = process.env) {
|
|
196
|
+
const normalized = typeof commandLine === "string" ? commandLine.trim() : "";
|
|
197
|
+
if (!normalized) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const parsed = parseCommandParts(normalized);
|
|
201
|
+
const unwrapped = unwrapEnvironmentCommand(parsed.command, parsed.args);
|
|
202
|
+
const command = unwrapped.command;
|
|
203
|
+
const args = unwrapped.args;
|
|
204
|
+
if (!command) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const cliArgs = normalizeCopilotCliArgs(args);
|
|
208
|
+
if (isDefaultCopilotCommand(command)) {
|
|
209
|
+
if (cliArgs.length === 0 && !hasOwnEnumerableKeys(unwrapped.env)) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
cliArgs,
|
|
214
|
+
env: unwrapped.env,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const launchEnv = {
|
|
218
|
+
...process.env,
|
|
219
|
+
...env,
|
|
220
|
+
...unwrapped.env,
|
|
221
|
+
};
|
|
222
|
+
const resolvedPath = resolveExecutablePath(command, launchEnv);
|
|
223
|
+
return {
|
|
224
|
+
cliPath: resolvedPath || command,
|
|
225
|
+
cliArgs,
|
|
226
|
+
env: unwrapped.env,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function toolPhaseForName(toolName) {
|
|
230
|
+
const normalized = String(toolName || "").trim().toLowerCase();
|
|
231
|
+
if (!normalized) {
|
|
232
|
+
return "tool_call";
|
|
233
|
+
}
|
|
234
|
+
if (normalized.includes("bash") || normalized.includes("shell") || normalized.includes("command")) {
|
|
235
|
+
return "command_execution";
|
|
236
|
+
}
|
|
237
|
+
if (normalized.includes("edit") ||
|
|
238
|
+
normalized.includes("write") ||
|
|
239
|
+
normalized.includes("patch") ||
|
|
240
|
+
normalized.includes("replace")) {
|
|
241
|
+
return "file_update";
|
|
242
|
+
}
|
|
243
|
+
if (normalized.includes("read") ||
|
|
244
|
+
normalized.includes("grep") ||
|
|
245
|
+
normalized.includes("glob") ||
|
|
246
|
+
normalized.includes("ls")) {
|
|
247
|
+
return "workspace_inspection";
|
|
248
|
+
}
|
|
249
|
+
if (normalized.includes("web") || normalized.includes("fetch") || normalized.includes("search")) {
|
|
250
|
+
return "web_lookup";
|
|
251
|
+
}
|
|
252
|
+
if (normalized.includes("task") || normalized.includes("agent")) {
|
|
253
|
+
return "task_progress";
|
|
254
|
+
}
|
|
255
|
+
return "tool_call";
|
|
256
|
+
}
|
|
257
|
+
function statusLineForPhase(phase, toolName = "", detail = "") {
|
|
258
|
+
switch (phase) {
|
|
259
|
+
case "reasoning":
|
|
260
|
+
return "copilot reasoning";
|
|
261
|
+
case "planning":
|
|
262
|
+
return detail || "copilot updating plan";
|
|
263
|
+
case "command_execution":
|
|
264
|
+
return toolName ? `copilot running ${toolName}` : "copilot running command";
|
|
265
|
+
case "file_update":
|
|
266
|
+
return toolName ? `copilot editing with ${toolName}` : "copilot editing files";
|
|
267
|
+
case "workspace_inspection":
|
|
268
|
+
return toolName ? `copilot reading with ${toolName}` : "copilot reading workspace";
|
|
269
|
+
case "web_lookup":
|
|
270
|
+
return toolName ? `copilot browsing with ${toolName}` : "copilot browsing";
|
|
271
|
+
case "task_progress":
|
|
272
|
+
return detail || (toolName ? `copilot running ${toolName}` : "copilot running task");
|
|
273
|
+
case "message_aggregation":
|
|
274
|
+
return "copilot composing reply";
|
|
275
|
+
case "tool_call":
|
|
276
|
+
return detail || (toolName ? `copilot calling ${toolName}` : "copilot calling tool");
|
|
277
|
+
default:
|
|
278
|
+
return "copilot is working";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function extractErrorMessage(error) {
|
|
282
|
+
if (typeof error?.message === "string" && error.message.trim()) {
|
|
283
|
+
return error.message.trim();
|
|
284
|
+
}
|
|
285
|
+
if (typeof error === "string" && error.trim()) {
|
|
286
|
+
return error.trim();
|
|
287
|
+
}
|
|
288
|
+
return "Copilot turn failed";
|
|
289
|
+
}
|
|
290
|
+
function normalizeTurnError(error) {
|
|
291
|
+
if (error instanceof Error) {
|
|
292
|
+
return error;
|
|
293
|
+
}
|
|
294
|
+
return createTurnError(extractErrorMessage(error));
|
|
295
|
+
}
|
|
296
|
+
function normalizeUsagePayload(payload) {
|
|
297
|
+
if (!payload || typeof payload !== "object") {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return { ...payload };
|
|
301
|
+
}
|
|
302
|
+
function extractToolResultPreview(result) {
|
|
303
|
+
if (!result || typeof result !== "object") {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
const contentBlocks = Array.isArray(result.contents) ? result.contents : [];
|
|
307
|
+
const blockPreview = contentBlocks
|
|
308
|
+
.map((block) => (typeof block?.text === "string" ? block.text : ""))
|
|
309
|
+
.filter(Boolean)
|
|
310
|
+
.join("\n");
|
|
311
|
+
const fallbackPreview = (typeof result.detailedContent === "string" && result.detailedContent.trim()
|
|
312
|
+
? result.detailedContent
|
|
313
|
+
: "") ||
|
|
314
|
+
(typeof result.content === "string" && result.content.trim() ? result.content : "");
|
|
315
|
+
return sanitizeSummary(blockPreview || fallbackPreview, 120);
|
|
316
|
+
}
|
|
317
|
+
function isAuthLikeError({ errorType = "", message = "", statusCode = undefined } = {}) {
|
|
318
|
+
const normalizedType = String(errorType || "").trim().toLowerCase();
|
|
319
|
+
const normalizedMessage = String(message || "").trim().toLowerCase();
|
|
320
|
+
return (normalizedType.includes("auth") ||
|
|
321
|
+
normalizedType.includes("login") ||
|
|
322
|
+
normalizedType.includes("permission") ||
|
|
323
|
+
normalizedMessage.includes("auth") ||
|
|
324
|
+
normalizedMessage.includes("login") ||
|
|
325
|
+
normalizedMessage.includes("sign in") ||
|
|
326
|
+
normalizedMessage.includes("device code") ||
|
|
327
|
+
normalizedMessage.includes("token") ||
|
|
328
|
+
statusCode === 401 ||
|
|
329
|
+
statusCode === 403);
|
|
330
|
+
}
|
|
331
|
+
function buildCopilotClientOptions(options, cwd, env) {
|
|
332
|
+
const clientOptions = isTruthyPlainObject(options.copilotClientOptions) ? { ...options.copilotClientOptions } : {};
|
|
333
|
+
const commandLine = typeof options.commandLine === "string" && options.commandLine.trim()
|
|
334
|
+
? options.commandLine.trim()
|
|
335
|
+
: "";
|
|
336
|
+
const cliLaunch = resolveCopilotCliLaunch(commandLine, env);
|
|
337
|
+
if (cliLaunch && clientOptions.cliPath === undefined && clientOptions.cliArgs === undefined && clientOptions.cliUrl === undefined) {
|
|
338
|
+
if (cliLaunch.cliPath !== undefined) {
|
|
339
|
+
clientOptions.cliPath = cliLaunch.cliPath;
|
|
340
|
+
}
|
|
341
|
+
if (cliLaunch.cliArgs !== undefined) {
|
|
342
|
+
clientOptions.cliArgs = cliLaunch.cliArgs;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const passthroughKeys = [
|
|
346
|
+
"cliPath",
|
|
347
|
+
"cliArgs",
|
|
348
|
+
"cliUrl",
|
|
349
|
+
"port",
|
|
350
|
+
"useStdio",
|
|
351
|
+
"isChildProcess",
|
|
352
|
+
"logLevel",
|
|
353
|
+
"autoStart",
|
|
354
|
+
"autoRestart",
|
|
355
|
+
"onListModels",
|
|
356
|
+
"telemetry",
|
|
357
|
+
"onGetTraceContext",
|
|
358
|
+
];
|
|
359
|
+
for (const key of passthroughKeys) {
|
|
360
|
+
if (clientOptions[key] !== undefined || options[key] === undefined) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
clientOptions[key] = key === "cliArgs" ? normalizeCopilotCliArgs(options[key]) : options[key];
|
|
364
|
+
}
|
|
365
|
+
const explicitGithubToken = typeof options.githubToken === "string" && options.githubToken.trim()
|
|
366
|
+
? options.githubToken.trim()
|
|
367
|
+
: "";
|
|
368
|
+
if (clientOptions.githubToken === undefined && explicitGithubToken) {
|
|
369
|
+
clientOptions.githubToken = explicitGithubToken;
|
|
370
|
+
}
|
|
371
|
+
const hasExplicitGithubToken = typeof clientOptions.githubToken === "string" && clientOptions.githubToken.trim();
|
|
372
|
+
if (clientOptions.useLoggedInUser === undefined && typeof options.useLoggedInUser === "boolean") {
|
|
373
|
+
clientOptions.useLoggedInUser = options.useLoggedInUser;
|
|
374
|
+
}
|
|
375
|
+
if (clientOptions.cwd === undefined) {
|
|
376
|
+
clientOptions.cwd = cwd;
|
|
377
|
+
}
|
|
378
|
+
let resolvedEnv;
|
|
379
|
+
if (clientOptions.env === undefined) {
|
|
380
|
+
resolvedEnv = {
|
|
381
|
+
...process.env,
|
|
382
|
+
...env,
|
|
383
|
+
...(hasOwnEnumerableKeys(cliLaunch?.env) ? cliLaunch.env : {}),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
else if (hasOwnEnumerableKeys(cliLaunch?.env)) {
|
|
387
|
+
resolvedEnv = {
|
|
388
|
+
...clientOptions.env,
|
|
389
|
+
...cliLaunch.env,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
resolvedEnv = { ...clientOptions.env };
|
|
394
|
+
}
|
|
395
|
+
clientOptions.env = hasExplicitGithubToken
|
|
396
|
+
? resolvedEnv
|
|
397
|
+
: withoutCopilotGithubTokenEnv(resolvedEnv);
|
|
398
|
+
if (!hasExplicitGithubToken && clientOptions.useLoggedInUser === undefined) {
|
|
399
|
+
clientOptions.useLoggedInUser = true;
|
|
400
|
+
}
|
|
401
|
+
return clientOptions;
|
|
402
|
+
}
|
|
403
|
+
function buildCopilotSessionConfig(options, cwd, permissionHandler) {
|
|
404
|
+
const sessionConfig = isTruthyPlainObject(options.copilotSessionConfig) ? { ...options.copilotSessionConfig } : {};
|
|
405
|
+
const passthroughKeys = [
|
|
406
|
+
"clientName",
|
|
407
|
+
"tools",
|
|
408
|
+
"commands",
|
|
409
|
+
"systemMessage",
|
|
410
|
+
"provider",
|
|
411
|
+
"modelCapabilities",
|
|
412
|
+
"onUserInputRequest",
|
|
413
|
+
"onElicitationRequest",
|
|
414
|
+
"hooks",
|
|
415
|
+
"configDir",
|
|
416
|
+
"enableConfigDiscovery",
|
|
417
|
+
"mcpServers",
|
|
418
|
+
"customAgents",
|
|
419
|
+
"agent",
|
|
420
|
+
"skillDirectories",
|
|
421
|
+
"disabledSkills",
|
|
422
|
+
"infiniteSessions",
|
|
423
|
+
"createSessionFsHandler",
|
|
424
|
+
];
|
|
425
|
+
for (const key of passthroughKeys) {
|
|
426
|
+
if (sessionConfig[key] !== undefined || options[key] === undefined) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
sessionConfig[key] = options[key];
|
|
430
|
+
}
|
|
431
|
+
const availableTools = normalizeStringList(options.availableTools);
|
|
432
|
+
if (sessionConfig.availableTools === undefined && availableTools) {
|
|
433
|
+
sessionConfig.availableTools = availableTools;
|
|
434
|
+
}
|
|
435
|
+
const excludedTools = normalizeStringList(options.excludedTools);
|
|
436
|
+
if (sessionConfig.excludedTools === undefined && excludedTools) {
|
|
437
|
+
sessionConfig.excludedTools = excludedTools;
|
|
438
|
+
}
|
|
439
|
+
if (sessionConfig.model === undefined && typeof options.model === "string" && options.model.trim()) {
|
|
440
|
+
sessionConfig.model = options.model.trim();
|
|
441
|
+
}
|
|
442
|
+
const reasoningEffort = normalizeReasoningEffort(options.reasoningEffort);
|
|
443
|
+
if (sessionConfig.reasoningEffort === undefined && reasoningEffort) {
|
|
444
|
+
sessionConfig.reasoningEffort = reasoningEffort;
|
|
445
|
+
}
|
|
446
|
+
if (sessionConfig.streaming === undefined) {
|
|
447
|
+
sessionConfig.streaming = true;
|
|
448
|
+
}
|
|
449
|
+
if (sessionConfig.workingDirectory === undefined) {
|
|
450
|
+
sessionConfig.workingDirectory = cwd;
|
|
451
|
+
}
|
|
452
|
+
if (sessionConfig.onPermissionRequest === undefined) {
|
|
453
|
+
sessionConfig.onPermissionRequest = permissionHandler;
|
|
454
|
+
}
|
|
455
|
+
return sessionConfig;
|
|
456
|
+
}
|
|
457
|
+
export class CopilotSdkSession extends EventEmitter {
|
|
458
|
+
constructor(backend, options = {}) {
|
|
459
|
+
super();
|
|
460
|
+
this.backend = normalizeCopilotBackend(backend);
|
|
461
|
+
this.options = options;
|
|
462
|
+
this.logger = normalizeLogger(options.logger);
|
|
463
|
+
this.cwd =
|
|
464
|
+
typeof options.cwd === "string" && options.cwd.trim()
|
|
465
|
+
? options.cwd.trim()
|
|
466
|
+
: process.cwd();
|
|
467
|
+
this.resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
|
|
468
|
+
this.sessionId = this.resumeSessionId || "";
|
|
469
|
+
this.sessionInfo = this.sessionId
|
|
470
|
+
? {
|
|
471
|
+
backend: this.backend,
|
|
472
|
+
sessionId: this.sessionId,
|
|
473
|
+
model: typeof options.model === "string" && options.model.trim()
|
|
474
|
+
? options.model.trim()
|
|
475
|
+
: undefined,
|
|
476
|
+
modelProvider: "github-copilot",
|
|
477
|
+
reasoningEffort: normalizeReasoningEffort(options.reasoningEffort),
|
|
478
|
+
}
|
|
479
|
+
: null;
|
|
480
|
+
this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
|
|
481
|
+
this.pendingHistorySeed = this.history.length > 0;
|
|
482
|
+
this.closeRequested = false;
|
|
483
|
+
this.closed = false;
|
|
484
|
+
this.closeWaiters = new Set();
|
|
485
|
+
this.sessionMessageHandler = null;
|
|
486
|
+
this.workingStatusHandler = null;
|
|
487
|
+
this.activeReplyTarget = "";
|
|
488
|
+
this.lastReplyTarget = "";
|
|
489
|
+
this.currentTurn = null;
|
|
490
|
+
this.lastUsage = null;
|
|
491
|
+
this.currentTurnStatus = null;
|
|
492
|
+
this.currentTurnActivityAt = 0;
|
|
493
|
+
this.now = typeof options.now === "function" ? options.now : () => Date.now();
|
|
494
|
+
this.closeTimeoutMs = resolvePositiveTimeoutMs(options.copilotCloseTimeoutMs, DEFAULT_CLOSE_TIMEOUT_MS);
|
|
495
|
+
this.turnDeadlineMs = getBoundedEnvInt("CONDUCTOR_TURN_DEADLINE_MS", DEFAULT_TURN_DEADLINE_MS, MIN_TURN_DEADLINE_MS, MAX_TURN_DEADLINE_MS);
|
|
496
|
+
this.client = null;
|
|
497
|
+
this.session = null;
|
|
498
|
+
this.booted = false;
|
|
499
|
+
this.bootPromise = null;
|
|
500
|
+
this.sdkModulePromise = null;
|
|
501
|
+
this.sessionSubscriptions = [];
|
|
502
|
+
this.lastAuthRequiredSignature = "";
|
|
503
|
+
const envConfig = loadEnvConfig(options.configFile);
|
|
504
|
+
const proxyEnv = proxyToEnv(envConfig);
|
|
505
|
+
const extraEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
|
|
506
|
+
this.env = {
|
|
507
|
+
...extraEnv,
|
|
508
|
+
...(options.env && typeof options.env === "object" ? options.env : {}),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
writeLog(message) {
|
|
512
|
+
emitLog(this.logger, message);
|
|
513
|
+
}
|
|
514
|
+
trace(message) {
|
|
515
|
+
this.writeLog(`[${this.backend}] [copilot-sdk] ${message}`);
|
|
516
|
+
}
|
|
517
|
+
get threadId() {
|
|
518
|
+
return this.sessionId;
|
|
519
|
+
}
|
|
520
|
+
get threadOptions() {
|
|
521
|
+
const model = this.sessionInfo?.model ||
|
|
522
|
+
(typeof this.options.model === "string" && this.options.model.trim()
|
|
523
|
+
? this.options.model.trim()
|
|
524
|
+
: this.backend);
|
|
525
|
+
return {
|
|
526
|
+
model,
|
|
527
|
+
modelProvider: this.sessionInfo?.modelProvider || undefined,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
getSnapshot() {
|
|
531
|
+
return {
|
|
532
|
+
backend: this.backend,
|
|
533
|
+
provider: COPILOT_PROVIDER_VARIANT,
|
|
534
|
+
cwd: this.cwd,
|
|
535
|
+
sessionId: this.sessionId || undefined,
|
|
536
|
+
sessionInfo: this.getSessionInfo(),
|
|
537
|
+
useSessionFileReplyStream: this.usesSessionFileReplyStream(),
|
|
538
|
+
resumeReady: Boolean(this.sessionId),
|
|
539
|
+
manualResume: this.sessionId
|
|
540
|
+
? {
|
|
541
|
+
ready: true,
|
|
542
|
+
command: `copilot --resume=${this.sessionId}`,
|
|
543
|
+
}
|
|
544
|
+
: null,
|
|
545
|
+
currentTurnStatus: this.getCurrentTurnStatus(),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
getSessionInfo() {
|
|
549
|
+
return this.sessionInfo ? { ...this.sessionInfo } : null;
|
|
550
|
+
}
|
|
551
|
+
getCurrentTurnStatus() {
|
|
552
|
+
return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
|
|
553
|
+
}
|
|
554
|
+
async ensureSessionInfo() {
|
|
555
|
+
await this.boot();
|
|
556
|
+
return this.getSessionInfo();
|
|
557
|
+
}
|
|
558
|
+
async getSessionUsageSummary() {
|
|
559
|
+
return {
|
|
560
|
+
sessionId: this.sessionId || undefined,
|
|
561
|
+
sessionFilePath: undefined,
|
|
562
|
+
totalCostUsd: undefined,
|
|
563
|
+
usage: this.lastUsage ? { ...this.lastUsage } : null,
|
|
564
|
+
rateLimits: this.lastUsage?.quotaSnapshots ? { ...this.lastUsage.quotaSnapshots } : null,
|
|
565
|
+
manualResume: this.sessionId
|
|
566
|
+
? {
|
|
567
|
+
ready: true,
|
|
568
|
+
command: `copilot --resume=${this.sessionId}`,
|
|
569
|
+
}
|
|
570
|
+
: null,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
usesSessionFileReplyStream() {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
setSessionMessageHandler(handler) {
|
|
577
|
+
this.sessionMessageHandler = typeof handler === "function" ? handler : null;
|
|
578
|
+
}
|
|
579
|
+
setWorkingStatusHandler(handler) {
|
|
580
|
+
this.workingStatusHandler = typeof handler === "function" ? handler : null;
|
|
581
|
+
}
|
|
582
|
+
setSessionReplyTarget(replyTo) {
|
|
583
|
+
const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
|
|
584
|
+
this.activeReplyTarget = normalizedReplyTo;
|
|
585
|
+
if (normalizedReplyTo) {
|
|
586
|
+
this.lastReplyTarget = normalizedReplyTo;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
getCurrentReplyTarget() {
|
|
590
|
+
return this.activeReplyTarget || this.lastReplyTarget || undefined;
|
|
591
|
+
}
|
|
592
|
+
touchTurnActivity() {
|
|
593
|
+
this.currentTurnActivityAt = this.now();
|
|
594
|
+
}
|
|
595
|
+
updateCurrentTurnStatus(payload) {
|
|
596
|
+
const updatedAtMs = this.now();
|
|
597
|
+
this.currentTurnActivityAt = updatedAtMs;
|
|
598
|
+
this.currentTurnStatus = {
|
|
599
|
+
...payload,
|
|
600
|
+
updated_at: new Date(updatedAtMs).toISOString(),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
markTurnStartedStatus() {
|
|
604
|
+
this.updateCurrentTurnStatus({
|
|
605
|
+
source: COPILOT_PROVIDER_VARIANT,
|
|
606
|
+
reply_in_progress: true,
|
|
607
|
+
replyTo: this.getCurrentReplyTarget(),
|
|
608
|
+
phase: "turn_started",
|
|
609
|
+
status_line: "copilot is working",
|
|
610
|
+
thread_id: this.sessionId || undefined,
|
|
611
|
+
session_id: this.sessionId || undefined,
|
|
612
|
+
session_file_path: undefined,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async emitWorkingStatus(payload, onProgress = null) {
|
|
616
|
+
const normalized = {
|
|
617
|
+
source: COPILOT_PROVIDER_VARIANT,
|
|
618
|
+
reply_in_progress: Boolean(payload?.reply_in_progress),
|
|
619
|
+
replyTo: payload?.replyTo || this.getCurrentReplyTarget(),
|
|
620
|
+
state: payload?.state,
|
|
621
|
+
phase: payload?.phase,
|
|
622
|
+
status_line: payload?.status_line,
|
|
623
|
+
status_done_line: payload?.status_done_line,
|
|
624
|
+
reply_preview: payload?.reply_preview,
|
|
625
|
+
thread_id: this.sessionId || undefined,
|
|
626
|
+
session_id: this.sessionId || undefined,
|
|
627
|
+
session_file_path: undefined,
|
|
628
|
+
};
|
|
629
|
+
this.updateCurrentTurnStatus(normalized);
|
|
630
|
+
const snapshot = this.getCurrentTurnStatus();
|
|
631
|
+
if (typeof onProgress === "function") {
|
|
632
|
+
onProgress(snapshot);
|
|
633
|
+
}
|
|
634
|
+
if (typeof this.workingStatusHandler === "function") {
|
|
635
|
+
await this.workingStatusHandler(snapshot);
|
|
636
|
+
}
|
|
637
|
+
this.emit("working_status", snapshot);
|
|
638
|
+
}
|
|
639
|
+
async emitAssistantMessage(text) {
|
|
640
|
+
this.touchTurnActivity();
|
|
641
|
+
const payload = {
|
|
642
|
+
text,
|
|
643
|
+
preserveWhitespace: true,
|
|
644
|
+
source: COPILOT_PROVIDER_VARIANT,
|
|
645
|
+
replyTo: this.getCurrentReplyTarget(),
|
|
646
|
+
sessionId: this.sessionId || undefined,
|
|
647
|
+
sessionFilePath: undefined,
|
|
648
|
+
timestamp: new Date().toISOString(),
|
|
649
|
+
};
|
|
650
|
+
if (typeof this.sessionMessageHandler === "function") {
|
|
651
|
+
await this.sessionMessageHandler(payload);
|
|
652
|
+
}
|
|
653
|
+
this.emit("assistant_message", payload);
|
|
654
|
+
}
|
|
655
|
+
async emitBufferedAssistantMessage(currentTurn, messageId) {
|
|
656
|
+
if (!currentTurn || !messageId || currentTurn.emittedMessageIds.has(messageId)) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
const text = currentTurn.messageText.get(messageId) || "";
|
|
660
|
+
if (!text) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
currentTurn.emittedMessageIds.add(messageId);
|
|
664
|
+
await this.emitAssistantMessage(text);
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
async emitPendingAssistantMessages(currentTurn) {
|
|
668
|
+
let emitted = false;
|
|
669
|
+
for (const messageId of currentTurn.messageOrder) {
|
|
670
|
+
emitted = (await this.emitBufferedAssistantMessage(currentTurn, messageId)) || emitted;
|
|
671
|
+
}
|
|
672
|
+
return emitted;
|
|
673
|
+
}
|
|
674
|
+
async emitTerminalWorkingStatus(currentTurn, payload, onProgress = null) {
|
|
675
|
+
if (!currentTurn || currentTurn.terminalWorkingStatusEmitted) {
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
currentTurn.terminalWorkingStatusEmitted = true;
|
|
679
|
+
await this.emitWorkingStatus({
|
|
680
|
+
...payload,
|
|
681
|
+
reply_in_progress: false,
|
|
682
|
+
}, onProgress);
|
|
683
|
+
}
|
|
684
|
+
createSessionClosedError() {
|
|
685
|
+
const error = new Error("Copilot SDK session closed");
|
|
686
|
+
error.reason = "session_closed";
|
|
687
|
+
return error;
|
|
688
|
+
}
|
|
689
|
+
createTurnTimeoutError(timeoutMs) {
|
|
690
|
+
const seconds = Math.max(1, Math.round(timeoutMs / 1000));
|
|
691
|
+
const error = new Error(`Turn exceeded hard deadline (${seconds}s)`);
|
|
692
|
+
error.reason = "turn_timeout";
|
|
693
|
+
error.timeoutMs = timeoutMs;
|
|
694
|
+
return error;
|
|
695
|
+
}
|
|
696
|
+
createTurnAlreadyRunningError() {
|
|
697
|
+
const error = new Error("Copilot turn already in progress");
|
|
698
|
+
error.reason = "turn_already_running";
|
|
699
|
+
return error;
|
|
700
|
+
}
|
|
701
|
+
createCloseGuard(onClose) {
|
|
702
|
+
if (this.closeRequested) {
|
|
703
|
+
return {
|
|
704
|
+
promise: Promise.reject(this.createSessionClosedError()),
|
|
705
|
+
cleanup: () => { },
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
let waiter = null;
|
|
709
|
+
const promise = new Promise((_, reject) => {
|
|
710
|
+
waiter = () => {
|
|
711
|
+
try {
|
|
712
|
+
onClose?.();
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
// best effort
|
|
716
|
+
}
|
|
717
|
+
reject(this.createSessionClosedError());
|
|
718
|
+
};
|
|
719
|
+
this.closeWaiters.add(waiter);
|
|
720
|
+
});
|
|
721
|
+
return {
|
|
722
|
+
promise,
|
|
723
|
+
cleanup: () => {
|
|
724
|
+
if (waiter) {
|
|
725
|
+
this.closeWaiters.delete(waiter);
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
createTurnTimeoutGuard(onTimeout) {
|
|
731
|
+
if (!Number.isFinite(this.turnDeadlineMs) || this.turnDeadlineMs <= 0) {
|
|
732
|
+
return {
|
|
733
|
+
promise: waitForever(),
|
|
734
|
+
cleanup: () => { },
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
let timer = null;
|
|
738
|
+
let settled = false;
|
|
739
|
+
const schedule = (reject) => {
|
|
740
|
+
const now = this.now();
|
|
741
|
+
const lastActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
|
|
742
|
+
? this.currentTurnActivityAt
|
|
743
|
+
: now;
|
|
744
|
+
const elapsedMs = Math.max(0, now - lastActivityAt);
|
|
745
|
+
const waitMs = Math.max(1, this.turnDeadlineMs - elapsedMs);
|
|
746
|
+
timer = setTimeout(() => {
|
|
747
|
+
if (settled) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const activityNow = this.now();
|
|
751
|
+
const latestActivityAt = Number.isFinite(this.currentTurnActivityAt) && this.currentTurnActivityAt > 0
|
|
752
|
+
? this.currentTurnActivityAt
|
|
753
|
+
: activityNow;
|
|
754
|
+
if (activityNow - latestActivityAt < this.turnDeadlineMs) {
|
|
755
|
+
schedule(reject);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
settled = true;
|
|
759
|
+
try {
|
|
760
|
+
onTimeout?.();
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
// best effort
|
|
764
|
+
}
|
|
765
|
+
reject(this.createTurnTimeoutError(this.turnDeadlineMs));
|
|
766
|
+
}, waitMs);
|
|
767
|
+
if (typeof timer?.unref === "function") {
|
|
768
|
+
timer.unref();
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
const promise = new Promise((_, reject) => {
|
|
772
|
+
schedule(reject);
|
|
773
|
+
});
|
|
774
|
+
return {
|
|
775
|
+
promise,
|
|
776
|
+
cleanup: () => {
|
|
777
|
+
settled = true;
|
|
778
|
+
if (timer) {
|
|
779
|
+
clearTimeout(timer);
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
flushCloseWaiters() {
|
|
785
|
+
if (this.closeWaiters.size === 0) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
for (const waiter of this.closeWaiters) {
|
|
789
|
+
try {
|
|
790
|
+
waiter();
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
// best effort
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
this.closeWaiters.clear();
|
|
797
|
+
}
|
|
798
|
+
buildPrompt(promptText, { useInitialImages = false } = {}) {
|
|
799
|
+
let effectivePrompt = String(promptText || "").trim();
|
|
800
|
+
if (!effectivePrompt) {
|
|
801
|
+
return "";
|
|
802
|
+
}
|
|
803
|
+
if (this.pendingHistorySeed) {
|
|
804
|
+
const historyText = this.history
|
|
805
|
+
.map((item) => {
|
|
806
|
+
const role = String(item?.role || "").toLowerCase() === "assistant" ? "Assistant" : "User";
|
|
807
|
+
return `${role}: ${String(item?.content || "").trim()}`;
|
|
808
|
+
})
|
|
809
|
+
.filter(Boolean)
|
|
810
|
+
.join("\n\n");
|
|
811
|
+
if (historyText) {
|
|
812
|
+
effectivePrompt = [
|
|
813
|
+
"Continue the existing conversation with this history.",
|
|
814
|
+
"",
|
|
815
|
+
historyText,
|
|
816
|
+
"",
|
|
817
|
+
`User: ${effectivePrompt}`,
|
|
818
|
+
].join("\n");
|
|
819
|
+
}
|
|
820
|
+
this.pendingHistorySeed = false;
|
|
821
|
+
}
|
|
822
|
+
const images = Array.isArray(this.options.initialImages) ? this.options.initialImages : [];
|
|
823
|
+
if (useInitialImages && images.length > 0) {
|
|
824
|
+
const imageContext = images.map((item, idx) => `${idx + 1}. ${item}`).join("\n");
|
|
825
|
+
effectivePrompt = `${effectivePrompt}\n\nAttached image files:\n${imageContext}`;
|
|
826
|
+
}
|
|
827
|
+
return effectivePrompt;
|
|
828
|
+
}
|
|
829
|
+
async getSdkModule() {
|
|
830
|
+
if (this.sdkModulePromise) {
|
|
831
|
+
return this.sdkModulePromise;
|
|
832
|
+
}
|
|
833
|
+
if (this.options.sdkModule && typeof this.options.sdkModule === "object") {
|
|
834
|
+
this.sdkModulePromise = Promise.resolve(this.options.sdkModule);
|
|
835
|
+
return this.sdkModulePromise;
|
|
836
|
+
}
|
|
837
|
+
this.sdkModulePromise = import("@github/copilot-sdk");
|
|
838
|
+
return this.sdkModulePromise;
|
|
839
|
+
}
|
|
840
|
+
async boot() {
|
|
841
|
+
if (this.booted) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (this.bootPromise) {
|
|
845
|
+
return this.bootPromise;
|
|
846
|
+
}
|
|
847
|
+
this.bootPromise = this.bootInternal();
|
|
848
|
+
try {
|
|
849
|
+
await this.bootPromise;
|
|
850
|
+
this.booted = true;
|
|
851
|
+
}
|
|
852
|
+
finally {
|
|
853
|
+
this.bootPromise = null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async bootInternal() {
|
|
857
|
+
const sdkModule = await this.getSdkModule();
|
|
858
|
+
if (!sdkModule || typeof sdkModule.CopilotClient !== "function") {
|
|
859
|
+
throw new Error("GitHub Copilot SDK client is unavailable");
|
|
860
|
+
}
|
|
861
|
+
const permissionHandler = typeof sdkModule.approveAll === "function"
|
|
862
|
+
? sdkModule.approveAll
|
|
863
|
+
: () => ({ kind: "approved" });
|
|
864
|
+
const clientOptions = buildCopilotClientOptions(this.options, this.cwd, this.env);
|
|
865
|
+
this.client = new sdkModule.CopilotClient(clientOptions);
|
|
866
|
+
const cleanupIfClosedDuringBoot = async (session = null) => {
|
|
867
|
+
if (!this.closeRequested) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
try {
|
|
871
|
+
await this.closeCopilotResources(session, this.client);
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
if (this.session === session) {
|
|
875
|
+
this.session = null;
|
|
876
|
+
}
|
|
877
|
+
this.client = null;
|
|
878
|
+
}
|
|
879
|
+
throw this.createSessionClosedError();
|
|
880
|
+
};
|
|
881
|
+
await cleanupIfClosedDuringBoot();
|
|
882
|
+
if (typeof this.client.start === "function") {
|
|
883
|
+
await this.client.start();
|
|
884
|
+
}
|
|
885
|
+
await cleanupIfClosedDuringBoot();
|
|
886
|
+
const sessionConfig = buildCopilotSessionConfig(this.options, this.cwd, permissionHandler);
|
|
887
|
+
this.session = this.resumeSessionId
|
|
888
|
+
? await this.requestOrThrow(this.client.resumeSession(this.resumeSessionId, sessionConfig))
|
|
889
|
+
: await this.requestOrThrow(this.client.createSession(sessionConfig));
|
|
890
|
+
await cleanupIfClosedDuringBoot(this.session);
|
|
891
|
+
this.attachSessionEventHandlers(this.session);
|
|
892
|
+
this.applySessionInfo(this.session?.sessionId, {
|
|
893
|
+
model: typeof sessionConfig.model === "string" && sessionConfig.model.trim()
|
|
894
|
+
? sessionConfig.model.trim()
|
|
895
|
+
: undefined,
|
|
896
|
+
reasoningEffort: typeof sessionConfig.reasoningEffort === "string" && sessionConfig.reasoningEffort.trim()
|
|
897
|
+
? sessionConfig.reasoningEffort.trim()
|
|
898
|
+
: undefined,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
attachSessionEventHandlers(session) {
|
|
902
|
+
if (!session || typeof session.on !== "function") {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const eventTypes = [
|
|
906
|
+
"session.error",
|
|
907
|
+
"session.idle",
|
|
908
|
+
"assistant.turn_start",
|
|
909
|
+
"assistant.intent",
|
|
910
|
+
"assistant.reasoning",
|
|
911
|
+
"assistant.reasoning_delta",
|
|
912
|
+
"assistant.message",
|
|
913
|
+
"assistant.message_delta",
|
|
914
|
+
"assistant.turn_end",
|
|
915
|
+
"assistant.usage",
|
|
916
|
+
"tool.execution_start",
|
|
917
|
+
"tool.execution_progress",
|
|
918
|
+
"tool.execution_partial_result",
|
|
919
|
+
"tool.execution_complete",
|
|
920
|
+
"abort",
|
|
921
|
+
];
|
|
922
|
+
for (const eventType of eventTypes) {
|
|
923
|
+
const unsubscribe = session.on(eventType, (event) => {
|
|
924
|
+
void this.handleCopilotEvent(event).catch((error) => {
|
|
925
|
+
this.handleCopilotEventFailure(error);
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
if (typeof unsubscribe === "function") {
|
|
929
|
+
this.sessionSubscriptions.push(unsubscribe);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
detachSessionEventHandlers() {
|
|
934
|
+
if (this.sessionSubscriptions.length === 0) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
for (const unsubscribe of this.sessionSubscriptions) {
|
|
938
|
+
try {
|
|
939
|
+
unsubscribe();
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
// best effort
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
this.sessionSubscriptions = [];
|
|
946
|
+
}
|
|
947
|
+
async requestOrThrow(promise) {
|
|
948
|
+
try {
|
|
949
|
+
return await promise;
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
this.maybeEmitAuthRequired({
|
|
953
|
+
message: extractErrorMessage(error),
|
|
954
|
+
});
|
|
955
|
+
throw error;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
applySessionInfo(sessionId, extras = {}) {
|
|
959
|
+
const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
960
|
+
if (!normalizedSessionId) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const changed = this.sessionId !== normalizedSessionId;
|
|
964
|
+
this.sessionId = normalizedSessionId;
|
|
965
|
+
const resolvedReasoningEffort = typeof extras.reasoningEffort === "string" && extras.reasoningEffort.trim()
|
|
966
|
+
? extras.reasoningEffort.trim()
|
|
967
|
+
: typeof this.lastUsage?.reasoningEffort === "string" && this.lastUsage.reasoningEffort.trim()
|
|
968
|
+
? this.lastUsage.reasoningEffort.trim()
|
|
969
|
+
: normalizeReasoningEffort(this.options.reasoningEffort);
|
|
970
|
+
const resolvedModel = typeof extras.model === "string" && extras.model.trim()
|
|
971
|
+
? extras.model.trim()
|
|
972
|
+
: typeof this.lastUsage?.model === "string" && this.lastUsage.model.trim()
|
|
973
|
+
? this.lastUsage.model.trim()
|
|
974
|
+
: typeof this.options.model === "string" && this.options.model.trim()
|
|
975
|
+
? this.options.model.trim()
|
|
976
|
+
: undefined;
|
|
977
|
+
this.sessionInfo = {
|
|
978
|
+
...(this.sessionInfo || {}),
|
|
979
|
+
backend: this.backend,
|
|
980
|
+
sessionId: normalizedSessionId,
|
|
981
|
+
model: resolvedModel,
|
|
982
|
+
modelProvider: "github-copilot",
|
|
983
|
+
reasoningEffort: resolvedReasoningEffort,
|
|
984
|
+
};
|
|
985
|
+
if (changed) {
|
|
986
|
+
this.trace(`session ready id=${normalizedSessionId}`);
|
|
987
|
+
this.emit("session", this.getSessionInfo());
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
maybeEmitAuthRequired({ errorType = "", message = "", statusCode = undefined, url = undefined } = {}) {
|
|
991
|
+
if (!isAuthLikeError({ errorType, message, statusCode })) {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const signature = `${String(errorType)}\n${String(statusCode ?? "")}\n${String(message)}\n${String(url || "")}`;
|
|
995
|
+
if (signature === this.lastAuthRequiredSignature) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
this.lastAuthRequiredSignature = signature;
|
|
999
|
+
this.emit("auth_required", {
|
|
1000
|
+
reason: "login_required",
|
|
1001
|
+
message: message || "GitHub Copilot authentication is required",
|
|
1002
|
+
url: typeof url === "string" && url.trim() ? url.trim() : undefined,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
createMessageId(currentTurn, messageId) {
|
|
1006
|
+
const normalizedMessageId = typeof messageId === "string" && messageId.trim()
|
|
1007
|
+
? messageId.trim()
|
|
1008
|
+
: `message-${currentTurn.messageOrder.length + 1}`;
|
|
1009
|
+
if (!currentTurn.messageOrder.includes(normalizedMessageId)) {
|
|
1010
|
+
currentTurn.messageOrder.push(normalizedMessageId);
|
|
1011
|
+
}
|
|
1012
|
+
if (!currentTurn.messageText.has(normalizedMessageId)) {
|
|
1013
|
+
currentTurn.messageText.set(normalizedMessageId, "");
|
|
1014
|
+
}
|
|
1015
|
+
return normalizedMessageId;
|
|
1016
|
+
}
|
|
1017
|
+
updateMessageText(currentTurn, messageId, text, { replace = false } = {}) {
|
|
1018
|
+
const normalizedMessageId = this.createMessageId(currentTurn, messageId);
|
|
1019
|
+
const existingText = currentTurn.messageText.get(normalizedMessageId) || "";
|
|
1020
|
+
const nextText = replace ? normalizeText(text) : `${existingText}${normalizeText(text)}`;
|
|
1021
|
+
currentTurn.messageText.set(normalizedMessageId, nextText);
|
|
1022
|
+
currentTurn.fullText = currentTurn.messageOrder
|
|
1023
|
+
.map((id) => currentTurn.messageText.get(id) || "")
|
|
1024
|
+
.join("");
|
|
1025
|
+
return {
|
|
1026
|
+
messageId: normalizedMessageId,
|
|
1027
|
+
fullText: currentTurn.fullText,
|
|
1028
|
+
messageText: nextText,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
applyCompletionText(currentTurn, completion) {
|
|
1032
|
+
const completionText = normalizeText(completion?.data?.content) ||
|
|
1033
|
+
normalizeText(completion?.content);
|
|
1034
|
+
if (!completionText) {
|
|
1035
|
+
return "";
|
|
1036
|
+
}
|
|
1037
|
+
const completionMessageId = typeof completion?.data?.messageId === "string" && completion.data.messageId.trim()
|
|
1038
|
+
? completion.data.messageId.trim()
|
|
1039
|
+
: typeof completion?.messageId === "string" && completion.messageId.trim()
|
|
1040
|
+
? completion.messageId.trim()
|
|
1041
|
+
: "";
|
|
1042
|
+
if (completionMessageId) {
|
|
1043
|
+
return this.updateMessageText(currentTurn, completionMessageId, completionText, { replace: true }).fullText;
|
|
1044
|
+
}
|
|
1045
|
+
if (currentTurn.messageOrder.length === 1) {
|
|
1046
|
+
return this.updateMessageText(currentTurn, currentTurn.messageOrder[0], completionText, { replace: true }).fullText;
|
|
1047
|
+
}
|
|
1048
|
+
if (currentTurn.messageOrder.length === 0) {
|
|
1049
|
+
return this.updateMessageText(currentTurn, undefined, completionText, { replace: true }).fullText;
|
|
1050
|
+
}
|
|
1051
|
+
return currentTurn.fullText || completionText;
|
|
1052
|
+
}
|
|
1053
|
+
async closeCopilotResources(session, client) {
|
|
1054
|
+
let forceStopAttempted = false;
|
|
1055
|
+
const forceStopClient = async () => {
|
|
1056
|
+
if (forceStopAttempted || !client || typeof client.forceStop !== "function") {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
forceStopAttempted = true;
|
|
1060
|
+
try {
|
|
1061
|
+
await client.forceStop();
|
|
1062
|
+
}
|
|
1063
|
+
catch {
|
|
1064
|
+
// best effort
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
try {
|
|
1068
|
+
if (session && typeof session.disconnect === "function") {
|
|
1069
|
+
await withTimeout(session.disconnect(), this.closeTimeoutMs, "copilot session disconnect timed out");
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
await forceStopClient();
|
|
1074
|
+
}
|
|
1075
|
+
if (forceStopAttempted) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
if (client && typeof client.stop === "function") {
|
|
1080
|
+
await withTimeout(client.stop(), this.closeTimeoutMs, "copilot SDK stop timed out");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
catch {
|
|
1084
|
+
await forceStopClient();
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
setCurrentTurnError(currentTurn, error) {
|
|
1088
|
+
if (!currentTurn || currentTurn.error) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
currentTurn.error = normalizeTurnError(error);
|
|
1092
|
+
}
|
|
1093
|
+
handleCopilotEventFailure(error) {
|
|
1094
|
+
this.setCurrentTurnError(this.currentTurn, error);
|
|
1095
|
+
this.trace(`event handling failed: ${extractErrorMessage(error)}`);
|
|
1096
|
+
void this.interruptCurrentTurn().catch(() => { });
|
|
1097
|
+
}
|
|
1098
|
+
async handleCopilotEvent(event) {
|
|
1099
|
+
if (!event || typeof event !== "object") {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
this.touchTurnActivity();
|
|
1103
|
+
const currentTurn = this.currentTurn;
|
|
1104
|
+
if (currentTurn) {
|
|
1105
|
+
currentTurn.items.push(event);
|
|
1106
|
+
currentTurn.events.push(event);
|
|
1107
|
+
}
|
|
1108
|
+
switch (event.type) {
|
|
1109
|
+
case "assistant.turn_start": {
|
|
1110
|
+
if (!currentTurn) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
await this.emitWorkingStatus({
|
|
1114
|
+
phase: "turn_started",
|
|
1115
|
+
reply_in_progress: true,
|
|
1116
|
+
status_line: "copilot is working",
|
|
1117
|
+
}, currentTurn.onProgress);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
case "assistant.intent": {
|
|
1121
|
+
if (!currentTurn) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const intent = sanitizeSummary(event.data?.intent);
|
|
1125
|
+
await this.emitWorkingStatus({
|
|
1126
|
+
phase: "planning",
|
|
1127
|
+
reply_in_progress: true,
|
|
1128
|
+
status_line: statusLineForPhase("planning", "", intent),
|
|
1129
|
+
}, currentTurn.onProgress);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
case "assistant.reasoning":
|
|
1133
|
+
case "assistant.reasoning_delta": {
|
|
1134
|
+
if (!currentTurn) {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
await this.emitWorkingStatus({
|
|
1138
|
+
phase: "reasoning",
|
|
1139
|
+
reply_in_progress: true,
|
|
1140
|
+
status_line: statusLineForPhase("reasoning"),
|
|
1141
|
+
}, currentTurn.onProgress);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
case "tool.execution_start": {
|
|
1145
|
+
if (!currentTurn) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
currentTurn.activeToolName = normalizeText(event.data?.toolName);
|
|
1149
|
+
currentTurn.activeToolPhase = toolPhaseForName(currentTurn.activeToolName);
|
|
1150
|
+
await this.emitWorkingStatus({
|
|
1151
|
+
phase: currentTurn.activeToolPhase,
|
|
1152
|
+
reply_in_progress: true,
|
|
1153
|
+
status_line: statusLineForPhase(currentTurn.activeToolPhase, currentTurn.activeToolName),
|
|
1154
|
+
}, currentTurn.onProgress);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
case "tool.execution_progress": {
|
|
1158
|
+
if (!currentTurn) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const progressMessage = sanitizeSummary(event.data?.progressMessage);
|
|
1162
|
+
const phase = currentTurn.activeToolPhase || "tool_call";
|
|
1163
|
+
await this.emitWorkingStatus({
|
|
1164
|
+
phase,
|
|
1165
|
+
reply_in_progress: true,
|
|
1166
|
+
status_line: progressMessage || statusLineForPhase(phase, currentTurn.activeToolName),
|
|
1167
|
+
}, currentTurn.onProgress);
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
case "tool.execution_partial_result": {
|
|
1171
|
+
if (!currentTurn) {
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const partialOutput = sanitizeSummary(event.data?.partialOutput, 120);
|
|
1175
|
+
const phase = currentTurn.activeToolPhase || "tool_call";
|
|
1176
|
+
await this.emitWorkingStatus({
|
|
1177
|
+
phase,
|
|
1178
|
+
reply_in_progress: true,
|
|
1179
|
+
status_line: statusLineForPhase(phase, currentTurn.activeToolName),
|
|
1180
|
+
reply_preview: partialOutput || undefined,
|
|
1181
|
+
}, currentTurn.onProgress);
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
case "tool.execution_complete": {
|
|
1185
|
+
if (!currentTurn) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
currentTurn.activeToolName = "";
|
|
1189
|
+
currentTurn.activeToolPhase = "";
|
|
1190
|
+
const preview = extractToolResultPreview(event.data?.result);
|
|
1191
|
+
await this.emitWorkingStatus({
|
|
1192
|
+
phase: "message_aggregation",
|
|
1193
|
+
reply_in_progress: true,
|
|
1194
|
+
status_line: statusLineForPhase("message_aggregation"),
|
|
1195
|
+
reply_preview: preview || undefined,
|
|
1196
|
+
}, currentTurn.onProgress);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
case "assistant.message_delta": {
|
|
1200
|
+
if (!currentTurn) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const deltaContent = normalizeText(event.data?.deltaContent);
|
|
1204
|
+
if (!deltaContent) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const normalizedMessageId = this.createMessageId(currentTurn, event.data?.messageId);
|
|
1208
|
+
const { fullText } = this.updateMessageText(currentTurn, normalizedMessageId, deltaContent);
|
|
1209
|
+
await this.emitWorkingStatus({
|
|
1210
|
+
phase: "message_aggregation",
|
|
1211
|
+
reply_in_progress: true,
|
|
1212
|
+
status_line: statusLineForPhase("message_aggregation"),
|
|
1213
|
+
reply_preview: sanitizeSummary(fullText, 120),
|
|
1214
|
+
}, currentTurn.onProgress);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
case "assistant.message": {
|
|
1218
|
+
if (!currentTurn) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const content = normalizeText(event.data?.content);
|
|
1222
|
+
if (!content) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const normalizedMessageId = this.createMessageId(currentTurn, event.data?.messageId);
|
|
1226
|
+
const { fullText } = this.updateMessageText(currentTurn, normalizedMessageId, content, {
|
|
1227
|
+
replace: true,
|
|
1228
|
+
});
|
|
1229
|
+
await this.emitWorkingStatus({
|
|
1230
|
+
phase: "message_aggregation",
|
|
1231
|
+
reply_in_progress: true,
|
|
1232
|
+
status_line: statusLineForPhase("message_aggregation"),
|
|
1233
|
+
reply_preview: sanitizeSummary(fullText, 120),
|
|
1234
|
+
}, currentTurn.onProgress);
|
|
1235
|
+
await this.emitBufferedAssistantMessage(currentTurn, normalizedMessageId);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
case "assistant.usage": {
|
|
1239
|
+
this.lastUsage = normalizeUsagePayload(event.data);
|
|
1240
|
+
this.applySessionInfo(this.sessionId || this.resumeSessionId, {
|
|
1241
|
+
model: typeof event.data?.model === "string" && event.data.model.trim()
|
|
1242
|
+
? event.data.model.trim()
|
|
1243
|
+
: undefined,
|
|
1244
|
+
reasoningEffort: typeof event.data?.reasoningEffort === "string" && event.data.reasoningEffort.trim()
|
|
1245
|
+
? event.data.reasoningEffort.trim()
|
|
1246
|
+
: undefined,
|
|
1247
|
+
});
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
case "assistant.turn_end": {
|
|
1251
|
+
if (!currentTurn) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
await this.emitWorkingStatus({
|
|
1255
|
+
phase: "message_aggregation",
|
|
1256
|
+
reply_in_progress: true,
|
|
1257
|
+
status_line: statusLineForPhase("message_aggregation"),
|
|
1258
|
+
reply_preview: sanitizeSummary(currentTurn.fullText, 120),
|
|
1259
|
+
}, currentTurn.onProgress);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
case "session.error": {
|
|
1263
|
+
const message = normalizeText(event.data?.message) || "Copilot turn failed";
|
|
1264
|
+
this.maybeEmitAuthRequired({
|
|
1265
|
+
errorType: event.data?.errorType,
|
|
1266
|
+
message,
|
|
1267
|
+
statusCode: event.data?.statusCode,
|
|
1268
|
+
url: event.data?.url,
|
|
1269
|
+
});
|
|
1270
|
+
if (!currentTurn) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
currentTurn.error = createTurnError(message, {
|
|
1274
|
+
reason: "provider_error",
|
|
1275
|
+
errorType: event.data?.errorType,
|
|
1276
|
+
statusCode: event.data?.statusCode,
|
|
1277
|
+
url: event.data?.url,
|
|
1278
|
+
});
|
|
1279
|
+
await this.emitTerminalWorkingStatus(currentTurn, {
|
|
1280
|
+
phase: "turn_failed",
|
|
1281
|
+
status_done_line: message,
|
|
1282
|
+
}, currentTurn.onProgress);
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
case "abort": {
|
|
1286
|
+
if (!currentTurn) {
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
currentTurn.abortRequested = true;
|
|
1290
|
+
await this.emitTerminalWorkingStatus(currentTurn, {
|
|
1291
|
+
phase: "turn_interrupted",
|
|
1292
|
+
status_done_line: normalizeText(event.data?.reason) || "copilot interrupted",
|
|
1293
|
+
}, currentTurn.onProgress);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
case "session.idle": {
|
|
1297
|
+
if (!currentTurn) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
await this.emitTerminalWorkingStatus(currentTurn, {
|
|
1301
|
+
phase: currentTurn.abortRequested || event.data?.aborted ? "turn_interrupted" : "turn_completed",
|
|
1302
|
+
status_done_line: currentTurn.abortRequested || event.data?.aborted ? "copilot interrupted" : "copilot finished",
|
|
1303
|
+
}, currentTurn.onProgress);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
default:
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async interruptCurrentTurn() {
|
|
1311
|
+
if (!this.currentTurn || !this.session || typeof this.session.abort !== "function") {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
try {
|
|
1315
|
+
await this.session.abort();
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
if (!this.closeRequested) {
|
|
1319
|
+
throw error;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
async runTurn(promptText, { useInitialImages = false, onProgress = null } = {}) {
|
|
1324
|
+
if (this.currentTurn) {
|
|
1325
|
+
throw this.createTurnAlreadyRunningError();
|
|
1326
|
+
}
|
|
1327
|
+
const prompt = this.buildPrompt(promptText, { useInitialImages });
|
|
1328
|
+
if (!prompt) {
|
|
1329
|
+
return {
|
|
1330
|
+
text: "",
|
|
1331
|
+
usage: this.lastUsage ? { ...this.lastUsage } : null,
|
|
1332
|
+
items: [],
|
|
1333
|
+
events: [],
|
|
1334
|
+
provider: this.backend,
|
|
1335
|
+
metadata: {
|
|
1336
|
+
source: COPILOT_PROVIDER_VARIANT,
|
|
1337
|
+
sessionId: this.sessionId || undefined,
|
|
1338
|
+
},
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
const currentTurn = {
|
|
1342
|
+
items: [],
|
|
1343
|
+
events: [],
|
|
1344
|
+
fullText: "",
|
|
1345
|
+
messageOrder: [],
|
|
1346
|
+
messageText: new Map(),
|
|
1347
|
+
emittedMessageIds: new Set(),
|
|
1348
|
+
activeToolName: "",
|
|
1349
|
+
activeToolPhase: "",
|
|
1350
|
+
abortRequested: false,
|
|
1351
|
+
terminalWorkingStatusEmitted: false,
|
|
1352
|
+
error: null,
|
|
1353
|
+
onProgress,
|
|
1354
|
+
};
|
|
1355
|
+
this.currentTurn = currentTurn;
|
|
1356
|
+
this.touchTurnActivity();
|
|
1357
|
+
this.markTurnStartedStatus();
|
|
1358
|
+
const closeGuard = this.createCloseGuard(() => {
|
|
1359
|
+
void this.interruptCurrentTurn();
|
|
1360
|
+
});
|
|
1361
|
+
const turnTimeoutGuard = this.createTurnTimeoutGuard(() => {
|
|
1362
|
+
void this.interruptCurrentTurn();
|
|
1363
|
+
});
|
|
1364
|
+
try {
|
|
1365
|
+
await Promise.race([
|
|
1366
|
+
this.boot(),
|
|
1367
|
+
closeGuard.promise,
|
|
1368
|
+
turnTimeoutGuard.promise,
|
|
1369
|
+
]);
|
|
1370
|
+
this.applySessionInfo(this.session?.sessionId || this.sessionId);
|
|
1371
|
+
const completion = await Promise.race([
|
|
1372
|
+
this.requestOrThrow(this.session.sendAndWait({
|
|
1373
|
+
prompt,
|
|
1374
|
+
mode: "immediate",
|
|
1375
|
+
}, this.turnDeadlineMs + SDK_SEND_AND_WAIT_TIMEOUT_GRACE_MS)),
|
|
1376
|
+
closeGuard.promise,
|
|
1377
|
+
turnTimeoutGuard.promise,
|
|
1378
|
+
]);
|
|
1379
|
+
if (currentTurn.error) {
|
|
1380
|
+
throw currentTurn.error;
|
|
1381
|
+
}
|
|
1382
|
+
const completionText = this.applyCompletionText(currentTurn, completion) ||
|
|
1383
|
+
currentTurn.fullText;
|
|
1384
|
+
if (completionText && !currentTurn.fullText) {
|
|
1385
|
+
currentTurn.fullText = completionText;
|
|
1386
|
+
}
|
|
1387
|
+
const emittedBufferedMessages = await this.emitPendingAssistantMessages(currentTurn);
|
|
1388
|
+
if (completionText && currentTurn.messageOrder.length === 0 && !emittedBufferedMessages) {
|
|
1389
|
+
await this.emitWorkingStatus({
|
|
1390
|
+
phase: "message_aggregation",
|
|
1391
|
+
reply_in_progress: true,
|
|
1392
|
+
status_line: statusLineForPhase("message_aggregation"),
|
|
1393
|
+
reply_preview: sanitizeSummary(completionText, 120),
|
|
1394
|
+
}, onProgress);
|
|
1395
|
+
await this.emitAssistantMessage(completionText);
|
|
1396
|
+
}
|
|
1397
|
+
if (completionText) {
|
|
1398
|
+
this.history.push({ role: "assistant", content: completionText });
|
|
1399
|
+
}
|
|
1400
|
+
this.activeReplyTarget = "";
|
|
1401
|
+
await this.emitTerminalWorkingStatus(currentTurn, {
|
|
1402
|
+
phase: currentTurn.abortRequested ? "turn_interrupted" : "turn_completed",
|
|
1403
|
+
status_done_line: currentTurn.abortRequested ? "copilot interrupted" : "copilot finished",
|
|
1404
|
+
}, onProgress);
|
|
1405
|
+
return {
|
|
1406
|
+
text: completionText,
|
|
1407
|
+
usage: this.lastUsage ? { ...this.lastUsage } : null,
|
|
1408
|
+
items: currentTurn.items,
|
|
1409
|
+
events: currentTurn.events,
|
|
1410
|
+
provider: this.backend,
|
|
1411
|
+
metadata: {
|
|
1412
|
+
source: COPILOT_PROVIDER_VARIANT,
|
|
1413
|
+
sessionId: this.sessionId || undefined,
|
|
1414
|
+
model: this.sessionInfo?.model,
|
|
1415
|
+
modelProvider: this.sessionInfo?.modelProvider,
|
|
1416
|
+
reasoningEffort: this.sessionInfo?.reasoningEffort,
|
|
1417
|
+
},
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
if (error?.reason === "turn_timeout") {
|
|
1422
|
+
await this.interruptCurrentTurn();
|
|
1423
|
+
}
|
|
1424
|
+
const message = extractErrorMessage(currentTurn.error || error);
|
|
1425
|
+
await this.emitTerminalWorkingStatus(currentTurn, {
|
|
1426
|
+
phase: error?.reason === "turn_interrupted" || currentTurn.abortRequested ? "turn_interrupted" : "turn_failed",
|
|
1427
|
+
status_done_line: message,
|
|
1428
|
+
}, onProgress);
|
|
1429
|
+
throw currentTurn.error || error;
|
|
1430
|
+
}
|
|
1431
|
+
finally {
|
|
1432
|
+
closeGuard.cleanup();
|
|
1433
|
+
turnTimeoutGuard.cleanup();
|
|
1434
|
+
this.activeReplyTarget = "";
|
|
1435
|
+
this.currentTurn = null;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
async close() {
|
|
1439
|
+
if (this.closed) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
this.closed = true;
|
|
1443
|
+
this.closeRequested = true;
|
|
1444
|
+
this.flushCloseWaiters();
|
|
1445
|
+
try {
|
|
1446
|
+
await this.interruptCurrentTurn();
|
|
1447
|
+
}
|
|
1448
|
+
catch {
|
|
1449
|
+
// best effort
|
|
1450
|
+
}
|
|
1451
|
+
this.detachSessionEventHandlers();
|
|
1452
|
+
const session = this.session;
|
|
1453
|
+
const client = this.client;
|
|
1454
|
+
this.session = null;
|
|
1455
|
+
this.client = null;
|
|
1456
|
+
try {
|
|
1457
|
+
await this.closeCopilotResources(session, client);
|
|
1458
|
+
}
|
|
1459
|
+
catch {
|
|
1460
|
+
// best effort
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|