morpheus-cli 0.8.0 → 0.8.3

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.
@@ -30,13 +30,13 @@ export class Neo {
30
30
  Neo.instance = null;
31
31
  }
32
32
  static async refreshDelegateCatalog() {
33
- const mcpTools = await Construtor.create();
33
+ const mcpTools = await Construtor.create(() => Neo.currentSessionId);
34
34
  updateNeoDelegateToolDescription(mcpTools);
35
35
  }
36
36
  async initialize() {
37
37
  const neoConfig = this.config.neo || this.config.llm;
38
38
  const personality = this.config.neo?.personality || 'analytical_engineer';
39
- const mcpTools = await Construtor.create();
39
+ const mcpTools = await Construtor.create(() => Neo.currentSessionId);
40
40
  const tools = [...mcpTools, ...morpheusTools];
41
41
  updateNeoDelegateToolDescription(mcpTools);
42
42
  this.display.log(`Neo initialized with ${tools.length} tools (personality: ${personality}).`, { source: "Neo" });
@@ -89,30 +89,46 @@ ${context ? `Context:\n${context}` : ""}
89
89
  origin_message_id: taskContext?.origin_message_id,
90
90
  origin_user_id: taskContext?.origin_user_id,
91
91
  };
92
+ const startMs = Date.now();
92
93
  const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }));
94
+ const durationMs = Date.now() - startMs;
93
95
  const lastMessage = response.messages[response.messages.length - 1];
94
96
  const content = typeof lastMessage.content === "string"
95
97
  ? lastMessage.content
96
98
  : 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;
105
+ const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
97
106
  const targetSession = sessionId ?? Neo.currentSessionId ?? "neo";
98
107
  const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
99
108
  try {
100
109
  const persisted = new AIMessage(content);
101
- persisted.usage_metadata = lastMessage.usage_metadata
102
- ?? lastMessage.response_metadata?.usage
103
- ?? lastMessage.response_metadata?.tokenUsage
104
- ?? lastMessage.usage;
105
- persisted.provider_metadata = {
106
- provider: neoConfig.provider,
107
- model: neoConfig.model,
108
- };
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;
109
115
  await history.addMessage(persisted);
110
116
  }
111
117
  finally {
112
118
  history.close();
113
119
  }
114
120
  this.display.log("Neo task completed.", { source: "Neo" });
115
- return content;
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
+ };
116
132
  }
117
133
  catch (err) {
118
134
  throw new ProviderError(neoConfig.provider, err, "Neo task execution failed");
@@ -19,6 +19,7 @@ import { Construtor } from "./tools/factory.js";
19
19
  import { MCPManager } from "../config/mcp-manager.js";
20
20
  import { SkillRegistry, SkillExecuteTool, SkillDelegateTool, updateSkillToolDescriptions } from "./skills/index.js";
21
21
  import { SmithRegistry } from "./smiths/registry.js";
22
+ import { AuditRepository } from "./audit/repository.js";
22
23
  export class Oracle {
23
24
  provider;
24
25
  config;
@@ -368,12 +369,34 @@ Use it to inform your response and tool selection (if needed), but do not assume
368
369
  };
369
370
  let contextDelegationAcks = [];
370
371
  let syncDelegationCount = 0;
372
+ const oracleStartMs = Date.now();
371
373
  const response = await TaskRequestContext.run(invokeContext, async () => {
372
374
  const agentResponse = await this.provider.invoke({ messages });
373
375
  contextDelegationAcks = TaskRequestContext.getDelegationAcks();
374
376
  syncDelegationCount = TaskRequestContext.getSyncDelegationCount();
375
377
  return agentResponse;
376
378
  });
379
+ const oracleDurationMs = Date.now() - oracleStartMs;
380
+ // Emit llm_call audit event for Oracle's own invocation
381
+ try {
382
+ const lastMsg = response.messages[response.messages.length - 1];
383
+ const rawUsage = lastMsg.usage_metadata
384
+ ?? lastMsg.response_metadata?.usage
385
+ ?? lastMsg.response_metadata?.tokenUsage
386
+ ?? lastMsg.usage;
387
+ AuditRepository.getInstance().insert({
388
+ session_id: currentSessionId ?? 'default',
389
+ event_type: 'llm_call',
390
+ agent: 'oracle',
391
+ provider: this.config.llm.provider,
392
+ model: this.config.llm.model,
393
+ input_tokens: rawUsage?.input_tokens ?? rawUsage?.prompt_tokens ?? 0,
394
+ output_tokens: rawUsage?.output_tokens ?? rawUsage?.completion_tokens ?? 0,
395
+ duration_ms: oracleDurationMs,
396
+ status: 'success',
397
+ });
398
+ }
399
+ catch { /* non-critical */ }
377
400
  // Identify new messages generated during the interaction
378
401
  // The `messages` array passed to invoke had length `messages.length`
379
402
  // The `response.messages` contains the full state.
@@ -381,12 +404,16 @@ Use it to inform your response and tool selection (if needed), but do not assume
381
404
  const startNewMessagesIndex = messages.length;
382
405
  const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
383
406
  // console.log('New generated messages', newGeneratedMessages);
384
- // Inject provider/model metadata into all new messages
407
+ // Inject provider/model metadata and duration into all new AI messages
385
408
  for (const msg of newGeneratedMessages) {
386
409
  msg.provider_metadata = {
387
410
  provider: this.config.llm.provider,
388
411
  model: this.config.llm.model
389
412
  };
413
+ msg.agent_metadata = { agent: 'oracle' };
414
+ if (msg instanceof AIMessage) {
415
+ msg.duration_ms = oracleDurationMs;
416
+ }
390
417
  }
391
418
  let responseContent;
392
419
  const toolDelegationAcks = this.extractDelegationAcksFromMessages(newGeneratedMessages);
@@ -145,6 +145,13 @@ export class SmithConnection {
145
145
  }
146
146
  });
