@dotsetlabs/dotclaw 1.1.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 +54 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/config-examples/groups/global/CLAUDE.md +21 -0
- package/config-examples/groups/main/CLAUDE.md +47 -0
- package/config-examples/mount-allowlist.json +25 -0
- package/config-examples/plugin-http.json +18 -0
- package/config-examples/runtime.json +30 -0
- package/config-examples/tool-budgets.json +24 -0
- package/config-examples/tool-policy.json +51 -0
- package/container/.dockerignore +6 -0
- package/container/Dockerfile +74 -0
- package/container/agent-runner/package-lock.json +92 -0
- package/container/agent-runner/package.json +20 -0
- package/container/agent-runner/src/agent-config.ts +295 -0
- package/container/agent-runner/src/container-protocol.ts +73 -0
- package/container/agent-runner/src/daemon.ts +91 -0
- package/container/agent-runner/src/index.ts +1428 -0
- package/container/agent-runner/src/ipc.ts +321 -0
- package/container/agent-runner/src/memory.ts +336 -0
- package/container/agent-runner/src/prompt-packs.ts +341 -0
- package/container/agent-runner/src/tools.ts +1720 -0
- package/container/agent-runner/tsconfig.json +19 -0
- package/container/build.sh +23 -0
- package/container/skills/agent-browser.md +159 -0
- package/dist/admin-commands.d.ts +7 -0
- package/dist/admin-commands.d.ts.map +1 -0
- package/dist/admin-commands.js +87 -0
- package/dist/admin-commands.js.map +1 -0
- package/dist/agent-context.d.ts +42 -0
- package/dist/agent-context.d.ts.map +1 -0
- package/dist/agent-context.js +92 -0
- package/dist/agent-context.js.map +1 -0
- package/dist/agent-execution.d.ts +68 -0
- package/dist/agent-execution.d.ts.map +1 -0
- package/dist/agent-execution.js +169 -0
- package/dist/agent-execution.js.map +1 -0
- package/dist/agent-semaphore.d.ts +2 -0
- package/dist/agent-semaphore.d.ts.map +1 -0
- package/dist/agent-semaphore.js +52 -0
- package/dist/agent-semaphore.js.map +1 -0
- package/dist/behavior-config.d.ts +14 -0
- package/dist/behavior-config.d.ts.map +1 -0
- package/dist/behavior-config.js +52 -0
- package/dist/behavior-config.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +626 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/container-protocol.d.ts +72 -0
- package/dist/container-protocol.d.ts.map +1 -0
- package/dist/container-protocol.js +3 -0
- package/dist/container-protocol.js.map +1 -0
- package/dist/container-runner.d.ts +59 -0
- package/dist/container-runner.d.ts.map +1 -0
- package/dist/container-runner.js +813 -0
- package/dist/container-runner.js.map +1 -0
- package/dist/cost.d.ts +9 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +11 -0
- package/dist/cost.js.map +1 -0
- package/dist/dashboard.d.ts +58 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +471 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.d.ts +99 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +423 -0
- package/dist/db.js.map +1 -0
- package/dist/error-messages.d.ts +17 -0
- package/dist/error-messages.d.ts.map +1 -0
- package/dist/error-messages.js +109 -0
- package/dist/error-messages.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2072 -0
- package/dist/index.js.map +1 -0
- package/dist/locks.d.ts +2 -0
- package/dist/locks.d.ts.map +1 -0
- package/dist/locks.js +26 -0
- package/dist/locks.js.map +1 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/maintenance.d.ts +13 -0
- package/dist/maintenance.d.ts.map +1 -0
- package/dist/maintenance.js +151 -0
- package/dist/maintenance.js.map +1 -0
- package/dist/memory-embeddings.d.ts +13 -0
- package/dist/memory-embeddings.d.ts.map +1 -0
- package/dist/memory-embeddings.js +126 -0
- package/dist/memory-embeddings.js.map +1 -0
- package/dist/memory-recall.d.ts +8 -0
- package/dist/memory-recall.d.ts.map +1 -0
- package/dist/memory-recall.js +127 -0
- package/dist/memory-recall.js.map +1 -0
- package/dist/memory-store.d.ts +149 -0
- package/dist/memory-store.d.ts.map +1 -0
- package/dist/memory-store.js +787 -0
- package/dist/memory-store.js.map +1 -0
- package/dist/metrics.d.ts +12 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +134 -0
- package/dist/metrics.js.map +1 -0
- package/dist/model-registry.d.ts +67 -0
- package/dist/model-registry.d.ts.map +1 -0
- package/dist/model-registry.js +230 -0
- package/dist/model-registry.js.map +1 -0
- package/dist/mount-security.d.ts +37 -0
- package/dist/mount-security.d.ts.map +1 -0
- package/dist/mount-security.js +284 -0
- package/dist/mount-security.js.map +1 -0
- package/dist/paths.d.ts +80 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +149 -0
- package/dist/paths.js.map +1 -0
- package/dist/personalization.d.ts +6 -0
- package/dist/personalization.d.ts.map +1 -0
- package/dist/personalization.js +180 -0
- package/dist/personalization.js.map +1 -0
- package/dist/progress.d.ts +15 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +92 -0
- package/dist/progress.js.map +1 -0
- package/dist/runtime-config.d.ts +227 -0
- package/dist/runtime-config.d.ts.map +1 -0
- package/dist/runtime-config.js +297 -0
- package/dist/runtime-config.js.map +1 -0
- package/dist/task-scheduler.d.ts +9 -0
- package/dist/task-scheduler.d.ts.map +1 -0
- package/dist/task-scheduler.js +195 -0
- package/dist/task-scheduler.js.map +1 -0
- package/dist/telegram-format.d.ts +3 -0
- package/dist/telegram-format.d.ts.map +1 -0
- package/dist/telegram-format.js +200 -0
- package/dist/telegram-format.js.map +1 -0
- package/dist/tool-budgets.d.ts +16 -0
- package/dist/tool-budgets.d.ts.map +1 -0
- package/dist/tool-budgets.js +83 -0
- package/dist/tool-budgets.js.map +1 -0
- package/dist/tool-policy.d.ts +18 -0
- package/dist/tool-policy.d.ts.map +1 -0
- package/dist/tool-policy.js +84 -0
- package/dist/tool-policy.js.map +1 -0
- package/dist/trace-writer.d.ts +39 -0
- package/dist/trace-writer.d.ts.map +1 -0
- package/dist/trace-writer.js +27 -0
- package/dist/trace-writer.js.map +1 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +30 -0
- package/dist/utils.js.map +1 -0
- package/launchd/com.dotclaw.plist +32 -0
- package/package.json +89 -0
- package/scripts/autotune.js +53 -0
- package/scripts/bootstrap.js +348 -0
- package/scripts/configure.js +200 -0
- package/scripts/doctor.js +164 -0
- package/scripts/init.js +209 -0
- package/scripts/install.sh +219 -0
- package/systemd/dotclaw.service +22 -0
|
@@ -0,0 +1,1428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DotClaw Agent Runner (OpenRouter)
|
|
3
|
+
* Runs inside a container, receives config via stdin, outputs result to stdout
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { OpenRouter, stepCountIs } from '@openrouter/sdk';
|
|
10
|
+
import { createTools, ToolCallRecord } from './tools.js';
|
|
11
|
+
import { createIpcHandlers } from './ipc.js';
|
|
12
|
+
import { loadAgentConfig } from './agent-config.js';
|
|
13
|
+
import { OUTPUT_START_MARKER, OUTPUT_END_MARKER, type ContainerInput, type ContainerOutput } from './container-protocol.js';
|
|
14
|
+
import {
|
|
15
|
+
createSessionContext,
|
|
16
|
+
appendHistory,
|
|
17
|
+
loadHistory,
|
|
18
|
+
splitRecentHistory,
|
|
19
|
+
shouldCompact,
|
|
20
|
+
archiveConversation,
|
|
21
|
+
buildSummaryPrompt,
|
|
22
|
+
parseSummaryResponse,
|
|
23
|
+
retrieveRelevantMemories,
|
|
24
|
+
saveMemoryState,
|
|
25
|
+
writeHistory,
|
|
26
|
+
MemoryConfig,
|
|
27
|
+
Message
|
|
28
|
+
} from './memory.js';
|
|
29
|
+
import { loadPromptPackWithCanary, formatTaskExtractionPack, formatResponseQualityPack, formatToolCallingPack, formatToolOutcomePack, formatMemoryPolicyPack, formatMemoryRecallPack, PromptPack } from './prompt-packs.js';
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
const SESSION_ROOT = '/workspace/session';
|
|
33
|
+
const GROUP_DIR = '/workspace/group';
|
|
34
|
+
const IPC_DIR = '/workspace/ipc';
|
|
35
|
+
const GLOBAL_DIR = '/workspace/global';
|
|
36
|
+
const PROMPTS_DIR = '/workspace/prompts';
|
|
37
|
+
const AVAILABLE_GROUPS_PATH = '/workspace/ipc/available_groups.json';
|
|
38
|
+
const GROUP_CLAUDE_PATH = path.join(GROUP_DIR, 'CLAUDE.md');
|
|
39
|
+
const GLOBAL_CLAUDE_PATH = path.join(GLOBAL_DIR, 'CLAUDE.md');
|
|
40
|
+
const CLAUDE_NOTES_MAX_CHARS = 4000;
|
|
41
|
+
|
|
42
|
+
const agentConfig = loadAgentConfig();
|
|
43
|
+
const agent = agentConfig.agent;
|
|
44
|
+
|
|
45
|
+
const PROMPT_PACKS_ENABLED = agent.promptPacks.enabled;
|
|
46
|
+
const PROMPT_PACKS_MAX_CHARS = agent.promptPacks.maxChars;
|
|
47
|
+
const PROMPT_PACKS_MAX_DEMOS = agent.promptPacks.maxDemos;
|
|
48
|
+
const PROMPT_PACKS_CANARY_RATE = agent.promptPacks.canaryRate;
|
|
49
|
+
|
|
50
|
+
let cachedOpenRouter: OpenRouter | null = null;
|
|
51
|
+
let cachedOpenRouterKey = '';
|
|
52
|
+
let cachedOpenRouterOptions = '';
|
|
53
|
+
|
|
54
|
+
function getCachedOpenRouter(apiKey: string, options: ReturnType<typeof getOpenRouterOptions>): OpenRouter {
|
|
55
|
+
const optionsKey = JSON.stringify(options);
|
|
56
|
+
if (cachedOpenRouter && cachedOpenRouterKey === apiKey && cachedOpenRouterOptions === optionsKey) {
|
|
57
|
+
return cachedOpenRouter;
|
|
58
|
+
}
|
|
59
|
+
cachedOpenRouter = new OpenRouter({
|
|
60
|
+
apiKey,
|
|
61
|
+
...options
|
|
62
|
+
});
|
|
63
|
+
cachedOpenRouterKey = apiKey;
|
|
64
|
+
cachedOpenRouterOptions = optionsKey;
|
|
65
|
+
return cachedOpenRouter;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function log(message: string): void {
|
|
69
|
+
console.error(`[agent-runner] ${message}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeOutput(output: ContainerOutput): void {
|
|
73
|
+
console.log(OUTPUT_START_MARKER);
|
|
74
|
+
console.log(JSON.stringify(output));
|
|
75
|
+
console.log(OUTPUT_END_MARKER);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function runSelfCheck(params: {
|
|
79
|
+
model: string;
|
|
80
|
+
}) {
|
|
81
|
+
const details: string[] = [];
|
|
82
|
+
|
|
83
|
+
if (!process.env.OPENROUTER_API_KEY) {
|
|
84
|
+
throw new Error('OPENROUTER_API_KEY is not set');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.mkdirSync(GROUP_DIR, { recursive: true });
|
|
88
|
+
fs.mkdirSync(SESSION_ROOT, { recursive: true });
|
|
89
|
+
fs.mkdirSync(IPC_DIR, { recursive: true });
|
|
90
|
+
fs.mkdirSync(path.join(IPC_DIR, 'messages'), { recursive: true });
|
|
91
|
+
fs.mkdirSync(path.join(IPC_DIR, 'tasks'), { recursive: true });
|
|
92
|
+
|
|
93
|
+
const filePath = path.join(GROUP_DIR, '.dotclaw-selfcheck');
|
|
94
|
+
fs.writeFileSync(filePath, `self-check-${Date.now()}`);
|
|
95
|
+
const readBack = fs.readFileSync(filePath, 'utf-8');
|
|
96
|
+
if (!readBack.startsWith('self-check-')) {
|
|
97
|
+
throw new Error('Failed to read back self-check file');
|
|
98
|
+
}
|
|
99
|
+
fs.unlinkSync(filePath);
|
|
100
|
+
details.push('group directory writable');
|
|
101
|
+
|
|
102
|
+
const sessionPath = path.join(SESSION_ROOT, 'self-check');
|
|
103
|
+
fs.mkdirSync(sessionPath, { recursive: true });
|
|
104
|
+
const sessionFile = path.join(sessionPath, 'probe.txt');
|
|
105
|
+
fs.writeFileSync(sessionFile, 'ok');
|
|
106
|
+
fs.readFileSync(sessionFile, 'utf-8');
|
|
107
|
+
fs.unlinkSync(sessionFile);
|
|
108
|
+
details.push('session directory writable');
|
|
109
|
+
|
|
110
|
+
const ipcFile = path.join(IPC_DIR, 'messages', `self-check-${Date.now()}.json`);
|
|
111
|
+
fs.writeFileSync(ipcFile, JSON.stringify({ ok: true }, null, 2));
|
|
112
|
+
fs.unlinkSync(ipcFile);
|
|
113
|
+
details.push('ipc directory writable');
|
|
114
|
+
|
|
115
|
+
const headers: Record<string, string> = {
|
|
116
|
+
'Authorization': `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
117
|
+
'Content-Type': 'application/json'
|
|
118
|
+
};
|
|
119
|
+
if (agent.openrouter.siteUrl) {
|
|
120
|
+
headers['HTTP-Referer'] = agent.openrouter.siteUrl;
|
|
121
|
+
}
|
|
122
|
+
if (agent.openrouter.siteName) {
|
|
123
|
+
headers['X-Title'] = agent.openrouter.siteName;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers,
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
model: params.model,
|
|
131
|
+
messages: [{ role: 'user', content: 'Return exactly the string "OK".' }],
|
|
132
|
+
max_tokens: 8,
|
|
133
|
+
temperature: 0
|
|
134
|
+
})
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const bodyText = await response.text();
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`OpenRouter HTTP ${response.status}: ${bodyText.slice(0, 500)}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const data = JSON.parse(bodyText);
|
|
144
|
+
const content = data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text;
|
|
145
|
+
if (!content || !String(content).trim()) {
|
|
146
|
+
throw new Error('OpenRouter call returned empty response');
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw new Error(`OpenRouter response parse failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
details.push('openrouter call ok');
|
|
153
|
+
|
|
154
|
+
return details;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function readStdin(): Promise<string> {
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
let data = '';
|
|
160
|
+
process.stdin.setEncoding('utf8');
|
|
161
|
+
process.stdin.on('data', chunk => { data += chunk; });
|
|
162
|
+
process.stdin.on('end', () => resolve(data));
|
|
163
|
+
process.stdin.on('error', reject);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getConfig(config: ReturnType<typeof loadAgentConfig>): MemoryConfig & {
|
|
168
|
+
maxOutputTokens: number;
|
|
169
|
+
summaryMaxOutputTokens: number;
|
|
170
|
+
temperature: number;
|
|
171
|
+
} {
|
|
172
|
+
return {
|
|
173
|
+
maxContextTokens: config.agent.context.maxContextTokens,
|
|
174
|
+
compactionTriggerTokens: config.agent.context.compactionTriggerTokens,
|
|
175
|
+
recentContextTokens: config.agent.context.recentContextTokens,
|
|
176
|
+
summaryUpdateEveryMessages: config.agent.context.summaryUpdateEveryMessages,
|
|
177
|
+
memoryMaxResults: config.agent.memory.maxResults,
|
|
178
|
+
memoryMaxTokens: config.agent.memory.maxTokens,
|
|
179
|
+
maxOutputTokens: config.agent.context.maxOutputTokens,
|
|
180
|
+
summaryMaxOutputTokens: config.agent.context.summaryMaxOutputTokens,
|
|
181
|
+
temperature: config.agent.context.temperature
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildPlannerPrompt(messages: Message[]): { instructions: string; input: string } {
|
|
186
|
+
const transcript = messages.map(msg => `${msg.role.toUpperCase()}: ${msg.content}`).join('\n\n');
|
|
187
|
+
const instructions = [
|
|
188
|
+
'You are a planning module for a personal assistant.',
|
|
189
|
+
'Given the conversation, produce a concise plan in JSON.',
|
|
190
|
+
'Return JSON only with keys:',
|
|
191
|
+
'- steps: array of short action steps',
|
|
192
|
+
'- tools: array of tool names you expect to use (if any)',
|
|
193
|
+
'- risks: array of potential pitfalls or missing info',
|
|
194
|
+
'- questions: array of clarifying questions (if any)',
|
|
195
|
+
'Keep each array short. Use empty arrays if not needed.'
|
|
196
|
+
].join('\n');
|
|
197
|
+
const input = `Conversation:\n${transcript}`;
|
|
198
|
+
return { instructions, input };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parsePlannerResponse(text: string): { steps: string[]; tools: string[]; risks: string[]; questions: string[] } | null {
|
|
202
|
+
const trimmed = text.trim();
|
|
203
|
+
let jsonText = trimmed;
|
|
204
|
+
const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
205
|
+
if (fenceMatch) {
|
|
206
|
+
jsonText = fenceMatch[1].trim();
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(jsonText) as Record<string, unknown>;
|
|
210
|
+
const steps = Array.isArray(parsed.steps) ? parsed.steps.filter(item => typeof item === 'string') : [];
|
|
211
|
+
const tools = Array.isArray(parsed.tools) ? parsed.tools.filter(item => typeof item === 'string') : [];
|
|
212
|
+
const risks = Array.isArray(parsed.risks) ? parsed.risks.filter(item => typeof item === 'string') : [];
|
|
213
|
+
const questions = Array.isArray(parsed.questions) ? parsed.questions.filter(item => typeof item === 'string') : [];
|
|
214
|
+
return { steps, tools, risks, questions };
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function formatPlanBlock(plan: { steps: string[]; tools: string[]; risks: string[]; questions: string[] }): string {
|
|
221
|
+
const lines: string[] = ['Planned approach (planner):'];
|
|
222
|
+
if (plan.steps.length > 0) {
|
|
223
|
+
lines.push('Steps:');
|
|
224
|
+
for (const step of plan.steps) lines.push(`- ${step}`);
|
|
225
|
+
}
|
|
226
|
+
if (plan.tools.length > 0) {
|
|
227
|
+
lines.push('Tools:');
|
|
228
|
+
for (const tool of plan.tools) lines.push(`- ${tool}`);
|
|
229
|
+
}
|
|
230
|
+
if (plan.risks.length > 0) {
|
|
231
|
+
lines.push('Risks:');
|
|
232
|
+
for (const risk of plan.risks) lines.push(`- ${risk}`);
|
|
233
|
+
}
|
|
234
|
+
if (plan.questions.length > 0) {
|
|
235
|
+
lines.push('Questions:');
|
|
236
|
+
for (const question of plan.questions) lines.push(`- ${question}`);
|
|
237
|
+
}
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getOpenRouterOptions(config: ReturnType<typeof loadAgentConfig>) {
|
|
242
|
+
const timeoutMs = config.agent.openrouter.timeoutMs;
|
|
243
|
+
const retryEnabled = config.agent.openrouter.retry;
|
|
244
|
+
const retryConfig = retryEnabled
|
|
245
|
+
? {
|
|
246
|
+
strategy: 'backoff' as const,
|
|
247
|
+
backoff: {
|
|
248
|
+
initialInterval: 500,
|
|
249
|
+
maxInterval: 5000,
|
|
250
|
+
exponent: 2,
|
|
251
|
+
maxElapsedTime: 20_000
|
|
252
|
+
},
|
|
253
|
+
retryConnectionErrors: true
|
|
254
|
+
}
|
|
255
|
+
: { strategy: 'none' as const };
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
timeoutMs,
|
|
259
|
+
retryConfig,
|
|
260
|
+
httpReferer: config.agent.openrouter.siteUrl || undefined,
|
|
261
|
+
xTitle: config.agent.openrouter.siteName || undefined
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function resolveTokenEstimate(
|
|
266
|
+
input: ContainerInput,
|
|
267
|
+
config: ReturnType<typeof loadAgentConfig>
|
|
268
|
+
): { tokensPerChar: number; tokensPerMessage: number; tokensPerRequest: number } {
|
|
269
|
+
const fallbackChar = config.agent.tokenEstimate.tokensPerChar;
|
|
270
|
+
const fallbackMessage = config.agent.tokenEstimate.tokensPerMessage;
|
|
271
|
+
const fallbackRequest = config.agent.tokenEstimate.tokensPerRequest;
|
|
272
|
+
const tokensPerChar = Number.isFinite(input.tokenEstimate?.tokens_per_char)
|
|
273
|
+
? Number(input.tokenEstimate?.tokens_per_char)
|
|
274
|
+
: fallbackChar;
|
|
275
|
+
const tokensPerMessage = Number.isFinite(input.tokenEstimate?.tokens_per_message)
|
|
276
|
+
? Number(input.tokenEstimate?.tokens_per_message)
|
|
277
|
+
: fallbackMessage;
|
|
278
|
+
const tokensPerRequest = Number.isFinite(input.tokenEstimate?.tokens_per_request)
|
|
279
|
+
? Number(input.tokenEstimate?.tokens_per_request)
|
|
280
|
+
: fallbackRequest;
|
|
281
|
+
return {
|
|
282
|
+
tokensPerChar: Math.max(0, tokensPerChar),
|
|
283
|
+
tokensPerMessage: Math.max(0, tokensPerMessage),
|
|
284
|
+
tokensPerRequest: Math.max(0, tokensPerRequest)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function estimateTokensForModel(text: string, tokensPerChar: number): number {
|
|
289
|
+
if (!text) return 0;
|
|
290
|
+
const bytes = Buffer.byteLength(text, 'utf-8');
|
|
291
|
+
return Math.ceil(bytes * tokensPerChar);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function estimateMessagesTokens(messages: Message[], tokensPerChar: number, tokensPerMessage: number): number {
|
|
295
|
+
let total = 0;
|
|
296
|
+
for (const message of messages) {
|
|
297
|
+
total += estimateTokensForModel(message.content, tokensPerChar);
|
|
298
|
+
total += tokensPerMessage;
|
|
299
|
+
}
|
|
300
|
+
return total;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildSystemInstructions(params: {
|
|
304
|
+
assistantName: string;
|
|
305
|
+
groupNotes?: string | null;
|
|
306
|
+
globalNotes?: string | null;
|
|
307
|
+
memorySummary: string;
|
|
308
|
+
memoryFacts: string[];
|
|
309
|
+
sessionRecall: string[];
|
|
310
|
+
longTermRecall: string[];
|
|
311
|
+
userProfile?: string | null;
|
|
312
|
+
memoryStats?: { total: number; user: number; group: number; global: number };
|
|
313
|
+
availableGroups?: Array<{ jid: string; name: string; lastActivity: string; isRegistered: boolean }>;
|
|
314
|
+
toolReliability?: Array<{ name: string; success_rate: number; count: number; avg_duration_ms: number | null }>;
|
|
315
|
+
behaviorConfig?: Record<string, unknown>;
|
|
316
|
+
isScheduledTask: boolean;
|
|
317
|
+
isBackgroundTask: boolean;
|
|
318
|
+
taskId?: string;
|
|
319
|
+
planBlock?: string;
|
|
320
|
+
taskExtractionPack?: PromptPack | null;
|
|
321
|
+
responseQualityPack?: PromptPack | null;
|
|
322
|
+
toolCallingPack?: PromptPack | null;
|
|
323
|
+
toolOutcomePack?: PromptPack | null;
|
|
324
|
+
memoryPolicyPack?: PromptPack | null;
|
|
325
|
+
memoryRecallPack?: PromptPack | null;
|
|
326
|
+
}): string {
|
|
327
|
+
const toolsDoc = [
|
|
328
|
+
'Tools available (use with care):',
|
|
329
|
+
'- `Bash`: run shell commands in `/workspace/group`.',
|
|
330
|
+
'- `Read`, `Write`, `Edit`, `Glob`, `Grep`: filesystem operations within mounted paths.',
|
|
331
|
+
'- `WebSearch`: Brave Search API (requires `BRAVE_SEARCH_API_KEY`).',
|
|
332
|
+
'- `WebFetch`: fetch URLs (limit payload sizes).',
|
|
333
|
+
'- `GitClone`: clone git repositories into the workspace.',
|
|
334
|
+
'- `NpmInstall`: install npm dependencies in the workspace.',
|
|
335
|
+
'- `mcp__dotclaw__send_message`: send Telegram messages.',
|
|
336
|
+
'- `mcp__dotclaw__schedule_task`: schedule tasks.',
|
|
337
|
+
'- `mcp__dotclaw__list_tasks`, `mcp__dotclaw__pause_task`, `mcp__dotclaw__resume_task`, `mcp__dotclaw__cancel_task`.',
|
|
338
|
+
'- `mcp__dotclaw__update_task`: update a task (state, prompt, schedule, status).',
|
|
339
|
+
'- `mcp__dotclaw__register_group`: main group only.',
|
|
340
|
+
'- `mcp__dotclaw__remove_group`, `mcp__dotclaw__list_groups`: main group only.',
|
|
341
|
+
'- `mcp__dotclaw__set_model`: main group only.',
|
|
342
|
+
'- `mcp__dotclaw__memory_upsert`: store durable memories.',
|
|
343
|
+
'- `mcp__dotclaw__memory_search`, `mcp__dotclaw__memory_list`, `mcp__dotclaw__memory_forget`, `mcp__dotclaw__memory_stats`.',
|
|
344
|
+
'- `plugin__*`: dynamically loaded plugin tools (if present and allowed by policy).'
|
|
345
|
+
].join('\n');
|
|
346
|
+
const browserAutomation = [
|
|
347
|
+
'Browser automation (via Bash):',
|
|
348
|
+
'- Use `agent-browser open <url>` then `agent-browser snapshot -i`.',
|
|
349
|
+
'- Interact with refs using `agent-browser click @e1`, `fill @e2 "text"`.',
|
|
350
|
+
'- Capture evidence with `agent-browser screenshot`.'
|
|
351
|
+
].join('\n');
|
|
352
|
+
|
|
353
|
+
const memorySummary = params.memorySummary ? params.memorySummary : 'None yet.';
|
|
354
|
+
const memoryFacts = params.memoryFacts.length > 0
|
|
355
|
+
? params.memoryFacts.map(fact => `- ${fact}`).join('\n')
|
|
356
|
+
: 'None yet.';
|
|
357
|
+
const sessionRecall = params.sessionRecall.length > 0
|
|
358
|
+
? params.sessionRecall.map(item => `- ${item}`).join('\n')
|
|
359
|
+
: 'None.';
|
|
360
|
+
|
|
361
|
+
const longTermRecall = params.longTermRecall.length > 0
|
|
362
|
+
? params.longTermRecall.map(item => `- ${item}`).join('\n')
|
|
363
|
+
: 'None.';
|
|
364
|
+
|
|
365
|
+
const userProfile = params.userProfile
|
|
366
|
+
? params.userProfile
|
|
367
|
+
: 'None.';
|
|
368
|
+
|
|
369
|
+
const memoryStats = params.memoryStats
|
|
370
|
+
? `Total: ${params.memoryStats.total}, User: ${params.memoryStats.user}, Group: ${params.memoryStats.group}, Global: ${params.memoryStats.global}`
|
|
371
|
+
: 'Unknown.';
|
|
372
|
+
|
|
373
|
+
const availableGroups = params.availableGroups && params.availableGroups.length > 0
|
|
374
|
+
? params.availableGroups
|
|
375
|
+
.map(group => `- ${group.name} (chat ${group.jid}, last: ${group.lastActivity})`)
|
|
376
|
+
.join('\n')
|
|
377
|
+
: 'None.';
|
|
378
|
+
|
|
379
|
+
const groupNotes = params.groupNotes ? `Group notes:\n${params.groupNotes}` : '';
|
|
380
|
+
const globalNotes = params.globalNotes ? `Global notes:\n${params.globalNotes}` : '';
|
|
381
|
+
|
|
382
|
+
const toolReliability = params.toolReliability && params.toolReliability.length > 0
|
|
383
|
+
? params.toolReliability
|
|
384
|
+
.sort((a, b) => b.success_rate - a.success_rate)
|
|
385
|
+
.map(tool => {
|
|
386
|
+
const pct = `${Math.round(tool.success_rate * 100)}%`;
|
|
387
|
+
const avg = Number.isFinite(tool.avg_duration_ms) ? `${Math.round(tool.avg_duration_ms!)}ms` : 'n/a';
|
|
388
|
+
return `- ${tool.name}: success ${pct} over ${tool.count} calls (avg ${avg})`;
|
|
389
|
+
})
|
|
390
|
+
.join('\n')
|
|
391
|
+
: 'No recent tool reliability data.';
|
|
392
|
+
|
|
393
|
+
const behaviorNotes: string[] = [];
|
|
394
|
+
const responseStyle = typeof params.behaviorConfig?.response_style === 'string'
|
|
395
|
+
? String(params.behaviorConfig.response_style)
|
|
396
|
+
: '';
|
|
397
|
+
if (responseStyle === 'concise') {
|
|
398
|
+
behaviorNotes.push('Response style: concise and action-oriented.');
|
|
399
|
+
} else if (responseStyle === 'detailed') {
|
|
400
|
+
behaviorNotes.push('Response style: detailed and step-by-step where helpful.');
|
|
401
|
+
}
|
|
402
|
+
const toolBias = typeof params.behaviorConfig?.tool_calling_bias === 'number'
|
|
403
|
+
? Number(params.behaviorConfig.tool_calling_bias)
|
|
404
|
+
: null;
|
|
405
|
+
if (toolBias !== null && toolBias < 0.4) {
|
|
406
|
+
behaviorNotes.push('Tool usage: be conservative, ask clarifying questions before calling tools.');
|
|
407
|
+
} else if (toolBias !== null && toolBias > 0.6) {
|
|
408
|
+
behaviorNotes.push('Tool usage: be proactive when tools add accuracy or save time.');
|
|
409
|
+
}
|
|
410
|
+
const cautionBias = typeof params.behaviorConfig?.caution_bias === 'number'
|
|
411
|
+
? Number(params.behaviorConfig.caution_bias)
|
|
412
|
+
: null;
|
|
413
|
+
if (cautionBias !== null && cautionBias > 0.6) {
|
|
414
|
+
behaviorNotes.push('Caution: verify uncertain facts and flag limitations.');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const behaviorConfig = params.behaviorConfig
|
|
418
|
+
? `Behavior overrides:\n${JSON.stringify(params.behaviorConfig, null, 2)}`
|
|
419
|
+
: '';
|
|
420
|
+
|
|
421
|
+
const scheduledNote = params.isScheduledTask
|
|
422
|
+
? `You are running as a scheduled task${params.taskId ? ` (task id: ${params.taskId})` : ''}. If you need to communicate, use \`mcp__dotclaw__send_message\`.`
|
|
423
|
+
: '';
|
|
424
|
+
const backgroundNote = params.isBackgroundTask
|
|
425
|
+
? '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.'
|
|
426
|
+
: '';
|
|
427
|
+
|
|
428
|
+
const taskExtractionBlock = params.taskExtractionPack
|
|
429
|
+
? formatTaskExtractionPack({
|
|
430
|
+
pack: params.taskExtractionPack,
|
|
431
|
+
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
432
|
+
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
433
|
+
})
|
|
434
|
+
: '';
|
|
435
|
+
|
|
436
|
+
const responseQualityBlock = params.responseQualityPack
|
|
437
|
+
? formatResponseQualityPack({
|
|
438
|
+
pack: params.responseQualityPack,
|
|
439
|
+
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
440
|
+
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
441
|
+
})
|
|
442
|
+
: '';
|
|
443
|
+
|
|
444
|
+
const toolCallingBlock = params.toolCallingPack
|
|
445
|
+
? formatToolCallingPack({
|
|
446
|
+
pack: params.toolCallingPack,
|
|
447
|
+
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
448
|
+
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
449
|
+
})
|
|
450
|
+
: '';
|
|
451
|
+
|
|
452
|
+
const toolOutcomeBlock = params.toolOutcomePack
|
|
453
|
+
? formatToolOutcomePack({
|
|
454
|
+
pack: params.toolOutcomePack,
|
|
455
|
+
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
456
|
+
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
457
|
+
})
|
|
458
|
+
: '';
|
|
459
|
+
|
|
460
|
+
const memoryPolicyBlock = params.memoryPolicyPack
|
|
461
|
+
? formatMemoryPolicyPack({
|
|
462
|
+
pack: params.memoryPolicyPack,
|
|
463
|
+
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
464
|
+
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
465
|
+
})
|
|
466
|
+
: '';
|
|
467
|
+
|
|
468
|
+
const memoryRecallBlock = params.memoryRecallPack
|
|
469
|
+
? formatMemoryRecallPack({
|
|
470
|
+
pack: params.memoryRecallPack,
|
|
471
|
+
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
472
|
+
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
473
|
+
})
|
|
474
|
+
: '';
|
|
475
|
+
|
|
476
|
+
return [
|
|
477
|
+
`You are ${params.assistantName}, a personal assistant running inside DotClaw.`,
|
|
478
|
+
scheduledNote,
|
|
479
|
+
backgroundNote,
|
|
480
|
+
toolsDoc,
|
|
481
|
+
browserAutomation,
|
|
482
|
+
groupNotes,
|
|
483
|
+
globalNotes,
|
|
484
|
+
params.planBlock || '',
|
|
485
|
+
toolCallingBlock,
|
|
486
|
+
toolOutcomeBlock,
|
|
487
|
+
taskExtractionBlock,
|
|
488
|
+
responseQualityBlock,
|
|
489
|
+
memoryPolicyBlock,
|
|
490
|
+
memoryRecallBlock,
|
|
491
|
+
'Long-term memory summary:',
|
|
492
|
+
memorySummary,
|
|
493
|
+
'Long-term facts:',
|
|
494
|
+
memoryFacts,
|
|
495
|
+
'User profile (if available):',
|
|
496
|
+
userProfile,
|
|
497
|
+
'Long-term memory recall (durable facts/preferences):',
|
|
498
|
+
longTermRecall,
|
|
499
|
+
'Session recall (recent/older conversation snippets):',
|
|
500
|
+
sessionRecall,
|
|
501
|
+
'Memory stats:',
|
|
502
|
+
memoryStats,
|
|
503
|
+
'Available groups (main group only):',
|
|
504
|
+
availableGroups,
|
|
505
|
+
'Tool reliability (recent):',
|
|
506
|
+
toolReliability,
|
|
507
|
+
behaviorNotes.length > 0 ? `Behavior notes:\n${behaviorNotes.join('\n')}` : '',
|
|
508
|
+
behaviorConfig,
|
|
509
|
+
'Respond succinctly and helpfully. If you perform tool actions, summarize the results.'
|
|
510
|
+
].filter(Boolean).join('\n\n');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function loadAvailableGroups(): Array<{ jid: string; name: string; lastActivity: string; isRegistered: boolean }> {
|
|
514
|
+
try {
|
|
515
|
+
if (!fs.existsSync(AVAILABLE_GROUPS_PATH)) return [];
|
|
516
|
+
const raw = JSON.parse(fs.readFileSync(AVAILABLE_GROUPS_PATH, 'utf-8')) as {
|
|
517
|
+
groups?: Array<{ jid: string; name: string; lastActivity: string; isRegistered: boolean }>;
|
|
518
|
+
};
|
|
519
|
+
return Array.isArray(raw.groups) ? raw.groups.filter(group => group && typeof group.jid === 'string') : [];
|
|
520
|
+
} catch {
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function readTextFileLimited(filePath: string, maxChars: number): string | null {
|
|
526
|
+
try {
|
|
527
|
+
if (!fs.existsSync(filePath)) return null;
|
|
528
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
529
|
+
if (!content) return null;
|
|
530
|
+
if (content.length <= maxChars) return content;
|
|
531
|
+
return `${content.slice(0, maxChars)}\n\n[Truncated for length]`;
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function loadClaudeNotes(): { group: string | null; global: string | null } {
|
|
538
|
+
return {
|
|
539
|
+
group: readTextFileLimited(GROUP_CLAUDE_PATH, CLAUDE_NOTES_MAX_CHARS),
|
|
540
|
+
global: readTextFileLimited(GLOBAL_CLAUDE_PATH, CLAUDE_NOTES_MAX_CHARS)
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function extractQueryFromPrompt(prompt: string): string {
|
|
545
|
+
if (!prompt) return '';
|
|
546
|
+
const messageMatches = [...prompt.matchAll(/<message[^>]*>([\s\S]*?)<\/message>/g)];
|
|
547
|
+
if (messageMatches.length > 0) {
|
|
548
|
+
const last = messageMatches[messageMatches.length - 1][1];
|
|
549
|
+
return decodeXml(last).trim();
|
|
550
|
+
}
|
|
551
|
+
return prompt.trim();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function decodeXml(value: string): string {
|
|
555
|
+
return value
|
|
556
|
+
.replace(/</g, '<')
|
|
557
|
+
.replace(/>/g, '>')
|
|
558
|
+
.replace(/"/g, '"')
|
|
559
|
+
.replace(/&/g, '&');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function messagesToOpenRouter(messages: Message[]) {
|
|
563
|
+
return messages.map(message => ({
|
|
564
|
+
role: message.role,
|
|
565
|
+
content: message.content
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function clampContextMessages(messages: Message[], tokensPerChar: number, maxTokens: number): Message[] {
|
|
570
|
+
if (!Number.isFinite(maxTokens) || maxTokens <= 0) return messages;
|
|
571
|
+
const tpc = tokensPerChar > 0 ? tokensPerChar : 0.25;
|
|
572
|
+
const maxBytes = Math.max(200, Math.floor(maxTokens / tpc));
|
|
573
|
+
const suffix = '\n\n[Context truncated for length]';
|
|
574
|
+
const suffixBytes = Buffer.byteLength(suffix, 'utf-8');
|
|
575
|
+
return messages.map(message => {
|
|
576
|
+
const contentBytes = Buffer.byteLength(message.content, 'utf-8');
|
|
577
|
+
if (contentBytes <= maxBytes) return message;
|
|
578
|
+
const budget = Math.max(0, maxBytes - suffixBytes);
|
|
579
|
+
const truncated = Buffer.from(message.content, 'utf-8')
|
|
580
|
+
.subarray(0, budget)
|
|
581
|
+
.toString('utf-8');
|
|
582
|
+
return { ...message, content: `${truncated}${suffix}` };
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function updateMemorySummary(params: {
|
|
587
|
+
openrouter: OpenRouter;
|
|
588
|
+
model: string;
|
|
589
|
+
existingSummary: string;
|
|
590
|
+
existingFacts: string[];
|
|
591
|
+
newMessages: Message[];
|
|
592
|
+
maxOutputTokens: number;
|
|
593
|
+
}): Promise<{ summary: string; facts: string[] } | null> {
|
|
594
|
+
if (params.newMessages.length === 0) return null;
|
|
595
|
+
const prompt = buildSummaryPrompt(params.existingSummary, params.existingFacts, params.newMessages);
|
|
596
|
+
const result = await params.openrouter.callModel({
|
|
597
|
+
model: params.model,
|
|
598
|
+
instructions: prompt.instructions,
|
|
599
|
+
input: prompt.input,
|
|
600
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
601
|
+
temperature: 0.1
|
|
602
|
+
});
|
|
603
|
+
const text = await result.getText();
|
|
604
|
+
return parseSummaryResponse(text);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function buildMemoryExtractionPrompt(params: {
|
|
608
|
+
assistantName: string;
|
|
609
|
+
userId?: string;
|
|
610
|
+
userName?: string;
|
|
611
|
+
messages: Message[];
|
|
612
|
+
memoryPolicyPack?: PromptPack | null;
|
|
613
|
+
}): { instructions: string; input: string } {
|
|
614
|
+
const policyBlock = params.memoryPolicyPack
|
|
615
|
+
? formatMemoryPolicyPack({
|
|
616
|
+
pack: params.memoryPolicyPack,
|
|
617
|
+
maxDemos: PROMPT_PACKS_MAX_DEMOS,
|
|
618
|
+
maxChars: PROMPT_PACKS_MAX_CHARS
|
|
619
|
+
})
|
|
620
|
+
: '';
|
|
621
|
+
|
|
622
|
+
const instructions = [
|
|
623
|
+
`You are ${params.assistantName}'s long-term memory extractor.`,
|
|
624
|
+
'Extract durable, user-approved memories only.',
|
|
625
|
+
'Prefer stable facts, preferences, identity details, projects, and long-running tasks.',
|
|
626
|
+
'Avoid transient details, ephemeral scheduling, or speculative statements.',
|
|
627
|
+
'If the user explicitly asked to remember something, include it.',
|
|
628
|
+
'Return JSON only with key "items": array of memory objects.',
|
|
629
|
+
'Each item fields:',
|
|
630
|
+
'- scope: "user" | "group" | "global"',
|
|
631
|
+
'- subject_id: user id for user scope (optional for group/global)',
|
|
632
|
+
'- type: "identity" | "preference" | "fact" | "relationship" | "project" | "task" | "note"',
|
|
633
|
+
'- kind: optional "semantic" | "episodic" | "procedural" | "preference"',
|
|
634
|
+
'- conflict_key: optional string to replace older memories with same key (e.g., "favorite_color")',
|
|
635
|
+
'- content: the memory string',
|
|
636
|
+
'- importance: 0-1 (higher = more important)',
|
|
637
|
+
'- confidence: 0-1',
|
|
638
|
+
'- tags: array of short tags',
|
|
639
|
+
'- ttl_days: optional number (omit for permanent memories).',
|
|
640
|
+
'- For preferences about response style, tool usage, caution, or memory strictness, use conflict_key:',
|
|
641
|
+
' response_style, tool_calling_bias, caution_bias, memory_importance_threshold.',
|
|
642
|
+
' Include metadata fields for these preferences where possible, e.g.',
|
|
643
|
+
' { "metadata": { "response_style": "concise" } } or { "metadata": { "bias": 0.7 } }.',
|
|
644
|
+
policyBlock
|
|
645
|
+
].filter(Boolean).join('\n');
|
|
646
|
+
|
|
647
|
+
const transcript = params.messages
|
|
648
|
+
.map(msg => `${msg.role.toUpperCase()}: ${msg.content}`)
|
|
649
|
+
.join('\n\n');
|
|
650
|
+
|
|
651
|
+
const input = [
|
|
652
|
+
`User: ${params.userName || 'Unknown'} (${params.userId || 'unknown'})`,
|
|
653
|
+
'Transcript:',
|
|
654
|
+
transcript
|
|
655
|
+
].join('\n\n');
|
|
656
|
+
|
|
657
|
+
return { instructions, input };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function parseMemoryExtraction(text: string): Array<Record<string, unknown>> {
|
|
661
|
+
const trimmed = text.trim();
|
|
662
|
+
let jsonText = trimmed;
|
|
663
|
+
const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
664
|
+
if (fenceMatch) {
|
|
665
|
+
jsonText = fenceMatch[1].trim();
|
|
666
|
+
}
|
|
667
|
+
try {
|
|
668
|
+
const parsed = JSON.parse(jsonText);
|
|
669
|
+
const items = Array.isArray(parsed?.items) ? parsed.items : [];
|
|
670
|
+
return items.filter((item: unknown) => !!item && typeof item === 'object');
|
|
671
|
+
} catch {
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
type ResponseValidation = {
|
|
677
|
+
verdict: 'pass' | 'fail';
|
|
678
|
+
issues: string[];
|
|
679
|
+
missing: string[];
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
function buildResponseValidationPrompt(params: { userPrompt: string; response: string }): { instructions: string; input: string } {
|
|
683
|
+
const instructions = [
|
|
684
|
+
'You are a strict response quality checker.',
|
|
685
|
+
'Given a user request and an assistant response, decide if the response fully addresses the request.',
|
|
686
|
+
'Fail if the response is empty, generic, deflects, promises work without results, or ignores any explicit questions.',
|
|
687
|
+
'Pass only if the response directly answers all parts with concrete, relevant content.',
|
|
688
|
+
'Return JSON only with keys: verdict ("pass"|"fail"), issues (array of strings), missing (array of strings).'
|
|
689
|
+
].join('\n');
|
|
690
|
+
|
|
691
|
+
const input = [
|
|
692
|
+
'User request:',
|
|
693
|
+
params.userPrompt,
|
|
694
|
+
'',
|
|
695
|
+
'Assistant response:',
|
|
696
|
+
params.response
|
|
697
|
+
].join('\n');
|
|
698
|
+
|
|
699
|
+
return { instructions, input };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function parseResponseValidation(text: string): ResponseValidation | null {
|
|
703
|
+
const trimmed = text.trim();
|
|
704
|
+
let jsonText = trimmed;
|
|
705
|
+
const fenceMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
706
|
+
if (fenceMatch) {
|
|
707
|
+
jsonText = fenceMatch[1].trim();
|
|
708
|
+
}
|
|
709
|
+
try {
|
|
710
|
+
const parsed = JSON.parse(jsonText);
|
|
711
|
+
const verdict = parsed?.verdict;
|
|
712
|
+
if (verdict !== 'pass' && verdict !== 'fail') return null;
|
|
713
|
+
const issues = Array.isArray(parsed?.issues)
|
|
714
|
+
? parsed.issues.filter((issue: unknown) => typeof issue === 'string')
|
|
715
|
+
: [];
|
|
716
|
+
const missing = Array.isArray(parsed?.missing)
|
|
717
|
+
? parsed.missing.filter((item: unknown) => typeof item === 'string')
|
|
718
|
+
: [];
|
|
719
|
+
return { verdict, issues, missing };
|
|
720
|
+
} catch {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function validateResponseQuality(params: {
|
|
726
|
+
openrouter: OpenRouter;
|
|
727
|
+
model: string;
|
|
728
|
+
userPrompt: string;
|
|
729
|
+
response: string;
|
|
730
|
+
maxOutputTokens: number;
|
|
731
|
+
temperature: number;
|
|
732
|
+
}): Promise<ResponseValidation | null> {
|
|
733
|
+
const prompt = buildResponseValidationPrompt({
|
|
734
|
+
userPrompt: params.userPrompt,
|
|
735
|
+
response: params.response
|
|
736
|
+
});
|
|
737
|
+
const result = await params.openrouter.callModel({
|
|
738
|
+
model: params.model,
|
|
739
|
+
instructions: prompt.instructions,
|
|
740
|
+
input: prompt.input,
|
|
741
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
742
|
+
temperature: params.temperature
|
|
743
|
+
});
|
|
744
|
+
const text = await result.getText();
|
|
745
|
+
return parseResponseValidation(text);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function buildRetryGuidance(validation: ResponseValidation | null): string {
|
|
749
|
+
const issues = validation?.issues || [];
|
|
750
|
+
const missing = validation?.missing || [];
|
|
751
|
+
const points = [...issues, ...missing].filter(Boolean).slice(0, 8);
|
|
752
|
+
const details = points.length > 0
|
|
753
|
+
? points.map(item => `- ${item}`).join('\n')
|
|
754
|
+
: '- The previous response did not fully address the request.';
|
|
755
|
+
return [
|
|
756
|
+
'IMPORTANT: Your previous response did not fully answer the user request.',
|
|
757
|
+
'Provide a direct, complete answer now. Do not mention this retry.',
|
|
758
|
+
'Issues to fix:',
|
|
759
|
+
details
|
|
760
|
+
].join('\n');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function buildPlannerTrigger(pattern: string | undefined): RegExp | null {
|
|
764
|
+
if (!pattern) return null;
|
|
765
|
+
try {
|
|
766
|
+
return new RegExp(pattern, 'i');
|
|
767
|
+
} catch {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function shouldRunPlanner(params: {
|
|
773
|
+
enabled: boolean;
|
|
774
|
+
mode: string;
|
|
775
|
+
prompt: string;
|
|
776
|
+
tokensPerChar: number;
|
|
777
|
+
minTokens: number;
|
|
778
|
+
trigger: RegExp | null;
|
|
779
|
+
}): boolean {
|
|
780
|
+
if (!params.enabled) return false;
|
|
781
|
+
const mode = params.mode.toLowerCase();
|
|
782
|
+
if (mode === 'always') return true;
|
|
783
|
+
if (mode === 'off') return false;
|
|
784
|
+
|
|
785
|
+
const estimatedTokens = estimateTokensForModel(params.prompt, params.tokensPerChar);
|
|
786
|
+
if (params.minTokens > 0 && estimatedTokens >= params.minTokens) return true;
|
|
787
|
+
if (params.trigger && params.trigger.test(params.prompt)) return true;
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutput> {
|
|
792
|
+
log(`Received input for group: ${input.groupFolder}`);
|
|
793
|
+
|
|
794
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
795
|
+
if (!apiKey) {
|
|
796
|
+
return {
|
|
797
|
+
status: 'error',
|
|
798
|
+
result: null,
|
|
799
|
+
error: 'OPENROUTER_API_KEY is not set'
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const model = input.modelOverride || agentConfig.defaultModel;
|
|
804
|
+
const summaryModel = agent.models.summary;
|
|
805
|
+
const memoryModel = agent.models.memory;
|
|
806
|
+
const assistantName = agent.assistantName;
|
|
807
|
+
const config = getConfig(agentConfig);
|
|
808
|
+
if (input.modelContextTokens && Number.isFinite(input.modelContextTokens)) {
|
|
809
|
+
config.maxContextTokens = Math.min(config.maxContextTokens, input.modelContextTokens);
|
|
810
|
+
const compactionTarget = input.modelContextTokens - config.maxOutputTokens;
|
|
811
|
+
config.compactionTriggerTokens = Math.max(1000, Math.min(config.compactionTriggerTokens, compactionTarget));
|
|
812
|
+
}
|
|
813
|
+
if (input.modelMaxOutputTokens && Number.isFinite(input.modelMaxOutputTokens)) {
|
|
814
|
+
config.maxOutputTokens = Math.min(config.maxOutputTokens, input.modelMaxOutputTokens);
|
|
815
|
+
}
|
|
816
|
+
if (input.modelTemperature && Number.isFinite(input.modelTemperature)) {
|
|
817
|
+
config.temperature = input.modelTemperature;
|
|
818
|
+
}
|
|
819
|
+
const openrouterOptions = getOpenRouterOptions(agentConfig);
|
|
820
|
+
const maxToolSteps = agent.tools.maxToolSteps;
|
|
821
|
+
const memoryExtractionEnabled = agent.memory.extraction.enabled;
|
|
822
|
+
const isDaemon = process.env.DOTCLAW_DAEMON === '1';
|
|
823
|
+
const memoryExtractionAsync = agent.memory.extraction.async;
|
|
824
|
+
const memoryExtractionMaxMessages = agent.memory.extraction.maxMessages;
|
|
825
|
+
const memoryExtractionMaxOutputTokens = agent.memory.extraction.maxOutputTokens;
|
|
826
|
+
const memoryExtractScheduled = agent.memory.extractScheduled;
|
|
827
|
+
const memoryArchiveSync = agent.memory.archiveSync;
|
|
828
|
+
const plannerEnabled = agent.planner.enabled;
|
|
829
|
+
const plannerMode = String(agent.planner.mode || 'auto').toLowerCase();
|
|
830
|
+
const plannerMinTokens = agent.planner.minTokens;
|
|
831
|
+
const plannerTrigger = buildPlannerTrigger(agent.planner.triggerRegex);
|
|
832
|
+
const plannerModel = agent.models.planner;
|
|
833
|
+
const plannerMaxOutputTokens = agent.planner.maxOutputTokens;
|
|
834
|
+
const plannerTemperature = agent.planner.temperature;
|
|
835
|
+
const responseValidateEnabled = agent.responseValidation.enabled;
|
|
836
|
+
const responseValidateModel = agent.models.responseValidation;
|
|
837
|
+
const responseValidateMaxOutputTokens = agent.responseValidation.maxOutputTokens;
|
|
838
|
+
const responseValidateTemperature = agent.responseValidation.temperature;
|
|
839
|
+
const responseValidateMaxRetries = agent.responseValidation.maxRetries;
|
|
840
|
+
const responseValidateAllowToolCalls = agent.responseValidation.allowToolCalls;
|
|
841
|
+
const maxContextMessageTokens = agent.context.maxContextMessageTokens;
|
|
842
|
+
const streamingEnabled = Boolean(input.streaming?.enabled && typeof input.streaming?.draftId === 'number');
|
|
843
|
+
const streamingDraftId = streamingEnabled ? input.streaming?.draftId : undefined;
|
|
844
|
+
const streamingMinIntervalMs = Math.max(
|
|
845
|
+
0,
|
|
846
|
+
Math.floor(
|
|
847
|
+
typeof input.streaming?.minIntervalMs === 'number'
|
|
848
|
+
? input.streaming.minIntervalMs
|
|
849
|
+
: agent.streaming.minIntervalMs
|
|
850
|
+
)
|
|
851
|
+
);
|
|
852
|
+
const streamingMinChars = Math.max(
|
|
853
|
+
1,
|
|
854
|
+
Math.floor(
|
|
855
|
+
typeof input.streaming?.minChars === 'number'
|
|
856
|
+
? input.streaming.minChars
|
|
857
|
+
: agent.streaming.minChars
|
|
858
|
+
)
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
const openrouter = getCachedOpenRouter(apiKey, openrouterOptions);
|
|
862
|
+
const tokenEstimate = resolveTokenEstimate(input, agentConfig);
|
|
863
|
+
const availableGroups = loadAvailableGroups();
|
|
864
|
+
const claudeNotes = loadClaudeNotes();
|
|
865
|
+
|
|
866
|
+
const { ctx: sessionCtx, isNew } = createSessionContext(SESSION_ROOT, input.sessionId);
|
|
867
|
+
const toolCalls: ToolCallRecord[] = [];
|
|
868
|
+
let memoryItemsUpserted = 0;
|
|
869
|
+
let memoryItemsExtracted = 0;
|
|
870
|
+
const ipc = createIpcHandlers({
|
|
871
|
+
chatJid: input.chatJid,
|
|
872
|
+
groupFolder: input.groupFolder,
|
|
873
|
+
isMain: input.isMain
|
|
874
|
+
}, agent.ipc);
|
|
875
|
+
const tools = createTools({
|
|
876
|
+
chatJid: input.chatJid,
|
|
877
|
+
groupFolder: input.groupFolder,
|
|
878
|
+
isMain: input.isMain
|
|
879
|
+
}, agent, {
|
|
880
|
+
onToolCall: (call) => {
|
|
881
|
+
toolCalls.push(call);
|
|
882
|
+
},
|
|
883
|
+
policy: input.toolPolicy
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
let streamLastSentAt = 0;
|
|
887
|
+
let streamLastSentLength = 0;
|
|
888
|
+
const sendStreamUpdate = (text: string, force = false) => {
|
|
889
|
+
if (!streamingEnabled || !streamingDraftId) return;
|
|
890
|
+
if (!text || !text.trim()) return;
|
|
891
|
+
const now = Date.now();
|
|
892
|
+
if (!force) {
|
|
893
|
+
if (now - streamLastSentAt < streamingMinIntervalMs) return;
|
|
894
|
+
if (text.length - streamLastSentLength < streamingMinChars) return;
|
|
895
|
+
}
|
|
896
|
+
streamLastSentAt = now;
|
|
897
|
+
streamLastSentLength = text.length;
|
|
898
|
+
void ipc.sendDraft(text, streamingDraftId).catch(() => undefined);
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
if (process.env.DOTCLAW_SELF_CHECK === '1') {
|
|
902
|
+
try {
|
|
903
|
+
const details = await runSelfCheck({ model });
|
|
904
|
+
return {
|
|
905
|
+
status: 'success',
|
|
906
|
+
result: `Self-check passed: ${details.join(', ')}`,
|
|
907
|
+
newSessionId: isNew ? sessionCtx.sessionId : undefined
|
|
908
|
+
};
|
|
909
|
+
} catch (err) {
|
|
910
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
911
|
+
log(`Self-check failed: ${errorMessage}`);
|
|
912
|
+
return {
|
|
913
|
+
status: 'error',
|
|
914
|
+
result: null,
|
|
915
|
+
newSessionId: isNew ? sessionCtx.sessionId : undefined,
|
|
916
|
+
error: errorMessage
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
let prompt = input.prompt;
|
|
922
|
+
if (input.isScheduledTask) {
|
|
923
|
+
prompt = `[SCHEDULED TASK - You are running automatically, not in response to a user message. Use mcp__dotclaw__send_message if needed to communicate with the user.]\n\n${input.prompt}`;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
appendHistory(sessionCtx, 'user', prompt);
|
|
927
|
+
let history = loadHistory(sessionCtx);
|
|
928
|
+
|
|
929
|
+
const tokenRatio = tokenEstimate.tokensPerChar > 0 ? (0.25 / tokenEstimate.tokensPerChar) : 1;
|
|
930
|
+
const adjustedRecentTokens = Math.max(1000, Math.floor(config.recentContextTokens * tokenRatio));
|
|
931
|
+
|
|
932
|
+
const totalTokens = history.reduce(
|
|
933
|
+
(sum, message) => sum + estimateTokensForModel(message.content, tokenEstimate.tokensPerChar) + tokenEstimate.tokensPerMessage,
|
|
934
|
+
0
|
|
935
|
+
);
|
|
936
|
+
let { recentMessages, olderMessages } = splitRecentHistory(history, adjustedRecentTokens);
|
|
937
|
+
|
|
938
|
+
if (shouldCompact(totalTokens, config)) {
|
|
939
|
+
log(`Compacting history: ${totalTokens} tokens`);
|
|
940
|
+
archiveConversation(history, sessionCtx.state.summary || null, GROUP_DIR);
|
|
941
|
+
|
|
942
|
+
const summaryUpdate = await updateMemorySummary({
|
|
943
|
+
openrouter,
|
|
944
|
+
model: summaryModel,
|
|
945
|
+
existingSummary: sessionCtx.state.summary,
|
|
946
|
+
existingFacts: sessionCtx.state.facts,
|
|
947
|
+
newMessages: olderMessages,
|
|
948
|
+
maxOutputTokens: config.summaryMaxOutputTokens
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
if (summaryUpdate) {
|
|
952
|
+
sessionCtx.state.summary = summaryUpdate.summary;
|
|
953
|
+
sessionCtx.state.facts = summaryUpdate.facts;
|
|
954
|
+
sessionCtx.state.lastSummarySeq = olderMessages.length > 0
|
|
955
|
+
? olderMessages[olderMessages.length - 1].seq
|
|
956
|
+
: sessionCtx.state.lastSummarySeq;
|
|
957
|
+
saveMemoryState(sessionCtx);
|
|
958
|
+
|
|
959
|
+
if (memoryArchiveSync) {
|
|
960
|
+
try {
|
|
961
|
+
const archiveItems: Array<Record<string, unknown>> = [];
|
|
962
|
+
if (summaryUpdate.summary) {
|
|
963
|
+
archiveItems.push({
|
|
964
|
+
scope: 'group',
|
|
965
|
+
type: 'archive',
|
|
966
|
+
content: `Conversation summary: ${summaryUpdate.summary}`,
|
|
967
|
+
importance: 0.6,
|
|
968
|
+
confidence: 0.7,
|
|
969
|
+
tags: ['summary', 'archive']
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
for (const fact of summaryUpdate.facts || []) {
|
|
973
|
+
if (!fact || typeof fact !== 'string') continue;
|
|
974
|
+
archiveItems.push({
|
|
975
|
+
scope: 'group',
|
|
976
|
+
type: 'fact',
|
|
977
|
+
content: fact,
|
|
978
|
+
importance: 0.7,
|
|
979
|
+
confidence: 0.7,
|
|
980
|
+
tags: ['fact', 'archive']
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
if (archiveItems.length > 0) {
|
|
984
|
+
await ipc.memoryUpsert({ items: archiveItems, source: 'compaction' });
|
|
985
|
+
memoryItemsUpserted += archiveItems.length;
|
|
986
|
+
}
|
|
987
|
+
} catch (err) {
|
|
988
|
+
log(`Memory archive sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
writeHistory(sessionCtx, recentMessages);
|
|
994
|
+
history = recentMessages;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Recompute split after possible compaction
|
|
998
|
+
({ recentMessages, olderMessages } = splitRecentHistory(history, adjustedRecentTokens));
|
|
999
|
+
|
|
1000
|
+
const query = extractQueryFromPrompt(prompt);
|
|
1001
|
+
const sessionRecall = retrieveRelevantMemories({
|
|
1002
|
+
query,
|
|
1003
|
+
summary: sessionCtx.state.summary,
|
|
1004
|
+
facts: sessionCtx.state.facts,
|
|
1005
|
+
olderMessages,
|
|
1006
|
+
config
|
|
1007
|
+
});
|
|
1008
|
+
const sessionRecallCount = sessionRecall.length;
|
|
1009
|
+
const memoryRecallCount = input.memoryRecall ? input.memoryRecall.length : 0;
|
|
1010
|
+
|
|
1011
|
+
const sharedPromptDir = fs.existsSync(PROMPTS_DIR) ? PROMPTS_DIR : undefined;
|
|
1012
|
+
const taskPackResult = PROMPT_PACKS_ENABLED
|
|
1013
|
+
? loadPromptPackWithCanary({ behavior: 'task-extraction', groupDir: GROUP_DIR, globalDir: GLOBAL_DIR, sharedDir: sharedPromptDir, canaryRate: PROMPT_PACKS_CANARY_RATE })
|
|
1014
|
+
: null;
|
|
1015
|
+
const responseQualityResult = PROMPT_PACKS_ENABLED
|
|
1016
|
+
? loadPromptPackWithCanary({ behavior: 'response-quality', groupDir: GROUP_DIR, globalDir: GLOBAL_DIR, sharedDir: sharedPromptDir, canaryRate: PROMPT_PACKS_CANARY_RATE })
|
|
1017
|
+
: null;
|
|
1018
|
+
const toolCallingResult = PROMPT_PACKS_ENABLED
|
|
1019
|
+
? loadPromptPackWithCanary({ behavior: 'tool-calling', groupDir: GROUP_DIR, globalDir: GLOBAL_DIR, sharedDir: sharedPromptDir, canaryRate: PROMPT_PACKS_CANARY_RATE })
|
|
1020
|
+
: null;
|
|
1021
|
+
const toolOutcomeResult = PROMPT_PACKS_ENABLED
|
|
1022
|
+
? loadPromptPackWithCanary({ behavior: 'tool-outcome', groupDir: GROUP_DIR, globalDir: GLOBAL_DIR, sharedDir: sharedPromptDir, canaryRate: PROMPT_PACKS_CANARY_RATE })
|
|
1023
|
+
: null;
|
|
1024
|
+
const memoryPolicyResult = PROMPT_PACKS_ENABLED
|
|
1025
|
+
? loadPromptPackWithCanary({ behavior: 'memory-policy', groupDir: GROUP_DIR, globalDir: GLOBAL_DIR, sharedDir: sharedPromptDir, canaryRate: PROMPT_PACKS_CANARY_RATE })
|
|
1026
|
+
: null;
|
|
1027
|
+
const memoryRecallResult = PROMPT_PACKS_ENABLED
|
|
1028
|
+
? loadPromptPackWithCanary({ behavior: 'memory-recall', groupDir: GROUP_DIR, globalDir: GLOBAL_DIR, sharedDir: sharedPromptDir, canaryRate: PROMPT_PACKS_CANARY_RATE })
|
|
1029
|
+
: null;
|
|
1030
|
+
|
|
1031
|
+
const logPack = (label: string, result: { pack: PromptPack; source: string; isCanary?: boolean } | null) => {
|
|
1032
|
+
if (!result) return;
|
|
1033
|
+
const canaryNote = result.isCanary ? ' (canary)' : '';
|
|
1034
|
+
log(`Loaded prompt pack (${label}${canaryNote}): ${result.pack.name}@${result.pack.version}`);
|
|
1035
|
+
};
|
|
1036
|
+
logPack(taskPackResult?.source || 'unknown', taskPackResult);
|
|
1037
|
+
logPack(responseQualityResult?.source || 'unknown', responseQualityResult);
|
|
1038
|
+
logPack(toolCallingResult?.source || 'unknown', toolCallingResult);
|
|
1039
|
+
logPack(toolOutcomeResult?.source || 'unknown', toolOutcomeResult);
|
|
1040
|
+
logPack(memoryPolicyResult?.source || 'unknown', memoryPolicyResult);
|
|
1041
|
+
logPack(memoryRecallResult?.source || 'unknown', memoryRecallResult);
|
|
1042
|
+
|
|
1043
|
+
const promptPackVersions: Record<string, string> = {};
|
|
1044
|
+
if (taskPackResult) promptPackVersions['task-extraction'] = taskPackResult.pack.version;
|
|
1045
|
+
if (responseQualityResult) promptPackVersions['response-quality'] = responseQualityResult.pack.version;
|
|
1046
|
+
if (toolCallingResult) promptPackVersions['tool-calling'] = toolCallingResult.pack.version;
|
|
1047
|
+
if (toolOutcomeResult) promptPackVersions['tool-outcome'] = toolOutcomeResult.pack.version;
|
|
1048
|
+
if (memoryPolicyResult) promptPackVersions['memory-policy'] = memoryPolicyResult.pack.version;
|
|
1049
|
+
if (memoryRecallResult) promptPackVersions['memory-recall'] = memoryRecallResult.pack.version;
|
|
1050
|
+
|
|
1051
|
+
const buildInstructions = (planBlockValue: string) => buildSystemInstructions({
|
|
1052
|
+
assistantName,
|
|
1053
|
+
groupNotes: claudeNotes.group,
|
|
1054
|
+
globalNotes: claudeNotes.global,
|
|
1055
|
+
memorySummary: sessionCtx.state.summary,
|
|
1056
|
+
memoryFacts: sessionCtx.state.facts,
|
|
1057
|
+
sessionRecall,
|
|
1058
|
+
longTermRecall: input.memoryRecall || [],
|
|
1059
|
+
userProfile: input.userProfile ?? null,
|
|
1060
|
+
memoryStats: input.memoryStats,
|
|
1061
|
+
availableGroups,
|
|
1062
|
+
toolReliability: input.toolReliability,
|
|
1063
|
+
behaviorConfig: input.behaviorConfig,
|
|
1064
|
+
isScheduledTask: !!input.isScheduledTask,
|
|
1065
|
+
isBackgroundTask: !!input.isBackgroundTask,
|
|
1066
|
+
taskId: input.taskId,
|
|
1067
|
+
planBlock: planBlockValue,
|
|
1068
|
+
taskExtractionPack: taskPackResult?.pack || null,
|
|
1069
|
+
responseQualityPack: responseQualityResult?.pack || null,
|
|
1070
|
+
toolCallingPack: toolCallingResult?.pack || null,
|
|
1071
|
+
toolOutcomePack: toolOutcomeResult?.pack || null,
|
|
1072
|
+
memoryPolicyPack: memoryPolicyResult?.pack || null,
|
|
1073
|
+
memoryRecallPack: memoryRecallResult?.pack || null
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
let planBlock = '';
|
|
1077
|
+
let instructions = buildInstructions(planBlock);
|
|
1078
|
+
let instructionsTokens = estimateTokensForModel(instructions, tokenEstimate.tokensPerChar);
|
|
1079
|
+
let maxContextTokens = Math.max(config.maxContextTokens - config.maxOutputTokens - instructionsTokens, 2000);
|
|
1080
|
+
let adjustedContextTokens = Math.max(1000, Math.floor(maxContextTokens * tokenRatio));
|
|
1081
|
+
let { recentMessages: plannerContextMessages } = splitRecentHistory(recentMessages, adjustedContextTokens, 6);
|
|
1082
|
+
plannerContextMessages = clampContextMessages(plannerContextMessages, tokenEstimate.tokensPerChar, maxContextMessageTokens);
|
|
1083
|
+
|
|
1084
|
+
if (shouldRunPlanner({
|
|
1085
|
+
enabled: plannerEnabled,
|
|
1086
|
+
mode: plannerMode,
|
|
1087
|
+
prompt,
|
|
1088
|
+
tokensPerChar: tokenEstimate.tokensPerChar,
|
|
1089
|
+
minTokens: plannerMinTokens,
|
|
1090
|
+
trigger: plannerTrigger
|
|
1091
|
+
})) {
|
|
1092
|
+
try {
|
|
1093
|
+
const plannerPrompt = buildPlannerPrompt(plannerContextMessages);
|
|
1094
|
+
const plannerResult = await openrouter.callModel({
|
|
1095
|
+
model: plannerModel,
|
|
1096
|
+
instructions: plannerPrompt.instructions,
|
|
1097
|
+
input: plannerPrompt.input,
|
|
1098
|
+
maxOutputTokens: plannerMaxOutputTokens,
|
|
1099
|
+
temperature: plannerTemperature
|
|
1100
|
+
});
|
|
1101
|
+
const plannerText = await plannerResult.getText();
|
|
1102
|
+
const plan = parsePlannerResponse(plannerText);
|
|
1103
|
+
if (plan) {
|
|
1104
|
+
planBlock = formatPlanBlock(plan);
|
|
1105
|
+
}
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
log(`Planner failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (planBlock) {
|
|
1112
|
+
instructions = buildInstructions(planBlock);
|
|
1113
|
+
instructionsTokens = estimateTokensForModel(instructions, tokenEstimate.tokensPerChar);
|
|
1114
|
+
maxContextTokens = Math.max(config.maxContextTokens - config.maxOutputTokens - instructionsTokens, 2000);
|
|
1115
|
+
adjustedContextTokens = Math.max(1000, Math.floor(maxContextTokens * tokenRatio));
|
|
1116
|
+
({ recentMessages: plannerContextMessages } = splitRecentHistory(recentMessages, adjustedContextTokens, 6));
|
|
1117
|
+
plannerContextMessages = clampContextMessages(plannerContextMessages, tokenEstimate.tokensPerChar, maxContextMessageTokens);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const buildContext = (extraInstruction?: string) => {
|
|
1121
|
+
let resolvedInstructions = buildInstructions(planBlock);
|
|
1122
|
+
if (extraInstruction) {
|
|
1123
|
+
resolvedInstructions = `${resolvedInstructions}\n\n${extraInstruction}`;
|
|
1124
|
+
}
|
|
1125
|
+
const resolvedInstructionTokens = estimateTokensForModel(resolvedInstructions, tokenEstimate.tokensPerChar);
|
|
1126
|
+
const resolvedMaxContext = Math.max(config.maxContextTokens - config.maxOutputTokens - resolvedInstructionTokens, 2000);
|
|
1127
|
+
const resolvedAdjusted = Math.max(1000, Math.floor(resolvedMaxContext * tokenRatio));
|
|
1128
|
+
let { recentMessages: contextMessages } = splitRecentHistory(recentMessages, resolvedAdjusted, 6);
|
|
1129
|
+
contextMessages = clampContextMessages(contextMessages, tokenEstimate.tokensPerChar, maxContextMessageTokens);
|
|
1130
|
+
return {
|
|
1131
|
+
instructions: resolvedInstructions,
|
|
1132
|
+
instructionsTokens: resolvedInstructionTokens,
|
|
1133
|
+
contextMessages
|
|
1134
|
+
};
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
let responseText = '';
|
|
1138
|
+
let completionTokens = 0;
|
|
1139
|
+
let promptTokens = 0;
|
|
1140
|
+
let modelToolCalls: Array<{ name: string }> = [];
|
|
1141
|
+
|
|
1142
|
+
let latencyMs: number | undefined;
|
|
1143
|
+
const runCompletion = async (extraInstruction?: string): Promise<{
|
|
1144
|
+
responseText: string;
|
|
1145
|
+
completionTokens: number;
|
|
1146
|
+
promptTokens: number;
|
|
1147
|
+
latencyMs?: number;
|
|
1148
|
+
modelToolCalls: Array<{ name: string }>;
|
|
1149
|
+
}> => {
|
|
1150
|
+
const { instructions: resolvedInstructions, instructionsTokens: resolvedInstructionTokens, contextMessages } = buildContext(extraInstruction);
|
|
1151
|
+
const resolvedPromptTokens = resolvedInstructionTokens
|
|
1152
|
+
+ estimateMessagesTokens(contextMessages, tokenEstimate.tokensPerChar, tokenEstimate.tokensPerMessage)
|
|
1153
|
+
+ tokenEstimate.tokensPerRequest;
|
|
1154
|
+
|
|
1155
|
+
log('Starting OpenRouter call...');
|
|
1156
|
+
const startedAt = Date.now();
|
|
1157
|
+
const callParams = {
|
|
1158
|
+
model,
|
|
1159
|
+
instructions: resolvedInstructions,
|
|
1160
|
+
input: messagesToOpenRouter(contextMessages),
|
|
1161
|
+
tools,
|
|
1162
|
+
stopWhen: stepCountIs(maxToolSteps),
|
|
1163
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
1164
|
+
temperature: config.temperature,
|
|
1165
|
+
stream: streamingEnabled
|
|
1166
|
+
};
|
|
1167
|
+
const result = await openrouter.callModel(callParams as Parameters<typeof openrouter.callModel>[0]);
|
|
1168
|
+
const localLatencyMs = Date.now() - startedAt;
|
|
1169
|
+
const toolCallsFromModel = await result.getToolCalls();
|
|
1170
|
+
if (toolCallsFromModel.length > 0) {
|
|
1171
|
+
log(`Model made ${toolCallsFromModel.length} tool call(s): ${toolCallsFromModel.map(t => t.name).join(', ')}`);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
let localResponseText = '';
|
|
1175
|
+
let streamed = false;
|
|
1176
|
+
if (streamingEnabled && typeof (result as { getTextStream?: () => AsyncIterable<unknown> }).getTextStream === 'function') {
|
|
1177
|
+
try {
|
|
1178
|
+
const stream = (result as { getTextStream: () => AsyncIterable<unknown> }).getTextStream();
|
|
1179
|
+
for await (const chunk of stream) {
|
|
1180
|
+
const delta = typeof chunk === 'string'
|
|
1181
|
+
? chunk
|
|
1182
|
+
: (typeof (chunk as { text?: unknown })?.text === 'string' ? (chunk as { text?: string }).text || '' : '');
|
|
1183
|
+
if (!delta) continue;
|
|
1184
|
+
localResponseText += delta;
|
|
1185
|
+
sendStreamUpdate(localResponseText);
|
|
1186
|
+
}
|
|
1187
|
+
if (localResponseText) {
|
|
1188
|
+
sendStreamUpdate(localResponseText, true);
|
|
1189
|
+
}
|
|
1190
|
+
streamed = true;
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
log(`Streaming failed, falling back to full response: ${err instanceof Error ? err.message : String(err)}`);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (!streamed || !localResponseText || !localResponseText.trim()) {
|
|
1196
|
+
localResponseText = await result.getText();
|
|
1197
|
+
if (localResponseText && localResponseText.trim()) {
|
|
1198
|
+
sendStreamUpdate(localResponseText, true);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (!localResponseText || !localResponseText.trim()) {
|
|
1202
|
+
if (toolCallsFromModel.length > 0) {
|
|
1203
|
+
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".';
|
|
1204
|
+
}
|
|
1205
|
+
log(`Warning: Model returned empty/whitespace response. Raw length: ${localResponseText?.length ?? 0}, tool calls: ${toolCallsFromModel.length}`);
|
|
1206
|
+
} else {
|
|
1207
|
+
log(`Model returned text response (${localResponseText.length} chars)`);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
const localCompletionTokens = estimateTokensForModel(localResponseText || '', tokenEstimate.tokensPerChar);
|
|
1211
|
+
return {
|
|
1212
|
+
responseText: localResponseText,
|
|
1213
|
+
completionTokens: localCompletionTokens,
|
|
1214
|
+
promptTokens: resolvedPromptTokens,
|
|
1215
|
+
latencyMs: localLatencyMs,
|
|
1216
|
+
modelToolCalls: toolCallsFromModel
|
|
1217
|
+
};
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
try {
|
|
1221
|
+
const firstAttempt = await runCompletion();
|
|
1222
|
+
responseText = firstAttempt.responseText;
|
|
1223
|
+
completionTokens = firstAttempt.completionTokens;
|
|
1224
|
+
promptTokens = firstAttempt.promptTokens;
|
|
1225
|
+
latencyMs = firstAttempt.latencyMs;
|
|
1226
|
+
modelToolCalls = firstAttempt.modelToolCalls;
|
|
1227
|
+
|
|
1228
|
+
const shouldValidate = responseValidateEnabled
|
|
1229
|
+
&& (responseValidateAllowToolCalls || modelToolCalls.length === 0);
|
|
1230
|
+
if (shouldValidate) {
|
|
1231
|
+
let retriesLeft = responseValidateMaxRetries;
|
|
1232
|
+
while (true) {
|
|
1233
|
+
if (!responseValidateAllowToolCalls && modelToolCalls.length > 0) {
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
let validationResult: ResponseValidation | null = null;
|
|
1237
|
+
if (!responseText || !responseText.trim()) {
|
|
1238
|
+
validationResult = { verdict: 'fail', issues: ['Response was empty.'], missing: [] };
|
|
1239
|
+
} else {
|
|
1240
|
+
try {
|
|
1241
|
+
validationResult = await validateResponseQuality({
|
|
1242
|
+
openrouter,
|
|
1243
|
+
model: responseValidateModel,
|
|
1244
|
+
userPrompt: query,
|
|
1245
|
+
response: responseText,
|
|
1246
|
+
maxOutputTokens: responseValidateMaxOutputTokens,
|
|
1247
|
+
temperature: responseValidateTemperature
|
|
1248
|
+
});
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
log(`Response validation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (!validationResult || validationResult.verdict === 'pass') {
|
|
1254
|
+
break;
|
|
1255
|
+
}
|
|
1256
|
+
if (retriesLeft <= 0) {
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
retriesLeft -= 1;
|
|
1260
|
+
log(`Response validation failed; retrying (${retriesLeft} retries left)`);
|
|
1261
|
+
streamLastSentAt = 0;
|
|
1262
|
+
streamLastSentLength = 0;
|
|
1263
|
+
const retryGuidance = buildRetryGuidance(validationResult);
|
|
1264
|
+
const retryAttempt = await runCompletion(retryGuidance);
|
|
1265
|
+
responseText = retryAttempt.responseText;
|
|
1266
|
+
completionTokens = retryAttempt.completionTokens;
|
|
1267
|
+
promptTokens = retryAttempt.promptTokens;
|
|
1268
|
+
latencyMs = retryAttempt.latencyMs;
|
|
1269
|
+
modelToolCalls = retryAttempt.modelToolCalls;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
} catch (err) {
|
|
1273
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1274
|
+
log(`Agent error: ${errorMessage}`);
|
|
1275
|
+
return {
|
|
1276
|
+
status: 'error',
|
|
1277
|
+
result: null,
|
|
1278
|
+
newSessionId: isNew ? sessionCtx.sessionId : undefined,
|
|
1279
|
+
error: errorMessage,
|
|
1280
|
+
model,
|
|
1281
|
+
prompt_pack_versions: Object.keys(promptPackVersions).length > 0 ? promptPackVersions : undefined,
|
|
1282
|
+
memory_summary: sessionCtx.state.summary,
|
|
1283
|
+
memory_facts: sessionCtx.state.facts,
|
|
1284
|
+
tokens_prompt: promptTokens,
|
|
1285
|
+
tokens_completion: completionTokens,
|
|
1286
|
+
memory_recall_count: memoryRecallCount,
|
|
1287
|
+
session_recall_count: sessionRecallCount,
|
|
1288
|
+
memory_items_upserted: memoryItemsUpserted,
|
|
1289
|
+
memory_items_extracted: memoryItemsExtracted,
|
|
1290
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
1291
|
+
latency_ms: latencyMs
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
appendHistory(sessionCtx, 'assistant', responseText || '');
|
|
1296
|
+
|
|
1297
|
+
history = loadHistory(sessionCtx);
|
|
1298
|
+
const newMessages = history.filter(m => m.seq > sessionCtx.state.lastSummarySeq);
|
|
1299
|
+
if (newMessages.length >= config.summaryUpdateEveryMessages) {
|
|
1300
|
+
const summaryUpdate = await updateMemorySummary({
|
|
1301
|
+
openrouter,
|
|
1302
|
+
model: summaryModel,
|
|
1303
|
+
existingSummary: sessionCtx.state.summary,
|
|
1304
|
+
existingFacts: sessionCtx.state.facts,
|
|
1305
|
+
newMessages,
|
|
1306
|
+
maxOutputTokens: config.summaryMaxOutputTokens
|
|
1307
|
+
});
|
|
1308
|
+
if (summaryUpdate) {
|
|
1309
|
+
sessionCtx.state.summary = summaryUpdate.summary;
|
|
1310
|
+
sessionCtx.state.facts = summaryUpdate.facts;
|
|
1311
|
+
sessionCtx.state.lastSummarySeq = newMessages[newMessages.length - 1].seq;
|
|
1312
|
+
saveMemoryState(sessionCtx);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const runMemoryExtraction = async () => {
|
|
1317
|
+
const extractionMessages = history.slice(-memoryExtractionMaxMessages);
|
|
1318
|
+
if (extractionMessages.length === 0) return;
|
|
1319
|
+
const extractionPrompt = buildMemoryExtractionPrompt({
|
|
1320
|
+
assistantName,
|
|
1321
|
+
userId: input.userId,
|
|
1322
|
+
userName: input.userName,
|
|
1323
|
+
messages: extractionMessages,
|
|
1324
|
+
memoryPolicyPack: memoryPolicyResult?.pack || null
|
|
1325
|
+
});
|
|
1326
|
+
const extractionResult = await openrouter.callModel({
|
|
1327
|
+
model: memoryModel,
|
|
1328
|
+
instructions: extractionPrompt.instructions,
|
|
1329
|
+
input: extractionPrompt.input,
|
|
1330
|
+
maxOutputTokens: memoryExtractionMaxOutputTokens,
|
|
1331
|
+
temperature: 0.1
|
|
1332
|
+
});
|
|
1333
|
+
const extractionText = await extractionResult.getText();
|
|
1334
|
+
const extractedItems = parseMemoryExtraction(extractionText);
|
|
1335
|
+
if (extractedItems.length === 0) return;
|
|
1336
|
+
|
|
1337
|
+
const behaviorThreshold = typeof input.behaviorConfig?.memory_importance_threshold === 'number'
|
|
1338
|
+
? Number(input.behaviorConfig?.memory_importance_threshold)
|
|
1339
|
+
: null;
|
|
1340
|
+
const normalizedItems = extractedItems
|
|
1341
|
+
.filter((item) => {
|
|
1342
|
+
if (behaviorThreshold === null) return true;
|
|
1343
|
+
const importance = typeof item.importance === 'number' ? item.importance : null;
|
|
1344
|
+
if (importance === null) return true;
|
|
1345
|
+
return importance >= behaviorThreshold;
|
|
1346
|
+
})
|
|
1347
|
+
.map((item) => {
|
|
1348
|
+
const scope = typeof item.scope === 'string' ? item.scope : '';
|
|
1349
|
+
const subject = item.subject_id;
|
|
1350
|
+
if (scope === 'user' && !subject && input.userId) {
|
|
1351
|
+
return { ...item, subject_id: input.userId };
|
|
1352
|
+
}
|
|
1353
|
+
return item;
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
if (normalizedItems.length > 0) {
|
|
1357
|
+
await ipc.memoryUpsert({
|
|
1358
|
+
items: normalizedItems as unknown[],
|
|
1359
|
+
source: 'agent-extraction'
|
|
1360
|
+
});
|
|
1361
|
+
memoryItemsExtracted += normalizedItems.length;
|
|
1362
|
+
memoryItemsUpserted += normalizedItems.length;
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
if (memoryExtractionEnabled && (!input.isScheduledTask || memoryExtractScheduled)) {
|
|
1367
|
+
if (memoryExtractionAsync && isDaemon) {
|
|
1368
|
+
void runMemoryExtraction().catch(err => {
|
|
1369
|
+
log(`Memory extraction failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1370
|
+
});
|
|
1371
|
+
} else {
|
|
1372
|
+
try {
|
|
1373
|
+
await runMemoryExtraction();
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
log(`Memory extraction failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// Normalize empty/whitespace-only responses to null
|
|
1381
|
+
const finalResult = responseText && responseText.trim() ? responseText : null;
|
|
1382
|
+
|
|
1383
|
+
return {
|
|
1384
|
+
status: 'success',
|
|
1385
|
+
result: finalResult,
|
|
1386
|
+
newSessionId: isNew ? sessionCtx.sessionId : undefined,
|
|
1387
|
+
model,
|
|
1388
|
+
prompt_pack_versions: Object.keys(promptPackVersions).length > 0 ? promptPackVersions : undefined,
|
|
1389
|
+
memory_summary: sessionCtx.state.summary,
|
|
1390
|
+
memory_facts: sessionCtx.state.facts,
|
|
1391
|
+
tokens_prompt: promptTokens,
|
|
1392
|
+
tokens_completion: completionTokens,
|
|
1393
|
+
memory_recall_count: memoryRecallCount,
|
|
1394
|
+
session_recall_count: sessionRecallCount,
|
|
1395
|
+
memory_items_upserted: memoryItemsUpserted,
|
|
1396
|
+
memory_items_extracted: memoryItemsExtracted,
|
|
1397
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
1398
|
+
latency_ms: latencyMs
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
async function main(): Promise<void> {
|
|
1403
|
+
try {
|
|
1404
|
+
const stdinData = await readStdin();
|
|
1405
|
+
const input = JSON.parse(stdinData) as ContainerInput;
|
|
1406
|
+
const output = await runAgentOnce(input);
|
|
1407
|
+
writeOutput(output);
|
|
1408
|
+
} catch (err) {
|
|
1409
|
+
writeOutput({
|
|
1410
|
+
status: 'error',
|
|
1411
|
+
result: null,
|
|
1412
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
1418
|
+
if (isMain) {
|
|
1419
|
+
main().catch(err => {
|
|
1420
|
+
log(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1421
|
+
writeOutput({
|
|
1422
|
+
status: 'error',
|
|
1423
|
+
result: null,
|
|
1424
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1425
|
+
});
|
|
1426
|
+
process.exit(1);
|
|
1427
|
+
});
|
|
1428
|
+
}
|