morpheus-cli 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +63 -43
  2. package/dist/channels/discord.js +71 -21
  3. package/dist/channels/telegram.js +73 -19
  4. package/dist/cli/commands/restart.js +15 -0
  5. package/dist/cli/commands/start.js +18 -0
  6. package/dist/config/manager.js +61 -0
  7. package/dist/config/paths.js +1 -0
  8. package/dist/config/schemas.js +11 -3
  9. package/dist/http/api.js +3 -0
  10. package/dist/http/routers/link.js +239 -0
  11. package/dist/http/routers/skills.js +1 -8
  12. package/dist/runtime/apoc.js +1 -1
  13. package/dist/runtime/audit/repository.js +1 -1
  14. package/dist/runtime/link-chunker.js +214 -0
  15. package/dist/runtime/link-repository.js +301 -0
  16. package/dist/runtime/link-search.js +298 -0
  17. package/dist/runtime/link-worker.js +284 -0
  18. package/dist/runtime/link.js +295 -0
  19. package/dist/runtime/memory/sati/service.js +1 -1
  20. package/dist/runtime/memory/sqlite.js +52 -0
  21. package/dist/runtime/neo.js +1 -1
  22. package/dist/runtime/oracle.js +81 -44
  23. package/dist/runtime/scaffold.js +4 -17
  24. package/dist/runtime/skills/__tests__/loader.test.js +7 -10
  25. package/dist/runtime/skills/__tests__/registry.test.js +2 -18
  26. package/dist/runtime/skills/__tests__/tool.test.js +55 -224
  27. package/dist/runtime/skills/index.js +1 -2
  28. package/dist/runtime/skills/loader.js +0 -2
  29. package/dist/runtime/skills/registry.js +8 -20
  30. package/dist/runtime/skills/schema.js +0 -4
  31. package/dist/runtime/skills/tool.js +42 -209
  32. package/dist/runtime/smiths/delegator.js +1 -1
  33. package/dist/runtime/smiths/registry.js +1 -1
  34. package/dist/runtime/tasks/worker.js +12 -44
  35. package/dist/runtime/trinity.js +1 -1
  36. package/dist/types/config.js +14 -0
  37. package/dist/ui/assets/AuditDashboard-93LCGHG1.js +1 -0
  38. package/dist/ui/assets/{Chat-BNtutgja.js → Chat-CK5sNcQ1.js} +8 -8
  39. package/dist/ui/assets/{Chronos-3C8RPZcl.js → Chronos-m2h--GEe.js} +1 -1
  40. package/dist/ui/assets/{ConfirmationModal-ZQPBeJ2Z.js → ConfirmationModal-Dd5pUJme.js} +1 -1
  41. package/dist/ui/assets/{Dashboard-CqkHzr2F.js → Dashboard-ODwl7d-a.js} +1 -1
  42. package/dist/ui/assets/{DeleteConfirmationModal-CioxFWn_.js → DeleteConfirmationModal-CCcojDmr.js} +1 -1
  43. package/dist/ui/assets/Documents-dWnSoxFO.js +7 -0
  44. package/dist/ui/assets/{Logs-DBVanS0O.js → Logs-Dc9Z2LBj.js} +1 -1
  45. package/dist/ui/assets/{MCPManager-vXfL3P2U.js → MCPManager-CMkb8vMn.js} +1 -1
  46. package/dist/ui/assets/{ModelPricing-DyfdunLT.js → ModelPricing-DtHPPbEQ.js} +1 -1
  47. package/dist/ui/assets/{Notifications-VL-vep6d.js → Notifications-BPvo-DWP.js} +1 -1
  48. package/dist/ui/assets/{Pagination-oTGieBLM.js → Pagination-BHZKk42X.js} +1 -1
  49. package/dist/ui/assets/{SatiMemories-jaadkW0U.js → SatiMemories-BUPu1Lxr.js} +1 -1
  50. package/dist/ui/assets/SessionAudit-CFKF4DA8.js +9 -0
  51. package/dist/ui/assets/Settings-C4JrXfsR.js +47 -0
  52. package/dist/ui/assets/{Skills-DE3zziXL.js → Skills-BUlvJgJ4.js} +1 -1
  53. package/dist/ui/assets/{Smiths-pmogN1mU.js → Smiths-CDtJdY0I.js} +1 -1
  54. package/dist/ui/assets/{Tasks-Bs8s34Jc.js → Tasks-DK_cOsNK.js} +1 -1
  55. package/dist/ui/assets/{TrinityDatabases-D7uihcdp.js → TrinityDatabases-X07by-19.js} +1 -1
  56. package/dist/ui/assets/{UsageStats-B9gePLZ0.js → UsageStats-dYcgckLq.js} +1 -1
  57. package/dist/ui/assets/{WebhookManager-B2L3rCLM.js → WebhookManager-DDw5eX2R.js} +1 -1
  58. package/dist/ui/assets/{audit-Cggeu9mM.js → audit-DZ5WLUEm.js} +1 -1
  59. package/dist/ui/assets/{chronos-D3-sWhfU.js → chronos-B_HI4mlq.js} +1 -1
  60. package/dist/ui/assets/{config-CBqRUPgn.js → config-B-YxlVrc.js} +1 -1
  61. package/dist/ui/assets/index-DVjwJ8jT.css +1 -0
  62. package/dist/ui/assets/{index-zKplfrXZ.js → index-DfJwcKqG.js} +5 -5
  63. package/dist/ui/assets/{mcp-uL1R9hyA.js → mcp-k-_pwbqA.js} +1 -1
  64. package/dist/ui/assets/{skills-jmw8yTJs.js → skills-xMXangks.js} +1 -1
  65. package/dist/ui/assets/{stats-HOms6GnM.js → stats-C4QZIv5O.js} +1 -1
  66. package/dist/ui/assets/{vendor-icons-DMd9RGvJ.js → vendor-icons-NHF9HNeN.js} +1 -1
  67. package/dist/ui/index.html +3 -3
  68. package/dist/ui/sw.js +1 -1
  69. package/package.json +3 -1
  70. package/dist/runtime/__tests__/keymaker.test.js +0 -148
  71. package/dist/runtime/keymaker.js +0 -157
  72. package/dist/ui/assets/AuditDashboard-DliJ1CX0.js +0 -1
  73. package/dist/ui/assets/SessionAudit-BsXrWlwz.js +0 -9
  74. package/dist/ui/assets/Settings-B4eezRcg.js +0 -47
  75. package/dist/ui/assets/index-D4fzIKy1.css +0 -1
