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.
- package/README.md +63 -43
- package/dist/channels/discord.js +71 -21
- package/dist/channels/telegram.js +73 -19
- package/dist/cli/commands/restart.js +15 -0
- package/dist/cli/commands/start.js +18 -0
- package/dist/config/manager.js +61 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/schemas.js +11 -3
- package/dist/http/api.js +3 -0
- package/dist/http/routers/link.js +239 -0
- package/dist/http/routers/skills.js +1 -8
- package/dist/runtime/apoc.js +1 -1
- package/dist/runtime/audit/repository.js +1 -1
- package/dist/runtime/link-chunker.js +214 -0
- package/dist/runtime/link-repository.js +301 -0
- package/dist/runtime/link-search.js +298 -0
- package/dist/runtime/link-worker.js +284 -0
- package/dist/runtime/link.js +295 -0
- package/dist/runtime/memory/sati/service.js +1 -1
- package/dist/runtime/memory/sqlite.js +52 -0
- package/dist/runtime/neo.js +1 -1
- package/dist/runtime/oracle.js +81 -44
- package/dist/runtime/scaffold.js +4 -17
- package/dist/runtime/skills/__tests__/loader.test.js +7 -10
- package/dist/runtime/skills/__tests__/registry.test.js +2 -18
- package/dist/runtime/skills/__tests__/tool.test.js +55 -224
- package/dist/runtime/skills/index.js +1 -2
- package/dist/runtime/skills/loader.js +0 -2
- package/dist/runtime/skills/registry.js +8 -20
- package/dist/runtime/skills/schema.js +0 -4
- package/dist/runtime/skills/tool.js +42 -209
- package/dist/runtime/smiths/delegator.js +1 -1
- package/dist/runtime/smiths/registry.js +1 -1
- package/dist/runtime/tasks/worker.js +12 -44
- package/dist/runtime/trinity.js +1 -1
- package/dist/types/config.js +14 -0
- package/dist/ui/assets/AuditDashboard-93LCGHG1.js +1 -0
- package/dist/ui/assets/{Chat-BNtutgja.js → Chat-CK5sNcQ1.js} +8 -8
- package/dist/ui/assets/{Chronos-3C8RPZcl.js → Chronos-m2h--GEe.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-ZQPBeJ2Z.js → ConfirmationModal-Dd5pUJme.js} +1 -1
- package/dist/ui/assets/{Dashboard-CqkHzr2F.js → Dashboard-ODwl7d-a.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-CioxFWn_.js → DeleteConfirmationModal-CCcojDmr.js} +1 -1
- package/dist/ui/assets/Documents-dWnSoxFO.js +7 -0
- package/dist/ui/assets/{Logs-DBVanS0O.js → Logs-Dc9Z2LBj.js} +1 -1
- package/dist/ui/assets/{MCPManager-vXfL3P2U.js → MCPManager-CMkb8vMn.js} +1 -1
- package/dist/ui/assets/{ModelPricing-DyfdunLT.js → ModelPricing-DtHPPbEQ.js} +1 -1
- package/dist/ui/assets/{Notifications-VL-vep6d.js → Notifications-BPvo-DWP.js} +1 -1
- package/dist/ui/assets/{Pagination-oTGieBLM.js → Pagination-BHZKk42X.js} +1 -1
- package/dist/ui/assets/{SatiMemories-jaadkW0U.js → SatiMemories-BUPu1Lxr.js} +1 -1
- package/dist/ui/assets/SessionAudit-CFKF4DA8.js +9 -0
- package/dist/ui/assets/Settings-C4JrXfsR.js +47 -0
- package/dist/ui/assets/{Skills-DE3zziXL.js → Skills-BUlvJgJ4.js} +1 -1
- package/dist/ui/assets/{Smiths-pmogN1mU.js → Smiths-CDtJdY0I.js} +1 -1
- package/dist/ui/assets/{Tasks-Bs8s34Jc.js → Tasks-DK_cOsNK.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-D7uihcdp.js → TrinityDatabases-X07by-19.js} +1 -1
- package/dist/ui/assets/{UsageStats-B9gePLZ0.js → UsageStats-dYcgckLq.js} +1 -1
- package/dist/ui/assets/{WebhookManager-B2L3rCLM.js → WebhookManager-DDw5eX2R.js} +1 -1
- package/dist/ui/assets/{audit-Cggeu9mM.js → audit-DZ5WLUEm.js} +1 -1
- package/dist/ui/assets/{chronos-D3-sWhfU.js → chronos-B_HI4mlq.js} +1 -1
- package/dist/ui/assets/{config-CBqRUPgn.js → config-B-YxlVrc.js} +1 -1
- package/dist/ui/assets/index-DVjwJ8jT.css +1 -0
- package/dist/ui/assets/{index-zKplfrXZ.js → index-DfJwcKqG.js} +5 -5
- package/dist/ui/assets/{mcp-uL1R9hyA.js → mcp-k-_pwbqA.js} +1 -1
- package/dist/ui/assets/{skills-jmw8yTJs.js → skills-xMXangks.js} +1 -1
- package/dist/ui/assets/{stats-HOms6GnM.js → stats-C4QZIv5O.js} +1 -1
- package/dist/ui/assets/{vendor-icons-DMd9RGvJ.js → vendor-icons-NHF9HNeN.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/sw.js +1 -1
- package/package.json +3 -1
- package/dist/runtime/__tests__/keymaker.test.js +0 -148
- package/dist/runtime/keymaker.js +0 -157
- package/dist/ui/assets/AuditDashboard-DliJ1CX0.js +0 -1
- package/dist/ui/assets/SessionAudit-BsXrWlwz.js +0 -9
- package/dist/ui/assets/Settings-B4eezRcg.js +0 -47
- 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:
|
|
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.
|
package/dist/runtime/neo.js
CHANGED
|
@@ -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:
|
|
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"
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
-
|
|
347
|
-
-
|
|
348
|
-
-
|
|
349
|
-
-
|
|
350
|
-
-
|
|
351
|
-
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
}
|