@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.
Files changed (40) hide show
  1. package/README.md +60 -14
  2. package/package.json +4 -2
  3. package/src/ai/copilot-client.ts +775 -0
  4. package/src/ai/daemon-ai.ts +32 -234
  5. package/src/ai/model-config.ts +55 -14
  6. package/src/ai/providers/capabilities.ts +16 -0
  7. package/src/ai/providers/copilot-provider.ts +632 -0
  8. package/src/ai/providers/openrouter-provider.ts +217 -0
  9. package/src/ai/providers/registry.ts +14 -0
  10. package/src/ai/providers/types.ts +31 -0
  11. package/src/ai/system-prompt.ts +16 -0
  12. package/src/ai/tools/subagents.ts +1 -1
  13. package/src/ai/tools/tool-registry.ts +22 -1
  14. package/src/ai/tools/write-file.ts +51 -0
  15. package/src/app/components/AppOverlays.tsx +9 -1
  16. package/src/app/components/ConversationPane.tsx +8 -2
  17. package/src/components/ModelMenu.tsx +202 -140
  18. package/src/components/OnboardingOverlay.tsx +147 -1
  19. package/src/components/SettingsMenu.tsx +27 -1
  20. package/src/components/TokenUsageDisplay.tsx +5 -3
  21. package/src/components/tool-layouts/layouts/index.ts +1 -0
  22. package/src/components/tool-layouts/layouts/write-file.tsx +117 -0
  23. package/src/hooks/daemon-event-handlers.ts +61 -14
  24. package/src/hooks/keyboard-handlers.ts +109 -28
  25. package/src/hooks/use-app-callbacks.ts +141 -43
  26. package/src/hooks/use-app-context-builder.ts +5 -0
  27. package/src/hooks/use-app-controller.ts +31 -2
  28. package/src/hooks/use-app-copilot-models-loader.ts +45 -0
  29. package/src/hooks/use-app-display-state.ts +24 -2
  30. package/src/hooks/use-app-model.ts +103 -17
  31. package/src/hooks/use-app-preferences-bootstrap.ts +54 -10
  32. package/src/hooks/use-bootstrap-controller.ts +5 -0
  33. package/src/hooks/use-daemon-events.ts +8 -2
  34. package/src/hooks/use-daemon-keyboard.ts +19 -6
  35. package/src/hooks/use-daemon-runtime-controller.ts +4 -0
  36. package/src/hooks/use-menu-keyboard.ts +6 -1
  37. package/src/state/app-context.tsx +6 -0
  38. package/src/types/index.ts +24 -1
  39. package/src/utils/copilot-models.ts +77 -0
  40. package/src/utils/preferences.ts +3 -0
@@ -4,97 +4,33 @@
4
4
  */
5
5
 
6
6
  import { createOpenAI } from "@ai-sdk/openai";
7
- import { createOpenRouter } from "@openrouter/ai-sdk-provider";
8
- import {
9
- type ModelMessage,
10
- ToolLoopAgent,
11
- generateText,
12
- stepCountIs,
13
- experimental_transcribe as transcribe,
14
- } from "ai";
7
+ import { type ModelMessage, experimental_transcribe as transcribe } from "ai";
15
8
  import { getDaemonManager } from "../state/daemon-state";
16
- import { getRuntimeContext } from "../state/runtime-context";
17
9
  import type {
18
10
  MemoryToastOperation,
19
11
  MemoryToastPreview,
20
12
  ReasoningEffort,
21
13
  StreamCallbacks,
22
- TokenUsage,
23
- ToolApprovalRequest,
24
- ToolApprovalResponse,
25
14
  TranscriptionResult,
26
15
  } from "../types";
27
- import { debug, toolDebug } from "../utils/debug-logger";
28
- import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
29
- import { getWorkspacePath } from "../utils/workspace-manager";
16
+ import { debug } from "../utils/debug-logger";
30
17
  import { buildMemoryInjection, getMemoryManager, isMemoryAvailable } from "./memory";