@@ -0,0 +1,295 @@
1
+ import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
+ import { z } from "zod";
3
+ import { DynamicStructuredTool } from "@langchain/core/tools";
4
+ import { ConfigManager } from '../config/manager.js';
5
+ import { LinkRepository } from './link-repository.js';
6
+ import { LinkSearch } from './link-search.js';
7
+ import { ProviderFactory } from './providers/factory.js';
8
+ import { ProviderError } from './errors.js';
9
+ import { DisplayManager } from './display.js';
10
+ import { TaskRequestContext } from './tasks/context.js';
11
+ import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from './subagent-utils.js';
12
+ import { buildDelegationTool } from './tools/delegation-utils.js';
13
+ const LINK_BASE_DESCRIPTION = `Delegate to Link, the documentation specialist subagent.
14
+
15
+ Link has access to indexed user documents (PDFs, Markdown, TXT, DOCX) stored in ~/.morpheus/docs.
16
+ It uses an LLM to search, reason over, and synthesize answers from document content.
17
+
18
+ Use this tool when the user asks about information that might be in their uploaded documents.
19
+ Input should be a natural language query or question about the user's documentation.`;
20
+ function buildDocumentCatalogSection(repository) {
21
+ try {
22
+ const docs = repository.listDocuments('indexed');
23
+ if (docs.length === 0) {
24
+ return '\n\nIndexed documents: none currently indexed.';
25
+ }
26
+ const lines = docs.map((d) => `- ${d.filename} (${d.chunk_count} chunks)`);
27
+ return `\n\nIndexed documents:\n${lines.join('\n')}`;
28
+ }
29
+ catch {
30
+ return '\n\nIndexed documents: unable to retrieve list.';
31
+ }
32
+ }
33
+ /**
34
+ * Link - Documentation Specialist Subagent
35
+ *
36
+ * Provides RAG (Retrieval-Augmented Generation) capabilities over user documents.
37
+ * Uses a ReactAgent with an LLM to reason over search results and synthesize answers.
38
+ */
39
+ export class Link {
40
+ static instance = null;
41
+ static currentSessionId = undefined;
42
+ static _delegateTool = null;
43
+ config;
44
+ agentConfig;
45
+ repository;
46
+ search;
47
+ agent;
48
+ display = DisplayManager.getInstance();
49
+ constructor(config) {
50
+ this.config = config;
51
+ this.agentConfig = ConfigManager.getInstance().getLinkConfig();
52
+ this.repository = LinkRepository.getInstance();
53
+ this.search = LinkSearch.getInstance();
54
+ }
55
+ static getInstance(config) {
56
+ if (!Link.instance) {
57
+ if (!config) {
58
+ config = ConfigManager.getInstance().get();
59
+ }
60
+ Link.instance = new Link(config);
61
+ }
62
+ return Link.instance;
63
+ }
64
+ static resetInstance() {
65
+ Link.instance = null;
66
+ Link._delegateTool = null;
67
+ }
68
+ static setSessionId(id) {
69
+ Link.currentSessionId = id;
70
+ }
71
+ /**
72
+ * Build the internal search tool that the ReactAgent will use.
73
+ */
74
+ buildTools() {
75
+ const search = this.search;
76
+ const repository = this.repository;
77
+ const agentConfig = this.agentConfig;
78
+ const searchTool = new DynamicStructuredTool({
79
+ name: 'link_search_documents',
80
+ description: 'Search ALL indexed user documents using hybrid vector + keyword search. Returns the most relevant document chunks for a given query. Use this for broad searches when you don\'t know which document contains the answer.',
81
+ schema: z.object({
82
+ query: z.string().describe('The search query to find relevant document passages'),
83
+ limit: z.number().optional().describe('Maximum number of results to return (default: max_results from config)'),
84
+ }),
85
+ func: async ({ query, limit }) => {
86
+ const maxResults = limit ?? agentConfig.max_results;
87
+ const threshold = agentConfig.score_threshold;
88
+ const results = await search.search(query, maxResults, threshold);
89
+ if (results.length === 0) {
90
+ return `No relevant documents found for query: "${query}"`;
91
+ }
92
+ const formatted = results
93
+ .map((r, i) => `[${i + 1}] Source: ${r.filename} (chunk ${r.position}, score: ${r.score.toFixed(3)})\n${r.content}`)
94
+ .join('\n\n---\n\n');
95
+ return `Found ${results.length} relevant passages:\n\n${formatted}`;
96
+ },
97
+ });
98
+ const listDocumentsTool = new DynamicStructuredTool({
99
+ name: 'link_list_documents',
100
+ description: 'List indexed documents. Use this to find documents by filename before searching within a specific one. Supports optional name filter (case-insensitive partial match).',
101
+ schema: z.object({
102
+ name_filter: z.string().optional().describe('Optional partial filename to filter by (case-insensitive). E.g. "CV", "contrato", "readme"'),
103
+ }),
104
+ func: async ({ name_filter }) => {
105
+ const docs = repository.listDocuments('indexed');
106
+ if (docs.length === 0) {
107
+ return 'No indexed documents found.';
108
+ }
109
+ let filtered = docs;
110
+ if (name_filter) {
111
+ const lower = name_filter.toLowerCase();
112
+ filtered = docs.filter(d => d.filename.toLowerCase().includes(lower));
113
+ }
114
+ if (filtered.length === 0) {
115
+ const allNames = docs.map(d => `- ${d.filename}`).join('\n');
116
+ return `No documents matching "${name_filter}". Available documents:\n${allNames}`;
117
+ }
118
+ const lines = filtered.map(d => `- [${d.id}] ${d.filename} (${d.chunk_count} chunks)`);
119
+ return `Found ${filtered.length} document(s):\n${lines.join('\n')}`;
120
+ },
121
+ });
122
+ const searchInDocumentTool = new DynamicStructuredTool({
123
+ name: 'link_search_in_document',
124
+ description: 'Search within a SPECIFIC document by its ID. Use this when you know which document to search (e.g. after using link_list_documents to find it). More precise than link_search_documents for targeted queries.',
125
+ schema: z.object({
126
+ document_id: z.string().describe('The document ID to search within (get this from link_list_documents)'),
127
+ query: z.string().describe('The search query to find relevant passages within this document'),
128
+ limit: z.number().optional().describe('Maximum number of results (default: max_results from config)'),
129
+ }),
130
+ func: async ({ document_id, query, limit }) => {
131
+ const doc = repository.getDocument(document_id);
132
+ if (!doc) {
133
+ return `Document not found: ${document_id}`;
134
+ }
135
+ const maxResults = limit ?? agentConfig.max_results;
136
+ const threshold = agentConfig.score_threshold;
137
+ const results = await search.searchInDocument(query, document_id, maxResults, threshold);
138
+ if (results.length === 0) {
139
+ return `No relevant passages found in "${doc.filename}" for query: "${query}"`;
140
+ }
141
+ const formatted = results
142
+ .map((r, i) => `[${i + 1}] (chunk ${r.position}, score: ${r.score.toFixed(3)})\n${r.content}`)
143
+ .join('\n\n---\n\n');
144
+ return `Found ${results.length} passages in "${doc.filename}":\n\n${formatted}`;
145
+ },
146
+ });
147
+ return [listDocumentsTool, searchTool, searchInDocumentTool];
148
+ }
149
+ async initialize() {
150
+ this.repository.initialize();
151
+ await this.search.initialize();
152
+ const linkConfig = this.agentConfig;
153
+ const personality = linkConfig.personality || 'documentation_specialist';
154
+ const tools = this.buildTools();
155
+ // Update delegate tool description with current document catalog
156
+ if (Link._delegateTool) {
157
+ const full = `${LINK_BASE_DESCRIPTION}${buildDocumentCatalogSection(this.repository)}`;
158
+ Link._delegateTool.description = full;
159
+ }
160
+ this.display.log(`Link initialized with personality: ${personality}.`, { source: 'Link' });
161
+ try {
162
+ this.agent = await ProviderFactory.create(linkConfig, tools);
163
+ }
164
+ catch (err) {
165
+ throw new ProviderError(linkConfig.provider, err, 'Link subagent initialization failed');
166
+ }
167
+ }
168
+ /**
169
+ * Search documents for relevant information (used internally by search tool and HTTP API).
170
+ */
171
+ async searchDocuments(query, limit) {
172
+ const maxResults = limit ?? this.agentConfig.max_results;
173
+ const threshold = this.agentConfig.score_threshold;
174
+ const results = await this.search.search(query, maxResults, threshold);
175
+ return {
176
+ results: results.map(r => ({
177
+ chunk_id: r.chunk_id,
178
+ content: r.content,
179
+ document_id: r.document_id,
180
+ filename: r.filename,
181
+ position: r.position,
182
+ score: r.score,
183
+ })),
184
+ total: results.length,
185
+ };
186
+ }
187
+ /**
188
+ * Execute a query using the LLM-powered ReactAgent.
189
+ */
190
+ async execute(task, context, sessionId, taskContext) {
191
+ const linkConfig = this.agentConfig;
192
+ if (!this.agent) {
193
+ await this.initialize();
194
+ }
195
+ this.display.log(`Executing delegated task in Link: ${task.slice(0, 80)}...`, {
196
+ source: 'Link',
197
+ });
198
+ const personality = linkConfig.personality || 'documentation_specialist';
199
+ const systemMessage = new SystemMessage(`
200
+ You are Link, ${personality === 'documentation_specialist' ? 'a documentation specialist and knowledge synthesizer' : personality}, a subagent in Morpheus.
201
+
202
+ You have access to the user's indexed documents via the link_search_documents tool.
203
+
204
+ Rules:
205
+ 1. ALWAYS search the documents before answering. Never answer from general knowledge alone when documents may contain relevant information.
206
+ 2. Synthesize search results into a clear, natural response. Do not just dump raw chunks.
207
+ 3. Cite sources by filename when referencing specific information (e.g., "According to readme.md, ...").
208
+ 4. If no relevant documents are found, clearly state that no matching documentation was found.
209
+ 5. NEVER fabricate or invent document content. Only report what the search actually returns.
210
+ 6. If the query is ambiguous, search with multiple relevant terms to maximize coverage.
211
+ 7. Keep responses concise and focused on the user's question.
212
+ 8. Respond in the language requested by the user. If not explicit, use the dominant language of the task/context.
213
+
214
+ ## Tool Selection Strategy
215
+ - When the user refers to a SPECIFIC document (by name or partial name like "meu currículo", "CV", "contrato"):
216
+ 1. First call **link_list_documents** with a name filter to find the document ID.
217
+ 2. Then call **link_search_in_document** with that document ID for a targeted search.
218
+ - When the user asks a general question without referencing a specific document:
219
+ - Use **link_search_documents** for a broad search across all documents.
220
+ - When unsure which document contains the answer, start with **link_search_documents**, then narrow down with **link_search_in_document** if results point to a specific file.
221
+
222
+ ${context ? `Context:\n${context}` : ''}
223
+ `);
224
+ const userMessage = new HumanMessage(task);
225
+ const messages = [systemMessage, userMessage];
226
+ try {
227
+ const invokeContext = {
228
+ origin_channel: taskContext?.origin_channel ?? 'api',
229
+ session_id: taskContext?.session_id ?? sessionId ?? 'default',
230
+ origin_message_id: taskContext?.origin_message_id,
231
+ origin_user_id: taskContext?.origin_user_id,
232
+ };
233
+ const inputCount = messages.length;
234
+ const startMs = Date.now();
235
+ const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 25 }));
236
+ const durationMs = Date.now() - startMs;
237
+ const lastMessage = response.messages[response.messages.length - 1];
238
+ const content = typeof lastMessage.content === 'string'
239
+ ? lastMessage.content
240
+ : JSON.stringify(lastMessage.content);
241
+ const rawUsage = extractRawUsage(lastMessage);
242
+ const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
243
+ const targetSession = sessionId ?? Link.currentSessionId ?? 'link';
244
+ await persistAgentMessage('link', content, linkConfig, targetSession, rawUsage, durationMs);
245
+ emitToolAuditEvents(response.messages.slice(inputCount), targetSession, 'link');
246
+ this.display.log('Link task completed.', { source: 'Link' });
247
+ return buildAgentResult(content, linkConfig, rawUsage, durationMs, stepCount);
248
+ }
249
+ catch (err) {
250
+ throw new ProviderError(linkConfig.provider, err, 'Link task execution failed');
251
+ }
252
+ }
253
+ /**
254
+ * Create the delegation tool for Oracle.
255
+ */
256
+ createDelegateTool() {
257
+ if (!Link._delegateTool) {
258
+ Link._delegateTool = buildDelegationTool({
259
+ name: 'link_delegate',
260
+ description: LINK_BASE_DESCRIPTION,
261
+ agentKey: 'link',
262
+ agentLabel: 'Link',
263
+ auditAgent: 'link',
264
+ isSync: () => ConfigManager.getInstance().getLinkConfig().execution_mode === 'sync',
265
+ notifyText: '📚 Link is searching your documentation...',
266
+ executeSync: (task, context, sessionId, ctx) => Link.getInstance().execute(task, context, sessionId, {
267
+ origin_channel: ctx?.origin_channel ?? 'api',
268
+ session_id: sessionId,
269
+ origin_message_id: ctx?.origin_message_id,
270
+ origin_user_id: ctx?.origin_user_id,
271
+ }),
272
+ });
273
+ }
274
+ return Link._delegateTool;
275
+ }
276
+ /**
277
+ * Refresh the delegate tool description with current document catalog.
278
+ */
279
+ static async refreshDelegateCatalog() {
280
+ if (Link._delegateTool) {
281
+ try {
282
+ const repository = LinkRepository.getInstance();
283
+ const full = `${LINK_BASE_DESCRIPTION}${buildDocumentCatalogSection(repository)}`;
284
+ Link._delegateTool.description = full;
285
+ }
286
+ catch { /* non-critical */ }
287
+ }
288
+ }
289
+ async reload() {
290
+ this.config = ConfigManager.getInstance().get();
291
+ this.agentConfig = ConfigManager.getInstance().getLinkConfig();
292
+ this.agent = undefined;
293
+ await this.initialize();
294
+ }
295
+ }
@@ -97,7 +97,7 @@ export class SatiService {
97
97
  console.warn('[SatiService] Failed to persist input log:', e);
98
98
  }
