morpheus-cli 0.8.5 → 0.8.7

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 (78) hide show
  1. package/dist/channels/telegram.js +43 -0
  2. package/dist/config/manager.js +3 -1
  3. package/dist/config/schemas.js +2 -0
  4. package/dist/http/api.js +70 -13
  5. package/dist/http/routers/chronos.js +12 -1
  6. package/dist/http/webhooks-router.js +11 -3
  7. package/dist/runtime/ISubagent.js +1 -0
  8. package/dist/runtime/apoc.js +49 -39
  9. package/dist/runtime/audit/repository.js +193 -6
  10. package/dist/runtime/chronos/repository.js +35 -0
  11. package/dist/runtime/keymaker.js +6 -30
  12. package/dist/runtime/memory/sati/index.js +22 -1
  13. package/dist/runtime/memory/sati/repository.js +39 -1
  14. package/dist/runtime/memory/sqlite.js +16 -3
  15. package/dist/runtime/neo.js +78 -34
  16. package/dist/runtime/oracle.js +68 -19
  17. package/dist/runtime/skills/tool.js +25 -0
  18. package/dist/runtime/subagent-utils.js +89 -0
  19. package/dist/runtime/tasks/repository.js +51 -0
  20. package/dist/runtime/tasks/worker.js +12 -2
  21. package/dist/runtime/telephonist.js +17 -9
  22. package/dist/runtime/tools/delegation-utils.js +120 -0
  23. package/dist/runtime/tools/index.js +0 -2
  24. package/dist/runtime/tools/time-verify-tools.js +15 -8
  25. package/dist/runtime/trinity.js +50 -34
  26. package/dist/runtime/webhooks/repository.js +31 -0
  27. package/dist/types/config.js +2 -0
  28. package/dist/types/pagination.js +1 -0
  29. package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +1 -0
  30. package/dist/ui/assets/Chat-CjxeAQmd.js +41 -0
  31. package/dist/ui/assets/Chronos-BAjeLobF.js +1 -0
  32. package/dist/ui/assets/{ConfirmationModal-MyIaIK_Z.js → ConfirmationModal-fvgnOWTY.js} +1 -1
  33. package/dist/ui/assets/{Dashboard-C52jjru9.js → Dashboard-Ca5mSefz.js} +1 -1
  34. package/dist/ui/assets/{DeleteConfirmationModal-B0nDocEK.js → DeleteConfirmationModal-A8EmnHoa.js} +1 -1
  35. package/dist/ui/assets/{Logs-fDrGC9Lq.js → Logs-CYu7se7R.js} +1 -1
  36. package/dist/ui/assets/MCPManager-DsDA_ZVT.js +1 -0
  37. package/dist/ui/assets/ModelPricing-DnSm_Nh-.js +1 -0
  38. package/dist/ui/assets/Notifications-CiljQzvM.js +1 -0
  39. package/dist/ui/assets/Pagination-JsiwxVNQ.js +1 -0
  40. package/dist/ui/assets/SatiMemories-rnO2b0LG.js +1 -0
  41. package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +9 -0
  42. package/dist/ui/assets/{Settings-Cgd4dJdc.js → Settings-OQlHAJoy.js} +6 -4
  43. package/dist/ui/assets/Skills-Crsybug0.js +7 -0
  44. package/dist/ui/assets/Smiths-wm90jRDT.js +1 -0
  45. package/dist/ui/assets/Tasks-C5FMu_Yu.js +1 -0
  46. package/dist/ui/assets/TrinityDatabases-BzYfecKI.js +1 -0
  47. package/dist/ui/assets/{UsageStats-EEwfbJ6C.js → UsageStats-CBo2vW2n.js} +1 -1
  48. package/dist/ui/assets/{WebhookManager-CyVUcscY.js → WebhookManager-0tDFkfHd.js} +1 -1
  49. package/dist/ui/assets/audit-B-F8XPLi.js +1 -0
  50. package/dist/ui/assets/chronos-BvMxfBQH.js +1 -0
  51. package/dist/ui/assets/{config-cslLZS3q.js → config-DteVgNGR.js} +1 -1
  52. package/dist/ui/assets/index-Cwqr-n0Y.js +10 -0
  53. package/dist/ui/assets/index-DcfyUdLI.css +1 -0
  54. package/dist/ui/assets/{mcp-M0iDC0mj.js → mcp-DxzodOdH.js} +1 -1
  55. package/dist/ui/assets/{skills-BvaaqiOT.js → skills--hAyQnmG.js} +1 -1
  56. package/dist/ui/assets/{stats-DALk3GOj.js → stats-Cibaisqd.js} +1 -1
  57. package/dist/ui/assets/vendor-icons-BVuQI-6R.js +1 -0
  58. package/dist/ui/index.html +3 -3
  59. package/dist/ui/sw.js +1 -1
  60. package/package.json +2 -1
  61. package/dist/runtime/tools/apoc-tool.js +0 -157
  62. package/dist/runtime/tools/neo-tool.js +0 -172
  63. package/dist/runtime/tools/trinity-tool.js +0 -157
  64. package/dist/ui/assets/Chat-Cx2OgATp.js +0 -38
  65. package/dist/ui/assets/Chronos--mut48fM.js +0 -1
  66. package/dist/ui/assets/MCPManager-CtRQzwM8.js +0 -1
  67. package/dist/ui/assets/ModelPricing-d4EYrGko.js +0 -1
  68. package/dist/ui/assets/Notifications-Dkqug57C.js +0 -1
  69. package/dist/ui/assets/SatiMemories-DykYVHgi.js +0 -1
  70. package/dist/ui/assets/SessionAudit-Bk0-DpW0.js +0 -9
  71. package/dist/ui/assets/Skills-DSi313oC.js +0 -7
  72. package/dist/ui/assets/Smiths-DLys0BWT.js +0 -1
  73. package/dist/ui/assets/Tasks-B1MbPNUQ.js +0 -1
  74. package/dist/ui/assets/TrinityDatabases-B5SeHOLt.js +0 -1
  75. package/dist/ui/assets/chronos-BVRpP__j.js +0 -1
  76. package/dist/ui/assets/index-CpVvCthh.js +0 -10
  77. package/dist/ui/assets/index-QQyZIsmH.css +0 -1
  78. package/dist/ui/assets/vendor-icons-DLvvGkeN.js +0 -1
