@makefinks/daemon 0.2.0 → 0.3.1

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/package.json CHANGED
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "module": "src/index.tsx",
30
30
  "type": "module",
31
- "version": "0.2.0",
31
+ "version": "0.3.1",
32
32
  "bin": {
33
33
  "daemon": "dist/cli.js"
34
34
  },
@@ -6,29 +6,32 @@
6
6
  import { createOpenAI } from "@ai-sdk/openai";
7
7
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
8
8
  import {
9
+ type ModelMessage,
9
10
  ToolLoopAgent,
10
11
  generateText,
11
12
  stepCountIs,
12
13
  experimental_transcribe as transcribe,
13
- type ModelMessage,
14
14
  } from "ai";
15
- import { getDaemonTools, isWebSearchAvailable } from "./tools/index";
16
- import { setSubagentProgressEmitter } from "./tools/subagents";
17
- import { buildDaemonSystemPrompt, type InteractionMode } from "./system-prompt";
18
- import { buildOpenRouterChatSettings, getResponseModel, TRANSCRIPTION_MODEL } from "./model-config";
19
- import { debug } from "../utils/debug-logger";
20
- import { getWorkspacePath } from "../utils/workspace-manager";
15
+ import { getDaemonManager } from "../state/daemon-state";
21
16
  import { getRuntimeContext } from "../state/runtime-context";
22
- import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
23
17
  import type {
24
- TokenUsage,
25
- TranscriptionResult,
26
- StreamCallbacks,
27
18
  ReasoningEffort,
19
+ StreamCallbacks,
20
+ TokenUsage,
28
21
  ToolApprovalRequest,
29
22
  ToolApprovalResponse,
23
+ TranscriptionResult,
30
24
  } from "../types";
25
+ import { debug } from "../utils/debug-logger";
26
+ import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
27
+ import { getWorkspacePath } from "../utils/workspace-manager";
28
+ import { TRANSCRIPTION_MODEL, buildOpenRouterChatSettings, getResponseModel } from "./model-config";
31
29
  import { sanitizeMessagesForInput } from "./sanitize-messages";
30
+ import { type InteractionMode, buildDaemonSystemPrompt } from "./system-prompt";
31
+ import { coordinateToolApprovals } from "./tool-approval-coordinator";
32
+ import { getCachedToolAvailability, getDaemonTools } from "./tools/index";
33
+ import { setSubagentProgressEmitter } from "./tools/subagents";
34
+ import { createToolAvailabilitySnapshot, resolveToolAvailability } from "./tools/tool-registry";
32
35
 
33
36
  // Re-export ModelMessage from AI SDK since it's commonly needed by consumers
34
37
  export type { ModelMessage } from "ai";
