@makefinks/daemon 0.9.1 → 0.11.0
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 +60 -14
- package/package.json +4 -2
- package/src/ai/copilot-client.ts +775 -0
- package/src/ai/daemon-ai.ts +32 -234
- package/src/ai/model-config.ts +55 -14
- package/src/ai/providers/capabilities.ts +16 -0
- package/src/ai/providers/copilot-provider.ts +632 -0
- package/src/ai/providers/openrouter-provider.ts +217 -0
- package/src/ai/providers/registry.ts +14 -0
- package/src/ai/providers/types.ts +31 -0
- package/src/ai/system-prompt.ts +16 -0
- package/src/ai/tools/subagents.ts +1 -1
- package/src/ai/tools/tool-registry.ts +22 -1
- package/src/ai/tools/write-file.ts +51 -0
- package/src/app/components/AppOverlays.tsx +9 -1
- package/src/app/components/ConversationPane.tsx +8 -2
- package/src/components/ModelMenu.tsx +202 -140
- package/src/components/OnboardingOverlay.tsx +147 -1
- package/src/components/SettingsMenu.tsx +27 -1
- package/src/components/TokenUsageDisplay.tsx +5 -3
- package/src/components/tool-layouts/layouts/index.ts +1 -0
- package/src/components/tool-layouts/layouts/write-file.tsx +117 -0
- package/src/hooks/daemon-event-handlers.ts +61 -14
- package/src/hooks/keyboard-handlers.ts +109 -28
- package/src/hooks/use-app-callbacks.ts +141 -43
- package/src/hooks/use-app-context-builder.ts +5 -0
- package/src/hooks/use-app-controller.ts +31 -2
- package/src/hooks/use-app-copilot-models-loader.ts +45 -0
- package/src/hooks/use-app-display-state.ts +24 -2
- package/src/hooks/use-app-model.ts +103 -17
- package/src/hooks/use-app-preferences-bootstrap.ts +54 -10
- package/src/hooks/use-bootstrap-controller.ts +5 -0
- package/src/hooks/use-daemon-events.ts +8 -2
- package/src/hooks/use-daemon-keyboard.ts +19 -6
- package/src/hooks/use-daemon-runtime-controller.ts +4 -0
- package/src/hooks/use-menu-keyboard.ts +6 -1
- package/src/state/app-context.tsx +6 -0
- package/src/types/index.ts +24 -1
- package/src/utils/copilot-models.ts +77 -0
- package/src/utils/preferences.ts +3 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import type { ModelMessage } from "ai";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { getDaemonManager } from "../../state/daemon-state";
|
|
4
|
+
import { getRuntimeContext } from "../../state/runtime-context";
|
|
5
|
+
import type { ReasoningEffort, StreamCallbacks } from "../../types";
|
|
6
|
+
import { debug, toolDebug } from "../../utils/debug-logger";
|
|
7
|
+
import { getWorkspacePath } from "../../utils/workspace-manager";
|
|
8
|
+
import { convertToolSetToCopilotTools, getOrCreateCopilotSession } from "../copilot-client";
|
|
9
|
+
import { getResponseModel } from "../model-config";
|
|
10
|
+
import { buildDaemonSystemPrompt } from "../system-prompt";
|
|
11
|
+
import { getCachedToolAvailability, getDaemonTools } from "../tools/index";
|
|
12
|
+
import { createToolAvailabilitySnapshot, resolveToolAvailability } from "../tools/tool-registry";
|
|
13
|
+
import { getProviderCapabilities } from "./capabilities";
|
|
14
|
+
import type { LlmProviderAdapter, ProviderStreamRequest, ProviderStreamResult } from "./types";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_COPILOT_SEND_TIMEOUT_MS = 20000;
|
|
17
|
+
const DEFAULT_COPILOT_IDLE_TIMEOUT_MS = 60000;
|
|
18
|
+
|
|
19
|
+
function buildCopilotModelsListErrorMessage(): string {
|
|
20
|
+
return [
|
|
21
|
+
"Copilot failed to list available models.",
|
|
22
|
+
"DAEMON uses logged-in-user auth for Copilot.",
|
|
23
|
+
"Verify `gh auth status` and run `copilot login`.",
|
|
24
|
+
"If your network uses custom certs set `COPILOT_USE_SYSTEM_CA=1` or `NODE_EXTRA_CA_CERTS`.",
|
|
25
|
+
].join(" ");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildCopilotModelsListErrorMessageWithCause(cause: string | undefined): string {
|
|
29
|
+
const baseMessage = buildCopilotModelsListErrorMessage();
|
|
30
|
+
if (!cause) return baseMessage;
|
|
31
|
+
const compactCause = cause.replace(/\s+/g, " ").trim();
|
|
32
|
+
if (!compactCause) return baseMessage;
|
|
33
|
+
return `${baseMessage} Underlying Copilot error: ${compactCause}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractErrorDiagnostics(error: unknown): {
|
|
37
|
+
message?: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
code?: string | number;
|
|
40
|
+
} {
|
|
41
|
+
if (!error || typeof error !== "object") {
|
|
42
|
+
return {
|
|
43
|
+
message: error === undefined ? undefined : String(error),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const candidate = error as {
|
|
48
|
+
message?: unknown;
|
|
49
|
+
name?: unknown;
|
|
50
|
+
code?: unknown;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const message = typeof candidate.message === "string" ? candidate.message : undefined;
|
|
54
|
+
const name = typeof candidate.name === "string" ? candidate.name : undefined;
|
|
55
|
+
const code =
|
|
56
|
+
typeof candidate.code === "string" || typeof candidate.code === "number" ? candidate.code : undefined;
|
|
57
|
+
|
|
58
|
+
return { message, name, code };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseTimeoutMs(value: string | undefined, fallback: number): number {
|
|
62
|
+
if (!value) return fallback;
|
|
63
|
+
const parsed = Number.parseInt(value, 10);
|
|
64
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
|
|
69
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
70
|
+
try {
|
|
71
|
+
return await Promise.race([
|
|
72
|
+
promise,
|
|
73
|
+
new Promise<T>((_, reject) => {
|
|
74
|
+
timeoutId = setTimeout(() => {
|
|
75
|
+
reject(new Error(message));
|
|
76
|
+
}, timeoutMs);
|
|
77
|
+
}),
|
|
78
|
+
]);
|
|
79
|
+
} finally {
|
|
80
|
+
if (timeoutId) {
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function modelMessageContentToText(message: ModelMessage): string {
|
|
87
|
+
if (typeof message.content === "string") {
|
|
88
|
+
return message.content;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!Array.isArray(message.content)) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const textParts: string[] = [];
|
|
96
|
+
for (const part of message.content) {
|
|
97
|
+
if (!part || typeof part !== "object") continue;
|
|
98
|
+
if ("type" in part && part.type === "text" && "text" in part && typeof part.text === "string") {
|
|
99
|
+
textParts.push(part.text);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return textParts.join("").trim();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildHistoryPreamble(messages: ModelMessage[]): string {
|
|
107
|
+
const lines: string[] = [];
|
|
108
|
+
for (const message of messages) {
|
|
109
|
+
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
110
|
+
const text = modelMessageContentToText(message).trim();
|
|
111
|
+
if (!text) continue;
|
|
112
|
+
const label = message.role === "user" ? "User" : "Assistant";
|
|
113
|
+
lines.push(`${label}: ${text}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (lines.length === 0) return "";
|
|
117
|
+
|
|
118
|
+
const trimmed = lines.slice(-20).join("\n");
|
|
119
|
+
return `Conversation context from earlier turns:\n${trimmed}\n\nContinue from this context.`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildCopilotPrompt(
|
|
123
|
+
userMessage: string,
|
|
124
|
+
memoryInjection: string | undefined,
|
|
125
|
+
conversationHistory: ModelMessage[],
|
|
126
|
+
includeHistory: boolean
|
|
127
|
+
): string {
|
|
128
|
+
const sections: string[] = [];
|
|
129
|
+
|
|
130
|
+
if (memoryInjection) {
|
|
131
|
+
sections.push(`Relevant memory:\n${memoryInjection}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (includeHistory) {
|
|
135
|
+
const history = buildHistoryPreamble(conversationHistory);
|
|
136
|
+
if (history) {
|
|
137
|
+
sections.push(history);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
sections.push(userMessage);
|
|
142
|
+
return sections.join("\n\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function streamCopilotSession(params: {
|
|
146
|
+
userMessage: string;
|
|
147
|
+
callbacks: StreamCallbacks;
|
|
148
|
+
conversationHistory: ModelMessage[];
|
|
149
|
+
interactionMode: ProviderStreamRequest["interactionMode"];
|
|
150
|
+
abortSignal?: AbortSignal;
|
|
151
|
+
reasoningEffort?: ReasoningEffort;
|
|
152
|
+
memoryInjection?: string;
|
|
153
|
+
}): Promise<{ fullText: string; finalText: string } | null> {
|
|
154
|
+
const {
|
|
155
|
+
userMessage,
|
|
156
|
+
callbacks,
|
|
157
|
+
conversationHistory,
|
|
158
|
+
interactionMode,
|
|
159
|
+
abortSignal,
|
|
160
|
+
reasoningEffort,
|
|
161
|
+
memoryInjection,
|
|
162
|
+
} = params;
|
|
163
|
+
|
|
164
|
+
const { sessionId } = getRuntimeContext();
|
|
165
|
+
const tools = await getDaemonTools();
|
|
166
|
+
const copilotTools = convertToolSetToCopilotTools(tools, callbacks);
|
|
167
|
+
const daemonToolNames = Object.keys(tools);
|
|
168
|
+
const toolAvailability =
|
|
169
|
+
getCachedToolAvailability() ?? (await resolveToolAvailability(getDaemonManager().toolToggles));
|
|
170
|
+
const workspacePath = sessionId ? getWorkspacePath(sessionId) : undefined;
|
|
171
|
+
|
|
172
|
+
const systemPrompt = buildDaemonSystemPrompt({
|
|
173
|
+
mode: interactionMode,
|
|
174
|
+
toolAvailability: createToolAvailabilitySnapshot(toolAvailability),
|
|
175
|
+
workspacePath,
|
|
176
|
+
memoryInjection,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const baseSessionConfig = {
|
|
180
|
+
model: getResponseModel(),
|
|
181
|
+
reasoningEffort,
|
|
182
|
+
tools: copilotTools,
|
|
183
|
+
availableTools: daemonToolNames,
|
|
184
|
+
systemMessage: {
|
|
185
|
+
mode: "replace" as const,
|
|
186
|
+
content: systemPrompt,
|
|
187
|
+
},
|
|
188
|
+
streaming: true,
|
|
189
|
+
workingDirectory: workspacePath ?? process.cwd(),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const requestId = randomUUID();
|
|
193
|
+
const requestStartedAt = Date.now();
|
|
194
|
+
const recentEvents: Array<Record<string, unknown>> = [];
|
|
195
|
+
const rememberEvent = (event: Record<string, unknown>) => {
|
|
196
|
+
recentEvents.push({
|
|
197
|
+
at: new Date().toISOString(),
|
|
198
|
+
...event,
|
|
199
|
+
});
|
|
200
|
+
if (recentEvents.length > 30) {
|
|
201
|
+
recentEvents.shift();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
let sendStartedAt: number | null = null;
|
|
206
|
+
let sendCompletedAt: number | null = null;
|
|
207
|
+
let idleWaitStartedAt: number | null = null;
|
|
208
|
+
let idleResolvedAt: number | null = null;
|
|
209
|
+
let lastEventAt: number | null = null;
|
|
210
|
+
let lastToolCompletionAt: number | null = null;
|
|
211
|
+
let duplicateEventCount = 0;
|
|
212
|
+
let assistantReasoningEvents = 0;
|
|
213
|
+
let assistantDeltaEvents = 0;
|
|
214
|
+
let assistantMessageEvents = 0;
|
|
215
|
+
let toolExecutionStartEvents = 0;
|
|
216
|
+
let toolExecutionCompleteEvents = 0;
|
|
217
|
+
let toolExecutionFailureEvents = 0;
|
|
218
|
+
|
|
219
|
+
debug.info("copilot-stream-start", {
|
|
220
|
+
requestId,
|
|
221
|
+
model: baseSessionConfig.model,
|
|
222
|
+
interactionMode,
|
|
223
|
+
conversationHistoryMessages: conversationHistory.length,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const { session, created } = await (async () => {
|
|
227
|
+
try {
|
|
228
|
+
return await getOrCreateCopilotSession(sessionId ?? randomUUID(), baseSessionConfig);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
231
|
+
debug.error("copilot-session-create-failed", {
|
|
232
|
+
sessionId: sessionId ?? null,
|
|
233
|
+
model: baseSessionConfig.model,
|
|
234
|
+
message: err.message,
|
|
235
|
+
});
|
|
236
|
+
if (err.message.includes("Failed to list models") || err.message.includes("models.list")) {
|
|
237
|
+
try {
|
|
238
|
+
debug.warn("copilot-session-create-retry-fresh-session", {
|
|
239
|
+
previousSessionId: sessionId ?? null,
|
|
240
|
+
model: baseSessionConfig.model,
|
|
241
|
+
});
|
|
242
|
+
return await getOrCreateCopilotSession(randomUUID(), baseSessionConfig);
|
|
243
|
+
} catch (retryError) {
|
|
244
|
+
const retryErr = retryError instanceof Error ? retryError : new Error(String(retryError));
|
|
245
|
+
debug.error("copilot-session-create-retry-failed", {
|
|
246
|
+
model: baseSessionConfig.model,
|
|
247
|
+
message: retryErr.message,
|
|
248
|
+
});
|
|
249
|
+
throw new Error(buildCopilotModelsListErrorMessageWithCause(retryErr.message));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (err.message.includes("Timed out while")) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
"Copilot provider timed out. Verify Copilot CLI access (run `copilot login`), then retry."
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
})();
|
|
260
|
+
|
|
261
|
+
let fullText = "";
|
|
262
|
+
let finalText = "";
|
|
263
|
+
let streamError: Error | null = null;
|
|
264
|
+
let settled = false;
|
|
265
|
+
const toolInputByCallId = new Map<string, { toolName: string; input?: unknown }>();
|
|
266
|
+
const seenSessionEventIds = new Set<string>();
|
|
267
|
+
const assistantTextByMessageId = new Map<string, string>();
|
|
268
|
+
const lastRawAssistantDeltaByMessageId = new Map<string, string>();
|
|
269
|
+
const unsubscribers: Array<() => void> = [];
|
|
270
|
+
|
|
271
|
+
const markAndCheckDuplicateEvent = (eventId: string): boolean => {
|
|
272
|
+
if (!eventId) return false;
|
|
273
|
+
if (seenSessionEventIds.has(eventId)) {
|
|
274
|
+
duplicateEventCount += 1;
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
seenSessionEventIds.add(eventId);
|
|
278
|
+
return false;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const normalizeAssistantDelta = (messageId: string, rawDelta: string): string => {
|
|
282
|
+
if (!rawDelta) return "";
|
|
283
|
+
|
|
284
|
+
const previousText = assistantTextByMessageId.get(messageId) ?? "";
|
|
285
|
+
const previousRawDelta = lastRawAssistantDeltaByMessageId.get(messageId);
|
|
286
|
+
if (previousRawDelta === rawDelta) {
|
|
287
|
+
return "";
|
|
288
|
+
}
|
|
289
|
+
lastRawAssistantDeltaByMessageId.set(messageId, rawDelta);
|
|
290
|
+
|
|
291
|
+
if (rawDelta.startsWith(previousText)) {
|
|
292
|
+
const normalized = rawDelta.slice(previousText.length);
|
|
293
|
+
assistantTextByMessageId.set(messageId, rawDelta);
|
|
294
|
+
return normalized;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (previousText.endsWith(rawDelta)) {
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
assistantTextByMessageId.set(messageId, `${previousText}${rawDelta}`);
|
|
302
|
+
return rawDelta;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const idlePromise = new Promise<void>((resolve, reject) => {
|
|
306
|
+
unsubscribers.push(
|
|
307
|
+
session.on("session.idle", () => {
|
|
308
|
+
if (settled) return;
|
|
309
|
+
idleResolvedAt = Date.now();
|
|
310
|
+
lastEventAt = idleResolvedAt;
|
|
311
|
+
rememberEvent({ type: "session.idle" });
|
|
312
|
+
settled = true;
|
|
313
|
+
resolve();
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
unsubscribers.push(
|
|
318
|
+
session.on("session.error", (event) => {
|
|
319
|
+
if (settled) return;
|
|
320
|
+
lastEventAt = Date.now();
|
|
321
|
+
rememberEvent({
|
|
322
|
+
type: "session.error",
|
|
323
|
+
message: event.data.message,
|
|
324
|
+
});
|
|
325
|
+
const err = new Error(event.data.message || "Copilot session error");
|
|
326
|
+
streamError = err;
|
|
327
|
+
settled = true;
|
|
328
|
+
reject(err);
|
|
329
|
+
})
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
unsubscribers.push(
|
|
334
|
+
session.on("assistant.reasoning_delta", (event) => {
|
|
335
|
+
if (markAndCheckDuplicateEvent(event.id)) return;
|
|
336
|
+
assistantReasoningEvents += 1;
|
|
337
|
+
lastEventAt = Date.now();
|
|
338
|
+
rememberEvent({ type: "assistant.reasoning_delta" });
|
|
339
|
+
callbacks.onReasoningToken?.(event.data.deltaContent);
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
unsubscribers.push(
|
|
344
|
+
session.on("assistant.message_delta", (event) => {
|
|
345
|
+
if (markAndCheckDuplicateEvent(event.id)) return;
|
|
346
|
+
assistantDeltaEvents += 1;
|
|
347
|
+
lastEventAt = Date.now();
|
|
348
|
+
rememberEvent({
|
|
349
|
+
type: "assistant.message_delta",
|
|
350
|
+
messageId: event.data.messageId,
|
|
351
|
+
deltaLength: event.data.deltaContent?.length ?? 0,
|
|
352
|
+
});
|
|
353
|
+
const messageId = event.data.messageId;
|
|
354
|
+
const rawDelta = event.data.deltaContent;
|
|
355
|
+
const normalizedDelta = normalizeAssistantDelta(messageId, rawDelta);
|
|
356
|
+
if (!normalizedDelta) return;
|
|
357
|
+
fullText += normalizedDelta;
|
|
358
|
+
callbacks.onToken?.(normalizedDelta);
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
unsubscribers.push(
|
|
363
|
+
session.on("assistant.message", (event) => {
|
|
364
|
+
if (markAndCheckDuplicateEvent(event.id)) return;
|
|
365
|
+
assistantMessageEvents += 1;
|
|
366
|
+
lastEventAt = Date.now();
|
|
367
|
+
rememberEvent({
|
|
368
|
+
type: "assistant.message",
|
|
369
|
+
contentLength: event.data.content?.length ?? 0,
|
|
370
|
+
});
|
|
371
|
+
const content = event.data.content?.trim();
|
|
372
|
+
if (!content) return;
|
|
373
|
+
finalText = content;
|
|
374
|
+
if (!fullText) {
|
|
375
|
+
fullText = content;
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
unsubscribers.push(
|
|
381
|
+
session.on("tool.execution_start", (event) => {
|
|
382
|
+
if (markAndCheckDuplicateEvent(event.id)) return;
|
|
383
|
+
toolExecutionStartEvents += 1;
|
|
384
|
+
lastEventAt = Date.now();
|
|
385
|
+
rememberEvent({
|
|
386
|
+
type: "tool.execution_start",
|
|
387
|
+
toolName: event.data.toolName,
|
|
388
|
+
toolCallId: event.data.toolCallId,
|
|
389
|
+
});
|
|
390
|
+
toolInputByCallId.set(event.data.toolCallId, {
|
|
391
|
+
toolName: event.data.toolName,
|
|
392
|
+
input: event.data.arguments,
|
|
393
|
+
});
|
|
394
|
+
// Copilot exposes a single "execution_start" event with full arguments.
|
|
395
|
+
// Emit only onToolCall so the UI creates one tool view per call.
|
|
396
|
+
callbacks.onToolCall?.(event.data.toolName, event.data.arguments, event.data.toolCallId);
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
unsubscribers.push(
|
|
401
|
+
session.on("tool.execution_complete", (event) => {
|
|
402
|
+
if (markAndCheckDuplicateEvent(event.id)) return;
|
|
403
|
+
const tracked = toolInputByCallId.get(event.data.toolCallId);
|
|
404
|
+
const toolName = tracked?.toolName ?? "unknown";
|
|
405
|
+
toolExecutionCompleteEvents += 1;
|
|
406
|
+
lastEventAt = Date.now();
|
|
407
|
+
lastToolCompletionAt = Date.now();
|
|
408
|
+
const errorDiagnostics = extractErrorDiagnostics(event.data.error);
|
|
409
|
+
if (!event.data.success) {
|
|
410
|
+
toolExecutionFailureEvents += 1;
|
|
411
|
+
toolDebug.warn("copilot-tool-execution-failed", {
|
|
412
|
+
requestId,
|
|
413
|
+
model: baseSessionConfig.model,
|
|
414
|
+
toolName,
|
|
415
|
+
toolCallId: event.data.toolCallId,
|
|
416
|
+
error: errorDiagnostics,
|
|
417
|
+
toolTelemetry: event.data.toolTelemetry,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
rememberEvent({
|
|
421
|
+
type: "tool.execution_complete",
|
|
422
|
+
toolName,
|
|
423
|
+
toolCallId: event.data.toolCallId,
|
|
424
|
+
success: event.data.success,
|
|
425
|
+
errorMessage: errorDiagnostics.message,
|
|
426
|
+
errorCode: errorDiagnostics.code,
|
|
427
|
+
});
|
|
428
|
+
const toolResult = event.data.success
|
|
429
|
+
? {
|
|
430
|
+
success: true,
|
|
431
|
+
output: event.data.result?.detailedContent ?? event.data.result?.content ?? "",
|
|
432
|
+
toolTelemetry: event.data.toolTelemetry,
|
|
433
|
+
}
|
|
434
|
+
: {
|
|
435
|
+
success: false,
|
|
436
|
+
error: event.data.error?.message ?? "Tool execution failed.",
|
|
437
|
+
};
|
|
438
|
+
callbacks.onToolResult?.(toolName, toolResult, event.data.toolCallId);
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
unsubscribers.push(
|
|
443
|
+
session.on("assistant.usage", (event) => {
|
|
444
|
+
lastEventAt = Date.now();
|
|
445
|
+
rememberEvent({
|
|
446
|
+
type: "assistant.usage",
|
|
447
|
+
inputTokens: event.data.inputTokens ?? 0,
|
|
448
|
+
outputTokens: event.data.outputTokens ?? 0,
|
|
449
|
+
});
|
|
450
|
+
if (!callbacks.onStepUsage) return;
|
|
451
|
+
callbacks.onStepUsage({
|
|
452
|
+
promptTokens: event.data.inputTokens ?? 0,
|
|
453
|
+
completionTokens: event.data.outputTokens ?? 0,
|
|
454
|
+
totalTokens: (event.data.inputTokens ?? 0) + (event.data.outputTokens ?? 0),
|
|
455
|
+
cachedInputTokens: event.data.cacheReadTokens ?? 0,
|
|
456
|
+
cost: event.data.cost,
|
|
457
|
+
});
|
|
458
|
+
})
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
let aborted = Boolean(abortSignal?.aborted);
|
|
462
|
+
const abortHandler = () => {
|
|
463
|
+
aborted = true;
|
|
464
|
+
void session.abort().catch(() => {});
|
|
465
|
+
};
|
|
466
|
+
abortSignal?.addEventListener("abort", abortHandler, { once: true });
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
if (aborted) return null;
|
|
470
|
+
|
|
471
|
+
const sendTimeoutMs = parseTimeoutMs(
|
|
472
|
+
process.env.COPILOT_SEND_TIMEOUT_MS,
|
|
473
|
+
DEFAULT_COPILOT_SEND_TIMEOUT_MS
|
|
474
|
+
);
|
|
475
|
+
const idleTimeoutMs = parseTimeoutMs(
|
|
476
|
+
process.env.COPILOT_IDLE_TIMEOUT_MS,
|
|
477
|
+
DEFAULT_COPILOT_IDLE_TIMEOUT_MS
|
|
478
|
+
);
|
|
479
|
+
const sendTimeoutMessage = "Copilot request timed out while submitting prompt.";
|
|
480
|
+
const idleTimeoutMessage = "Copilot request timed out while waiting for response completion.";
|
|
481
|
+
|
|
482
|
+
sendStartedAt = Date.now();
|
|
483
|
+
await withTimeout(
|
|
484
|
+
session.send({
|
|
485
|
+
prompt: buildCopilotPrompt(userMessage, memoryInjection, conversationHistory, created),
|
|
486
|
+
}),
|
|
487
|
+
sendTimeoutMs,
|
|
488
|
+
sendTimeoutMessage
|
|
489
|
+
);
|
|
490
|
+
sendCompletedAt = Date.now();
|
|
491
|
+
lastEventAt = sendCompletedAt;
|
|
492
|
+
rememberEvent({ type: "session.send.complete" });
|
|
493
|
+
|
|
494
|
+
idleWaitStartedAt = Date.now();
|
|
495
|
+
await withTimeout(idlePromise, idleTimeoutMs, idleTimeoutMessage);
|
|
496
|
+
|
|
497
|
+
if (aborted) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (streamError) {
|
|
502
|
+
throw streamError;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
debug.info("copilot-stream-complete", {
|
|
506
|
+
requestId,
|
|
507
|
+
model: baseSessionConfig.model,
|
|
508
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
509
|
+
assistantDeltaEvents,
|
|
510
|
+
assistantMessageEvents,
|
|
511
|
+
toolExecutionStartEvents,
|
|
512
|
+
toolExecutionCompleteEvents,
|
|
513
|
+
toolExecutionFailureEvents,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
fullText: fullText.trim(),
|
|
518
|
+
finalText: finalText.trim(),
|
|
519
|
+
};
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (!aborted) {
|
|
522
|
+
void session.abort().catch(() => {});
|
|
523
|
+
}
|
|
524
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
525
|
+
const timeoutPhase = err.message.includes("while submitting prompt")
|
|
526
|
+
? "send"
|
|
527
|
+
: err.message.includes("while waiting for response completion")
|
|
528
|
+
? "idle"
|
|
529
|
+
: undefined;
|
|
530
|
+
const now = Date.now();
|
|
531
|
+
debug.error("copilot-stream-failed", {
|
|
532
|
+
requestId,
|
|
533
|
+
message: err.message,
|
|
534
|
+
model: baseSessionConfig.model,
|
|
535
|
+
timeoutPhase,
|
|
536
|
+
elapsedMs: now - requestStartedAt,
|
|
537
|
+
sendDurationMs:
|
|
538
|
+
sendStartedAt !== null && sendCompletedAt !== null ? sendCompletedAt - sendStartedAt : undefined,
|
|
539
|
+
idleWaitElapsedMs: idleWaitStartedAt !== null ? now - idleWaitStartedAt : undefined,
|
|
540
|
+
sinceLastEventMs: lastEventAt !== null ? now - lastEventAt : undefined,
|
|
541
|
+
sinceLastToolCompletionMs: lastToolCompletionAt !== null ? now - lastToolCompletionAt : undefined,
|
|
542
|
+
idleResolved: idleResolvedAt !== null,
|
|
543
|
+
eventCounts: {
|
|
544
|
+
assistantReasoningEvents,
|
|
545
|
+
assistantDeltaEvents,
|
|
546
|
+
assistantMessageEvents,
|
|
547
|
+
toolExecutionStartEvents,
|
|
548
|
+
toolExecutionCompleteEvents,
|
|
549
|
+
toolExecutionFailureEvents,
|
|
550
|
+
duplicateEventCount,
|
|
551
|
+
},
|
|
552
|
+
recentEvents,
|
|
553
|
+
});
|
|
554
|
+
if (err.message.includes("Failed to list models") || err.message.includes("models.list")) {
|
|
555
|
+
throw new Error(buildCopilotModelsListErrorMessageWithCause(err.message));
|
|
556
|
+
}
|
|
557
|
+
if (err.message.includes("Timed out while")) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
"Copilot provider timed out. Verify Copilot CLI access (run `copilot login`), then retry."
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
throw err;
|
|
563
|
+
} finally {
|
|
564
|
+
abortSignal?.removeEventListener("abort", abortHandler);
|
|
565
|
+
for (const unsubscribe of unsubscribers) {
|
|
566
|
+
try {
|
|
567
|
+
unsubscribe();
|
|
568
|
+
} catch {
|
|
569
|
+
// Ignore unsubscription errors.
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function streamCopilotResponse(request: ProviderStreamRequest): Promise<ProviderStreamResult | null> {
|
|
576
|
+
const result = await streamCopilotSession({
|
|
577
|
+
userMessage: request.userMessage,
|
|
578
|
+
callbacks: request.callbacks,
|
|
579
|
+
conversationHistory: request.conversationHistory,
|
|
580
|
+
interactionMode: request.interactionMode,
|
|
581
|
+
abortSignal: request.abortSignal,
|
|
582
|
+
reasoningEffort: request.reasoningEffort,
|
|
583
|
+
memoryInjection: request.memoryInjection,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
if (!result) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const finalText = result.finalText || result.fullText;
|
|
591
|
+
if (!finalText) {
|
|
592
|
+
request.callbacks.onError?.(new Error("Model returned empty response. Check Copilot authentication."));
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const responseMessages: ModelMessage[] = [{ role: "assistant", content: finalText }];
|
|
597
|
+
return {
|
|
598
|
+
fullText: result.fullText || finalText,
|
|
599
|
+
responseMessages,
|
|
600
|
+
finalText,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function generateCopilotSessionTitle(firstMessage: string): Promise<string> {
|
|
605
|
+
const titleSessionId = randomUUID();
|
|
606
|
+
const copilotTitleModel = "gpt-4.1";
|
|
607
|
+
const { session } = await getOrCreateCopilotSession(titleSessionId, {
|
|
608
|
+
model: copilotTitleModel,
|
|
609
|
+
streaming: false,
|
|
610
|
+
});
|
|
611
|
+
try {
|
|
612
|
+
const response = await session.sendAndWait(
|
|
613
|
+
{
|
|
614
|
+
prompt: `Generate a very short, descriptive title (3-6 words) for this first message:\n\n${firstMessage}`,
|
|
615
|
+
},
|
|
616
|
+
20000
|
|
617
|
+
);
|
|
618
|
+
const title = response?.data.content?.trim();
|
|
619
|
+
if (title) return title;
|
|
620
|
+
} finally {
|
|
621
|
+
void session.destroy().catch(() => {});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return "New Session";
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export const copilotProviderAdapter: LlmProviderAdapter = {
|
|
628
|
+
id: "copilot",
|
|
629
|
+
capabilities: getProviderCapabilities("copilot"),
|
|
630
|
+
streamResponse: streamCopilotResponse,
|
|
631
|
+
generateSessionTitle: generateCopilotSessionTitle,
|
|
632
|
+
};
|