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.
- package/README.md +275 -1116
- package/dist/channels/telegram.js +210 -73
- package/dist/cli/commands/doctor.js +34 -0
- package/dist/cli/commands/init.js +128 -0
- package/dist/cli/commands/restart.js +17 -0
- package/dist/cli/commands/start.js +15 -0
- package/dist/config/manager.js +51 -0
- package/dist/config/schemas.js +7 -0
- package/dist/devkit/tools/network.js +1 -1
- package/dist/http/api.js +177 -10
- package/dist/runtime/apoc.js +139 -32
- package/dist/runtime/memory/sati/repository.js +30 -2
- package/dist/runtime/memory/sati/service.js +46 -15
- package/dist/runtime/memory/sati/system-prompts.js +71 -29
- package/dist/runtime/memory/sqlite.js +24 -0
- package/dist/runtime/neo.js +134 -0
- package/dist/runtime/oracle.js +244 -133
- package/dist/runtime/providers/factory.js +1 -12
- package/dist/runtime/tasks/context.js +53 -0
- package/dist/runtime/tasks/dispatcher.js +70 -0
- package/dist/runtime/tasks/notifier.js +68 -0
- package/dist/runtime/tasks/repository.js +370 -0
- package/dist/runtime/tasks/types.js +1 -0
- package/dist/runtime/tasks/worker.js +96 -0
- package/dist/runtime/tools/apoc-tool.js +61 -8
- package/dist/runtime/tools/delegation-guard.js +29 -0
- package/dist/runtime/tools/index.js +1 -0
- package/dist/runtime/tools/neo-tool.js +99 -0
- package/dist/runtime/tools/task-query-tool.js +76 -0
- package/dist/runtime/webhooks/dispatcher.js +10 -19
- package/dist/types/config.js +10 -0
- package/dist/ui/assets/index-20lLB1sM.js +112 -0
- package/dist/ui/assets/index-BJ56bRfs.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-LemKVRjC.js +0 -112
- package/dist/ui/assets/index-TCQ7VNYO.css +0 -1
package/dist/runtime/oracle.js
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
When
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
333
|
-
this.provider = await ProviderFactory.create(this.config.llm,
|
|
334
|
-
|
|
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
|
-
|
|
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
|
+
}
|