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.
- package/dist/devkit/registry.js +46 -1
- package/dist/http/api.js +31 -0
- package/dist/runtime/apoc.js +28 -11
- package/dist/runtime/audit/repository.js +132 -0
- package/dist/runtime/audit/types.js +1 -0
- package/dist/runtime/chronos/worker.js +20 -0
- package/dist/runtime/keymaker.js +22 -10
- package/dist/runtime/memory/sati/repository.js +1 -1
- package/dist/runtime/memory/sati/service.js +21 -0
- package/dist/runtime/memory/sqlite.js +73 -7
- package/dist/runtime/neo.js +27 -11
- package/dist/runtime/oracle.js +28 -1
- package/dist/runtime/smiths/connection.js +7 -0
- package/dist/runtime/smiths/delegator.js +30 -14
- package/dist/runtime/smiths/registry.js +15 -4
- package/dist/runtime/tasks/repository.js +15 -3
- package/dist/runtime/tasks/worker.js +79 -8
- package/dist/runtime/tools/apoc-tool.js +16 -1
- package/dist/runtime/tools/factory.js +42 -2
- package/dist/runtime/tools/neo-tool.js +16 -1
- package/dist/runtime/tools/smith-tool.js +18 -2
- package/dist/runtime/tools/trinity-tool.js +16 -1
- package/dist/runtime/trinity.js +25 -10
- package/dist/ui/assets/index-CZS235KG.js +177 -0
- package/dist/ui/assets/index-QQyZIsmH.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-Clx8mDZ2.js +0 -117
- package/dist/ui/assets/index-KRT9p6jS.css +0 -1
package/dist/runtime/neo.js
CHANGED
|
@@ -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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
persisted.
|
|
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
|
|
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");
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
persisted.
|
|
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
|
|
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
|
-
* -
|
|
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
|
|
226
|
+
// Add new Smiths or reconnect existing ones whose connection details changed
|
|
227
227
|
for (const entry of config.entries) {
|
|
228
|
-
|
|
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
|
|
64
|
+
let result;
|
|
55
65
|
switch (task.agent) {
|
|
56
66
|
case 'apoc': {
|
|
57
67
|
const apoc = Apoc.getInstance();
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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();
|