morpheus-cli 0.9.12 → 0.9.20

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 (106) hide show
  1. package/README.md +49 -18
  2. package/dist/channels/discord.js +93 -6
  3. package/dist/channels/telegram.js +112 -12
  4. package/dist/cli/commands/restart.js +2 -2
  5. package/dist/cli/commands/start.js +17 -2
  6. package/dist/config/manager.js +20 -1
  7. package/dist/config/paths.js +4 -0
  8. package/dist/config/schemas.js +15 -0
  9. package/dist/http/api.js +7 -3
  10. package/dist/http/routers/agents.js +9 -0
  11. package/dist/http/routers/danger.js +4 -5
  12. package/dist/http/routers/link.js +4 -4
  13. package/dist/runtime/__tests__/telephonist-tts.test.js +84 -0
  14. package/dist/runtime/adapters/AuditRepositoryAdapter.js +6 -0
  15. package/dist/runtime/adapters/ChannelNotifierAdapter.js +9 -0
  16. package/dist/runtime/adapters/LangChainProviderAdapter.js +9 -0
  17. package/dist/runtime/adapters/SQLiteChatHistoryAdapter.js +15 -0
  18. package/dist/runtime/adapters/SQLiteTaskEnqueuerAdapter.js +6 -0
  19. package/dist/runtime/adapters/index.js +5 -0
  20. package/dist/runtime/audit/repository.js +6 -2
  21. package/dist/runtime/chronos/repository.js +7 -5
  22. package/dist/runtime/chronos/worker.js +27 -4
  23. package/dist/runtime/chronos/worker.test.js +3 -2
  24. package/dist/runtime/container.js +50 -0
  25. package/dist/runtime/hot-reload.js +6 -9
  26. package/dist/runtime/memory/backfill-embeddings.js +2 -3
  27. package/dist/runtime/memory/sati/repository.js +3 -3
  28. package/dist/runtime/memory/sqlite.js +40 -14
  29. package/dist/runtime/memory/trinity-db.js +2 -2
  30. package/dist/runtime/oracle.js +78 -43
  31. package/dist/runtime/ports/IChatHistory.js +1 -0
  32. package/dist/runtime/ports/ILLMProviderFactory.js +1 -0
  33. package/dist/runtime/ports/INotifier.js +1 -0
  34. package/dist/runtime/ports/ITaskEnqueuer.js +1 -0
  35. package/dist/runtime/ports/index.js +1 -0
  36. package/dist/runtime/providers/factory.js +8 -52
  37. package/dist/runtime/providers/strategies.js +66 -0
  38. package/dist/runtime/setup/repository.js +2 -2
  39. package/dist/runtime/subagents/ISubagent.js +1 -0
  40. package/dist/runtime/{apoc.js → subagents/apoc.js} +20 -7
  41. package/dist/runtime/{devkit-instrument.js → subagents/devkit-instrument.js} +1 -1
  42. package/dist/runtime/subagents/index.js +12 -0
  43. package/dist/runtime/{link.js → subagents/link/link.js} +24 -10
  44. package/dist/runtime/{link-repository.js → subagents/link/repository.js} +4 -4
  45. package/dist/runtime/{link-search.js → subagents/link/search.js} +3 -3
  46. package/dist/runtime/{link-worker.js → subagents/link/worker.js} +9 -9
  47. package/dist/runtime/{neo.js → subagents/neo.js} +24 -10
  48. package/dist/runtime/subagents/registry.js +134 -0
  49. package/dist/runtime/{trinity.js → subagents/trinity/trinity.js} +23 -9
  50. package/dist/runtime/{subagent-utils.js → subagents/utils.js} +2 -2
  51. package/dist/runtime/tasks/repository.js +2 -2
  52. package/dist/runtime/tasks/worker.js +6 -70
  53. package/dist/runtime/telephonist.js +160 -0
  54. package/dist/runtime/tools/chronos-tools.js +1 -0
  55. package/dist/runtime/tools/delegation-utils.js +5 -7
  56. package/dist/runtime/tools/morpheus-tools.js +9 -10
  57. package/dist/runtime/tools/smith-tool.js +5 -7
  58. package/dist/runtime/webhooks/dispatcher.js +4 -0
  59. package/dist/runtime/webhooks/repository.js +2 -2
  60. package/dist/types/config.js +6 -0
  61. package/dist/ui/assets/AuditDashboard-Cu33zb_7.js +1 -0
  62. package/dist/ui/assets/Chat-mt1j5V55.js +41 -0
  63. package/dist/ui/assets/{Chronos-D1yAb4M5.js → Chronos-Bq_h41cw.js} +1 -1
  64. package/dist/ui/assets/{ConfirmationModal-DxUHZgTy.js → ConfirmationModal-CxLP8iC6.js} +1 -1
  65. package/dist/ui/assets/{Dashboard-BzxmcHaS.js → Dashboard-D0LAlHtG.js} +1 -1
  66. package/dist/ui/assets/{DeleteConfirmationModal-CqNXT_YQ.js → DeleteConfirmationModal-kZ_c3sFk.js} +1 -1
  67. package/dist/ui/assets/{Documents-DLFZdmim.js → Documents-nlQNoUcq.js} +1 -1
  68. package/dist/ui/assets/{Logs-B1Bpy9dB.js → Logs-C1tlg574.js} +1 -1
  69. package/dist/ui/assets/{MCPManager-BbUDMh5Q.js → MCPManager-Do7isizG.js} +1 -1
  70. package/dist/ui/assets/ModelPricing-BeJ7oXBA.js +1 -0
  71. package/dist/ui/assets/{Notifications-8Cqj-mNp.js → Notifications-Cg5CMlY0.js} +1 -1
  72. package/dist/ui/assets/{SatiMemories-CdHUe6di.js → SatiMemories-D9l6s8Pc.js} +1 -1
  73. package/dist/ui/assets/SessionAudit-Da1ySlYg.js +9 -0
  74. package/dist/ui/assets/Settings-DpXwpEhO.js +49 -0
  75. package/dist/ui/assets/{Skills-0k7A2T5_.js → Skills-DaqCY8QH.js} +1 -1
  76. package/dist/ui/assets/Smiths-DA-x4KFT.js +1 -0
  77. package/dist/ui/assets/Switch-CJTE4ZQm.js +1 -0
  78. package/dist/ui/assets/Tasks-DU49M9U-.js +1 -0
  79. package/dist/ui/assets/{TrinityDatabases-CGna6IMX.js → TrinityDatabases-CoKzKTL-.js} +1 -1
  80. package/dist/ui/assets/{UsageStats-B7EzZlZe.js → UsageStats-cds352Pj.js} +1 -1
  81. package/dist/ui/assets/{WebhookManager-Bb7KiucS.js → WebhookManager-DdAdHQUk.js} +1 -1
  82. package/dist/ui/assets/agents-B1z_dlQC.js +1 -0
  83. package/dist/ui/assets/{audit-CJ2Ms81U.js → audit-BAhaGrKY.js} +1 -1
  84. package/dist/ui/assets/{chronos-Bm68OSy4.js → chronos-DGD_Md9M.js} +1 -1
  85. package/dist/ui/assets/config-BwTXe5M2.js +1 -0
  86. package/dist/ui/assets/{index-BxN2w9sY.js → index-BcX5O7kY.js} +2 -2
  87. package/dist/ui/assets/index-Cjli-AD7.css +1 -0
  88. package/dist/ui/assets/{mcp-BE_OVkBe.js → mcp-BlkruPaA.js} +1 -1
  89. package/dist/ui/assets/{skills-Dt0qU4gH.js → skills-CtCb-52u.js} +1 -1
  90. package/dist/ui/assets/{stats-Bmdps1LR.js → stats-BiPI2kaw.js} +1 -1
  91. package/dist/ui/assets/useCurrency-BCdG-pHx.js +1 -0
  92. package/dist/ui/index.html +2 -2
  93. package/dist/ui/sw.js +1 -1
  94. package/package.json +1 -1
  95. package/dist/ui/assets/AuditDashboard-CM1YN1uk.js +0 -1
  96. package/dist/ui/assets/Chat-D4y-g6Tw.js +0 -41
  97. package/dist/ui/assets/ModelPricing-DCl-2_eJ.js +0 -1
  98. package/dist/ui/assets/SessionAudit-CtVHK_IH.js +0 -9
  99. package/dist/ui/assets/Settings-Clge45Z0.js +0 -49
  100. package/dist/ui/assets/Smiths-gjgBMN1F.js +0 -1
  101. package/dist/ui/assets/Tasks-AQ3MrrMp.js +0 -1
  102. package/dist/ui/assets/config-C88yQ_CP.js +0 -1
  103. package/dist/ui/assets/index-C3Ff736M.css +0 -1
  104. /package/dist/runtime/{ISubagent.js → ports/IAuditEmitter.js} +0 -0
  105. /package/dist/runtime/{link-chunker.js → subagents/link/chunker.js} +0 -0
  106. /package/dist/runtime/{trinity-connector.js → subagents/trinity/connector.js} +0 -0
