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.
- package/dist/channels/telegram.js +43 -0
- package/dist/config/manager.js +3 -1
- package/dist/config/schemas.js +2 -0
- package/dist/http/api.js +70 -13
- package/dist/http/routers/chronos.js +12 -1
- package/dist/http/webhooks-router.js +11 -3
- package/dist/runtime/ISubagent.js +1 -0
- package/dist/runtime/apoc.js +49 -39
- package/dist/runtime/audit/repository.js +193 -6
- package/dist/runtime/chronos/repository.js +35 -0
- package/dist/runtime/keymaker.js +6 -30
- package/dist/runtime/memory/sati/index.js +22 -1
- package/dist/runtime/memory/sati/repository.js +39 -1
- package/dist/runtime/memory/sqlite.js +16 -3
- package/dist/runtime/neo.js +78 -34
- package/dist/runtime/oracle.js +68 -19
- package/dist/runtime/skills/tool.js +25 -0
- package/dist/runtime/subagent-utils.js +89 -0
- package/dist/runtime/tasks/repository.js +51 -0
- package/dist/runtime/tasks/worker.js +12 -2
- package/dist/runtime/telephonist.js +17 -9
- package/dist/runtime/tools/delegation-utils.js +120 -0
- package/dist/runtime/tools/index.js +0 -2
- package/dist/runtime/tools/time-verify-tools.js +15 -8
- package/dist/runtime/trinity.js +50 -34
- package/dist/runtime/webhooks/repository.js +31 -0
- package/dist/types/config.js +2 -0
- package/dist/types/pagination.js +1 -0
- package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +1 -0
- package/dist/ui/assets/Chat-CjxeAQmd.js +41 -0
- package/dist/ui/assets/Chronos-BAjeLobF.js +1 -0
- package/dist/ui/assets/{ConfirmationModal-MyIaIK_Z.js → ConfirmationModal-fvgnOWTY.js} +1 -1
- package/dist/ui/assets/{Dashboard-C52jjru9.js → Dashboard-Ca5mSefz.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-B0nDocEK.js → DeleteConfirmationModal-A8EmnHoa.js} +1 -1
- package/dist/ui/assets/{Logs-fDrGC9Lq.js → Logs-CYu7se7R.js} +1 -1
- package/dist/ui/assets/MCPManager-DsDA_ZVT.js +1 -0
- package/dist/ui/assets/ModelPricing-DnSm_Nh-.js +1 -0
- package/dist/ui/assets/Notifications-CiljQzvM.js +1 -0
- package/dist/ui/assets/Pagination-JsiwxVNQ.js +1 -0
- package/dist/ui/assets/SatiMemories-rnO2b0LG.js +1 -0
- package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +9 -0
- package/dist/ui/assets/{Settings-Cgd4dJdc.js → Settings-OQlHAJoy.js} +6 -4
- package/dist/ui/assets/Skills-Crsybug0.js +7 -0
- package/dist/ui/assets/Smiths-wm90jRDT.js +1 -0
- package/dist/ui/assets/Tasks-C5FMu_Yu.js +1 -0
- package/dist/ui/assets/TrinityDatabases-BzYfecKI.js +1 -0
- package/dist/ui/assets/{UsageStats-EEwfbJ6C.js → UsageStats-CBo2vW2n.js} +1 -1
- package/dist/ui/assets/{WebhookManager-CyVUcscY.js → WebhookManager-0tDFkfHd.js} +1 -1
- package/dist/ui/assets/audit-B-F8XPLi.js +1 -0
- package/dist/ui/assets/chronos-BvMxfBQH.js +1 -0
- package/dist/ui/assets/{config-cslLZS3q.js → config-DteVgNGR.js} +1 -1
- package/dist/ui/assets/index-Cwqr-n0Y.js +10 -0
- package/dist/ui/assets/index-DcfyUdLI.css +1 -0
- package/dist/ui/assets/{mcp-M0iDC0mj.js → mcp-DxzodOdH.js} +1 -1
- package/dist/ui/assets/{skills-BvaaqiOT.js → skills--hAyQnmG.js} +1 -1
- package/dist/ui/assets/{stats-DALk3GOj.js → stats-Cibaisqd.js} +1 -1
- package/dist/ui/assets/vendor-icons-BVuQI-6R.js +1 -0
- package/dist/ui/index.html +3 -3
- package/dist/ui/sw.js +1 -1
- package/package.json +2 -1
- package/dist/runtime/tools/apoc-tool.js +0 -157
- package/dist/runtime/tools/neo-tool.js +0 -172
- package/dist/runtime/tools/trinity-tool.js +0 -157
- package/dist/ui/assets/Chat-Cx2OgATp.js +0 -38
- package/dist/ui/assets/Chronos--mut48fM.js +0 -1
- package/dist/ui/assets/MCPManager-CtRQzwM8.js +0 -1
- package/dist/ui/assets/ModelPricing-d4EYrGko.js +0 -1
- package/dist/ui/assets/Notifications-Dkqug57C.js +0 -1
- package/dist/ui/assets/SatiMemories-DykYVHgi.js +0 -1
- package/dist/ui/assets/SessionAudit-Bk0-DpW0.js +0 -9
- package/dist/ui/assets/Skills-DSi313oC.js +0 -7
- package/dist/ui/assets/Smiths-DLys0BWT.js +0 -1
- package/dist/ui/assets/Tasks-B1MbPNUQ.js +0 -1
- package/dist/ui/assets/TrinityDatabases-B5SeHOLt.js +0 -1
- package/dist/ui/assets/chronos-BVRpP__j.js +0 -1
- package/dist/ui/assets/index-CpVvCthh.js +0 -10
- package/dist/ui/assets/index-QQyZIsmH.css +0 -1
- package/dist/ui/assets/vendor-icons-DLvvGkeN.js +0 -1
package/dist/runtime/oracle.js
CHANGED
|
@@ -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 = [
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
495
|
-
|
|
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, [
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|
|
@@ -115,17 +115,24 @@ export const timeVerifierTool = tool(async ({ text, timezone }) => {
|
|
|
115
115
|
}, {
|
|
116
116
|
name: "time_verifier",
|
|
117
117
|
description: `
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
});
|