31
- import { extractFinalAssistantText } from "./message-utils";
32
- import { TRANSCRIPTION_MODEL, buildOpenRouterChatSettings, getResponseModel } from "./model-config";
33
- import { sanitizeMessagesForInput } from "./sanitize-messages";
34
- import { type InteractionMode, buildDaemonSystemPrompt } from "./system-prompt";
35
- import { coordinateToolApprovals } from "./tool-approval-coordinator";
36
- import { getCachedToolAvailability, getDaemonTools } from "./tools/index";
18
+ import { TRANSCRIPTION_MODEL } from "./model-config";
19
+ import { getProviderAdapter } from "./providers/registry";
20
+ import { type InteractionMode } from "./system-prompt";
37
21
  import { setSubagentProgressEmitter } from "./tools/subagents";
38
- import { createToolAvailabilitySnapshot, resolveToolAvailability } from "./tools/tool-registry";
39
22
 
40
- // Re-export ModelMessage from AI SDK since it's commonly needed by consumers
41
23
  export type { ModelMessage } from "ai";
42
24
 
43
- // OpenRouter client for AI SDK (response generation)
44
- const openrouter = createOpenRouter();
45
-
46
- // OpenAI client for transcription (OpenRouter doesn't support transcription)
47
25
  const openai = createOpenAI({});
48
26
 