147
147
  }
148
+ /** Returns true if the given entry differs from what this connection was created with */
149
+ hasEntryChanged(entry) {
150
+ return (entry.host !== this.entry.host ||
151
+ entry.port !== this.entry.port ||
152
+ entry.auth_token !== this.entry.auth_token ||
153
+ (entry.tls ?? false) !== (this.entry.tls ?? false));
154
+ }
148
155
  /** Register a handler for incoming messages */
149
156
  onMessage(handler) {
150
157
  this.messageHandlers.push(handler);
@@ -98,14 +98,14 @@ export class SmithDelegator {
98
98
  async delegate(smithName, task, context) {
99
99
  const smith = this.registry.get(smithName);
100
100
  if (!smith) {
101
- return `❌ Smith '${smithName}' not found. Available: ${this.registry.list().map(s => s.name).join(', ') || 'none'}`;
101
+ return { output: `❌ Smith '${smithName}' not found. Available: ${this.registry.list().map(s => s.name).join(', ') || 'none'}` };
102
102
  }
103
103
  if (smith.state !== 'online') {
104
- return `❌ Smith '${smithName}' is ${smith.state}. Cannot delegate.`;
104
+ return { output: `❌ Smith '${smithName}' is ${smith.state}. Cannot delegate.` };
105
105
  }
106
106
  const connection = this.registry.getConnection(smithName);
107
107
  if (!connection || !connection.connected) {
108
- return `❌ No active connection to Smith '${smithName}'.`;
108
+ return { output: `❌ No active connection to Smith '${smithName}'.` };
109
109
  }
110
110
  this.display.log(`Delegating to Smith '${smithName}': ${task.slice(0, 100)}...`, {
111
111
  source: 'SmithDelegator',
@@ -116,7 +116,7 @@ export class SmithDelegator {
116
116
  // Build proxy tools for this Smith's capabilities
117
117
  const proxyTools = this.buildProxyTools(smithName);
118
118
  if (proxyTools.length === 0) {
119
- return `❌ Smith '${smithName}' has no available tools.`;
119
+ return { output: `❌ Smith '${smithName}' has no available tools.` };
120
120
  }
121
121
  // Create a fresh ReactAgent with proxy tools
122
122
  const config = ConfigManager.getInstance().get();
@@ -134,25 +134,31 @@ Respond in the same language as the task.`);
134
134
  ? `Context: ${context}\n\nTask: ${task}`
135
135
  : task;
136
136
  const messages = [systemMessage, new HumanMessage(userContent)];
137
+ const startMs = Date.now();
137
138
  const response = await agent.invoke({ messages });
139
+ const durationMs = Date.now() - startMs;
138
140
  // Extract final response
139
141
  const lastMessage = response.messages[response.messages.length - 1];
140
142
  const content = typeof lastMessage.content === 'string'
141
143
  ? lastMessage.content
142
144
  : JSON.stringify(lastMessage.content);
145
+ const rawUsage = lastMessage.usage_metadata
146
+ ?? lastMessage.response_metadata?.usage
147
+ ?? lastMessage.response_metadata?.tokenUsage
148
+ ?? lastMessage.usage;
149
+ const inputTokens = rawUsage?.input_tokens ?? 0;
150
+ const outputTokens = rawUsage?.output_tokens ?? 0;
151
+ const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
143
152
  // Persist token usage to session history
144
153
  try {
145
154
  const history = new SQLiteChatMessageHistory({ sessionId: 'smith' });
146
155
  try {
147
156
  const persisted = new AIMessage(content);
148
- persisted.usage_metadata = lastMessage.usage_metadata
149
- ?? lastMessage.response_metadata?.usage
150
- ?? lastMessage.response_metadata?.tokenUsage
151
- ?? lastMessage.usage;
152
- persisted.provider_metadata = {
153
- provider: llmConfig.provider,
154
- model: llmConfig.model,
155
- };
157
+ if (rawUsage)
158
+ persisted.usage_metadata = rawUsage;
159
+ persisted.provider_metadata = { provider: llmConfig.provider, model: llmConfig.model };
160
+ persisted.agent_metadata = { agent: 'smith' };
161
+ persisted.duration_ms = durationMs;
156
162
  await history.addMessage(persisted);
157
163
  }
158
164
  finally {
@@ -166,14 +172,24 @@ Respond in the same language as the task.`);
166
172
  source: 'SmithDelegator',
167
173
  level: 'info',
168
174
  });
169
- return content;
175
+ return {
176
+ output: content,
177
+ usage: {
178
+ provider: llmConfig.provider,
179
+ model: llmConfig.model,
180
+ inputTokens,
181
+ outputTokens,
182
+ durationMs,
183
+ stepCount,
184
+ },
185
+ };
170
186
  }
171
187
  catch (err) {
172
188
  this.display.log(`Smith delegation error: ${err.message}`, {
173
189
  source: 'SmithDelegator',
174
190
  level: 'error',
175
191
  });
176
- return `❌ Smith '${smithName}' delegation failed: ${err.message}`;
192
+ return { output: `❌ Smith '${smithName}' delegation failed: ${err.message}` };
177
193
  }
178
194
  }
179
195
  /**
@@ -193,7 +193,7 @@ export class SmithRegistry extends EventEmitter {
193
193
  * Hot-reload Smiths from current config.
194
194
  * - Connects new entries that aren't yet registered
195
195
  * - Disconnects entries that were removed from config
196
- * - Leaves existing connections untouched
196
+ * - Reconnects existing entries whose connection details changed (host/port/tls/auth_token)
197
197
  */
198
198
  async reload() {
199
199
  const config = ConfigManager.getInstance().getSmithsConfig();
@@ -223,9 +223,20 @@ export class SmithRegistry extends EventEmitter {
223
223
  });
224
224
  }