@@ -0,0 +1,134 @@
1
+ import { DisplayManager } from "../display.js";
2
+ /**
3
+ * System-level agents that are not subagents but need display metadata for audit/UI.
4
+ */
5
+ export const SYSTEM_AGENTS = [
6
+ {
7
+ agentKey: 'oracle', auditAgent: 'oracle', label: 'Oracle',
8
+ delegateToolName: '', emoji: '🔮', color: 'blue',
9
+ description: 'Root orchestrator',
10
+ colorClass: 'text-blue-600 dark:text-blue-400',
11
+ bgClass: 'bg-blue-50 dark:bg-blue-900/10',
12
+ badgeClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
13
+ },
14
+ {
15
+ agentKey: 'chronos', auditAgent: 'chronos', label: 'Chronos',
16
+ delegateToolName: '', emoji: '⏰', color: 'orange',
17
+ description: 'Temporal scheduler',
18
+ colorClass: 'text-orange-600 dark:text-orange-400',
19
+ bgClass: 'bg-orange-50 dark:bg-orange-900/10',
20
+ badgeClass: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
21
+ },
22
+ {
23
+ agentKey: 'sati', auditAgent: 'sati', label: 'Sati',
24
+ delegateToolName: '', emoji: '🧠', color: 'emerald',
25
+ description: 'Long-term memory',
26
+ colorClass: 'text-emerald-600 dark:text-emerald-400',
27
+ bgClass: 'bg-emerald-50 dark:bg-emerald-900/10',
28
+ badgeClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
29
+ },
30
+ {
31
+ agentKey: 'telephonist', auditAgent: 'telephonist', label: 'Telephonist',
32
+ delegateToolName: '', emoji: '📞', color: 'rose',
33
+ description: 'Audio transcription',
34
+ colorClass: 'text-rose-600 dark:text-rose-400',
35
+ bgClass: 'bg-rose-50 dark:bg-rose-900/10',
36
+ badgeClass: 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300',
37
+ },
38
+ ];
39
+ /**
40
+ * Central registry for all subagents. Static singleton following ChannelRegistry pattern.
41
+ */
42
+ export class SubagentRegistry {
43
+ static agents = new Map();
44
+ static display = DisplayManager.getInstance();
45
+ static register(reg) {
46
+ SubagentRegistry.agents.set(reg.agentKey, reg);
47
+ SubagentRegistry.display.log(`Subagent registered: ${reg.label} (${reg.agentKey})`, { source: 'SubagentRegistry', level: 'info' });
48
+ }
49
+ static get(agentKey) {
50
+ return SubagentRegistry.agents.get(agentKey);
51
+ }
52
+ static getAll() {
53
+ return [...SubagentRegistry.agents.values()];
54
+ }
55
+ static getByToolName(toolName) {
56
+ for (const reg of SubagentRegistry.agents.values()) {
57
+ if (reg.delegateToolName === toolName)
58
+ return reg;
59
+ }
60
+ return undefined;
61
+ }
62
+ /** Returns the set of all delegation tool names (replaces ORACLE_DELEGATION_TOOLS). */
63
+ static getDelegationToolNames() {
64
+ const names = new Set();
65
+ for (const reg of SubagentRegistry.agents.values()) {
66
+ if (reg.delegateToolName)
67
+ names.add(reg.delegateToolName);
68
+ }
69
+ return names;
70
+ }
71
+ /** Returns all delegation tools for Oracle's coreTools array. */
72
+ static getDelegationTools() {
73
+ return SubagentRegistry.getAll().map(reg => reg.instance.createDelegateTool());
74
+ }
75
+ /** Sets session ID on all registered subagents that support it. */
76
+ static setAllSessionIds(sessionId) {
77
+ for (const reg of SubagentRegistry.agents.values()) {
78
+ reg.setSessionId?.(sessionId);
79
+ }
80
+ }
81
+ /** Refreshes dynamic descriptions on all subagents that support it. */
82
+ static async refreshAllCatalogs() {
83
+ const promises = [];
84
+ for (const reg of SubagentRegistry.agents.values()) {
85
+ if (reg.hasDynamicDescription && reg.refreshCatalog) {
86
+ promises.push(reg.refreshCatalog().catch(() => { }));
87
+ }
88
+ }
89
+ await Promise.all(promises);
90
+ }
91
+ /** Executes a task by routing to the correct subagent (replaces worker switch/case). */
92
+ static async executeTask(task) {
93
+ const reg = SubagentRegistry.agents.get(task.agent);
94
+ if (!reg) {
95
+ throw new Error(`Unknown task agent: ${task.agent}`);
96
+ }
97
+ if (reg.executeTask) {
98
+ return reg.executeTask(task);
99
+ }
100
+ return reg.instance.execute(task.input, task.context ?? undefined, task.session_id, {
101
+ origin_channel: task.origin_channel,
102
+ session_id: task.session_id,
103
+ origin_message_id: task.origin_message_id ?? undefined,
104
+ origin_user_id: task.origin_user_id ?? undefined,
105
+ });
106
+ }
107
+ /** Maps task agent key to audit agent name (e.g. 'trinit' -> 'trinity'). */
108
+ static resolveAuditAgent(taskAgent) {
109
+ const reg = SubagentRegistry.agents.get(taskAgent);
110
+ return (reg?.auditAgent ?? taskAgent);
111
+ }
112
+ /** Returns display metadata for all agents (subagents + system agents). */
113
+ static getDisplayMetadata() {
114
+ const subagents = SubagentRegistry.getAll().map(reg => ({
115
+ agentKey: reg.agentKey,
116
+ auditAgent: reg.auditAgent,
117
+ label: reg.label,
118
+ delegateToolName: reg.delegateToolName,
119
+ emoji: reg.emoji,
120
+ color: reg.color,
121
+ description: reg.description,
122
+ colorClass: reg.colorClass,
123
+ bgClass: reg.bgClass,
124
+ badgeClass: reg.badgeClass,
125
+ }));
126
+ return [...SYSTEM_AGENTS, ...subagents];
127
+ }
128
+ /** Reloads all registered subagents. */
129
+ static async reloadAll() {
130
+ for (const reg of SubagentRegistry.agents.values()) {
131
+ await reg.instance.reload();
132
+ }
133
+ }
134
+ }
@@ -1,14 +1,15 @@
1
1
  import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
