morpheus-cli 0.9.22 → 0.9.24

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 (54) hide show
  1. package/dist/channels/discord.js +5 -0
  2. package/dist/channels/telegram.js +5 -0
  3. package/dist/cli/commands/restart.js +15 -0
  4. package/dist/cli/commands/start.js +9 -9
  5. package/dist/http/routers/display.js +5 -0
  6. package/dist/http/webhooks-router.js +12 -6
  7. package/dist/runtime/audit/repository.js +69 -1
  8. package/dist/runtime/chronos/worker.js +2 -0
  9. package/dist/runtime/display.js +32 -0
  10. package/dist/runtime/memory/sati/service.js +5 -0
  11. package/dist/runtime/oracle.js +5 -0
  12. package/dist/runtime/providers/factory.js +14 -2
  13. package/dist/runtime/smiths/delegator.js +3 -0
  14. package/dist/runtime/subagents/devkit-instrument.js +5 -0
  15. package/dist/runtime/subagents/link/link.js +120 -77
  16. package/dist/runtime/subagents/trinity/trinity.js +64 -34
  17. package/dist/runtime/tools/factory.js +6 -2
  18. package/dist/runtime/webhooks/dispatcher.js +12 -4
  19. package/dist/runtime/webhooks/repository.js +17 -6
  20. package/dist/ui/assets/{AuditDashboard-z3OBbJ8I.js → AuditDashboard-CfYKdOEt.js} +1 -1
  21. package/dist/ui/assets/{Chat-aFz9FjrD.js → Chat-CYev7-CJ.js} +1 -1
  22. package/dist/ui/assets/{Chronos-MP_NCj2A.js → Chronos-5KR8aZud.js} +1 -1
  23. package/dist/ui/assets/{ConfirmationModal-B3gHIVKY.js → ConfirmationModal-NFwIYI7B.js} +1 -1
  24. package/dist/ui/assets/{Dashboard-OyZXnj44.js → Dashboard-hsjB56la.js} +174 -174
  25. package/dist/ui/assets/{DeleteConfirmationModal-D8QsQzwP.js → DeleteConfirmationModal-BfV370Vv.js} +1 -1
  26. package/dist/ui/assets/{Documents-B8g_yv4f.js → Documents-BNo2tMfG.js} +1 -1
  27. package/dist/ui/assets/{Logs-BWufAtHa.js → Logs-1hBpMPZE.js} +1 -1
  28. package/dist/ui/assets/{MCPManager-lLoGEyBy.js → MCPManager-CvPRHn4C.js} +1 -1
  29. package/dist/ui/assets/{ModelPricing-CuYIUwXt.js → ModelPricing-BbwJFdz4.js} +1 -1
  30. package/dist/ui/assets/{Notifications-nI--fmYx.js → Notifications-C_MA51Gf.js} +1 -1
  31. package/dist/ui/assets/{SatiMemories-DO3JDQBi.js → SatiMemories-Cd9xn98_.js} +1 -1
  32. package/dist/ui/assets/{SessionAudit-BWtJRkj1.js → SessionAudit-BTABenGk.js} +1 -1
  33. package/dist/ui/assets/{Settings-CblauAVd.js → Settings-DRVx4ICA.js} +1 -1
  34. package/dist/ui/assets/{Skills-Dw6G5c8W.js → Skills-DS9p1-S8.js} +1 -1
  35. package/dist/ui/assets/{Smiths-B6-CnRMv.js → Smiths-CMCZaAF_.js} +1 -1
  36. package/dist/ui/assets/{Tasks-DzUyw5z3.js → Tasks-Cvt4sTcs.js} +1 -1
  37. package/dist/ui/assets/{TrinityDatabases-DCjdwnLH.js → TrinityDatabases-qhSUMeCw.js} +1 -1
  38. package/dist/ui/assets/{UsageStats-VajzjndO.js → UsageStats-Cy9HKYOp.js} +1 -1
  39. package/dist/ui/assets/WebhookManager-ByqkTyqs.js +4 -0
  40. package/dist/ui/assets/{agents-CN_AKX_I.js → agents-svEaAPka.js} +1 -1
  41. package/dist/ui/assets/{audit-M-5UGwoK.js → audit-gxRPR5Jb.js} +1 -1
  42. package/dist/ui/assets/{chronos-mZ0RIvh4.js → chronos-ZrBE4yA4.js} +1 -1
  43. package/dist/ui/assets/{config-7LGRnJ26.js → config-B1i6Xxwk.js} +1 -1
  44. package/dist/ui/assets/{index-Db1XEN8v.js → index-DyKlGDg1.js} +2 -2
  45. package/dist/ui/assets/index-gx__iEcl.css +1 -0
  46. package/dist/ui/assets/{mcp-YiYC-9IH.js → mcp-DSddQR1h.js} +1 -1
  47. package/dist/ui/assets/{skills-dc6Xqqhb.js → skills-DIuMjpPF.js} +1 -1
  48. package/dist/ui/assets/{stats-BzqxCDuj.js → stats-CxlRAO2g.js} +1 -1
  49. package/dist/ui/assets/{useCurrency-CEc5edm2.js → useCurrency-BkHiWfcT.js} +1 -1
  50. package/dist/ui/index.html +2 -2
  51. package/dist/ui/sw.js +1 -1
  52. package/package.json +1 -1
  53. package/dist/ui/assets/WebhookManager-BbfMCiy-.js +0 -4
  54. package/dist/ui/assets/index-Bko2TlZY.css +0 -1
