daemora 1.0.3 → 1.0.5
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/LICENSE +663 -0
- package/README.md +69 -19
- package/SOUL.md +25 -24
- package/daemora-ui/README.md +11 -0
- package/package.json +12 -2
- package/skills/api-development.md +35 -0
- package/skills/artifacts-builder/SKILL.md +74 -0
- package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
- package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/brand-guidelines.md +73 -0
- package/skills/browser.md +77 -0
- package/skills/changelog-generator.md +104 -0
- package/skills/coding.md +26 -10
- package/skills/content-research-writer.md +538 -0
- package/skills/data-analysis.md +27 -0
- package/skills/debugging.md +33 -0
- package/skills/devops.md +37 -0
- package/skills/document-docx.md +197 -0
- package/skills/document-pdf.md +294 -0
- package/skills/document-pptx.md +484 -0
- package/skills/document-xlsx.md +289 -0
- package/skills/domain-name-brainstormer.md +212 -0
- package/skills/file-organizer.md +433 -0
- package/skills/frontend-design.md +42 -0
- package/skills/image-enhancer.md +99 -0
- package/skills/invoice-organizer.md +446 -0
- package/skills/lead-research-assistant.md +199 -0
- package/skills/mcp-builder/SKILL.md +328 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/meeting-insights-analyzer.md +327 -0
- package/skills/orchestration.md +93 -0
- package/skills/raffle-winner-picker.md +159 -0
- package/skills/slack-gif-creator/SKILL.md +646 -0
- package/skills/slack-gif-creator/core/color_palettes.py +302 -0
- package/skills/slack-gif-creator/core/easing.py +230 -0
- package/skills/slack-gif-creator/core/frame_composer.py +469 -0
- package/skills/slack-gif-creator/core/gif_builder.py +246 -0
- package/skills/slack-gif-creator/core/typography.py +357 -0
- package/skills/slack-gif-creator/core/validators.py +264 -0
- package/skills/slack-gif-creator/core/visual_effects.py +494 -0
- package/skills/slack-gif-creator/requirements.txt +4 -0
- package/skills/slack-gif-creator/templates/bounce.py +106 -0
- package/skills/slack-gif-creator/templates/explode.py +331 -0
- package/skills/slack-gif-creator/templates/fade.py +329 -0
- package/skills/slack-gif-creator/templates/flip.py +291 -0
- package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
- package/skills/slack-gif-creator/templates/morph.py +329 -0
- package/skills/slack-gif-creator/templates/move.py +293 -0
- package/skills/slack-gif-creator/templates/pulse.py +268 -0
- package/skills/slack-gif-creator/templates/shake.py +127 -0
- package/skills/slack-gif-creator/templates/slide.py +291 -0
- package/skills/slack-gif-creator/templates/spin.py +269 -0
- package/skills/slack-gif-creator/templates/wiggle.py +300 -0
- package/skills/slack-gif-creator/templates/zoom.py +312 -0
- package/skills/system-admin.md +44 -0
- package/skills/tailored-resume-generator.md +345 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/video-downloader.md +99 -0
- package/skills/web-development.md +32 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/src/agents/SubAgentManager.js +57 -12
- package/src/api/openai-compat.js +212 -0
- package/src/channels/TelegramChannel.js +5 -2
- package/src/channels/index.js +7 -10
- package/src/cli.js +129 -50
- package/src/config/agentProfiles.js +1 -0
- package/src/config/default.js +10 -0
- package/src/config/models.js +317 -71
- package/src/config/permissions.js +12 -0
- package/src/core/AgentLoop.js +70 -50
- package/src/core/Compaction.js +84 -2
- package/src/core/MessageQueue.js +90 -0
- package/src/core/Task.js +13 -0
- package/src/core/TaskQueue.js +1 -1
- package/src/core/TaskRunner.js +80 -5
- package/src/index.js +328 -48
- package/src/mcp/MCPAgentRunner.js +48 -11
- package/src/mcp/MCPManager.js +40 -2
- package/src/models/ModelRouter.js +67 -1
- package/src/safety/DockerSandbox.js +212 -0
- package/src/safety/ExecApproval.js +118 -0
- package/src/scheduler/Heartbeat.js +56 -21
- package/src/services/cleanup.js +106 -0
- package/src/services/sessions.js +39 -1
- package/src/setup/wizard.js +75 -4
- package/src/skills/SkillLoader.js +104 -17
- package/src/storage/TaskStore.js +19 -1
- package/src/systemPrompt.js +171 -328
- package/src/tools/browserAutomation.js +615 -104
- package/src/tools/executeCommand.js +19 -1
- package/src/tools/index.js +6 -0
- package/src/tools/manageAgents.js +55 -4
- package/src/tools/replyWithFile.js +62 -0
- package/src/tools/screenCapture.js +12 -1
- package/src/tools/taskManager.js +164 -0
- package/src/tools/useMCP.js +3 -1
- package/src/utils/Embeddings.js +157 -10
- package/src/webhooks/WebhookHandler.js +107 -0
package/src/core/AgentLoop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { generateObject } from "ai";
|
|
2
|
-
import { getModelWithFallback } from "../models/ModelRouter.js";
|
|
2
|
+
import { getModelWithFallback, resolveThinkingConfig } from "../models/ModelRouter.js";
|
|
3
3
|
import { compactIfNeeded, estimateTokens } from "./Compaction.js";
|
|
4
4
|
import { config } from "../config/default.js";
|
|
5
5
|
import eventBus from "./EventBus.js";
|
|
@@ -45,6 +45,10 @@ export async function runAgentLoop({
|
|
|
45
45
|
const selectedModelId = modelId || config.defaultModel;
|
|
46
46
|
const { model, meta, modelId: resolvedModelId } = getModelWithFallback(selectedModelId, apiKeys);
|
|
47
47
|
|
|
48
|
+
// Resolve thinking level config
|
|
49
|
+
const thinkingConfig = resolveThinkingConfig(resolvedModelId, config.thinkingLevel);
|
|
50
|
+
const thinkingParams = thinkingConfig?.thinkingParams || {};
|
|
51
|
+
|
|
48
52
|
// Build set of known secret values to redact from tool outputs (dynamic - catches tenant keys)
|
|
49
53
|
const _knownSecrets = new Set([
|
|
50
54
|
...Object.values(apiKeys),
|
|
@@ -63,17 +67,16 @@ export async function runAgentLoop({
|
|
|
63
67
|
|
|
64
68
|
let messages = [systemPrompt, ...msgs];
|
|
65
69
|
let stepCount = 0;
|
|
66
|
-
let writeToolUsed = false; // Track if model actually modified anything
|
|
67
70
|
let loopCount = 0;
|
|
68
71
|
let lastToolCall = null;
|
|
69
72
|
let repeatCount = 0;
|
|
70
73
|
let totalInputTokens = 0;
|
|
71
74
|
let totalOutputTokens = 0;
|
|
72
75
|
let consecutiveErrors = 0;
|
|
76
|
+
const toolCallLog = []; // Track tool calls for task history
|
|
73
77
|
|
|
74
78
|
const WRITE_TOOLS = new Set(["writeFile", "editFile", "applyPatch", "executeCommand", "sendEmail", "createDocument", "browserAction", "messageChannel"]);
|
|
75
79
|
let gitSnapshotDone = false; // Only snapshot once per task
|
|
76
|
-
const ACTION_WORDS = /\b(fixed|updated|created|added|modified|changed|removed|deleted|wrote|edited|replaced|refactored|implemented|styled|applied)\b/i;
|
|
77
80
|
|
|
78
81
|
console.log(`\n--- AGENT LOOP STARTED ---`);
|
|
79
82
|
console.log(`Model: ${resolvedModelId}`);
|
|
@@ -90,6 +93,7 @@ export async function runAgentLoop({
|
|
|
90
93
|
text: "Agent was stopped by the supervisor.",
|
|
91
94
|
messages: messages.slice(1),
|
|
92
95
|
cost: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens, estimatedCost: 0, modelCalls: loopCount, model: resolvedModelId },
|
|
96
|
+
toolCalls: toolCallLog,
|
|
93
97
|
};
|
|
94
98
|
}
|
|
95
99
|
|
|
@@ -100,6 +104,7 @@ export async function runAgentLoop({
|
|
|
100
104
|
text: "Task was stopped by the safety supervisor due to excessive tool usage or a dangerous pattern.",
|
|
101
105
|
messages: messages.slice(1),
|
|
102
106
|
cost: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens, estimatedCost: 0, modelCalls: loopCount, model: resolvedModelId },
|
|
107
|
+
toolCalls: toolCallLog,
|
|
103
108
|
};
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -122,6 +127,17 @@ export async function runAgentLoop({
|
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
if (loopCount > config.maxLoops + 3) {
|
|
131
|
+
// Hard exit — agent ignored the soft stop message
|
|
132
|
+
console.log(`[FATAL] Agent exceeded hard limit (${config.maxLoops + 3}). Forcing exit.`);
|
|
133
|
+
return {
|
|
134
|
+
text: "Task stopped: exceeded maximum iterations. Here is what was accomplished before stopping.",
|
|
135
|
+
messages: messages.slice(1),
|
|
136
|
+
cost: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens, estimatedCost: 0, modelCalls: loopCount, model: resolvedModelId },
|
|
137
|
+
toolCalls: toolCallLog,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
if (loopCount > config.maxLoops) {
|
|
126
142
|
console.log(`[WARN] Hit max loop limit (${config.maxLoops}). Forcing agent to stop.`);
|
|
127
143
|
messages.push({
|
|
@@ -130,8 +146,8 @@ export async function runAgentLoop({
|
|
|
130
146
|
});
|
|
131
147
|
}
|
|
132
148
|
|
|
133
|
-
// Compaction check before model call
|
|
134
|
-
messages = await compactIfNeeded(messages, meta, taskId);
|
|
149
|
+
// Compaction check before model call (pass tools for pre-compaction memory flush)
|
|
150
|
+
messages = await compactIfNeeded(messages, meta, taskId, tools);
|
|
135
151
|
|
|
136
152
|
console.log(`\n[Loop ${loopCount}] Sending ${messages.length} messages (~${estimateTokens(messages)} tokens) to ${resolvedModelId}...`);
|
|
137
153
|
|
|
@@ -142,25 +158,34 @@ export async function runAgentLoop({
|
|
|
142
158
|
model,
|
|
143
159
|
schema: outputSchema,
|
|
144
160
|
messages,
|
|
145
|
-
maxTokens:
|
|
161
|
+
maxTokens: 8192,
|
|
146
162
|
abortSignal: signal || undefined,
|
|
163
|
+
...thinkingParams,
|
|
147
164
|
});
|
|
148
165
|
|
|
149
166
|
const elapsed = Date.now() - startTime;
|
|
150
167
|
consecutiveErrors = 0; // Reset on success
|
|
151
168
|
|
|
152
|
-
// Track token usage
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
169
|
+
// Track token usage (Vercel AI SDK uses inputTokens/outputTokens)
|
|
170
|
+
const usage = response.usage;
|
|
171
|
+
if (usage && (usage.inputTokens || usage.outputTokens || usage.promptTokens || usage.completionTokens)) {
|
|
172
|
+
totalInputTokens += usage.inputTokens || usage.promptTokens || 0;
|
|
173
|
+
totalOutputTokens += usage.outputTokens || usage.completionTokens || 0;
|
|
174
|
+
} else {
|
|
175
|
+
// Fallback: estimate from message sizes if usage not available
|
|
176
|
+
console.log(`[Loop ${loopCount}] WARNING: No token usage returned. response.usage = ${JSON.stringify(usage)}`);
|
|
177
|
+
const inputChars = messages.reduce((sum, m) => sum + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
178
|
+
const outputChars = JSON.stringify(response.object).length;
|
|
179
|
+
totalInputTokens += Math.ceil(inputChars / 4);
|
|
180
|
+
totalOutputTokens += Math.ceil(outputChars / 4);
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
eventBus.emitEvent("model:called", {
|
|
159
184
|
modelId: resolvedModelId,
|
|
160
185
|
loopCount,
|
|
161
186
|
elapsed,
|
|
162
|
-
inputTokens:
|
|
163
|
-
outputTokens:
|
|
187
|
+
inputTokens: usage?.inputTokens || usage?.promptTokens || 0,
|
|
188
|
+
outputTokens: usage?.outputTokens || usage?.completionTokens || 0,
|
|
164
189
|
});
|
|
165
190
|
|
|
166
191
|
const parsedOutput = response.object;
|
|
@@ -274,14 +299,19 @@ export async function runAgentLoop({
|
|
|
274
299
|
const outputStr = typeof toolOutput === "string" ? toolOutput : JSON.stringify(toolOutput);
|
|
275
300
|
const preview = outputStr.slice(0, 300) + (outputStr.length > 300 ? "..." : "");
|
|
276
301
|
|
|
277
|
-
// Track if a write tool was successfully used
|
|
278
|
-
if (WRITE_TOOLS.has(tool_name)) {
|
|
279
|
-
writeToolUsed = true;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
302
|
console.log(`[Step ${stepCount}] Done in ${toolElapsed}ms`);
|
|
283
303
|
console.log(`[Step ${stepCount}] Output: ${preview}`);
|
|
284
304
|
|
|
305
|
+
// Record tool call in log
|
|
306
|
+
toolCallLog.push({
|
|
307
|
+
tool: tool_name,
|
|
308
|
+
params,
|
|
309
|
+
duration: toolElapsed,
|
|
310
|
+
output_preview: outputStr.slice(0, 500),
|
|
311
|
+
status: "success",
|
|
312
|
+
step: stepCount,
|
|
313
|
+
});
|
|
314
|
+
|
|
285
315
|
eventBus.emitEvent("tool:after", {
|
|
286
316
|
tool_name,
|
|
287
317
|
params,
|
|
@@ -311,6 +341,16 @@ export async function runAgentLoop({
|
|
|
311
341
|
} catch (error) {
|
|
312
342
|
console.log(`[Step ${stepCount}] FAILED: ${error.message}`);
|
|
313
343
|
|
|
344
|
+
// Record failed tool call in log
|
|
345
|
+
toolCallLog.push({
|
|
346
|
+
tool: tool_name,
|
|
347
|
+
params,
|
|
348
|
+
duration: 0,
|
|
349
|
+
output_preview: `Error: ${error.message}`,
|
|
350
|
+
status: "error",
|
|
351
|
+
step: stepCount,
|
|
352
|
+
});
|
|
353
|
+
|
|
314
354
|
// Record failure for circuit breaker
|
|
315
355
|
circuitBreaker.recordToolFailure(tool_name);
|
|
316
356
|
|
|
@@ -356,33 +396,6 @@ export async function runAgentLoop({
|
|
|
356
396
|
continue;
|
|
357
397
|
}
|
|
358
398
|
|
|
359
|
-
// --- Lazy model safeguard ---
|
|
360
|
-
// SAFEGUARD 1: Model claimed done but used ZERO tools
|
|
361
|
-
// If the user's message is a real request (not just "ok"/"yes"), force tool use.
|
|
362
|
-
if (stepCount === 0 && loopCount <= 2) {
|
|
363
|
-
const lastUserMsg = msgs[msgs.length - 1]?.content?.toLowerCase() || "";
|
|
364
|
-
const isAck = /^(ok|okay|yes|yeah|sure|thanks|thank you|no|nah|k|yep|yup|got it|cool|nice|great|good|alright|👍)\.?$/i.test(lastUserMsg.trim());
|
|
365
|
-
if (!isAck && lastUserMsg.length > 5) {
|
|
366
|
-
console.log(`[Loop ${loopCount}] LAZY MODEL DETECTED - claimed done but used 0 tools. Forcing tool use.`);
|
|
367
|
-
messages.push({
|
|
368
|
-
role: "user",
|
|
369
|
-
content: `You responded without using any tools. You MUST actually use tools (readFile, editFile, writeFile, executeCommand, etc.) to complete the task. Do NOT claim you fixed or changed something without actually doing it. Use your tools NOW to fulfill the request.`,
|
|
370
|
-
});
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// SAFEGUARD 2: Model used only READ tools but claims it modified/fixed something
|
|
376
|
-
// If the response contains action words but no write tool was ever called, push back.
|
|
377
|
-
if (!writeToolUsed && stepCount > 0 && loopCount <= 4 && ACTION_WORDS.test(parsedOutput.text_content)) {
|
|
378
|
-
console.log(`[Loop ${loopCount}] PHANTOM WRITE DETECTED - model claims "${parsedOutput.text_content.slice(0, 80)}..." but only used read tools. Forcing actual writes.`);
|
|
379
|
-
messages.push({
|
|
380
|
-
role: "user",
|
|
381
|
-
content: `You claim to have made changes but you only used read tools - you never called writeFile or editFile to actually modify any file. The files are UNCHANGED. You must use writeFile or editFile to actually make the changes. Do it now.`,
|
|
382
|
-
});
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
399
|
const cost = {
|
|
387
400
|
inputTokens: totalInputTokens,
|
|
388
401
|
outputTokens: totalOutputTokens,
|
|
@@ -403,7 +416,7 @@ export async function runAgentLoop({
|
|
|
403
416
|
messages.push({ role: "assistant", content: parsedOutput.text_content });
|
|
404
417
|
|
|
405
418
|
const conversationMessages = messages.slice(1);
|
|
406
|
-
return { text: parsedOutput.text_content, messages: conversationMessages, cost };
|
|
419
|
+
return { text: parsedOutput.text_content, messages: conversationMessages, cost, toolCalls: toolCallLog };
|
|
407
420
|
}
|
|
408
421
|
} catch (error) {
|
|
409
422
|
// Abort signal fires as an error - exit cleanly
|
|
@@ -413,15 +426,16 @@ export async function runAgentLoop({
|
|
|
413
426
|
text: "Agent was stopped by the supervisor.",
|
|
414
427
|
messages: messages.slice(1),
|
|
415
428
|
cost: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens, estimatedCost: 0, modelCalls: loopCount, model: resolvedModelId },
|
|
429
|
+
toolCalls: toolCallLog,
|
|
416
430
|
};
|
|
417
431
|
}
|
|
418
432
|
|
|
419
433
|
consecutiveErrors++;
|
|
420
|
-
console.log(`[Loop ${loopCount}] Model call failed (${consecutiveErrors}/
|
|
434
|
+
console.log(`[Loop ${loopCount}] Model call failed (${consecutiveErrors}/5): ${error.message}`);
|
|
421
435
|
|
|
422
|
-
// Give up after
|
|
423
|
-
if (consecutiveErrors >=
|
|
424
|
-
console.log(`[FATAL]
|
|
436
|
+
// Give up after 5 consecutive failures
|
|
437
|
+
if (consecutiveErrors >= 5) {
|
|
438
|
+
console.log(`[FATAL] 5 consecutive model failures. Stopping.`);
|
|
425
439
|
return {
|
|
426
440
|
text: `I encountered an error while processing your request: ${error.message}`,
|
|
427
441
|
messages: messages.slice(1),
|
|
@@ -432,13 +446,19 @@ export async function runAgentLoop({
|
|
|
432
446
|
modelCalls: loopCount,
|
|
433
447
|
model: resolvedModelId,
|
|
434
448
|
},
|
|
449
|
+
toolCalls: toolCallLog,
|
|
435
450
|
};
|
|
436
451
|
}
|
|
437
452
|
|
|
453
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
|
|
454
|
+
const backoffMs = Math.min(1000 * Math.pow(2, consecutiveErrors - 1), 16000);
|
|
455
|
+
console.log(`[Loop ${loopCount}] Retrying in ${backoffMs}ms...`);
|
|
456
|
+
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
457
|
+
|
|
438
458
|
// Retry with a user-role nudge (compatible with all providers)
|
|
439
459
|
messages.push({
|
|
440
460
|
role: "user",
|
|
441
|
-
content: `[System: previous call failed: ${error.message}]
|
|
461
|
+
content: `[System: previous call failed: ${error.message}] Try again with the same approach, or provide your final answer. Set type to "text" and finalResponse to true.`,
|
|
442
462
|
});
|
|
443
463
|
continue;
|
|
444
464
|
}
|
package/src/core/Compaction.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { generateText } from "ai";
|
|
1
|
+
import { generateText, generateObject } from "ai";
|
|
2
2
|
import { getCheapModel } from "../models/ModelRouter.js";
|
|
3
3
|
import { writeFileSync, mkdirSync } from "fs";
|
|
4
4
|
import { config } from "../config/default.js";
|
|
5
5
|
import eventBus from "./EventBus.js";
|
|
6
|
+
import outputSchema from "../services/models/outputSchema.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Context compaction system.
|
|
@@ -55,15 +56,93 @@ function persistLargeOutput(content, taskId, stepIndex) {
|
|
|
55
56
|
return `[Output saved to disk: ${filePath} - ${content.length} chars]`;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Run a mini agent loop before compaction so the agent can save important context
|
|
61
|
+
* to memory files. Only uses memory tools. Max 3 turns.
|
|
62
|
+
*/
|
|
63
|
+
async function runPreCompactionFlush(messages, tools = {}) {
|
|
64
|
+
try {
|
|
65
|
+
const memoryToolNames = ["readMemory", "writeMemory", "writeDailyLog", "readDailyLog"];
|
|
66
|
+
const memoryTools = {};
|
|
67
|
+
for (const name of memoryToolNames) {
|
|
68
|
+
if (tools[name]) memoryTools[name] = tools[name];
|
|
69
|
+
}
|
|
70
|
+
if (Object.keys(memoryTools).length === 0) {
|
|
71
|
+
console.log("[Compaction] No memory tools available — skipping pre-compaction flush");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { model } = getCheapModel();
|
|
76
|
+
|
|
77
|
+
// Build a summary of recent context for the flush agent
|
|
78
|
+
const recentMessages = messages.slice(-10);
|
|
79
|
+
const contextSummary = recentMessages
|
|
80
|
+
.map(m => `[${m.role}]: ${(typeof m.content === "string" ? m.content : JSON.stringify(m.content)).slice(0, 500)}`)
|
|
81
|
+
.join("\n");
|
|
82
|
+
|
|
83
|
+
const flushPrompt = `Pre-compaction memory flush. The conversation is about to be compacted (older messages summarized).
|
|
84
|
+
|
|
85
|
+
Recent conversation context:
|
|
86
|
+
${contextSummary}
|
|
87
|
+
|
|
88
|
+
If there are important details worth preserving (decisions, file paths, user preferences, task progress), save them now using writeMemory or writeDailyLog.
|
|
89
|
+
If nothing important to save, respond with finalResponse: true immediately.
|
|
90
|
+
|
|
91
|
+
Available tools: ${Object.keys(memoryTools).join(", ")}`;
|
|
92
|
+
|
|
93
|
+
let flushMessages = [
|
|
94
|
+
{ role: "system", content: "You are a memory-flush agent. Save important context from the conversation to long-term memory before it gets compacted. Be brief." },
|
|
95
|
+
{ role: "user", content: flushPrompt },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
for (let turn = 0; turn < 3; turn++) {
|
|
99
|
+
const response = await generateObject({
|
|
100
|
+
model,
|
|
101
|
+
schema: outputSchema,
|
|
102
|
+
messages: flushMessages,
|
|
103
|
+
maxTokens: 2048,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const parsed = response.object;
|
|
107
|
+
if (parsed.finalResponse || parsed.type === "text") {
|
|
108
|
+
console.log("[Compaction] Pre-flush complete" + (turn === 0 ? " (nothing to save)" : ` (${turn} tool calls)`));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (parsed.type === "tool_call" && parsed.tool_call) {
|
|
113
|
+
const { tool_name, params } = parsed.tool_call;
|
|
114
|
+
flushMessages.push({ role: "assistant", content: JSON.stringify(parsed) });
|
|
115
|
+
|
|
116
|
+
if (memoryTools[tool_name]) {
|
|
117
|
+
try {
|
|
118
|
+
const output = await Promise.resolve(memoryTools[tool_name](...params));
|
|
119
|
+
const outputStr = typeof output === "string" ? output : JSON.stringify(output);
|
|
120
|
+
console.log(`[Compaction] Pre-flush: ${tool_name} → ${outputStr.slice(0, 100)}`);
|
|
121
|
+
flushMessages.push({ role: "user", content: JSON.stringify({ tool_name, params, output: outputStr }) });
|
|
122
|
+
} catch (e) {
|
|
123
|
+
flushMessages.push({ role: "user", content: JSON.stringify({ tool_name, params, output: `Error: ${e.message}` }) });
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
flushMessages.push({ role: "user", content: JSON.stringify({ tool_name, params, output: `Unknown tool. Available: ${Object.keys(memoryTools).join(", ")}` }) });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log("[Compaction] Pre-flush hit max turns (3)");
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.log(`[Compaction] Pre-flush failed (non-blocking): ${error.message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
58
136
|
/**
|
|
59
137
|
* Check if compaction is needed and perform it.
|
|
60
138
|
*
|
|
61
139
|
* @param {Array} messages - Current message history
|
|
62
140
|
* @param {object} modelMeta - Model metadata (from models.js) with compactAt threshold
|
|
63
141
|
* @param {string} taskId - Current task ID for file persistence
|
|
142
|
+
* @param {object} [tools] - Available tool functions (used for pre-compaction flush)
|
|
64
143
|
* @returns {Array} Possibly compacted messages
|
|
65
144
|
*/
|
|
66
|
-
export async function compactIfNeeded(messages, modelMeta, taskId = null) {
|
|
145
|
+
export async function compactIfNeeded(messages, modelMeta, taskId = null, tools = {}) {
|
|
67
146
|
const tokenCount = estimateTokens(messages);
|
|
68
147
|
|
|
69
148
|
if (tokenCount < modelMeta.compactAt) {
|
|
@@ -75,6 +154,9 @@ export async function compactIfNeeded(messages, modelMeta, taskId = null) {
|
|
|
75
154
|
);
|
|
76
155
|
eventBus.emitEvent("compact:triggered", { tokenCount, threshold: modelMeta.compactAt });
|
|
77
156
|
|
|
157
|
+
// Pre-compaction memory flush — let agent save important context before we compact
|
|
158
|
+
await runPreCompactionFlush(messages, tools);
|
|
159
|
+
|
|
78
160
|
// Step 1: Identify protected messages (system prompt + last 3 exchanges)
|
|
79
161
|
const systemMsg = messages[0]; // always protect system prompt
|
|
80
162
|
const recentCount = 6; // last 3 user+assistant pairs
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbound message debouncer — batches rapid-fire messages from the same session
|
|
3
|
+
* into a single task instead of spawning separate agent loops for each.
|
|
4
|
+
*
|
|
5
|
+
* When messages arrive within the debounce window (default 1.5s), they're
|
|
6
|
+
* concatenated into a single task input:
|
|
7
|
+
* [Queued messages]
|
|
8
|
+
* ---
|
|
9
|
+
* Message 1: first message
|
|
10
|
+
* ---
|
|
11
|
+
* Message 2: second message
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { config } from "../config/default.js";
|
|
15
|
+
|
|
16
|
+
class InboundDebouncer {
|
|
17
|
+
constructor() {
|
|
18
|
+
// sessionId → { messages: string[], timer: NodeJS.Timeout, resolve: Function }
|
|
19
|
+
this._pending = new Map();
|
|
20
|
+
this._debounceMs = parseInt(process.env.DEBOUNCE_MS || "1500", 10);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Add a message to the debounce queue for a session.
|
|
25
|
+
* Returns a Promise that resolves with the batched message(s) when the debounce window closes.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} sessionId
|
|
28
|
+
* @param {string} message
|
|
29
|
+
* @returns {Promise<string>} Batched message (may be single or multi)
|
|
30
|
+
*/
|
|
31
|
+
debounce(sessionId, message) {
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const existing = this._pending.get(sessionId);
|
|
34
|
+
|
|
35
|
+
if (existing) {
|
|
36
|
+
// Add to existing batch, reset timer
|
|
37
|
+
existing.messages.push(message);
|
|
38
|
+
clearTimeout(existing.timer);
|
|
39
|
+
// Only the first caller's resolve gets used; subsequent callers get null
|
|
40
|
+
existing.resolvers.push(resolve);
|
|
41
|
+
existing.timer = setTimeout(() => this._flush(sessionId), this._debounceMs);
|
|
42
|
+
} else {
|
|
43
|
+
// New batch
|
|
44
|
+
const entry = {
|
|
45
|
+
messages: [message],
|
|
46
|
+
resolvers: [resolve],
|
|
47
|
+
timer: setTimeout(() => this._flush(sessionId), this._debounceMs),
|
|
48
|
+
};
|
|
49
|
+
this._pending.set(sessionId, entry);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_flush(sessionId) {
|
|
55
|
+
const entry = this._pending.get(sessionId);
|
|
56
|
+
if (!entry) return;
|
|
57
|
+
this._pending.delete(sessionId);
|
|
58
|
+
|
|
59
|
+
let batched;
|
|
60
|
+
if (entry.messages.length === 1) {
|
|
61
|
+
batched = entry.messages[0];
|
|
62
|
+
} else {
|
|
63
|
+
const lines = entry.messages.map((m, i) => `Message ${i + 1}: ${m}`);
|
|
64
|
+
batched = `[Queued messages]\n---\n${lines.join("\n---\n")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// First resolver gets the batched message; others get null (they won't create tasks)
|
|
68
|
+
entry.resolvers[0](batched);
|
|
69
|
+
for (let i = 1; i < entry.resolvers.length; i++) {
|
|
70
|
+
entry.resolvers[i](null);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if a session has pending debounced messages.
|
|
76
|
+
*/
|
|
77
|
+
hasPending(sessionId) {
|
|
78
|
+
return this._pending.has(sessionId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get debounce window in ms.
|
|
83
|
+
*/
|
|
84
|
+
get debounceMs() {
|
|
85
|
+
return this._debounceMs;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const debouncer = new InboundDebouncer();
|
|
90
|
+
export default debouncer;
|
package/src/core/Task.js
CHANGED
|
@@ -16,10 +16,20 @@ export function createTask({
|
|
|
16
16
|
model = null,
|
|
17
17
|
maxCost = null,
|
|
18
18
|
approvalMode = "auto",
|
|
19
|
+
// ── Task system fields ──────────────────────────────────────────────────
|
|
20
|
+
type = "chat", // "chat" (from user message) | "task" (agent-created)
|
|
21
|
+
title = null, // short descriptive title (agent-created tasks)
|
|
22
|
+
description = null, // detailed task description
|
|
23
|
+
parentTaskId = null, // ID of parent task (for hierarchy)
|
|
24
|
+
agentId = null, // which agent/sub-agent is executing
|
|
25
|
+
agentCreated = false, // whether the agent created this task vs system
|
|
19
26
|
}) {
|
|
20
27
|
return {
|
|
21
28
|
id: uuidv4(),
|
|
22
29
|
status: "pending",
|
|
30
|
+
type, // chat | task
|
|
31
|
+
title, // short title for agent-created tasks
|
|
32
|
+
description, // detailed description
|
|
23
33
|
input, // user's message text
|
|
24
34
|
channel, // http | telegram | whatsapp | email | a2a
|
|
25
35
|
channelMeta, // channel-specific metadata (chat_id, phone, email, etc.)
|
|
@@ -28,6 +38,9 @@ export function createTask({
|
|
|
28
38
|
model, // explicit model override or null (use default)
|
|
29
39
|
maxCost, // per-task cost budget or null (use global)
|
|
30
40
|
approvalMode, // auto | dangerous-only | every-tool | milestones
|
|
41
|
+
parentTaskId, // parent task ID (for hierarchy)
|
|
42
|
+
agentId, // executing agent/sub-agent ID
|
|
43
|
+
agentCreated, // true if created by agent via taskManager tool
|
|
31
44
|
result: null, // final response text
|
|
32
45
|
error: null, // error message if failed
|
|
33
46
|
cost: {
|
package/src/core/TaskQueue.js
CHANGED
|
@@ -102,7 +102,7 @@ class TaskQueue {
|
|
|
102
102
|
saveTask(task);
|
|
103
103
|
this.active.delete(taskId);
|
|
104
104
|
|
|
105
|
-
eventBus.emitEvent("task:completed", { taskId: task.id, cost: task.cost });
|
|
105
|
+
eventBus.emitEvent("task:completed", { taskId: task.id, cost: task.cost, result: task.result });
|
|
106
106
|
|
|
107
107
|
// Resolve any sync waiters (normal flow - channel is waiting for completion)
|
|
108
108
|
const waiter = this.waiters.get(taskId);
|
package/src/core/TaskRunner.js
CHANGED
|
@@ -8,6 +8,41 @@ import { config } from "../config/default.js";
|
|
|
8
8
|
import tenantManager from "../tenants/TenantManager.js";
|
|
9
9
|
import tenantContext from "../tenants/TenantContext.js";
|
|
10
10
|
import inputSanitizer from "../safety/InputSanitizer.js";
|
|
11
|
+
import eventBus from "./EventBus.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Filter out internal tool call/result JSON from messages before saving to session.
|
|
15
|
+
* Keeps only clean user text and assistant text that users should see.
|
|
16
|
+
*/
|
|
17
|
+
function filterCleanMessages(messages) {
|
|
18
|
+
return messages.filter(msg => {
|
|
19
|
+
if (!msg.content || typeof msg.content !== "string") return false;
|
|
20
|
+
|
|
21
|
+
const trimmed = msg.content.trimStart();
|
|
22
|
+
if (trimmed.startsWith("{")) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(trimmed);
|
|
25
|
+
// Assistant tool_call messages
|
|
26
|
+
if (parsed.type === "tool_call" || parsed.tool_call) return false;
|
|
27
|
+
// User tool_result messages
|
|
28
|
+
if (parsed.tool_name) return false;
|
|
29
|
+
// Structured finalResponse wrappers (the actual text is saved separately)
|
|
30
|
+
if (parsed.type === "text" && parsed.finalResponse !== undefined) return false;
|
|
31
|
+
} catch {
|
|
32
|
+
// Not valid JSON - keep it (probably natural language that starts with {)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Filter out system injection messages
|
|
37
|
+
if (msg.role === "user" && msg.content.startsWith("[Supervisor instruction]:")) return false;
|
|
38
|
+
if (msg.role === "user" && msg.content.startsWith("[System:")) return false;
|
|
39
|
+
if (msg.role === "user" && msg.content.includes("You have used") && msg.content.includes("iterations")) return false;
|
|
40
|
+
if (msg.role === "user" && msg.content.includes("You are calling") && msg.content.includes("same params repeatedly")) return false;
|
|
41
|
+
if (msg.role === "user" && msg.content.includes("Provide a text summary of what you did")) return false;
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
11
46
|
|
|
12
47
|
/**
|
|
13
48
|
* Task runner - worker loop that picks tasks from the queue and executes them.
|
|
@@ -176,7 +211,7 @@ class TaskRunner {
|
|
|
176
211
|
// Wrap entire task execution in tenant context (AsyncLocalStorage).
|
|
177
212
|
// This allows FilesystemGuard, memory tools, and other tools to read per-tenant config
|
|
178
213
|
// without any race conditions across concurrent tasks.
|
|
179
|
-
await tenantContext.run({ tenant, resolvedConfig, resolvedModel, apiKeys }, async () => {
|
|
214
|
+
await tenantContext.run({ tenant, resolvedConfig, resolvedModel, apiKeys, sessionId: task.sessionId, channelMeta: task.channelMeta || null, directReplySent: false, currentTaskId: task.id, agentId: "main" }, async () => {
|
|
180
215
|
// Get or create session
|
|
181
216
|
let session = task.sessionId ? getSession(task.sessionId) : null;
|
|
182
217
|
if (!session) {
|
|
@@ -185,7 +220,10 @@ class TaskRunner {
|
|
|
185
220
|
}
|
|
186
221
|
|
|
187
222
|
// Build system prompt (SOUL.md + MEMORY.md + semantic recall + daily log + matched skills)
|
|
188
|
-
const systemPrompt = await buildSystemPrompt(task.input
|
|
223
|
+
const systemPrompt = await buildSystemPrompt(task.input, "full", {
|
|
224
|
+
model: resolvedModel,
|
|
225
|
+
agentId: "main",
|
|
226
|
+
});
|
|
189
227
|
|
|
190
228
|
// Build message history
|
|
191
229
|
const previousMessages = session.messages.map((m) => ({
|
|
@@ -194,6 +232,31 @@ class TaskRunner {
|
|
|
194
232
|
}));
|
|
195
233
|
const messages = [...previousMessages, { role: "user", content: task.input }];
|
|
196
234
|
|
|
235
|
+
// Track sub-agents spawned during this task
|
|
236
|
+
const subAgents = [];
|
|
237
|
+
const onSpawn = (evt) => {
|
|
238
|
+
if (evt.parentTaskId === task.id) {
|
|
239
|
+
subAgents.push({ agentId: evt.agentId, taskId: evt.taskId, description: evt.taskDescription, depth: evt.depth, status: "running", startedAt: new Date().toISOString() });
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const onFinish = (evt) => {
|
|
243
|
+
if (evt.parentTaskId === task.id) {
|
|
244
|
+
const sa = subAgents.find(s => s.agentId === evt.agentId);
|
|
245
|
+
if (sa) {
|
|
246
|
+
sa.status = evt.error ? "failed" : (evt.killed ? "killed" : "completed");
|
|
247
|
+
sa.cost = evt.cost || null;
|
|
248
|
+
sa.error = evt.error || null;
|
|
249
|
+
sa.toolCalls = evt.toolCalls || [];
|
|
250
|
+
sa.resultPreview = evt.resultPreview || null;
|
|
251
|
+
sa.model = evt.model || null;
|
|
252
|
+
sa.role = evt.role || null;
|
|
253
|
+
sa.completedAt = new Date().toISOString();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
eventBus.on("agent:spawned", onSpawn);
|
|
258
|
+
eventBus.on("agent:finished", onFinish);
|
|
259
|
+
|
|
197
260
|
// Run agent loop with resolved model, cost limits, and per-tenant API keys.
|
|
198
261
|
// steerQueue lets follow-up messages from the same user be injected live
|
|
199
262
|
// between tool calls instead of spawning a competing agent loop.
|
|
@@ -210,11 +273,17 @@ class TaskRunner {
|
|
|
210
273
|
steerQueue,
|
|
211
274
|
});
|
|
212
275
|
|
|
213
|
-
//
|
|
214
|
-
|
|
276
|
+
// Clean up event listeners
|
|
277
|
+
eventBus.removeListener("agent:spawned", onSpawn);
|
|
278
|
+
eventBus.removeListener("agent:finished", onFinish);
|
|
215
279
|
|
|
216
|
-
// Update
|
|
280
|
+
// Update session with CLEAN conversation only (strip internal tool JSON)
|
|
281
|
+
setMessages(session.sessionId, filterCleanMessages(result.messages));
|
|
282
|
+
|
|
283
|
+
// Update task cost info and tool calls
|
|
217
284
|
task.cost = result.cost;
|
|
285
|
+
task.toolCalls = result.toolCalls || [];
|
|
286
|
+
if (subAgents.length > 0) task.subAgents = subAgents;
|
|
218
287
|
|
|
219
288
|
// Record cost against tenant lifetime totals
|
|
220
289
|
const estimatedCost = result.cost?.estimatedCost || 0;
|
|
@@ -222,6 +291,12 @@ class TaskRunner {
|
|
|
222
291
|
tenantManager.recordCost(tenant.id, estimatedCost);
|
|
223
292
|
}
|
|
224
293
|
|
|
294
|
+
// If agent already replied directly (via replyWithFile), mark task so channel skips text reply
|
|
295
|
+
const store = tenantContext.getStore();
|
|
296
|
+
if (store?.directReplySent) {
|
|
297
|
+
task.directReplySent = true;
|
|
298
|
+
}
|
|
299
|
+
|
|
225
300
|
// Complete the task
|
|
226
301
|
taskQueue.complete(task.id, result.text);
|
|
227
302
|
const costStr = estimatedCost ? ` cost: $${estimatedCost.toFixed(4)}` : "";
|