2
  import { tool } from "@langchain/core/tools";
3
3
  import { z } from "zod";
4
- import { ConfigManager } from "../config/manager.js";
5
- import { ProviderFactory } from "./providers/factory.js";
6
- import { ProviderError } from "./errors.js";
7
- import { DisplayManager } from "./display.js";
8
- import { DatabaseRegistry } from "./memory/trinity-db.js";
9
- import { testConnection, introspectSchema, executeQuery } from "./trinity-connector.js";
10
- import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
11
- import { buildDelegationTool } from "./tools/delegation-utils.js";
4
+ import { ConfigManager } from "../../../config/manager.js";
5
+ import { ServiceContainer, SERVICE_KEYS } from "../../container.js";
6
+ import { ProviderError } from "../../errors.js";
7
+ import { DisplayManager } from "../../display.js";
8
+ import { DatabaseRegistry } from "../../memory/trinity-db.js";
9
+ import { testConnection, introspectSchema, executeQuery } from "./connector.js";
10
+ import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "../utils.js";
11
+ import { buildDelegationTool } from "../../tools/delegation-utils.js";
12
+ import { SubagentRegistry } from "../registry.js";
12
13
  const TRINITY_BASE_DESCRIPTION = `Delegate a database task to Trinity, the specialized database subagent, asynchronously.
13
14
 
14
15
  This tool enqueues a background task and returns an acknowledgement with task id.
@@ -52,6 +53,19 @@ export class Trinity {
52
53
  static getInstance(config) {
53
54
  if (!Trinity.instance) {
54
55
  Trinity.instance = new Trinity(config);
56
+ SubagentRegistry.register({
57
+ agentKey: 'trinit', auditAgent: 'trinity', label: 'Trinity',
58
+ delegateToolName: 'trinity_delegate', emoji: '👩‍💻', color: 'teal',
59
+ description: 'Database specialist',
60
+ colorClass: 'text-teal-600 dark:text-teal-400',
61
+ bgClass: 'bg-teal-50 dark:bg-teal-900/10',
62
+ badgeClass: 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300',
63
+ instance: Trinity.instance,
64
+ hasDynamicDescription: true,
65
+ isMultiInstance: false,
66
+ setSessionId: (id) => Trinity.setSessionId(id),
67
+ refreshCatalog: () => Trinity.refreshDelegateCatalog(),
68
+ });
55
69
  }
56
70
  return Trinity.instance;
57
71
  }
@@ -190,7 +204,7 @@ export class Trinity {
190
204
  const tools = this.buildTrinityTools();
191
205
  this.display.log(`Trinity initialized with ${tools.length} tools (personality: ${personality}).`, { source: 'Trinity' });
192
206
  try {
193
- this.agent = await ProviderFactory.createBare(trinityConfig, tools);
207
+ this.agent = await ServiceContainer.get(SERVICE_KEYS.providerFactory).createBare(trinityConfig, tools);
194
208
  }
195
209
  catch (err) {
196
210
  throw new ProviderError(trinityConfig.provider, err, 'Trinity subagent initialization failed');
@@ -1,6 +1,6 @@
1
1
  import { AIMessage, ToolMessage } from "@langchain/core/messages";
2
- import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
3
- import { AuditRepository } from "./audit/repository.js";
2
+ import { SQLiteChatMessageHistory } from "../memory/sqlite.js";
3
+ import { AuditRepository } from "../audit/repository.js";
4
4
  /** Extract token usage from a LangChain message using 4-fallback chain. */
5
5
  export function extractRawUsage(lastMessage) {
6
6
  return lastMessage.usage_metadata
@@ -1,13 +1,13 @@
1
1
  import Database from 'better-sqlite3';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'path';
4
- import { homedir } from 'os';
5
4
  import { randomUUID } from 'crypto';
5
+ import { PATHS } from '../../config/paths.js';
6
6
  export class TaskRepository {
7
7
  static instance = null;
8
8
  db;
9
9
  constructor() {
10
- const dbPath = path.join(homedir(), '.morpheus', 'memory', 'short-memory.db');
10
+ const dbPath = PATHS.shortMemoryDb;
11
11
  fs.ensureDirSync(path.dirname(dbPath));
12
12
  this.db = new Database(dbPath, { timeout: 5000 });
13
13
  this.db.pragma('journal_mode = WAL');
@@ -1,10 +1,6 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import { DisplayManager } from '../display.js';
3
- import { Apoc } from '../apoc.js';
4
- import { Neo } from '../neo.js';
5
- import { Trinity } from '../trinity.js';
6
- import { Link } from '../link.js';
7
- import { SmithDelegator } from '../smiths/delegator.js';
3
+ import { SubagentRegistry } from '../subagents/registry.js';
8
4
  import { TaskRepository } from './repository.js';
9
5
  import { AuditRepository } from '../audit/repository.js';
10
6
  export class TaskWorker {
@@ -52,77 +48,17 @@ export class TaskWorker {
52
48
  }
53
49
  async executeTask(task) {
54
50
  const audit = AuditRepository.getInstance();
51
+ const auditAgent = SubagentRegistry.resolveAuditAgent(task.agent);
55
52
  audit.insert({
56
53
  session_id: task.session_id,
57
54
  task_id: task.id,
58
55
  event_type: 'task_created',
59
- agent: task.agent === 'trinit' ? 'trinity' : task.agent,
56
+ agent: auditAgent,
60
57
  status: 'success',
61
58
  metadata: { agent: task.agent, input_preview: task.input.slice(0, 200) },
62
59
  });
63
60
  try {
64
- let result;
65
- switch (task.agent) {
66
- case 'apoc': {
67
- const apoc = Apoc.getInstance();
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
- });
74
- break;
75
- }
76
- case 'neo': {
77
- const neo = Neo.getInstance();
78
- result = await neo.execute(task.input, task.context ?? undefined, task.session_id, {
79
- origin_channel: task.origin_channel,
80
- session_id: task.session_id,
81
- origin_message_id: task.origin_message_id ?? undefined,
82
- origin_user_id: task.origin_user_id ?? undefined,
83
- });
84
- break;
85
- }
86
- case 'trinit': {
87
- const trinity = Trinity.getInstance();
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
- });
94
- break;
95
- }
96
- case 'smith': {
97
- // Parse smith name from context JSON
98
- let smithName = 'unknown';
99
- if (task.context) {
100
- try {
101
- const parsed = JSON.parse(task.context);
102
- smithName = parsed.smith_name || parsed.smith || 'unknown';
103
- }
104
- catch {
105
- smithName = task.context;
106
- }
107
- }
108
- const delegator = SmithDelegator.getInstance();
109
- result = await delegator.delegate(smithName, task.input, task.context ?? undefined);
110
- break;
111
- }
112
- case 'link': {
113
- const link = Link.getInstance();
114
- result = await link.execute(task.input, task.context ?? undefined, task.session_id, {
115
- origin_channel: task.origin_channel,
116
- session_id: task.session_id,
117
- origin_message_id: task.origin_message_id ?? undefined,
118
- origin_user_id: task.origin_user_id ?? undefined,
119
- });
120
- break;
121
- }
122
- default: {
123
- throw new Error(`Unknown task agent: ${task.agent}`);
124
- }
125
- }
61
+ const result = await SubagentRegistry.executeTask(task);
126
62
  this.repository.markCompleted(task.id, result.output, result.usage ? {
127
63
  provider: result.usage.provider,
128
64
  model: result.usage.model,
@@ -131,7 +67,7 @@ export class TaskWorker {
131
67
  durationMs: result.usage.durationMs,
132
68
  stepCount: result.usage.stepCount,
133
69
  } : undefined);
134
- const agentName = (task.agent === 'trinit' ? 'trinity' : task.agent);
70
+ const agentName = auditAgent;
135
71
  // Emit task_completed audit event
136
72
  audit.insert({
137
73
  session_id: task.session_id,
@@ -175,7 +111,7 @@ export class TaskWorker {
175
111
  session_id: task.session_id,
176
112
  task_id: task.id,
177
113
  event_type: 'task_completed',
178
- agent: (task.agent === 'trinit' ? 'trinity' : task.agent),
114
+ agent: auditAgent,
179
115
  status: 'error',
180
116
  metadata: { error: errorMessage },
181
117
  });
@@ -2,6 +2,9 @@ import { GoogleGenAI } from '@google/genai';
2
2
  import OpenAI from 'openai';
3
3
  import { OpenRouter } from '@openrouter/sdk';
4
4
  import fs from 'fs';
5
+ import fsExtra from 'fs-extra';
6
+ import path from 'path';
7
+ import os from 'os';
5
8
  import { parseFile } from 'music-metadata';
6
9
  /**
7
10
  * Returns the actual audio duration in seconds by parsing the file header.
@@ -25,6 +28,7 @@ async function getAudioDurationSeconds(filePath) {
25
28
  return 0;
26
29
  }
27
30
  }
31
+ export const TTS_MAX_CHARS = 4096;
28
32
  class GeminiTelephonist {
29
33
  model;
30
34
  constructor(model) {
@@ -187,6 +191,162 @@ export function createTelephonist(config) {
187
191
  throw new Error(`Unsupported audio provider: '${config.provider}'. Supported: google, openai, openrouter, ollama.`);
188
192
  }
189
193
  }
194
+ // ─── TTS Implementations ─────────────────────────────────────────────────────
195
+ function truncateForTts(text) {
196
+ if (text.length <= TTS_MAX_CHARS)
197
+ return text;
198
+ console.warn(`[Telephonist] TTS input truncated from ${text.length} to ${TTS_MAX_CHARS} chars.`);
199
+ return text.slice(0, TTS_MAX_CHARS);
200
+ }
201
+ function mimeTypeToExt(mimeType) {
202
+ if (mimeType.includes('ogg'))
203
+ return '.ogg';
204
+ if (mimeType.includes('mp3') || mimeType.includes('mpeg'))
205
+ return '.mp3';
206
+ if (mimeType.includes('wav'))
207
+ return '.wav';
208
+ if (mimeType.includes('aac'))
209
+ return '.aac';
210
+ return '.audio';
211
+ }
212
+ async function writeTempAudio(buffer, ext) {
213
+ const filePath = path.join(os.tmpdir(), `morpheus-tts-${Date.now()}${ext}`);
214
+ await fsExtra.writeFile(filePath, buffer);
215
+ return filePath;
216
+ }
217
+ /**
218
+ * Wraps raw PCM data in a WAV container header.
219
+ * Gemini TTS returns audio/pcm at 24000Hz, 16-bit, mono.
220
+ */
221
+ function pcmToWav(pcmBuffer, sampleRate = 24000, channels = 1, bitDepth = 16) {
222
+ const header = Buffer.alloc(44);
223
+ const dataSize = pcmBuffer.length;
224
+ const byteRate = sampleRate * channels * (bitDepth / 8);
225
+ const blockAlign = channels * (bitDepth / 8);
226
+ header.write('RIFF', 0);
227
+ header.writeUInt32LE(36 + dataSize, 4);
228
+ header.write('WAVE', 8);
229
+ header.write('fmt ', 12);
230
+ header.writeUInt32LE(16, 16); // PCM chunk size
231
+ header.writeUInt16LE(1, 20); // PCM format
232
+ header.writeUInt16LE(channels, 22);
233
+ header.writeUInt32LE(sampleRate, 24);
234
+ header.writeUInt32LE(byteRate, 28);
235
+ header.writeUInt16LE(blockAlign, 32);
236
+ header.writeUInt16LE(bitDepth, 34);
237
+ header.write('data', 36);
238
+ header.writeUInt32LE(dataSize, 40);
239
+ return Buffer.concat([header, pcmBuffer]);
240
+ }
241
+ class OpenAITtsTelephonist {
242
+ model;
243
+ defaultVoice;
244
+ constructor(model, defaultVoice) {
245
+ this.model = model;
246
+ this.defaultVoice = defaultVoice;
247
+ }
248
+ async transcribe() {
249
+ throw new Error('OpenAITtsTelephonist does not support transcription.');
250
+ }
251
+ async synthesize(text, apiKey, voice, stylePrompt) {
252
+ const client = new OpenAI({ apiKey });
253
+ const raw = stylePrompt ? `${stylePrompt}: ${text}` : text;
254
+ const input = truncateForTts(raw);
255
+ const response = await client.audio.speech.create({
256
+ model: this.model,
257
+ voice: (voice || this.defaultVoice),
258
+ input,
259
+ response_format: 'mp3',
260
+ });
261
+ const buffer = Buffer.from(await response.arrayBuffer());
262
+ const filePath = await writeTempAudio(buffer, '.mp3');
263
+ return {
264
+ filePath,
265
+ mimeType: 'audio/mpeg',
266
+ usage: {
267
+ input_tokens: 0,
268
+ output_tokens: 0,
269
+ total_tokens: 0,
270
+ audio_duration_seconds: 0,
271
+ },
272
+ };
273
+ }
274
+ }
275
+ class GeminiTtsTelephonist {
276
+ model;
277
+ defaultVoice;
278
+ constructor(model, defaultVoice) {
279
+ this.model = model;
280
+ this.defaultVoice = defaultVoice;
281
+ }
282
+ async transcribe() {
283
+ throw new Error('GeminiTtsTelephonist does not support transcription.');
284
+ }
285
+ async synthesize(text, apiKey, voice, stylePrompt) {
286
+ const ai = new GoogleGenAI({ apiKey });
287
+ const raw = stylePrompt ? `${stylePrompt}: ${text}` : text;
288
+ const input = truncateForTts(raw);
289
+ const response = await ai.models.generateContent({
290
+ model: this.model,
291
+ contents: [{ role: 'user', parts: [{ text: input }] }],
292
+ config: {
293
+ responseModalities: ['AUDIO'],
294
+ speechConfig: {
295
+ voiceConfig: {
296
+ prebuiltVoiceConfig: { voiceName: voice || this.defaultVoice },
297
+ },
298
+ },
299
+ },
300
+ });
301
+ const audioPart = response.candidates?.[0]?.content?.parts?.find((p) => p.inlineData?.mimeType?.startsWith('audio/'));
302
+ if (!audioPart?.inlineData?.data) {
303
+ throw new Error('Gemini TTS: no audio data in response');
304
+ }
305
+ const rawMimeType = audioPart.inlineData.mimeType ?? 'audio/pcm';
306
+ const rawBuffer = Buffer.from(audioPart.inlineData.data, 'base64');
307
+ let mimeType = rawMimeType;
308
+ let buffer;
309
+ // Gemini returns raw PCM — wrap it in a WAV container
310
+ if (rawMimeType.includes('pcm') || rawMimeType.includes('l16')) {
311
+ // Parse sample rate from mimeType params e.g. "audio/pcm;rate=24000"
312
+ const rateMatch = rawMimeType.match(/rate=(\d+)/i);
313
+ const sampleRate = rateMatch ? parseInt(rateMatch[1], 10) : 24000;
314
+ buffer = pcmToWav(rawBuffer, sampleRate);
315
+ mimeType = 'audio/wav';
316
+ }
317
+ else {
318
+ buffer = rawBuffer;
319
+ }
320
+ const ext = mimeTypeToExt(mimeType);
321
+ const filePath = await writeTempAudio(buffer, ext);
322
+ const usage = response.usageMetadata;
323
+ return {
324
+ filePath,
325
+ mimeType,
326
+ usage: {
327
+ input_tokens: usage?.promptTokenCount ?? 0,
328
+ output_tokens: usage?.candidatesTokenCount ?? 0,
329
+ total_tokens: usage?.totalTokenCount ?? 0,
330
+ audio_duration_seconds: 0,
331
+ },
332
+ };
333
+ }
334
+ }
335
+ /**
336
+ * Factory that creates an ITelephonist with TTS (synthesize) support.
337
+ * Supports providers: openai, google.
338
+ */
339
+ export function createTtsTelephonist(config) {
340
+ switch (config.provider) {
341
+ case 'openai':
342
+ return new OpenAITtsTelephonist(config.model, config.voice);
343
+ case 'google':
344
+ return new GeminiTtsTelephonist(config.model, config.voice);
345
+ default:
346
+ throw new Error(`Unsupported TTS provider: '${config.provider}'. Supported: openai, google.`);
347
+ }
348
+ }
349
+ // ─── Legacy export for backward compatibility ─────────────────────────────────
190
350
  // Legacy export for backward compatibility
191
351
  export class Telephonist {
192
352
  delegate;
@@ -40,6 +40,7 @@ export const ChronosScheduleTool = tool(async ({ prompt, schedule_type, schedule
40
40
  cron_normalized: parsed.cron_normalized,
41
41
  created_by: 'oracle',
42
42
  notify_channels: channels,
43
+ origin_session_id: TaskRequestContext.get()?.session_id,
43
44
  });
44
45
  return JSON.stringify({
45
46
  success: true,
@@ -1,11 +1,9 @@
1
1
  import { tool } from "@langchain/core/tools";
2
2
  import { z } from "zod";
3
- import { TaskRepository } from "../tasks/repository.js";
4
3
  import { TaskRequestContext } from "../tasks/context.js";
5
4
  import { compositeDelegationError, isLikelyCompositeDelegationTask } from "./delegation-guard.js";
6
5
  import { DisplayManager } from "../display.js";
7
- import { ChannelRegistry } from "../../channels/registry.js";
8
- import { AuditRepository } from "../audit/repository.js";
6
+ import { ServiceContainer, SERVICE_KEYS } from "../container.js";
9
7
  /**
10
8
  * Factory that builds a delegation StructuredTool for Apoc/Neo/Trinity.
11
9
  * Handles: composite guard, sync branch (notify→execute→audit→increment),
@@ -33,7 +31,8 @@ export function buildDelegationTool(opts) {
33
31
  const ctx = TaskRequestContext.get();
34
32
  const sessionId = ctx?.session_id ?? "default";
35
33
  if (ctx?.origin_channel && ctx.origin_user_id && ctx.origin_channel !== 'api' && ctx.origin_channel !== 'ui') {
36
- ChannelRegistry.sendToUser(ctx.origin_channel, ctx.origin_user_id, notifyText)
34
+ ServiceContainer.get(SERVICE_KEYS.notifier)
35
+ .sendToUser(ctx.origin_channel, ctx.origin_user_id, notifyText)
37
36
  .catch(() => { });
38
37
  }
39
38
  try {
@@ -41,7 +40,7 @@ export function buildDelegationTool(opts) {
41
40
  TaskRequestContext.incrementSyncDelegation();
42
41
  display.log(`${agentLabel} sync execution completed.`, { source, level: "info" });
43
42
  if (result.usage) {
44
- AuditRepository.getInstance().insert({
43
+ ServiceContainer.get(SERVICE_KEYS.auditEmitter).emit({
45
44
  session_id: sessionId,
46
45
  event_type: 'llm_call',
47
46
  agent: auditAgent,
@@ -76,8 +75,7 @@ export function buildDelegationTool(opts) {
76
75
  return "Delegation limit reached for this user turn. Split the request or wait for current tasks.";
77
76
  }
78
77
  const ctx = TaskRequestContext.get();
79
- const repository = TaskRepository.getInstance();
80
- const created = repository.createTask({
78
+ const created = ServiceContainer.get(SERVICE_KEYS.taskEnqueuer).enqueue({
81
79
  agent: agentKey,
82
80
  input: task,
83
81
  context: context ?? null,
@@ -2,14 +2,13 @@ import { tool } from "@langchain/core/tools";
2
2
  import { z } from "zod";
3
3
  import { ConfigManager } from "../../config/manager.js";
4
4
  import { promises as fsPromises } from "fs";
5
- import path from "path";
6
- import { homedir } from "os";
7
5
  import Database from "better-sqlite3";
6
+ import { PATHS } from "../../config/paths.js";
8
7
  import { TaskRepository } from "../tasks/repository.js";
9
8
  import { TaskRequestContext } from "../tasks/context.js";
10
9
  import { isEnvVarSet } from "../../config/precedence.js";
11
10
  // ─── Shared ───────────────────────────────────────────────────────────────────
12
- const shortMemoryDbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
11
+ const shortMemoryDbPath = PATHS.shortMemoryDb;
13
12
  /**
14
13
  * Map of config paths to their corresponding environment variable names.
15
14
  * Used to check if a config field is being overridden by an env var.
@@ -145,7 +144,7 @@ export const DiagnosticTool = tool(async () => {
145
144
  try {
146
145
  const timestamp = new Date().toISOString();
147
146
  const components = {};
148
- const morpheusRoot = path.join(homedir(), ".morpheus");
147
+ const morpheusRoot = PATHS.root;
149
148
  // Configuration
150
149
  try {
151
150
  const configManager = ConfigManager.getInstance();
@@ -198,7 +197,7 @@ export const DiagnosticTool = tool(async () => {
198
197
  }
199
198
  // Short-term memory DB
200
199
  try {
201
- const dbPath = path.join(morpheusRoot, "memory", "short-memory.db");
200
+ const dbPath = PATHS.shortMemoryDb;
202
201
  await fsPromises.access(dbPath);
203
202
  const stat = await fsPromises.stat(dbPath);
204
203
  components.shortMemoryDb = {
@@ -216,7 +215,7 @@ export const DiagnosticTool = tool(async () => {
216
215
  }
217
216
  // Sati long-term memory DB
218
217
  try {
219
- const satiDbPath = path.join(morpheusRoot, "memory", "sati-memory.db");
218
+ const satiDbPath = PATHS.satiDb;
220
219
  await fsPromises.access(satiDbPath);
221
220
  const stat = await fsPromises.stat(satiDbPath);
222
221
  components.satiMemoryDb = {
@@ -266,7 +265,7 @@ export const DiagnosticTool = tool(async () => {
266
265
  };
267
266
  // Logs directory
268
267
  try {
269
- const logsDir = path.join(morpheusRoot, "logs");
268
+ const logsDir = PATHS.logs;
270
269
  await fsPromises.access(logsDir);
271
270
  components.logs = {
272
271
  status: "healthy",
@@ -762,7 +761,7 @@ export const TrinityDbManageTool = tool(async ({ action, name, id, type, host, p
762
761
  const db = registry.getDatabase(dbId);
763
762
  if (!db)
764
763
  return JSON.stringify({ error: `Database "${name}" not found` });
765
- const { testConnection } = await import("../trinity-connector.js");
764
+ const { testConnection } = await import("../subagents/trinity/connector.js");
766
765
  const ok = await testConnection(db);
767
766
  return JSON.stringify({ status: ok ? "connected" : "failed", database: db.name });
768
767
  }
@@ -771,10 +770,10 @@ export const TrinityDbManageTool = tool(async ({ action, name, id, type, host, p
771
770
  const db = registry.getDatabase(dbId);
772
771
  if (!db)
773
772
  return JSON.stringify({ error: `Database "${name}" not found` });
774
- const { introspectSchema } = await import("../trinity-connector.js");
773
+ const { introspectSchema } = await import("../subagents/trinity/connector.js");
775
774
  const schema = await introspectSchema(db);
776
775
  registry.updateSchema(dbId, JSON.stringify(schema));
777
- const { Trinity } = await import("../trinity.js");
776
+ const { Trinity } = await import("../subagents/trinity/trinity.js");
778
777
  await Trinity.refreshDelegateCatalog();
779
778
  return JSON.stringify({ success: true, message: `Schema refreshed for "${db.name}"` });
780
779
  }