morpheus-cli 0.4.14 → 0.5.0

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 (38) hide show
  1. package/README.md +275 -1116
  2. package/dist/channels/telegram.js +210 -73
  3. package/dist/cli/commands/doctor.js +34 -0
  4. package/dist/cli/commands/init.js +128 -0
  5. package/dist/cli/commands/restart.js +17 -0
  6. package/dist/cli/commands/start.js +15 -0
  7. package/dist/config/manager.js +51 -0
  8. package/dist/config/schemas.js +7 -0
  9. package/dist/devkit/tools/network.js +1 -1
  10. package/dist/http/api.js +177 -10
  11. package/dist/runtime/apoc.js +139 -32
  12. package/dist/runtime/memory/sati/repository.js +30 -2
  13. package/dist/runtime/memory/sati/service.js +46 -15
  14. package/dist/runtime/memory/sati/system-prompts.js +71 -29
  15. package/dist/runtime/memory/sqlite.js +24 -0
  16. package/dist/runtime/neo.js +134 -0
  17. package/dist/runtime/oracle.js +244 -133
  18. package/dist/runtime/providers/factory.js +1 -12
  19. package/dist/runtime/tasks/context.js +53 -0
  20. package/dist/runtime/tasks/dispatcher.js +70 -0
  21. package/dist/runtime/tasks/notifier.js +68 -0
  22. package/dist/runtime/tasks/repository.js +370 -0
  23. package/dist/runtime/tasks/types.js +1 -0
  24. package/dist/runtime/tasks/worker.js +96 -0
  25. package/dist/runtime/tools/apoc-tool.js +61 -8
  26. package/dist/runtime/tools/delegation-guard.js +29 -0
  27. package/dist/runtime/tools/index.js +1 -0
  28. package/dist/runtime/tools/neo-tool.js +99 -0
  29. package/dist/runtime/tools/task-query-tool.js +76 -0
  30. package/dist/runtime/webhooks/dispatcher.js +10 -19
  31. package/dist/types/config.js +10 -0
  32. package/dist/ui/assets/index-20lLB1sM.js +112 -0
  33. package/dist/ui/assets/index-BJ56bRfs.css +1 -0
  34. package/dist/ui/index.html +2 -2
  35. package/dist/ui/sw.js +1 -1
  36. package/package.json +1 -1
  37. package/dist/ui/assets/index-LemKVRjC.js +0 -112
  38. package/dist/ui/assets/index-TCQ7VNYO.css +0 -1
