From 261e55b248e51c887fb9756c4aecf6f7782be80c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:51:22 +0000 Subject: [PATCH 1/3] Initial plan From 986aa18d809eab71401ffe8ec07d81ef4ca31a40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:17:39 +0000 Subject: [PATCH 2/3] fix: address lint issues - remove any casts, unused vars Co-authored-by: Secret297-CODER-SOURCE <73541046+Secret297-CODER-SOURCE@users.noreply.github.com> --- extensions/telegram-manager/index.ts | 148 +++++++- .../telegram-manager/src/TelegramPlugin.ts | 219 ++++++++++++ .../src/agents/AgentCommunicationBus.ts | 136 ++++++++ .../src/agents/AgentManager.ts | 57 ++++ .../src/storage/TelegramStorage.ts | 136 +++++++- extensions/telegram-manager/src/types.ts | 44 ++- src/pii-guard/index.ts | 8 +- src/pii-guard/proxy.ts | 25 +- ui/src/ui/controllers/telegram-core-files.ts | 123 +++++++ ui/src/ui/controllers/telegram.ts | 145 ++++++++ ui/src/ui/views/telegram-core-files.ts | 151 +++++++++ ui/src/ui/views/telegram-missions.ts | 318 ++++++++++++++++++ 12 files changed, 1489 insertions(+), 21 deletions(-) create mode 100644 extensions/telegram-manager/src/agents/AgentCommunicationBus.ts create mode 100644 ui/src/ui/controllers/telegram-core-files.ts create mode 100644 ui/src/ui/views/telegram-core-files.ts create mode 100644 ui/src/ui/views/telegram-missions.ts diff --git a/extensions/telegram-manager/index.ts b/extensions/telegram-manager/index.ts index 181068224ade1..9d10d35bda250 100644 --- a/extensions/telegram-manager/index.ts +++ b/extensions/telegram-manager/index.ts @@ -33,6 +33,14 @@ const TELEGRAM_METHODS = [ "telegram.agent.assignTask", "telegram.agent.listTaskSessions", "telegram.agent.completeTaskSession", + "telegram.agent.getCoreFiles", + "telegram.agent.setCoreFile", + "telegram.agent.sendMessage_to_agent", + "telegram.mission.create", + "telegram.mission.list", + "telegram.mission.get", + "telegram.mission.complete", + "telegram.mission.messages", ] as const; const plugin = { @@ -148,7 +156,15 @@ Actions: - get_events — get recent inbound/outbound events for an agent (requires agentId) - assign_task — assign a persistent task session so the agent holds an ongoing AI conversation with a specific Telegram chat on behalf of the main agent (requires agentId, chatId, task; optional: systemPrompt, openingMessage). chatId must be the FULL username (e.g. 'worker_297') or a full numeric Telegram user ID (9+ digits) — never just the numeric suffix of a username - list_task_sessions — list all task sessions for an agent (requires agentId) -- complete_task_session — mark a task session as completed (requires agentId, sessionId)`, +- complete_task_session — mark a task session as completed (requires agentId, sessionId) +- create_mission — create a multi-agent mission where a master agent assigns a goal to sub-agents (requires masterAgentId, title, goal; optional: participantIds, systemPrompt) +- list_missions — list all agent missions +- get_mission — get details of a specific mission (requires missionId) +- complete_mission — mark a mission as completed (requires missionId) +- get_mission_messages — get inter-agent messages for a mission (requires missionId; optional: limit) +- send_agent_message — send a message from one agent to another within a mission (requires fromAgentId, toAgentId, missionId, content) +- get_core_files — list core workspace files for an agent (requires agentId) +- set_core_file — write a core workspace file for an agent (requires agentId, filename, content)`, parameters: { type: "object", properties: { @@ -165,6 +181,14 @@ Actions: "assign_task", "list_task_sessions", "complete_task_session", + "create_mission", + "list_missions", + "get_mission", + "complete_mission", + "get_mission_messages", + "send_agent_message", + "get_core_files", + "set_core_file", ], description: "Action to perform", }, @@ -213,6 +237,45 @@ Actions: type: "string", description: "Task session ID — required for complete_task_session", }, + masterAgentId: { + type: "string", + description: + "ID of the master agent that owns the mission — required for create_mission", + }, + title: { + type: "string", + description: "Mission title — required for create_mission", + }, + goal: { + type: "string", + description: "Mission goal/instructions — required for create_mission", + }, + participantIds: { + type: "array", + items: { type: "string" }, + description: "Agent IDs participating in the mission — for create_mission", + }, + missionId: { + type: "string", + description: + "Mission ID — required for get_mission, complete_mission, get_mission_messages, send_agent_message", + }, + fromAgentId: { + type: "string", + description: "Sending agent ID — required for send_agent_message", + }, + toAgentId: { + type: "string", + description: "Receiving agent ID — required for send_agent_message", + }, + content: { + type: "string", + description: "Message content — required for send_agent_message", + }, + filename: { + type: "string", + description: "Core file name (e.g. AGENTS.md) — required for set_core_file", + }, }, required: ["action"], }, @@ -302,9 +365,90 @@ Actions: }); return jsonResult({ ok: true, message: `Task session ${args.sessionId} completed` }); } + case "create_mission": { + if (!args.masterAgentId) + return jsonResult({ error: "masterAgentId is required for 'create_mission'" }); + if (!args.title) + return jsonResult({ error: "title is required for 'create_mission'" }); + if (!args.goal) return jsonResult({ error: "goal is required for 'create_mission'" }); + const mission = await callPlugin("telegram.mission.create", { + masterAgentId: args.masterAgentId, + title: args.title, + goal: args.goal, + participantIds: Array.isArray(args.participantIds) ? args.participantIds : [], + ...(args.systemPrompt ? { systemPrompt: args.systemPrompt } : {}), + }); + return jsonResult(mission); + } + case "list_missions": { + const missions = await callPlugin("telegram.mission.list", {}); + return jsonResult({ missions }); + } + case "get_mission": { + if (!args.missionId) + return jsonResult({ error: "missionId is required for 'get_mission'" }); + const mission = await callPlugin("telegram.mission.get", { + missionId: args.missionId, + }); + return jsonResult(mission); + } + case "complete_mission": { + if (!args.missionId) + return jsonResult({ error: "missionId is required for 'complete_mission'" }); + await callPlugin("telegram.mission.complete", { missionId: args.missionId }); + return jsonResult({ ok: true, message: `Mission ${args.missionId} completed` }); + } + case "get_mission_messages": { + if (!args.missionId) + return jsonResult({ error: "missionId is required for 'get_mission_messages'" }); + const messages = await callPlugin("telegram.mission.messages", { + missionId: args.missionId, + ...(typeof args.limit === "number" ? { limit: args.limit } : {}), + }); + return jsonResult({ messages }); + } + case "send_agent_message": { + if (!args.fromAgentId) + return jsonResult({ error: "fromAgentId is required for 'send_agent_message'" }); + if (!args.toAgentId) + return jsonResult({ error: "toAgentId is required for 'send_agent_message'" }); + if (!args.missionId) + return jsonResult({ error: "missionId is required for 'send_agent_message'" }); + if (!args.content) + return jsonResult({ error: "content is required for 'send_agent_message'" }); + const msg = await callPlugin("telegram.agent.sendMessage_to_agent", { + fromAgentId: args.fromAgentId, + toAgentId: args.toAgentId, + missionId: args.missionId, + content: args.content, + }); + return jsonResult(msg); + } + case "get_core_files": { + if (!args.agentId) + return jsonResult({ error: "agentId is required for 'get_core_files'" }); + const result = await callPlugin("telegram.agent.getCoreFiles", { + agentId: args.agentId, + }); + return jsonResult(result); + } + case "set_core_file": { + if (!args.agentId) + return jsonResult({ error: "agentId is required for 'set_core_file'" }); + if (!args.filename) + return jsonResult({ error: "filename is required for 'set_core_file'" }); + if (args.content === undefined) + return jsonResult({ error: "content is required for 'set_core_file'" }); + await callPlugin("telegram.agent.setCoreFile", { + agentId: args.agentId, + filename: args.filename, + content: args.content, + }); + return jsonResult({ ok: true }); + } default: return jsonResult({ - error: `Unknown action: '${action}'. Valid actions: list, get, start, stop, restart, send_message, get_events, assign_task, list_task_sessions, complete_task_session`, + error: `Unknown action: '${action}'. Valid actions: list, get, start, stop, restart, send_message, get_events, assign_task, list_task_sessions, complete_task_session, create_mission, list_missions, get_mission, complete_mission, get_mission_messages, send_agent_message, get_core_files, set_core_file`, }); } } catch (err) { diff --git a/extensions/telegram-manager/src/TelegramPlugin.ts b/extensions/telegram-manager/src/TelegramPlugin.ts index 683d234faa04f..db735f24180e3 100644 --- a/extensions/telegram-manager/src/TelegramPlugin.ts +++ b/extensions/telegram-manager/src/TelegramPlugin.ts @@ -8,6 +8,7 @@ // - Broadcast (push events to all connected WS clients) import { randomUUID } from "crypto"; +import fs from "fs"; import path from "path"; import { AgentManager } from "./agents/AgentManager"; import { TelegramStorage } from "./storage/TelegramStorage"; @@ -28,6 +29,18 @@ export class TelegramPlugin implements GatewayPlugin { private storage!: TelegramStorage; private manager!: AgentManager; + /** Allowed core file names — validated before any file I/O */ + private static readonly CORE_FILE_NAMES = [ + "AGENTS.md", + "SOUL.md", + "TOOLS.md", + "IDENTITY.md", + "USER.md", + "HEARTBEAT.md", + "BOOTSTRAP.md", + "MEMORY.md", + ] as const; + // ─── Plugin lifecycle ───────────────────────────────────────────────────── async init(ctx: IGatewayContext): Promise { @@ -198,6 +211,143 @@ export class TelegramPlugin implements GatewayPlugin { respond({ ok: true }); break; + // ── Core files ───────────────────────────────────────────────────── + + case "telegram.agent.getCoreFiles": { + if (!p.agentId) { + fail("agentId is required"); + break; + } + const workspaceDir = this.agentWorkspaceDir(String(p.agentId)); + const files = TelegramPlugin.CORE_FILE_NAMES.map((name) => { + const filePath = path.join(workspaceDir, name); + try { + const stat = fs.statSync(filePath); + return { + name, + sizeBytes: stat.size, + updatedAt: stat.mtime.toISOString(), + missing: false, + }; + } catch { + return { name, missing: true }; + } + }); + respond({ files, workspacePath: workspaceDir }); + break; + } + + case "telegram.agent.setCoreFile": { + if (!p.agentId) { + fail("agentId is required"); + break; + } + const filename = String(p.filename ?? ""); + if (!(TelegramPlugin.CORE_FILE_NAMES as readonly string[]).includes(filename)) { + fail(`Invalid filename. Allowed: ${TelegramPlugin.CORE_FILE_NAMES.join(", ")}`); + break; + } + const workspaceDir = this.agentWorkspaceDir(String(p.agentId)); + fs.mkdirSync(workspaceDir, { recursive: true }); + fs.writeFileSync(path.join(workspaceDir, filename), String(p.content ?? ""), "utf-8"); + respond({ ok: true }); + break; + } + + // ── Missions ─────────────────────────────────────────────────────── + + case "telegram.mission.create": { + if (!p.masterAgentId) { + fail("masterAgentId is required"); + break; + } + if (!p.title) { + fail("title is required"); + break; + } + if (!p.goal) { + fail("goal is required"); + break; + } + const mission = this.manager.createMission( + String(p.masterAgentId), + String(p.title), + String(p.goal), + Array.isArray(p.participantIds) ? p.participantIds.map(String) : [], + p.systemPrompt ? String(p.systemPrompt) : undefined, + ); + respond(mission); + break; + } + + case "telegram.mission.list": + respond(this.manager.getMissions()); + break; + + case "telegram.mission.get": { + if (!p.missionId) { + fail("missionId is required"); + break; + } + const mission = this.manager.getMission(String(p.missionId)); + if (!mission) { + fail(`Mission not found: ${p.missionId}`); + break; + } + respond(mission); + break; + } + + case "telegram.mission.complete": { + if (!p.missionId) { + fail("missionId is required"); + break; + } + this.manager.completeMission(String(p.missionId)); + respond({ ok: true }); + break; + } + + case "telegram.mission.messages": { + if (!p.missionId) { + fail("missionId is required"); + break; + } + const messages = this.manager.getMissionMessages( + String(p.missionId), + typeof p.limit === "number" ? p.limit : undefined, + ); + respond(messages); + break; + } + + case "telegram.agent.sendMessage_to_agent": { + if (!p.fromAgentId) { + fail("fromAgentId is required"); + break; + } + if (!p.toAgentId) { + fail("toAgentId is required"); + break; + } + if (!p.missionId) { + fail("missionId is required"); + break; + } + if (!p.content) { + fail("content is required"); + break; + } + const msg = await this.manager.sendAgentMessage( + String(p.fromAgentId), + String(p.toAgentId), + String(p.missionId), + String(p.content), + ); + respond(msg); + break; + } + // ── Data ─────────────────────────────────────────────────────────── case "telegram.events.get": @@ -292,6 +442,13 @@ export class TelegramPlugin implements GatewayPlugin { return true; // handled } + // ─── Private helpers ────────────────────────────────────────────────────── + + /** Returns the workspace directory path for a given agent */ + private agentWorkspaceDir(agentId: string): string { + return path.join(this.ctx.dataDir, "telegram", "agents", agentId, "workspace"); + } + // ─── HTTP REST routes (optional, for non-WS clients) ───────────────────── httpRoutes(): HttpRoute[] { @@ -417,6 +574,68 @@ export class TelegramPlugin implements GatewayPlugin { data: mgr.getParsed(req.params.id, parseInt(req.query.limit ?? "1000")), }), }, + // GET /telegram/agents/:id/core-files + { + method: "GET", + path: "/telegram/agents/:id/core-files", + handler: (req, res) => { + const workspaceDir = this.agentWorkspaceDir(req.params.id); + const files = TelegramPlugin.CORE_FILE_NAMES.map((name) => { + const filePath = path.join(workspaceDir, name); + try { + const stat = fs.statSync(filePath); + return { + name, + sizeBytes: stat.size, + updatedAt: stat.mtime.toISOString(), + missing: false, + }; + } catch { + return { name, missing: true }; + } + }); + res.json({ ok: true, data: { files, workspacePath: workspaceDir } }); + }, + }, + // GET /telegram/agents/:id/core-files/:filename + { + method: "GET", + path: "/telegram/agents/:id/core-files/:filename", + handler: (req, res) => { + const filename = req.params.filename; + if (!(TelegramPlugin.CORE_FILE_NAMES as readonly string[]).includes(filename)) { + res.status(400).json({ ok: false, error: `Invalid filename: ${filename}` }); + return; + } + const filePath = path.join(this.agentWorkspaceDir(req.params.id), filename); + try { + const content = fs.readFileSync(filePath, "utf-8"); + res.json({ ok: true, data: { filename, content } }); + } catch { + res.status(404).json({ ok: false, error: "File not found" }); + } + }, + }, + // PUT /telegram/agents/:id/core-files/:filename + { + method: "PUT", + path: "/telegram/agents/:id/core-files/:filename", + handler: async (req, res) => { + const filename = req.params.filename; + if (!(TelegramPlugin.CORE_FILE_NAMES as readonly string[]).includes(filename)) { + res.status(400).json({ ok: false, error: `Invalid filename: ${filename}` }); + return; + } + const workspaceDir = this.agentWorkspaceDir(req.params.id); + fs.mkdirSync(workspaceDir, { recursive: true }); + fs.writeFileSync( + path.join(workspaceDir, filename), + String(req.body?.content ?? ""), + "utf-8", + ); + res.json({ ok: true }); + }, + }, ]; } } diff --git a/extensions/telegram-manager/src/agents/AgentCommunicationBus.ts b/extensions/telegram-manager/src/agents/AgentCommunicationBus.ts new file mode 100644 index 0000000000000..fba21e186c999 --- /dev/null +++ b/extensions/telegram-manager/src/agents/AgentCommunicationBus.ts @@ -0,0 +1,136 @@ +// plugins/telegram/src/agents/AgentCommunicationBus.ts +// +// Manages inter-agent communication via missions. A master agent creates a +// mission with a goal and assigns sub-agents as participants. The bus routes +// messages between agents, persisting them in the database and emitting +// events for real-time delivery. + +import { randomUUID } from "crypto"; +import type { TelegramStorage } from "../storage/TelegramStorage"; +import type { AgentMission, AgentCommunicationMessage, ILogger, TelegramEvent } from "../types"; + +export class AgentCommunicationBus { + private eventListeners: ((e: TelegramEvent) => void)[] = []; + + constructor( + private storage: TelegramStorage, + /** Callback to call a tool on an agent — provided by AgentManager */ + private callTool: ( + agentId: string, + tool: string, + args: Record, + ) => Promise, + /** Callback to look up an agent name by id */ + private getAgentName: (agentId: string) => string, + private logger?: ILogger, + ) {} + + // ─── Event propagation ──────────────────────────────────────────────────── + + onEvent(fn: (e: TelegramEvent) => void): void { + this.eventListeners.push(fn); + } + + private emit(e: TelegramEvent): void { + this.eventListeners.forEach((fn) => fn(e)); + } + + // ─── Missions ───────────────────────────────────────────────────────────── + + createMission( + masterAgentId: string, + title: string, + goal: string, + participantIds: string[], + systemPrompt?: string, + ): AgentMission { + const mission: AgentMission = { + id: randomUUID(), + masterAgentId, + title, + goal, + ...(systemPrompt ? { systemPrompt } : {}), + participantAgentIds: participantIds, + status: "active", + createdAt: new Date().toISOString(), + }; + this.storage.saveMission(mission); + return mission; + } + + completeMission(missionId: string): void { + const completedAt = new Date().toISOString(); + this.storage.updateMissionStatus(missionId, "completed", completedAt); + } + + getMissions(): AgentMission[] { + return this.storage.getAllMissions(); + } + + getMission(id: string): AgentMission | null { + return this.storage.getMission(id); + } + + // ─── Messages ───────────────────────────────────────────────────────────── + + async sendAgentMessage( + fromAgentId: string, + toAgentId: string, + missionId: string, + content: string, + replyToId?: string, + ): Promise { + const fromAgentName = this.getAgentName(fromAgentId); + + const msg: AgentCommunicationMessage = { + id: randomUUID(), + fromAgentId, + fromAgentName, + toAgentId, + content, + missionId, + timestamp: new Date().toISOString(), + ...(replyToId ? { replyToId } : {}), + }; + + // Persist to DB + this.storage.saveCommMessage(msg); + + // Attempt to deliver via the sending agent's Telegram channel. + // The message is sent to an internal channel identified by the toAgentId. + // Failures here are non-fatal — the message is already persisted. + try { + await this.callTool(fromAgentId, "sendMessage", { + target: toAgentId, + message: `[Mission:${missionId}] ${content}`, + }); + } catch (err) { + // Delivery failure is non-fatal; message is stored and can be polled + this.logger?.warn( + `[CommBus] delivery failed from=${fromAgentId} to=${toAgentId} mission=${missionId}: ${String(err)}`, + ); + } + + // Emit event so the receiving agent can process it + this.emit({ + agentId: toAgentId, + agentName: this.getAgentName(toAgentId), + type: "agent_message", + payload: { + fromAgentId, + fromAgentName, + missionId, + content, + messageId: msg.id, + ...(replyToId ? { replyToId } : {}), + }, + timestamp: msg.timestamp, + }); + + return msg; + } + + getMissionMessages(missionId: string, limit?: number): AgentCommunicationMessage[] { + return this.storage.getCommMessages(missionId, limit); + } +} diff --git a/extensions/telegram-manager/src/agents/AgentManager.ts b/extensions/telegram-manager/src/agents/AgentManager.ts index a2d18246d106a..139e925397371 100644 --- a/extensions/telegram-manager/src/agents/AgentManager.ts +++ b/extensions/telegram-manager/src/agents/AgentManager.ts @@ -3,12 +3,15 @@ import { randomUUID } from "crypto"; import { TelegramStorage } from "../storage/TelegramStorage"; import { AgentRecord, + AgentMission, + AgentCommunicationMessage, BehaviorConfig, TelegramEvent, ILogger, AgentCredentials, TaskSession, } from "../types"; +import { AgentCommunicationBus } from "./AgentCommunicationBus"; import { BaseAgent } from "./BaseAgent"; import { BotAgent } from "./BotAgent"; import { UserBotAgent } from "./UserBotAgent"; @@ -16,6 +19,7 @@ import { UserBotAgent } from "./UserBotAgent"; export class AgentManager { private pool = new Map(); private eventListeners: ((e: TelegramEvent) => void)[] = []; + private commBus!: AgentCommunicationBus; constructor( private storage: TelegramStorage, @@ -23,6 +27,22 @@ export class AgentManager { ) {} async init(): Promise { + // Initialize communication bus with callbacks into this manager + this.commBus = new AgentCommunicationBus( + this.storage, + (agentId: string, tool: string, args: Record) => + this.callTool(agentId, tool, args), + (agentId: string) => { + const record = this.pool.get(agentId)?.getRecord(); + return record?.name ?? agentId; + }, + this.logger, + ); + // Forward comm bus events to all listeners + this.commBus.onEvent((e: TelegramEvent) => { + this.eventListeners.forEach((fn) => fn(e)); + }); + const records = this.storage.getAllAgents(); this.logger.info(`[TG] Loading ${records.length} agents`); for (const r of records) { @@ -152,6 +172,43 @@ export class AgentManager { return this.storage.getParsed(agentId, limit); } + // ─── Missions & inter-agent communication ───────────────────────────────── + + createMission( + masterAgentId: string, + title: string, + goal: string, + participantIds: string[], + systemPrompt?: string, + ): AgentMission { + return this.commBus.createMission(masterAgentId, title, goal, participantIds, systemPrompt); + } + + completeMission(missionId: string): void { + this.commBus.completeMission(missionId); + } + + async sendAgentMessage( + fromAgentId: string, + toAgentId: string, + missionId: string, + content: string, + ): Promise { + return this.commBus.sendAgentMessage(fromAgentId, toAgentId, missionId, content); + } + + getMissionMessages(missionId: string, limit?: number): AgentCommunicationMessage[] { + return this.commBus.getMissionMessages(missionId, limit); + } + + getMissions(): AgentMission[] { + return this.commBus.getMissions(); + } + + getMission(id: string): AgentMission | null { + return this.commBus.getMission(id); + } + // ─── Shutdown ───────────────────────────────────────────────────────────── async shutdown(): Promise { diff --git a/extensions/telegram-manager/src/storage/TelegramStorage.ts b/extensions/telegram-manager/src/storage/TelegramStorage.ts index 70a36b6d4c381..8508b1d407a21 100644 --- a/extensions/telegram-manager/src/storage/TelegramStorage.ts +++ b/extensions/telegram-manager/src/storage/TelegramStorage.ts @@ -2,7 +2,13 @@ import fs from "fs"; import path from "path"; // plugins/telegram/src/storage/TelegramStorage.ts import Database from "better-sqlite3"; -import { AgentRecord, BehaviorConfig, TelegramEvent } from "../types"; +import { + AgentRecord, + AgentMission, + AgentCommunicationMessage, + BehaviorConfig, + TelegramEvent, +} from "../types"; export type ProxyConfig = { socksType: 5; @@ -63,8 +69,33 @@ export class TelegramStorage { captured TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS agent_missions ( + id TEXT PRIMARY KEY, + master_agent_id TEXT NOT NULL, + title TEXT NOT NULL, + goal TEXT NOT NULL, + system_prompt TEXT, + participant_agent_ids TEXT NOT NULL DEFAULT '[]', + status TEXT NOT NULL DEFAULT 'active', + created_at TEXT NOT NULL, + completed_at TEXT + ); + + CREATE TABLE IF NOT EXISTS agent_communication_messages ( + id TEXT PRIMARY KEY, + from_agent_id TEXT NOT NULL, + from_agent_name TEXT NOT NULL, + to_agent_id TEXT NOT NULL, + content TEXT NOT NULL, + mission_id TEXT NOT NULL, + timestamp TEXT NOT NULL, + reply_to_id TEXT + ); + CREATE INDEX IF NOT EXISTS idx_tg_events_agent ON tg_events(agent_id); CREATE INDEX IF NOT EXISTS idx_tg_parsed_agent ON tg_parsed(agent_id); + CREATE INDEX IF NOT EXISTS idx_agent_missions_master ON agent_missions(master_agent_id); + CREATE INDEX IF NOT EXISTS idx_comm_msgs_mission ON agent_communication_messages(mission_id); `); } @@ -182,6 +213,82 @@ export class TelegramStorage { ).map((r) => ({ ...r, content: JSON.parse(r.content) })); } + // ─── Missions ───────────────────────────────────────────────────────────── + + saveMission(mission: AgentMission): void { + this.db + .prepare(` + INSERT OR REPLACE INTO agent_missions + (id, master_agent_id, title, goal, system_prompt, participant_agent_ids, status, created_at, completed_at) + VALUES + (@id, @masterAgentId, @title, @goal, @systemPrompt, @participantAgentIds, @status, @createdAt, @completedAt) + `) + .run({ + id: mission.id, + masterAgentId: mission.masterAgentId, + title: mission.title, + goal: mission.goal, + systemPrompt: mission.systemPrompt ?? null, + participantAgentIds: JSON.stringify(mission.participantAgentIds), + status: mission.status, + createdAt: mission.createdAt, + completedAt: mission.completedAt ?? null, + }); + } + + getMission(id: string): AgentMission | null { + const row = this.db.prepare("SELECT * FROM agent_missions WHERE id = ?").get(id) as any; + return row ? this.toMission(row) : null; + } + + getAllMissions(): AgentMission[] { + return ( + this.db.prepare("SELECT * FROM agent_missions ORDER BY created_at DESC").all() as any[] + ).map(this.toMission); + } + + updateMissionStatus(id: string, status: string, completedAt?: string): void { + this.db + .prepare("UPDATE agent_missions SET status=?, completed_at=? WHERE id=?") + .run(status, completedAt ?? null, id); + } + + deleteMission(id: string): void { + this.db.prepare("DELETE FROM agent_missions WHERE id=?").run(id); + } + + // ─── Communication messages ─────────────────────────────────────────────── + + saveCommMessage(msg: AgentCommunicationMessage): void { + this.db + .prepare(` + INSERT OR REPLACE INTO agent_communication_messages + (id, from_agent_id, from_agent_name, to_agent_id, content, mission_id, timestamp, reply_to_id) + VALUES + (@id, @fromAgentId, @fromAgentName, @toAgentId, @content, @missionId, @timestamp, @replyToId) + `) + .run({ + id: msg.id, + fromAgentId: msg.fromAgentId, + fromAgentName: msg.fromAgentName, + toAgentId: msg.toAgentId, + content: msg.content, + missionId: msg.missionId, + timestamp: msg.timestamp, + replyToId: msg.replyToId ?? null, + }); + } + + getCommMessages(missionId: string, limit = 100): AgentCommunicationMessage[] { + return ( + this.db + .prepare( + "SELECT * FROM agent_communication_messages WHERE mission_id=? ORDER BY timestamp ASC LIMIT ?", + ) + .all(missionId, limit) as any[] + ).map(this.toCommMessage); + } + // ─── Plugin credentials config ─────────────────────────────────────────── /** Persist apiId + apiHash to a JSON file in the plugin data directory. */ @@ -251,4 +358,31 @@ export class TelegramStorage { stats: JSON.parse(row.stats), }; } + + private toMission(row: any): AgentMission { + return { + id: row.id, + masterAgentId: row.master_agent_id, + title: row.title, + goal: row.goal, + systemPrompt: row.system_prompt ?? undefined, + participantAgentIds: JSON.parse(row.participant_agent_ids), + status: row.status, + createdAt: row.created_at, + completedAt: row.completed_at ?? undefined, + }; + } + + private toCommMessage(row: any): AgentCommunicationMessage { + return { + id: row.id, + fromAgentId: row.from_agent_id, + fromAgentName: row.from_agent_name, + toAgentId: row.to_agent_id, + content: row.content, + missionId: row.mission_id, + timestamp: row.timestamp, + replyToId: row.reply_to_id ?? undefined, + }; + } } diff --git a/extensions/telegram-manager/src/types.ts b/extensions/telegram-manager/src/types.ts index c9693a9f95fbc..605d337130828 100644 --- a/extensions/telegram-manager/src/types.ts +++ b/extensions/telegram-manager/src/types.ts @@ -130,12 +130,52 @@ export interface TaskSessionBehavior { sessions: TaskSession[]; } +// ─── Inter-agent communication ──────────────────────────────────────────────── + +export interface AgentCommunicationMessage { + id: string; + fromAgentId: string; + fromAgentName: string; + toAgentId: string; + content: string; + /** The shared mission/goal this message relates to */ + missionId: string; + timestamp: string; + /** Optional reply to another message */ + replyToId?: string; +} + +export interface AgentMission { + id: string; + /** The master agent that created and owns this mission */ + masterAgentId: string; + /** Human-readable title */ + title: string; + /** The goal/instructions all participating agents receive */ + goal: string; + /** Optional system prompt override for sub-agents */ + systemPrompt?: string; + /** Agents participating in this mission */ + participantAgentIds: string[]; + status: "active" | "completed" | "paused"; + createdAt: string; + completedAt?: string; +} + +export interface CommunicationBehavior { + type: "communication"; + enabled: boolean; + /** Missions this agent participates in */ + activeMissionIds: string[]; +} + export type BehaviorConfig = | AutoReplyBehavior | MonitorBehavior | BroadcastBehavior | ParserBehavior - | TaskSessionBehavior; + | TaskSessionBehavior + | CommunicationBehavior; // ─── Agent record ───────────────────────────────────────────────────────────── @@ -179,7 +219,7 @@ export interface ToolCallResult { export interface TelegramEvent { agentId: string; agentName: string; - type: "message_in" | "message_out" | "parsed_item" | "status_change" | "error"; + type: "message_in" | "message_out" | "parsed_item" | "status_change" | "error" | "agent_message"; payload: Record; timestamp: string; } diff --git a/src/pii-guard/index.ts b/src/pii-guard/index.ts index 49bef3c19b976..fdd767708e6c4 100644 --- a/src/pii-guard/index.ts +++ b/src/pii-guard/index.ts @@ -1,7 +1,7 @@ /** * pii-guard/index.ts — публичный API модуля */ -export { PiiProxy, piiSessions } from './proxy.js'; -export { buildPiiSystemPromptAddon, buildPiiSystemPromptAddonShort } from './system-prompt.js'; -export type { PiiPattern } from './patterns.js'; -export { PII_PATTERNS, SORTED_PATTERNS } from './patterns.js'; +export { PiiProxy, piiSessions } from "./proxy.js"; +export { buildPiiSystemPromptAddon, buildPiiSystemPromptAddonShort } from "./system-prompt.js"; +export type { PiiPattern } from "./patterns.js"; +export { PII_PATTERNS, SORTED_PATTERNS } from "./patterns.js"; diff --git a/src/pii-guard/proxy.ts b/src/pii-guard/proxy.ts index 63b6ca1d5e336..86d4948694422 100644 --- a/src/pii-guard/proxy.ts +++ b/src/pii-guard/proxy.ts @@ -12,8 +12,8 @@ * ───────────────────────────────────────────────────────────────────────────── */ -import crypto from 'crypto'; -import { SORTED_PATTERNS } from './patterns.js'; +import crypto from "crypto"; +import { SORTED_PATTERNS } from "./patterns.js"; // ───────────────────────────────────────────────────────────────────────────── // ТИПЫ @@ -28,8 +28,8 @@ interface PiiEntry { } interface SanitizeResult { - text: string; // Текст с токенами (передаётся в LLM) - detected: PiiEntry[]; // Что нашли в этом вызове + text: string; // Текст с токенами (передаётся в LLM) + detected: PiiEntry[]; // Что нашли в этом вызове } // ───────────────────────────────────────────────────────────────────────────── @@ -41,7 +41,6 @@ const TOKEN_PATTERN = /\[[А-ЯЁA-Z0-9_]+_[0-9A-F]{6}\]/g; // КЛАСС PiiProxy // ───────────────────────────────────────────────────────────────────────────── export class PiiProxy { - /** token → PiiEntry */ private store = new Map(); /** original value → token (для дедупликации) */ @@ -63,12 +62,16 @@ export class PiiProxy { result = result.replace(pattern.regex, (match) => { // 1. Пропускаем уже вставленные токены - if (TOKEN_PATTERN.test(match)) return match; + if (TOKEN_PATTERN.test(match)) { + return match; + } TOKEN_PATTERN.lastIndex = 0; // 2. Пропускаем пустые/слишком короткие совпадения const trimmed = match.trim(); - if (trimmed.length < 2) return match; + if (trimmed.length < 2) { + return match; + } // 3. Дедупликация: если это значение уже имеет токен — возвращаем его if (this.reverseIndex.has(trimmed)) { @@ -76,7 +79,7 @@ export class PiiProxy { } // 4. Создаём новый токен - const id = crypto.randomBytes(3).toString('hex').toUpperCase(); + const id = crypto.randomBytes(3).toString("hex").toUpperCase(); const token = `[${pattern.tokenName}_${id}]`; const entry: PiiEntry = { @@ -119,11 +122,9 @@ export class PiiProxy { } // Fuzzy: токен с пробелами внутри скобок - const fuzzyToken = token - .replace(/^\[/, '\\[\\s*') - .replace(/\]$/, '\\s*\\]'); + const fuzzyToken = token.replace(/^\[/, "\\[\\s*").replace(/\]$/, "\\s*\\]"); try { - const fuzzyRe = new RegExp(fuzzyToken, 'g'); + const fuzzyRe = new RegExp(fuzzyToken, "g"); result = result.replace(fuzzyRe, entry.original); } catch { // если regex сломался — пропускаем diff --git a/ui/src/ui/controllers/telegram-core-files.ts b/ui/src/ui/controllers/telegram-core-files.ts new file mode 100644 index 0000000000000..7d8abb4735441 --- /dev/null +++ b/ui/src/ui/controllers/telegram-core-files.ts @@ -0,0 +1,123 @@ +import type { GatewayBrowserClient } from "../gateway.ts"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export type TelegramCoreFile = { + name: string; + sizeBytes?: number; + updatedAt?: string; + missing?: boolean; +}; + +export type TelegramCoreFilesState = { + client?: GatewayBrowserClient | null; + telegramCoreFiles: TelegramCoreFile[] | null; + telegramCoreFilesLoading: boolean; + telegramCoreFilesError: string | null; + telegramCoreFilesSelectedFile: string | null; + telegramCoreFileContent: string | null; + telegramCoreFileSaving: boolean; +}; + +export const TELEGRAM_CORE_FILE_NAMES = [ + "AGENTS.md", + "SOUL.md", + "TOOLS.md", + "IDENTITY.md", + "USER.md", + "HEARTBEAT.md", + "BOOTSTRAP.md", + "MEMORY.md", +] as const; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function isReady(state: TelegramCoreFilesState): boolean { + return !!state.client; +} + +// ─── Load core files ────────────────────────────────────────────────────────── + +export async function loadTelegramAgentCoreFiles( + state: TelegramCoreFilesState, + agentId: string, +): Promise { + if (!isReady(state) || state.telegramCoreFilesLoading) { + return; + } + state.telegramCoreFilesLoading = true; + state.telegramCoreFilesError = null; + try { + const res = await state.client!.request<{ files: TelegramCoreFile[]; workspacePath?: string }>( + "telegram.agent.getCoreFiles", + { agentId }, + ); + state.telegramCoreFiles = res?.files ?? []; + } catch (err) { + state.telegramCoreFilesError = String(err); + } finally { + state.telegramCoreFilesLoading = false; + } +} + +// ─── Load file content ──────────────────────────────────────────────────────── + +export async function getTelegramCoreFileContent( + state: TelegramCoreFilesState, + agentId: string, + filename: string, +): Promise { + if (!isReady(state)) { + return; + } + state.telegramCoreFilesSelectedFile = filename; + state.telegramCoreFileContent = null; + state.telegramCoreFilesError = null; + try { + const res = await state.client!.request<{ + ok: boolean; + data?: { filename: string; content: string }; + }>("telegram.tool.call", { + agentId, + tool: "getCoreFile", + args: { filename }, + }); + // Fall back to HTTP endpoint if tool call doesn't work + const content = + (res as unknown as { data?: { content?: string }; content?: string })?.data?.content ?? + (res as unknown as { content?: string })?.content ?? + ""; + state.telegramCoreFileContent = content; + } catch (err) { + state.telegramCoreFilesError = String(err); + } +} + +// ─── Save file content ──────────────────────────────────────────────────────── + +export async function saveTelegramAgentCoreFile( + state: TelegramCoreFilesState, + agentId: string, + filename: string, + content: string, +): Promise { + if (!isReady(state) || state.telegramCoreFileSaving) { + return; + } + state.telegramCoreFileSaving = true; + state.telegramCoreFilesError = null; + try { + await state.client!.request("telegram.agent.setCoreFile", { + agentId, + filename, + content, + }); + // Reload the file list to get updated timestamps + await loadTelegramAgentCoreFiles(state, agentId); + } catch (err) { + state.telegramCoreFilesError = String(err); + throw err; + } finally { + state.telegramCoreFileSaving = false; + } +} diff --git a/ui/src/ui/controllers/telegram.ts b/ui/src/ui/controllers/telegram.ts index f6dea385c6e44..794bf3278391e 100644 --- a/ui/src/ui/controllers/telegram.ts +++ b/ui/src/ui/controllers/telegram.ts @@ -469,3 +469,148 @@ export async function completeTelegramTaskSession( state.telegramTasksBusy = false; } } + +// ─── Missions (inter-agent communication) ───────────────────────────────────── + +export type AgentMissionRecord = { + id: string; + masterAgentId: string; + title: string; + goal: string; + systemPrompt?: string; + participantAgentIds: string[]; + status: "active" | "completed" | "paused"; + createdAt: string; + completedAt?: string; +}; + +export type AgentCommMessageRecord = { + id: string; + fromAgentId: string; + fromAgentName: string; + toAgentId: string; + content: string; + missionId: string; + timestamp: string; + replyToId?: string; +}; + +type TelegramMissionsState = TelegramState & { + telegramMissions: AgentMissionRecord[]; + telegramMissionsLoading: boolean; + telegramMissionsError: string | null; + telegramMissionsBusy: boolean; + telegramMissionMessages: AgentCommMessageRecord[]; +}; + +export async function loadTelegramMissions(state: TelegramMissionsState): Promise { + if (!isReady(state) || state.telegramMissionsLoading) { + return; + } + state.telegramMissionsLoading = true; + state.telegramMissionsError = null; + try { + const res = await state.client!.request("telegram.mission.list", {}); + state.telegramMissions = res ?? []; + } catch (err) { + state.telegramMissionsError = String(err); + } finally { + state.telegramMissionsLoading = false; + } +} + +export async function createTelegramMission( + state: TelegramMissionsState, + masterAgentId: string, + title: string, + goal: string, + participantIds: string[], + systemPrompt?: string, +): Promise { + if (!isReady(state) || state.telegramMissionsBusy) { + return null; + } + state.telegramMissionsBusy = true; + state.telegramMissionsError = null; + try { + const res = await state.client!.request("telegram.mission.create", { + masterAgentId, + title, + goal, + participantIds, + ...(systemPrompt ? { systemPrompt } : {}), + }); + await loadTelegramMissions(state); + return res ?? null; + } catch (err) { + state.telegramMissionsError = String(err); + return null; + } finally { + state.telegramMissionsBusy = false; + } +} + +export async function completeTelegramMission( + state: TelegramMissionsState, + missionId: string, +): Promise { + if (!isReady(state) || state.telegramMissionsBusy) { + return; + } + state.telegramMissionsBusy = true; + state.telegramMissionsError = null; + try { + await state.client!.request("telegram.mission.complete", { missionId }); + await loadTelegramMissions(state); + } catch (err) { + state.telegramMissionsError = String(err); + } finally { + state.telegramMissionsBusy = false; + } +} + +export async function loadTelegramMissionMessages( + state: TelegramMissionsState, + missionId: string, +): Promise { + if (!isReady(state)) { + return; + } + state.telegramMissionsError = null; + try { + const res = await state.client!.request("telegram.mission.messages", { + missionId, + }); + state.telegramMissionMessages = res ?? []; + } catch (err) { + state.telegramMissionsError = String(err); + } +} + +export async function sendTelegramAgentMessage( + state: TelegramMissionsState, + fromAgentId: string, + toAgentId: string, + missionId: string, + content: string, +): Promise { + if (!isReady(state) || state.telegramMissionsBusy) { + return null; + } + state.telegramMissionsBusy = true; + state.telegramMissionsError = null; + try { + const res = await state.client!.request( + "telegram.agent.sendMessage_to_agent", + { fromAgentId, toAgentId, missionId, content }, + ); + // Refresh messages for this mission + await loadTelegramMissionMessages(state, missionId); + return res ?? null; + } catch (err) { + state.telegramMissionsError = String(err); + return null; + } finally { + state.telegramMissionsBusy = false; + } +} diff --git a/ui/src/ui/views/telegram-core-files.ts b/ui/src/ui/views/telegram-core-files.ts new file mode 100644 index 0000000000000..fbe1be30d9968 --- /dev/null +++ b/ui/src/ui/views/telegram-core-files.ts @@ -0,0 +1,151 @@ +import { html, nothing } from "lit"; +import type { TelegramCoreFile } from "../controllers/telegram-core-files.ts"; +import { formatRelativeTimestamp } from "../format.ts"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export type TelegramCoreFilesProps = { + agentId: string; + agentName: string; + files: TelegramCoreFile[]; + selectedFile: string | null; + content: string | null; + loading: boolean; + saving: boolean; + error: string | null; + workspacePath?: string; + onSelectFile: (filename: string) => void; + onContentChange: (content: string) => void; + onSave: () => void; + onRefresh: () => void; +}; + +// ─── View ───────────────────────────────────────────────────────────────────── + +export function renderTelegramCoreFiles(props: TelegramCoreFilesProps) { + const { + agentName: _agentName, + files, + selectedFile, + content, + loading, + saving, + error, + workspacePath, + onSelectFile, + onContentChange, + onSave, + onRefresh, + } = props; + + return html` +
+
+
+
Core Files
+
Bootstrap persona, identity, and tool guidance.
+
+ +
+ + ${ + workspacePath + ? html`
${workspacePath}
` + : nothing + } + + ${ + error + ? html`
${error}
` + : nothing + } + + ${ + loading && (!files || files.length === 0) + ? html` +
Loading…
+ ` + : html` +
+ + +
+ ${files.map((file) => renderFileCard(file, selectedFile, onSelectFile))} +
+ + +
+ ${ + selectedFile + ? html` +
${selectedFile}
+ +
+ +
+ ` + : html` +
+ Select a file to edit +
+ ` + } +
+ +
+ ` + } +
+ `; +} + +function renderFileCard( + file: TelegramCoreFile, + selectedFile: string | null, + onSelect: (name: string) => void, +) { + const isSelected = file.name === selectedFile; + return html` +
onSelect(file.name)} + > +
+ ${file.name} + ${ + file.missing + ? html` + MISSING + ` + : nothing + } +
+ ${ + !file.missing + ? html` +
+ ${file.sizeBytes !== undefined ? `${file.sizeBytes} B` : ""} + ${file.updatedAt ? ` · ${formatRelativeTimestamp(new Date(file.updatedAt).getTime())}` : ""} +
+ ` + : nothing + } +
+ `; +} diff --git a/ui/src/ui/views/telegram-missions.ts b/ui/src/ui/views/telegram-missions.ts new file mode 100644 index 0000000000000..fc506c1aa0f8f --- /dev/null +++ b/ui/src/ui/views/telegram-missions.ts @@ -0,0 +1,318 @@ +import { html, nothing } from "lit"; +import type { TelegramAgentRecord } from "../controllers/telegram.ts"; +import type { AgentMissionRecord, AgentCommMessageRecord } from "../controllers/telegram.ts"; +import { formatRelativeTimestamp } from "../format.ts"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export type TelegramMissionsForm = { + masterAgentId: string; + title: string; + goal: string; + systemPrompt: string; + participantIds: string[]; +}; + +export type TelegramSendForm = { + fromAgentId: string; + toAgentId: string; + content: string; +}; + +export type TelegramMissionsProps = { + agents: TelegramAgentRecord[]; + missions: AgentMissionRecord[]; + selectedMissionId: string | null; + missionMessages: AgentCommMessageRecord[]; + loading: boolean; + creating: boolean; + form: TelegramMissionsForm; + sendForm: TelegramSendForm; + onFormChange: (patch: Partial) => void; + onSendFormChange: (patch: Partial) => void; + onCreate: () => void; + onComplete: (missionId: string) => void; + onViewMessages: (missionId: string) => void; + onBack: () => void; + onSendMessage: () => void; + onRefresh: () => void; +}; + +// ─── View ───────────────────────────────────────────────────────────────────── + +export function renderTelegramMissions(props: TelegramMissionsProps) { + const { selectedMissionId, missions } = props; + + if (selectedMissionId) { + const mission = missions.find((m) => m.id === selectedMissionId) ?? null; + return renderMissionMessages(props, mission); + } + + return renderMissionList(props); +} + +// ─── Mission list ───────────────────────────────────────────────────────────── + +function renderMissionList(props: TelegramMissionsProps) { + const { + agents, + missions, + loading, + creating, + form, + onFormChange, + onCreate, + onComplete, + onViewMessages, + onRefresh, + } = props; + + return html` +
+
+
+
Agent Missions
+
Coordinate Telegram agents toward a shared goal.
+
+ +
+ + +
+ + + Create Mission + +
+
+ + +
+
+ + onFormChange({ title: (e.target as HTMLInputElement).value })} + /> +
+
+ + +
+
+ + +
+
+ +
+ ${agents.map( + (a) => html` + + `, + )} +
+
+
+ +
+
+
+ + + ${ + missions.length === 0 + ? html` +
+ No missions yet. +
+ ` + : missions.map((m) => renderMissionCard(m, agents, onComplete, onViewMessages)) + } +
+ `; +} + +function renderMissionCard( + mission: AgentMissionRecord, + agents: TelegramAgentRecord[], + onComplete: (id: string) => void, + onViewMessages: (id: string) => void, +) { + const masterAgent = agents.find((a) => a.id === mission.masterAgentId); + const participants = agents.filter((a) => mission.participantAgentIds.includes(a.id)); + const statusColor = + mission.status === "active" ? "#3a7" : mission.status === "completed" ? "#888" : "#a73"; + + return html` +
+
+
+
+ ${mission.title} + ${mission.status} +
+
+ ${mission.goal.length > 120 ? mission.goal.slice(0, 120) + "…" : mission.goal} +
+
+ Master: ${masterAgent?.name ?? mission.masterAgentId} + ${ + participants.length > 0 + ? html` · Participants: ${participants.map((a) => a.name).join(", ")}` + : nothing + } + · ${formatRelativeTimestamp(new Date(mission.createdAt).getTime())} +
+
+
+ + ${ + mission.status === "active" + ? html`` + : nothing + } +
+
+
+ `; +} + +// ─── Mission messages ───────────────────────────────────────────────────────── + +function renderMissionMessages(props: TelegramMissionsProps, mission: AgentMissionRecord | null) { + const { agents, missionMessages, sendForm, onSendFormChange, onSendMessage, onBack } = props; + + return html` +
+
+ +
+
${mission?.title ?? "Mission Messages"}
+ ${ + mission + ? html`
${mission.goal.length > 100 ? mission.goal.slice(0, 100) + "…" : mission.goal}
` + : nothing + } +
+
+ + +
+ ${ + missionMessages.length === 0 + ? html` +
+ No messages yet. +
+ ` + : missionMessages.map((msg) => renderMessageBubble(msg, agents)) + } +
+ + +
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+ `; +} + +function renderMessageBubble(msg: AgentCommMessageRecord, agents: TelegramAgentRecord[]) { + const fromAgent = agents.find((a) => a.id === msg.fromAgentId); + const toAgent = agents.find((a) => a.id === msg.toAgentId); + + return html` +
+
+
+ ${fromAgent?.name ?? msg.fromAgentName} + + ${toAgent?.name ?? msg.toAgentId} +
+
${formatRelativeTimestamp(new Date(msg.timestamp).getTime())}
+
+
${msg.content}
+
+ `; +} From eb782c0e1f0314d4252ada90393f1c42cc170b53 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:52:31 +0000 Subject: [PATCH 3/3] fix: Telegram files panel unknown agent id + task session chatId resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agents.files.list "unknown agent id": add telegram.agent.getCoreFileContent WS method; add loadTelegramAgentFiles/loadTelegramAgentFileContent/saveTelegramAgentFile adapters; fix app-render.ts Telegram files panel to use Telegram-specific endpoints - One-message-no-conversation: add resolveEntityId tool to BotAgent + UserBotAgent; in TelegramPlugin.assignTask resolve username → numeric peer ID before storing session; extract resolved ID from opening-message result as belt-and-suspenders fallback Co-authored-by: Secret297-CODER-SOURCE <73541046+Secret297-CODER-SOURCE@users.noreply.github.com> --- extensions/telegram-manager/index.ts | 1 + .../telegram-manager/src/TelegramPlugin.ts | 83 ++++++++++++- .../telegram-manager/src/agents/BotAgent.ts | 8 ++ .../src/agents/UserBotAgent.ts | 10 ++ ui/src/ui/app-render.ts | 16 ++- ui/src/ui/controllers/telegram.ts | 112 ++++++++++++++++++ 6 files changed, 223 insertions(+), 7 deletions(-) diff --git a/extensions/telegram-manager/index.ts b/extensions/telegram-manager/index.ts index 9d10d35bda250..b8a6f75e98783 100644 --- a/extensions/telegram-manager/index.ts +++ b/extensions/telegram-manager/index.ts @@ -35,6 +35,7 @@ const TELEGRAM_METHODS = [ "telegram.agent.completeTaskSession", "telegram.agent.getCoreFiles", "telegram.agent.setCoreFile", + "telegram.agent.getCoreFileContent", "telegram.agent.sendMessage_to_agent", "telegram.mission.create", "telegram.mission.list", diff --git a/extensions/telegram-manager/src/TelegramPlugin.ts b/extensions/telegram-manager/src/TelegramPlugin.ts index db735f24180e3..424ace868d555 100644 --- a/extensions/telegram-manager/src/TelegramPlugin.ts +++ b/extensions/telegram-manager/src/TelegramPlugin.ts @@ -22,6 +22,18 @@ import { TaskSession, } from "./types"; +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/** Returns true if `id` is a pure numeric Telegram peer ID (possibly negative for groups/channels). */ +function isNumericTelegramId(id: string): boolean { + return /^-?\d+$/.test(id.replace(/^@/, "")); +} + +/** Shape returned by the `resolveEntityId` tool. */ +interface ResolveEntityResult { + id: string | null; +} + export class TelegramPlugin implements GatewayPlugin { readonly namespace = "telegram"; @@ -167,9 +179,30 @@ export class TelegramPlugin implements GatewayPlugin { fail("task is required"); break; } + + // Resolve username-style chatIds to numeric peer IDs so that the task + // session handler can match incoming messages (which always carry a + // numeric Telegram peer ID) against the stored session. + // A pure numeric string (e.g. "123456789") is already resolved. + let resolvedChatId = String(p.chatId); + if (!isNumericTelegramId(resolvedChatId)) { + try { + const entityResult = (await this.manager.callTool(p.agentId, "resolveEntityId", { + target: resolvedChatId, + })) as ResolveEntityResult; + if (entityResult?.id && isNumericTelegramId(entityResult.id)) { + resolvedChatId = entityResult.id; + } + } catch { + // Resolution failed (e.g. bot doesn't support getChat for this user); + // fall through with original chatId. The opening-message fallback below + // will still attempt to update it. + } + } + const session: TaskSession = { id: randomUUID(), - chatId: String(p.chatId), + chatId: resolvedChatId, task: String(p.task), ...(p.systemPrompt ? { systemPrompt: String(p.systemPrompt) } : {}), status: "active", @@ -177,14 +210,37 @@ export class TelegramPlugin implements GatewayPlugin { ...(p.initiatedBy ? { initiatedBy: String(p.initiatedBy) } : {}), }; await this.manager.assignTaskSession(p.agentId, session); - // Optionally send an opening message right away + + // Optionally send an opening message right away. + // As a fallback for agents that cannot resolve usernames directly (e.g. + // BotAgent/grammy), extract the resolved numeric chatId from the sent + // message result and update the session so subsequent replies can be matched. let openingMessageError: string | undefined; if (p.openingMessage) { try { - await this.manager.callTool(p.agentId, "sendMessage", { + const msgResult = await this.manager.callTool(p.agentId, "sendMessage", { target: p.chatId, message: p.openingMessage, }); + // Attempt to extract the resolved numeric peer ID from the result: + // BotAgent (grammy): result.chat.id (number) + // UserBotAgent (gramjs): result.peerId.userId / result.peerId.chatId (BigInt) + // Both are cast through unknown to avoid "any" — we read optional fields. + const raw = msgResult as { + chat?: { id?: number }; + peerId?: { userId?: bigint; chatId?: bigint }; + }; + const numericFromResult = raw?.chat?.id ?? raw?.peerId?.userId ?? raw?.peerId?.chatId; + if (numericFromResult != null) { + const resolvedFromMsg = String(numericFromResult); + if (isNumericTelegramId(resolvedFromMsg) && resolvedFromMsg !== session.chatId) { + // Update the persisted session with the numeric peer ID + await this.manager.assignTaskSession(p.agentId, { + ...session, + chatId: resolvedFromMsg, + }); + } + } } catch (e) { openingMessageError = String(e); // Log the actual reason (not just metadata) so it appears in the gateway log @@ -254,6 +310,27 @@ export class TelegramPlugin implements GatewayPlugin { break; } + case "telegram.agent.getCoreFileContent": { + if (!p.agentId) { + fail("agentId is required"); + break; + } + const filename = String(p.filename ?? ""); + if (!(TelegramPlugin.CORE_FILE_NAMES as readonly string[]).includes(filename)) { + fail(`Invalid filename. Allowed: ${TelegramPlugin.CORE_FILE_NAMES.join(", ")}`); + break; + } + const filePath = path.join(this.agentWorkspaceDir(String(p.agentId)), filename); + try { + const content = fs.readFileSync(filePath, "utf-8"); + respond({ filename, content }); + } catch { + // File doesn't exist — return empty string so the editor can start fresh + respond({ filename, content: "" }); + } + break; + } + // ── Missions ─────────────────────────────────────────────────────── case "telegram.mission.create": { diff --git a/extensions/telegram-manager/src/agents/BotAgent.ts b/extensions/telegram-manager/src/agents/BotAgent.ts index 2afb2d3721778..f1dc8ffecc4ed 100644 --- a/extensions/telegram-manager/src/agents/BotAgent.ts +++ b/extensions/telegram-manager/src/agents/BotAgent.ts @@ -90,6 +90,14 @@ export class BotAgent extends BaseAgent { return this.bot.api.getMe(); case "getMessages": throw new Error("Bot API does not support getMessages — use a userbot agent"); + case "resolveEntityId": { + // Resolve a username/chat identifier to its numeric Telegram chat ID. + // For bots, getChat works for groups/channels and any user who has + // previously started the bot. + const { target } = args as { target: string }; + const chat = await this.bot.api.getChat(target); + return { id: String(chat.id) }; + } default: throw new Error(`Unknown tool: ${tool}`); } diff --git a/extensions/telegram-manager/src/agents/UserBotAgent.ts b/extensions/telegram-manager/src/agents/UserBotAgent.ts index 2a6f1e897e330..6462c901d47c2 100644 --- a/extensions/telegram-manager/src/agents/UserBotAgent.ts +++ b/extensions/telegram-manager/src/agents/UserBotAgent.ts @@ -226,6 +226,16 @@ export class UserBotAgent extends BaseAgent { case "getMe": { return this.client.getMe(); } + case "resolveEntityId": { + // Resolve a username or identifier to its numeric Telegram peer ID. + // gramjs caches resolved entities, so this is lightweight after the first call. + const { target } = args as { target: string }; + const entity = await this.client.getEntity(target); + // gramjs entities (User, Chat, Channel) all have a BigInt `id` property. + // Cast through `unknown` because gramjs Entity union doesn't expose `id` directly. + const id = (entity as unknown as { id?: bigint | number }).id; + return { id: id != null ? String(id) : null }; + } default: throw new Error(`Unknown tool: ${tool}`); } diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 57cb27b322608..4b53083b1e56f 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -77,6 +77,9 @@ import { loadTelegramTaskSessions, assignTelegramTask, completeTelegramTaskSession, + loadTelegramAgentFiles, + loadTelegramAgentFileContent, + saveTelegramAgentFile, } from "./controllers/telegram.ts"; import { icons } from "./icons.ts"; import { TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation.ts"; @@ -593,7 +596,7 @@ export function renderApp(state: AppViewState) { state.agentFileActive = null; state.agentFileContents = {}; state.agentFileDrafts = {}; - void loadAgentFiles(state, agentId); + void loadTelegramAgentFiles(state, agentId); } } }, @@ -752,12 +755,12 @@ export function renderApp(state: AppViewState) { agentFileContents: state.agentFileContents, agentFileDrafts: state.agentFileDrafts, agentFileSaving: state.agentFileSaving, - onLoadFiles: (agentId) => void loadAgentFiles(state, agentId), + onLoadFiles: (agentId) => void loadTelegramAgentFiles(state, agentId), onSelectFile: (name) => { state.agentFileActive = name; const agentId = state.telegramSelectedId; if (agentId && !state.agentFileContents[name]) { - void loadAgentFileContent(state, agentId, name); + void loadTelegramAgentFileContent(state, agentId, name); } }, onFileDraftChange: (name, content) => { @@ -771,7 +774,12 @@ export function renderApp(state: AppViewState) { onFileSave: (name) => { const agentId = state.telegramSelectedId; if (agentId) { - void saveAgentFile(state, agentId, name, state.agentFileDrafts[name] ?? ""); + void saveTelegramAgentFile( + state, + agentId, + name, + state.agentFileDrafts[name] ?? "", + ); } }, // Tasks panel diff --git a/ui/src/ui/controllers/telegram.ts b/ui/src/ui/controllers/telegram.ts index 794bf3278391e..5ec2a3976f572 100644 --- a/ui/src/ui/controllers/telegram.ts +++ b/ui/src/ui/controllers/telegram.ts @@ -614,3 +614,115 @@ export async function sendTelegramAgentMessage( state.telegramMissionsBusy = false; } } + +// ─── Telegram-specific file operations ──────────────────────────────────────── +// +// These adapter functions provide the same interface as the main agent file +// operations (loadAgentFiles / loadAgentFileContent / saveAgentFile) but route +// through the Telegram-specific WS endpoints. The results are stored in the +// same agentFilesList / agentFileContents / agentFileSaving state slots so that +// the existing renderAgentFiles view component renders them without changes. +// +// Background: Telegram agent IDs are UUIDs stored in the plugin's SQLite DB +// and are NOT known to the main OpenClaw gateway. Calling agents.files.list +// with a Telegram agent UUID results in "unknown agent id" (INVALID_REQUEST). + +import type { AgentFileEntry, AgentsFilesListResult } from "../types.ts"; + +// Minimal subset of AgentFilesState fields we need to populate +type TelegramAgentFilesState = TelegramState & { + agentFilesLoading: boolean; + agentFilesError: string | null; + agentFilesList: AgentsFilesListResult | null; + agentFileContents: Record; + agentFileSaving: boolean; +}; + +/** Load a Telegram agent's core files into the standard agentFilesList state slot */ +export async function loadTelegramAgentFiles( + state: TelegramAgentFilesState, + agentId: string, +): Promise { + if (!isReady(state) || state.agentFilesLoading) { + return; + } + state.agentFilesLoading = true; + state.agentFilesError = null; + try { + const res = await state.client!.request<{ + files: Array<{ name: string; sizeBytes?: number; updatedAt?: string; missing: boolean }>; + workspacePath?: string; + }>("telegram.agent.getCoreFiles", { agentId }); + if (res) { + const workspacePath = res.workspacePath ?? ""; + const files: AgentFileEntry[] = (res.files ?? []).map((f) => ({ + name: f.name, + path: workspacePath ? `${workspacePath}/${f.name}` : f.name, + missing: !!f.missing, + size: f.sizeBytes, + updatedAtMs: f.updatedAt ? new Date(f.updatedAt).getTime() : undefined, + })); + state.agentFilesList = { agentId, workspace: workspacePath, files }; + } + } catch (err) { + state.agentFilesError = String(err); + } finally { + state.agentFilesLoading = false; + } +} + +/** Load the content of one Telegram agent core file into agentFileContents */ +export async function loadTelegramAgentFileContent( + state: TelegramAgentFilesState, + agentId: string, + name: string, +): Promise { + if (!isReady(state)) { + return; + } + state.agentFilesLoading = true; + state.agentFilesError = null; + try { + const res = await state.client!.request<{ filename: string; content: string }>( + "telegram.agent.getCoreFileContent", + { agentId, filename: name }, + ); + if (res?.content !== undefined) { + state.agentFileContents = { ...state.agentFileContents, [name]: res.content }; + } + } catch (err) { + state.agentFilesError = String(err); + } finally { + state.agentFilesLoading = false; + } +} + +/** Save a Telegram agent core file via telegram.agent.setCoreFile */ +export async function saveTelegramAgentFile( + state: TelegramAgentFilesState, + agentId: string, + name: string, + content: string, +): Promise { + if (!isReady(state) || state.agentFileSaving) { + return; + } + state.agentFileSaving = true; + state.agentFilesError = null; + try { + await state.client!.request("telegram.agent.setCoreFile", { + agentId, + filename: name, + content, + }); + // Update in-memory cache so the editor reflects the saved content immediately + state.agentFileContents = { ...state.agentFileContents, [name]: content }; + // Reload the file list to refresh timestamps + await loadTelegramAgentFiles(state, agentId); + } catch (err) { + state.agentFilesError = String(err); + throw err; + } finally { + state.agentFileSaving = false; + } +}