99
99
  const satiStartMs = Date.now();
100
- const response = await agent.invoke({ messages }, { recursionLimit: 50 });
100
+ const response = await agent.invoke({ messages }, { recursionLimit: 10 });
101
101
  const satiDurationMs = Date.now() - satiStartMs;
102
102
  const lastMessage = response.messages[response.messages.length - 1];
103
103
  let content = lastMessage.content.toString();
@@ -130,6 +130,14 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
130
130
  embedding_status TEXT CHECK (embedding_status IN ('none', 'pending', 'embedded', 'failed')) NOT NULL DEFAULT 'none'
131
131
  );
132
132
 
133
+ CREATE TABLE IF NOT EXISTS user_channel_sessions (
134
+ channel TEXT NOT NULL,
135
+ user_id TEXT NOT NULL,
136
+ session_id TEXT NOT NULL,
137
+ updated_at INTEGER NOT NULL,
138
+ PRIMARY KEY (channel, user_id)
139
+ );
140
+
133
141
  CREATE TABLE IF NOT EXISTS model_pricing (
134
142
  provider TEXT NOT NULL,
135
143
  model TEXT NOT NULL,
@@ -957,6 +965,50 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
957
965
  `).all();
958
966
  return sessions;
959
967
  }
968
+ /**
969
+ * Gets the session ID for a specific channel+user combination.
970
+ * Returns null if not found.
971
+ */
972
+ async getUserChannelSession(channel, userId) {
973
+ const result = this.db.prepare(`
974
+ SELECT session_id FROM user_channel_sessions
975
+ WHERE channel = ? AND user_id = ?
976
+ `).get(channel, userId);
977
+ return result ? result.session_id : null;
978
+ }
979
+ /**
980
+ * Sets or updates the session ID for a specific channel+user.
981
+ * Uses INSERT ... ON CONFLICT for upsert behavior.
982
+ */
983
+ async setUserChannelSession(channel, userId, sessionId) {
984
+ this.db.prepare(`
985
+ INSERT INTO user_channel_sessions (channel, user_id, session_id, updated_at)
986
+ VALUES (?, ?, ?, ?)
987
+ ON CONFLICT(channel, user_id) DO UPDATE SET
988
+ session_id = excluded.session_id,
989
+ updated_at = excluded.updated_at
990
+ `).run(channel, userId, sessionId, Date.now());
991
+ }
992
+ /**
993
+ * Lists all user sessions for a channel.
994
+ * Returns an array of {userId, sessionId} objects.
995
+ */
996
+ async listUserChannelSessions(channel) {
997
+ const rows = this.db.prepare(`
998
+ SELECT user_id, session_id FROM user_channel_sessions
999
+ WHERE channel = ?
1000
+ `).all(channel);
1001
+ return rows.map(row => ({ userId: row.user_id, sessionId: row.session_id }));
1002
+ }
1003
+ /**
1004
+ * Removes the session mapping for a channel+user.
1005
+ */
1006
+ async deleteUserChannelSession(channel, userId) {
1007
+ this.db.prepare(`
1008
+ DELETE FROM user_channel_sessions
1009
+ WHERE channel = ? AND user_id = ?
1010
+ `).run(channel, userId);
1011
+ }
960
1012
  /**
961
1013
  * Closes the database connection.
962
1014
  * Should be called when the history object is no longer needed.
@@ -137,7 +137,7 @@ ${context ? `Context:\n${context}` : ""}
137
137
  };
138
138
  const inputCount = messages.length;
139
139
  const startMs = Date.now();
140
- const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 50 }));
140
+ const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 10 }));
141
141
  const durationMs = Date.now() - startMs;
142
142
  const lastMessage = response.messages[response.messages.length - 1];
143
143
  const content = typeof lastMessage.content === "string"
@@ -10,19 +10,21 @@ import { TaskRequestContext } from "./tasks/context.js";
10
10
  import { TaskRepository } from "./tasks/repository.js";
11
11
  import { Neo } from "./neo.js";
12
12
  import { Trinity } from "./trinity.js";
13
+ import { Link } from "./link.js";
13
14
  import { SmithDelegateTool } from "./tools/smith-tool.js";
14
15
  import { TaskQueryTool, chronosTools, timeVerifierTool } from "./tools/index.js";
15
16
  import { Construtor } from "./tools/factory.js";
16
17
  import { MCPManager } from "../config/mcp-manager.js";
17
- import { SkillRegistry, SkillExecuteTool, SkillDelegateTool, updateSkillToolDescriptions } from "./skills/index.js";
18
+ import { SkillRegistry, createLoadSkillTool } from "./skills/index.js";
18
19
  import { SmithRegistry } from "./smiths/registry.js";
19
20
  import { AuditRepository } from "./audit/repository.js";
20
21
  import { SetupRepository } from './setup/repository.js';
21
22
  import { buildSetupTool } from './tools/setup-tool.js';
22
23
  import { emitToolAuditEvents } from "./subagent-utils.js";
24
+ import { PATHS } from "../config/paths.js";
25
+ import { writeFileSync } from "fs";
23
26
  const ORACLE_DELEGATION_TOOLS = new Set([
24
- 'apoc_delegate', 'neo_delegate', 'trinity_delegate', 'smith_delegate',
25
- 'skill_delegate', 'skill_execute',
27
+ 'apoc_delegate', 'neo_delegate', 'trinity_delegate', 'smith_delegate', 'link_delegate',
26
28
  ]);
27
29
  export class Oracle {
28
30
  provider;
@@ -119,7 +121,7 @@ export class Oracle {
119
121
  const toolCalls = msg.tool_calls ?? [];
120
122
  if (!Array.isArray(toolCalls))
121
123
  continue;
122
- if (toolCalls.some((tc) => tc?.name === "apoc_delegate" || tc?.name === "neo_delegate" || tc?.name === "trinity_delegate" || tc?.name === "smith_delegate")) {
124
+ if (toolCalls.some((tc) => tc?.name === "apoc_delegate" || tc?.name === "neo_delegate" || tc?.name === "trinity_delegate" || tc?.name === "smith_delegate" || tc?.name === "link_delegate")) {
123
125
  return true;
124
126
  }
125
127
  }
@@ -154,7 +156,7 @@ export class Oracle {
154
156
  // Fail-open: Oracle can still initialize even if catalog refresh fails.
155
157
  await Neo.refreshDelegateCatalog().catch(() => { });
156
158
  await Trinity.refreshDelegateCatalog().catch(() => { });
157
- updateSkillToolDescriptions();
159
+ await Link.refreshDelegateCatalog().catch(() => { });
158
160
  // Build tool list — conditionally include SmithDelegateTool based on config
159
161
  // Initialize setup repository (creates table if needed)
160
162
  SetupRepository.getInstance();
@@ -164,8 +166,8 @@ export class Oracle {
164
166
  Neo.getInstance().createDelegateTool(),
165
167
  Apoc.getInstance().createDelegateTool(),
166
168
  Trinity.getInstance().createDelegateTool(),
167
- SkillExecuteTool,
168
- SkillDelegateTool,
169
+ Link.getInstance().createDelegateTool(),
170
+ createLoadSkillTool(),
169
171
  timeVerifierTool,
170
172
  ...chronosTools,
171
173
  ];
@@ -245,11 +247,14 @@ Do NOT proceed with other tasks until all required fields have been collected an
245
247
  `;
246
248
  }
