feat: add controlled Telegram terminal agent
This commit is contained in:
121
server.mjs
121
server.mjs
@@ -15,6 +15,8 @@ import { MemoryManager } from './lib/delta/index.mjs';
|
||||
import { createLLMProvider } from './lib/llm/index.mjs';
|
||||
import { generateLLMIdeas } from './lib/llm/ideas.mjs';
|
||||
import { TelegramChatAssistant, buildTelegramChatContext } from './lib/llm/telegram-chat.mjs';
|
||||
import { TerminalAgent } from './lib/agent/terminal-agent.mjs';
|
||||
import { createTerminalToolRegistry } from './lib/agent/terminal-tools.mjs';
|
||||
import { TelegramAlerter } from './lib/alerts/telegram.mjs';
|
||||
import { DiscordAlerter } from './lib/alerts/discord.mjs';
|
||||
import { getFetchMetrics } from './apis/utils/fetch.mjs';
|
||||
@@ -54,8 +56,28 @@ await intelligenceStore.init();
|
||||
const llmProvider = createLLMProvider(config.llm);
|
||||
const telegramAlerter = new TelegramAlerter(config.telegram);
|
||||
const discordAlerter = new DiscordAlerter(config.discord || {});
|
||||
const terminalToolRegistry = createTerminalToolRegistry({
|
||||
getData: () => currentData,
|
||||
getHealth: () => buildHealth(),
|
||||
getDelta: () => memory.getLastDelta(),
|
||||
buildBrief,
|
||||
intelligenceStore,
|
||||
triggerSweep: () => runSweepCycle().catch(error => console.error('[Agent] Confirmed sweep failed:', error.message)),
|
||||
isSweepInProgress: () => sweepInProgress,
|
||||
telegramAlerter,
|
||||
});
|
||||
const terminalAgent = new TerminalAgent({
|
||||
provider: llmProvider,
|
||||
registry: terminalToolRegistry,
|
||||
maxSteps: config.telegram.agentMaxSteps,
|
||||
maxTokens: config.telegram.aiMaxTokens,
|
||||
timeoutMs: config.telegram.aiTimeoutMs,
|
||||
confirmationTtlMs: config.telegram.agentConfirmationTtlSeconds * 1000,
|
||||
proactiveCooldownMs: config.telegram.agentProactiveCooldownMinutes * 60 * 1000,
|
||||
});
|
||||
const telegramChatAssistant = new TelegramChatAssistant({
|
||||
provider: llmProvider,
|
||||
agent: config.telegram.agentEnabled ? terminalAgent : null,
|
||||
getContext: () => buildTelegramChatContext(currentData, buildHealth()),
|
||||
historyMessages: config.telegram.aiHistoryMessages,
|
||||
maxInputChars: config.telegram.aiMaxInputChars,
|
||||
@@ -92,6 +114,7 @@ if (telegramAlerter.isConfigured) {
|
||||
`Sources: ${sourcesOk}/${sourcesTotal} OK${sourcesFailed > 0 ? ` (${sourcesFailed} failed)` : ''}`,
|
||||
`LLM: ${llmStatus}`,
|
||||
`AI chat: ${config.telegram.aiChatEnabled && telegramChatAssistant.isConfigured ? 'enabled' : 'disabled'}`,
|
||||
`Tool agent: ${config.telegram.agentEnabled && terminalAgent.isConfigured ? 'enabled' : 'disabled'} (${terminalAgent.listTools().length} tools)`,
|
||||
`SSE clients: ${sseClients.size}`,
|
||||
`Dashboard: http://localhost:${config.port}`,
|
||||
].join('\n');
|
||||
@@ -162,8 +185,24 @@ if (telegramAlerter.isConfigured) {
|
||||
if (!config.telegram.aiChatEnabled) {
|
||||
return { text: 'AI chat is disabled by TELEGRAM_AI_CHAT_ENABLED.', parseMode: null };
|
||||
}
|
||||
const text = await telegramChatAssistant.reply(question, { chatId: msg?.chat?.id || config.telegram.chatId });
|
||||
return { text, parseMode: null };
|
||||
const chatId = msg?.chat?.id || config.telegram.chatId;
|
||||
const result = await telegramChatAssistant.replyDetailed(question, { chatId });
|
||||
const tools = [...new Set((result.trace || []).filter(item => item.status === 'ok').map(item => item.tool))];
|
||||
const traceSuffix = tools.length ? `\n\nTools used: ${tools.join(', ')}` : '';
|
||||
if (result.pendingAction) {
|
||||
const action = result.pendingAction;
|
||||
return {
|
||||
text: `${result.answer}\nAction: ${action.tool}\nReason: ${action.rationale || 'requested by agent'}\nExpires: ${action.expiresAt}`,
|
||||
parseMode: null,
|
||||
replyMarkup: {
|
||||
inline_keyboard: [[
|
||||
{ text: 'Confirm', callback_data: `agent_confirm:${action.id}` },
|
||||
{ text: 'Cancel', callback_data: `agent_cancel:${action.id}` },
|
||||
]],
|
||||
},
|
||||
};
|
||||
}
|
||||
return { text: `${result.answer}${traceSuffix}`, parseMode: null };
|
||||
};
|
||||
|
||||
telegramAlerter.onMessage((text, msg) => answerTelegramQuestion(text, msg));
|
||||
@@ -178,6 +217,40 @@ if (telegramAlerter.isConfigured) {
|
||||
return { text: 'AI conversation history cleared.', parseMode: null };
|
||||
});
|
||||
|
||||
telegramAlerter.onCommand('/tools', async () => ({
|
||||
text: terminalAgent.listTools().map(tool => `${tool.mutating ? '[confirm]' : '[read]'} ${tool.name}: ${tool.description}`).join('\n'),
|
||||
parseMode: null,
|
||||
}));
|
||||
|
||||
telegramAlerter.onCommand('/trace', async (_args, _messageId, msg) => {
|
||||
const trace = terminalAgent.getLastTrace(msg?.chat?.id || config.telegram.chatId);
|
||||
return {
|
||||
text: trace.length
|
||||
? trace.map(item => `${item.status}: ${item.tool} (${item.durationMs}ms)${item.rationale ? ` - ${item.rationale}` : ''}`).join('\n')
|
||||
: 'No tool trace is available for this chat.',
|
||||
parseMode: null,
|
||||
};
|
||||
});
|
||||
|
||||
const confirmAgentAction = async (id, chatId) => {
|
||||
const result = await terminalAgent.confirm(id, chatId);
|
||||
return { text: result.message, parseMode: null };
|
||||
};
|
||||
const cancelAgentAction = (id, chatId) => ({
|
||||
text: terminalAgent.cancel(id, chatId) ? 'Pending action cancelled.' : 'Pending action is unknown, expired, or belongs to another chat.',
|
||||
parseMode: null,
|
||||
});
|
||||
|
||||
telegramAlerter.onCommand('/confirm', async (args, _messageId, msg) => confirmAgentAction(args.trim(), msg?.chat?.id || config.telegram.chatId));
|
||||
telegramAlerter.onCommand('/cancel', async (args, _messageId, msg) => cancelAgentAction(args.trim(), msg?.chat?.id || config.telegram.chatId));
|
||||
telegramAlerter.onCallback(async (data, query) => {
|
||||
const [operation, id] = String(data).split(':', 2);
|
||||
const chatId = query.message?.chat?.id || config.telegram.chatId;
|
||||
if (operation === 'agent_confirm') return confirmAgentAction(id, chatId);
|
||||
if (operation === 'agent_cancel') return cancelAgentAction(id, chatId);
|
||||
return { text: 'Unknown agent action.', parseMode: null };
|
||||
});
|
||||
|
||||
telegramAlerter.onCommand('/portfolio', async () => {
|
||||
return '📊 Portfolio integration requires Alpaca MCP connection.\nUse the Crucix dashboard or Claude agent for portfolio queries.';
|
||||
});
|
||||
@@ -582,6 +655,12 @@ function buildHealth() {
|
||||
historyMessages: config.telegram.aiHistoryMessages,
|
||||
maxInputChars: config.telegram.aiMaxInputChars,
|
||||
},
|
||||
telegramAgent: {
|
||||
enabled: Boolean(config.telegram.agentEnabled && terminalAgent.isConfigured),
|
||||
tools: terminalAgent.listTools().length,
|
||||
maxSteps: config.telegram.agentMaxSteps,
|
||||
proactive: Boolean(config.telegram.agentEnabled && config.telegram.agentProactiveEnabled),
|
||||
},
|
||||
discordEnabled: !!(config.discord?.botToken || config.discord?.webhookUrl),
|
||||
terminalActionsEnabled: config.terminalActionsEnabled,
|
||||
terminalActionsTokenRequired: !!config.sweepToken,
|
||||
@@ -734,9 +813,18 @@ async function runSweepCycle() {
|
||||
// 6. Alert evaluation — Telegram + Discord (LLM with rule-based fallback, multi-tier, semantic dedup)
|
||||
if (delta?.summary?.totalChanges > 0) {
|
||||
if (telegramAlerter.isConfigured) {
|
||||
telegramAlerter.evaluateAndAlert(llmProvider, delta, memory).catch(err => {
|
||||
console.error('[Crucix] Telegram alert error:', err.message);
|
||||
});
|
||||
if (config.telegram.agentEnabled && config.telegram.agentProactiveEnabled && shouldRunProactiveAgent(delta)) {
|
||||
runProactiveAgent(synthesized, delta).catch(err => {
|
||||
console.error('[Agent] Proactive analysis failed, using rule fallback:', err.message);
|
||||
telegramAlerter.evaluateAndAlert(null, delta, memory).catch(fallbackError => {
|
||||
console.error('[Crucix] Telegram alert fallback error:', fallbackError.message);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
telegramAlerter.evaluateAndAlert(llmProvider, delta, memory).catch(err => {
|
||||
console.error('[Crucix] Telegram alert error:', err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (discordAlerter.isConfigured) {
|
||||
discordAlerter.evaluateAndAlert(llmProvider, delta, memory).catch(err => {
|
||||
@@ -772,6 +860,29 @@ async function runSweepCycle() {
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRunProactiveAgent(delta) {
|
||||
return (delta?.summary?.criticalChanges || 0) > 0
|
||||
|| (delta?.summary?.totalChanges || 0) >= config.telegram.agentProactiveMinChanges;
|
||||
}
|
||||
|
||||
async function runProactiveAgent(data, delta) {
|
||||
if (telegramAlerter.getMuteStatus().muted) return false;
|
||||
const prompt = `Evaluate the latest sweep for a proactive operator notification. Cross-check material changes with source health, evidence, scenarios, memory, and predictions as needed. Do not call mutating tools. Delta summary: ${JSON.stringify(delta?.summary || {})}`;
|
||||
const result = await terminalAgent.analyzeProactively(prompt, {
|
||||
context: buildTelegramChatContext(data, buildHealth()),
|
||||
runtime: { data, delta },
|
||||
});
|
||||
if (result.pendingAction) return false;
|
||||
if (!result.notify) {
|
||||
return telegramAlerter.evaluateAndAlert(null, delta, memory);
|
||||
}
|
||||
const evidence = result.evidence?.length ? `\nEvidence:\n${result.evidence.map(item => `- ${item}`).join('\n')}` : '';
|
||||
const tools = [...new Set((result.trace || []).filter(item => item.status === 'ok').map(item => item.tool))];
|
||||
const trace = tools.length ? `\nTools: ${tools.join(', ')}` : '';
|
||||
const sent = await telegramAlerter.sendMessage(`[AGENT ${String(result.priority || 'routine').toUpperCase()}]\n${result.answer}${evidence}${trace}`, { parseMode: null });
|
||||
return sent.ok;
|
||||
}
|
||||
|
||||
// === Startup ===
|
||||
async function start() {
|
||||
const port = config.port;
|
||||
|
||||
Reference in New Issue
Block a user