225
225
  }
226
- // Add new Smiths from config
226
+ // Add new Smiths or reconnect existing ones whose connection details changed
227
227
  for (const entry of config.entries) {
228
- if (!currentNames.has(entry.name)) {
228
+ const isNew = !currentNames.has(entry.name);
229
+ const existingConn = this.connections.get(entry.name);
230
+ const changed = !isNew && existingConn && existingConn.hasEntryChanged(entry);
231
+ if (isNew || changed) {
232
+ if (changed && existingConn) {
233
+ await existingConn.disconnect().catch(() => { });
234
+ this.connections.delete(entry.name);
235
+ this.smiths.delete(entry.name);
236
+ this.display.log(`Smith '${entry.name}' reconnecting with updated config (hot-reload)`, {
237
+ source: 'SmithRegistry', level: 'info',
238
+ });
239
+ }
229
240
  this.register(entry);
230
241
  const connection = new SmithConnection(entry, this);
231
242
  this.connections.set(entry.name, connection);
@@ -235,7 +246,7 @@ export class SmithRegistry extends EventEmitter {
235
246
  });
236
247
  });
237
248
  added.push(entry.name);
238
- this.display.log(`Smith '${entry.name}' added and connecting (hot-reload)`, {
249
+ this.display.log(`Smith '${entry.name}' ${isNew ? 'added and connecting' : 'reconnecting'} (hot-reload)`, {
239
250
  source: 'SmithRegistry', level: 'info',
240
251
  });
241
252
  }
@@ -62,6 +62,12 @@ export class TaskRepository {
62
62
  addColumn(`ALTER TABLE tasks ADD COLUMN notified_at INTEGER`, 'notified_at');
63
63
  addColumn(`ALTER TABLE tasks ADD COLUMN notify_after_at INTEGER`, 'notify_after_at');
64
64
  addColumn(`ALTER TABLE tasks ADD COLUMN ack_sent INTEGER NOT NULL DEFAULT 0`, 'ack_sent');
65
+ addColumn(`ALTER TABLE tasks ADD COLUMN provider TEXT`, 'provider');
66
+ addColumn(`ALTER TABLE tasks ADD COLUMN model TEXT`, 'model');
67
+ addColumn(`ALTER TABLE tasks ADD COLUMN input_tokens INTEGER NOT NULL DEFAULT 0`, 'input_tokens');
68
+ addColumn(`ALTER TABLE tasks ADD COLUMN output_tokens INTEGER NOT NULL DEFAULT 0`, 'output_tokens');
69
+ addColumn(`ALTER TABLE tasks ADD COLUMN duration_ms INTEGER`, 'duration_ms');
70
+ addColumn(`ALTER TABLE tasks ADD COLUMN step_count INTEGER NOT NULL DEFAULT 0`, 'step_count');
65
71
  this.db.exec(`
66
72
  UPDATE tasks
67
73
  SET
@@ -244,7 +250,7 @@ export class TaskRepository {
244
250
  });
245
251
  return tx();
246
252
  }
247
- markCompleted(id, output) {
253
+ markCompleted(id, output, usage) {
248
254
  const now = Date.now();
249
255
  const normalizedOutput = (output ?? '').trim();
250
256
  this.db.prepare(`
@@ -256,9 +262,15 @@ export class TaskRepository {
256
262
  updated_at = ?,
257
263
  notify_status = 'pending',
258
264
  notify_last_error = NULL,
259
- notified_at = NULL
265
+ notified_at = NULL,
266
+ provider = COALESCE(?, provider),
267
+ model = COALESCE(?, model),
268
+ input_tokens = COALESCE(?, input_tokens),
269
+ output_tokens = COALESCE(?, output_tokens),
270
+ duration_ms = COALESCE(?, duration_ms),
271
+ step_count = COALESCE(?, step_count)
260
272
  WHERE id = ? AND status != 'cancelled'
261
- `).run(normalizedOutput.length > 0 ? normalizedOutput : 'Task completed without output.', now, now, id);
273
+ `).run(normalizedOutput.length > 0 ? normalizedOutput : 'Task completed without output.', now, now, usage?.provider ?? null, usage?.model ?? null, usage?.inputTokens ?? null, usage?.outputTokens ?? null, usage?.durationMs ?? null, usage?.stepCount ?? null, id);
262
274
  }
263
275
  markFailed(id, error) {
264
276
  const now = Date.now();
@@ -6,6 +6,7 @@ import { Trinity } from '../trinity.js';
6
6
  import { executeKeymakerTask } from '../keymaker.js';
7
7
  import { SmithDelegator } from '../smiths/delegator.js';
8
8
  import { TaskRepository } from './repository.js';
9
+ import { AuditRepository } from '../audit/repository.js';
9
10
  export class TaskWorker {
10
11
  workerId;
11
12
  pollIntervalMs;
@@ -50,17 +51,26 @@ export class TaskWorker {
50
51
  this.executeTask(task).finally(() => this.activeTasks.delete(task.id));
51
52
  }
52
53
  async executeTask(task) {
54
+ const audit = AuditRepository.getInstance();
55
+ audit.insert({
56
+ session_id: task.session_id,
57
+ task_id: task.id,
58
+ event_type: 'task_created',
59
+ agent: task.agent === 'trinit' ? 'trinity' : task.agent,
60
+ status: 'success',
61
+ metadata: { agent: task.agent, input_preview: task.input.slice(0, 200) },
62
+ });
53
63
  try {
54
- let output;
64
+ let result;
55
65
  switch (task.agent) {
56
66
  case 'apoc': {
57
67
  const apoc = Apoc.getInstance();
58
- output = await apoc.execute(task.input, task.context ?? undefined, task.session_id);
68
+ result = await apoc.execute(task.input, task.context ?? undefined, task.session_id);
59
69
  break;
60
70
  }
61
71
  case 'neo': {
62
72
  const neo = Neo.getInstance();
63
- output = await neo.execute(task.input, task.context ?? undefined, task.session_id, {
73
+ result = await neo.execute(task.input, task.context ?? undefined, task.session_id, {
64
74
  origin_channel: task.origin_channel,
65
75
  session_id: task.session_id,
66
76
  origin_message_id: task.origin_message_id ?? undefined,
@@ -70,7 +80,7 @@ export class TaskWorker {
70
80
  }
71
81
  case 'trinit': {
72
82
  const trinity = Trinity.getInstance();
73
- output = await trinity.execute(task.input, task.context ?? undefined, task.session_id);
83
+ result = await trinity.execute(task.input, task.context ?? undefined, task.session_id);
74
84
  break;
75
85
  }
76
86
  case 'keymaker': {
@@ -86,7 +96,7 @@ export class TaskWorker {
86
96
  skillName = task.context;
87
97
  }
88
98
  }
89
- output = await executeKeymakerTask(skillName, task.input, {
99
+ result = await executeKeymakerTask(skillName, task.input, {
90
100
  origin_channel: task.origin_channel,
91
101
  session_id: task.session_id,
92
102
  origin_message_id: task.origin_message_id ?? undefined,
@@ -107,15 +117,68 @@ export class TaskWorker {
107
117
  }
108
118
  }
109
119
  const delegator = SmithDelegator.getInstance();
110
- const result = await delegator.delegate(smithName, task.input, task.context ?? undefined);
111
- output = typeof result === 'string' ? result : JSON.stringify(result);
120
+ result = await delegator.delegate(smithName, task.input, task.context ?? undefined);
112
121
  break;
113
122
  }
114
123
  default: {
115
124
  throw new Error(`Unknown task agent: ${task.agent}`);
116
125
  }
117
126
  }
118
- this.repository.markCompleted(task.id, output);
127
+ this.repository.markCompleted(task.id, result.output, result.usage ? {
128
+ provider: result.usage.provider,
129
+ model: result.usage.model,
130
+ inputTokens: result.usage.inputTokens,
131
+ outputTokens: result.usage.outputTokens,
132
+ durationMs: result.usage.durationMs,
133
+ stepCount: result.usage.stepCount,
134
+ } : undefined);
135
+ const agentName = (task.agent === 'trinit' ? 'trinity' : task.agent);
136
+ // Emit task_completed audit event
137
+ audit.insert({
138
+ session_id: task.session_id,
139
+ task_id: task.id,
140
+ event_type: 'task_completed',
141
+ agent: agentName,
142
+ duration_ms: result.usage?.durationMs,
143
+ status: 'success',
144
+ });
145
+ // Emit llm_call audit event if usage data is present (not keymaker skills)
146
+ if (result.usage && (result.usage.inputTokens > 0 || result.usage.outputTokens > 0)) {
147
+ audit.insert({
148
+ session_id: task.session_id,
149
+ task_id: task.id,
150
+ event_type: 'llm_call',
151
+ agent: agentName,
152
+ provider: result.usage.provider,
153
+ model: result.usage.model,
154
+ input_tokens: result.usage.inputTokens,
155
+ output_tokens: result.usage.outputTokens,
156
+ duration_ms: result.usage.durationMs,
157
+ status: 'success',
158
+ metadata: { step_count: result.usage.stepCount },
159
+ });
160
+ }
161
+ // Emit skill_executed for keymaker
162
+ if (task.agent === 'keymaker') {
163
+ let skillName = 'unknown';
164
+ if (task.context) {
165
+ try {
166
+ skillName = JSON.parse(task.context).skill || task.context;
167
+ }
168
+ catch {
169
+ skillName = task.context;
170
+ }
171
+ }
172
+ audit.insert({
173
+ session_id: task.session_id,
174
+ task_id: task.id,
175
+ event_type: 'skill_executed',
176
+ agent: 'keymaker',
177
+ tool_name: skillName,
178
+ duration_ms: result.usage?.durationMs,
179
+ status: 'success',
180
+ });
181
+ }
119
182
  this.display.log(`Task completed: ${task.id}`, { source: 'TaskWorker', level: 'success' });
120
183
  }
121
184
  catch (err) {
@@ -130,6 +193,14 @@ export class TaskWorker {
130
193
  return;
131
194
  }
132
195
  this.repository.markFailed(task.id, errorMessage);
196
+ audit.insert({
197
+ session_id: task.session_id,
198
+ task_id: task.id,
199
+ event_type: 'task_completed',
200
+ agent: (task.agent === 'trinit' ? 'trinity' : task.agent),
201
+ status: 'error',
202
+ metadata: { error: errorMessage },
203
+ });
133
204
  this.display.log(`Task failed: ${task.id} (${errorMessage})`, { source: 'TaskWorker', level: 'error' });
134
205
  }
135
206
  }
@@ -7,6 +7,7 @@ import { DisplayManager } from "../display.js";
7
7
  import { ConfigManager } from "../../config/manager.js";
8
8
  import { Apoc } from "../apoc.js";
9
9
  import { ChannelRegistry } from "../../channels/registry.js";
10
+ import { AuditRepository } from "../audit/repository.js";
10
11
  /**
11
12
  * Returns true when Apoc is configured to execute synchronously (inline).
12
13
  */
@@ -56,7 +57,21 @@ export const ApocDelegateTool = tool(async ({ task, context }) => {
56
57
  source: "ApocDelegateTool",
57
58
  level: "info",
58
59
  });
59
- return result;
60
+ if (result.usage) {
61
+ AuditRepository.getInstance().insert({
62
+ session_id: sessionId,
63
+ event_type: 'llm_call',
64
+ agent: 'apoc',
65
+ provider: result.usage.provider,
66
+ model: result.usage.model,
67
+ input_tokens: result.usage.inputTokens,
68
+ output_tokens: result.usage.outputTokens,
69
+ duration_ms: result.usage.durationMs,
70
+ status: 'success',
71
+ metadata: { step_count: result.usage.stepCount, mode: 'sync' },
72
+ });
73
+ }
74
+ return result.output;
60
75
  }
61
76
  catch (syncErr) {
62
77
  // Still count as sync delegation so Oracle passes through the error message
@@ -1,6 +1,39 @@
1
1
  import { DisplayManager } from "../display.js";
2
2
  import { MCPToolCache } from "./cache.js";
3
+ import { AuditRepository } from "../audit/repository.js";
3
4
  const display = DisplayManager.getInstance();
5
+ function instrumentMcpTool(tool, serverName, getSessionId) {
6
+ const original = tool._call.bind(tool);
7
+ tool._call = async function (input, runManager) {
8
+ const startMs = Date.now();
9
+ const sessionId = getSessionId() ?? 'unknown';
10
+ try {
11
+ const result = await original(input, runManager);
12
+ AuditRepository.getInstance().insert({
13
+ session_id: sessionId,
14
+ event_type: 'mcp_tool',
15
+ agent: 'neo',
16
+ tool_name: `${serverName}/${tool.name}`,
17
+ duration_ms: Date.now() - startMs,
18
+ status: 'success',
19
+ });
20
+ return result;
21
+ }
22
+ catch (err) {
23
+ AuditRepository.getInstance().insert({
24
+ session_id: sessionId,
25
+ event_type: 'mcp_tool',
26
+ agent: 'neo',
27
+ tool_name: `${serverName}/${tool.name}`,
28
+ duration_ms: Date.now() - startMs,
29
+ status: 'error',
30
+ metadata: { error: err?.message ?? String(err) },
31
+ });
32
+ throw err;
33
+ }
34
+ };
35
+ return tool;
36
+ }
4
37
  export class Construtor {
5
38
  /**
6
39
  * Probe MCP servers by checking cache stats.
@@ -21,13 +54,20 @@ export class Construtor {
21
54
  * Get MCP tools from cache (fast path).
22
55
  * If cache is not loaded, loads it first.
23
56
  * Tools are cached and returned instantly on subsequent calls.
57
+ * If getSessionId is provided, tools are wrapped with audit instrumentation.
24
58
  */
25
- static async create() {
59
+ static async create(getSessionId) {
26
60
  const cache = MCPToolCache.getInstance();
27
61
  await cache.ensureLoaded();
28
62
  const tools = cache.getTools();
29
63
  display.log(`Returning ${tools.length} cached MCP tools`, { level: 'debug', source: 'Construtor' });
30
- return tools;
64
+ if (!getSessionId)
65
+ return tools;
66
+ // Wrap each tool with audit tracking; derive server name from tool name prefix
67
+ return tools.map(tool => {
68
+ const serverName = tool.serverName ?? tool.name.split('_')[0] ?? 'mcp';
69
+ return instrumentMcpTool(tool, serverName, getSessionId);
70
+ });
31
71
  }
32
72
  /**
33
73
  * Force reload MCP tools from servers (slow path).
@@ -7,6 +7,7 @@ import { DisplayManager } from "../display.js";
7
7
  import { ConfigManager } from "../../config/manager.js";
8
8
  import { Neo } from "../neo.js";
9
9
  import { ChannelRegistry } from "../../channels/registry.js";
10
+ import { AuditRepository } from "../audit/repository.js";
10
11
  const NEO_BUILTIN_CAPABILITIES = `
11
12
  Neo built-in capabilities (always available — no MCP required):
12
13
  • Config: morpheus_config_query, morpheus_config_update — read/write Morpheus configuration (LLM, channels, UI, etc.)
@@ -89,7 +90,21 @@ export const NeoDelegateTool = tool(async ({ task, context }) => {
89
90
  source: "NeoDelegateTool",
90
91
  level: "info",
91
92
  });
92
- return result;
93
+ if (result.usage) {
94
+ AuditRepository.getInstance().insert({
95
+ session_id: sessionId,
96
+ event_type: 'llm_call',
97
+ agent: 'neo',
98
+ provider: result.usage.provider,
99
+ model: result.usage.model,
100
+ input_tokens: result.usage.inputTokens,
101
+ output_tokens: result.usage.outputTokens,
102
+ duration_ms: result.usage.durationMs,
103
+ status: 'success',
104
+ metadata: { step_count: result.usage.stepCount, mode: 'sync' },
105
+ });
106
+ }
107
+ return result.output;
93
108
  }
94
109
  catch (syncErr) {
95
110
  // Still count as sync delegation so Oracle passes through the error message
@@ -7,6 +7,7 @@ import { ConfigManager } from "../../config/manager.js";
7
7
  import { SmithDelegator } from "../smiths/delegator.js";
8
8
  import { SmithRegistry } from "../smiths/registry.js";
9
9
  import { ChannelRegistry } from "../../channels/registry.js";
10
+ import { AuditRepository } from "../audit/repository.js";
10
11
  /**
11
12
  * Returns true when Smiths are configured in sync mode (inline execution).
12
13
  */
@@ -39,9 +40,10 @@ export const SmithDelegateTool = tool(async ({ smith, task, context }) => {
39
40
  level: "info",
40
41
  });
41
42
  const ctx = TaskRequestContext.get();
43
+ const sessionId = ctx?.session_id ?? 'default';
42
44
  // Notify originating channel
43
45
  if (ctx?.origin_channel && ctx.origin_user_id && ctx.origin_channel !== 'api' && ctx.origin_channel !== 'ui') {
44
- ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, `🤖 Smith '${smith}' is executing your request...`)
46
+ ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, `🕶️ Smith '${smith}' is executing your request...`)
45
47
  .catch(() => { });
46
48
  }
47
49
  try {
@@ -52,7 +54,21 @@ export const SmithDelegateTool = tool(async ({ smith, task, context }) => {
52
54
  source: "SmithDelegateTool",
53
55
  level: "info",
54
56
  });
55
- return result;
57
+ if (result.usage) {
58
+ AuditRepository.getInstance().insert({
59
+ session_id: sessionId,
60
+ event_type: 'llm_call',
61
+ agent: 'smith',
62
+ provider: result.usage.provider,
63
+ model: result.usage.model,
64
+ input_tokens: result.usage.inputTokens,
65
+ output_tokens: result.usage.outputTokens,
66
+ duration_ms: result.usage.durationMs,
67
+ status: 'success',
68
+ metadata: { smith_name: smith, step_count: result.usage.stepCount, mode: 'sync' },
69
+ });
70
+ }
71
+ return result.output;
56
72
  }
57
73
  catch (syncErr) {
58
74
  TaskRequestContext.incrementSyncDelegation();