@makefinks/daemon 0.1.4 → 0.3.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/package.json +5 -4
- package/src/ai/daemon-ai.ts +30 -85
- package/src/ai/system-prompt.ts +134 -111
- package/src/ai/tool-approval-coordinator.ts +113 -0
- package/src/ai/tools/index.ts +12 -32
- package/src/ai/tools/subagents.ts +16 -30
- package/src/ai/tools/tool-registry.ts +203 -0
- package/src/app/App.tsx +23 -631
- package/src/app/components/AppOverlays.tsx +25 -1
- package/src/app/components/ConversationPane.tsx +5 -3
- package/src/components/HotkeysPane.tsx +3 -1
- package/src/components/TokenUsageDisplay.tsx +11 -11
- package/src/components/ToolsMenu.tsx +235 -0
- package/src/components/UrlMenu.tsx +182 -0
- package/src/hooks/daemon-event-handlers/interrupted-turn.ts +148 -0
- package/src/hooks/daemon-event-handlers.ts +11 -151
- package/src/hooks/use-app-context-builder.ts +4 -0
- package/src/hooks/use-app-controller.ts +546 -0
- package/src/hooks/use-app-menus.ts +12 -0
- package/src/hooks/use-app-preferences-bootstrap.ts +9 -0
- package/src/hooks/use-bootstrap-controller.ts +92 -0
- package/src/hooks/use-daemon-keyboard.ts +63 -57
- package/src/hooks/use-daemon-runtime-controller.ts +147 -0
- package/src/hooks/use-grounding-menu-controller.ts +51 -0
- package/src/hooks/use-overlay-controller.ts +84 -0
- package/src/hooks/use-session-controller.ts +79 -0
- package/src/hooks/use-url-menu-items.ts +19 -0
- package/src/state/app-context.tsx +4 -0
- package/src/state/daemon-state.ts +19 -8
- package/src/state/session-store.ts +4 -0
- package/src/types/index.ts +39 -0
- package/src/utils/derive-url-menu-items.ts +155 -0
- package/src/utils/formatters.ts +1 -7
- package/src/utils/preferences.ts +10 -0
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"module": "src/index.tsx",
|
|
30
30
|
"type": "module",
|
|
31
|
-
"version": "0.
|
|
31
|
+
"version": "0.3.0",
|
|
32
32
|
"bin": {
|
|
33
33
|
"daemon": "dist/cli.js"
|
|
34
34
|
},
|
|
@@ -54,12 +54,13 @@
|
|
|
54
54
|
"preview:avatar": "bun run src/avatar-preview.ts",
|
|
55
55
|
"preview:avatar:mp4": "bun run src/avatar-preview.ts --mp4 tmp/avatar-preview.mp4",
|
|
56
56
|
"setup:browsers": "bun run src/scripts/setup-browsers.ts",
|
|
57
|
+
"setup:hooks": "bash .githooks/setup.sh",
|
|
57
58
|
"test": "bun test",
|
|
58
59
|
"test:watch": "bun test --watch",
|
|
59
60
|
"prepublishOnly": "bun run build:cli",
|
|
60
|
-
"release:patch": "npm version patch && git push && git push --tags && npm publish",
|
|
61
|
-
"release:minor": "npm version minor && git push && git push --tags && npm publish",
|
|
62
|
-
"release:major": "npm version major && git push && git push --tags && npm publish"
|
|
61
|
+
"release:patch": "npm version patch && git push && git push --tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes && npm publish",
|
|
62
|
+
"release:minor": "npm version minor && git push && git push --tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes && npm publish",
|
|
63
|
+
"release:major": "npm version major && git push && git push --tags && gh release create v$(node -p \"require('./package.json').version\") --generate-notes && npm publish"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@biomejs/biome": "^1.9.4",
|
package/src/ai/daemon-ai.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
349
294
|
|
|
350
295
|
if (streamError) {
|
|
351
296
|
return;
|
package/src/ai/system-prompt.ts
CHANGED
|
@@ -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
|
-
|
|
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(),
|
|
43
|
+
const { mode = "text", currentDate = new Date(), toolAvailability, workspacePath } = options;
|
|
33
44
|
const currentDateString = formatLocalIsoDate(currentDate);
|
|
34
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
204
|
-
- Do not include newlines, bullets, numbering, or markdown/table artifacts
|
|
205
|
-
|
|
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.
|
|
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
|
|
215
|
-
- Before anything that modifies the system
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
258
|
+
Also use tools for overcoming limitations with your architecture:
|
|
259
|
+
- Use python for calculation
|
|
260
|
+
${webNote}
|
|
243
261
|
|
|
244
|
-
|
|
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
|
+
}
|