morpheus-cli 0.6.0 → 0.6.2
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/dist/devkit/tools/browser.js +167 -51
- package/dist/runtime/apoc.js +164 -141
- package/dist/runtime/chronos/worker.js +6 -7
- package/dist/runtime/memory/sati/repository.js +1 -1
- package/dist/runtime/memory/sqlite.js +63 -49
- package/dist/runtime/oracle.js +40 -3
- package/dist/runtime/providers/factory.js +1 -1
- package/dist/runtime/tasks/repository.js +24 -7
- package/dist/runtime/tasks/worker.js +12 -15
- package/dist/runtime/tools/index.js +1 -0
- package/dist/runtime/tools/neo-tool.js +2 -3
- package/dist/runtime/tools/time-verify-tools.js +70 -0
- package/package.json +2 -1
|
@@ -295,81 +295,197 @@ const browserFillTool = tool(async ({ selector, value, press_enter, timeout_ms }
|
|
|
295
295
|
*/
|
|
296
296
|
const browserSearchTool = tool(async ({ query, num_results, language }) => {
|
|
297
297
|
try {
|
|
298
|
-
const max = num_results ?? 10;
|
|
299
|
-
|
|
300
|
-
|
|
298
|
+
const max = Math.min(num_results ?? 10, 20);
|
|
299
|
+
const year = new Date().getFullYear().toString();
|
|
300
|
+
const lang = language ?? "pt";
|
|
301
|
+
// ─────────────────────────────────────────────
|
|
302
|
+
// 1️⃣ Intent Classification (heurístico leve)
|
|
303
|
+
// ─────────────────────────────────────────────
|
|
304
|
+
const qLower = query.toLowerCase();
|
|
305
|
+
let intent = "general";
|
|
306
|
+
if (/(hoje|último|resultado|placar|próximos|futebol|202\d)/.test(qLower))
|
|
307
|
+
intent = "news";
|
|
308
|
+
if (/(site oficial|gov|receita federal|ministério)/.test(qLower))
|
|
309
|
+
intent = "official";
|
|
310
|
+
if (/(api|sdk|npm|docs|documentação)/.test(qLower))
|
|
311
|
+
intent = "documentation";
|
|
312
|
+
if (/(preço|valor|quanto custa)/.test(qLower))
|
|
313
|
+
intent = "price";
|
|
314
|
+
// ─────────────────────────────────────────────
|
|
315
|
+
// 2️⃣ Query Refinement
|
|
316
|
+
// ─────────────────────────────────────────────
|
|
317
|
+
let refinedQuery = query;
|
|
318
|
+
if (intent === "news") {
|
|
319
|
+
refinedQuery = `${query} ${year}`;
|
|
320
|
+
}
|
|
321
|
+
if (intent === "official") {
|
|
322
|
+
refinedQuery = `${query} site:gov.br OR site:org`;
|
|
323
|
+
}
|
|
324
|
+
if (intent === "documentation") {
|
|
325
|
+
refinedQuery = `${query} documentation OR docs OR github`;
|
|
326
|
+
}
|
|
327
|
+
if (intent === "price") {
|
|
328
|
+
refinedQuery = `${query} preço ${year} Brasil`;
|
|
329
|
+
}
|
|
330
|
+
// ─────────────────────────────────────────────
|
|
331
|
+
// 3️⃣ DuckDuckGo Lite Fetch
|
|
332
|
+
// ─────────────────────────────────────────────
|
|
301
333
|
const regionMap = {
|
|
302
|
-
pt:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
jp: 'jp-jp', ar: 'ar-es',
|
|
334
|
+
pt: "br-pt",
|
|
335
|
+
br: "br-pt",
|
|
336
|
+
en: "us-en",
|
|
337
|
+
us: "us-en",
|
|
307
338
|
};
|
|
308
|
-
const lang = language ?? 'pt';
|
|
309
339
|
const kl = regionMap[lang] ?? lang;
|
|
310
|
-
const body = new URLSearchParams({ q:
|
|
311
|
-
const res = await fetch(
|
|
312
|
-
method:
|
|
340
|
+
const body = new URLSearchParams({ q: refinedQuery, kl }).toString();
|
|
341
|
+
const res = await fetch("https://lite.duckduckgo.com/lite/", {
|
|
342
|
+
method: "POST",
|
|
313
343
|
headers: {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
'(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
|
344
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
345
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
317
346
|
},
|
|
318
347
|
body,
|
|
319
|
-
signal: AbortSignal.timeout(
|
|
348
|
+
signal: AbortSignal.timeout(20000),
|
|
320
349
|
});
|
|
321
350
|
if (!res.ok) {
|
|
322
|
-
return JSON.stringify({ success: false,
|
|
351
|
+
return JSON.stringify({ success: false, error: `HTTP ${res.status}` });
|
|
323
352
|
}
|
|
324
353
|
const html = await res.text();
|
|
325
|
-
// Extract all result-link anchors (href uses double quotes, class uses single quotes)
|
|
326
354
|
const linkPattern = /href="(https?:\/\/[^"]+)"[^>]*class='result-link'>([^<]+)<\/a>/g;
|
|
327
355
|
const snippetPattern = /class='result-snippet'>([\s\S]*?)<\/td>/g;
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
356
|
+
const links = [...html.matchAll(linkPattern)];
|
|
357
|
+
const snippets = [...html.matchAll(snippetPattern)];
|
|
358
|
+
if (!links.length) {
|
|
359
|
+
return JSON.stringify({
|
|
360
|
+
success: false,
|
|
361
|
+
query: refinedQuery,
|
|
362
|
+
error: "No results found",
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// ─────────────────────────────────────────────
|
|
366
|
+
// 4️⃣ Helpers
|
|
367
|
+
// ─────────────────────────────────────────────
|
|
368
|
+
function normalizeUrl(url) {
|
|
369
|
+
try {
|
|
370
|
+
const u = new URL(url);
|
|
371
|
+
u.search = ""; // remove tracking params
|
|
372
|
+
return u.toString();
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return url;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function getDomain(url) {
|
|
379
|
+
try {
|
|
380
|
+
return new URL(url).hostname.replace("www.", "");
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return "";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const trustedDomains = [
|
|
387
|
+
"gov.br",
|
|
388
|
+
"bbc.com",
|
|
389
|
+
"reuters.com",
|
|
390
|
+
"globo.com",
|
|
391
|
+
"uol.com",
|
|
392
|
+
"cnn.com",
|
|
393
|
+
"github.com",
|
|
394
|
+
"npmjs.com",
|
|
395
|
+
"com.br"
|
|
396
|
+
];
|
|
397
|
+
function scoreResult(result) {
|
|
398
|
+
let score = 0;
|
|
399
|
+
const domain = getDomain(result.url);
|
|
400
|
+
if (trustedDomains.some((d) => domain.includes(d)))
|
|
401
|
+
score += 5;
|
|
402
|
+
if (intent === "official" && domain.includes("gov"))
|
|
403
|
+
score += 5;
|
|
404
|
+
if (intent === "documentation" && domain.includes("github"))
|
|
405
|
+
score += 4;
|
|
406
|
+
if (intent === "news" && /(globo|uol|cnn|bbc)/.test(domain))
|
|
407
|
+
score += 3;
|
|
408
|
+
if (result.title.toLowerCase().includes(query.toLowerCase()))
|
|
409
|
+
score += 2;
|
|
410
|
+
if (result.snippet.length > 120)
|
|
411
|
+
score += 1;
|
|
412
|
+
if (/login|assine|subscribe|paywall/i.test(result.snippet))
|
|
413
|
+
score -= 3;
|
|
414
|
+
return score;
|
|
415
|
+
}
|
|
416
|
+
// ─────────────────────────────────────────────
|
|
417
|
+
// 5️⃣ Build Results + Deduplicate Domain
|
|
418
|
+
// ─────────────────────────────────────────────
|
|
419
|
+
const domainSeen = new Set();
|
|
331
420
|
const results = [];
|
|
332
|
-
for (let i = 0; i <
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
// Skip sponsored ads (redirected through duckduckgo.com/y.js)
|
|
336
|
-
if (url.startsWith('https://duckduckgo.com/'))
|
|
421
|
+
for (let i = 0; i < links.length; i++) {
|
|
422
|
+
const rawUrl = links[i][1];
|
|
423
|
+
if (rawUrl.startsWith("https://duckduckgo.com/"))
|
|
337
424
|
continue;
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
425
|
+
const url = normalizeUrl(rawUrl);
|
|
426
|
+
const domain = getDomain(url);
|
|
427
|
+
if (domainSeen.has(domain))
|
|
428
|
+
continue;
|
|
429
|
+
domainSeen.add(domain);
|
|
430
|
+
const title = links[i][2].trim();
|
|
431
|
+
const snippet = snippets[i]
|
|
432
|
+
? snippets[i][1].replace(/<[^>]+>/g, "").trim()
|
|
433
|
+
: "";
|
|
434
|
+
const result = { title, url, snippet };
|
|
435
|
+
const score = scoreResult(result);
|
|
436
|
+
results.push({ ...result, domain, score });
|
|
342
437
|
}
|
|
343
|
-
if (results.length
|
|
438
|
+
if (!results.length) {
|
|
344
439
|
return JSON.stringify({
|
|
345
440
|
success: false,
|
|
346
|
-
query,
|
|
347
|
-
error:
|
|
441
|
+
query: refinedQuery,
|
|
442
|
+
error: "No valid results after filtering",
|
|
348
443
|
});
|
|
349
444
|
}
|
|
350
|
-
|
|
445
|
+
// ─────────────────────────────────────────────
|
|
446
|
+
// 6️⃣ Ranking
|
|
447
|
+
// ─────────────────────────────────────────────
|
|
448
|
+
results.sort((a, b) => b.score - a.score);
|
|
449
|
+
const topResults = results.slice(0, max);
|
|
450
|
+
const avgScore = topResults.reduce((acc, r) => acc + r.score, 0) /
|
|
451
|
+
topResults.length;
|
|
452
|
+
// ─────────────────────────────────────────────
|
|
453
|
+
// 7️⃣ Low-Confidence Auto Retry
|
|
454
|
+
// ─────────────────────────────────────────────
|
|
455
|
+
if (avgScore < 2 && intent !== "general") {
|
|
456
|
+
return JSON.stringify({
|
|
457
|
+
success: false,
|
|
458
|
+
query: refinedQuery,
|
|
459
|
+
warning: "Low confidence results. Consider refining query further.",
|
|
460
|
+
results: topResults,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return JSON.stringify({
|
|
464
|
+
success: true,
|
|
465
|
+
original_query: query,
|
|
466
|
+
refined_query: refinedQuery,
|
|
467
|
+
intent,
|
|
468
|
+
results: topResults.map((r) => ({
|
|
469
|
+
title: r.title,
|
|
470
|
+
url: r.url,
|
|
471
|
+
snippet: r.snippet,
|
|
472
|
+
score: r.score,
|
|
473
|
+
})),
|
|
474
|
+
});
|
|
351
475
|
}
|
|
352
476
|
catch (err) {
|
|
353
|
-
return JSON.stringify({
|
|
477
|
+
return JSON.stringify({
|
|
478
|
+
success: false,
|
|
479
|
+
error: err.message,
|
|
480
|
+
});
|
|
354
481
|
}
|
|
355
482
|
}, {
|
|
356
|
-
name:
|
|
357
|
-
description:
|
|
358
|
-
'Use this when you need to find current information, news, articles, documentation, or any web content. ' +
|
|
359
|
-
'Returns up to 10 results by default. Does NOT require browser_navigate first — it is self-contained and fast.',
|
|
483
|
+
name: "browser_search",
|
|
484
|
+
description: "Enhanced internet search with query refinement, ranking, deduplication, and confidence scoring. Uses DuckDuckGo Lite.",
|
|
360
485
|
schema: z.object({
|
|
361
|
-
query: z.string()
|
|
362
|
-
num_results: z
|
|
363
|
-
|
|
364
|
-
.int()
|
|
365
|
-
.min(1)
|
|
366
|
-
.max(20)
|
|
367
|
-
.optional()
|
|
368
|
-
.describe('Number of results to return. Default: 10, max: 20'),
|
|
369
|
-
language: z
|
|
370
|
-
.string()
|
|
371
|
-
.optional()
|
|
372
|
-
.describe('Language/region code (e.g. "pt" for Portuguese/Brazil, "en" for English). Default: "pt"'),
|
|
486
|
+
query: z.string(),
|
|
487
|
+
num_results: z.number().int().min(1).max(20).optional(),
|
|
488
|
+
language: z.string().optional(),
|
|
373
489
|
}),
|
|
374
490
|
});
|
|
375
491
|
// ─── Factory ────────────────────────────────────────────────────────────────
|
package/dist/runtime/apoc.js
CHANGED
|
@@ -73,151 +73,174 @@ export class Apoc {
|
|
|
73
73
|
source: "Apoc",
|
|
74
74
|
});
|
|
75
75
|
const systemMessage = new SystemMessage(`
|
|
76
|
-
You are Apoc, a
|
|
77
|
-
|
|
78
|
-
You are called by Oracle when the user needs dev operations performed.
|
|
79
|
-
Your job is to execute the requested task accurately using your available tools.
|
|
80
|
-
|
|
81
|
-
Available capabilities:
|
|
82
|
-
- Read, write, append, and delete files
|
|
83
|
-
- Execute shell commands
|
|
84
|
-
- Inspect and manage processes
|
|
85
|
-
- Run git operations (status, log, diff, clone, commit, etc.)
|
|
86
|
-
- Perform network operations (curl, DNS, ping)
|
|
87
|
-
- Manage packages (npm, yarn)
|
|
88
|
-
- Inspect system information
|
|
89
|
-
- Navigate websites, inspect DOM, click elements, fill forms using a real browser (for JS-heavy pages and SPAs)
|
|
90
|
-
- Search the internet with browser_search (DuckDuckGo, returns structured results)
|
|
91
|
-
|
|
92
|
-
OPERATING RULES:
|
|
93
|
-
1. Use tools to accomplish the task. Do not speculate.
|
|
94
|
-
2. Always verify results after execution.
|
|
95
|
-
3. Report clearly what was done and what the result was.
|
|
96
|
-
4. If something fails, report the error and what you tried.
|
|
97
|
-
5. Stay focused on the delegated task only.
|
|
98
|
-
6. Respond in the language requested by the user. If not explicit, use the dominant language of the task/context.
|
|
99
|
-
7. For connectivity checks, prefer the dedicated network tool "ping" (TCP reachability) instead of shell "ping".
|
|
100
|
-
8. Only use shell ping when explicitly required by the user. If shell ping is needed, detect OS first:
|
|
101
|
-
- Windows: use "-n" (never use "-c")
|
|
102
|
-
- Linux/macOS: use "-c"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
────────────────────────────────────────
|
|
106
|
-
BROWSER AUTOMATION PROTOCOL
|
|
107
|
-
────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
When using browser tools (browser_navigate, browser_get_dom, browser_click, browser_fill), follow this protocol exactly.
|
|
110
|
-
|
|
111
|
-
GENERAL PRINCIPLES
|
|
112
|
-
- Never guess selectors.
|
|
113
|
-
- Never assume page state.
|
|
114
|
-
- Always verify page transitions.
|
|
115
|
-
- Always extract evidence of success.
|
|
116
|
-
- If required user data is missing, STOP and return to Oracle immediately.
|
|
117
|
-
|
|
118
|
-
PHASE 1 — Navigation
|
|
119
|
-
1. ALWAYS call browser_navigate first.
|
|
120
|
-
2. Use:
|
|
121
|
-
- wait_until: "networkidle0" for SPAs or JS-heavy pages.
|
|
122
|
-
- wait_until: "domcontentloaded" for simple pages.
|
|
123
|
-
3. After navigation, confirm current_url and title.
|
|
124
|
-
4. If navigation fails, report the error and stop.
|
|
125
|
-
|
|
126
|
-
PHASE 2 — DOM Inspection (MANDATORY BEFORE ACTION)
|
|
127
|
-
1. ALWAYS call browser_get_dom before browser_click or browser_fill.
|
|
128
|
-
2. Identify stable selectors (prefer id > name > role > unique class).
|
|
129
|
-
3. Understand page structure and expected flow before interacting.
|
|
130
|
-
4. Never click or fill blindly.
|
|
131
|
-
|
|
132
|
-
PHASE 3 — Interaction
|
|
133
|
-
When clicking:
|
|
134
|
-
- Prefer stable selectors.
|
|
135
|
-
- If ambiguous, refine selector.
|
|
136
|
-
- Use visible text only if selector is unstable.
|
|
137
|
-
|
|
138
|
-
When filling:
|
|
139
|
-
- Confirm correct input field via DOM.
|
|
140
|
-
- Fill field.
|
|
141
|
-
- Submit using press_enter OR clicking submit button.
|
|
142
|
-
|
|
143
|
-
If login or personal data is required:
|
|
144
|
-
STOP and return required fields clearly.
|
|
145
|
-
|
|
146
|
-
PHASE 4 — State Verification (MANDATORY)
|
|
147
|
-
After ANY interaction:
|
|
148
|
-
1. Call browser_get_dom again.
|
|
149
|
-
2. Verify URL change or content change.
|
|
150
|
-
3. Confirm success or detect error message.
|
|
151
|
-
|
|
152
|
-
If expected change did not occur:
|
|
153
|
-
- Reinspect DOM.
|
|
154
|
-
- Attempt one justified alternative.
|
|
155
|
-
- If still failing, report failure clearly.
|
|
156
|
-
|
|
157
|
-
Maximum 2 attempts per step.
|
|
158
|
-
Never assume success.
|
|
159
|
-
|
|
160
|
-
PHASE 5 — Reporting
|
|
161
|
-
Include:
|
|
162
|
-
- Step-by-step actions
|
|
163
|
-
- Final URL
|
|
164
|
-
- Evidence of success
|
|
165
|
-
- Errors encountered
|
|
166
|
-
- Completion status (true/false)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
────────────────────────────────────────
|
|
170
|
-
WEB RESEARCH PROTOCOL
|
|
171
|
-
────────────────────────────────────────
|
|
172
|
-
|
|
173
|
-
When using browser_search for factual verification, follow this protocol strictly.
|
|
174
|
-
|
|
175
|
-
PHASE 1 — Query Design
|
|
176
|
-
1. Identify core entity, information type, and time constraint.
|
|
177
|
-
2. Build a precise search query.
|
|
178
|
-
3. If time-sensitive, include the current year.
|
|
179
|
-
|
|
180
|
-
PHASE 2 — Source Discovery
|
|
181
|
-
1. Call browser_search.
|
|
182
|
-
2. Collect results.
|
|
183
|
-
3. Prioritize official sources and major publications.
|
|
184
|
-
4. Reformulate query if necessary.
|
|
185
|
-
5. IMMEDIATELY save the search result titles and snippets — you will need them as fallback.
|
|
186
|
-
|
|
187
|
-
PHASE 3 — Source Validation
|
|
188
|
-
1. Try to open up to 3 distinct URLs with browser_navigate.
|
|
189
|
-
- For news/sports/media sites (GE, Globo, UOL, Terra, ESPN, etc.): ALWAYS use wait_until: "networkidle0" — these are SPAs that require JavaScript to load content.
|
|
190
|
-
- For simple/static pages: use wait_until: "domcontentloaded".
|
|
191
|
-
2. Read actual page content from accessible pages.
|
|
192
|
-
3. Ignore inaccessible pages (timeouts, bot blocks, errors).
|
|
193
|
-
4. If ALL navigations fail OR page content does not contain useful information:
|
|
194
|
-
- DO NOT attempt further workarounds (wget, curl, python scripts, http_request).
|
|
195
|
-
- Use the search snippets from Phase 2 as your source and proceed to Phase 5.
|
|
196
|
-
|
|
197
|
-
PHASE 4 — Cross-Verification
|
|
198
|
-
1. Extract relevant information from each accessible source.
|
|
199
|
-
2. Compare findings across sources when possible.
|
|
200
|
-
3. If content came from snippets only, state clearly:
|
|
201
|
-
"Source: DuckDuckGo search snippets (direct page access unavailable)."
|
|
202
|
-
|
|
203
|
-
PHASE 5 — Structured Report
|
|
204
|
-
Include:
|
|
205
|
-
- Direct answer based ONLY on what was found online
|
|
206
|
-
- Source URLs (from search results or navigated pages)
|
|
207
|
-
- Confidence level (High / Medium / Low)
|
|
208
|
-
|
|
209
|
-
ABSOLUTE RULES — NEVER VIOLATE
|
|
210
|
-
1. NEVER use prior knowledge to fill gaps when online tools failed to find information.
|
|
211
|
-
2. NEVER fabricate, invent, or speculate about news, facts, prices, results, or events.
|
|
212
|
-
3. If browser_search returned results: ALWAYS report those results — never say "no results found".
|
|
213
|
-
4. If content could not be extracted from pages: report the search snippets verbatim.
|
|
214
|
-
5. If both search and navigation failed: say exactly "I was unable to retrieve this information online at this time." Stop there. Do not continue with "based on general knowledge...".
|
|
215
|
-
6. Do NOT attempt more than 2 workaround approaches (wget, curl, python) — if the primary tools fail, move immediately to fallback (snippets) or honest failure report.
|
|
76
|
+
You are Apoc, a high-reliability execution and verification subagent inside the Morpheus system.
|
|
216
77
|
|
|
78
|
+
You are NOT a conversational assistant.
|
|
79
|
+
You are a task executor, evidence collector, and autonomous verifier.
|
|
217
80
|
|
|
81
|
+
Accuracy is more important than speed.
|
|
82
|
+
If verification fails, you must state it clearly.
|
|
83
|
+
|
|
84
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
85
|
+
CORE PRINCIPLES
|
|
86
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
87
|
+
|
|
88
|
+
• Never fabricate.
|
|
89
|
+
• Never rely on prior knowledge when online tools are available.
|
|
90
|
+
• Prefer authoritative sources over secondary commentary.
|
|
91
|
+
• Prefer verification over assumption.
|
|
92
|
+
• Explicitly measure and report confidence.
|
|
93
|
+
|
|
94
|
+
If reliable evidence cannot be obtained:
|
|
95
|
+
State clearly:
|
|
96
|
+
"I was unable to retrieve this information online at this time."
|
|
97
|
+
|
|
98
|
+
Stop there.
|
|
99
|
+
|
|
100
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
101
|
+
TASK CLASSIFICATION
|
|
102
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
103
|
+
|
|
104
|
+
Before using tools:
|
|
105
|
+
|
|
106
|
+
1. Identify task type:
|
|
107
|
+
- Dev operation
|
|
108
|
+
- Web research
|
|
109
|
+
- Browser automation
|
|
110
|
+
- System inspection
|
|
111
|
+
- Network verification
|
|
112
|
+
|
|
113
|
+
2. Determine whether external verification is required.
|
|
114
|
+
If yes → use tools.
|
|
115
|
+
If no → respond directly.
|
|
116
|
+
|
|
117
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
118
|
+
WEB RESEARCH STRATEGY (QUALITY-FIRST)
|
|
119
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
120
|
+
|
|
121
|
+
You operate in iterative cycles.
|
|
122
|
+
|
|
123
|
+
Maximum cycles: 2
|
|
124
|
+
|
|
125
|
+
━━━━━━━━━━━━━━
|
|
126
|
+
CYCLE 1
|
|
127
|
+
━━━━━━━━━━━━━━
|
|
128
|
+
|
|
129
|
+
PHASE 1 — Intelligent Query Design
|
|
130
|
+
• Identify intent: news, official, documentation, price, general.
|
|
131
|
+
• Add year if time-sensitive.
|
|
132
|
+
• Add region if relevant.
|
|
133
|
+
• Make query precise and focused.
|
|
134
|
+
|
|
135
|
+
PHASE 2 — Search
|
|
136
|
+
• Use browser_search.
|
|
137
|
+
• Immediately store titles and snippets.
|
|
138
|
+
|
|
139
|
+
PHASE 3 — Source Selection
|
|
140
|
+
Select up to 3 URLs.
|
|
141
|
+
Prefer:
|
|
142
|
+
- One official source
|
|
143
|
+
- One major publication
|
|
144
|
+
- One independent alternative
|
|
145
|
+
Avoid:
|
|
146
|
+
- Multiple links from same domain group
|
|
147
|
+
- Obvious paywalls or login walls
|
|
148
|
+
|
|
149
|
+
PHASE 4 — Navigation & Extraction
|
|
150
|
+
• Use browser_navigate.
|
|
151
|
+
• For news/media → wait_until: "networkidle0"
|
|
152
|
+
• Extract content from:
|
|
153
|
+
article > main > body
|
|
154
|
+
• Remove navigation noise.
|
|
155
|
+
|
|
156
|
+
PHASE 5 — Cross Verification
|
|
157
|
+
• Compare findings across sources.
|
|
158
|
+
• Detect inconsistencies.
|
|
159
|
+
• Identify strongest source.
|
|
160
|
+
|
|
161
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
162
|
+
AUTO-REFINEMENT LOOP
|
|
163
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
164
|
+
|
|
165
|
+
After completing Cycle 1, evaluate:
|
|
166
|
+
|
|
167
|
+
Trigger refinement if ANY condition is true:
|
|
168
|
+
|
|
169
|
+
• No authoritative source was successfully opened.
|
|
170
|
+
• Only snippets were available.
|
|
171
|
+
• Extracted content did not contain concrete answer.
|
|
172
|
+
• Sources contradict each other.
|
|
173
|
+
• Confidence would be LOW.
|
|
174
|
+
• Search results appear irrelevant or weak.
|
|
175
|
+
|
|
176
|
+
If refinement is triggered:
|
|
177
|
+
|
|
178
|
+
1. Reformulate query:
|
|
179
|
+
- Add year
|
|
180
|
+
- Add country
|
|
181
|
+
- Add "official"
|
|
182
|
+
- Add domain filters (gov, org, major media)
|
|
183
|
+
- Remove ambiguous words
|
|
184
|
+
|
|
185
|
+
2. Execute a second search cycle (Cycle 2).
|
|
186
|
+
3. Repeat selection, navigation, extraction, verification.
|
|
187
|
+
4. Choose the stronger cycle’s evidence.
|
|
188
|
+
5. Do NOT perform more than 2 cycles.
|
|
189
|
+
|
|
190
|
+
If Cycle 2 also fails:
|
|
191
|
+
Report inability clearly.
|
|
192
|
+
|
|
193
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
194
|
+
SELF-CRITIQUE (MANDATORY BEFORE OUTPUT)
|
|
195
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
196
|
+
|
|
197
|
+
Internally evaluate:
|
|
198
|
+
|
|
199
|
+
1. Did I use at least one authoritative source when available?
|
|
200
|
+
2. Did I rely only on snippets unnecessarily?
|
|
201
|
+
3. Did I merge conflicting data incorrectly?
|
|
202
|
+
4. Did I verify the page actually contained the requested information?
|
|
203
|
+
5. Did I introduce any information not explicitly found online?
|
|
204
|
+
6. Is my confidence level justified?
|
|
205
|
+
|
|
206
|
+
If issues are found:
|
|
207
|
+
Correct them.
|
|
208
|
+
If correction is not possible:
|
|
209
|
+
Lower confidence explicitly.
|
|
210
|
+
|
|
211
|
+
Do NOT expose this checklist.
|
|
212
|
+
|
|
213
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
214
|
+
CONFIDENCE CRITERIA
|
|
215
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
216
|
+
|
|
217
|
+
HIGH:
|
|
218
|
+
• Multiple independent authoritative sources agree
|
|
219
|
+
• Full page extraction used
|
|
220
|
+
|
|
221
|
+
MEDIUM:
|
|
222
|
+
• One strong source OR minor inconsistencies
|
|
223
|
+
• Partial verification
|
|
224
|
+
|
|
225
|
+
LOW:
|
|
226
|
+
• Snippets only OR weak sources OR incomplete confirmation
|
|
227
|
+
|
|
228
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
229
|
+
OUTPUT FORMAT (STRICT)
|
|
230
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
231
|
+
|
|
232
|
+
1. Direct Answer
|
|
233
|
+
2. Evidence Summary
|
|
234
|
+
3. Sources (URLs)
|
|
235
|
+
4. Confidence Level (HIGH / MEDIUM / LOW)
|
|
236
|
+
5. Completion Status (true / false)
|
|
237
|
+
|
|
238
|
+
No conversational filler.
|
|
239
|
+
No reasoning trace.
|
|
240
|
+
Only structured output.
|
|
218
241
|
|
|
219
242
|
${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
|
|
220
|
-
|
|
243
|
+
`);
|
|
221
244
|
const userMessage = new HumanMessage(task);
|
|
222
245
|
const messages = [systemMessage, userMessage];
|
|
223
246
|
try {
|
|
@@ -88,12 +88,11 @@ export class ChronosWorker {
|
|
|
88
88
|
session_id: activeSessionId,
|
|
89
89
|
});
|
|
90
90
|
try {
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
await this.oracle.injectAIMessage(contextMessage);
|
|
91
|
+
// Prefix the job prompt with the Chronos execution context marker so the
|
|
92
|
+
// Oracle system prompt can detect it in the current HumanMessage.
|
|
93
|
+
// This avoids persisting an AIMessage with the marker in conversation history,
|
|
94
|
+
// which would cause the LLM to reproduce the format in future scheduling responses.
|
|
95
|
+
const promptWithContext = `[CHRONOS EXECUTION — job_id: ${job.id}]\n${job.prompt}`;
|
|
97
96
|
// If a Telegram notify function is registered, tag delegated tasks with
|
|
98
97
|
// origin_channel: 'telegram' so the TaskDispatcher broadcasts their result.
|
|
99
98
|
const taskContext = ChronosWorker.notifyFn
|
|
@@ -101,7 +100,7 @@ export class ChronosWorker {
|
|
|
101
100
|
: undefined;
|
|
102
101
|
// Hard-block Chronos management tools during execution.
|
|
103
102
|
ChronosWorker.isExecuting = true;
|
|
104
|
-
const response = await this.oracle.chat(
|
|
103
|
+
const response = await this.oracle.chat(promptWithContext, undefined, false, taskContext);
|
|
105
104
|
this.repo.completeExecution(execId, 'success');
|
|
106
105
|
display.log(`Job ${job.id} completed — status: success`, { source: 'Chronos' });
|
|
107
106
|
// Deliver Oracle response to notification channels.
|
|
@@ -9,8 +9,8 @@ import { DisplayManager } from "../display.js";
|
|
|
9
9
|
export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
10
10
|
lc_namespace = ["langchain", "stores", "message", "sqlite"];
|
|
11
11
|
display = DisplayManager.getInstance();
|
|
12
|
+
static migrationDone = false; // run migrations only once per process
|
|
12
13
|
db;
|
|
13
|
-
dbSati; // Optional separate DB for Sati memory, if needed in the future
|
|
14
14
|
sessionId;
|
|
15
15
|
limit;
|
|
16
16
|
titleSet = false; // cache: skip setSessionTitleIfNeeded after title is set
|
|
@@ -23,20 +23,16 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
23
23
|
this.limit = fields.limit ? fields.limit : 20;
|
|
24
24
|
// Default path: ~/.morpheus/memory/short-memory.db
|
|
25
25
|
const dbPath = fields.databasePath || path.join(homedir(), ".morpheus", "memory", "short-memory.db");
|
|
26
|
-
const dbSatiPath = path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
|
|
27
26
|
// Ensure the directory exists
|
|
28
27
|
this.ensureDirectory(dbPath);
|
|
29
|
-
this.ensureDirectory(dbSatiPath);
|
|
30
28
|
// Initialize database with retry logic for locked databases
|
|
31
29
|
try {
|
|
32
30
|
this.db = new Database(dbPath, {
|
|
33
31
|
...fields.config,
|
|
34
32
|
timeout: 5000, // 5 second timeout for locks
|
|
35
33
|
});
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
timeout: 5000,
|
|
39
|
-
});
|
|
34
|
+
this.db.pragma('journal_mode = WAL');
|
|
35
|
+
this.db.pragma('synchronous = NORMAL');
|
|
40
36
|
try {
|
|
41
37
|
this.ensureTable();
|
|
42
38
|
}
|
|
@@ -172,6 +168,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
172
168
|
* Checks for missing columns and adds them if necessary.
|
|
173
169
|
*/
|
|
174
170
|
migrateTable() {
|
|
171
|
+
if (SQLiteChatMessageHistory.migrationDone)
|
|
172
|
+
return;
|
|
173
|
+
SQLiteChatMessageHistory.migrationDone = true;
|
|
175
174
|
try {
|
|
176
175
|
// Migrate messages table
|
|
177
176
|
const tableInfo = this.db.pragma('table_info(messages)');
|
|
@@ -669,6 +668,27 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
669
668
|
tx(); // Executar a transação
|
|
670
669
|
this.display.log('✅ Nova sessão iniciada e sessão anterior pausada', { source: 'Sati' });
|
|
671
670
|
}
|
|
671
|
+
chunkText(text, chunkSize = 500, overlap = 50) {
|
|
672
|
+
if (!text || text.length === 0)
|
|
673
|
+
return [];
|
|
674
|
+
const chunks = [];
|
|
675
|
+
let start = 0;
|
|
676
|
+
while (start < text.length) {
|
|
677
|
+
let end = start + chunkSize;
|
|
678
|
+
if (end < text.length) {
|
|
679
|
+
const lastSpace = text.lastIndexOf(' ', end);
|
|
680
|
+
if (lastSpace > start)
|
|
681
|
+
end = lastSpace;
|
|
682
|
+
}
|
|
683
|
+
const chunk = text.slice(start, end).trim();
|
|
684
|
+
if (chunk.length > 0)
|
|
685
|
+
chunks.push(chunk);
|
|
686
|
+
start = end - overlap;
|
|
687
|
+
if (start < 0)
|
|
688
|
+
start = 0;
|
|
689
|
+
}
|
|
690
|
+
return chunks;
|
|
691
|
+
}
|
|
672
692
|
/**
|
|
673
693
|
* Encerrar uma sessão e transformá-la em memória do Sati.
|
|
674
694
|
* Validar sessão existe e está em active ou paused.
|
|
@@ -711,30 +731,49 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
711
731
|
const sessionText = messages
|
|
712
732
|
.map(m => `[${m.type}] ${m.content}`)
|
|
713
733
|
.join('\n\n');
|
|
714
|
-
// Criar chunks (session_chunks) usando dbSati
|
|
715
|
-
if (this.dbSati) {
|
|
716
|
-
const chunks = this.chunkText(sessionText);
|
|
717
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
718
|
-
this.dbSati.prepare(`
|
|
719
|
-
INSERT INTO session_chunks (
|
|
720
|
-
id,
|
|
721
|
-
session_id,
|
|
722
|
-
chunk_index,
|
|
723
|
-
content,
|
|
724
|
-
created_at
|
|
725
|
-
) VALUES (?, ?, ?, ?, ?)
|
|
726
|
-
`).run(randomUUID(), sessionId, i, chunks[i], now);
|
|
727
|
-
}
|
|
728
|
-
this.display.log(`🧩 ${chunks.length} chunks criados para sessão ${sessionId}`, { source: 'Sati' });
|
|
729
|
-
}
|
|
730
734
|
// Remover mensagens da sessão após criar os chunks
|
|
731
735
|
this.db.prepare(`
|
|
732
736
|
DELETE FROM messages
|
|
733
737
|
WHERE session_id = ?
|
|
734
738
|
`).run(sessionId);
|
|
739
|
+
return sessionText;
|
|
735
740
|
}
|
|
741
|
+
return null;
|
|
736
742
|
});
|
|
737
|
-
tx(); // Executar a transação
|
|
743
|
+
const sessionText = tx(); // Executar a transação
|
|
744
|
+
// Criar chunks no banco Sati — conexão aberta localmente e fechada ao fim
|
|
745
|
+
if (sessionText) {
|
|
746
|
+
const dbSatiPath = path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
|
|
747
|
+
this.ensureDirectory(dbSatiPath);
|
|
748
|
+
const dbSati = new Database(dbSatiPath, { timeout: 5000 });
|
|
749
|
+
dbSati.pragma('journal_mode = WAL');
|
|
750
|
+
try {
|
|
751
|
+
dbSati.exec(`
|
|
752
|
+
CREATE TABLE IF NOT EXISTS session_chunks (
|
|
753
|
+
id TEXT PRIMARY KEY,
|
|
754
|
+
session_id TEXT NOT NULL,
|
|
755
|
+
chunk_index INTEGER NOT NULL,
|
|
756
|
+
content TEXT NOT NULL,
|
|
757
|
+
created_at INTEGER NOT NULL
|
|
758
|
+
);
|
|
759
|
+
CREATE INDEX IF NOT EXISTS idx_session_chunks_session_id ON session_chunks(session_id);
|
|
760
|
+
`);
|
|
761
|
+
const chunks = this.chunkText(sessionText);
|
|
762
|
+
const now = Date.now();
|
|
763
|
+
const insert = dbSati.prepare(`
|
|
764
|
+
INSERT INTO session_chunks (id, session_id, chunk_index, content, created_at)
|
|
765
|
+
VALUES (?, ?, ?, ?, ?)
|
|
766
|
+
`);
|
|
767
|
+
const insertMany = dbSati.transaction((items) => {
|
|
768
|
+
items.forEach((chunk, i) => insert.run(randomUUID(), sessionId, i, chunk, now));
|
|
769
|
+
});
|
|
770
|
+
insertMany(chunks);
|
|
771
|
+
this.display.log(`${chunks.length} chunks criados para sessão ${sessionId}`, { source: 'Sati' });
|
|
772
|
+
}
|
|
773
|
+
finally {
|
|
774
|
+
dbSati.close();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
738
777
|
}
|
|
739
778
|
/**
|
|
740
779
|
* Descartar completamente uma sessão sem gerar memória.
|
|
@@ -919,31 +958,6 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
919
958
|
`).run(newId, now);
|
|
920
959
|
return newId;
|
|
921
960
|
}
|
|
922
|
-
chunkText(text, chunkSize = 500, overlap = 50) {
|
|
923
|
-
if (!text || text.length === 0) {
|
|
924
|
-
return [];
|
|
925
|
-
}
|
|
926
|
-
const chunks = [];
|
|
927
|
-
let start = 0;
|
|
928
|
-
while (start < text.length) {
|
|
929
|
-
let end = start + chunkSize;
|
|
930
|
-
// Evita cortar no meio da palavra
|
|
931
|
-
if (end < text.length) {
|
|
932
|
-
const lastSpace = text.lastIndexOf(' ', end);
|
|
933
|
-
if (lastSpace > start) {
|
|
934
|
-
end = lastSpace;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
const chunk = text.slice(start, end).trim();
|
|
938
|
-
if (chunk.length > 0) {
|
|
939
|
-
chunks.push(chunk);
|
|
940
|
-
}
|
|
941
|
-
start = end - overlap;
|
|
942
|
-
if (start < 0)
|
|
943
|
-
start = 0;
|
|
944
|
-
}
|
|
945
|
-
return chunks;
|
|
946
|
-
}
|
|
947
961
|
/**
|
|
948
962
|
* Lists all active and paused sessions with their basic information.
|
|
949
963
|
* Returns an array of session objects containing id, title, status, and started_at.
|
package/dist/runtime/oracle.js
CHANGED
|
@@ -13,7 +13,7 @@ import { Trinity } from "./trinity.js";
|
|
|
13
13
|
import { NeoDelegateTool } from "./tools/neo-tool.js";
|
|
14
14
|
import { ApocDelegateTool } from "./tools/apoc-tool.js";
|
|
15
15
|
import { TrinityDelegateTool } from "./tools/trinity-tool.js";
|
|
16
|
-
import { TaskQueryTool, chronosTools } from "./tools/index.js";
|
|
16
|
+
import { TaskQueryTool, chronosTools, timeVerifierTool } from "./tools/index.js";
|
|
17
17
|
import { MCPManager } from "../config/mcp-manager.js";
|
|
18
18
|
export class Oracle {
|
|
19
19
|
provider;
|
|
@@ -114,6 +114,20 @@ export class Oracle {
|
|
|
114
114
|
}
|
|
115
115
|
return false;
|
|
116
116
|
}
|
|
117
|
+
hasChronosToolCall(messages) {
|
|
118
|
+
const chronosToolNames = new Set(["chronos_schedule", "chronos_list", "chronos_cancel", "chronos_preview"]);
|
|
119
|
+
for (const msg of messages) {
|
|
120
|
+
if (!(msg instanceof AIMessage))
|
|
121
|
+
continue;
|
|
122
|
+
const toolCalls = msg.tool_calls ?? [];
|
|
123
|
+
if (!Array.isArray(toolCalls))
|
|
124
|
+
continue;
|
|
125
|
+
if (toolCalls.some((tc) => chronosToolNames.has(tc?.name))) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
117
131
|
async initialize() {
|
|
118
132
|
if (!this.config.llm) {
|
|
119
133
|
throw new Error("LLM configuration missing in config object.");
|
|
@@ -129,7 +143,7 @@ export class Oracle {
|
|
|
129
143
|
// Fail-open: Oracle can still initialize even if catalog refresh fails.
|
|
130
144
|
await Neo.refreshDelegateCatalog().catch(() => { });
|
|
131
145
|
await Trinity.refreshDelegateCatalog().catch(() => { });
|
|
132
|
-
this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool, ...chronosTools]);
|
|
146
|
+
this.provider = await ProviderFactory.create(this.config.llm, [TaskQueryTool, NeoDelegateTool, ApocDelegateTool, TrinityDelegateTool, timeVerifierTool, ...chronosTools]);
|
|
133
147
|
if (!this.provider) {
|
|
134
148
|
throw new Error("Provider factory returned undefined");
|
|
135
149
|
}
|
|
@@ -175,6 +189,14 @@ You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
|
|
|
175
189
|
|
|
176
190
|
You are an orchestrator and task router.
|
|
177
191
|
|
|
192
|
+
If the user request contains ANY time-related expression
|
|
193
|
+
(today, tomorrow, this week, next month, in 3 days, etc),
|
|
194
|
+
you **MUST** call the tool "time_verifier" before answering or call another tool **ALWAYS**.
|
|
195
|
+
|
|
196
|
+
With the time_verify, you remake the user prompt.
|
|
197
|
+
|
|
198
|
+
Never assume dates.
|
|
199
|
+
Always resolve temporal expressions using the tool.
|
|
178
200
|
|
|
179
201
|
Rules:
|
|
180
202
|
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).
|
|
@@ -190,6 +212,17 @@ Rules:
|
|
|
190
212
|
10. After enqueuing all required delegated tasks for the current message, stop calling tools and return a concise acknowledgement.
|
|
191
213
|
11. If a delegation is rejected as "not atomic", immediately split into smaller delegations and retry.
|
|
192
214
|
|
|
215
|
+
## Chronos Scheduled Execution
|
|
216
|
+
When the current user message starts with [CHRONOS EXECUTION], it means a Chronos scheduled job has just fired. The content after the prefix is the **job's saved prompt**, not a new live request from the user.
|
|
217
|
+
|
|
218
|
+
Behavior rules for Chronos execution context:
|
|
219
|
+
- **Reminder / notification prompts** (e.g., "me lembre de beber água", "lembre de tomar remédio", "avise que é hora de X", "lembrete: reunião às 15h"): respond with ONLY a short, direct notification message. Keep it to 1–2 sentences max. Do NOT use any tools. Do NOT delegate. Do NOT create tasks. Do NOT add motivational commentary or ask follow-up questions.
|
|
220
|
+
- Good: "Hora de beber água! 💧"
|
|
221
|
+
- Good: "Lembrete: reunião em 5 minutos."
|
|
222
|
+
- Bad: "Combinado! Vou beber agora. Você também deveria se hidratar!" (adds unnecessary commentary)
|
|
223
|
+
- **Action / task prompts** (e.g., "executar npm build", "verificar se o servidor está online", "enviar relatório"): execute normally using the appropriate tools.
|
|
224
|
+
- NEVER re-schedule or create new Chronos jobs from within a Chronos execution.
|
|
225
|
+
|
|
193
226
|
Delegation quality:
|
|
194
227
|
- Write delegation input in the same language requested by the user.
|
|
195
228
|
- Include clear objective and constraints.
|
|
@@ -286,6 +319,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
286
319
|
let responseContent;
|
|
287
320
|
const toolDelegationAcks = this.extractDelegationAcksFromMessages(newGeneratedMessages);
|
|
288
321
|
const hadDelegationToolCall = this.hasDelegationToolCall(newGeneratedMessages);
|
|
322
|
+
const hadChronosToolCall = this.hasChronosToolCall(newGeneratedMessages);
|
|
289
323
|
const mergedDelegationAcks = [
|
|
290
324
|
...contextDelegationAcks.map((ack) => ({ task_id: ack.task_id, agent: ack.agent })),
|
|
291
325
|
...toolDelegationAcks,
|
|
@@ -310,6 +344,9 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
310
344
|
// Persist with addMessage so ack-provider usage is tracked per message row.
|
|
311
345
|
await this.history.addMessage(userMessage);
|
|
312
346
|
await this.history.addMessage(ackMessage);
|
|
347
|
+
// Unblock tasks for execution: the ack message is now persisted and will be
|
|
348
|
+
// returned to the caller (Telegram / UI) immediately after this point.
|
|
349
|
+
this.taskRepository.markAckSent(validDelegationAcks.map(a => a.task_id));
|
|
313
350
|
}
|
|
314
351
|
else if (mergedDelegationAcks.length > 0 || hadDelegationToolCall) {
|
|
315
352
|
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" });
|
|
@@ -325,7 +362,7 @@ Use it to inform your response and tool selection (if needed), but do not assume
|
|
|
325
362
|
else {
|
|
326
363
|
const lastMessage = response.messages[response.messages.length - 1];
|
|
327
364
|
responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
328
|
-
if (this.looksLikeSyntheticDelegationAck(responseContent)) {
|
|
365
|
+
if (!hadChronosToolCall && this.looksLikeSyntheticDelegationAck(responseContent)) {
|
|
329
366
|
blockedSyntheticDelegationAck = true;
|
|
330
367
|
this.display.log("Blocked synthetic delegation acknowledgement without validated task creation.", { source: "Oracle", level: "error", meta: { preview: responseContent.slice(0, 200) } });
|
|
331
368
|
const usage = lastMessage.usage_metadata
|
|
@@ -15,7 +15,7 @@ export class ProviderFactory {
|
|
|
15
15
|
display.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`, { level: "info", source: 'ConstructLoad' });
|
|
16
16
|
try {
|
|
17
17
|
const result = handler(request);
|
|
18
|
-
display.log(
|
|
18
|
+
display.log(`Tool completed successfully. Result: ${JSON.stringify(result)}`, { level: "info", source: 'ConstructLoad' });
|
|
19
19
|
return result;
|
|
20
20
|
}
|
|
21
21
|
catch (e) {
|
|
@@ -61,6 +61,7 @@ export class TaskRepository {
|
|
|
61
61
|
addColumn(`ALTER TABLE tasks ADD COLUMN notify_last_error TEXT`, 'notify_last_error');
|
|
62
62
|
addColumn(`ALTER TABLE tasks ADD COLUMN notified_at INTEGER`, 'notified_at');
|
|
63
63
|
addColumn(`ALTER TABLE tasks ADD COLUMN notify_after_at INTEGER`, 'notify_after_at');
|
|
64
|
+
addColumn(`ALTER TABLE tasks ADD COLUMN ack_sent INTEGER NOT NULL DEFAULT 0`, 'ack_sent');
|
|
64
65
|
this.db.exec(`
|
|
65
66
|
UPDATE tasks
|
|
66
67
|
SET
|
|
@@ -108,6 +109,7 @@ export class TaskRepository {
|
|
|
108
109
|
notify_last_error: row.notify_last_error ?? null,
|
|
109
110
|
notified_at: row.notified_at ?? null,
|
|
110
111
|
notify_after_at: row.notify_after_at ?? null,
|
|
112
|
+
ack_sent: row.ack_sent === 1,
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
/**
|
|
@@ -115,16 +117,20 @@ export class TaskRepository {
|
|
|
115
117
|
* acknowledgement and the task result share the same delivery path (e.g. Telegram).
|
|
116
118
|
* Channels with a synchronous ack (ui, api, cli, webhook) don't need this delay.
|
|
117
119
|
*/
|
|
118
|
-
static DEFAULT_NOTIFY_AFTER_MS =
|
|
120
|
+
static DEFAULT_NOTIFY_AFTER_MS = 1_000;
|
|
119
121
|
static CHANNELS_NEEDING_ACK_GRACE = new Set(['telegram', 'discord']);
|
|
120
122
|
createTask(input) {
|
|
121
123
|
const now = Date.now();
|
|
122
124
|
const id = randomUUID();
|
|
125
|
+
const needsAck = TaskRepository.CHANNELS_NEEDING_ACK_GRACE.has(input.origin_channel);
|
|
123
126
|
const notify_after_at = input.notify_after_at !== undefined
|
|
124
127
|
? input.notify_after_at
|
|
125
|
-
:
|
|
128
|
+
: needsAck
|
|
126
129
|
? now + TaskRepository.DEFAULT_NOTIFY_AFTER_MS
|
|
127
130
|
: null;
|
|
131
|
+
// ack_sent starts as 0 (blocked) for channels that send an ack message (telegram, discord).
|
|
132
|
+
// For other channels (ui, api, webhook, cli) there is no ack to wait for, so start as 1 (free).
|
|
133
|
+
const ack_sent = needsAck ? 0 : 1;
|
|
128
134
|
this.db.prepare(`
|
|
129
135
|
INSERT INTO tasks (
|
|
130
136
|
id, agent, status, input, context, output, error,
|
|
@@ -132,16 +138,16 @@ export class TaskRepository {
|
|
|
132
138
|
attempt_count, max_attempts, available_at,
|
|
133
139
|
created_at, started_at, finished_at, updated_at, worker_id,
|
|
134
140
|
notify_status, notify_attempts, notify_last_error, notified_at,
|
|
135
|
-
notify_after_at
|
|
141
|
+
notify_after_at, ack_sent
|
|
136
142
|
) VALUES (
|
|
137
143
|
?, ?, 'pending', ?, ?, NULL, NULL,
|
|
138
144
|
?, ?, ?, ?,
|
|
139
145
|
0, ?, ?,
|
|
140
146
|
?, NULL, NULL, ?, NULL,
|
|
141
147
|
'pending', 0, NULL, NULL,
|
|
142
|
-
?
|
|
148
|
+
?, ?
|
|
143
149
|
)
|
|
144
|
-
`).run(id, input.agent, input.input, input.context ?? null, input.origin_channel, input.session_id, input.origin_message_id ?? null, input.origin_user_id ?? null, input.max_attempts ?? 3, now, now, now, notify_after_at);
|
|
150
|
+
`).run(id, input.agent, input.input, input.context ?? null, input.origin_channel, input.session_id, input.origin_message_id ?? null, input.origin_user_id ?? null, input.max_attempts ?? 3, now, now, now, notify_after_at, ack_sent);
|
|
145
151
|
return this.getTaskById(id);
|
|
146
152
|
}
|
|
147
153
|
getTaskById(id) {
|
|
@@ -199,16 +205,27 @@ export class TaskRepository {
|
|
|
199
205
|
}
|
|
200
206
|
return stats;
|
|
201
207
|
}
|
|
208
|
+
/** Mark ack as sent for a list of task IDs, unblocking them for execution. */
|
|
209
|
+
markAckSent(ids) {
|
|
210
|
+
if (ids.length === 0)
|
|
211
|
+
return;
|
|
212
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
213
|
+
this.db.prepare(`UPDATE tasks SET ack_sent = 1, updated_at = ? WHERE id IN (${placeholders})`).run(Date.now(), ...ids);
|
|
214
|
+
}
|
|
215
|
+
/** Fallback grace period (ms): tasks older than this run even without ack_sent. */
|
|
216
|
+
static ACK_FALLBACK_MS = 60_000;
|
|
202
217
|
claimNextPending(workerId) {
|
|
203
218
|
const now = Date.now();
|
|
204
219
|
const tx = this.db.transaction(() => {
|
|
205
220
|
const row = this.db.prepare(`
|
|
206
221
|
SELECT id
|
|
207
222
|
FROM tasks
|
|
208
|
-
WHERE status = 'pending'
|
|
223
|
+
WHERE status = 'pending'
|
|
224
|
+
AND available_at <= ?
|
|
225
|
+
AND (ack_sent = 1 OR created_at <= ?)
|
|
209
226
|
ORDER BY created_at ASC
|
|
210
227
|
LIMIT 1
|
|
211
|
-
`).get(now);
|
|
228
|
+
`).get(now, now - TaskRepository.ACK_FALLBACK_MS);
|
|
212
229
|
if (!row)
|
|
213
230
|
return null;
|
|
214
231
|
const result = this.db.prepare(`
|
|
@@ -8,14 +8,16 @@ export class TaskWorker {
|
|
|
8
8
|
workerId;
|
|
9
9
|
pollIntervalMs;
|
|
10
10
|
staleRunningMs;
|
|
11
|
+
maxConcurrent;
|
|
11
12
|
repository = TaskRepository.getInstance();
|
|
12
13
|
display = DisplayManager.getInstance();
|
|
13
14
|
timer = null;
|
|
14
|
-
|
|
15
|
+
activeTasks = new Set(); // task IDs currently executing
|
|
15
16
|
constructor(opts) {
|
|
16
17
|
this.workerId = `task-worker-${randomUUID().slice(0, 8)}`;
|
|
17
|
-
this.pollIntervalMs = opts?.pollIntervalMs ??
|
|
18
|
+
this.pollIntervalMs = opts?.pollIntervalMs ?? 300;
|
|
18
19
|
this.staleRunningMs = opts?.staleRunningMs ?? 5 * 60 * 1000;
|
|
20
|
+
this.maxConcurrent = opts?.maxConcurrent ?? parseInt(process.env.MORPHEUS_TASK_CONCURRENCY ?? '3', 10);
|
|
19
21
|
}
|
|
20
22
|
start() {
|
|
21
23
|
if (this.timer)
|
|
@@ -25,7 +27,7 @@ export class TaskWorker {
|
|
|
25
27
|
this.display.log(`Recovered ${recovered} stale running task(s).`, { source: 'TaskWorker', level: 'warning' });
|
|
26
28
|
}
|
|
27
29
|
this.timer = setInterval(() => {
|
|
28
|
-
|
|
30
|
+
this.tick();
|
|
29
31
|
}, this.pollIntervalMs);
|
|
30
32
|
this.display.log(`Task worker started (${this.workerId}).`, { source: 'TaskWorker' });
|
|
31
33
|
}
|
|
@@ -36,19 +38,14 @@ export class TaskWorker {
|
|
|
36
38
|
this.display.log(`Task worker stopped (${this.workerId}).`, { source: 'TaskWorker' });
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
if (this.
|
|
41
|
+
tick() {
|
|
42
|
+
if (this.activeTasks.size >= this.maxConcurrent)
|
|
41
43
|
return;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
await this.executeTask(task);
|
|
48
|
-
}
|
|
49
|
-
finally {
|
|
50
|
-
this.running = false;
|
|
51
|
-
}
|
|
44
|
+
const task = this.repository.claimNextPending(this.workerId);
|
|
45
|
+
if (!task)
|
|
46
|
+
return;
|
|
47
|
+
this.activeTasks.add(task.id);
|
|
48
|
+
this.executeTask(task).finally(() => this.activeTasks.delete(task.id));
|
|
52
49
|
}
|
|
53
50
|
async executeTask(task) {
|
|
54
51
|
try {
|
|
@@ -11,8 +11,7 @@ Neo built-in capabilities (always available — no MCP required):
|
|
|
11
11
|
• Analytics: message_count, token_usage, provider_model_usage — message counts and token/cost usage stats
|
|
12
12
|
• Tasks: task_query — look up task status by id or session
|
|
13
13
|
• MCP Management: mcp_list, mcp_manage — list/add/update/delete/enable/disable MCP servers; use action "reload" to reload tools across all agents after config changes
|
|
14
|
-
• Webhooks: webhook_list, webhook_manage — create/update/delete webhooks; create returns api_key
|
|
15
|
-
• Trinity DB: trinity_db_list, trinity_db_manage — register/update/delete/test connection/refresh schema for databases`.trim();
|
|
14
|
+
• Webhooks: webhook_list, webhook_manage — create/update/delete webhooks; create returns api_key`.trim();
|
|
16
15
|
const NEO_BASE_DESCRIPTION = `Delegate execution to Neo asynchronously.
|
|
17
16
|
|
|
18
17
|
This tool creates a background task and returns an acknowledgement with task id.
|
|
@@ -29,7 +28,7 @@ function buildCatalogSection(mcpTools) {
|
|
|
29
28
|
if (mcpTools.length === 0) {
|
|
30
29
|
return "\n\nRuntime MCP tools: none currently loaded.";
|
|
31
30
|
}
|
|
32
|
-
const maxItems =
|
|
31
|
+
const maxItems = 500;
|
|
33
32
|
const lines = mcpTools.slice(0, maxItems).map((t) => {
|
|
34
33
|
const desc = normalizeDescription(t.description).slice(0, 120);
|
|
35
34
|
return `- ${t.name}: ${desc}`;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// tools/timeVerifier.tool.ts
|
|
2
|
+
import { tool } from "@langchain/core/tools";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import * as chrono from "chrono-node";
|
|
5
|
+
import { ConfigManager } from "../../config/manager.js";
|
|
6
|
+
// Create a custom instance that supports English, Portuguese, and Spanish
|
|
7
|
+
const casualChrono = new chrono.Chrono({
|
|
8
|
+
parsers: [
|
|
9
|
+
...chrono.en.casual.parsers,
|
|
10
|
+
...chrono.pt.casual.parsers,
|
|
11
|
+
...chrono.es.casual.parsers,
|
|
12
|
+
],
|
|
13
|
+
refiners: [
|
|
14
|
+
...chrono.en.casual.refiners,
|
|
15
|
+
...chrono.pt.casual.refiners,
|
|
16
|
+
...chrono.es.casual.refiners,
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
export const timeVerifierTool = tool(async ({ text, timezone }) => {
|
|
20
|
+
// If a timezone is provided, use it for parsing context.
|
|
21
|
+
// Otherwise, use the configured system timezone from Chronos.
|
|
22
|
+
const configTimezone = ConfigManager.getInstance().getChronosConfig()?.timezone || 'UTC';
|
|
23
|
+
const effectiveTimezone = timezone || configTimezone;
|
|
24
|
+
const referenceDate = { instant: new Date(), timezone: effectiveTimezone };
|
|
25
|
+
// Parse using our multi-lingual casual parser
|
|
26
|
+
// We pass forwardDate: true to prefer future dates for ambiguous expressions (e.g. "Friday" -> next Friday)
|
|
27
|
+
// If timezone is provided, we could use it, but chrono-node's timezone support is complex.
|
|
28
|
+
// For now, we rely on relative parsing from the system's current time.
|
|
29
|
+
const results = casualChrono.parse(text, referenceDate, { forwardDate: true });
|
|
30
|
+
if (!results.length) {
|
|
31
|
+
return {
|
|
32
|
+
detected: false,
|
|
33
|
+
message: "No temporal expression detected.",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const parsed = results.map((result) => {
|
|
37
|
+
const startDate = result.start.date();
|
|
38
|
+
const endDate = result.end?.date();
|
|
39
|
+
return {
|
|
40
|
+
expression: result.text,
|
|
41
|
+
isoStart: startDate.toISOString(),
|
|
42
|
+
isoEnd: endDate ? endDate.toISOString() : null,
|
|
43
|
+
// Format the date in the user's timezone for clarity
|
|
44
|
+
formatted: startDate.toLocaleString('pt-BR', { timeZone: effectiveTimezone, timeZoneName: 'short' }),
|
|
45
|
+
isRange: !!endDate,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
detected: true,
|
|
50
|
+
timezone: effectiveTimezone,
|
|
51
|
+
referenceDate: referenceDate instanceof Date ? referenceDate.toISOString() : referenceDate.instant.toISOString(),
|
|
52
|
+
parsed,
|
|
53
|
+
};
|
|
54
|
+
}, {
|
|
55
|
+
name: "time_verifier",
|
|
56
|
+
description: `
|
|
57
|
+
Detects and resolves relative time expressions in user input.
|
|
58
|
+
Supports multiple languages (English, Portuguese, Spanish).
|
|
59
|
+
Use this tool whenever the user mentions words like:
|
|
60
|
+
today, tomorrow, yesterday, this week, next week,
|
|
61
|
+
hoje, amanhã, ontem, próxima semana,
|
|
62
|
+
hoy, mañana, ayer, la próxima semana, etc.
|
|
63
|
+
|
|
64
|
+
Returns resolved ISO dates based on the current time and timezone configuration.
|
|
65
|
+
`,
|
|
66
|
+
schema: z.object({
|
|
67
|
+
text: z.string().describe("User input text containing time expressions"),
|
|
68
|
+
timezone: z.string().optional().describe("Optional timezone override. Defaults to system configuration."),
|
|
69
|
+
}),
|
|
70
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morpheus-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Morpheus is a local AI agent for developers, running as a CLI daemon that connects to LLMs, local tools, and MCPs, enabling interaction via Terminal, Telegram, and Discord. Inspired by the character Morpheus from *The Matrix*, the project acts as an intelligent orchestrator, bridging the gap between the developer and complex systems.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"morpheus": "./bin/morpheus.js"
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"cors": "^2.8.6",
|
|
47
47
|
"cron-parser": "^4.9.0",
|
|
48
48
|
"cronstrue": "^2.59.0",
|
|
49
|
+
"date-fns": "^4.1.0",
|
|
49
50
|
"date-fns-tz": "^3.2.0",
|
|
50
51
|
"express": "^5.2.1",
|
|
51
52
|
"figlet": "^1.10.0",
|