49
- // Maximum steps for the agent loop to prevent infinite loops
50
- const MAX_AGENT_STEPS = 100;
51
-
52
- function normalizeStreamError(error: unknown): Error {
53
- if (error instanceof Error) return error;
54
- if (error && typeof error === "object" && "message" in error) {
55
- const message = (error as { message?: unknown }).message;
56
- if (typeof message === "string") return new Error(message);
27
+ async function buildMemoryInjectionForPrompt(userMessage: string): Promise<string | undefined> {
28
+ if (!getDaemonManager().memoryEnabled || !isMemoryAvailable()) {
29
+ return undefined;
57
30
  }
58
- return new Error(String(error));
59
- }
60
-
61
- /**
62
- * The DAEMON agent instance.
63
- * Handles the agent loop internally, allowing for multi-step tool usage.
64
- * Created dynamically to use the current model selection and reasoning effort.
65
- * @param interactionMode - "text" for terminal output, "voice" for speech-optimized
66
- * @param reasoningEffort - Optional reasoning effort level for models that support it
67
- */
68
- async function createDaemonAgent(
69
- interactionMode: InteractionMode = "text",
70
- reasoningEffort?: ReasoningEffort,
71
- memoryInjection?: string
72
- ) {
73
- const modelConfig = buildOpenRouterChatSettings(
74
- reasoningEffort ? { reasoning: { effort: reasoningEffort } } : undefined
75
- );
76
-
77
- const { sessionId } = getRuntimeContext();
78
- const tools = await getDaemonTools();
79
- const toolAvailability =
80
- getCachedToolAvailability() ?? (await resolveToolAvailability(getDaemonManager().toolToggles));
81
31
 
82
- const workspacePath = sessionId ? getWorkspacePath(sessionId) : undefined;
83
-
84
- return new ToolLoopAgent({
85
- model: openrouter.chat(getResponseModel(), modelConfig),
86
- instructions: buildDaemonSystemPrompt({
87
- mode: interactionMode,
88
- toolAvailability: createToolAvailabilitySnapshot(toolAvailability),
89
- workspacePath,
90
- memoryInjection,
91
- }),
92
- tools,
93
- stopWhen: stepCountIs(MAX_AGENT_STEPS),
94
- prepareStep: async ({ messages }) => ({
95
- messages: sanitizeMessagesForInput(messages),
96
- }),
97
- });
32
+ const injection = await buildMemoryInjection(userMessage);
33
+ return injection || undefined;
98
34
  }
99
35
 
100
36
  /**
@@ -118,9 +54,8 @@ export async function transcribeAudio(
118
54
  text: result.text,
119
55
  };
120
56
  } catch (error) {
121
- // Check if this was an abort
122
57
  if (error instanceof Error && error.name === "AbortError") {
123
- throw error; // Re-throw abort errors as-is
58
+ throw error;
124
59
  }
125
60
  const err = error instanceof Error ? error : new Error(String(error));
126
61
  throw new Error(`Transcription failed: ${err.message}`);
@@ -128,15 +63,8 @@ export async function transcribeAudio(
128
63
  }
129
64
 
130
65
  /**
131
- * Generate a streaming response from DAEMON using the Agent class.
132
- * The agent handles the tool loop internally.
133
- *
134
- * @param userMessage - The transcribed user message
135
- * @param callbacks - Callbacks for streaming tokens, tool calls, and completion
136
- * @param conversationHistory - Previous AI SDK messages for context
137
- * @param interactionMode - "text" for terminal output, "voice" for speech-optimized
138
- * @param abortSignal - Optional abort signal to cancel the request
139
- * @param reasoningEffort - Optional reasoning effort level for models that support it
66
+ * Generate a streaming response from DAEMON.
67
+ * Delegates provider-specific execution to the active provider adapter.
140
68
  */
141
69
  export async function generateResponse(
142
70
  userMessage: string,
@@ -146,7 +74,6 @@ export async function generateResponse(
146
74
  abortSignal?: AbortSignal,
147
75
  reasoningEffort?: ReasoningEffort
148
76
  ): Promise<void> {
149
- // Set up subagent progress emitter to forward events to callbacks
150
77
  setSubagentProgressEmitter({
151
78
  onSubagentToolCall: (toolCallId: string, toolName: string, input?: unknown) => {
152
79
  callbacks.onSubagentToolCall?.(toolCallId, toolName, input);
@@ -163,146 +90,30 @@ export async function generateResponse(
163
90
  });
164
91
 
165
92
  try {
166
- // Build messages array with history and new user message
167
- const messages: ModelMessage[] = [...conversationHistory];
168
-
169
- // Include relevant memories in the system prompt if available
170
- let memoryInjection: string | undefined;
171
- if (getDaemonManager().memoryEnabled && isMemoryAvailable()) {
172
- const injection = await buildMemoryInjection(userMessage);
173
- if (injection) {
174
- memoryInjection = injection;
175
- }
176
- }
177
-
178
- // Add the user message
179
- messages.push({ role: "user" as const, content: userMessage });
180
-
181
- // Stream response from the agent with mode-specific system prompt
182
- const agent = await createDaemonAgent(interactionMode, reasoningEffort, memoryInjection);
183
-
184
- let currentMessages = messages;
185
- let fullText = "";
186
- let streamError: Error | null = null;
187
- let allResponseMessages: ModelMessage[] = [];
188
-
189
- while (true) {
190
- const stream = await agent.stream({
191
- messages: currentMessages,
192
- });
193
-
194
- const pendingApprovals: ToolApprovalRequest[] = [];
195
-
196
- for await (const part of stream.fullStream) {
197
- if (abortSignal?.aborted) {
198
- return;
199
- }
200
-
201
- if (part.type === "error") {
202
- const err = normalizeStreamError(part.error);
203
- streamError = err;
204
- debug.error("agent-stream-error", {
205
- message: err.message,
206
- error: part.error,
207
- });
208
- callbacks.onError?.(err);
209
- } else if (part.type === "abort") {
210
- return;
211
- } else if (part.type === "reasoning-delta") {
212
- callbacks.onReasoningToken?.(part.text);
213
- } else if (part.type === "text-delta") {
214
- fullText += part.text;
215
- callbacks.onToken?.(part.text);
216
- } else if (part.type === "tool-input-start") {
217
- callbacks.onToolCallStart?.(part.toolName, part.id);
218
- } else if (part.type === "tool-call") {
219
- callbacks.onToolCall?.(part.toolName, part.input, part.toolCallId);
220
- } else if (part.type === "tool-result") {
221
- callbacks.onToolResult?.(part.toolName, part.output, part.toolCallId);
222
- } else if (part.type === "tool-error") {
223
- const errorMessage = part.error instanceof Error ? part.error.message : String(part.error);
224
- toolDebug.error("tool-error", {
225
- toolName: part.toolName,
226
- toolCallId: part.toolCallId,
227
- input: part.input,
228
- error: errorMessage,
229
- });
230
- callbacks.onToolResult?.(
231
- part.toolName,
232
- { error: errorMessage, input: part.input },
233
- part.toolCallId
234
- );
235
- } else if (part.type === "tool-approval-request") {
236
- const approvalRequest: ToolApprovalRequest = {
237
- approvalId: part.approvalId,
238
- toolName: part.toolCall.toolName,
239
- toolCallId: part.toolCall.toolCallId,
240
- input: part.toolCall.input,
241
- };
242
- pendingApprovals.push(approvalRequest);
243
- callbacks.onToolApprovalRequest?.(approvalRequest);
244
- } else if (part.type === "finish-step") {
245
- if (part.usage && callbacks.onStepUsage) {
246
- const reportedCost = getOpenRouterReportedCost(part.providerMetadata);
247
-
248
- // reportedCost may be undefined when provider doesn't supply it
249
-
250
- callbacks.onStepUsage({
251
- promptTokens: part.usage.inputTokens ?? 0,
252
- completionTokens: part.usage.outputTokens ?? 0,
253
- totalTokens: part.usage.totalTokens ?? 0,
254
- reasoningTokens: part.usage.outputTokenDetails?.reasoningTokens ?? 0,
255
- cachedInputTokens: part.usage.inputTokenDetails?.cacheReadTokens ?? 0,
256
- cost: reportedCost,
257
- });
258
- }
259
- }
260
- }
261
-
262
- if (streamError) {
263
- return;
264
- }
265
-
266
- const rawResponseMessages = await stream.response.then((r) => r.messages);
267
- const responseMessages = sanitizeMessagesForInput(rawResponseMessages);
268
- allResponseMessages = [...allResponseMessages, ...responseMessages];
269
- currentMessages = [...currentMessages, ...responseMessages];
270
-
271
- if (pendingApprovals.length > 0 && callbacks.onAwaitingApprovals) {
272
- const { toolMessage } = await coordinateToolApprovals({
273
- pendingApprovals,
274
- requestApprovals: callbacks.onAwaitingApprovals,
275
- });
276
-
277
- if (toolMessage) {
278
- currentMessages = [...currentMessages, toolMessage];
279
- }
280
-
281
- continue;
282
- }
283
-
284
- break;
285
- }
286
-
287
- if (streamError) {
288
- return;
289
- }
290
-
291
- const finalText = extractFinalAssistantText(allResponseMessages);
93
+ const memoryInjection = await buildMemoryInjectionForPrompt(userMessage);
94
+ const provider = getProviderAdapter();
95
+ const result = await provider.streamResponse({
96
+ userMessage,
97
+ callbacks,
98
+ conversationHistory,
99
+ interactionMode,
100
+ abortSignal,
101
+ reasoningEffort,
102
+ memoryInjection,
103
+ });
292
104
 
293
- if (!fullText && allResponseMessages.length === 0) {
294
- callbacks.onError?.(new Error("Model returned empty response. Check API key and model availability."));
105
+ if (!result) {
295
106
  return;
296
107
  }
297
108
 
298
- callbacks.onComplete?.(fullText, allResponseMessages, undefined, finalText);
109
+ callbacks.onComplete?.(result.fullText, result.responseMessages, result.usage, result.finalText);
299
110
 
300
- void persistConversationMemory(userMessage, finalText ?? fullText).then((preview) => {
111
+ const assistantTextForMemory = result.finalText ?? result.fullText;
112
+ void persistConversationMemory(userMessage, assistantTextForMemory).then((preview) => {
301
113
  if (!preview) return;
302
114
  callbacks.onMemorySaved?.(preview);
303
115
  });
304
116
  } catch (error) {
305
- // Check if this was an abort - don't treat as error
306
117
  if (abortSignal?.aborted) {
307
118
  return;
308
119
  }
@@ -310,10 +121,8 @@ export async function generateResponse(
310
121
  return;
311
122
  }
312
123
  const err = error instanceof Error ? error : new Error(String(error));
313
- let errorMessage = err.message;
314
- callbacks.onError?.(new Error(errorMessage));
124
+ callbacks.onError?.(new Error(err.message));
315
125
  } finally {
316
- // Clean up the subagent progress emitter
317
126
  setSubagentProgressEmitter(null);
318
127
  }
319
128
  }
@@ -386,23 +195,12 @@ function truncatePreview(text: string, maxChars: number): string {
386
195
 
387
196
  /**
388
197
  * Generate a short descriptive title for a session based on the first user message.
389
- * Uses the currently selected model.
390
- * @param firstMessage - The first user message in the session
391
- * @returns A short title (3-6 words) describing the session topic
392
198
  */
393
199
  export async function generateSessionTitle(firstMessage: string): Promise<string> {
394
200
  try {
395
- const result = await generateText({
396
- model: openrouter.chat(getResponseModel(), buildOpenRouterChatSettings()),
397
- system: `You are a title generator. Generate a very short, descriptive title (3-6 words) for a conversation based on the user's first message. The title should capture the main topic or intent. Do not use quotes, punctuation, or prefixes like "Title:". Just output the title text directly.`,
398
- messages: [
399
- {
400
- role: "user",
401
- content: `Generate a short descriptive title for the following message <message>${firstMessage}</message>`,
402
- },
403
- ],
404
- });
405
- return result.text.trim() || "New Session";
201
+ const provider = getProviderAdapter();
202
+ const title = await provider.generateSessionTitle(firstMessage);
203
+ return title.trim() || "New Session";
406
204
  } catch (error) {
407
205
  const err = error instanceof Error ? error : new Error(String(error));
408
206
  debug.error("session-title-generation-failed", { message: err.message });
@@ -3,35 +3,66 @@
3
3
  */
4
4
 
5
5
  import type { OpenRouterChatSettings } from "@openrouter/ai-sdk-provider";
6
- import type { ModelOption } from "../types";
6
+ import type { LlmProvider, ModelOption } from "../types";
7
7
  import { loadManualConfig } from "../utils/config";
8
8
 
9
9
  // Available models for selection (OpenRouter format)
10
- export const AVAILABLE_MODELS: ModelOption[] = [
10
+ export const AVAILABLE_OPENROUTER_MODELS: ModelOption[] = [
11
11
  { id: "x-ai/grok-4.1-fast", name: "Grok 4.1 Fast" },
12
+ { id: "arcee-ai/trinity-large-preview:free", name: "Trinity Large Preview" },
12
13
  { id: "z-ai/glm-4.7", name: "GLM 4.7" },
13
14
  { id: "minimax/minimax-m2.1", name: "Minimax M2.1" },
14
15
  { id: "google/gemini-3-flash-preview", name: "Gemini 3 Flash" },
15
16
  { id: "google/gemini-3-pro-preview", name: "Gemini 3 Pro" },
16
17
  { id: "openai/gpt-5.2", name: "GPT 5.2" },
17
- { id: "moonshotai/kimi-k2-thinking", name: "Kimi K2 Thinking" },
18
+ { id: "moonshotai/kimi-k2.5", name: "Kimi K2.5" },
18
19
  { id: "openai/gpt-oss-120b:exacto", name: "GPT-OSS-120" },
19
- { id: "mistralai/devstral-2512:free", name: "Mistral Devstral" },
20
20
  { id: "nvidia/nemotron-3-nano-30b-a3b:free", name: "Nemotron 3 Nano" },
21
21
  ];
22
22
 
23
- // Default model ID
24
- export const DEFAULT_MODEL_ID = "z-ai/glm-4.7";
23
+ // Default model IDs
24
+ export const DEFAULT_OPENROUTER_MODEL_ID = "z-ai/glm-4.7";
25
+ export const DEFAULT_COPILOT_MODEL_ID = "claude-sonnet-4.5";
26
+ export const DEFAULT_MODEL_ID = DEFAULT_OPENROUTER_MODEL_ID;
27
+ export const DEFAULT_MODEL_PROVIDER: LlmProvider = "openrouter";
25
28
 
26
- // Current selected model (mutable)
27
- let currentModelId = DEFAULT_MODEL_ID;
29
+ // Backward-compatible alias used by existing OpenRouter pricing loaders.
30
+ export const AVAILABLE_MODELS = AVAILABLE_OPENROUTER_MODELS;
31
+
32
+ // Current selected provider + model IDs (mutable)
33
+ let currentModelProvider: LlmProvider = DEFAULT_MODEL_PROVIDER;
34
+ const currentModelIdByProvider: Record<LlmProvider, string> = {
35
+ openrouter: DEFAULT_OPENROUTER_MODEL_ID,
36
+ copilot: DEFAULT_COPILOT_MODEL_ID,
37
+ };
28
38
  let currentOpenRouterProviderTag: string | undefined;
29
39
 
30
40
  /**
31
41
  * Get the current response model ID.
32
42
  */
33
43
  export function getResponseModel(): string {
34
- return currentModelId;
44
+ return currentModelIdByProvider[currentModelProvider];
45
+ }
46
+
47
+ /**
48
+ * Get selected model ID for a specific provider.
49
+ */
50
+ export function getResponseModelForProvider(provider: LlmProvider): string {
51
+ return currentModelIdByProvider[provider];
52
+ }
53
+
54
+ /**
55
+ * Get the currently selected LLM provider.
56
+ */
57
+ export function getModelProvider(): LlmProvider {
58
+ return currentModelProvider;
59
+ }
60
+
61
+ /**
62
+ * Set the currently selected LLM provider.
63
+ */
64
+ export function setModelProvider(provider: LlmProvider): void {
65
+ currentModelProvider = provider;
35
66
  }
36
67
 
37
68
  /**
@@ -57,10 +88,20 @@ export function setOpenRouterProviderTag(providerTag: string | undefined): void
57
88
  */
58
89
  export function setResponseModel(modelId: string): void {
59
90
  if (!modelId) return;
60
- if (modelId !== currentModelId) {
61
- currentModelId = modelId;
62
- // Always reset provider when switching to a DIFFERENT model
63
- currentOpenRouterProviderTag = undefined;
91
+ setResponseModelForProvider(currentModelProvider, modelId);
92
+ }
93
+
94
+ /**
95
+ * Set model ID for a specific provider.
96
+ */
97
+ export function setResponseModelForProvider(provider: LlmProvider, modelId: string): void {
98
+ if (!modelId) return;
99
+ if (modelId !== currentModelIdByProvider[provider]) {
100
+ currentModelIdByProvider[provider] = modelId;
101
+ // Reset OpenRouter routing provider when switching OpenRouter models.
102
+ if (provider === "openrouter") {
103
+ currentOpenRouterProviderTag = undefined;
104
+ }
64
105
  }
65
106
  }
66
107
 
@@ -68,7 +109,7 @@ export function setResponseModel(modelId: string): void {
68
109
  * Get the current subagent model ID (same as main agent).
69
110
  */
70
111
  export function getSubagentModel(): string {
71
- return currentModelId;
112
+ return getResponseModel();
72
113
  }
73
114
 
74
115
  /**
@@ -0,0 +1,16 @@
1
+ import type { LlmProvider } from "../../types";
2
+ import { getModelProvider } from "../model-config";
3
+ import type { ProviderCapabilities } from "./types";
4
+
5
+ const PROVIDER_CAPABILITIES: Record<LlmProvider, ProviderCapabilities> = {
6
+ openrouter: {
7
+ supportsSubagentTool: true,
8
+ },
9
+ copilot: {
10
+ supportsSubagentTool: false,
11
+ },
12
+ };
13
+
14
+ export function getProviderCapabilities(provider: LlmProvider = getModelProvider()): ProviderCapabilities {
15
+ return PROVIDER_CAPABILITIES[provider];
16
+ }