@@ -1,23 +1,116 @@
1
- import { HumanMessage, SystemMessage } from "@langchain/core/messages";
1
+ import { HumanMessage, SystemMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
2
2
  import { ProviderFactory } from "./providers/factory.js";
3
- import { Construtor } from "./tools/factory.js";
4
3
  import { ConfigManager } from "../config/manager.js";
5
4
  import { ProviderError } from "./errors.js";
6
5
  import { DisplayManager } from "./display.js";
7
6
  import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
8
7
  import { SatiMemoryMiddleware } from "./memory/sati/index.js";
9
8
  import { Apoc } from "./apoc.js";
9
+ import { TaskRequestContext } from "./tasks/context.js";
10
+ import { TaskRepository } from "./tasks/repository.js";
11
+ import { Neo } from "./neo.js";
12
+ import { NeoDelegateTool } from "./tools/neo-tool.js";
13
+ import { ApocDelegateTool } from "./tools/apoc-tool.js";
14
+ import { TaskQueryTool } from "./tools/task-query-tool.js";
10
15
  export class Oracle {
11
16
  provider;
12
17
  config;
13
18
  history;
14
19
  display = DisplayManager.getInstance();
20
+ taskRepository = TaskRepository.getInstance();
15
21
  databasePath;
16
22
  satiMiddleware = SatiMemoryMiddleware.getInstance();
17
23
  constructor(config, overrides) {
18
24
  this.config = config || ConfigManager.getInstance().get();
19
25
  this.databasePath = overrides?.databasePath;
20
26
  }
27
+ buildDelegationFailureResponse() {
28
+ return "Task enqueue could not be confirmed in the database. No task was created. Please retry.";
29
+ }
30
+ looksLikeSyntheticDelegationAck(text) {
31
+ const raw = (text || "").trim();
32
+ if (!raw)
33
+ return false;
34
+ // Detect the structured ack format that Oracle itself generates.
35
+ // LLMs can learn to reproduce this format from conversation history without calling any tool.
36
+ const hasAckTaskLine = /Task\s+`[0-9a-fA-F]{8}-[0-9a-fA-F]{4}/i.test(raw);
37
+ const hasAckAgentLine = /Agent:\s*`(APOC|NEO|apoc|neo)/i.test(raw);
38
+ const hasAckStatusLine = /Status:\s*`(QUEUED|PENDING|RUNNING|COMPLETED|FAILED)/i.test(raw);
39
+ if (hasAckTaskLine && hasAckAgentLine && hasAckStatusLine)
40
+ return true;
41
+ const hasCreationClaim = /(as\s+tarefas?\s+foram\s+criadas|tarefa\s+criada|nova\s+tarefa\s+criada|deleguei|delegado|delegada|tasks?\s+created|task\s+created|queued\s+for|agendei|agendado|agendada|foi\s+agendad)/i.test(raw);
42
+ if (!hasCreationClaim)
43
+ return false;
44
+ const hasAgentMention = /\b(apoc|neo|trinit)\b/i.test(raw);
45
+ const hasUuid = /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b/.test(raw);
46
+ const hasAgentListLine = /(?:\*|-)?.{0,8}(apoc|neo|trinit)\s*[::]/i.test(raw);
47
+ return hasCreationClaim && (hasAgentMention || hasUuid || hasAgentListLine);
48
+ }
49
+ buildDelegationAck(acks) {
50
+ const truncate = (s, max = 72) => s.length > max ? s.slice(0, max).trimEnd() + '…' : s;
51
+ if (acks.length === 1) {
52
+ const { task_id, agent } = acks[0];
53
+ const task = this.taskRepository.getTaskById(task_id);
54
+ const taskLine = task?.input ? `\n${truncate(task.input)}` : '';
55
+ return `✅\ Task \`${task_id.toUpperCase()}\`\nAgent: \`${agent.toUpperCase()}\`\nStatus: \`QUEUED\`${taskLine}`;
56
+ }
57
+ const lines = acks.map((a) => {
58
+ const task = this.taskRepository.getTaskById(a.task_id);
59
+ const label = task?.input ? ` — ${truncate(task.input, 50)}` : '';
60
+ return `• ${a.agent.toUpperCase()}: \`${a.task_id}\`${label}`;
61
+ }).join('\n');
62
+ return `Tasks:\n${lines}\n\nRunning...`;
63
+ }
64
+ buildDelegationAckResult(acks) {
65
+ return { content: this.buildDelegationAck(acks) };
66
+ }
67
+ extractDelegationAcksFromMessages(messages) {
68
+ const acks = [];
69
+ const regex = /Task\s+([0-9a-fA-F-]{36})\s+(?:queued|already queued)\s+for\s+(Apoc|Neo|apoc|neo)\s+execution/i;
70
+ for (const msg of messages) {
71
+ if (!(msg instanceof ToolMessage))
72
+ continue;
73
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
74
+ const match = regex.exec(content);
75
+ if (!match)
76
+ continue;
77
+ acks.push({ task_id: match[1], agent: match[2].toLowerCase() });
78
+ }
79
+ return acks;
80
+ }
81
+ validateDelegationAcks(acks, requestMessage) {
82
+ const deduped = new Map();
83
+ for (const ack of acks) {
84
+ deduped.set(`${ack.agent}:${ack.task_id}`, { task_id: ack.task_id, agent: ack.agent });
85
+ }
86
+ const valid = [];
87
+ for (const ack of deduped.values()) {
88
+ const task = this.taskRepository.getTaskById(ack.task_id);
89
+ if (!task) {
90
+ this.display.log(`Discarded delegation ack with unknown task id: ${ack.task_id}`, { source: "Oracle", level: "warning", meta: { requestMessage, agent: ack.agent } });
91
+ continue;
92
+ }
93
+ if (task.agent !== ack.agent) {
94
+ this.display.log(`Discarded delegation ack with agent mismatch for task ${ack.task_id}: ack=${ack.agent}, db=${task.agent}`, { source: "Oracle", level: "warning", meta: { requestMessage } });
95
+ continue;
96
+ }
97
+ valid.push(ack);
98
+ }
99
+ return valid;
100
+ }
101
+ hasDelegationToolCall(messages) {
102
+ for (const msg of messages) {
103
+ if (!(msg instanceof AIMessage))
104
+ continue;
105
+ const toolCalls = msg.tool_calls ?? [];
106
+ if (!Array.isArray(toolCalls))
107
+ continue;
108
+ if (toolCalls.some((tc) => tc?.name === "apoc_delegate" || tc?.name === "neo_delegate")) {
109
+ return true;
110
+ }
111
+ }
112
+ return false;
113
+ }
21
114
  async initialize() {
22
115
  if (!this.config.llm) {
23
116
  throw new Error("LLM configuration missing in config object.");
@@ -29,8 +122,10 @@ export class Oracle {
29
122
  // Note: API Key validation is delegated to ProviderFactory or the Provider itself
30
123
  // to allow for Environment Variable fallback supported by LangChain.
31
124
  try {
32
- const tools = await Construtor.create();
33
- this.provider = await ProviderFactory.create(this.config.llm, tools);
125
+ // Refresh Neo tool catalog so neo_delegate description contains runtime tools list.
126
+ // Fail-open: Oracle can still initialize even if catalog refresh fails.
127
+ await Neo.refreshDelegateCatalog().catch(() => { });
128
+ this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool]);
34
129
  if (!this.provider) {
35
130
  throw new Error("Provider factory returned undefined");
36
131
  }
@@ -50,7 +145,7 @@ export class Oracle {
50
145
  throw new ProviderError(this.config.llm.provider || 'unknown', err, "Oracle initialization failed");
51
146
  }
52
147
  }
53
- async chat(message, extraUsage, isTelephonist) {
148
+ async chat(message, extraUsage, isTelephonist, taskContext) {
54
149
  if (!this.provider) {
55
150
  throw new Error("Oracle not initialized. Call initialize() first.");
56
151
  }
@@ -70,122 +165,54 @@ export class Oracle {
70
165
  userMessage.usage_metadata = extraUsage;
71
166
  }
72
167
  const systemMessage = new SystemMessage(`
73
- You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
74
-
75
- Your role is to orchestrate tools, MCPs, and language models to accurately fulfill the Architect’s request.
76
-
77
- You are an operator, not a guesser.
78
- Accuracy, verification, and task completion are more important than speed.
79
-
80
- --------------------------------------------------
81
- CORE OPERATING PRINCIPLES
82
- --------------------------------------------------
83
-
84
- 1. TOOL EVALUATION FIRST
85
-
86
- Before generating any final answer, evaluate whether an available tool or MCP can provide a more accurate, up-to-date, or authoritative result.
87
-
88
- If a tool can provide the answer, you MUST call the tool.
89
-
90
- Never generate speculative values when a tool can verify them.
91
-
92
-
93
- 2. ACTIVE INTENT TRACKING (CRITICAL)
94
-
95
- You must always maintain the current active user intent until it is fully resolved.
96
-
97
- If you ask a clarification question, the original intent remains ACTIVE.
98
-
99
- When the user responds to a clarification, you MUST:
100
-
101
- - Combine the new information with the original request
102
- - Resume the same task
103
- - Continue the tool evaluation process
104
- - Complete the original objective
105
-
106
- You MUST NOT:
107
- - Treat clarification answers as new unrelated requests
108
- - Drop the original task
109
- - Change subject unexpectedly
110
-
111
- Clarifications are part of the same execution chain.
112
-
113
-
114
- 3. NO HISTORICAL ASSUMPTIONS FOR DYNAMIC DATA
115
-
116
- If the user asks something that:
117
-
118
- - may change over time
119
- - depends on system state
120
- - depends on filesystem
121
- - depends on external APIs
122
- - was previously asked in the conversation
123
-
124
- You MUST NOT reuse previous outputs as final truth.
125
-
126
- You MUST:
127
- - Re-evaluate available tools
128
- - Re-execute relevant tools
129
- - Provide a fresh result
130
-
131
- Repeated queries require fresh verification.
132
-
133
-
134
- 4. HISTORY IS CONTEXT, NOT SOURCE OF TRUTH
135
-
136
- Conversation history provides context, not verified data.
137
-
138
- Never assume:
139
- - System state
140
- - File contents
141
- - Database values
142
- - API responses
143
-
144
- based only on previous messages.
145
-
146
-
147
- 5. TASK RESOLUTION LOOP
148
-
149
- You must operate in this loop:
150
-
151
- - Identify intent
152
- - Determine missing information (if any)
153
- - Ask clarification ONLY if necessary
154
- - When clarification is received, resume original task
155
- - Evaluate tools
156
- - Execute tools if applicable
157
- - Deliver verified answer
158
-
159
- Do not break this loop.
160
-
161
-
162
- 6. TOOL PRIORITY OVER LANGUAGE GUESSING
163
-
164
- If a tool can compute, fetch, inspect, or verify something, prefer tool usage.
165
-
166
- Never hallucinate values retrievable via tools.
167
-
168
-
169
- 7. FINAL ANSWER POLICY
170
-
171
- Provide a natural language answer only if:
172
-
173
- - No tool is relevant
174
- - Tools are unavailable
175
- - The request is purely conceptual
176
-
177
- Otherwise, use tools first.
178
-
179
- --------------------------------------------------
180
-
181
- You are a deterministic orchestration layer.
182
- You do not drift.
183
- You do not abandon tasks.
184
- You do not speculate when verification is possible.
185
-
186
- You maintain intent until resolution.
187
-
188
- `);
168
+ You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
169
+
170
+ You are an orchestrator and task router.
171
+
172
+
173
+ Rules:
174
+ 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).
175
+ **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.*
176
+ 2. For requests that require execution, verification, external/system state, or non-trivial operations, evaluate the available tools and choose the best one.
177
+ 3. For task status/check questions (for example: "consultou?", "status da task", "andamento"), use task_query directly and do not delegate.
178
+ 4. Prefer delegation tools when execution should be asynchronous, and return the task acknowledgement clearly.
179
+ 5. If the user asked for multiple independent actions in the same message, enqueue one delegated task per action. Each task must be atomic (single objective).
180
+ 6. If the user asked for a single action, do not create additional delegated tasks.
181
+ 7. Never fabricate execution results for delegated tasks.
182
+ 8. Keep responses concise and objective.
183
+ 9. Avoid duplicate delegations to the same tool or agent.
184
+ 10. After enqueuing all required delegated tasks for the current message, stop calling tools and return a concise acknowledgement.
185
+ 11. If a delegation is rejected as "not atomic", immediately split into smaller delegations and retry.
186
+
187
+ Delegation quality:
188
+ - Write delegation input in the same language requested by the user.
189
+ - Include clear objective and constraints.
190
+ - Include OS-aware guidance for network checks when relevant.
191
+ - Use Sati memories only as context to complement the task, never as source of truth for dynamic data.
192
+ - Use Sati memories to fill missing stable context fields (for example: city, timezone, language, currency, preferred units).
193
+ - If Sati memory is conflicting or uncertain for a required field, ask one short clarification before delegating.
194
+ - When completing missing fields from Sati, include explicit assumptions in delegation context using the format: "Assumption from Sati: key=value".
195
+ - Never infer sensitive data from Sati memories (credentials, legal identifiers, health details, financial account data).
196
+ - When assumptions were used, mention them briefly in the user-facing response and allow correction.
197
+ - break the request into multiple delegations if it contains multiple independent actions.
198
+ - Set a single task per delegation tool call. Do not combine multiple actions into one delegation, as it complicates execution and error handling.
199
+ - If user requested N independent actions, produce N delegated tasks (or direct answers), each one singular and tool-scoped.
200
+ - If use a delegation dont use the sati or messages history to answer directly in the same response. Just response with the delegations.
201
+ Example 1:
202
+ ask: "Tell me my account balance and do a ping on google.com"
203
+ good:
204
+ - delegate to "neo_delegate" with task "Check account balance using morpheus analytics MCP and return the result."
205
+ - 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."
206
+ bad:
207
+ - 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)
208
+
209
+ Example 2:
210
+ ask: "I have two cats" or "My name is John"
211
+ good:
212
+ - Answer directly acknowledging the fact. Do NOT delegate.
213
+ bad:
214
+ - delegate to "neo_delegate" or "apoc_delegate" to save the fact. (Sati handles this automatically in the background)
215
+ `);
189
216
  // Load existing history from database in reverse order (most recent first)
190
217
  let previousMessages = await this.history.getMessages();
191
218
  previousMessages = previousMessages.reverse();
@@ -205,7 +232,15 @@ You maintain intent until resolution.
205
232
  systemMessage
206
233
  ];
207
234
  if (memoryMessage) {
208
- messages.push(memoryMessage);
235
+ // messages.push(memoryMessage);
236
+ systemMessage.content += `
237
+
238
+ ## Retrieved SATI Memory:
239
+ ${memoryMessage.content}
240
+
241
+ This memory may be relevant to the user's request.
242
+ Use this to complemento the informal conversatrion.
243
+ Use it to inform your response and tool selection (if needed), but do not assume it is 100% accurate or complete. Always validate against current inputs and tools.`;
209
244
  }
210
245
  messages.push(...previousMessages);
211
246
  messages.push(userMessage);
@@ -214,7 +249,19 @@ You maintain intent until resolution.
214
249
  ? this.history.currentSessionId
215
250
  : undefined;
216
251
  Apoc.setSessionId(currentSessionId);
217
- const response = await this.provider.invoke({ messages });
252
+ Neo.setSessionId(currentSessionId);
253
+ const invokeContext = {
254
+ origin_channel: taskContext?.origin_channel ?? "api",
255
+ session_id: taskContext?.session_id ?? currentSessionId ?? "default",
256
+ origin_message_id: taskContext?.origin_message_id,
257
+ origin_user_id: taskContext?.origin_user_id,
258
+ };
259
+ let contextDelegationAcks = [];
260
+ const response = await TaskRequestContext.run(invokeContext, async () => {
261
+ const agentResponse = await this.provider.invoke({ messages });
262
+ contextDelegationAcks = TaskRequestContext.getDelegationAcks();
263
+ return agentResponse;
264
+ });
218
265
  // Identify new messages generated during the interaction
219
266
  // The `messages` array passed to invoke had length `messages.length`
220
267
  // The `response.messages` contains the full state.
@@ -229,14 +276,77 @@ You maintain intent until resolution.
229
276
  model: this.config.llm.model
230
277
  };
231
278
  }
232
- // Persist user message + all generated messages in a single transaction
233
- await this.history.addMessages([userMessage, ...newGeneratedMessages]);
279
+ let responseContent;
280
+ const toolDelegationAcks = this.extractDelegationAcksFromMessages(newGeneratedMessages);
281
+ const hadDelegationToolCall = this.hasDelegationToolCall(newGeneratedMessages);
282
+ const mergedDelegationAcks = [
283
+ ...contextDelegationAcks.map((ack) => ({ task_id: ack.task_id, agent: ack.agent })),
284
+ ...toolDelegationAcks,
285
+ ];
286
+ const validDelegationAcks = this.validateDelegationAcks(mergedDelegationAcks, message);
287
+ if (mergedDelegationAcks.length > 0) {
288
+ this.display.log(`Delegation trace: context=${contextDelegationAcks.length}, tool_messages=${toolDelegationAcks.length}, valid=${validDelegationAcks.length}`, { source: "Oracle", level: "info" });
289
+ }
290
+ const delegatedThisTurn = validDelegationAcks.length > 0;
291
+ let blockedSyntheticDelegationAck = false;
292
+ if (delegatedThisTurn) {
293
+ const ackResult = this.buildDelegationAckResult(validDelegationAcks);
294
+ responseContent = ackResult.content;
295
+ const ackMessage = new AIMessage(responseContent);
296
+ ackMessage.provider_metadata = {
297
+ provider: this.config.llm.provider,
298
+ model: this.config.llm.model,
299
+ };
300
+ if (ackResult.usage_metadata) {
301
+ ackMessage.usage_metadata = ackResult.usage_metadata;
302
+ }
303
+ // Persist with addMessage so ack-provider usage is tracked per message row.
304
+ await this.history.addMessage(userMessage);
305
+ await this.history.addMessage(ackMessage);
306
+ }
307
+ else if (mergedDelegationAcks.length > 0 || hadDelegationToolCall) {
308
+ this.display.log(`Delegation attempted but no valid task id was confirmed (context=${contextDelegationAcks.length}, tool_messages=${toolDelegationAcks.length}, had_tool_call=${hadDelegationToolCall}).`, { source: "Oracle", level: "error" });
309
+ // Delegation was attempted but no valid task id could be confirmed in DB.
310
+ responseContent = this.buildDelegationFailureResponse();
311
+ const failureMessage = new AIMessage(responseContent);
312
+ failureMessage.provider_metadata = {
313
+ provider: this.config.llm.provider,
314
+ model: this.config.llm.model,
315
+ };
316
+ await this.history.addMessages([userMessage, failureMessage]);
317
+ }
318
+ else {
319
+ const lastMessage = response.messages[response.messages.length - 1];
320
+ responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
321
+ if (this.looksLikeSyntheticDelegationAck(responseContent)) {
322
+ blockedSyntheticDelegationAck = true;
323
+ this.display.log("Blocked synthetic delegation acknowledgement without validated task creation.", { source: "Oracle", level: "error", meta: { preview: responseContent.slice(0, 200) } });
324
+ const usage = lastMessage.usage_metadata
325
+ ?? lastMessage.response_metadata?.usage
326
+ ?? lastMessage.response_metadata?.tokenUsage
327
+ ?? lastMessage.usage;
328
+ responseContent = this.buildDelegationFailureResponse();
329
+ const failureMessage = new AIMessage(responseContent);
330
+ failureMessage.provider_metadata = {
331
+ provider: this.config.llm.provider,
332
+ model: this.config.llm.model,
333
+ };
334
+ if (usage) {
335
+ failureMessage.usage_metadata = usage;
336
+ }
337
+ await this.history.addMessages([userMessage, failureMessage]);
338
+ }
339
+ else {
340
+ // Persist user message + all generated messages in a single transaction
341
+ await this.history.addMessages([userMessage, ...newGeneratedMessages]);
342
+ }
343
+ }
234
344
  this.display.log('Response generated.', { source: 'Oracle' });
235
- const lastMessage = response.messages[response.messages.length - 1];
236
- const responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
237
- // Sati Middleware: Evaluation (Fire and forget)
238
- this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage], currentSessionId)
239
- .catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
345
+ // Sati Middleware: skip memory evaluation for delegation-only acknowledgements.
346
+ if (!delegatedThisTurn && !blockedSyntheticDelegationAck) {
347
+ this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage], currentSessionId)
348
+ .catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
349
+ }
240
350
  return responseContent;
241
351
  }
242
352
  catch (err) {
@@ -329,8 +439,9 @@ You maintain intent until resolution.
329
439
  if (!this.provider) {
330
440
  throw new Error("Oracle not initialized. Call initialize() first.");
331
441
  }
332
- const tools = await Construtor.create();
333
- this.provider = await ProviderFactory.create(this.config.llm, tools);
334
- this.display.log(`MCP tools reloaded (${tools.length} tools)`, { source: 'Oracle' });
442
+ await Neo.refreshDelegateCatalog().catch(() => { });
443
+ this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool]);
444
+ await Neo.getInstance().reload();
445
+ this.display.log(`Oracle and Neo tools reloaded`, { source: 'Oracle' });
335
446
  }
336
447
  }
@@ -5,7 +5,6 @@ import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
5
5
  import { ProviderError } from "../errors.js";
6
6
  import { createAgent, createMiddleware } from "langchain";
7
7
  import { DisplayManager } from "../display.js";
8
- import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool, ProviderModelUsageTool, ApocDelegateTool } from "../tools/index.js";
9
8
  export class ProviderFactory {
10
9
  static buildMonitoringMiddleware() {
11
10
  const display = DisplayManager.getInstance();
@@ -103,17 +102,7 @@ export class ProviderFactory {
103
102
  try {
104
103
  const model = ProviderFactory.buildModel(config);
105
104
  const middleware = ProviderFactory.buildMonitoringMiddleware();
106
- const toolsForAgent = [
107
- ...tools,
108
- ConfigQueryTool,
109
- ConfigUpdateTool,
110
- DiagnosticTool,
111
- MessageCountTool,
112
- TokenUsageTool,
113
- ProviderModelUsageTool,
114
- ApocDelegateTool
115
- ];
116
- return createAgent({ model, tools: toolsForAgent, middleware: [middleware] });
105
+ return createAgent({ model, tools, middleware: [middleware] });
117
106
  }
118
107
  catch (error) {
119
108
  ProviderFactory.handleProviderError(config, error);
@@ -0,0 +1,53 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const storage = new AsyncLocalStorage();
3
+ export class TaskRequestContext {
4
+ static MAX_DELEGATIONS_PER_TURN = 6;
5
+ static run(ctx, fn) {
6
+ return storage.run({ ...ctx }, fn);
7
+ }
8
+ static get() {
9
+ return storage.getStore();
10
+ }
11
+ static getDelegationAck() {
12
+ const acks = storage.getStore()?.delegation_acks ?? [];
13
+ return acks[0];
14
+ }
15
+ static setDelegationAck(ack) {
16
+ const current = storage.getStore();
17
+ if (!current)
18
+ return;
19
+ if (!current.delegation_acks) {
20
+ current.delegation_acks = [];
21
+ }
22
+ current.delegation_acks.push(ack);
23
+ }
24
+ static getDelegationAcks() {
25
+ return storage.getStore()?.delegation_acks ?? [];
26
+ }
27
+ static canEnqueueDelegation() {
28
+ return this.getDelegationAcks().length < this.MAX_DELEGATIONS_PER_TURN;
29
+ }
30
+ static findDuplicateDelegation(agent, task) {
31
+ const acks = this.getDelegationAcks();
32
+ if (acks.length === 0)
33
+ return undefined;
34
+ const normalized = this.normalizeTask(task);
35
+ for (const ack of acks) {
36
+ if (ack.agent !== agent) {
37
+ continue;
38
+ }
39
+ const existing = this.normalizeTask(ack.task);
40
+ if (existing === normalized) {
41
+ return ack;
42
+ }
43
+ }
44
+ return undefined;
45
+ }
46
+ static normalizeTask(task) {
47
+ return task
48
+ .toLowerCase()
49
+ .replace(/[^\p{L}\p{N}\s]/gu, " ")
50
+ .replace(/\s+/g, " ")
51
+ .trim();
52
+ }
53
+ }
@@ -0,0 +1,70 @@
1
+ import { DisplayManager } from '../display.js';
2
+ import { WebhookRepository } from '../webhooks/repository.js';
3
+ import { AIMessage } from '@langchain/core/messages';
4
+ import { SQLiteChatMessageHistory } from '../memory/sqlite.js';
5
+ export class TaskDispatcher {
6
+ static telegramAdapter = null;
7
+ static display = DisplayManager.getInstance();
8
+ static setTelegramAdapter(adapter) {
9
+ TaskDispatcher.telegramAdapter = adapter;
10
+ }
11
+ static async notifyTaskResult(task) {
12
+ if (task.origin_channel === 'webhook') {
13
+ if (!task.origin_message_id) {
14
+ throw new Error('Webhook-origin task has no origin_message_id');
15
+ }
16
+ const repo = WebhookRepository.getInstance();
17
+ const status = task.status === 'completed' ? 'completed' : 'failed';
18
+ const result = task.status === 'completed'
19
+ ? (task.output && task.output.trim().length > 0 ? task.output : 'Task completed without output.')
20
+ : (task.error && task.error.trim().length > 0 ? task.error : 'Task failed with unknown error.');
21
+ repo.updateNotificationResult(task.origin_message_id, status, result);
22
+ return;
23
+ }
24
+ if (task.origin_channel === 'ui') {
25
+ const statusIcon = task.status === 'completed' ? '✅' : '❌';
26
+ const body = task.status === 'completed'
27
+ ? (task.output && task.output.trim().length > 0 ? task.output : 'Task completed without output.')
28
+ : (task.error && task.error.trim().length > 0 ? task.error : 'Task failed with unknown error.');
29
+ const content = `${statusIcon}\ Task \`${task.id.toUpperCase()}\`\n` +
30
+ `Agent: \`${task.agent.toUpperCase()}\`\n` +
31
+ `Status: \`${task.status.toUpperCase()}\`\n\n${body}`;
32
+ TaskDispatcher.display.log(`Writing UI task result to session "${task.session_id}" (task ${task.id})`, { source: 'TaskDispatcher', level: 'info' });
33
+ const history = new SQLiteChatMessageHistory({ sessionId: task.session_id });
34
+ try {
35
+ const msg = new AIMessage(content);
36
+ msg.provider_metadata = { provider: task.agent, model: 'task-result' };
37
+ await history.addMessage(msg);
38
+ TaskDispatcher.display.log(`UI task result written successfully to session "${task.session_id}"`, { source: 'TaskDispatcher' });
39
+ }
40
+ finally {
41
+ history.close();
42
+ }
43
+ return;
44
+ }
45
+ if (task.origin_channel !== 'telegram') {
46
+ return;
47
+ }
48
+ const adapter = TaskDispatcher.telegramAdapter;
49
+ if (!adapter) {
50
+ throw new Error('Telegram adapter not connected');
51
+ }
52
+ const statusIcon = task.status === 'completed' ? '✅' : '❌';
53
+ const body = task.status === 'completed'
54
+ ? (task.output && task.output.trim().length > 0 ? task.output : 'Task completed without output.')
55
+ : (task.error && task.error.trim().length > 0 ? task.error : 'Task failed with unknown error.');
56
+ const header = `${statusIcon}\ Task \`${task.id.toUpperCase()}\`\n` +
57
+ `Agent: \`${task.agent.toUpperCase()}\`\n` +
58
+ `Status: \`${task.status.toUpperCase()}\``;
59
+ const message = `${header}\n\n${body}`;
60
+ if (task.origin_user_id) {
61
+ await adapter.sendMessageToUser(task.origin_user_id, message);
62
+ return;
63
+ }
64
+ TaskDispatcher.display.log(`Task ${task.id} has telegram origin but no origin_user_id; broadcasting to allowed users.`, { source: 'TaskDispatcher', level: 'warning' });
65
+ await adapter.sendMessage(message);
66
+ }
67
+ static async onTaskFinished(task) {
68
+ await TaskDispatcher.notifyTaskResult(task);
69
+ }
70
+ }