@@ -10,9 +10,6 @@ import { TaskRequestContext } from "./tasks/context.js";
10
10
  import { TaskRepository } from "./tasks/repository.js";
11
11
  import { Neo } from "./neo.js";
12
12
  import { Trinity } from "./trinity.js";
13
- import { NeoDelegateTool } from "./tools/neo-tool.js";
14
- import { ApocDelegateTool } from "./tools/apoc-tool.js";
15
- import { TrinityDelegateTool } from "./tools/trinity-tool.js";
16
13
  import { SmithDelegateTool } from "./tools/smith-tool.js";
17
14
  import { TaskQueryTool, chronosTools, timeVerifierTool } from "./tools/index.js";
18
15
  import { Construtor } from "./tools/factory.js";
@@ -20,6 +17,11 @@ import { MCPManager } from "../config/mcp-manager.js";
20
17
  import { SkillRegistry, SkillExecuteTool, SkillDelegateTool, updateSkillToolDescriptions } from "./skills/index.js";
21
18
  import { SmithRegistry } from "./smiths/registry.js";
22
19
  import { AuditRepository } from "./audit/repository.js";
20
+ import { emitToolAuditEvents } from "./subagent-utils.js";
21
+ const ORACLE_DELEGATION_TOOLS = new Set([
22
+ 'apoc_delegate', 'neo_delegate', 'trinity_delegate', 'smith_delegate',
23
+ 'skill_delegate', 'skill_execute',
24
+ ]);
23
25
  export class Oracle {
24
26
  provider;
25
27
  config;
@@ -28,6 +30,8 @@ export class Oracle {
28
30
  taskRepository = TaskRepository.getInstance();
29
31
  databasePath;
30
32
  satiMiddleware = SatiMemoryMiddleware.getInstance();
33
+ /** Turn counter per session — tracks how many chat() calls have occurred per session ID. */
34
+ satiTurnCounters = new Map();
31
35
  constructor(config, overrides) {
32
36
  this.config = config || ConfigManager.getInstance().get();
33
37
  this.databasePath = overrides?.databasePath;
@@ -150,7 +154,16 @@ export class Oracle {
150
154
  await Trinity.refreshDelegateCatalog().catch(() => { });
151
155
  updateSkillToolDescriptions();
152
156
  // Build tool list — conditionally include SmithDelegateTool based on config
153
- const coreTools = [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool, SkillExecuteTool, SkillDelegateTool, timeVerifierTool, ...chronosTools];
157
+ const coreTools = [
158
+ TaskQueryTool,
159
+ Neo.getInstance().createDelegateTool(),
160
+ Apoc.getInstance().createDelegateTool(),
161
+ Trinity.getInstance().createDelegateTool(),
162
+ SkillExecuteTool,
163
+ SkillDelegateTool,
164
+ timeVerifierTool,
165
+ ...chronosTools,
166
+ ];
154
167
  const smithsConfig = ConfigManager.getInstance().getSmithsConfig();
155
168
  if (smithsConfig.enabled && smithsConfig.entries.length > 0) {
156
169
  coreTools.push(SmithDelegateTool);
@@ -212,14 +225,25 @@ You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
212
225
 
213
226
  You are an orchestrator and task router.
214
227
 
215
- If the user request contains ANY time-related expression
216
- (today, tomorrow, this week, next month, in 3 days, etc),
217
- you **MUST** call the tool "time_verifier" before answering or call another tool **ALWAYS**.
228
+ ## Date & Time Resolution MANDATORY
229
+
230
+ You **MUST** call "time_verifier" before answering or delegating ANY request that depends on the current date or time.
231
+ This includes — but is not limited to — **two categories**:
218
232
 
219
- With the time_verify, you remake the user prompt.
233
+ **Category A Explicit temporal expressions** (pass the expression itself as 'text'):
234
+ - Examples: "today", "tomorrow", "next week", "in 3 days", "this Friday", "at 20h"
235
+ - PT: "hoje", "amanhã", "próxima semana", "em 3 dias", "na sexta"
236
+ - Pass: '{ text: "<the expression>" }' → get resolved date → include it in the search/delegation
220
237
 
221
- Never assume dates.
222
- Always resolve temporal expressions using the tool.
238
+ **Category B — Implicit temporal intent** (pass "hoje" as 'text' to anchor the search to now):
239
+ - "próximo jogo do Flamengo" next scheduled event AFTER today
240
+ - "próximo episódio de X" → upcoming release AFTER today
241
+ - "latest version of Y", "resultado mais recente", "quem está liderando agora"
242
+ - Any query whose answer changes depending on what day it is today
243
+ - Pass: '{ text: "hoje" }' → get today's resolved date → include it in the search/delegation
244
+
245
+ **NEVER assume or invent a date. NEVER guess "today is [date]".**
246
+ Always call time_verifier first, then use the resolved date in your tool call or delegation prompt.
223
247
 
224
248
  Rules:
225
249
  1. For conversation-only requests (greetings, conceptual explanation, memory follow-up, statements of fact, sharing personal information), answer directly. DO NOT create tasks or delegate for simple statements like "I have two cats" or "My name is John". Sati will automatically memorize facts in the background ( **ALWAYS** use SATI Memories to review or retrieve these facts if needed).
@@ -326,10 +350,14 @@ ${SmithRegistry.getInstance().getSystemPromptSection()}
326
350
  // Load existing history from database in reverse order (most recent first)
327
351
  let previousMessages = await this.history.getMessages();
328
352
  previousMessages = previousMessages.reverse();
353
+ // Propagate current session to Apoc so its token usage lands in the right session
354
+ const currentSessionId = (this.history instanceof SQLiteChatMessageHistory)
355
+ ? this.history.currentSessionId
356
+ : undefined;
329
357
  // Sati Middleware: Retrieval
330
358
  let memoryMessage = null;
331
359
  try {
332
- memoryMessage = await this.satiMiddleware.beforeAgent(message, previousMessages);
360
+ memoryMessage = await this.satiMiddleware.beforeAgent(message, previousMessages, currentSessionId);
333
361
  if (memoryMessage) {
334
362
  this.display.log('Sati memory retrieved.', { source: 'Sati' });
335
363
  }
@@ -354,10 +382,6 @@ Use it to inform your response and tool selection (if needed), but do not assume
354
382
  }
355
383
  messages.push(...previousMessages);
356
384
  messages.push(userMessage);
357
- // Propagate current session to Apoc so its token usage lands in the right session
358
- const currentSessionId = (this.history instanceof SQLiteChatMessageHistory)
359
- ? this.history.currentSessionId
360
- : undefined;
361
385
  Apoc.setSessionId(currentSessionId);
362
386
  Neo.setSessionId(currentSessionId);
363
387
  Trinity.setSessionId(currentSessionId);
@@ -403,7 +427,12 @@ Use it to inform your response and tool selection (if needed), but do not assume
403
427
  // New messages start after the inputs.
404
428
  const startNewMessagesIndex = messages.length;
405
429
  const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
406
- // console.log('New generated messages', newGeneratedMessages);
430
+ // Emit tool_call audit events for Oracle's independent tool calls.
431
+ // Delegation tools (apoc/neo/trinity/smith/skill) are already audited
432
+ // inside buildDelegationTool or the task system — skip them here.
433
+ emitToolAuditEvents(newGeneratedMessages, currentSessionId ?? 'default', 'oracle', {
434
+ skipTools: ORACLE_DELEGATION_TOOLS,
435
+ });
407
436
  // Inject provider/model metadata and duration into all new AI messages
408
437
  for (const msg of newGeneratedMessages) {
409
438
  msg.provider_metadata = {
@@ -491,8 +520,19 @@ Use it to inform your response and tool selection (if needed), but do not assume
491
520
  this.display.log('Response generated.', { source: 'Oracle' });
492
521
  // Sati Middleware: skip memory evaluation for delegation-only acknowledgements.
493
522
  if (!delegatedThisTurn && !blockedSyntheticDelegationAck) {
494
- this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage], currentSessionId)
495
- .catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
523
+ const sessionKey = currentSessionId ?? 'default';
524
+ const turnCount = (this.satiTurnCounters.get(sessionKey) ?? 0) + 1;
525
+ this.satiTurnCounters.set(sessionKey, turnCount);
526
+ const satiCfg = ConfigManager.getInstance().getSatiConfig();
527
+ const evalInterval = satiCfg.evaluation_interval ?? 1;
528
+ const contextWindow = this.config.llm?.context_window ?? this.config.memory?.limit ?? 100;
529
+ const effectiveInterval = Math.min(evalInterval, contextWindow);
530
+ const shouldEval = turnCount % effectiveInterval === 0;
531
+ if (shouldEval) {
532
+ this.display.log(`Sati eval triggered (turn ${turnCount}, effective interval ${effectiveInterval})`, { source: 'Sati', level: 'debug' });
533
+ this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage], currentSessionId)
534
+ .catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
535
+ }
496
536
  }
497
537
  return responseContent;
498
538
  }
@@ -604,7 +644,16 @@ Use it to inform your response and tool selection (if needed), but do not assume
604
644
  await Neo.refreshDelegateCatalog().catch(() => { });
605
645
  await Trinity.refreshDelegateCatalog().catch(() => { });
606
646
  updateSkillToolDescriptions();
607
- this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool, SkillExecuteTool, SkillDelegateTool, timeVerifierTool, ...chronosTools]);
647
+ this.provider = await ProviderFactory.create(this.config.llm, [
648
+ TaskQueryTool,
649
+ Neo.getInstance().createDelegateTool(),
650
+ Apoc.getInstance().createDelegateTool(),
651
+ Trinity.getInstance().createDelegateTool(),
652
+ SkillExecuteTool,
653
+ SkillDelegateTool,
654
+ timeVerifierTool,
655
+ ...chronosTools,
656
+ ]);
608
657
  await Neo.getInstance().reload();
