@dotsetlabs/dotclaw 1.8.0 → 2.0.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/.env.example +6 -0
- package/README.md +14 -7
- package/config-examples/groups/global/CLAUDE.md +6 -14
- package/config-examples/groups/main/CLAUDE.md +8 -39
- package/config-examples/runtime.json +4 -4
- package/container/Dockerfile +20 -2
- package/container/agent-runner/package-lock.json +260 -2
- package/container/agent-runner/package.json +2 -1
- package/container/agent-runner/src/agent-config.ts +57 -8
- package/container/agent-runner/src/browser.ts +180 -0
- package/container/agent-runner/src/container-protocol.ts +5 -0
- package/container/agent-runner/src/id.ts +3 -2
- package/container/agent-runner/src/index.ts +184 -390
- package/container/agent-runner/src/ipc.ts +33 -1
- package/container/agent-runner/src/mcp-client.ts +222 -0
- package/container/agent-runner/src/mcp-registry.ts +163 -0
- package/container/agent-runner/src/skill-loader.ts +375 -0
- package/container/agent-runner/src/tools.ts +260 -32
- package/container/agent-runner/src/tts.ts +61 -0
- package/container/agent-runner/tsconfig.json +1 -0
- package/dist/admin-commands.d.ts.map +1 -1
- package/dist/admin-commands.js +12 -0
- package/dist/admin-commands.js.map +1 -1
- package/dist/agent-execution.d.ts +3 -1
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +21 -0
- package/dist/agent-execution.js.map +1 -1
- package/dist/background-job-classifier.d.ts +1 -1
- package/dist/background-job-classifier.d.ts.map +1 -1
- package/dist/background-jobs.d.ts.map +1 -1
- package/dist/background-jobs.js +30 -10
- package/dist/background-jobs.js.map +1 -1
- package/dist/cli.js +61 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -3
- package/dist/config.js.map +1 -1
- package/dist/container-protocol.d.ts +5 -0
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +33 -14
- package/dist/container-runner.js.map +1 -1
- package/dist/dashboard.d.ts +5 -0
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +11 -5
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +0 -13
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +41 -70
- package/dist/db.js.map +1 -1
- package/dist/error-messages.d.ts.map +1 -1
- package/dist/error-messages.js +13 -5
- package/dist/error-messages.js.map +1 -1
- package/dist/hooks.d.ts +7 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +93 -0
- package/dist/hooks.js.map +1 -0
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +2 -1
- package/dist/id.js.map +1 -1
- package/dist/index.js +742 -2793
- package/dist/index.js.map +1 -1
- package/dist/ipc-dispatcher.d.ts +26 -0
- package/dist/ipc-dispatcher.d.ts.map +1 -0
- package/dist/ipc-dispatcher.js +1044 -0
- package/dist/ipc-dispatcher.js.map +1 -0
- package/dist/local-embeddings.d.ts +7 -0
- package/dist/local-embeddings.d.ts.map +1 -0
- package/dist/local-embeddings.js +60 -0
- package/dist/local-embeddings.js.map +1 -0
- package/dist/maintenance.d.ts.map +1 -1
- package/dist/maintenance.js +7 -1
- package/dist/maintenance.js.map +1 -1
- package/dist/memory-embeddings.d.ts +1 -1
- package/dist/memory-embeddings.d.ts.map +1 -1
- package/dist/memory-embeddings.js +59 -31
- package/dist/memory-embeddings.js.map +1 -1
- package/dist/memory-store.d.ts +0 -10
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +12 -28
- package/dist/memory-store.js.map +1 -1
- package/dist/message-pipeline.d.ts +47 -0
- package/dist/message-pipeline.d.ts.map +1 -0
- package/dist/message-pipeline.js +876 -0
- package/dist/message-pipeline.js.map +1 -0
- package/dist/metrics.d.ts +8 -8
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/model-registry.d.ts +0 -14
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +0 -36
- package/dist/model-registry.js.map +1 -1
- package/dist/orchestration.d.ts +39 -0
- package/dist/orchestration.d.ts.map +1 -0
- package/dist/orchestration.js +136 -0
- package/dist/orchestration.js.map +1 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +2 -0
- package/dist/paths.js.map +1 -1
- package/dist/providers/discord/discord-format.d.ts +16 -0
- package/dist/providers/discord/discord-format.d.ts.map +1 -0
- package/dist/providers/discord/discord-format.js +153 -0
- package/dist/providers/discord/discord-format.js.map +1 -0
- package/dist/providers/discord/discord-provider.d.ts +50 -0
- package/dist/providers/discord/discord-provider.d.ts.map +1 -0
- package/dist/providers/discord/discord-provider.js +604 -0
- package/dist/providers/discord/discord-provider.js.map +1 -0
- package/dist/providers/discord/index.d.ts +4 -0
- package/dist/providers/discord/index.d.ts.map +1 -0
- package/dist/providers/discord/index.js +3 -0
- package/dist/providers/discord/index.js.map +1 -0
- package/dist/providers/registry.d.ts +14 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +49 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/telegram/index.d.ts +4 -0
- package/dist/providers/telegram/index.d.ts.map +1 -0
- package/dist/providers/telegram/index.js +3 -0
- package/dist/providers/telegram/index.js.map +1 -0
- package/dist/providers/telegram/telegram-format.d.ts +3 -0
- package/dist/providers/telegram/telegram-format.d.ts.map +1 -0
- package/dist/providers/telegram/telegram-format.js +215 -0
- package/dist/providers/telegram/telegram-format.js.map +1 -0
- package/dist/providers/telegram/telegram-provider.d.ts +51 -0
- package/dist/providers/telegram/telegram-provider.d.ts.map +1 -0
- package/dist/providers/telegram/telegram-provider.js +824 -0
- package/dist/providers/telegram/telegram-provider.js.map +1 -0
- package/dist/providers/types.d.ts +107 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/request-router.d.ts.map +1 -1
- package/dist/request-router.js +12 -26
- package/dist/request-router.js.map +1 -1
- package/dist/runtime-config.d.ts +70 -6
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +119 -65
- package/dist/runtime-config.js.map +1 -1
- package/dist/skill-manager.d.ts +39 -0
- package/dist/skill-manager.d.ts.map +1 -0
- package/dist/skill-manager.js +286 -0
- package/dist/skill-manager.js.map +1 -0
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +56 -11
- package/dist/task-scheduler.js.map +1 -1
- package/dist/telegram-format.d.ts.map +1 -1
- package/dist/telegram-format.js +16 -1
- package/dist/telegram-format.js.map +1 -1
- package/dist/tool-intent-probe.d.ts +11 -0
- package/dist/tool-intent-probe.d.ts.map +1 -0
- package/dist/tool-intent-probe.js +63 -0
- package/dist/tool-intent-probe.js.map +1 -0
- package/dist/tool-policy.d.ts.map +1 -1
- package/dist/tool-policy.js +18 -0
- package/dist/tool-policy.js.map +1 -1
- package/dist/transcription.d.ts +8 -0
- package/dist/transcription.d.ts.map +1 -0
- package/dist/transcription.js +174 -0
- package/dist/transcription.js.map +1 -0
- package/dist/types.d.ts +2 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/workflow-engine.d.ts +51 -0
- package/dist/workflow-engine.d.ts.map +1 -0
- package/dist/workflow-engine.js +281 -0
- package/dist/workflow-engine.js.map +1 -0
- package/dist/workflow-store.d.ts +39 -0
- package/dist/workflow-store.d.ts.map +1 -0
- package/dist/workflow-store.js +173 -0
- package/dist/workflow-store.js.map +1 -0
- package/package.json +15 -3
- package/scripts/bootstrap.js +40 -4
- package/scripts/configure.js +48 -7
- package/scripts/doctor.js +30 -4
- package/scripts/init.js +13 -6
|
@@ -7,7 +7,7 @@ import fs from 'fs';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { OpenRouter, stepCountIs } from '@openrouter/sdk';
|
|
10
|
-
import { createTools, ToolCallRecord } from './tools.js';
|
|
10
|
+
import { createTools, discoverMcpTools, ToolCallRecord } from './tools.js';
|
|
11
11
|
import { createIpcHandlers } from './ipc.js';
|
|
12
12
|
import { loadAgentConfig } from './agent-config.js';
|
|
13
13
|
import { OUTPUT_START_MARKER, OUTPUT_END_MARKER, type ContainerInput, type ContainerOutput } from './container-protocol.js';
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
Message
|
|
28
28
|
} from './memory.js';
|
|
29
29
|
import { loadPromptPackWithCanary, formatPromptPack, PromptPack } from './prompt-packs.js';
|
|
30
|
+
import { buildSkillCatalog, formatSkillCatalog, type SkillCatalog } from './skill-loader.js';
|
|
30
31
|
|
|
31
32
|
type OpenRouterResult = ReturnType<OpenRouter['callModel']>;
|
|
32
33
|
|
|
@@ -40,9 +41,6 @@ const AVAILABLE_GROUPS_PATH = '/workspace/ipc/available_groups.json';
|
|
|
40
41
|
const GROUP_CLAUDE_PATH = path.join(GROUP_DIR, 'CLAUDE.md');
|
|
41
42
|
const GLOBAL_CLAUDE_PATH = path.join(GLOBAL_DIR, 'CLAUDE.md');
|
|
42
43
|
const CLAUDE_NOTES_MAX_CHARS = 4000;
|
|
43
|
-
const SKILL_NOTES_MAX_FILES = 16;
|
|
44
|
-
const SKILL_NOTES_MAX_CHARS = 3000;
|
|
45
|
-
const SKILL_NOTES_TOTAL_MAX_CHARS = 18_000;
|
|
46
44
|
|
|
47
45
|
const agentConfig = loadAgentConfig();
|
|
48
46
|
const agent = agentConfig.agent;
|
|
@@ -74,167 +72,17 @@ function log(message: string): void {
|
|
|
74
72
|
console.error(`[agent-runner] ${message}`);
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
// ── Response extraction
|
|
78
|
-
// OpenRouter SDK v0.3.x returns raw response IDs (gen-*, resp-*, etc.) instead
|
|
79
|
-
// of text for fast reasoning models (GPT-5-mini/nano). Reasoning tokens consume
|
|
80
|
-
// the output budget, leaving nothing for actual text. This multi-layer pipeline
|
|
81
|
-
// works around that:
|
|
82
|
-
// 1. isLikelyResponseId — detect leaked IDs so we never surface them
|
|
83
|
-
// 2. extractTextFromRawResponse — walk raw response fields ourselves
|
|
84
|
-
// 3. getTextWithFallback — try SDK getText(), fall back to raw extraction
|
|
85
|
-
// 4. chatCompletionsFallback — retry via /chat/completions when all else fails
|
|
86
|
-
// Remove this pipeline once the SDK reliably returns text for reasoning models.
|
|
87
|
-
|
|
88
|
-
const RESPONSE_ID_PREFIXES = ['gen-', 'resp-', 'resp_', 'chatcmpl-', 'msg_'];
|
|
89
|
-
|
|
90
|
-
function isLikelyResponseId(value: string): boolean {
|
|
91
|
-
const trimmed = value.trim();
|
|
92
|
-
if (!trimmed || trimmed.includes(' ') || trimmed.includes('\n')) return false;
|
|
93
|
-
return RESPONSE_ID_PREFIXES.some(prefix => trimmed.startsWith(prefix));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function isValidText(value: unknown): value is string {
|
|
97
|
-
return typeof value === 'string' && value.trim().length > 0 && !isLikelyResponseId(value);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function extractTextFromRawResponse(response: unknown): string {
|
|
101
|
-
if (!response || typeof response !== 'object') return '';
|
|
102
|
-
const record = response as Record<string, unknown>;
|
|
103
|
-
|
|
104
|
-
// 1. SDK-parsed camelCase field
|
|
105
|
-
if (isValidText(record.outputText)) return record.outputText;
|
|
106
|
-
|
|
107
|
-
// 2. Raw API snake_case field
|
|
108
|
-
if (isValidText(record.output_text)) return record.output_text;
|
|
109
|
-
|
|
110
|
-
// 3. Walk response.output[] for message/output_text items
|
|
111
|
-
if (Array.isArray(record.output)) {
|
|
112
|
-
const parts: string[] = [];
|
|
113
|
-
for (const item of record.output) {
|
|
114
|
-
if (!item || typeof item !== 'object') continue;
|
|
115
|
-
const typed = item as { type?: string; content?: unknown; text?: string };
|
|
116
|
-
if (typed.type === 'message' && Array.isArray(typed.content)) {
|
|
117
|
-
for (const part of typed.content as Array<{ type?: string; text?: string }>) {
|
|
118
|
-
if (part?.type === 'output_text' && isValidText(part.text)) {
|
|
119
|
-
parts.push(part.text);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
} else if (typed.type === 'output_text' && isValidText(typed.text)) {
|
|
123
|
-
parts.push(typed.text);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const joined = parts.join('');
|
|
127
|
-
if (joined.trim()) return joined;
|
|
128
|
-
}
|
|
75
|
+
// ── Response text extraction ─────────────────────────────────────────
|
|
129
76
|
|
|
130
|
-
|
|
131
|
-
if (Array.isArray(record.choices) && record.choices.length > 0) {
|
|
132
|
-
const choice = record.choices[0] as { message?: { content?: unknown } } | null | undefined;
|
|
133
|
-
if (choice?.message && isValidText(choice.message.content)) {
|
|
134
|
-
return choice.message.content;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return '';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function getTextWithFallback(result: OpenRouterResult, context: string): Promise<string> {
|
|
142
|
-
// 1. Try the SDK's proper getText() first — this handles tool execution and
|
|
143
|
-
// extracts text from the final response via the SDK's own logic.
|
|
77
|
+
async function getResponseText(result: OpenRouterResult, context: string): Promise<string> {
|
|
144
78
|
try {
|
|
145
79
|
const text = await result.getText();
|
|
146
|
-
if (
|
|
80
|
+
if (typeof text === 'string' && text.trim()) {
|
|
147
81
|
return text;
|
|
148
82
|
}
|
|
149
|
-
if (text && isLikelyResponseId(text)) {
|
|
150
|
-
log(`Ignored response id from getText (${context}): ${String(text).slice(0, 60)}`);
|
|
151
|
-
}
|
|
152
83
|
} catch (err) {
|
|
153
84
|
log(`getText failed (${context}): ${err instanceof Error ? err.message : String(err)}`);
|
|
154
85
|
}
|
|
155
|
-
|
|
156
|
-
// 2. Fall back to raw response extraction — walk known fields ourselves
|
|
157
|
-
try {
|
|
158
|
-
const response = await result.getResponse();
|
|
159
|
-
const fallbackText = extractTextFromRawResponse(response);
|
|
160
|
-
if (fallbackText) {
|
|
161
|
-
log(`Recovered text from raw response (${context})`);
|
|
162
|
-
return fallbackText;
|
|
163
|
-
}
|
|
164
|
-
const r = response as Record<string, unknown>;
|
|
165
|
-
const outputLen = Array.isArray(r.output) ? (r.output as unknown[]).length : 0;
|
|
166
|
-
log(`No text in raw response (${context}): id=${String(r.id ?? 'none').slice(0, 40)} status=${String(r.status ?? '?')} outputs=${outputLen}`);
|
|
167
|
-
} catch (err) {
|
|
168
|
-
log(`Raw response extraction failed (${context}): ${err instanceof Error ? err.message : String(err)}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// 3. Never return a response ID
|
|
172
|
-
return '';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Direct Chat Completions API fallback.
|
|
177
|
-
* When the Responses API returns a gen-ID instead of text (common with fast
|
|
178
|
-
* models like gpt-5-nano/mini via OpenRouter), retry using the standard
|
|
179
|
-
* /chat/completions endpoint which reliably returns text content.
|
|
180
|
-
*/
|
|
181
|
-
async function chatCompletionsFallback(params: {
|
|
182
|
-
model: string;
|
|
183
|
-
instructions: string;
|
|
184
|
-
messages: Array<{ role: string; content: string }>;
|
|
185
|
-
maxOutputTokens: number;
|
|
186
|
-
temperature: number;
|
|
187
|
-
}): Promise<string> {
|
|
188
|
-
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
189
|
-
if (!apiKey) return '';
|
|
190
|
-
|
|
191
|
-
const headers: Record<string, string> = {
|
|
192
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
193
|
-
'Content-Type': 'application/json'
|
|
194
|
-
};
|
|
195
|
-
if (agent.openrouter.siteUrl) {
|
|
196
|
-
headers['HTTP-Referer'] = agent.openrouter.siteUrl;
|
|
197
|
-
}
|
|
198
|
-
if (agent.openrouter.siteName) {
|
|
199
|
-
headers['X-Title'] = agent.openrouter.siteName;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const chatMessages = [
|
|
203
|
-
{ role: 'system', content: params.instructions },
|
|
204
|
-
...params.messages
|
|
205
|
-
];
|
|
206
|
-
|
|
207
|
-
log(`Chat Completions fallback: model=${params.model}, messages=${chatMessages.length}`);
|
|
208
|
-
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
209
|
-
method: 'POST',
|
|
210
|
-
headers,
|
|
211
|
-
body: JSON.stringify({
|
|
212
|
-
model: params.model,
|
|
213
|
-
messages: chatMessages,
|
|
214
|
-
max_completion_tokens: params.maxOutputTokens,
|
|
215
|
-
temperature: params.temperature,
|
|
216
|
-
reasoning_effort: 'low'
|
|
217
|
-
}),
|
|
218
|
-
signal: AbortSignal.timeout(agent.openrouter.timeoutMs)
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const bodyText = await response.text();
|
|
222
|
-
if (!response.ok) {
|
|
223
|
-
log(`Chat Completions fallback HTTP ${response.status}: ${bodyText.slice(0, 300)}`);
|
|
224
|
-
return '';
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
const data = JSON.parse(bodyText);
|
|
229
|
-
const content = data?.choices?.[0]?.message?.content;
|
|
230
|
-
if (isValidText(content)) {
|
|
231
|
-
log(`Chat Completions fallback recovered text (${String(content).length} chars)`);
|
|
232
|
-
return content;
|
|
233
|
-
}
|
|
234
|
-
log(`Chat Completions fallback returned no text: ${JSON.stringify(data).slice(0, 300)}`);
|
|
235
|
-
} catch (err) {
|
|
236
|
-
log(`Chat Completions fallback parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
237
|
-
}
|
|
238
86
|
return '';
|
|
239
87
|
}
|
|
240
88
|
|
|
@@ -469,11 +317,23 @@ function estimateMessagesTokens(messages: Message[], tokensPerChar: number, toke
|
|
|
469
317
|
return total;
|
|
470
318
|
}
|
|
471
319
|
|
|
320
|
+
const MEMORY_SUMMARY_MAX_CHARS = 2000;
|
|
321
|
+
|
|
322
|
+
const compactToolsDoc = [
|
|
323
|
+
'Tools: Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch, GitClone, NpmInstall,',
|
|
324
|
+
'mcp__dotclaw__send_message, send_file, send_photo, send_voice, send_audio, send_location,',
|
|
325
|
+
'send_contact, send_poll, send_buttons, edit_message, delete_message, download_url,',
|
|
326
|
+
'schedule_task, run_task, list_tasks, pause_task, resume_task, cancel_task, update_task,',
|
|
327
|
+
'spawn_job, job_status, list_jobs, cancel_job, job_update, register_group, remove_group,',
|
|
328
|
+
'list_groups, set_model, memory_upsert, memory_search, memory_list, memory_forget, memory_stats.',
|
|
329
|
+
'plugin__* (plugins), mcp_ext__* (external MCP).'
|
|
330
|
+
].join('\n');
|
|
331
|
+
|
|
472
332
|
function buildSystemInstructions(params: {
|
|
473
333
|
assistantName: string;
|
|
474
334
|
groupNotes?: string | null;
|
|
475
335
|
globalNotes?: string | null;
|
|
476
|
-
|
|
336
|
+
skillCatalog?: SkillCatalog | null;
|
|
477
337
|
memorySummary: string;
|
|
478
338
|
memoryFacts: string[];
|
|
479
339
|
sessionRecall: string[];
|
|
@@ -489,7 +349,10 @@ function buildSystemInstructions(params: {
|
|
|
489
349
|
isBackgroundJob: boolean;
|
|
490
350
|
jobId?: string;
|
|
491
351
|
timezone?: string;
|
|
352
|
+
hostPlatform?: string;
|
|
353
|
+
messagingPlatform?: string;
|
|
492
354
|
planBlock?: string;
|
|
355
|
+
profile?: 'fast' | 'standard' | 'deep' | 'background';
|
|
493
356
|
taskExtractionPack?: PromptPack | null;
|
|
494
357
|
responseQualityPack?: PromptPack | null;
|
|
495
358
|
toolCallingPack?: PromptPack | null;
|
|
@@ -497,122 +360,107 @@ function buildSystemInstructions(params: {
|
|
|
497
360
|
memoryPolicyPack?: PromptPack | null;
|
|
498
361
|
memoryRecallPack?: PromptPack | null;
|
|
499
362
|
}): string {
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
'
|
|
506
|
-
'-
|
|
507
|
-
'-
|
|
508
|
-
'-
|
|
509
|
-
'-
|
|
510
|
-
'- `
|
|
511
|
-
'-
|
|
512
|
-
'-
|
|
513
|
-
'-
|
|
514
|
-
'- `mcp__dotclaw__send_contact`: send a contact card (phone + name).',
|
|
515
|
-
'- `mcp__dotclaw__send_poll`: create a Telegram poll.',
|
|
516
|
-
'- `mcp__dotclaw__send_buttons`: send a message with inline keyboard buttons.',
|
|
517
|
-
'- `mcp__dotclaw__edit_message`: edit a previously sent message.',
|
|
518
|
-
'- `mcp__dotclaw__delete_message`: delete a message.',
|
|
519
|
-
'- `mcp__dotclaw__download_url`: download a URL to the workspace as a file.',
|
|
520
|
-
'- Users may send photos, documents, voice messages, and videos. These are downloaded to `/workspace/group/inbox/` and referenced as `<attachment>` tags in messages. Process them with Read/Bash/Python tools.',
|
|
521
|
-
'- `mcp__dotclaw__schedule_task`: schedule tasks (set `timezone` for locale-specific schedules).',
|
|
522
|
-
'- `mcp__dotclaw__run_task`: run a scheduled task immediately.',
|
|
523
|
-
'- `mcp__dotclaw__list_tasks`, `mcp__dotclaw__pause_task`, `mcp__dotclaw__resume_task`, `mcp__dotclaw__cancel_task`.',
|
|
524
|
-
'- `mcp__dotclaw__update_task`: update a task (state, prompt, schedule, status).',
|
|
525
|
-
'- `mcp__dotclaw__spawn_job`: start a background job.',
|
|
526
|
-
'- `mcp__dotclaw__job_status`, `mcp__dotclaw__list_jobs`, `mcp__dotclaw__cancel_job`.',
|
|
527
|
-
'- `mcp__dotclaw__job_update`: log job progress or notify the user.',
|
|
528
|
-
'Rule: If the task is likely to take more than ~2 minutes or needs multi-step research/coding, you MUST call `mcp__dotclaw__spawn_job` immediately and tell the user you queued it. Do not run long tasks in the foreground.',
|
|
529
|
-
'- `mcp__dotclaw__register_group`: main group only.',
|
|
530
|
-
'- `mcp__dotclaw__remove_group`, `mcp__dotclaw__list_groups`: main group only.',
|
|
531
|
-
'- `mcp__dotclaw__set_model`: main group only.',
|
|
532
|
-
'- `mcp__dotclaw__memory_upsert`: store durable memories.',
|
|
533
|
-
'- `mcp__dotclaw__memory_search`, `mcp__dotclaw__memory_list`, `mcp__dotclaw__memory_forget`, `mcp__dotclaw__memory_stats`.',
|
|
534
|
-
'- `plugin__*`: dynamically loaded plugin tools (if present and allowed by policy).'
|
|
535
|
-
].join('\n');
|
|
536
|
-
const browserAutomation = [
|
|
537
|
-
'Browser automation (via Bash):',
|
|
538
|
-
'- Use `agent-browser open <url>` then `agent-browser snapshot -i`.',
|
|
539
|
-
'- Interact with refs using `agent-browser click @e1`, `fill @e2 "text"`.',
|
|
540
|
-
'- Capture evidence with `agent-browser screenshot`.'
|
|
363
|
+
const profile = params.profile || 'standard';
|
|
364
|
+
const isFast = profile === 'fast';
|
|
365
|
+
|
|
366
|
+
// Tool descriptions are already in each tool's schema — only add essential guidance here
|
|
367
|
+
const toolGuidance = isFast ? '' : [
|
|
368
|
+
'Key tool rules:',
|
|
369
|
+
'- User attachments arrive in /workspace/group/inbox/ (see <attachment> tags). Process with Read/Bash/Python.',
|
|
370
|
+
'- To send media from the web: download_url → send_photo/send_file/send_audio. Always foreground.',
|
|
371
|
+
'- Charts/plots: matplotlib → savefig → send_photo. Graphviz → dot -Tpng → send_photo.',
|
|
372
|
+
'- Voice messages are auto-transcribed (<transcript> in <attachment>). Reply with normal text — the host auto-converts to voice.',
|
|
373
|
+
'- GitHub CLI (`gh`) is available if GH_TOKEN is set.',
|
|
374
|
+
'- Use spawn_job ONLY for tasks >2 minutes (deep research, large projects). Everything else: foreground.',
|
|
375
|
+
'- When you spawn a job, reply minimally (e.g. "Working on it"). No job IDs, bullet plans, or status offers.',
|
|
376
|
+
'- plugin__* and mcp_ext__* tools may be available if configured.'
|
|
541
377
|
].join('\n');
|
|
542
378
|
|
|
543
|
-
const
|
|
379
|
+
const toolsSection = isFast ? compactToolsDoc : toolGuidance;
|
|
380
|
+
|
|
381
|
+
const browserAutomation = !isFast && agentConfig.agent.browser.enabled ? [
|
|
382
|
+
'Browser Tool: actions: navigate, snapshot, click, fill, screenshot, extract, evaluate, close.',
|
|
383
|
+
'Use snapshot with interactive=true for clickable refs (@e1, @e2). Screenshots → /workspace/group/screenshots/.'
|
|
384
|
+
].join('\n') : '';
|
|
385
|
+
|
|
386
|
+
// Memory section: skip for fast profile, consolidate empty placeholders for other profiles
|
|
387
|
+
const includeMemory = !isFast;
|
|
388
|
+
const hasAnyMemory = params.memorySummary || params.memoryFacts.length > 0 ||
|
|
389
|
+
params.longTermRecall.length > 0 || params.userProfile;
|
|
390
|
+
|
|
391
|
+
const memorySummary = params.memorySummary
|
|
392
|
+
? params.memorySummary.slice(0, MEMORY_SUMMARY_MAX_CHARS)
|
|
393
|
+
: '';
|
|
544
394
|
const memoryFacts = params.memoryFacts.length > 0
|
|
545
395
|
? params.memoryFacts.map(fact => `- ${fact}`).join('\n')
|
|
546
|
-
: '
|
|
396
|
+
: '';
|
|
547
397
|
const sessionRecall = params.sessionRecall.length > 0
|
|
548
398
|
? params.sessionRecall.map(item => `- ${item}`).join('\n')
|
|
549
|
-
: '
|
|
550
|
-
|
|
399
|
+
: '';
|
|
551
400
|
const longTermRecall = params.longTermRecall.length > 0
|
|
552
401
|
? params.longTermRecall.map(item => `- ${item}`).join('\n')
|
|
553
|
-
: '
|
|
554
|
-
|
|
555
|
-
const userProfile = params.userProfile
|
|
556
|
-
? params.userProfile
|
|
557
|
-
: 'None.';
|
|
558
|
-
|
|
402
|
+
: '';
|
|
403
|
+
const userProfile = params.userProfile || '';
|
|
559
404
|
const memoryStats = params.memoryStats
|
|
560
405
|
? `Total: ${params.memoryStats.total}, User: ${params.memoryStats.user}, Group: ${params.memoryStats.group}, Global: ${params.memoryStats.global}`
|
|
561
|
-
: '
|
|
406
|
+
: '';
|
|
562
407
|
|
|
563
408
|
const availableGroups = params.availableGroups && params.availableGroups.length > 0
|
|
564
409
|
? params.availableGroups
|
|
565
410
|
.map(group => `- ${group.name} (chat ${group.jid}, last: ${group.lastActivity})`)
|
|
566
411
|
.join('\n')
|
|
567
|
-
: '
|
|
412
|
+
: '';
|
|
568
413
|
|
|
569
414
|
const groupNotes = params.groupNotes ? `Group notes:\n${params.groupNotes}` : '';
|
|
570
415
|
const globalNotes = params.globalNotes ? `Global notes:\n${params.globalNotes}` : '';
|
|
571
|
-
const skillNotes =
|
|
416
|
+
const skillNotes = params.skillCatalog ? formatSkillCatalog(params.skillCatalog) : '';
|
|
572
417
|
|
|
573
|
-
const toolReliability = params.toolReliability && params.toolReliability.length > 0
|
|
418
|
+
const toolReliability = !isFast && params.toolReliability && params.toolReliability.length > 0
|
|
574
419
|
? params.toolReliability
|
|
575
|
-
.sort((a, b) =>
|
|
420
|
+
.sort((a, b) => a.success_rate - b.success_rate)
|
|
421
|
+
.slice(0, 20)
|
|
576
422
|
.map(tool => {
|
|
577
423
|
const pct = `${Math.round(tool.success_rate * 100)}%`;
|
|
578
424
|
const avg = Number.isFinite(tool.avg_duration_ms) ? `${Math.round(tool.avg_duration_ms!)}ms` : 'n/a';
|
|
579
425
|
return `- ${tool.name}: success ${pct} over ${tool.count} calls (avg ${avg})`;
|
|
580
426
|
})
|
|
581
427
|
.join('\n')
|
|
582
|
-
: '
|
|
428
|
+
: '';
|
|
583
429
|
|
|
584
430
|
const behaviorNotes: string[] = [];
|
|
585
431
|
const responseStyle = typeof params.behaviorConfig?.response_style === 'string'
|
|
586
432
|
? String(params.behaviorConfig.response_style)
|
|
587
433
|
: '';
|
|
588
434
|
if (responseStyle === 'concise') {
|
|
589
|
-
behaviorNotes.push('
|
|
435
|
+
behaviorNotes.push('Keep responses short and to the point.');
|
|
590
436
|
} else if (responseStyle === 'detailed') {
|
|
591
|
-
behaviorNotes.push('
|
|
437
|
+
behaviorNotes.push('Give detailed, step-by-step responses when helpful.');
|
|
592
438
|
}
|
|
593
439
|
const toolBias = typeof params.behaviorConfig?.tool_calling_bias === 'number'
|
|
594
440
|
? Number(params.behaviorConfig.tool_calling_bias)
|
|
595
441
|
: null;
|
|
596
442
|
if (toolBias !== null && toolBias < 0.4) {
|
|
597
|
-
behaviorNotes.push('
|
|
443
|
+
behaviorNotes.push('Ask before using tools unless the intent is obvious.');
|
|
598
444
|
} else if (toolBias !== null && toolBias > 0.6) {
|
|
599
|
-
behaviorNotes.push('
|
|
445
|
+
behaviorNotes.push('Use tools proactively when they add accuracy or save time.');
|
|
600
446
|
}
|
|
601
447
|
const cautionBias = typeof params.behaviorConfig?.caution_bias === 'number'
|
|
602
448
|
? Number(params.behaviorConfig.caution_bias)
|
|
603
449
|
: null;
|
|
604
450
|
if (cautionBias !== null && cautionBias > 0.6) {
|
|
605
|
-
behaviorNotes.push('
|
|
451
|
+
behaviorNotes.push('Double-check uncertain facts and flag limitations.');
|
|
606
452
|
}
|
|
607
453
|
|
|
608
|
-
const behaviorConfig = params.behaviorConfig
|
|
609
|
-
? `Behavior overrides:\n${JSON.stringify(params.behaviorConfig, null, 2)}`
|
|
610
|
-
: '';
|
|
611
|
-
|
|
612
454
|
const timezoneNote = params.timezone
|
|
613
455
|
? `Timezone: ${params.timezone}. Use this timezone when interpreting or presenting timestamps unless the user specifies another.`
|
|
614
456
|
: '';
|
|
615
457
|
|
|
458
|
+
const hostPlatformNote = params.hostPlatform
|
|
459
|
+
? (params.hostPlatform.startsWith('linux')
|
|
460
|
+
? `Host platform: ${params.hostPlatform} (matches container).`
|
|
461
|
+
: `You are running inside a Linux container, but the user's host machine is ${params.hostPlatform}. Packages with platform-specific native binaries (e.g. esbuild, swc, sharp) installed here won't work on the host. When you create projects with dependencies, delete node_modules before finishing and tell the user to run the install command on their machine.`)
|
|
462
|
+
: '';
|
|
463
|
+
|
|
616
464
|
const scheduledNote = params.isScheduledTask
|
|
617
465
|
? `You are running as a scheduled task${params.taskId ? ` (task id: ${params.taskId})` : ''}. If you need to communicate, use \`mcp__dotclaw__send_message\`.`
|
|
618
466
|
: '';
|
|
@@ -620,7 +468,7 @@ function buildSystemInstructions(params: {
|
|
|
620
468
|
? 'You are running in the background for a user request. Focus on completing the task and return a complete response without asking follow-up questions unless strictly necessary.'
|
|
621
469
|
: '';
|
|
622
470
|
const jobNote = params.isBackgroundJob
|
|
623
|
-
? `You are running as a background job${params.jobId ? ` (job id: ${params.jobId})` : ''}.
|
|
471
|
+
? `You are running as a background job${params.jobId ? ` (job id: ${params.jobId})` : ''}. Complete the task silently and return the result. Do NOT call \`mcp__dotclaw__job_update\` for routine progress — only for critical blockers or required user decisions. Do NOT send messages to the chat about your progress. Just do the work and return the final result. The system will deliver your result to the user automatically.`
|
|
624
472
|
: '';
|
|
625
473
|
const jobArtifactsNote = params.isBackgroundJob && params.jobId
|
|
626
474
|
? `Job artifacts directory: /workspace/group/jobs/${params.jobId}`
|
|
@@ -629,20 +477,71 @@ function buildSystemInstructions(params: {
|
|
|
629
477
|
const fmtPack = (label: string, pack: PromptPack | null | undefined) =>
|
|
630
478
|
pack ? formatPromptPack({ label, pack, maxDemos: PROMPT_PACKS_MAX_DEMOS, maxChars: PROMPT_PACKS_MAX_CHARS }) : '';
|
|
631
479
|
|
|
632
|
-
|
|
633
|
-
const
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
480
|
+
// Skip prompt packs for fast profile; enforce aggregate budget for others
|
|
481
|
+
const PROMPT_PACKS_TOTAL_BUDGET = PROMPT_PACKS_MAX_CHARS * 3; // Aggregate cap across all packs
|
|
482
|
+
const allPackBlocks: string[] = [];
|
|
483
|
+
if (!isFast) {
|
|
484
|
+
const packEntries: Array<[string, PromptPack | null | undefined]> = [
|
|
485
|
+
['Tool Calling Guidelines', params.toolCallingPack],
|
|
486
|
+
['Tool Outcome Guidelines', params.toolOutcomePack],
|
|
487
|
+
['Task Extraction Guidelines', params.taskExtractionPack],
|
|
488
|
+
['Response Quality Guidelines', params.responseQualityPack],
|
|
489
|
+
['Memory Policy Guidelines', params.memoryPolicyPack],
|
|
490
|
+
['Memory Recall Guidelines', params.memoryRecallPack],
|
|
491
|
+
];
|
|
492
|
+
let totalChars = 0;
|
|
493
|
+
for (const [label, pack] of packEntries) {
|
|
494
|
+
const block = fmtPack(label, pack);
|
|
495
|
+
if (!block) continue;
|
|
496
|
+
if (totalChars + block.length > PROMPT_PACKS_TOTAL_BUDGET) break;
|
|
497
|
+
allPackBlocks.push(block);
|
|
498
|
+
totalChars += block.length;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const taskExtractionBlock = allPackBlocks.find(b => b.includes('Task Extraction')) || '';
|
|
502
|
+
const responseQualityBlock = allPackBlocks.find(b => b.includes('Response Quality')) || '';
|
|
503
|
+
const toolCallingBlock = allPackBlocks.find(b => b.includes('Tool Calling')) || '';
|
|
504
|
+
const toolOutcomeBlock = allPackBlocks.find(b => b.includes('Tool Outcome')) || '';
|
|
505
|
+
const memoryPolicyBlock = allPackBlocks.find(b => b.includes('Memory Policy')) || '';
|
|
506
|
+
const memoryRecallBlock = allPackBlocks.find(b => b.includes('Memory Recall')) || '';
|
|
507
|
+
|
|
508
|
+
// Build memory sections — omit entirely for fast, consolidate empty placeholders for others
|
|
509
|
+
const memorySections: string[] = [];
|
|
510
|
+
if (includeMemory) {
|
|
511
|
+
if (hasAnyMemory) {
|
|
512
|
+
if (memorySummary) {
|
|
513
|
+
memorySections.push('Long-term memory summary:', memorySummary);
|
|
514
|
+
}
|
|
515
|
+
if (memoryFacts) {
|
|
516
|
+
memorySections.push('Long-term facts:', memoryFacts);
|
|
517
|
+
}
|
|
518
|
+
if (userProfile) {
|
|
519
|
+
memorySections.push('User profile (if available):', userProfile);
|
|
520
|
+
}
|
|
521
|
+
if (longTermRecall) {
|
|
522
|
+
memorySections.push('What you remember about the user (long-term):', longTermRecall);
|
|
523
|
+
}
|
|
524
|
+
if (memoryStats) {
|
|
525
|
+
memorySections.push('Memory stats:', memoryStats);
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
memorySections.push('No long-term memory available yet.');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Session recall is always included (local context from current conversation)
|
|
533
|
+
if (sessionRecall) {
|
|
534
|
+
memorySections.push('Recent conversation context:', sessionRecall);
|
|
535
|
+
}
|
|
638
536
|
|
|
639
537
|
return [
|
|
640
|
-
`You are ${params.assistantName}, a personal assistant running inside DotClaw
|
|
538
|
+
`You are ${params.assistantName}, a personal assistant running inside DotClaw.${params.messagingPlatform ? ` You are currently connected via ${params.messagingPlatform}.` : ''}`,
|
|
539
|
+
hostPlatformNote,
|
|
641
540
|
scheduledNote,
|
|
642
541
|
backgroundNote,
|
|
643
542
|
jobNote,
|
|
644
543
|
jobArtifactsNote,
|
|
645
|
-
|
|
544
|
+
toolsSection,
|
|
646
545
|
browserAutomation,
|
|
647
546
|
groupNotes,
|
|
648
547
|
globalNotes,
|
|
@@ -655,25 +554,11 @@ function buildSystemInstructions(params: {
|
|
|
655
554
|
responseQualityBlock,
|
|
656
555
|
memoryPolicyBlock,
|
|
657
556
|
memoryRecallBlock,
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
memoryFacts,
|
|
662
|
-
'User profile (if available):',
|
|
663
|
-
userProfile,
|
|
664
|
-
'Long-term memory recall (durable facts/preferences):',
|
|
665
|
-
longTermRecall,
|
|
666
|
-
'Session recall (recent/older conversation snippets):',
|
|
667
|
-
sessionRecall,
|
|
668
|
-
'Memory stats:',
|
|
669
|
-
memoryStats,
|
|
670
|
-
'Available groups (main group only):',
|
|
671
|
-
availableGroups,
|
|
672
|
-
'Tool reliability (recent):',
|
|
673
|
-
toolReliability,
|
|
557
|
+
...memorySections,
|
|
558
|
+
availableGroups ? `Available groups (main group only):\n${availableGroups}` : '',
|
|
559
|
+
toolReliability ? `Tool reliability (recent):\n${toolReliability}` : '',
|
|
674
560
|
behaviorNotes.length > 0 ? `Behavior notes:\n${behaviorNotes.join('\n')}` : '',
|
|
675
|
-
|
|
676
|
-
'Respond succinctly and helpfully. If you perform tool actions, summarize the results.'
|
|
561
|
+
'Be concise and helpful. When you use tools, summarize what happened rather than dumping raw output.'
|
|
677
562
|
].filter(Boolean).join('\n\n');
|
|
678
563
|
}
|
|
679
564
|
|
|
@@ -708,122 +593,6 @@ function loadClaudeNotes(): { group: string | null; global: string | null } {
|
|
|
708
593
|
};
|
|
709
594
|
}
|
|
710
595
|
|
|
711
|
-
export type SkillNote = {
|
|
712
|
-
scope: 'group' | 'global';
|
|
713
|
-
path: string;
|
|
714
|
-
content: string;
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
function collectSkillFiles(rootDir: string, maxFiles: number): string[] {
|
|
718
|
-
const files: string[] = [];
|
|
719
|
-
const seen = new Set<string>();
|
|
720
|
-
const addFile = (filePath: string) => {
|
|
721
|
-
const normalized = path.resolve(filePath);
|
|
722
|
-
if (seen.has(normalized)) return;
|
|
723
|
-
if (!fs.existsSync(normalized)) return;
|
|
724
|
-
let stat: fs.Stats;
|
|
725
|
-
try {
|
|
726
|
-
stat = fs.statSync(normalized);
|
|
727
|
-
} catch {
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
if (!stat.isFile()) return;
|
|
731
|
-
if (!normalized.toLowerCase().endsWith('.md')) return;
|
|
732
|
-
seen.add(normalized);
|
|
733
|
-
files.push(normalized);
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
addFile(path.join(rootDir, 'SKILL.md'));
|
|
737
|
-
|
|
738
|
-
const skillsDir = path.join(rootDir, 'skills');
|
|
739
|
-
if (fs.existsSync(skillsDir)) {
|
|
740
|
-
const stack = [skillsDir];
|
|
741
|
-
while (stack.length > 0 && files.length < maxFiles) {
|
|
742
|
-
const current = stack.pop();
|
|
743
|
-
if (!current) continue;
|
|
744
|
-
let entries: fs.Dirent[];
|
|
745
|
-
try {
|
|
746
|
-
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
747
|
-
} catch {
|
|
748
|
-
continue;
|
|
749
|
-
}
|
|
750
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
751
|
-
for (const entry of entries) {
|
|
752
|
-
const nextPath = path.join(current, entry.name);
|
|
753
|
-
if (entry.isSymbolicLink()) continue;
|
|
754
|
-
if (entry.isDirectory()) {
|
|
755
|
-
stack.push(nextPath);
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
if (entry.isFile()) {
|
|
759
|
-
addFile(nextPath);
|
|
760
|
-
}
|
|
761
|
-
if (files.length >= maxFiles) break;
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
files.sort((a, b) => a.localeCompare(b));
|
|
767
|
-
return files.slice(0, maxFiles);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
export function loadSkillNotesFromRoots(params: {
|
|
771
|
-
groupDir: string;
|
|
772
|
-
globalDir: string;
|
|
773
|
-
maxFiles?: number;
|
|
774
|
-
maxCharsPerFile?: number;
|
|
775
|
-
maxTotalChars?: number;
|
|
776
|
-
}): SkillNote[] {
|
|
777
|
-
const maxFiles = Number.isFinite(params.maxFiles) ? Math.max(1, Math.floor(params.maxFiles!)) : SKILL_NOTES_MAX_FILES;
|
|
778
|
-
const maxCharsPerFile = Number.isFinite(params.maxCharsPerFile)
|
|
779
|
-
? Math.max(200, Math.floor(params.maxCharsPerFile!))
|
|
780
|
-
: SKILL_NOTES_MAX_CHARS;
|
|
781
|
-
const maxTotalChars = Number.isFinite(params.maxTotalChars)
|
|
782
|
-
? Math.max(maxCharsPerFile, Math.floor(params.maxTotalChars!))
|
|
783
|
-
: SKILL_NOTES_TOTAL_MAX_CHARS;
|
|
784
|
-
|
|
785
|
-
const notes: SkillNote[] = [];
|
|
786
|
-
let consumedChars = 0;
|
|
787
|
-
|
|
788
|
-
const appendScopeNotes = (scope: 'group' | 'global', rootDir: string) => {
|
|
789
|
-
const skillFiles = collectSkillFiles(rootDir, maxFiles);
|
|
790
|
-
for (const filePath of skillFiles) {
|
|
791
|
-
if (notes.length >= maxFiles) break;
|
|
792
|
-
if (consumedChars >= maxTotalChars) break;
|
|
793
|
-
const content = readTextFileLimited(filePath, maxCharsPerFile);
|
|
794
|
-
if (!content) continue;
|
|
795
|
-
const remaining = maxTotalChars - consumedChars;
|
|
796
|
-
const truncated = content.length > remaining
|
|
797
|
-
? `${content.slice(0, remaining)}\n\n[Truncated for total skill budget]`
|
|
798
|
-
: content;
|
|
799
|
-
const relativePath = path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
800
|
-
notes.push({
|
|
801
|
-
scope,
|
|
802
|
-
path: relativePath || path.basename(filePath),
|
|
803
|
-
content: truncated
|
|
804
|
-
});
|
|
805
|
-
consumedChars += truncated.length;
|
|
806
|
-
if (consumedChars >= maxTotalChars) break;
|
|
807
|
-
}
|
|
808
|
-
};
|
|
809
|
-
|
|
810
|
-
appendScopeNotes('group', params.groupDir);
|
|
811
|
-
appendScopeNotes('global', params.globalDir);
|
|
812
|
-
return notes;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
function formatSkillNotes(notes: SkillNote[]): string {
|
|
816
|
-
if (!notes || notes.length === 0) return '';
|
|
817
|
-
const lines: string[] = [
|
|
818
|
-
'Skill instructions (loaded from SKILL.md / skills/*.md):',
|
|
819
|
-
'When a task matches a skill, follow that skill workflow first and keep output concise.'
|
|
820
|
-
];
|
|
821
|
-
for (const note of notes) {
|
|
822
|
-
lines.push(`[${note.scope}] ${note.path}`);
|
|
823
|
-
lines.push(note.content);
|
|
824
|
-
}
|
|
825
|
-
return lines.join('\n\n');
|
|
826
|
-
}
|
|
827
596
|
|
|
828
597
|
function extractQueryFromPrompt(prompt: string): string {
|
|
829
598
|
if (!prompt) return '';
|
|
@@ -886,7 +655,7 @@ async function updateMemorySummary(params: {
|
|
|
886
655
|
temperature: 0.1,
|
|
887
656
|
reasoning: { effort: 'low' as const }
|
|
888
657
|
});
|
|
889
|
-
const text = await
|
|
658
|
+
const text = await getResponseText(result, 'summary');
|
|
890
659
|
return parseSummaryResponse(text);
|
|
891
660
|
}
|
|
892
661
|
|
|
@@ -1029,7 +798,7 @@ async function validateResponseQuality(params: {
|
|
|
1029
798
|
temperature: params.temperature,
|
|
1030
799
|
reasoning: { effort: 'low' as const }
|
|
1031
800
|
});
|
|
1032
|
-
const text = await
|
|
801
|
+
const text = await getResponseText(result, 'response_validation');
|
|
1033
802
|
return parseResponseValidation(text);
|
|
1034
803
|
}
|
|
1035
804
|
|
|
@@ -1138,9 +907,10 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1138
907
|
const tokenEstimate = resolveTokenEstimate(input, agentConfig);
|
|
1139
908
|
const availableGroups = loadAvailableGroups();
|
|
1140
909
|
const claudeNotes = loadClaudeNotes();
|
|
1141
|
-
const
|
|
910
|
+
const skillCatalog = buildSkillCatalog({
|
|
1142
911
|
groupDir: GROUP_DIR,
|
|
1143
|
-
globalDir: GLOBAL_DIR
|
|
912
|
+
globalDir: GLOBAL_DIR,
|
|
913
|
+
maxSkills: agent.skills.maxSkills
|
|
1144
914
|
});
|
|
1145
915
|
|
|
1146
916
|
const { ctx: sessionCtx, isNew } = createSessionContext(SESSION_ROOT, input.sessionId);
|
|
@@ -1168,6 +938,36 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1168
938
|
}
|
|
1169
939
|
});
|
|
1170
940
|
|
|
941
|
+
// Discover MCP external tools if enabled
|
|
942
|
+
let mcpCleanup: (() => Promise<void>) | null = null;
|
|
943
|
+
if (agent.mcp.enabled && agent.mcp.servers.length > 0) {
|
|
944
|
+
try {
|
|
945
|
+
// Build a minimal wrapExecute for MCP tools (policy + logging handled by createTools wrapExecute pattern)
|
|
946
|
+
const wrapMcp = <TInput, TOutput>(name: string, execute: (args: TInput) => Promise<TOutput>) => {
|
|
947
|
+
return async (args: TInput): Promise<TOutput> => {
|
|
948
|
+
const start = Date.now();
|
|
949
|
+
try {
|
|
950
|
+
const result = await execute(args);
|
|
951
|
+
toolCalls.push({ name, ok: true, duration_ms: Date.now() - start });
|
|
952
|
+
return result;
|
|
953
|
+
} catch (err) {
|
|
954
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
955
|
+
toolCalls.push({ name, ok: false, duration_ms: Date.now() - start, error });
|
|
956
|
+
throw err;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
};
|
|
960
|
+
const mcp = await discoverMcpTools(agent, wrapMcp);
|
|
961
|
+
tools.push(...mcp.tools);
|
|
962
|
+
mcpCleanup = mcp.cleanup;
|
|
963
|
+
if (mcp.tools.length > 0) {
|
|
964
|
+
log(`MCP: discovered ${mcp.tools.length} external tools`);
|
|
965
|
+
}
|
|
966
|
+
} catch (err) {
|
|
967
|
+
log(`MCP discovery failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
1171
971
|
if (process.env.DOTCLAW_SELF_CHECK === '1') {
|
|
1172
972
|
try {
|
|
1173
973
|
const details = await runSelfCheck({ model });
|
|
@@ -1332,7 +1132,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1332
1132
|
assistantName,
|
|
1333
1133
|
groupNotes: claudeNotes.group,
|
|
1334
1134
|
globalNotes: claudeNotes.global,
|
|
1335
|
-
|
|
1135
|
+
skillCatalog,
|
|
1336
1136
|
memorySummary: sessionCtx.state.summary,
|
|
1337
1137
|
memoryFacts: sessionCtx.state.facts,
|
|
1338
1138
|
sessionRecall,
|
|
@@ -1348,7 +1148,10 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1348
1148
|
isBackgroundJob: !!input.isBackgroundJob,
|
|
1349
1149
|
jobId: input.jobId,
|
|
1350
1150
|
timezone: typeof input.timezone === 'string' ? input.timezone : undefined,
|
|
1151
|
+
hostPlatform: typeof input.hostPlatform === 'string' ? input.hostPlatform : undefined,
|
|
1152
|
+
messagingPlatform: input.chatJid?.includes(':') ? input.chatJid.split(':')[0] : undefined,
|
|
1351
1153
|
planBlock: planBlockValue,
|
|
1154
|
+
profile: input.profile,
|
|
1352
1155
|
taskExtractionPack: taskPackResult?.pack || null,
|
|
1353
1156
|
responseQualityPack: responseQualityResult?.pack || null,
|
|
1354
1157
|
toolCallingPack: toolCallingResult?.pack || null,
|
|
@@ -1384,7 +1187,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1384
1187
|
temperature: plannerTemperature,
|
|
1385
1188
|
reasoning: { effort: 'low' as const }
|
|
1386
1189
|
});
|
|
1387
|
-
const plannerText = await
|
|
1190
|
+
const plannerText = await getResponseText(plannerResult, 'planner');
|
|
1388
1191
|
const plan = parsePlannerResponse(plannerText);
|
|
1389
1192
|
if (plan) {
|
|
1390
1193
|
planBlock = formatPlanBlock(plan);
|
|
@@ -1465,7 +1268,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1465
1268
|
const localLatencyMs = Date.now() - startedAt;
|
|
1466
1269
|
|
|
1467
1270
|
// Get the complete response text via the SDK's proper getText() path
|
|
1468
|
-
let localResponseText = await
|
|
1271
|
+
let localResponseText = await getResponseText(result, 'completion');
|
|
1469
1272
|
|
|
1470
1273
|
const toolCallsFromModel = await result.getToolCalls();
|
|
1471
1274
|
if (toolCallsFromModel.length > 0) {
|
|
@@ -1476,21 +1279,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1476
1279
|
if (toolCallsFromModel.length > 0) {
|
|
1477
1280
|
localResponseText = 'I started running tool calls but did not get a final response. If you want me to continue, please ask a narrower subtask or say "continue".';
|
|
1478
1281
|
} else {
|
|
1479
|
-
|
|
1480
|
-
try {
|
|
1481
|
-
localResponseText = await chatCompletionsFallback({
|
|
1482
|
-
model,
|
|
1483
|
-
instructions: resolvedInstructions,
|
|
1484
|
-
messages: messagesToOpenRouter(contextMessages),
|
|
1485
|
-
maxOutputTokens: config.maxOutputTokens,
|
|
1486
|
-
temperature: config.temperature
|
|
1487
|
-
});
|
|
1488
|
-
} catch (err) {
|
|
1489
|
-
log(`Chat Completions fallback error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
if (!localResponseText || !localResponseText.trim()) {
|
|
1493
|
-
log(`Warning: Model returned empty/whitespace response after all fallbacks. tool calls: ${toolCallsFromModel.length}`);
|
|
1282
|
+
log(`Warning: Model returned empty/whitespace response. tool calls: ${toolCallsFromModel.length}`);
|
|
1494
1283
|
}
|
|
1495
1284
|
} else {
|
|
1496
1285
|
log(`Model returned text response (${localResponseText.length} chars)`);
|
|
@@ -1625,7 +1414,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1625
1414
|
temperature: 0.1,
|
|
1626
1415
|
reasoning: { effort: 'low' as const }
|
|
1627
1416
|
});
|
|
1628
|
-
const extractionText = await
|
|
1417
|
+
const extractionText = await getResponseText(extractionResult, 'memory_extraction');
|
|
1629
1418
|
const extractedItems = parseMemoryExtraction(extractionText);
|
|
1630
1419
|
if (extractedItems.length === 0) return;
|
|
1631
1420
|
|
|
@@ -1691,6 +1480,11 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
|
|
|
1691
1480
|
}
|
|
1692
1481
|
}
|
|
1693
1482
|
|
|
1483
|
+
// Cleanup MCP connections
|
|
1484
|
+
if (mcpCleanup) {
|
|
1485
|
+
try { await mcpCleanup(); } catch { /* ignore cleanup errors */ }
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1694
1488
|
return {
|
|
1695
1489
|
status: 'success',
|
|
1696
1490
|
result: finalResult,
|