morpheus-cli 0.8.6 → 0.8.8
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/http/api.js +49 -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/repository.js +37 -0
- package/dist/runtime/memory/sqlite.js +16 -3
- package/dist/runtime/neo.js +78 -34
- package/dist/runtime/oracle.js +31 -6
- package/dist/runtime/skills/tool.js +25 -0
- package/dist/runtime/smiths/delegator.js +21 -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/trinity.js +50 -34
- package/dist/runtime/webhooks/repository.js +31 -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-Bx-GtD9B.js → ConfirmationModal-fvgnOWTY.js} +1 -1
- package/dist/ui/assets/{Dashboard-DDyN_X-J.js → Dashboard-Ca5mSefz.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-DIXbckY8.js → DeleteConfirmationModal-A8EmnHoa.js} +1 -1
- package/dist/ui/assets/{Logs-dzPLW45U.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-DNDe62-H.js → Settings-OQlHAJoy.js} +1 -1
- 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-doBLB7Lc.js → UsageStats-CBo2vW2n.js} +1 -1
- package/dist/ui/assets/{WebhookManager-D3A5pdjC.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-DX3Xb0XE.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-DfhJYN14.js → mcp-DxzodOdH.js} +1 -1
- package/dist/ui/assets/{skills-BPjq0qV7.js → skills--hAyQnmG.js} +1 -1
- package/dist/ui/assets/{stats-DHCRNkJp.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-CO15OnaY.js +0 -38
- package/dist/ui/assets/Chronos-CUZDQLh2.js +0 -1
- package/dist/ui/assets/MCPManager-CRHWR4S7.js +0 -1
- package/dist/ui/assets/ModelPricing-TRBesy0r.js +0 -1
- package/dist/ui/assets/Notifications-DMke7Dr7.js +0 -1
- package/dist/ui/assets/SatiMemories-CaLrgdZV.js +0 -1
- package/dist/ui/assets/SessionAudit-DedGO5XK.js +0 -9
- package/dist/ui/assets/Skills-KUhW7UXP.js +0 -7
- package/dist/ui/assets/Smiths-Btoqw4Ex.js +0 -1
- package/dist/ui/assets/Tasks-cwA25Hq2.js +0 -1
- package/dist/ui/assets/TrinityDatabases-CQhettEJ.js +0 -1
- package/dist/ui/assets/chronos-DlDM2UBT.js +0 -1
- package/dist/ui/assets/index-CQIUjucB.js +0 -10
- package/dist/ui/assets/index-DAh3q_hR.css +0 -1
- package/dist/ui/assets/vendor-icons-DLvvGkeN.js +0 -1
|
@@ -156,6 +156,41 @@ export class ChronosRepository {
|
|
|
156
156
|
const rows = this.db.prepare(query).all(...params);
|
|
157
157
|
return rows.map((r) => this.deserializeJob(r));
|
|
158
158
|
}
|
|
159
|
+
countJobs(filters) {
|
|
160
|
+
const params = [];
|
|
161
|
+
let query = 'SELECT COUNT(*) as cnt FROM chronos_jobs WHERE 1=1';
|
|
162
|
+
if (filters?.enabled !== undefined) {
|
|
163
|
+
query += ' AND enabled = ?';
|
|
164
|
+
params.push(filters.enabled ? 1 : 0);
|
|
165
|
+
}
|
|
166
|
+
if (filters?.created_by) {
|
|
167
|
+
query += ' AND created_by = ?';
|
|
168
|
+
params.push(filters.created_by);
|
|
169
|
+
}
|
|
170
|
+
const row = this.db.prepare(query).get(...params);
|
|
171
|
+
return row.cnt;
|
|
172
|
+
}
|
|
173
|
+
listJobsPaginated(filters) {
|
|
174
|
+
const page = Math.max(1, filters?.page ?? 1);
|
|
175
|
+
const per_page = Math.min(100, Math.max(1, filters?.per_page ?? 20));
|
|
176
|
+
const offset = (page - 1) * per_page;
|
|
177
|
+
const total = this.countJobs(filters);
|
|
178
|
+
const total_pages = Math.ceil(total / per_page);
|
|
179
|
+
const params = [];
|
|
180
|
+
let query = 'SELECT * FROM chronos_jobs WHERE 1=1';
|
|
181
|
+
if (filters?.enabled !== undefined) {
|
|
182
|
+
query += ' AND enabled = ?';
|
|
183
|
+
params.push(filters.enabled ? 1 : 0);
|
|
184
|
+
}
|
|
185
|
+
if (filters?.created_by) {
|
|
186
|
+
query += ' AND created_by = ?';
|
|
187
|
+
params.push(filters.created_by);
|
|
188
|
+
}
|
|
189
|
+
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
|
190
|
+
params.push(per_page, offset);
|
|
191
|
+
const rows = this.db.prepare(query).all(...params);
|
|
192
|
+
return { data: rows.map((r) => this.deserializeJob(r)), total, page, per_page, total_pages };
|
|
193
|
+
}
|
|
159
194
|
updateJob(id, patch) {
|
|
160
195
|
const now = Date.now();
|
|
161
196
|
const sets = ['updated_at = ?'];
|
package/dist/runtime/keymaker.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Construtor } from "./tools/factory.js";
|
|
|
8
8
|
import { morpheusTools } from "./tools/index.js";
|
|
9
9
|
import { SkillRegistry } from "./skills/registry.js";
|
|
10
10
|
import { TaskRequestContext } from "./tasks/context.js";
|
|
11
|
-
import {
|
|
11
|
+
import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
|
|
12
12
|
/**
|
|
13
13
|
* Keymaker is a specialized agent for executing skills.
|
|
14
14
|
* "The one who opens any door" - has access to ALL tools:
|
|
@@ -122,38 +122,14 @@ CRITICAL — NEVER FABRICATE DATA:
|
|
|
122
122
|
const content = typeof lastMessage.content === "string"
|
|
123
123
|
? lastMessage.content
|
|
124
124
|
: JSON.stringify(lastMessage.content);
|
|
125
|
-
// Persist message with token usage metadata (like Trinity/Neo/Apoc)
|
|
126
125
|
const keymakerConfig = this.config.keymaker || this.config.llm;
|
|
127
126
|
const targetSession = taskContext?.session_id ?? "keymaker";
|
|
128
|
-
const rawUsage = lastMessage
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
|
|
133
|
-
try {
|
|
134
|
-
const persisted = new AIMessage(content);
|
|
135
|
-
if (rawUsage)
|
|
136
|
-
persisted.usage_metadata = rawUsage;
|
|
137
|
-
persisted.provider_metadata = { provider: keymakerConfig.provider, model: keymakerConfig.model };
|
|
138
|
-
persisted.agent_metadata = { agent: 'keymaker' };
|
|
139
|
-
persisted.duration_ms = durationMs;
|
|
140
|
-
await history.addMessage(persisted);
|
|
141
|
-
}
|
|
142
|
-
finally {
|
|
143
|
-
history.close();
|
|
144
|
-
}
|
|
127
|
+
const rawUsage = extractRawUsage(lastMessage);
|
|
128
|
+
const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
|
|
129
|
+
await persistAgentMessage('keymaker', content, keymakerConfig, targetSession, rawUsage, durationMs);
|
|
130
|
+
emitToolAuditEvents(response.messages.slice(2), targetSession, 'keymaker');
|
|
145
131
|
this.display.log(`Keymaker completed skill "${this.skillName}" execution`, { source: "Keymaker" });
|
|
146
|
-
return
|
|
147
|
-
output: content,
|
|
148
|
-
usage: {
|
|
149
|
-
provider: keymakerConfig.provider,
|
|
150
|
-
model: keymakerConfig.model,
|
|
151
|
-
inputTokens: rawUsage?.input_tokens ?? 0,
|
|
152
|
-
outputTokens: rawUsage?.output_tokens ?? 0,
|
|
153
|
-
durationMs,
|
|
154
|
-
stepCount: response.messages.filter((m) => m instanceof AIMessage).length,
|
|
155
|
-
},
|
|
156
|
-
};
|
|
132
|
+
return buildAgentResult(content, keymakerConfig, rawUsage, durationMs, stepCount);
|
|
157
133
|
}
|
|
158
134
|
catch (err) {
|
|
159
135
|
this.display.log(`Keymaker execution error: ${err.message}`, { source: "Keymaker", level: "error" });
|
|
@@ -413,6 +413,43 @@ export class SatiRepository {
|
|
|
413
413
|
const rows = this.db.prepare('SELECT * FROM long_term_memory WHERE archived = 0 ORDER BY created_at DESC').all();
|
|
414
414
|
return rows.map(this.mapRowToRecord);
|
|
415
415
|
}
|
|
416
|
+
buildMemoryFilterQuery(filters) {
|
|
417
|
+
const params = [];
|
|
418
|
+
let where = 'WHERE archived = 0';
|
|
419
|
+
if (filters?.category) {
|
|
420
|
+
where += ' AND category = ?';
|
|
421
|
+
params.push(filters.category);
|
|
422
|
+
}
|
|
423
|
+
if (filters?.importance) {
|
|
424
|
+
where += ' AND importance = ?';
|
|
425
|
+
params.push(filters.importance);
|
|
426
|
+
}
|
|
427
|
+
if (filters?.search) {
|
|
428
|
+
where += ' AND (summary LIKE ? OR details LIKE ? OR category LIKE ?)';
|
|
429
|
+
const pattern = `%${filters.search}%`;
|
|
430
|
+
params.push(pattern, pattern, pattern);
|
|
431
|
+
}
|
|
432
|
+
return { where, params };
|
|
433
|
+
}
|
|
434
|
+
countMemories(filters) {
|
|
435
|
+
if (!this.db)
|
|
436
|
+
this.initialize();
|
|
437
|
+
const { where, params } = this.buildMemoryFilterQuery(filters);
|
|
438
|
+
const row = this.db.prepare(`SELECT COUNT(*) as cnt FROM long_term_memory ${where}`).get(...params);
|
|
439
|
+
return row.cnt;
|
|
440
|
+
}
|
|
441
|
+
getMemoriesPaginated(filters) {
|
|
442
|
+
if (!this.db)
|
|
443
|
+
this.initialize();
|
|
444
|
+
const page = Math.max(1, filters?.page ?? 1);
|
|
445
|
+
const per_page = Math.min(100, Math.max(1, filters?.per_page ?? 20));
|
|
446
|
+
const offset = (page - 1) * per_page;
|
|
447
|
+
const total = this.countMemories(filters);
|
|
448
|
+
const total_pages = Math.ceil(total / per_page);
|
|
449
|
+
const { where, params } = this.buildMemoryFilterQuery(filters);
|
|
450
|
+
const rows = this.db.prepare(`SELECT * FROM long_term_memory ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, per_page, offset);
|
|
451
|
+
return { data: rows.map(this.mapRowToRecord), total, page, per_page, total_pages };
|
|
452
|
+
}
|
|
416
453
|
mapRowToRecord(row) {
|
|
417
454
|
return {
|
|
418
455
|
id: row.id,
|
|
@@ -204,6 +204,19 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
204
204
|
catch (error) {
|
|
205
205
|
console.warn(`[SQLite] Migration check failed: ${error}`);
|
|
206
206
|
}
|
|
207
|
+
// Migrate model_pricing table
|
|
208
|
+
try {
|
|
209
|
+
const pricingInfo = this.db.pragma('table_info(model_pricing)');
|
|
210
|
+
const pricingCols = new Set(pricingInfo.map(c => c.name));
|
|
211
|
+
if (!pricingCols.has('audio_cost_per_second')) {
|
|
212
|
+
this.db.exec('ALTER TABLE model_pricing ADD COLUMN audio_cost_per_second REAL');
|
|
213
|
+
// Seed Whisper pricing: $0.006/minute = $0.0001/second
|
|
214
|
+
this.db.prepare('INSERT OR IGNORE INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m, audio_cost_per_second) VALUES (?, ?, ?, ?, ?)').run('openai', 'whisper-1', 0, 0, 0.0001);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.warn(`[SQLite] model_pricing migration failed: ${error}`);
|
|
219
|
+
}
|
|
207
220
|
}
|
|
208
221
|
/**
|
|
209
222
|
* Retrieves all messages for the current session from the database.
|
|
@@ -301,7 +314,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
301
314
|
}
|
|
302
315
|
try {
|
|
303
316
|
const placeholders = sessionIds.map(() => '?').join(', ');
|
|
304
|
-
const stmt = this.db.prepare(`SELECT id, session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, agent, duration_ms
|
|
317
|
+
const stmt = this.db.prepare(`SELECT id, session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model, agent, duration_ms, audio_duration_seconds
|
|
305
318
|
FROM messages
|
|
306
319
|
WHERE session_id IN (${placeholders})
|
|
307
320
|
ORDER BY id DESC
|
|
@@ -648,11 +661,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
648
661
|
}
|
|
649
662
|
// --- Model Pricing CRUD ---
|
|
650
663
|
listModelPricing() {
|
|
651
|
-
const rows = this.db.prepare('SELECT provider, model, input_price_per_1m, output_price_per_1m FROM model_pricing ORDER BY provider, model').all();
|
|
664
|
+
const rows = this.db.prepare('SELECT provider, model, input_price_per_1m, output_price_per_1m, audio_cost_per_second FROM model_pricing ORDER BY provider, model').all();
|
|
652
665
|
return rows;
|
|
653
666
|
}
|
|
654
667
|
upsertModelPricing(entry) {
|
|
655
|
-
this.db.prepare('INSERT INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m) VALUES (?, ?, ?, ?) ON CONFLICT(provider, model) DO UPDATE SET input_price_per_1m = excluded.input_price_per_1m, output_price_per_1m = excluded.output_price_per_1m').run(entry.provider, entry.model, entry.input_price_per_1m, entry.output_price_per_1m);
|
|
668
|
+
this.db.prepare('INSERT INTO model_pricing (provider, model, input_price_per_1m, output_price_per_1m, audio_cost_per_second) VALUES (?, ?, ?, ?, ?) ON CONFLICT(provider, model) DO UPDATE SET input_price_per_1m = excluded.input_price_per_1m, output_price_per_1m = excluded.output_price_per_1m, audio_cost_per_second = excluded.audio_cost_per_second').run(entry.provider, entry.model, entry.input_price_per_1m, entry.output_price_per_1m, entry.audio_cost_per_second ?? null);
|
|
656
669
|
}
|
|
657
670
|
deleteModelPricing(provider, model) {
|
|
658
671
|
const result = this.db.prepare('DELETE FROM model_pricing WHERE provider = ? AND model = ?').run(provider, model);
|
package/dist/runtime/neo.js
CHANGED
|
@@ -5,12 +5,50 @@ import { ProviderError } from "./errors.js";
|
|
|
5
5
|
import { DisplayManager } from "./display.js";
|
|
6
6
|
import { Construtor } from "./tools/factory.js";
|
|
7
7
|
import { morpheusTools } from "./tools/index.js";
|
|
8
|
-
import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
|
|
9
8
|
import { TaskRequestContext } from "./tasks/context.js";
|
|
10
|
-
import {
|
|
9
|
+
import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
|
|
10
|
+
import { buildDelegationTool } from "./tools/delegation-utils.js";
|
|
11
|
+
// Internal Morpheus tools get 'tool_call' event type; MCP tools get 'mcp_tool'
|
|
12
|
+
const MORPHEUS_TOOL_NAMES = new Set(morpheusTools.map((t) => t.name));
|
|
13
|
+
const NEO_BUILTIN_CAPABILITIES = `
|
|
14
|
+
Neo built-in capabilities (always available — no MCP required):
|
|
15
|
+
• Config: morpheus_config_query, morpheus_config_update — read/write Morpheus configuration (LLM, channels, UI, etc.)
|
|
16
|
+
• Diagnostics: diagnostic_check — full system health report (config, databases, LLM provider, logs)
|
|
17
|
+
• Analytics: message_count, token_usage, provider_model_usage — message counts and token/cost usage stats
|
|
18
|
+
• Tasks: task_query — look up task status by id or session
|
|
19
|
+
• MCP Management: mcp_list, mcp_manage — list/add/update/delete/enable/disable MCP servers; use action "reload" to reload tools across all agents after config changes
|
|
20
|
+
• Webhooks: webhook_list, webhook_manage — create/update/delete webhooks; create returns api_key`.trim();
|
|
21
|
+
const NEO_BASE_DESCRIPTION = `Delegate execution to Neo asynchronously.
|
|
22
|
+
|
|
23
|
+
This tool creates a background task and returns an acknowledgement with task id.
|
|
24
|
+
Use it for any request that requires Neo's built-in capabilities or a runtime MCP tool listed below.
|
|
25
|
+
Each delegated task must contain one atomic objective.
|
|
26
|
+
|
|
27
|
+
${NEO_BUILTIN_CAPABILITIES}`;
|
|
28
|
+
function normalizeDescription(text) {
|
|
29
|
+
if (!text)
|
|
30
|
+
return "No description";
|
|
31
|
+
return text.replace(/\s+/g, " ").trim();
|
|
32
|
+
}
|
|
33
|
+
function buildCatalogSection(mcpTools) {
|
|
34
|
+
if (mcpTools.length === 0) {
|
|
35
|
+
return "\n\nRuntime MCP tools: none currently loaded.";
|
|
36
|
+
}
|
|
37
|
+
const maxItems = 500;
|
|
38
|
+
const lines = mcpTools.slice(0, maxItems).map((t) => {
|
|
39
|
+
const desc = normalizeDescription(t.description).slice(0, 120);
|
|
40
|
+
return `- ${t.name}: ${desc}`;
|
|
41
|
+
});
|
|
42
|
+
const hidden = mcpTools.length - lines.length;
|
|
43
|
+
if (hidden > 0) {
|
|
44
|
+
lines.push(`- ... and ${hidden} more tools`);
|
|
45
|
+
}
|
|
46
|
+
return `\n\nRuntime MCP tools:\n${lines.join("\n")}`;
|
|
47
|
+
}
|
|
11
48
|
export class Neo {
|
|
12
49
|
static instance = null;
|
|
13
50
|
static currentSessionId = undefined;
|
|
51
|
+
static _delegateTool = null;
|
|
14
52
|
agent;
|
|
15
53
|
config;
|
|
16
54
|
display = DisplayManager.getInstance();
|
|
@@ -28,17 +66,25 @@ export class Neo {
|
|
|
28
66
|
}
|
|
29
67
|
static resetInstance() {
|
|
30
68
|
Neo.instance = null;
|
|
69
|
+
Neo._delegateTool = null;
|
|
31
70
|
}
|
|
32
71
|
static async refreshDelegateCatalog() {
|
|
33
72
|
const mcpTools = await Construtor.create(() => Neo.currentSessionId);
|
|
34
|
-
|
|
73
|
+
if (Neo._delegateTool) {
|
|
74
|
+
const full = `${NEO_BASE_DESCRIPTION}${buildCatalogSection(mcpTools)}`;
|
|
75
|
+
Neo._delegateTool.description = full;
|
|
76
|
+
}
|
|
35
77
|
}
|
|
36
78
|
async initialize() {
|
|
37
79
|
const neoConfig = this.config.neo || this.config.llm;
|
|
38
80
|
const personality = this.config.neo?.personality || 'analytical_engineer';
|
|
39
81
|
const mcpTools = await Construtor.create(() => Neo.currentSessionId);
|
|
40
82
|
const tools = [...mcpTools, ...morpheusTools];
|
|
41
|
-
|
|
83
|
+
// Update delegate tool description with current catalog
|
|
84
|
+
if (Neo._delegateTool) {
|
|
85
|
+
const full = `${NEO_BASE_DESCRIPTION}${buildCatalogSection(mcpTools)}`;
|
|
86
|
+
Neo._delegateTool.description = full;
|
|
87
|
+
}
|
|
42
88
|
this.display.log(`Neo initialized with ${tools.length} tools (personality: ${personality}).`, { source: "Neo" });
|
|
43
89
|
try {
|
|
44
90
|
this.agent = await ProviderFactory.create(neoConfig, tools);
|
|
@@ -89,6 +135,7 @@ ${context ? `Context:\n${context}` : ""}
|
|
|
89
135
|
origin_message_id: taskContext?.origin_message_id,
|
|
90
136
|
origin_user_id: taskContext?.origin_user_id,
|
|
91
137
|
};
|
|
138
|
+
const inputCount = messages.length;
|
|
92
139
|
const startMs = Date.now();
|
|
93
140
|
const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }));
|
|
94
141
|
const durationMs = Date.now() - startMs;
|
|
@@ -96,44 +143,41 @@ ${context ? `Context:\n${context}` : ""}
|
|
|
96
143
|
const content = typeof lastMessage.content === "string"
|
|
97
144
|
? lastMessage.content
|
|
98
145
|
: JSON.stringify(lastMessage.content);
|
|
99
|
-
const rawUsage = lastMessage
|
|
100
|
-
?? lastMessage.response_metadata?.usage
|
|
101
|
-
?? lastMessage.response_metadata?.tokenUsage
|
|
102
|
-
?? lastMessage.usage;
|
|
103
|
-
const inputTokens = rawUsage?.input_tokens ?? 0;
|
|
104
|
-
const outputTokens = rawUsage?.output_tokens ?? 0;
|
|
146
|
+
const rawUsage = extractRawUsage(lastMessage);
|
|
105
147
|
const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
|
|
106
148
|
const targetSession = sessionId ?? Neo.currentSessionId ?? "neo";
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
persisted.provider_metadata = { provider: neoConfig.provider, model: neoConfig.model };
|
|
113
|
-
persisted.agent_metadata = { agent: 'neo' };
|
|
114
|
-
persisted.duration_ms = durationMs;
|
|
115
|
-
await history.addMessage(persisted);
|
|
116
|
-
}
|
|
117
|
-
finally {
|
|
118
|
-
history.close();
|
|
119
|
-
}
|
|
149
|
+
await persistAgentMessage('neo', content, neoConfig, targetSession, rawUsage, durationMs);
|
|
150
|
+
emitToolAuditEvents(response.messages.slice(inputCount), targetSession, 'neo', {
|
|
151
|
+
defaultEventType: 'mcp_tool',
|
|
152
|
+
internalToolNames: MORPHEUS_TOOL_NAMES,
|
|
153
|
+
});
|
|
120
154
|
this.display.log("Neo task completed.", { source: "Neo" });
|
|
121
|
-
return
|
|
122
|
-
output: content,
|
|
123
|
-
usage: {
|
|
124
|
-
provider: neoConfig.provider,
|
|
125
|
-
model: neoConfig.model,
|
|
126
|
-
inputTokens,
|
|
127
|
-
outputTokens,
|
|
128
|
-
durationMs,
|
|
129
|
-
stepCount,
|
|
130
|
-
},
|
|
131
|
-
};
|
|
155
|
+
return buildAgentResult(content, neoConfig, rawUsage, durationMs, stepCount);
|
|
132
156
|
}
|
|
133
157
|
catch (err) {
|
|
134
158
|
throw new ProviderError(neoConfig.provider, err, "Neo task execution failed");
|
|
135
159
|
}
|
|
136
160
|
}
|
|
161
|
+
createDelegateTool() {
|
|
162
|
+
if (!Neo._delegateTool) {
|
|
163
|
+
Neo._delegateTool = buildDelegationTool({
|
|
164
|
+
name: "neo_delegate",
|
|
165
|
+
description: NEO_BASE_DESCRIPTION,
|
|
166
|
+
agentKey: "neo",
|
|
167
|
+
agentLabel: "Neo",
|
|
168
|
+
auditAgent: "neo",
|
|
169
|
+
isSync: () => ConfigManager.getInstance().get().neo?.execution_mode === 'sync',
|
|
170
|
+
notifyText: '🥷 Neo is executing your request...',
|
|
171
|
+
executeSync: (task, context, sessionId, ctx) => Neo.getInstance().execute(task, context, sessionId, {
|
|
172
|
+
origin_channel: ctx?.origin_channel ?? "api",
|
|
173
|
+
session_id: sessionId,
|
|
174
|
+
origin_message_id: ctx?.origin_message_id,
|
|
175
|
+
origin_user_id: ctx?.origin_user_id,
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return Neo._delegateTool;
|
|
180
|
+
}
|
|
137
181
|
async reload() {
|
|
138
182
|
this.config = ConfigManager.getInstance().get();
|
|
139
183
|
this.agent = undefined;
|
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;
|
|
@@ -152,7 +154,16 @@ export class Oracle {
|
|
|
152
154
|
await Trinity.refreshDelegateCatalog().catch(() => { });
|
|
153
155
|
updateSkillToolDescriptions();
|
|
154
156
|
// Build tool list — conditionally include SmithDelegateTool based on config
|
|
155
|
-
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
|
+
];
|
|
156
167
|
const smithsConfig = ConfigManager.getInstance().getSmithsConfig();
|
|
157
168
|
if (smithsConfig.enabled && smithsConfig.entries.length > 0) {
|
|
158
169
|
coreTools.push(SmithDelegateTool);
|
|
@@ -416,7 +427,12 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
416
427
|
// New messages start after the inputs.
|
|
417
428
|
const startNewMessagesIndex = messages.length;
|
|
418
429
|
const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
|
|
419
|
-
//
|
|
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
|
+
});
|
|
420
436
|
// Inject provider/model metadata and duration into all new AI messages
|
|
421
437
|
for (const msg of newGeneratedMessages) {
|
|
422
438
|
msg.provider_metadata = {
|
|
@@ -628,7 +644,16 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
628
644
|
await Neo.refreshDelegateCatalog().catch(() => { });
|
|
629
645
|
await Trinity.refreshDelegateCatalog().catch(() => { });
|
|
630
646
|
updateSkillToolDescriptions();
|
|
631
|
-
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
|
+
]);
|
|
632
657
|
await Neo.getInstance().reload();
|
|
633
658
|
this.display.log(`Oracle and Neo tools reloaded`, { source: 'Oracle' });
|
|
634
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) {
|
|
@@ -7,6 +7,8 @@ import { ConfigManager } from '../../config/manager.js';
|
|
|
7
7
|
import { ProviderFactory } from '../providers/factory.js';
|
|
8
8
|
import { buildDevKit } from '../../devkit/index.js';
|
|
9
9
|
import { SQLiteChatMessageHistory } from '../memory/sqlite.js';
|
|
10
|
+
import { AuditRepository } from '../audit/repository.js';
|
|
11
|
+
import { TaskRequestContext } from '../tasks/context.js';
|
|
10
12
|
/**
|
|
11
13
|
* SmithDelegator — delegates natural-language tasks to a specific Smith.
|
|
12
14
|
*
|
|
@@ -70,6 +72,25 @@ export class SmithDelegator {
|
|
|
70
72
|
connection.onMessage(progressHandler);
|
|
71
73
|
try {
|
|
72
74
|
const result = await connection.sendTask(taskId, localTool.name, args);
|
|
75
|
+
// Audit the remote tool execution — data already available from Smith's response
|
|
76
|
+
const sessionId = TaskRequestContext.get()?.session_id ?? 'smith';
|
|
77
|
+
const resultStr = result.data !== undefined
|
|
78
|
+
? (typeof result.data === 'string' ? result.data : JSON.stringify(result.data))
|
|
79
|
+
: result.error;
|
|
80
|
+
const meta = { smith: smithName };
|
|
81
|
+
if (args && Object.keys(args).length > 0)
|
|
82
|
+
meta.args = args;
|
|
83
|
+
if (resultStr)
|
|
84
|
+
meta.result = resultStr.length > 500 ? resultStr.slice(0, 500) + '…' : resultStr;
|
|
85
|
+
AuditRepository.getInstance().insert({
|
|
86
|
+
session_id: sessionId,
|
|
87
|
+
event_type: 'tool_call',
|
|
88
|
+
agent: 'smith',
|
|
89
|
+
tool_name: `${smithName}/${localTool.name}`,
|
|
90
|
+
duration_ms: result.duration_ms,
|
|
91
|
+
status: result.success ? 'success' : 'error',
|
|
92
|
+
metadata: meta,
|
|
93
|
+
});
|
|
73
94
|
if (result.success) {
|
|
74
95
|
return typeof result.data === 'string'
|
|
75
96
|
? result.data
|
|
@@ -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
|