morpheus-cli 0.8.6 → 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 (73) hide show
  1. package/dist/channels/telegram.js +43 -0
  2. package/dist/http/api.js +49 -13
  3. package/dist/http/routers/chronos.js +12 -1
  4. package/dist/http/webhooks-router.js +11 -3
  5. package/dist/runtime/ISubagent.js +1 -0
  6. package/dist/runtime/apoc.js +49 -39
  7. package/dist/runtime/audit/repository.js +193 -6
  8. package/dist/runtime/chronos/repository.js +35 -0
  9. package/dist/runtime/keymaker.js +6 -30
  10. package/dist/runtime/memory/sati/repository.js +37 -0
  11. package/dist/runtime/memory/sqlite.js +16 -3
  12. package/dist/runtime/neo.js +78 -34
  13. package/dist/runtime/oracle.js +31 -6
  14. package/dist/runtime/skills/tool.js +25 -0
  15. package/dist/runtime/subagent-utils.js +89 -0
  16. package/dist/runtime/tasks/repository.js +51 -0
  17. package/dist/runtime/tasks/worker.js +12 -2
  18. package/dist/runtime/telephonist.js +17 -9
  19. package/dist/runtime/tools/delegation-utils.js +120 -0
  20. package/dist/runtime/tools/index.js +0 -2
  21. package/dist/runtime/trinity.js +50 -34
  22. package/dist/runtime/webhooks/repository.js +31 -0
  23. package/dist/types/pagination.js +1 -0
  24. package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +1 -0
  25. package/dist/ui/assets/Chat-CjxeAQmd.js +41 -0
  26. package/dist/ui/assets/Chronos-BAjeLobF.js +1 -0
  27. package/dist/ui/assets/{ConfirmationModal-Bx-GtD9B.js → ConfirmationModal-fvgnOWTY.js} +1 -1
  28. package/dist/ui/assets/{Dashboard-DDyN_X-J.js → Dashboard-Ca5mSefz.js} +1 -1
  29. package/dist/ui/assets/{DeleteConfirmationModal-DIXbckY8.js → DeleteConfirmationModal-A8EmnHoa.js} +1 -1
  30. package/dist/ui/assets/{Logs-dzPLW45U.js → Logs-CYu7se7R.js} +1 -1
  31. package/dist/ui/assets/MCPManager-DsDA_ZVT.js +1 -0
  32. package/dist/ui/assets/ModelPricing-DnSm_Nh-.js +1 -0
  33. package/dist/ui/assets/Notifications-CiljQzvM.js +1 -0
  34. package/dist/ui/assets/Pagination-JsiwxVNQ.js +1 -0
  35. package/dist/ui/assets/SatiMemories-rnO2b0LG.js +1 -0
  36. package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +9 -0
  37. package/dist/ui/assets/{Settings-DNDe62-H.js → Settings-OQlHAJoy.js} +1 -1
  38. package/dist/ui/assets/Skills-Crsybug0.js +7 -0
  39. package/dist/ui/assets/Smiths-wm90jRDT.js +1 -0
  40. package/dist/ui/assets/Tasks-C5FMu_Yu.js +1 -0
  41. package/dist/ui/assets/TrinityDatabases-BzYfecKI.js +1 -0
  42. package/dist/ui/assets/{UsageStats-doBLB7Lc.js → UsageStats-CBo2vW2n.js} +1 -1
  43. package/dist/ui/assets/{WebhookManager-D3A5pdjC.js → WebhookManager-0tDFkfHd.js} +1 -1
  44. package/dist/ui/assets/audit-B-F8XPLi.js +1 -0
  45. package/dist/ui/assets/chronos-BvMxfBQH.js +1 -0
  46. package/dist/ui/assets/{config-DX3Xb0XE.js → config-DteVgNGR.js} +1 -1
  47. package/dist/ui/assets/index-Cwqr-n0Y.js +10 -0
  48. package/dist/ui/assets/index-DcfyUdLI.css +1 -0
  49. package/dist/ui/assets/{mcp-DfhJYN14.js → mcp-DxzodOdH.js} +1 -1
  50. package/dist/ui/assets/{skills-BPjq0qV7.js → skills--hAyQnmG.js} +1 -1
  51. package/dist/ui/assets/{stats-DHCRNkJp.js → stats-Cibaisqd.js} +1 -1
  52. package/dist/ui/assets/vendor-icons-BVuQI-6R.js +1 -0
  53. package/dist/ui/index.html +3 -3
  54. package/dist/ui/sw.js +1 -1
  55. package/package.json +2 -1
  56. package/dist/runtime/tools/apoc-tool.js +0 -157
  57. package/dist/runtime/tools/neo-tool.js +0 -172
  58. package/dist/runtime/tools/trinity-tool.js +0 -157
  59. package/dist/ui/assets/Chat-CO15OnaY.js +0 -38
  60. package/dist/ui/assets/Chronos-CUZDQLh2.js +0 -1
  61. package/dist/ui/assets/MCPManager-CRHWR4S7.js +0 -1
  62. package/dist/ui/assets/ModelPricing-TRBesy0r.js +0 -1
  63. package/dist/ui/assets/Notifications-DMke7Dr7.js +0 -1
  64. package/dist/ui/assets/SatiMemories-CaLrgdZV.js +0 -1
  65. package/dist/ui/assets/SessionAudit-DedGO5XK.js +0 -9
  66. package/dist/ui/assets/Skills-KUhW7UXP.js +0 -7
  67. package/dist/ui/assets/Smiths-Btoqw4Ex.js +0 -1
  68. package/dist/ui/assets/Tasks-cwA25Hq2.js +0 -1
  69. package/dist/ui/assets/TrinityDatabases-CQhettEJ.js +0 -1
  70. package/dist/ui/assets/chronos-DlDM2UBT.js +0 -1
  71. package/dist/ui/assets/index-CQIUjucB.js +0 -10
  72. package/dist/ui/assets/index-DAh3q_hR.css +0 -1
  73. 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 = ?'];
@@ -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 { SQLiteChatMessageHistory } from "./memory/sqlite.js";
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.usage_metadata
129
- ?? lastMessage.response_metadata?.usage
130
- ?? lastMessage.response_metadata?.tokenUsage
131
- ?? lastMessage.usage;
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);
@@ -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 { updateNeoDelegateToolDescription } from "./tools/neo-tool.js";
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
- updateNeoDelegateToolDescription(mcpTools);
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
- updateNeoDelegateToolDescription(mcpTools);
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.usage_metadata
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
- const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
108
- try {
109
- const persisted = new AIMessage(content);
110
- if (rawUsage)
111
- persisted.usage_metadata = rawUsage;
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;
@@ -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 = [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
+ ];
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
- // 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
+ });
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, [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
+ ]);
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) {
@@ -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': {