247
249
  }
248
- const systemMessage = new SystemMessage(`${setupBlock}You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
250
+ const systemMessage = new SystemMessage(`
251
+ ${setupBlock}
252
+
253
+ You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
249
254
 
250
255
  You are an orchestrator and task router.
251
256
 
252
- ## Date & Time Resolution — MANDATORY
257
+ ## Date & Time Resolution — MANDATORY ##
253
258
 
254
259
  You **MUST** call "time_verifier" before answering or delegating ANY request that depends on the current date or time.
255
260
  This includes — but is not limited to — **two categories**:
@@ -269,7 +274,7 @@ This includes — but is not limited to — **two categories**:
269
274
  **NEVER assume or invent a date. NEVER guess "today is [date]".**
270
275
  Always call time_verifier first, then use the resolved date in your tool call or delegation prompt.
271
276
 
272
- Rules:
277
+ ## Rules: ##
273
278
  1. For conversation-only requests (greetings, conceptual explanation, memory follow-up, statements of fact, sharing personal information), answer directly. DO NOT create tasks or delegate for simple statements like "I have two cats" or "My name is John". Sati will automatically memorize facts in the background ( **ALWAYS** use SATI Memories to review or retrieve these facts if needed).
274
279
  **NEVER** Create data, use SATI memories to response on informal conversation or say that dont know abaout the awsor if the answer is in the memories. Always use the memories as source of truth for user facts, preferences, stable context and informal conversation. Use tools only for execution, verification or when external/system state is required.*
275
280
  2. For requests that require execution, verification, external/system state, or non-trivial operations, evaluate the available tools and choose the best one.
@@ -282,9 +287,42 @@ Rules:
282
287
  9. Avoid duplicate delegations to the same tool or agent.
283
288
  10. After enqueuing all required delegated tasks for the current message, stop calling tools and return a concise acknowledgement.
284
289
  11. If a delegation is rejected as "not atomic", immediately split into smaller delegations and retry.
285
- 12. When the user message contains @neo, @apoc, or @trinity (case-insensitive), delegate to that specific agent. The mention is an explicit routing directive — respect it even if another agent might also handle the request.
290
+ 12. When the user message contains @link, @neo, @apoc, or @trinity (case-insensitive), delegate to that specific agent. The mention is an explicit routing directive — respect it even if another agent might also handle the request.
291
+ 13. Smiths also have names and could be called by @smithname — respect this as an explicit routing directive as well.
292
+
293
+ ## Delegation quality ##
294
+ - Write delegation input in the same language requested by the user.
295
+ - Include clear objective and constraints.
296
+ - Include OS-aware guidance for network checks when relevant.
297
+ - Use Sati memories only as context to complement the task, never as source of truth for dynamic data.
298
+ - Use Sati memories to fill missing stable context fields (for example: city, timezone, language, currency, preferred units).
299
+ - If Sati memory is conflicting or uncertain for a required field, ask one short clarification before delegating.
300
+ - When completing missing fields from Sati, include explicit assumptions in delegation context using the format: "Assumption from Sati: key=value".
301
+ - Never infer sensitive data from Sati memories (credentials, legal identifiers, health details, financial account data).
302
+ - When assumptions were used, mention them briefly in the user-facing response and allow correction.
303
+ - break the request into multiple delegations if it contains multiple independent actions.
304
+ - Set a single task per delegation tool call. Do not combine multiple actions into one delegation, as it complicates execution and error handling.
305
+ - If user requested N independent actions, produce N delegated tasks (or direct answers), each one singular and tool-scoped.
306
+ - If use a delegation dont use the sati or messages history to answer directly in the same response. Just response with the delegations.
307
+ Example 1:
308
+ ask: "Tell me my account balance and do a ping on google.com"
309
+ good:
310
+ - delegate to "neo_delegate" with task "Check account balance using morpheus analytics MCP and return the result."
311
+ - delegate to "apoc_delegate" with task "Ping google.com using the network diagnostics MCP and return reachability status. Use '-n' flag for Windows and '-c' for Linux/macOS."
312
+ bad:
313
+ - delegate to "neo_delegate" with task "Check account balance using morpheus analytics MCP and ping google.com using the network diagnostics MCP, then return both results." (combines two independent actions into one delegation, which is not atomic and complicates execution and error handling)
314
+
315
+ Example 2:
316
+ ask: "I have two cats" or "My name is John"
317
+ good:
318
+ - Answer directly acknowledging the fact. Do NOT delegate.
319
+ bad:
320
+ - delegate to "neo_delegate" or "apoc_delegate" to save the fact. (Sati handles this automatically in the background)
286
321
 
287
- ## Chronos Channel Routing
322
+ --------------------------------------------------
323
+ CHRONOS SCHEDULING RULES
324
+ --------------------------------------------------
325
+ ## Chronos Channel Routing ##
288
326
  When calling chronos_schedule, set notify_channels based on the user's message:
289
327
  - User mentions a specific channel (e.g., "no Discord", "no Telegram", "on Discord", "me avise pelo Discord"): set notify_channels to that channel — e.g. ["discord"] or ["telegram"].
290
328
  - User says "all channels", "todos os canais", "em todos os canais": set notify_channels to [] (empty = broadcast to all active channels).
@@ -339,38 +377,33 @@ Behavior rules for Chronos execution context:
339
377
  - **Action / task prompts** (e.g., "executar npm build", "verificar se o servidor está online", "enviar relatório"): execute normally using the appropriate tools.
340
378
  - NEVER re-schedule or create new Chronos jobs from within a Chronos execution.
341
379
 
342
- Delegation quality:
343
- - Write delegation input in the same language requested by the user.
344
- - Include clear objective and constraints.
345
- - Include OS-aware guidance for network checks when relevant.
346
- - Use Sati memories only as context to complement the task, never as source of truth for dynamic data.
347
- - Use Sati memories to fill missing stable context fields (for example: city, timezone, language, currency, preferred units).
348
- - If Sati memory is conflicting or uncertain for a required field, ask one short clarification before delegating.
349
- - When completing missing fields from Sati, include explicit assumptions in delegation context using the format: "Assumption from Sati: key=value".
350
- - Never infer sensitive data from Sati memories (credentials, legal identifiers, health details, financial account data).
351
- - When assumptions were used, mention them briefly in the user-facing response and allow correction.
352
- - break the request into multiple delegations if it contains multiple independent actions.
353
- - Set a single task per delegation tool call. Do not combine multiple actions into one delegation, as it complicates execution and error handling.
354
- - If user requested N independent actions, produce N delegated tasks (or direct answers), each one singular and tool-scoped.
355
- - If use a delegation dont use the sati or messages history to answer directly in the same response. Just response with the delegations.
356
- Example 1:
357
- ask: "Tell me my account balance and do a ping on google.com"
358
- good:
359
- - delegate to "neo_delegate" with task "Check account balance using morpheus analytics MCP and return the result."
360
- - delegate to "apoc_delegate" with task "Ping google.com using the network diagnostics MCP and return reachability status. Use '-n' flag for Windows and '-c' for Linux/macOS."
361
- bad:
362
- - delegate to "neo_delegate" with task "Check account balance using morpheus analytics MCP and ping google.com using the network diagnostics MCP, then return both results." (combines two independent actions into one delegation, which is not atomic and complicates execution and error handling)
380
+ ---------------------
381
+ LINK DELEGATION RULES
382
+ ---------------------
383
+ When delegating to Link:
384
+ - Include the user's original request and intent.
385
+ - Include a clear objective for what the Link agent should achieve.
386
+ - **NEVER inject Sati memories, assumptions, or pre-existing knowledge into the search query.** Link searches documents — the query must reflect ONLY what the user asked, not what you already know or assume.
387
+ - If the user asks "qual empresa trabalho atualmente?", delegate as-is: "Find the user's current company in their CV." Do NOT add specific names, values, or context from Sati memories to the query.
388
+ - Sati context may be included ONLY as a separate "context for interpretation" section, clearly separated from the search objective, so Link can use it to interpret results — never to filter or bias the search itself.
389
+ - Constraints such as response length, specific documents to search, or resources to avoid may be included.
363
390
 
364
- Example 2:
365
- ask: "I have two cats" or "My name is John"
366
- good:
367
- - Answer directly acknowledging the fact. Do NOT delegate.
368
- bad:
369
- - delegate to "neo_delegate" or "apoc_delegate" to save the fact. (Sati handles this automatically in the background)
370
391
 
392
+ ---------------------
393
+ SKILLS
394
+ ---------------------
371
395
  ${SkillRegistry.getInstance().getSystemPromptSection()}
396
+
397
+ ---------------------
398
+ SMITHS
399
+ ---------------------
372
400
  ${SmithRegistry.getInstance().getSystemPromptSection()}
373
401
  `);
402
+ //save the system prompt on ~/.morpheus/system_prompt.txt for debugging and prompt engineering purposes
403
+ try {
404
+ writeFileSync(`${PATHS.root}/system_prompt.txt`, String(systemMessage.content), 'utf-8');
405
+ }
406
+ catch { }
374
407
  // Resolve the authoritative session ID for this call.
375
408
  // Priority: explicit taskContext > current history instance > fallback.
376
409
  const currentSessionId = taskContext?.session_id
@@ -421,6 +454,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
421
454
  Apoc.setSessionId(currentSessionId);
422
455
  Neo.setSessionId(currentSessionId);
423
456
  Trinity.setSessionId(currentSessionId);
457
+ Link.setSessionId(currentSessionId);
424
458
  const invokeContext = {
425
459
  origin_channel: taskContext?.origin_channel ?? "api",
426
460
  session_id: taskContext?.session_id ?? currentSessionId ?? "default",
@@ -431,7 +465,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
431
465
  let syncDelegationCount = 0;
432
466
  const oracleStartMs = Date.now();
433
467
  const response = await TaskRequestContext.run(invokeContext, async () => {
434
- const agentResponse = await this.provider.invoke({ messages }, { recursionLimit: 50 });
468
+ const agentResponse = await this.provider.invoke({ messages }, { recursionLimit: 10 });
435
469
  contextDelegationAcks = TaskRequestContext.getDelegationAcks();
436
470
  syncDelegationCount = TaskRequestContext.getSyncDelegationCount();
437
471
  return agentResponse;
@@ -464,7 +498,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
464
498
  const startNewMessagesIndex = messages.length;
465
499
  const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
466
500
  // Emit tool_call audit events for Oracle's independent tool calls.
467
- // Delegation tools (apoc/neo/trinity/smith/skill) are already audited
501
+ // Delegation tools (apoc/neo/trinity/smith/skill/link) are already audited
468
502
  // inside buildDelegationTool or the task system — skip them here.
469
503
  emitToolAuditEvents(newGeneratedMessages, currentSessionId ?? 'default', 'oracle', {
470
504
  skipTools: ORACLE_DELEGATION_TOOLS,
@@ -648,19 +682,22 @@ Use it to inform your response and tool selection (if needed), but do not assume
648
682
  await Construtor.reload();
649
683
  await Neo.refreshDelegateCatalog().catch(() => { });
650
684
  await Trinity.refreshDelegateCatalog().catch(() => { });
651
- updateSkillToolDescriptions();
685
+ await Link.refreshDelegateCatalog().catch(() => { });
652
686
  this.provider = await ProviderFactory.create(this.config.llm, [
653
687
  buildSetupTool(),
654
688
  TaskQueryTool,
655
689
  Neo.getInstance().createDelegateTool(),
656
690
  Apoc.getInstance().createDelegateTool(),
657
691
  Trinity.getInstance().createDelegateTool(),
658
- SkillExecuteTool,
659
- SkillDelegateTool,
692
+ Link.getInstance().createDelegateTool(),
693
+ createLoadSkillTool(),
660
694
  timeVerifierTool,
661
695
  ...chronosTools,
662
696
  ]);
663
697
  await Neo.getInstance().reload();
698
+ await Apoc.getInstance().reload();
699
+ await Trinity.getInstance().reload();
700
+ await Link.getInstance().reload();
664
701
  this.display.log(`Oracle and Neo tools reloaded`, { source: 'Oracle' });
665
702
  }
666
703
  }