@@ -308,7 +308,9 @@ export class DiscordAdapter {
308
308
  filePath = await this.downloadAudioToTemp(attachment.url, contentType);
309
309
  // Transcribe
310
310
  this.display.log(`Transcribing audio for ${message.author.tag}...`, { source: 'Telephonist' });
311
+ this.display.startActivity('telephonist', 'Transcribing audio...');
311
312
  const { text, usage } = await this.telephonist.transcribe(filePath, contentType, apiKey);
313
+ this.display.endActivity('telephonist', true);
312
314
  this.display.log(`Transcription for ${message.author.tag}: "${text}"`, { source: 'Telephonist', level: 'success' });
313
315
  // Show transcription
314
316
  await channel.send(`🎤 "${text}"`);
@@ -326,6 +328,7 @@ export class DiscordAdapter {
326
328
  let ttsFilePath = null;
327
329
  const ttsStart = Date.now();
328
330
  try {
331
+ this.display.startActivity('telephonist', 'Synthesizing TTS...');
329
332
  const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
330
333
  getUsableApiKey(config.audio.apiKey) ||
331
334
  (config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
@@ -333,6 +336,7 @@ export class DiscordAdapter {
333
336
  const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
334
337
  ttsFilePath = ttsResult.filePath;
335
338
  const ttsDurationMs = Date.now() - ttsStart;
339
+ this.display.endActivity('telephonist', true);
336
340
  const attachment = new AttachmentBuilder(ttsFilePath, { name: 'response.ogg' });
337
341
  await channel.send({ files: [attachment] });
338
342
  this.display.log(`Responded to ${message.author.tag} (TTS audio)`, { source: 'Discord' });
@@ -362,6 +366,7 @@ export class DiscordAdapter {
362
366
  }
363
367
  }
364
368
  catch (ttsError) {
369
+ this.display.endActivity('telephonist', false);
365
370
  const ttsDetail = ttsError?.message || String(ttsError);
366
371
  this.display.log(`TTS synthesis failed for ${message.author.tag}: ${ttsDetail} — falling back to text`, { source: 'Telephonist', level: 'warning' });
367
372
  // Audit TTS failure
@@ -352,9 +352,11 @@ export class TelegramAdapter {
352
352
  filePath = await this.downloadToTemp(fileLink);
353
353
  // Transcribe
354
354
  this.display.log(`Transcribing audio for @${user}...`, { source: 'Telephonist' });
355
+ this.display.startActivity('telephonist', 'Transcribing audio...');
355
356
  const transcribeStart = Date.now();
356
357
  const { text, usage } = await this.telephonist.transcribe(filePath, 'audio/ogg', apiKey);
357
358
  const transcribeDurationMs = Date.now() - transcribeStart;
359
+ this.display.endActivity('telephonist', true);
358
360
  this.display.log(`Transcription success for @${user}: "${text}"`, { source: 'Telephonist', level: 'success' });
359
361
  // Audit: record telephonist execution
360
362
  try {
@@ -408,6 +410,7 @@ export class TelegramAdapter {
408
410
  let ttsFilePath = null;
409
411
  const ttsStart = Date.now();
410
412
  try {
413
+ this.display.startActivity('telephonist', 'Synthesizing TTS...');
411
414
  const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
412
415
  getUsableApiKey(config.audio.apiKey) ||
413
416
  (config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
@@ -415,6 +418,7 @@ export class TelegramAdapter {
415
418
  const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
416
419
  ttsFilePath = ttsResult.filePath;
417
420
  const ttsDurationMs = Date.now() - ttsStart;
421
+ this.display.endActivity('telephonist', true);
418
422
  // OGG/Opus → replyWithVoice; everything else (mp3, wav) → replyWithAudio
419
423
  const isOgg = ttsResult.mimeType.includes('ogg') || ttsResult.mimeType.includes('opus');
420
424
  if (isOgg) {
@@ -450,6 +454,7 @@ export class TelegramAdapter {
450
454
  }
451
455
  }
452
456
  catch (ttsError) {
457
+ this.display.endActivity('telephonist', false);
453
458
  const ttsDetail = ttsError?.message || String(ttsError);
454
459
  this.display.log(`TTS synthesis failed for @${user}: ${ttsDetail} — falling back to text`, { source: 'Telephonist', level: 'warning' });
455
460
  // Audit TTS failure
@@ -17,6 +17,12 @@ import { TaskWorker } from '../../runtime/tasks/worker.js';
17
17
  import { TaskNotifier } from '../../runtime/tasks/notifier.js';
18
18
  import { Link } from '../../runtime/subagents/link/link.js';
19
19
  import { LinkWorker } from '../../runtime/subagents/link/worker.js';
20
+ import { ServiceContainer, SERVICE_KEYS } from '../../runtime/container.js';
21
+ import { ChannelNotifierAdapter } from '../../runtime/adapters/ChannelNotifierAdapter.js';
22
+ import { SQLiteTaskEnqueuerAdapter } from '../../runtime/adapters/SQLiteTaskEnqueuerAdapter.js';
23
+ import { SQLiteChatHistoryAdapter } from '../../runtime/adapters/SQLiteChatHistoryAdapter.js';
24
+ import { LangChainProviderAdapter } from '../../runtime/adapters/LangChainProviderAdapter.js';
25
+ import { AuditRepositoryAdapter } from '../../runtime/adapters/AuditRepositoryAdapter.js';
20
26
  export const restartCommand = new Command('restart')
21
27
  .description('Restart the Morpheus agent')
22
28
  .option('--ui', 'Enable web UI', true)
@@ -67,6 +73,15 @@ export const restartCommand = new Command('restart')
67
73
  const config = await configManager.load();
68
74
  // Initialize persistent logging
69
75
  await display.initialize(config.logging);
76
+ // ── Composition Root ─────────────────────────────────────────────────────
77
+ // Register port adapters in the ServiceContainer so consumers can
78
+ // depend on interfaces instead of concrete implementations.
79
+ ServiceContainer.register(SERVICE_KEYS.notifier, new ChannelNotifierAdapter());
80
+ ServiceContainer.register(SERVICE_KEYS.taskEnqueuer, new SQLiteTaskEnqueuerAdapter());
81
+ ServiceContainer.register(SERVICE_KEYS.chatHistory, new SQLiteChatHistoryAdapter());
82
+ ServiceContainer.register(SERVICE_KEYS.providerFactory, new LangChainProviderAdapter());
83
+ ServiceContainer.register(SERVICE_KEYS.auditEmitter, new AuditRepositoryAdapter());
84
+ // ─────────────────────────────────────────────────────────────────────────
70
85
  display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`), { source: 'Zaion' });
71
86
  display.log(chalk.gray(`PID: ${process.pid}`), { source: 'Zaion' });
72
87
  if (options.ui) {
@@ -133,6 +133,15 @@ export const startCommand = new Command('start')
133
133
  const config = await configManager.load();
134
134
  // Initialize persistent logging
135
135
  await display.initialize(config.logging);
136
+ // ── Composition Root ─────────────────────────────────────────────────────
137
+ // Register port adapters in the ServiceContainer so consumers can
138
+ // depend on interfaces instead of concrete implementations.
139
+ ServiceContainer.register(SERVICE_KEYS.notifier, new ChannelNotifierAdapter());
140
+ ServiceContainer.register(SERVICE_KEYS.taskEnqueuer, new SQLiteTaskEnqueuerAdapter());
141
+ ServiceContainer.register(SERVICE_KEYS.chatHistory, new SQLiteChatHistoryAdapter());
142
+ ServiceContainer.register(SERVICE_KEYS.providerFactory, new LangChainProviderAdapter());
143
+ ServiceContainer.register(SERVICE_KEYS.auditEmitter, new AuditRepositoryAdapter());
144
+ // ─────────────────────────────────────────────────────────────────────────
136
145
  display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`));
137
146
  display.log(chalk.gray(`PID: ${process.pid}`));
138
147
  if (options.ui) {
@@ -220,15 +229,6 @@ export const startCommand = new Command('start')
220
229
  await clearPid();
221
230
  process.exit(1);
222
231
  }
223
- // ── Composition Root ─────────────────────────────────────────────────────
224
- // Register port adapters in the ServiceContainer so consumers can
225
- // depend on interfaces instead of concrete implementations.
226
- ServiceContainer.register(SERVICE_KEYS.notifier, new ChannelNotifierAdapter());
227
- ServiceContainer.register(SERVICE_KEYS.taskEnqueuer, new SQLiteTaskEnqueuerAdapter());
228
- ServiceContainer.register(SERVICE_KEYS.chatHistory, new SQLiteChatHistoryAdapter());
229
- ServiceContainer.register(SERVICE_KEYS.providerFactory, new LangChainProviderAdapter());
230
- ServiceContainer.register(SERVICE_KEYS.auditEmitter, new AuditRepositoryAdapter());
231
- // ─────────────────────────────────────────────────────────────────────────
232
232
  const adapters = [];
233
233
  let httpServer;
234
234
  const taskWorker = new TaskWorker();
@@ -20,13 +20,18 @@ export function createDisplayRouter() {
20
20
  const onMessage = (payload) => {
21
21
  res.write(`data: ${JSON.stringify({ type: 'message', ...payload })}\n\n`);
22
22
  };
23
+ const onMessageSent = (payload) => {
24
+ res.write(`data: ${JSON.stringify({ type: 'message_sent', ...payload })}\n\n`);
25
+ };
23
26
  display.on('activity_start', onActivityStart);
24
27
  display.on('activity_end', onActivityEnd);
25
28
  display.on('message', onMessage);
29
+ display.on('message_sent', onMessageSent);
26
30
  req.on('close', () => {
27
31
  display.off('activity_start', onActivityStart);
28
32
  display.off('activity_end', onActivityEnd);
29
33
  display.off('message', onMessage);
34
+ display.off('message_sent', onMessageSent);
30
35
  });
31
36
  });
32
37
  return router;
@@ -12,6 +12,7 @@ const CreateWebhookSchema = z.object({
12
12
  .regex(/^[a-z0-9-_]+$/, 'Name must be a slug: lowercase letters, numbers, hyphens, underscores only'),
13
13
  prompt: z.string().min(1).max(10_000),
14
14
  notification_channels: z.array(z.enum(['ui', 'telegram', 'discord'])).min(1).default(['ui']),
15
+ requires_api_key: z.boolean().default(true),
15
16
  });
16
17
  const UpdateWebhookSchema = z.object({
17
18
  name: z
@@ -23,6 +24,7 @@ const UpdateWebhookSchema = z.object({
23
24
  prompt: z.string().min(1).max(10_000).optional(),
24
25
  enabled: z.boolean().optional(),
25
26
  notification_channels: z.array(z.enum(['ui', 'telegram', 'discord'])).min(1).optional(),
27
+ requires_api_key: z.boolean().optional(),
26
28
  });
27
29
  const MarkReadSchema = z.object({
28
30
  ids: z.array(z.string().uuid()).min(1),
@@ -39,14 +41,18 @@ export function createWebhooksRouter() {
39
41
  router.post('/trigger/:webhook_name', async (req, res) => {
40
42
  const { webhook_name } = req.params;
41
43
  const apiKey = req.headers['x-api-key'];
42
- if (!apiKey || typeof apiKey !== 'string') {
43
- return res.status(401).json({ error: 'Missing x-api-key header' });
44
- }
45
- const webhook = repo.getAndValidateWebhook(webhook_name, apiKey);
46
- if (!webhook) {
47
- // Intentionally ambiguous — don't reveal whether the name exists or is disabled
44
+ // 1. First lookup by name to check if it requires an API key
45
+ const webhook = repo.getWebhookByName(webhook_name);
46
+ if (!webhook || !webhook.enabled) {
47
+ // Intentionally ambiguous
48
48
  return res.status(401).json({ error: 'Invalid webhook name or api key' });
49
49
  }
50
+ // 2. Validate API key if required
51
+ if (webhook.requires_api_key) {
52
+ if (!apiKey || typeof apiKey !== 'string' || apiKey !== webhook.api_key) {
53
+ return res.status(401).json({ error: 'Invalid webhook name or api key' });
54
+ }
55
+ }
50
56
  const payload = req.body ?? {};
51
57
  // Create pending notification immediately
52
58
  const notification = repo.createNotification({
@@ -46,19 +46,87 @@ export class AuditRepository {
46
46
  `);
47
47
  }
48
48
  insert(event) {
49
+ const createdAt = Date.now();
50
+ const eventId = randomUUID();
49
51
  try {
50
52
  this.db.prepare(`
51
53
  INSERT INTO audit_events
52
54
  (id, session_id, task_id, event_type, agent, tool_name, provider, model,
53
55
  input_tokens, output_tokens, duration_ms, status, metadata, created_at)
54
56
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
55
- `).run(randomUUID(), event.session_id, event.task_id ?? null, event.event_type, event.agent ?? null, event.tool_name ?? null, event.provider ?? null, event.model ?? null, event.input_tokens ?? null, event.output_tokens ?? null, event.duration_ms ?? null, event.status ?? null, event.metadata ? JSON.stringify(event.metadata) : null, Date.now());
57
+ `).run(eventId, event.session_id, event.task_id ?? null, event.event_type, event.agent ?? null, event.tool_name ?? null, event.provider ?? null, event.model ?? null, event.input_tokens ?? null, event.output_tokens ?? null, event.duration_ms ?? null, event.status ?? null, event.metadata ? JSON.stringify(event.metadata) : null, createdAt);
58
+ // Emit activity event for visualization when agent is working
59
+ this.emitActivityEvent(event, createdAt);
56
60
  }
57
61
  catch (err) {
58
62
  // Non-critical — never let audit recording break the main flow
59
63
  DisplayManager.getInstance().log(`AuditRepository.insert failed: ${err?.message ?? String(err)}`, { source: 'Audit', level: 'error' });
60
64
  }
61
65
  }
66
+ /**
67
+ * Emit activity events to DisplayManager for real-time visualization.
68
+ * This allows the frontend to show which agent is actively working.
69
+ */
70
+ emitActivityEvent(event, timestamp) {
71
+ const display = DisplayManager.getInstance();
72
+ const agent = event.agent;
73
+ // Only emit for agent-specific events (not system events like task_created/completed)
74
+ const agentEventTypes = ['llm_call', 'tool_call', 'mcp_tool', 'telephonist', 'skill_loaded', 'chronos_job', 'memory_recovery', 'memory_persist'];
75
+ if (!agent || !agentEventTypes.includes(event.event_type)) {
76
+ return;
77
+ }
78
+ // Build descriptive message based on event type
79
+ let message;
80
+ switch (event.event_type) {
81
+ case 'llm_call':
82
+ message = `LLM call (${event.model || event.provider || 'unknown'})`;
83
+ break;
84
+ case 'tool_call':
85
+ message = `Executing tool: ${event.tool_name || 'unknown'}`;
86
+ break;
87
+ case 'mcp_tool':
88
+ message = `MCP tool: ${event.tool_name || 'unknown'}`;
89
+ break;
90
+ case 'telephonist':
91
+ const op = event.metadata;
92
+ const operation = op?.operation;
93
+ message = operation === 'tts' ? 'Synthesizing TTS...' : 'Transcribing audio...';
94
+ break;
95
+ case 'skill_loaded':
96
+ message = `Loading skill: ${event.tool_name || 'unknown'}`;
97
+ break;
98
+ case 'chronos_job':
99
+ message = `Running scheduled job: ${event.tool_name || 'unknown'}`;
100
+ break;
101
+ case 'memory_recovery':
102
+ message = 'Recovering memories...';
103
+ break;
104
+ case 'memory_persist':
105
+ message = 'Persisting memories...';
106
+ break;
107
+ default:
108
+ message = `Working (${event.event_type})`;
109
+ }
110
+ // Emit activity_start with duration hint
111
+ // Frontend will use duration_ms to determine how long to keep agent active
112
+ display.emit('activity_start', {
113
+ agent: agent.toLowerCase(),
114
+ message,
115
+ timestamp,
116
+ duration_ms: event.duration_ms || 0,
117
+ event_type: event.event_type,
118
+ });
119
+ // Emit activity_end after duration (if duration_ms is provided)
120
+ if (event.duration_ms && event.duration_ms > 0) {
121
+ setTimeout(() => {
122
+ display.emit('activity_end', {
123
+ agent: agent.toLowerCase(),
124
+ timestamp: Date.now(),
125
+ duration_ms: event.duration_ms,
126
+ });
127
+ }, Math.min(event.duration_ms, 30000)); // Cap at 30s to avoid long-running timeouts
128
+ }
129
+ }
62
130
  countBySession(sessionId) {
63
131
  const row = this.db.prepare(`SELECT COUNT(*) as n FROM audit_events WHERE session_id = ?`).get(sessionId);
64
132
  return row?.n ?? 0;
@@ -74,6 +74,7 @@ export class ChronosWorker {
74
74
  async executeJob(job) {
75
75
  const display = DisplayManager.getInstance();
76
76
  const execId = randomUUID();
77
+ display.startActivity('chronos', 'Running scheduled job...');
77
78
  display.log(`Job ${job.id} triggered — "${job.prompt.slice(0, 60)}"`, { source: 'Chronos' });
78
79
  // Resolve session: prefer the session where the job was originally created,
79
80
  // fall back to Oracle's current session, then to most recent active session.
@@ -150,6 +151,7 @@ export class ChronosWorker {
150
151
  }
151
152
  finally {
152
153
  ChronosWorker.isExecuting = false;
154
+ display.endActivity('chronos', true);
153
155
  if (job.schedule_type === 'once') {
154
156
  this.repo.disableJob(job.id);
155
157
  display.log(`Job ${job.id} auto-disabled (once-type)`, { source: 'Chronos' });
@@ -70,6 +70,38 @@ export class DisplayManager extends EventEmitter {
70
70
  this.spinner.stop();
71
71
  }
72
72
  }
73
+ /**
74
+ * Start an activity for an agent - emits activity_start event for visualization.
75
+ * Use this when an agent begins a task (e.g., before calling an API).
76
+ */
77
+ startActivity(agent, message) {
78
+ this.emit('activity_start', {
79
+ agent: agent.toLowerCase(),
80
+ message,
81
+ timestamp: Date.now(),
82
+ });
83
+ }
84
+ /**
85
+ * End an activity for an agent - emits activity_end event for visualization.
86
+ * Use this when an agent finishes a task (e.g., after receiving API response).
87
+ */
88
+ endActivity(agent, success) {
89
+ this.emit('activity_end', {
90
+ agent: agent.toLowerCase(),
91
+ timestamp: Date.now(),
92
+ success,
93
+ });
94
+ }
95
+ /**
96
+ * Emit a message sent event - for visualizing outgoing messages (e.g., rocket launching).
97
+ * This is a transient event that appears briefly in the visualizer.
98
+ */
99
+ emitMessageSent(agent = 'oracle') {
100
+ this.emit('message_sent', {
101
+ agent: agent.toLowerCase(),
102
+ timestamp: Date.now(),
103
+ });
104
+ }
73
105
  log(message, options) {
74
106
  const wasSpinning = this.spinner.isSpinning;
75
107
  const previousText = this.spinner.text;
@@ -25,6 +25,7 @@ export class SatiService {
25
25
  this.repository.initialize();
26
26
  }
27
27
  async recover(currentMessage, recentMessages) {
28
+ display.startActivity('sati', 'Recovering memories...');
28
29
  const satiConfig = ConfigManager.getInstance().getSatiConfig();
29
30
  const memoryLimit = satiConfig.memory_limit || 10;
30
31
  const enabled_vector_search = satiConfig.enabled_archived_sessions ?? true;
@@ -43,6 +44,7 @@ export class SatiService {
43
44
  console.warn('[Sati] Failed to generate embedding:', err);
44
45
  }
45
46
  const memories = this.repository.search(currentMessage, memoryLimit, queryEmbedding);
47
+ display.endActivity('sati', true);
46
48
  return {
47
49
  relevant_memories: memories.map(m => ({
48
50
  summary: m.summary,
@@ -52,6 +54,7 @@ export class SatiService {
52
54
  };
53
55
  }
54
56
  async evaluateAndPersist(conversation, userSessionId) {
57
+ display.startActivity('sati', 'Evaluating and persisting memories...');
55
58
  try {
56
59
  const satiConfig = ConfigManager.getInstance().getSatiConfig();
57
60
  if (!satiConfig)
@@ -239,7 +242,9 @@ export class SatiService {
239
242
  }
240
243
  catch (error) {
241
244
  console.error('[SatiService] Evaluation failed:', error);
245
+ display.endActivity('sati', false);
242
246
  }
247
+ display.endActivity('sati', true);
243
248
  }
244
249
  generateHash(content) {
245
250
  return createHash('sha256').update(content.trim().toLowerCase()).digest('hex');
@@ -519,6 +519,8 @@ Use it to inform your response and tool selection (if needed), but do not assume
519
519
  let contextDelegationAcks = [];
520
520
  let syncDelegationCount = 0;
521
521
  const oracleStartMs = Date.now();
522
+ const display = DisplayManager.getInstance();
523
+ display.startActivity('oracle', `LLM call (${this.config.llm.model})`);
522
524
  const response = await TaskRequestContext.run(invokeContext, async () => {
523
525
  const agentResponse = await this.provider.invoke({ messages }, { recursionLimit: 100 });
524
526
  contextDelegationAcks = TaskRequestContext.getDelegationAcks();
@@ -526,6 +528,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
526
528
  return agentResponse;
527
529
  });
528
530
  const oracleDurationMs = Date.now() - oracleStartMs;
531
+ display.endActivity('oracle', true);
529
532
  // Emit llm_call audit event for Oracle's own invocation
530
533
  try {
531
534
  const lastMsg = response.messages[response.messages.length - 1];
@@ -643,6 +646,8 @@ Use it to inform your response and tool selection (if needed), but do not assume
643
646
  // Persist user message + all generated messages in a single transaction
644
647
  await callHistory.addMessages([userMessage, ...newGeneratedMessages]);
645
648
  }
649
+ // Emit message sent event for visualization (rocket animation)
650
+ this.display.emitMessageSent('oracle');
646
651
  }
647
652
  this.display.log('Response generated.', { source: 'Oracle' });
648
653
  // Sati Middleware: skip memory evaluation for delegation-only acknowledgements.
@@ -14,23 +14,35 @@ export class ProviderFactory {
14
14
  return createMiddleware({
15
15
  name: "ToolMonitoringMiddleware",
16
16
  wrapToolCall: (request, handler) => {
17
- display.log(`Executing tool: ${request.toolCall.name}`, { level: "warning", source: 'ConstructLoad' });
17
+ const toolName = request.toolCall.name;
18
+ // Determine which agent is running this tool based on context
19
+ // This is a heuristic - the actual agent should be passed in context
20
+ let agent = 'neo'; // Default to neo for MCP tools
21
+ const ctx = TaskRequestContext.get();
22
+ if (ctx?.session_id) {
23
+ // Try to determine agent from context - this is a simplified approach
24
+ // In practice, we'd need to pass the agent through the context
25
+ }
26
+ display.startActivity(agent, `Executing tool: ${toolName}`);
27
+ display.log(`Executing tool: ${toolName}`, { level: "warning", source: 'ConstructLoad' });
18
28
  display.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`, { level: "info", source: 'ConstructLoad' });
19
29
  // Verbose mode: notify originating channel about which tool is running
20
30
  const verboseEnabled = ConfigManager.getInstance().get().verbose_mode !== false;
21
31
  if (verboseEnabled) {
22
32
  const ctx = TaskRequestContext.get();
23
33
  if (ctx?.origin_channel && ctx.origin_user_id && !SILENT_CHANNELS.has(ctx.origin_channel)) {
24
- ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, `🔧 executing: ${request.toolCall.name}`)
34
+ ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, `🔧 executing: ${toolName}`)
25
35
  .catch(() => { });
26
36
  }
27
37
  }
28
38
  try {
29
39
  const result = handler(request);
40
+ display.endActivity(agent, true);
30
41
  display.log(`Tool completed successfully. Result: ${JSON.stringify(result)}`, { level: "info", source: 'ConstructLoad' });
31
42
  return result;
32
43
  }
33
44
  catch (e) {
45
+ display.endActivity(agent, false);
34
46
  display.log(`Tool failed: ${e}`, { level: "error", source: 'ConstructLoad' });
35
47
  throw e;
36
48
  }
@@ -133,6 +133,7 @@ export class SmithDelegator {
133
133
  level: 'info',
134
134
  meta: { smith: smithName },
135
135
  });
136
+ this.display.startActivity('smith', `Delegating to Smith '${smithName}'...`);
136
137
  try {
137
138
  // Build proxy tools for this Smith's capabilities
138
139
  const proxyTools = this.buildProxyTools(smithName);
@@ -193,6 +194,7 @@ Respond in the same language as the task.`);
193
194
  source: 'SmithDelegator',
194
195
  level: 'info',
195
196
  });
197
+ this.display.endActivity('smith', true);
196
198
  return {
197
199
  output: content,
198
200
  usage: {
@@ -210,6 +212,7 @@ Respond in the same language as the task.`);
210
212
  source: 'SmithDelegator',
211
213
  level: 'error',
212
214
  });
215
+ this.display.endActivity('smith', false);
213
216
  return { output: `❌ Smith '${smithName}' delegation failed: ${err.message}` };
214
217
  }
215
218
  }
@@ -1,4 +1,6 @@
1
1
  import { AuditRepository } from '../audit/repository.js';
2
+ import { DisplayManager } from '../display.js';
3
+ const display = DisplayManager.getInstance();
2
4
  /**
3
5
  * Wraps a StructuredTool to record audit events on each invocation.
4
6
  * The `getSessionId` getter is called at invocation time so it reflects
@@ -10,6 +12,7 @@ function instrumentTool(tool, getSessionId, getAgent) {
10
12
  const startMs = Date.now();
11
13
  const sessionId = getSessionId() ?? 'unknown';
12
14
  const agent = getAgent();
15
+ display.startActivity(agent, `Executing tool: ${tool.name}`);
13
16
  try {
14
17
  const result = await original(input, runManager);
15
18
  const durationMs = Date.now() - startMs;
@@ -21,9 +24,11 @@ function instrumentTool(tool, getSessionId, getAgent) {
21
24
  duration_ms: durationMs,
22
25
  status: 'success',
23
26
  });
27
+ display.endActivity(agent, true);
24
28
  return result;
25
29
  }
26
30
  catch (err) {
31
+ display.endActivity(agent, false);
27
32
  const durationMs = Date.now() - startMs;
28
33
  AuditRepository.getInstance().insert({
29
34
  session_id: sessionId,