@@ -102,6 +105,8 @@ async function createDaemonAgent(
102
105
 
103
106
  const { sessionId } = getRuntimeContext();
104
107
  const tools = await getDaemonTools();
108
+ const toolAvailability =
109
+ getCachedToolAvailability() ?? (await resolveToolAvailability(getDaemonManager().toolToggles));
105
110
 
106
111
  const workspacePath = sessionId ? getWorkspacePath(sessionId) : undefined;
107
112
 
@@ -109,7 +114,7 @@ async function createDaemonAgent(
109
114
  model: openrouter.chat(getResponseModel(), modelConfig),
110
115
  instructions: buildDaemonSystemPrompt({
111
116
  mode: interactionMode,
112
- webSearchAvailable: isWebSearchAvailable(),
117
+ toolAvailability: createToolAvailabilitySnapshot(toolAvailability),
113
118
  workspacePath,
114
119
  }),
115
120
  tools,
@@ -198,11 +203,9 @@ export async function generateResponse(
198
203
  let currentMessages = messages;
199
204
  let fullText = "";
200
205
  let streamError: Error | null = null;
201
- let costTotal = 0;
202
- let hasCost = false;
203
206
  let allResponseMessages: ModelMessage[] = [];
204
207
 
205
- const processStream = async (): Promise<void> => {
208
+ while (true) {
206
209
  const stream = await agent.stream({
207
210
  messages: currentMessages,
208
211
  });
@@ -250,10 +253,7 @@ export async function generateResponse(
250
253
  if (part.usage && callbacks.onStepUsage) {
251
254
  const reportedCost = getOpenRouterReportedCost(part.providerMetadata);
252
255
 
253
- if (reportedCost !== undefined) {
254
- costTotal += reportedCost;
255
- hasCost = true;
256
- }
256
+ // reportedCost may be undefined when provider doesn't supply it
257
257
 
258
258
  callbacks.onStepUsage({
259
259
  promptTokens: part.usage.inputTokens ?? 0,
@@ -277,75 +277,20 @@ export async function generateResponse(
277
277
  currentMessages = [...currentMessages, ...responseMessages];
278
278
 
279
279
  if (pendingApprovals.length > 0 && callbacks.onAwaitingApprovals) {
280
- return new Promise<void>((resolve) => {
281
- callbacks.onAwaitingApprovals!(pendingApprovals, async (responses) => {
282
- debug.info("tool-approval-responses", { responses, pendingApprovals });
283
- const approvalMap = new Map(pendingApprovals.map((p) => [p.approvalId, p]));
284
-
285
- const approvedResponses: Array<{
286
- type: "tool-approval-response";
287
- approvalId: string;
288
- approved: true;
289
- }> = [];
290
- const deniedResults: Array<{
291
- type: "tool-result";
292
- toolCallId: string;
293
- toolName: string;
294
- output: { type: "text"; value: string };
295
- }> = [];
296
-
297
- for (const r of responses) {
298
- const originalRequest = approvalMap.get(r.approvalId);
299
- if (!originalRequest) continue;
300
-
301
- if (r.approved) {
302
- approvedResponses.push({
303
- type: "tool-approval-response" as const,
304
- approvalId: r.approvalId,
305
- approved: true,
306
- });
307
- } else {
308
- // OpenRouter provider doesn't handle execution-denied type properly,
309
- // so we send a text output that the model can understand
310
- const denialMessage =
311
- r.reason ?? "Tool execution was denied by the user. Do not retry this command.";
312
- deniedResults.push({
313
- type: "tool-result" as const,
314
- toolCallId: originalRequest.toolCallId,
315
- toolName: originalRequest.toolName,
316
- output: {
317
- type: "text" as const,
318
- value: `[DENIED] ${denialMessage}`,
319
- },
320
- });
321
- }
322
- }
323
-
324
- // Combine approved and denied into a single tool message so the SDK
325
- // can execute approved tools and the model sees all results together
326
- const combinedContent: Array<
327
- | { type: "tool-approval-response"; approvalId: string; approved: true }
328
- | {
329
- type: "tool-result";
330
- toolCallId: string;
331
- toolName: string;
332
- output: { type: "text"; value: string };
333
- }
334
- > = [...approvedResponses, ...deniedResults];
335
-
336
- if (combinedContent.length > 0) {
337
- debug.info("tool-approval-combined", { combinedContent });
338
- currentMessages = [...currentMessages, { role: "tool" as const, content: combinedContent }];
339
- }
340
-
341
- await processStream();
342
- resolve();
343
- });
280
+ const { toolMessage } = await coordinateToolApprovals({
281
+ pendingApprovals,
282
+ requestApprovals: callbacks.onAwaitingApprovals,
344
283
  });
284
+
285
+ if (toolMessage) {
286
+ currentMessages = [...currentMessages, toolMessage];
287
+ }
288
+
289
+ continue;
345
290
  }
346
- };
347
291
 
348
- await processStream();
292
+ break;
293
+ }
349
294
 
350
295
  if (streamError) {
351
296
  return;
@@ -7,10 +7,21 @@
7
7
  */
8
8
  export type InteractionMode = "text" | "voice";
9
9
 
10
+ export interface ToolAvailability {
11
+ readFile: boolean;
12
+ runBash: boolean;
13
+ webSearch: boolean;
14
+ fetchUrls: boolean;
15
+ renderUrl: boolean;
16
+ todoManager: boolean;
17
+ groundingManager: boolean;
18
+ subagent: boolean;
19
+ }
20
+
10
21
  export interface SystemPromptOptions {
11
22
  mode?: InteractionMode;
12
23
  currentDate?: Date;
13
- webSearchAvailable?: boolean;
24
+ toolAvailability?: Partial<ToolAvailability>;
14
25
  workspacePath?: string;
15
26
  }
16
27
 
@@ -29,9 +40,10 @@ function formatLocalIsoDate(date: Date): string {
29
40
  * @param mode - "text" for terminal output with markdown, "voice" for speech-optimized responses
30
41
  */
31
42
  export function buildDaemonSystemPrompt(options: SystemPromptOptions = {}): string {
32
- const { mode = "text", currentDate = new Date(), webSearchAvailable = true, workspacePath } = options;
43
+ const { mode = "text", currentDate = new Date(), toolAvailability, workspacePath } = options;
33
44
  const currentDateString = formatLocalIsoDate(currentDate);
34
- const toolDefinitions = buildToolDefinitions(webSearchAvailable);
45
+ const availability = normalizeToolAvailability(toolAvailability);
46
+ const toolDefinitions = buildToolDefinitions(availability);
35
47
  const workspaceSection = workspacePath ? buildWorkspaceSection(workspacePath) : "";
36
48
 
37
49
  if (mode === "voice") {
@@ -41,25 +53,79 @@ export function buildDaemonSystemPrompt(options: SystemPromptOptions = {}): stri
41
53
  return buildTextSystemPrompt(currentDateString, toolDefinitions, workspaceSection);
42
54
  }
43
55
 
44
- const WEB_SEARCH_AVAILABLE_SECTION = `
56
+ function normalizeToolAvailability(toolAvailability?: Partial<ToolAvailability>): ToolAvailability {
57
+ return {
58
+ readFile: toolAvailability?.readFile ?? true,
59
+ runBash: toolAvailability?.runBash ?? true,
60
+ webSearch: toolAvailability?.webSearch ?? true,
61
+ fetchUrls: toolAvailability?.fetchUrls ?? true,
62
+ renderUrl: toolAvailability?.renderUrl ?? true,
63
+ todoManager: toolAvailability?.todoManager ?? true,
64
+ groundingManager: toolAvailability?.groundingManager ?? true,
65
+ subagent: toolAvailability?.subagent ?? true,
66
+ };
67
+ }
68
+
69
+ const TOOL_SECTIONS = {
70
+ todoManager: `
71
+ ### 'todoManager' (task planning & tracking)
72
+ Use this tool to **plan and track tasks VERY frequently**.
73
+ Default: use it for **almost every request**.
74
+ skip it for **trivial, single-step replies** that can be answered immediately without calling any tools.
75
+
76
+ **ToDo Principles**
77
+ - Update todos immediately as you begin/finish each step.
78
+ - Do **not** emit todoManager updates *after* you have started writing the final answer.
79
+
80
+ **Todo Workflow:**
81
+ 1. At the start of a task use \`write\` with an array of descriptive todos
82
+ 2. Use \`update\` with index and status to mark items as 'in_progress' or 'completed'
83
+ 3. Only have ONE item 'in_progress' at a time
84
+
85
+ Note: You can also skip writing a list of todos initally until you have gathered enough context, or batch update the todo list if the plan needs to change drastically during exeuction.
86
+ It is **very important** that you update the todos to reflect the actual state of progress.
87
+
88
+ **Todo content rules**
89
+ - Todos must be strictly limited to **concrete, observable actions** (e.g., "Search for X", "Read file Y", "Run command Z").
90
+ - If a task involves writing the final response to the user, summarizing findings, or explaining a concept, it is **NOT** a Todo.
91
+ - **Banned Verbs**: You are strictly forbidden from using communication or synthesis verbs in Todos. **NEVER** write todos containing:
92
+ - "Summarize" / "Synthesize"
93
+ - "Explain" / "Describe"
94
+ - "Inform" / "Tell" / "Clarify"
95
+ - "Answer" / "Respond"
96
+ `,
97
+ webSearch: `
45
98
  ### 'webSearch'
46
99
  Searches the web for up-to-date facts, references, or when the user asks 'latest / current / source'.
47
100
  Returns potentially relevant URLs which you can then fetch with fetchUrls.
48
- `;
49
-
50
- const WEB_SEARCH_DISABLED_SECTION = `
51
- ### 'webSearch' and 'fetchUrls' (DISABLED)
52
- Web search and URL fetching are currently disabled because the EXA_API_KEY environment variable is not configured.
53
- If the user asks you to search the web or fetch URL contents, inform them that these features are disabled and they need to either:
54
- 1. Set the EXA_API_KEY environment variable before starting the application, or
55
- 2. Re-run the application and enter the key when prompted during setup
56
- `;
57
-
58
- const FETCH_URLS_SECTION = `
101
+ Do NOT use web search for every request the user makes. Determine if web search is actually needed to answer the question.
102
+
103
+ **Use webSearch when:**
104
+ - The user asks for *current* info (prices, releases, CVEs, breaking news, policy changes, "as of 2026", etc.)
105
+ - You need an authoritative citation (docs, spec, changelog, research paper)
106
+ - The question is likely to have changed since your training cutoff
107
+ - You need to confirm a niche factual claim (exact flag, API behavior, compatibility)
108
+
109
+ **Do not use webSearch when:**
110
+ - The user is asking about something local (read files / run commands instead)
111
+ - The answer is a general programming concept (e.g. "what is a mutex", "how does HTTP caching work")
112
+ - The user wants brainstorming, design suggestions, copywriting, or refactors
113
+ - The user provides all necessary context in the prompt
114
+
115
+ **Examples (use webSearch):**
116
+ - "What's the latest Bun version and what changed in the last release?"
117
+ - "Find the official docs for boto3 count_tokens api."
118
+ - "Has CVE-XXXX been fixed in Node 20 yet?"
119
+
120
+ **Examples (don't use webSearch):**
121
+ - "Write a regex to match ISO-8601 dates."
122
+ - "Which processes take up most of my ram right now?"
123
+ `,
124
+ fetchUrls: `
59
125
  ### 'fetchUrls'
60
- The fetchUrl tools allows for getting the actual contents of web pages.
126
+ The fetchUrl tool allows for getting the actual contents of web pages.
61
127
  Use this tool to read the content of potentially relevant websites returned by the webSearch tool.
62
- If the user provides an URL always fetch the content of the url first before answering.
128
+ If the user provides a URL, always fetch the content of the URL first before answering.
63
129
 
64
130
  **Recommended flow**
65
131
 
@@ -113,77 +179,27 @@ fetchUrls({ url: "https://example.com/article", highlightQuery: "machine learnin
113
179
  </pagination-example>
114
180
 
115
181
  Use pagination this way unless instructed otherwise. This avoids fetching page content reduntantly.
116
- `;
117
-
118
- function buildToolDefinitions(webSearchAvailable: boolean): string {
119
- let webSearchSection: string;
120
- if (!webSearchAvailable) {
121
- webSearchSection = WEB_SEARCH_DISABLED_SECTION;
122
- } else {
123
- webSearchSection = WEB_SEARCH_AVAILABLE_SECTION + FETCH_URLS_SECTION;
124
- }
125
-
126
- return `
127
- # Tools
128
- Use tools to improve the quality and corectness of your responses.
129
-
130
- Also use tools for overcoming limitations with your architecture:
131
- - Use python for calculation
132
- ${webSearchAvailable ? "- use web searches for questions that require up to date information or factual grounding." : ""}
133
-
134
- You are allowed to use tools multiple times especially for tasks that require precise information or if previous tool calls did not lead to sufficient results.
135
- However prevent exessive tool use when not necessary. Be efficent with the tools at hand.
136
-
137
- Here is an overview of your tools:
138
- <tool_overview>
139
- ### 'todoManager' (task planning & tracking)
140
- Use this tool to **plan and track tasks VERY frequently**.
141
- Default: use it for **almost every request**.
142
- skip it for **trivial, single-step replies** that can be answered immediately without calling any tools.
143
-
144
- **ToDo Principles**
145
- - Update todos immediately as you begin/finish each step.
146
- - Do **not** emit todoManager updates *after* you have started writing the final answer.
147
-
148
- **Todo Workflow:**
149
- 1. At the start of a task use \`write\` with an array of descriptive todos
150
- 2. Use \`update\` with index and status to mark items as 'in_progress' or 'completed'
151
- 3. Only have ONE item 'in_progress' at a time
152
-
153
- Note: You can also skip writing a list of todos initally until you have gathered enough context, or batch update the todo list if the plan needs to change drastically during exeuction.
154
- It is **very important** that you update the todos to reflect the actual state of progress.
155
-
156
- **Todo content rules**
157
- - Todos must be strictly limited to **concrete, observable actions** (e.g., "Search for X", "Read file Y", "Run command Z").
158
- - If a task involves writing the final response to the user, summarizing findings, or explaining a concept, it is **NOT** a Todo.
159
- - **Banned Verbs**: You are strictly forbidden from using communication or synthesis verbs in Todos. **NEVER** write todos containing:
160
- - "Summarize" / "Synthesize"
161
- - "Explain" / "Describe"
162
- - "Inform" / "Tell" / "Clarify"
163
- - "Answer" / "Respond"
164
-
165
- ${webSearchSection}
166
-
167
- ### 'renderUrl'
182
+ `,
183
+ renderUrl: `
184
+ ### 'renderUrl'
168
185
  Use this tool to extract content from **JavaScript-rendered** pages (SPAs) when \`fetchUrls\` returns suspiciously short, shell-like, or nav-only text.
169
186
 
170
187
  Rules:
171
188
  - Prefer \`fetchUrls\` first (faster, cheaper).
172
189
  - If the page appears JS-heavy or fetchUrls returns "shell-only" text, use \`renderUrl\` to render locally and extract the text.
173
- - \`renderUrl\` might not be available on all installs. If it isn't available, fall back to \`fetchUrls\` and explain limits.
174
190
 
175
191
  Pagination mirrors \`fetchUrls\`:
176
192
  - Start with \`lineLimit\` (default 80) from the start.
177
193
  - For pagination, provide both \`lineOffset\` and \`lineLimit\`.
178
-
179
- ### 'groundingManager' (source attribution) — CRITICAL
194
+ `,
195
+ groundingManager: `
196
+ ### 'groundingManager' (source attribution)
180
197
  Manages a list of grounded statements (facts supported by sources).
181
198
  You can 'set' (overwrite) the entire list or 'append' new items to the existing list.
182
199
 
183
- **MANDATORY usage rule:**
200
+ **MANDATORY usage rule:**
184
201
  - If you used webSearch or fetchUrls to answer the user's question, you MUST call groundingManager BEFORE writing your final answer.
185
- - This is NOT optional. Every answer that relies on web data MUST be grounded.
186
-
202
+
187
203
  **When to use which action:**
188
204
  - 'set': Use when grounding a new topic or if previous facts are no longer relevant.
189
205
  - 'append': Use when adding more facts to the current topic without losing previous context.
@@ -192,56 +208,63 @@ Here is an overview of your tools:
192
208
  - If searches yielded no relevant info -> do not invent groundings or use irrelevant groundings.
193
209
  - If answering from your training knowledge alone (no web tools used) -> grounding not needed.
194
210
 
195
- All statements should be intrinsically relevant to instructions of the user.
196
-
197
- **Importance of text fragments**
198
- Text fragments only work when the textFragment is within a single content block (html tag).
199
- Choose textFragment defensively so that text highlighting works.
200
- Avoid text fragments that span tables or lists since these texts are within different tags and will break highlighting.
201
-
202
211
  **Text fragment rules**
203
- - \`source.textFragment\` must be a **contiguous verbatim substring** from the page content you were shown (do not stitch across paragraphs/columns/cells).
204
- - Do not include newlines, bullets, numbering, or markdown/table artifacts (e.g. \`|\`, leading \`-\`, \`*\`, \`1.\`).
205
- - Prefer a mid-sentence phrase from a normal paragraph or heading; avoid tables, lists, nav, and sidebars.
206
-
207
- If you want to reference recorded groundings to it with an identifiers (eg. (g1), (g2)) at the end of sentences.
208
-
212
+ - \`source.textFragment\` must be a **contiguous verbatim substring** from the page content you were shown.
213
+ - Do not include newlines, bullets, numbering, or markdown/table artifacts.
214
+ `,
215
+ runBash: `
209
216
  ### 'runBash' (local shell)
210
217
  This runs the specified command on the user's machine/environment.
211
- **Tool approval**: runBash requires user approval before execution. The user can approve or deny the command.
212
- - If the user **denies** the command, you will receive a denial message. Do NOT retry the same command - acknowledge the denial and offer alternatives or ask for guidance.
218
+ **Tool approval**: runBash requires user approval before execution.
213
219
  Rules:
214
- - Prefer **read-only** inspection commands first (ls, cat, rg, jq, node/bun --version).
215
- - Before anything that modifies the system (rm, mv, git push, installs, writes files, sudo), **ask for confirmation** and explain what it will change.
220
+ - Prefer **read-only** inspection commands first.
221
+ - Before anything that modifies the system, **ask for confirmation** and explain what it will change.
216
222
  - Never run destructive/wipe commands or anything that exfiltrates data.
217
- - Keep output concise; if output is large, propose a filter (head, tail, rg, jq).
218
-
223
+ `,
224
+ readFile: `
219
225
  ### 'readFile' (local file reader)
220
226
  Use this to read local text files.
221
227
  By default it reads up to 2000 lines from the start when no offset/limit are provided.
222
228
  For partial reads, you must provide both a 0-based line offset and a line limit.
223
- Only use partial reads when needed; prefer full reads by omitting offset/limit.
224
-
225
- ### 'getSystemInfo'
226
- Use only when system context is needed (OS/CPU/memory) and keep it minimal.
227
-
229
+ `,
230
+ subagent: `
228
231
  ### 'subagent'
229
- Call this tool to spawn subagents for specific tasks. Each subagent has access to the same tools as you.
230
-
232
+ Call this tool to spawn subagents for specific tasks.
231
233
  **Call multiple times in parallel** for concurrent execution.
234
+ `,
235
+ } as const;
236
+
237
+ function buildToolDefinitions(availability: ToolAvailability): string {
238
+ const blocks: string[] = [];
239
+
240
+ if (availability.todoManager) blocks.push(TOOL_SECTIONS.todoManager);
241
+ if (availability.webSearch) blocks.push(TOOL_SECTIONS.webSearch);
242
+ if (availability.fetchUrls) blocks.push(TOOL_SECTIONS.fetchUrls);
243
+ if (availability.renderUrl) blocks.push(TOOL_SECTIONS.renderUrl);
244
+ if (availability.groundingManager) blocks.push(TOOL_SECTIONS.groundingManager);
245
+ if (availability.runBash) blocks.push(TOOL_SECTIONS.runBash);
246
+ if (availability.readFile) blocks.push(TOOL_SECTIONS.readFile);
247
+ if (availability.subagent) blocks.push(TOOL_SECTIONS.subagent);
248
+
249
+ const webNote =
250
+ availability.webSearch || availability.fetchUrls
251
+ ? "- use web tools when up-to-date info or citations are required."
252
+ : "";
232
253
 
233
- **When to use:**
234
- Here are some specific scenarios where subagents should be used:
235
- - Researching multiple topics simultaneously
236
- - Performing several independent operations at once
237
- - Gathering information from multiple sources in parallel
238
- - Finding specific websites containing relevant content from a web search
254
+ return `
255
+ # Tools
256
+ Use tools to improve the quality and corectness of your responses.
239
257
 
240
- **How to write subagent inputs:**
241
- - \`task\`: make it concrete and scoped. For ambitious or complex work, be very specific about steps, constraints, and expected outputs.
242
- - \`summary\`: not just a title for the task; include a bit of detail; Only the summary is shown to the user.
258
+ Also use tools for overcoming limitations with your architecture:
259
+ - Use python for calculation
260
+ ${webNote}
243
261
 
244
- Each subagent works independently and returns a summary of the information it gathered based on the requirements you define in the tasks.
262
+ You are allowed to use tools multiple times especially for tasks that require precise information or if previous tool calls did not lead to sufficient results.
263
+ However prevent exessive tool use when not necessary. Be efficent with the tools at hand.
264
+
265
+ Here is an overview of your tools:
266
+ <tool_overview>
267
+ ${blocks.join("\n")}
245
268
  </tool_overview>
246
269
  `;
247
270
  }
@@ -0,0 +1,113 @@
1
+ import type { ModelMessage } from "ai";
2
+ import type { ToolApprovalRequest, ToolApprovalResponse } from "../types";
3
+ import { debug } from "../utils/debug-logger";
4
+
5
+ interface CoordinateToolApprovalsParams {
6
+ pendingApprovals: ToolApprovalRequest[];
7
+ requestApprovals: (
8
+ pendingApprovals: ToolApprovalRequest[],
9
+ respondToApprovals: (responses: ToolApprovalResponse[]) => void
10
+ ) => void;
11
+ }
12
+
13
+ interface CoordinateToolApprovalsResult {
14
+ /** A tool message to append before resuming streaming, or null if no-op. */
15
+ toolMessage: ModelMessage | null;
16
+ responses: ToolApprovalResponse[];
17
+ }
18
+
19
+ function buildDeniedToolResultPart(params: {
20
+ request: ToolApprovalRequest;
21
+ response: ToolApprovalResponse;
22
+ }): {
23
+ type: "tool-result";
24
+ toolCallId: string;
25
+ toolName: string;
26
+ output: { type: "text"; value: string };
27
+ } {
28
+ // OpenRouter provider doesn't handle execution-denied type properly,
29
+ // so we send a text output that the model can understand.
30
+ const denialMessage =
31
+ params.response.reason ?? "Tool execution was denied by the user. Do not retry this command.";
32
+
33
+ return {
34
+ type: "tool-result" as const,
35
+ toolCallId: params.request.toolCallId,
36
+ toolName: params.request.toolName,
37
+ output: {
38
+ type: "text" as const,
39
+ value: `[DENIED] ${denialMessage}`,
40
+ },
41
+ };
42
+ }
43
+
44
+ export async function coordinateToolApprovals(
45
+ params: CoordinateToolApprovalsParams
46
+ ): Promise<CoordinateToolApprovalsResult> {
47
+ if (params.pendingApprovals.length === 0) {
48
+ return { toolMessage: null, responses: [] };
49
+ }
50
+
51
+ const responses = await new Promise<ToolApprovalResponse[]>((resolve) => {
52
+ params.requestApprovals(params.pendingApprovals, (r) => resolve(r));
53
+ });
54
+
55
+ debug.info("tool-approval-responses", {
56
+ responses,
57
+ pendingApprovals: params.pendingApprovals,
58
+ });
59
+
60
+ const approvalMap = new Map(params.pendingApprovals.map((p) => [p.approvalId, p]));
61
+
62
+ const approvedParts: Array<{
63
+ type: "tool-approval-response";
64
+ approvalId: string;
65
+ approved: true;
66
+ }> = [];
67
+
68
+ const deniedParts: Array<{
69
+ type: "tool-result";
70
+ toolCallId: string;
71
+ toolName: string;
72
+ output: { type: "text"; value: string };
73
+ }> = [];
74
+
75
+ for (const r of responses) {
76
+ const originalRequest = approvalMap.get(r.approvalId);
77
+ if (!originalRequest) continue;
78
+
79
+ if (r.approved) {
80
+ approvedParts.push({
81
+ type: "tool-approval-response" as const,
82
+ approvalId: r.approvalId,
83
+ approved: true,
84
+ });
85
+ } else {
86
+ deniedParts.push(buildDeniedToolResultPart({ request: originalRequest, response: r }));
87
+ }
88
+ }
89
+
90
+ const combinedContent: Array<
91
+ | { type: "tool-approval-response"; approvalId: string; approved: true }
92
+ | {
93
+ type: "tool-result";
94
+ toolCallId: string;
95
+ toolName: string;
96
+ output: { type: "text"; value: string };
97
+ }
98
+ > = [...approvedParts, ...deniedParts];
99
+
100
+ if (combinedContent.length === 0) {
101
+ return { toolMessage: null, responses };
102
+ }
103
+
104
+ debug.info("tool-approval-combined", { combinedContent });
105
+
106
+ return {
107
+ responses,
108
+ toolMessage: {
109
+ role: "tool" as const,
110
+ content: combinedContent,
111
+ },
112
+ };
113
+ }
@@ -1,24 +1,19 @@
1
1
  import type { ToolSet } from "ai";
2
- import { fetchUrls } from "./fetch-urls";
3
2
 
4
- import { readFile } from "./read-file";
5
- import { groundingManager } from "./grounding-manager";
6
- import { renderUrl } from "./render-url";
7
- import { runBash } from "./run-bash";
8
- import { todoManager } from "./todo-manager";
9
- import { subagent } from "./subagents";
10
- import { webSearch } from "./web-search";
11
-
12
- import { detectLocalPlaywrightChromium } from "../../utils/js-rendering";
3
+ import { getDaemonManager } from "../../state/daemon-state";
4
+ import type { ToolAvailabilityMap } from "./tool-registry";
5
+ import { buildToolSet } from "./tool-registry";
13
6
 
14
7
  let cachedDaemonTools: Promise<ToolSet> | null = null;
15
-
16
- export function isWebSearchAvailable(): boolean {
17
- return Boolean(process.env.EXA_API_KEY);
18
- }
8
+ let cachedAvailability: ToolAvailabilityMap | null = null;
19
9
 
20
10
  export function invalidateDaemonToolsCache(): void {
21
11
  cachedDaemonTools = null;
12
+ cachedAvailability = null;
13
+ }
14
+
15
+ export function getCachedToolAvailability(): ToolAvailabilityMap | null {
16
+ return cachedAvailability;
22
17
  }
23
18
 
24
19
  export async function getDaemonTools(): Promise<ToolSet> {
@@ -27,24 +22,9 @@ export async function getDaemonTools(): Promise<ToolSet> {
27
22
  }
28
23
 
29
24
  cachedDaemonTools = (async () => {
30
- const tools: ToolSet = {
31
- readFile,
32
- groundingManager,
33
- runBash,
34
- todoManager,
35
- subagent,
36
- };
37
-
38
- if (isWebSearchAvailable()) {
39
- (tools as ToolSet & { webSearch: typeof webSearch }).webSearch = webSearch;
40
- (tools as ToolSet & { fetchUrls: typeof fetchUrls }).fetchUrls = fetchUrls;
41
- }
42
-
43
- const jsRendering = await detectLocalPlaywrightChromium();
44
- if (jsRendering.available) {
45
- return { ...tools, renderUrl };
46
- }
47
-
25
+ const toggles = getDaemonManager().toolToggles;
26
+ const { tools, availability } = await buildToolSet(toggles);
27
+ cachedAvailability = availability;
48
28
  return tools;
49
29
  })();
50
30