609
658
  this.display.log(`Oracle and Neo tools reloaded`, { source: 'Oracle' });
610
659
  }
@@ -8,6 +8,7 @@ import { TaskRequestContext } from "../tasks/context.js";
8
8
  import { DisplayManager } from "../display.js";
9
9
  import { SkillRegistry } from "./registry.js";
10
10
  import { executeKeymakerTask } from "../keymaker.js";
11
+ import { AuditRepository } from "../audit/repository.js";
11
12
  // ============================================================================
12
13
  // skill_execute - Synchronous execution
13
14
  // ============================================================================
@@ -67,6 +68,30 @@ export const SkillExecuteTool = tool(async ({ skillName, objective }) => {
67
68
  source: "SkillExecuteTool",
68
69
  level: "info",
69
70
  });
71
+ // Emit audit events for sync execution (async path is handled by TaskWorker)
72
+ const audit = AuditRepository.getInstance();
73
+ if (result.usage && (result.usage.inputTokens > 0 || result.usage.outputTokens > 0)) {
74
+ audit.insert({
75
+ session_id: sessionId,
76
+ event_type: 'llm_call',
77
+ agent: 'keymaker',
78
+ provider: result.usage.provider,
79
+ model: result.usage.model,
80
+ input_tokens: result.usage.inputTokens,
81
+ output_tokens: result.usage.outputTokens,
82
+ duration_ms: result.usage.durationMs,
83
+ status: 'success',
84
+ metadata: { step_count: result.usage.stepCount },
85
+ });
86
+ }
87
+ audit.insert({
88
+ session_id: sessionId,
89
+ event_type: 'skill_executed',
90
+ agent: 'keymaker',
91
+ tool_name: skillName,
92
+ duration_ms: result.usage?.durationMs,
93
+ status: 'success',
94
+ });
70
95
  return result;
