feat: add controlled Telegram terminal agent
This commit is contained in:
103
lib/agent/terminal-tools.mjs
Normal file
103
lib/agent/terminal-tools.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
import { TerminalToolRegistry } from './terminal-agent.mjs';
|
||||
|
||||
export function createTerminalToolRegistry({
|
||||
getData,
|
||||
getHealth,
|
||||
getDelta,
|
||||
buildBrief,
|
||||
intelligenceStore,
|
||||
triggerSweep,
|
||||
isSweepInProgress,
|
||||
telegramAlerter,
|
||||
} = {}) {
|
||||
const dataFor = runtime => runtime?.data || getData?.() || null;
|
||||
const deltaFor = runtime => runtime?.delta || getDelta?.() || null;
|
||||
return new TerminalToolRegistry([
|
||||
tool('get_system_status', 'Get server, sweep, LLM, Telegram, source, and memory health.', {}, async () => getHealth?.() || {}),
|
||||
tool('get_latest_brief', 'Build the latest operator intelligence brief.', {}, async (_args, runtime) => {
|
||||
const data = dataFor(runtime);
|
||||
return { brief: data && buildBrief ? buildBrief(data) : 'No completed sweep.' };
|
||||
}),
|
||||
tool('get_sweep_delta', 'Inspect changes, escalations, de-escalations, and direction from the latest sweep.', {}, async (_args, runtime) => compactDelta(deltaFor(runtime))),
|
||||
tool('get_market_snapshot', 'Get key rates, volatility, energy, metals, and current generated ideas.', {}, async (_args, runtime) => compactMarkets(dataFor(runtime))),
|
||||
tool('get_source_health', 'Inspect healthy, degraded, or failed sources. Optional arguments: status, name, limit.', { status: 'string', name: 'string', limit: 'number' }, async (args, runtime) => {
|
||||
const data = dataFor(runtime);
|
||||
const status = clean(args.status, 30).toLowerCase();
|
||||
const name = clean(args.name, 80).toLowerCase();
|
||||
const limit = bounded(args.limit, 1, 25, 12);
|
||||
return (data?.sourceHealth || data?.health || [])
|
||||
.filter(item => !status || String(item.status || '').toLowerCase() === status)
|
||||
.filter(item => !name || String(item.name || item.n || '').toLowerCase().includes(name))
|
||||
.slice(0, limit)
|
||||
.map(item => ({ name: item.name || item.n, status: item.status, ms: item.ms, error: clean(item.error || item.message, 240) || null }));
|
||||
}),
|
||||
tool('get_evidence', 'Search recent news, feed items, and urgent OSINT. Arguments: query, limit.', { query: 'string', limit: 'number' }, async (args, runtime) => {
|
||||
const data = dataFor(runtime);
|
||||
const query = clean(args.query, 120).toLowerCase();
|
||||
const limit = bounded(args.limit, 1, 20, 8);
|
||||
const rows = [
|
||||
...(data?.news || []),
|
||||
...(data?.newsFeed || []),
|
||||
...(data?.tg?.urgent || []).map(item => ({ ...item, title: item.text, source: item.source || 'Telegram OSINT' })),
|
||||
];
|
||||
return rows.filter(item => !query || `${item.headline || item.title || item.text || ''} ${item.source || ''}`.toLowerCase().includes(query))
|
||||
.slice(0, limit)
|
||||
.map(item => ({ title: clean(item.headline || item.title || item.text, 400), source: clean(item.source, 100), url: clean(item.url, 500) || null, timestamp: item.timestamp || item.date || null }));
|
||||
}),
|
||||
tool('search_memory', 'Search persisted cross-sweep events. Arguments: query, limit.', { query: 'string', limit: 'number' }, async args => intelligenceStore?.queryMemory({ q: clean(args.query, 120), limit: bounded(args.limit, 1, 25, 8) }) || { available: false }),
|
||||
tool('list_predictions', 'List persisted predictions and their current outcome states. Arguments: state, limit.', { state: 'string', limit: 'number' }, async args => intelligenceStore?.listPredictions({ state: clean(args.state, 30) || null, limit: bounded(args.limit, 1, 25, 8) }) || { available: false }),
|
||||
tool('get_scenarios', 'Inspect current scenario watchlist states and confidence.', {}, async (_args, runtime) => {
|
||||
const scenarios = dataFor(runtime)?.scenarios || {};
|
||||
return { summary: scenarios.summary || null, items: (scenarios.items || scenarios.scenarios || []).slice(0, 20), changed: (scenarios.changed || []).slice(0, 10) };
|
||||
}),
|
||||
tool('get_trade_ideas', 'Inspect current LLM-generated ideas. Optional argument: ticker.', { ticker: 'string' }, async (args, runtime) => {
|
||||
const ticker = clean(args.ticker, 30).toLowerCase();
|
||||
return (dataFor(runtime)?.ideas || []).filter(item => !ticker || String(item.ticker || '').toLowerCase().includes(ticker)).slice(0, 10);
|
||||
}),
|
||||
tool('trigger_sweep', 'Start a new full intelligence sweep.', {}, async (_args, runtime) => {
|
||||
if (!runtime.confirmed) throw new Error('Operator confirmation required');
|
||||
if (isSweepInProgress?.()) return { accepted: false, status: 'already_running' };
|
||||
triggerSweep?.();
|
||||
return { accepted: true, status: 'started' };
|
||||
}, true),
|
||||
tool('mute_alerts', 'Mute proactive Telegram alerts for a bounded number of hours.', { hours: 'number' }, async (args, runtime) => {
|
||||
if (!runtime.confirmed) throw new Error('Operator confirmation required');
|
||||
const hours = Math.max(0.25, Math.min(24, Number(args.hours) || 1));
|
||||
telegramAlerter?.muteAlerts(hours);
|
||||
return { muted: true, hours };
|
||||
}, true),
|
||||
tool('unmute_alerts', 'Resume proactive Telegram alerts.', {}, async (_args, runtime) => {
|
||||
if (!runtime.confirmed) throw new Error('Operator confirmation required');
|
||||
telegramAlerter?.unmuteAlerts();
|
||||
return { muted: false };
|
||||
}, true),
|
||||
]);
|
||||
}
|
||||
|
||||
function tool(name, description, parameters, handler, mutating = false) {
|
||||
return { name, description, parameters, handler, mutating };
|
||||
}
|
||||
|
||||
function compactDelta(delta) {
|
||||
return {
|
||||
summary: delta?.summary || null,
|
||||
new: (delta?.signals?.new || []).slice(0, 15),
|
||||
escalated: (delta?.signals?.escalated || []).slice(0, 15),
|
||||
deescalated: (delta?.signals?.deescalated || []).slice(0, 15),
|
||||
};
|
||||
}
|
||||
|
||||
function compactMarkets(data) {
|
||||
if (!data) return { available: false };
|
||||
const fred = Object.fromEntries((data.fred || []).filter(item => ['VIXCLS', 'DFF', 'DGS10', 'DGS2', 'T10Y2Y', 'BAMLH0A0HYM2'].includes(item.id)).map(item => [item.id, item.value]));
|
||||
return { available: true, generatedAt: data.meta?.generatedAt || data.meta?.timestamp, fred, energy: data.energy || null, metals: data.metals || null, ideasSource: data.ideasSource, ideas: (data.ideas || []).slice(0, 8) };
|
||||
}
|
||||
|
||||
function clean(value, maxLength) {
|
||||
return String(value || '').replace(/[\u0000-\u001f]/g, ' ').trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function bounded(value, min, max, fallback) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? Math.max(min, Math.min(max, parsed)) : fallback;
|
||||
}
|
||||
Reference in New Issue
Block a user