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.
Files changed (121) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +69 -19
  3. package/SOUL.md +25 -24
  4. package/daemora-ui/README.md +11 -0
  5. package/package.json +12 -2
  6. package/skills/api-development.md +35 -0
  7. package/skills/artifacts-builder/SKILL.md +74 -0
  8. package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  9. package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  10. package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  11. package/skills/brand-guidelines.md +73 -0
  12. package/skills/browser.md +77 -0
  13. package/skills/changelog-generator.md +104 -0
  14. package/skills/coding.md +26 -10
  15. package/skills/content-research-writer.md +538 -0
  16. package/skills/data-analysis.md +27 -0
  17. package/skills/debugging.md +33 -0
  18. package/skills/devops.md +37 -0
  19. package/skills/document-docx.md +197 -0
  20. package/skills/document-pdf.md +294 -0
  21. package/skills/document-pptx.md +484 -0
  22. package/skills/document-xlsx.md +289 -0
  23. package/skills/domain-name-brainstormer.md +212 -0
  24. package/skills/file-organizer.md +433 -0
  25. package/skills/frontend-design.md +42 -0
  26. package/skills/image-enhancer.md +99 -0
  27. package/skills/invoice-organizer.md +446 -0
  28. package/skills/lead-research-assistant.md +199 -0
  29. package/skills/mcp-builder/SKILL.md +328 -0
  30. package/skills/mcp-builder/reference/evaluation.md +602 -0
  31. package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
  32. package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
  33. package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
  34. package/skills/mcp-builder/scripts/connections.py +151 -0
  35. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  36. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  37. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  38. package/skills/meeting-insights-analyzer.md +327 -0
  39. package/skills/orchestration.md +93 -0
  40. package/skills/raffle-winner-picker.md +159 -0
  41. package/skills/slack-gif-creator/SKILL.md +646 -0
  42. package/skills/slack-gif-creator/core/color_palettes.py +302 -0
  43. package/skills/slack-gif-creator/core/easing.py +230 -0
  44. package/skills/slack-gif-creator/core/frame_composer.py +469 -0
  45. package/skills/slack-gif-creator/core/gif_builder.py +246 -0
  46. package/skills/slack-gif-creator/core/typography.py +357 -0
  47. package/skills/slack-gif-creator/core/validators.py +264 -0
  48. package/skills/slack-gif-creator/core/visual_effects.py +494 -0
  49. package/skills/slack-gif-creator/requirements.txt +4 -0
  50. package/skills/slack-gif-creator/templates/bounce.py +106 -0
  51. package/skills/slack-gif-creator/templates/explode.py +331 -0
  52. package/skills/slack-gif-creator/templates/fade.py +329 -0
  53. package/skills/slack-gif-creator/templates/flip.py +291 -0
  54. package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  55. package/skills/slack-gif-creator/templates/morph.py +329 -0
  56. package/skills/slack-gif-creator/templates/move.py +293 -0
  57. package/skills/slack-gif-creator/templates/pulse.py +268 -0
  58. package/skills/slack-gif-creator/templates/shake.py +127 -0
  59. package/skills/slack-gif-creator/templates/slide.py +291 -0
  60. package/skills/slack-gif-creator/templates/spin.py +269 -0
  61. package/skills/slack-gif-creator/templates/wiggle.py +300 -0
  62. package/skills/slack-gif-creator/templates/zoom.py +312 -0
  63. package/skills/system-admin.md +44 -0
  64. package/skills/tailored-resume-generator.md +345 -0
  65. package/skills/theme-factory/SKILL.md +59 -0
  66. package/skills/theme-factory/theme-showcase.pdf +0 -0
  67. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  68. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  69. package/skills/theme-factory/themes/desert-rose.md +19 -0
  70. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  71. package/skills/theme-factory/themes/golden-hour.md +19 -0
  72. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  73. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  74. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  75. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  76. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  77. package/skills/video-downloader.md +99 -0
  78. package/skills/web-development.md +32 -0
  79. package/skills/webapp-testing/SKILL.md +96 -0
  80. package/skills/webapp-testing/examples/console_logging.py +35 -0
  81. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  82. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  83. package/skills/webapp-testing/scripts/with_server.py +106 -0
  84. package/src/agents/SubAgentManager.js +57 -12
  85. package/src/api/openai-compat.js +212 -0
  86. package/src/channels/TelegramChannel.js +5 -2
  87. package/src/channels/index.js +7 -10
  88. package/src/cli.js +129 -50
  89. package/src/config/agentProfiles.js +1 -0
  90. package/src/config/default.js +10 -0
  91. package/src/config/models.js +317 -71
  92. package/src/config/permissions.js +12 -0
  93. package/src/core/AgentLoop.js +70 -50
  94. package/src/core/Compaction.js +84 -2
  95. package/src/core/MessageQueue.js +90 -0
  96. package/src/core/Task.js +13 -0
  97. package/src/core/TaskQueue.js +1 -1
  98. package/src/core/TaskRunner.js +80 -5
  99. package/src/index.js +328 -48
  100. package/src/mcp/MCPAgentRunner.js +48 -11
  101. package/src/mcp/MCPManager.js +40 -2
  102. package/src/models/ModelRouter.js +67 -1
  103. package/src/safety/DockerSandbox.js +212 -0
  104. package/src/safety/ExecApproval.js +118 -0
  105. package/src/scheduler/Heartbeat.js +56 -21
  106. package/src/services/cleanup.js +106 -0
  107. package/src/services/sessions.js +39 -1
  108. package/src/setup/wizard.js +75 -4
  109. package/src/skills/SkillLoader.js +104 -17
  110. package/src/storage/TaskStore.js +19 -1
  111. package/src/systemPrompt.js +171 -328
  112. package/src/tools/browserAutomation.js +615 -104
  113. package/src/tools/executeCommand.js +19 -1
  114. package/src/tools/index.js +6 -0
  115. package/src/tools/manageAgents.js +55 -4
  116. package/src/tools/replyWithFile.js +62 -0
  117. package/src/tools/screenCapture.js +12 -1
  118. package/src/tools/taskManager.js +164 -0
  119. package/src/tools/useMCP.js +3 -1
  120. package/src/utils/Embeddings.js +157 -10
  121. package/src/webhooks/WebhookHandler.js +107 -0
@@ -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: 4096,
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
- if (response.usage) {
154
- totalInputTokens += response.usage.promptTokens || 0;
155
- totalOutputTokens += response.usage.completionTokens || 0;
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: response.usage?.promptTokens || 0,
163
- outputTokens: response.usage?.completionTokens || 0,
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}/3): ${error.message}`);
434
+ console.log(`[Loop ${loopCount}] Model call failed (${consecutiveErrors}/5): ${error.message}`);
421
435
 
422
- // Give up after 3 consecutive failures
423
- if (consecutiveErrors >= 3) {
424
- console.log(`[FATAL] 3 consecutive model failures. Stopping.`);
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}] Please provide your final answer. Set type to "text" and finalResponse to true.`,
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
  }
@@ -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: {
@@ -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);
@@ -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
- // Update session with conversation
214
- setMessages(session.sessionId, result.messages);
276
+ // Clean up event listeners
277
+ eventBus.removeListener("agent:spawned", onSpawn);
278
+ eventBus.removeListener("agent:finished", onFinish);
215
279
 
216
- // Update task cost info
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)}` : "";