71
96
  }
72
97
  catch (err) {
@@ -0,0 +1,89 @@
1
+ import { AIMessage, ToolMessage } from "@langchain/core/messages";
2
+ import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
3
+ import { AuditRepository } from "./audit/repository.js";
4
+ /** Extract token usage from a LangChain message using 4-fallback chain. */
5
+ export function extractRawUsage(lastMessage) {
6
+ return lastMessage.usage_metadata
7
+ ?? lastMessage.response_metadata?.usage
8
+ ?? lastMessage.response_metadata?.tokenUsage
9
+ ?? lastMessage.usage;
10
+ }
11
+ /** Persist an agent's AI message to SQLite with provider + agent metadata. */
12
+ export async function persistAgentMessage(agentName, content, config, sessionId, rawUsage, durationMs) {
13
+ const history = new SQLiteChatMessageHistory({ sessionId });
14
+ try {
15
+ const persisted = new AIMessage(content);
16
+ if (rawUsage)
17
+ persisted.usage_metadata = rawUsage;
18
+ persisted.provider_metadata = { provider: config.provider, model: config.model };
19
+ persisted.agent_metadata = { agent: agentName };
20
+ persisted.duration_ms = durationMs;
21
+ await history.addMessage(persisted);
22
+ }
23
+ finally {
24
+ history.close();
25
+ }
26
+ }
27
+ /**
28
+ * Emit audit events for each tool call found in the given messages.
29
+ * Scans AIMessage.tool_calls and matches results from ToolMessage instances.
30
+ * - defaultEventType: 'tool_call' for DevKit/internal, 'mcp_tool' for MCP tools (default: 'tool_call')
31
+ * - skipTools: tool names to ignore entirely (e.g. delegation tools already audited elsewhere)
32
+ * - internalToolNames: tool names that should always use 'tool_call' even when defaultEventType is 'mcp_tool'
33
+ */
34
+ export function emitToolAuditEvents(messages, sessionId, agent, opts) {
35
+ try {
36
+ const defaultEventType = opts?.defaultEventType ?? 'tool_call';
37
+ const skipTools = opts?.skipTools;
38
+ const internalToolNames = opts?.internalToolNames;
39
+ const toolResults = new Map();
40
+ for (const msg of messages) {
41
+ if (msg instanceof ToolMessage) {
42
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
43
+ toolResults.set(msg.tool_call_id, content);
44
+ }
45
+ }
46
+ for (const msg of messages) {
47
+ if (!(msg instanceof AIMessage))
48
+ continue;
49
+ const toolCalls = msg.tool_calls ?? [];
50
+ for (const tc of toolCalls) {
51
+ if (!tc?.name)
52
+ continue;
53
+ if (skipTools?.has(tc.name))
54
+ continue;
55
+ const result = tc.id ? toolResults.get(tc.id) : undefined;
56
+ const isError = typeof result === 'string' && /^error:/i.test(result.trim());
57
+ const eventType = internalToolNames?.has(tc.name) ? 'tool_call' : defaultEventType;
58
+ const meta = {};
59
+ if (tc.args && Object.keys(tc.args).length > 0)
60
+ meta.args = tc.args;
61
+ if (result !== undefined)
62
+ meta.result = result.length > 500 ? result.slice(0, 500) + '…' : result;
63
+ AuditRepository.getInstance().insert({
64
+ session_id: sessionId,
65
+ event_type: eventType,
66
+ agent,
67
+ tool_name: tc.name,
68
+ status: isError ? 'error' : 'success',
69
+ metadata: Object.keys(meta).length > 0 ? meta : undefined,
70
+ });
71
+ }
72
+ }
73
+ }
74
+ catch { /* non-critical */ }
75
+ }
76
+ /** Assemble an AgentResult from extracted usage data. */
77
+ export function buildAgentResult(content, config, rawUsage, durationMs, stepCount) {
78
+ return {
79
+ output: content,
80
+ usage: {
81
+ provider: config.provider,
82
+ model: config.model,
83
+ inputTokens: rawUsage?.input_tokens ?? rawUsage?.prompt_tokens ?? 0,
84
+ outputTokens: rawUsage?.output_tokens ?? rawUsage?.completion_tokens ?? 0,
85
+ durationMs,
86
+ stepCount,
87
+ },
88
+ };
89
+ }
@@ -188,6 +188,57 @@ export class TaskRepository {
188
188
  const rows = this.db.prepare(query).all(...params);
189
189
  return rows.map((row) => this.deserializeTask(row));
190
190
  }
191
+ countTasks(filters) {
192
+ const params = [];
193
+ let query = 'SELECT COUNT(*) as cnt FROM tasks WHERE 1=1';
194
+ if (filters?.status) {
195
+ query += ' AND status = ?';
196
+ params.push(filters.status);
197
+ }
198
+ if (filters?.agent) {
199
+ query += ' AND agent = ?';
200
+ params.push(filters.agent);
201
+ }
202
+ if (filters?.origin_channel) {
203
+ query += ' AND origin_channel = ?';
204
+ params.push(filters.origin_channel);
205
+ }
206
+ if (filters?.session_id) {
207
+ query += ' AND session_id = ?';
208
+ params.push(filters.session_id);
209
+ }
210
+ const row = this.db.prepare(query).get(...params);
211
+ return row.cnt;
212
+ }
213
+ listTasksPaginated(filters) {
214
+ const page = Math.max(1, filters?.page ?? 1);
215
+ const per_page = Math.min(100, Math.max(1, filters?.per_page ?? 20));
216
+ const offset = (page - 1) * per_page;
217
+ const total = this.countTasks(filters);
218
+ const total_pages = Math.ceil(total / per_page);
219
+ const params = [];
220
+ let query = 'SELECT * FROM tasks WHERE 1=1';
221
+ if (filters?.status) {
222
+ query += ' AND status = ?';
223
+ params.push(filters.status);
224
+ }
225
+ if (filters?.agent) {
226
+ query += ' AND agent = ?';
227
+ params.push(filters.agent);
228
+ }
229
+ if (filters?.origin_channel) {
230
+ query += ' AND origin_channel = ?';
231
+ params.push(filters.origin_channel);
232
+ }
233
+ if (filters?.session_id) {
234
+ query += ' AND session_id = ?';
235
+ params.push(filters.session_id);
236
+ }
237
+ query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
238
+ params.push(per_page, offset);
239
+ const rows = this.db.prepare(query).all(...params);
240
+ return { data: rows.map((row) => this.deserializeTask(row)), total, page, per_page, total_pages };
241
+ }
191
242
  getStats() {
192
243
  const rows = this.db.prepare(`
193
244
  SELECT status, COUNT(*) as cnt
@@ -65,7 +65,12 @@ export class TaskWorker {
65
65
  switch (task.agent) {
66
66
  case 'apoc': {
67
67
  const apoc = Apoc.getInstance();
68
- result = await apoc.execute(task.input, task.context ?? undefined, task.session_id);
68
+ result = await apoc.execute(task.input, task.context ?? undefined, task.session_id, {
69
+ origin_channel: task.origin_channel,
70
+ session_id: task.session_id,
71
+ origin_message_id: task.origin_message_id ?? undefined,
72
+ origin_user_id: task.origin_user_id ?? undefined,
73
+ });
69
74
  break;
70
75
  }
71
76
  case 'neo': {
@@ -80,7 +85,12 @@ export class TaskWorker {
80
85
  }
81
86
  case 'trinit': {
82
87
  const trinity = Trinity.getInstance();
83
- result = await trinity.execute(task.input, task.context ?? undefined, task.session_id);
88
+ result = await trinity.execute(task.input, task.context ?? undefined, task.session_id, {
89
+ origin_channel: task.origin_channel,
90
+ session_id: task.session_id,
91
+ origin_message_id: task.origin_message_id ?? undefined,
92
+ origin_user_id: task.origin_user_id ?? undefined,
93
+ });
84
94
  break;
85
95
  }
86
96
  case 'keymaker': {
@@ -2,16 +2,24 @@ import { GoogleGenAI } from '@google/genai';
2
2
  import OpenAI from 'openai';
3
3
  import { OpenRouter } from '@openrouter/sdk';
4
4
  import fs from 'fs';
5
+ import { parseFile } from 'music-metadata';
5
6
  /**
6
- * Estimates audio duration in seconds based on file size and a typical bitrate.
7
- * Uses 32 kbps (4000 bytes/sec) as a conservative baseline for compressed audio (OGG, MP3, etc.).
8
- * This is an approximation — actual duration depends on encoding settings.
7
+ * Returns the actual audio duration in seconds by parsing the file header.
8
+ * Falls back to a size-based estimate (~32 kbps) if parsing fails.
9
9
  */
10
- function estimateAudioDurationSeconds(filePath) {
10
+ async function getAudioDurationSeconds(filePath) {
11
+ try {
12
+ const metadata = await parseFile(filePath);
13
+ const duration = metadata.format.duration;
14
+ if (duration != null && duration > 0)
15
+ return Math.round(duration);
16
+ }
17
+ catch {
18
+ // fall through to estimate
19
+ }
11
20
  try {
12
21
  const stats = fs.statSync(filePath);
13
- const bytesPerSecond = 4000; // ~32 kbps
14
- return Math.round(stats.size / bytesPerSecond);
22
+ return Math.round(stats.size / 4000); // ~32 kbps fallback
15
23
  }
16
24
  catch {
17
25
  return 0;
@@ -57,7 +65,7 @@ class GeminiTelephonist {
57
65
  input_token_details: {
58
66
  cache_read: usage?.cachedContentTokenCount ?? 0
59
67
  },
60
- audio_duration_seconds: estimateAudioDurationSeconds(filePath)
68
+ audio_duration_seconds: await getAudioDurationSeconds(filePath)
61
69
  };
62
70
  return { text, usage: usageMetadata };
63
71
  }
@@ -91,7 +99,7 @@ class WhisperTelephonist {
91
99
  input_tokens: 0,
92
100
  output_tokens: 0,
93
101
  total_tokens: 0,
94
- audio_duration_seconds: estimateAudioDurationSeconds(filePath)
102
+ audio_duration_seconds: await getAudioDurationSeconds(filePath)
95
103
  };
96
104
  return { text, usage: usageMetadata };
97
105
  }
@@ -148,7 +156,7 @@ class OpenRouterTelephonist {
148
156
  input_tokens: usage?.prompt_tokens ?? 0,
149
157
  output_tokens: usage?.completion_tokens ?? 0,
150
158
  total_tokens: usage?.total_tokens ?? 0,
151
- audio_duration_seconds: estimateAudioDurationSeconds(filePath)
159
+ audio_duration_seconds: await getAudioDurationSeconds(filePath)
152
160
  };
153
161
  return { text, usage: usageMetadata };
154
162
  }
@@ -0,0 +1,120 @@
1
+ import { tool } from "@langchain/core/tools";
2
+ import { z } from "zod";
3
+ import { TaskRepository } from "../tasks/repository.js";
4
+ import { TaskRequestContext } from "../tasks/context.js";
5
+ import { compositeDelegationError, isLikelyCompositeDelegationTask } from "./delegation-guard.js";
6
+ import { DisplayManager } from "../display.js";
7
+ import { ChannelRegistry } from "../../channels/registry.js";
8
+ import { AuditRepository } from "../audit/repository.js";
9
+ /**
10
+ * Factory that builds a delegation StructuredTool for Apoc/Neo/Trinity.
11
+ * Handles: composite guard, sync branch (notify→execute→audit→increment),
12
+ * async branch (dedup→canEnqueue→createTask→setAck).
13
+ */
14
+ export function buildDelegationTool(opts) {
15
+ const { name, agentKey, agentLabel, auditAgent, isSync, notifyText, executeSync, } = opts;
16
+ const toolInstance = tool(async ({ task, context }) => {
17
+ const display = DisplayManager.getInstance();
18
+ const source = `${agentLabel}DelegateTool`;
19
+ try {
20
+ if (isLikelyCompositeDelegationTask(task)) {
21
+ display.log(`${agentLabel} delegation rejected (non-atomic task): ${task.slice(0, 140)}`, {
22
+ source,
23
+ level: "warning",
24
+ });
25
+ return compositeDelegationError();
26
+ }
27
+ // ── Sync mode: execute inline and return result directly ──
28
+ if (isSync()) {
29
+ display.log(`${agentLabel} executing synchronously: ${task.slice(0, 80)}...`, {
30
+ source,
31
+ level: "info",
32
+ });
33
+ const ctx = TaskRequestContext.get();
34
+ const sessionId = ctx?.session_id ?? "default";
35
+ if (ctx?.origin_channel && ctx.origin_user_id && ctx.origin_channel !== 'api' && ctx.origin_channel !== 'ui') {
36
+ ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, notifyText)
37
+ .catch(() => { });
38
+ }
39
+ try {
40
+ const result = await executeSync(task, context, sessionId, ctx);
41
+ TaskRequestContext.incrementSyncDelegation();
42
+ display.log(`${agentLabel} sync execution completed.`, { source, level: "info" });
43
+ if (result.usage) {
44
+ AuditRepository.getInstance().insert({
45
+ session_id: sessionId,
46
+ event_type: 'llm_call',
47
+ agent: auditAgent,
48
+ provider: result.usage.provider,
49
+ model: result.usage.model,
50
+ input_tokens: result.usage.inputTokens,
51
+ output_tokens: result.usage.outputTokens,
52
+ duration_ms: result.usage.durationMs,
53
+ status: 'success',
54
+ metadata: { step_count: result.usage.stepCount, mode: 'sync' },
55
+ });
56
+ }
57
+ return result.output;
58
+ }
59
+ catch (syncErr) {
60
+ TaskRequestContext.incrementSyncDelegation();
61
+ display.log(`${agentLabel} sync execution failed: ${syncErr.message}`, { source, level: "error" });
62
+ return `❌ ${agentLabel} error: ${syncErr.message}`;
63
+ }
64
+ }
65
+ // ── Async mode (default): create background task ──
66
+ const existingAck = TaskRequestContext.findDuplicateDelegation(agentKey, task);
67
+ if (existingAck) {
68
+ display.log(`${agentLabel} delegation deduplicated. Reusing task ${existingAck.task_id}.`, {
69
+ source,
70
+ level: "info",
71
+ });
72
+ return `Task ${existingAck.task_id} already queued for ${existingAck.agent} execution.`;
73
+ }
74
+ if (!TaskRequestContext.canEnqueueDelegation()) {
75
+ display.log(`${agentLabel} delegation blocked by per-turn limit.`, { source, level: "warning" });
76
+ return "Delegation limit reached for this user turn. Split the request or wait for current tasks.";
77
+ }
78
+ const ctx = TaskRequestContext.get();
79
+ const repository = TaskRepository.getInstance();
80
+ const created = repository.createTask({
81
+ agent: agentKey,
82
+ input: task,
83
+ context: context ?? null,
84
+ origin_channel: ctx?.origin_channel ?? "api",
85
+ session_id: ctx?.session_id ?? "default",
86
+ origin_message_id: ctx?.origin_message_id ?? null,
87
+ origin_user_id: ctx?.origin_user_id ?? null,
88
+ max_attempts: 3,
89
+ });
90
+ TaskRequestContext.setDelegationAck({ task_id: created.id, agent: agentKey, task });
91
+ display.log(`${agentLabel} task created: ${created.id}`, {
92
+ source,
93
+ level: "info",
94
+ meta: {
95
+ agent: created.agent,
96
+ origin_channel: created.origin_channel,
97
+ session_id: created.session_id,
98
+ input: created.input,
99
+ },
100
+ });
101
+ return `Task ${created.id} queued for ${agentLabel} execution.`;
102
+ }
103
+ catch (err) {
104
+ display.log(`${source} error: ${err.message}`, { source, level: "error" });
105
+ return `${agentLabel} task enqueue failed: ${err.message}`;
106
+ }
107
+ }, {
108
+ name,
109
+ description: typeof opts.description === 'string' ? opts.description : opts.description(),
110
+ schema: z.object({
111
+ task: z.string().describe(`Clear task description **in the user's language**`),
112
+ context: z.string().optional().describe(`Optional context from the conversation **in the user's language**`),
113
+ }),
114
+ });
115
+ // If description is dynamic, expose an updater method on the tool instance
116
+ if (typeof opts.description === 'function') {
117
+ toolInstance._descriptionFn = opts.description;
118
+ }
119
+ return toolInstance;
120
+ }
@@ -1,7 +1,5 @@
1
1
  // Export all tools from the tools module
2
2
  export * from './morpheus-tools.js';
3
- export * from './apoc-tool.js';
4
- export * from './neo-tool.js';
5
3
  export * from './smith-tool.js';
6
4
  export * from './chronos-tools.js';
7
5
  export * from './time-verify-tools.js';
@@ -115,17 +115,24 @@ export const timeVerifierTool = tool(async ({ text, timezone }) => {
115
115
  }, {
116
116
  name: "time_verifier",
117
117
  description: `
118
- Detects and resolves relative time expressions in user input.
119
- Supports multiple languages (English, Portuguese, Spanish).
120
- Use this tool whenever the user mentions words like:
121
- today, tomorrow, yesterday, this week, next week,
122
- hoje, amanhã, ontem, próxima semana,
123
- hoy, mañana, ayer, la próxima semana, etc.
118
+ Resolves temporal expressions into concrete ISO dates anchored to the current date and timezone.
119
+ Use this tool in TWO scenarios:
124
120
 
125
- Returns resolved ISO dates based on the current time and timezone configuration.
121
+ 1. EXPLICIT temporal expression pass the expression itself as "text":
122
+ - "tomorrow at 9am", "next Friday", "in 3 days", "this week"
123
+ - "amanhã às 9h", "próxima sexta", "em 3 dias", "esta semana"
124
+
125
+ 2. IMPLICIT temporal query — the user asks about something whose answer depends on today's date,
126
+ but without stating a date explicitly. Pass "hoje" as text to resolve today's date:
127
+ - "próximo jogo do Flamengo" → you need today's date to search for games AFTER today
128
+ - "próximo episódio de X", "latest release", "resultado mais recente"
129
+ - Any "next", "upcoming", "latest", "current", "recent", "agora" query about real-world events
130
+
131
+ After calling this tool, incorporate the resolved ISO date into your search query or delegation prompt
132
+ so the agent knows the time context (e.g. "find Flamengo games after 2026-03-01").
126
133
  `,
127
134
  schema: z.object({
128
- text: z.string().describe("User input text containing time expressions"),
135
+ text: z.string().describe('Temporal expression to resolve (e.g. "amanhã", "next Friday") OR pass "hoje" to get today\'s date when the query has implicit temporal intent'),
129
136
  timezone: z.string().optional().describe("Optional timezone override. Defaults to system configuration."),
130
137
  }),
131
138
  });