diff --git a/src/router.ts b/src/router.ts index 484610f..a293c95 100644 --- a/src/router.ts +++ b/src/router.ts @@ -14,10 +14,8 @@ export interface TaskTracker { export class EventRouter { private log = logger.child({ component: 'event-router' }); - private activeTasks = 0; private trackerTools: ToolDefinition[]; - /** Tasks taken via tool call (agent already knows about them — skip auto-processing) */ - private selfAssignedTasks = new Set(); + constructor( private config: AgentConfig, private client: TrackerClient, @@ -26,7 +24,7 @@ export class EventRouter { this.trackerTools = createTrackerTools({ trackerClient: client, agentSlug: config.slug, - selfAssignedTasks: this.selfAssignedTasks, + selfAssignedTasks: new Set(), // kept for ToolContext compat, no longer used in router }); this.log.info({ toolCount: this.trackerTools.length }, 'Tracker tools registered'); } @@ -39,8 +37,6 @@ export class EventRouter { await this.handleTaskAssigned(event.data); break; case 'message.new': - await this.handleMessageNew(event.data); - break; case 'chat.message': await this.handleMessageNew(event.data); break; @@ -49,105 +45,47 @@ export class EventRouter { case 'agent.status': case 'agent.online': case 'agent.offline': - this.log.info({ event: event.event, data: event.data }, 'Informational event'); + this.log.info({ event: event.event }, 'Informational event, skipping'); break; default: this.log.warn({ event: event.event }, 'Unknown event type, ignoring'); } } + /** + * task.assigned — notify agent via session, no side effects. + * Agent decides what to do (change status, start work, etc.) via tools. + */ private async handleTaskAssigned(data: Record): Promise { - // Protocol: data = { task: TaskOut } or data IS the task const task = (data.task as TrackerTask) || (data as unknown as TrackerTask); if (!task?.id) { this.log.error({ data }, 'task.assigned event missing task data'); return; } - // Skip if agent took this task itself via tool call (already in conversation context) - if (this.selfAssignedTasks.has(task.id)) { - this.selfAssignedTasks.delete(task.id); - this.log.info('│ TASK %s self-assigned via tool, skipping auto-processing', task.key); - return; - } + this.log.info('│ TASK ASSIGNED: %s — %s', task.key || task.id, task.title); - if (this.activeTasks >= this.config.maxConcurrentTasks) { - this.log.warn({ taskId: task.id, activeTasks: this.activeTasks }, 'Max concurrent tasks reached, skipping'); - return; - } + // Build human-readable prompt — agent decides what to do + const prompt = [ + `Тебе назначена задача: ${task.key || ''} — ${task.title}`, + task.description ? `\nОписание: ${task.description}` : '', + task.priority ? `Приоритет: ${task.priority}` : '', + '', + 'Ознакомься с задачей. Если готов — возьми в работу (обнови статус через update_task). Если нужна информация — спроси.', + ].filter(Boolean).join('\n'); - this.activeTasks++; - this.taskTracker.addTask(task.id); - this.log.info('│ TASK ASSIGNED: %s — %s', task.key, task.title); - this.log.info('│ Priority: %s | Status: %s', task.priority || '-', task.status || '-'); - if (task.description) this.log.info('│ Description: %s', task.description.slice(0, 200)); - - try { - // Update status → in_progress - this.log.info('│ → Updating task status to in_progress...'); - await this.client.updateTask(task.id, { status: 'in_progress' }).catch((err) => { - this.log.warn({ err, taskId: task.id }, 'Failed to update task status to in_progress'); - }); - - // Build prompt from task - const prompt = buildPromptFromTask(task); - - // Run agent and collect output - let collectedText = ''; - for await (const msg of runAgent(prompt, { - workDir: this.config.workDir, - sessionId: this.config.sessionId, - model: this.config.model, - provider: this.config.provider, - systemPrompt: this.config.prompt || undefined, - skillsDir: this.config.agentHome, - sessionDir: path.join(this.config.agentHome, 'sessions'), - allowedPaths: this.config.allowedPaths, - customTools: this.trackerTools, - })) { - if (msg.type === 'text') { - collectedText += msg.content; - } else if (msg.type === 'error') { - this.log.error({ taskId: task.id, error: msg.content }, 'Agent error during task'); - } - } - - // Post result as comment to task - if (collectedText.trim()) { - this.log.info('│ → Sending result comment (%d chars)...', collectedText.trim().length); - this.log.info('│ Result preview: %s', collectedText.trim().slice(0, 300)); - await this.client.sendMessage({ task_id: task.id, content: collectedText.trim() }, this.config.slug).catch((err) => { - this.log.error({ err, taskId: task.id }, 'Failed to add comment'); - }); - } - - // Update status → in_review - this.log.info('│ → Updating task status to in_review...'); - await this.client.updateTask(task.id, { status: 'in_review' }).catch((err) => { - this.log.warn({ err, taskId: task.id }, 'Failed to update task status to in_review'); - }); - - this.log.info('└── TASK DONE: %s (%d chars output)', task.key, collectedText.length); - } catch (err) { - this.log.error({ err, taskId: task.id }, 'Task processing failed'); - - await this.client.sendMessage({ - task_id: task.id, - content: `Agent error: ${err instanceof Error ? err.message : String(err)}`, - }, this.config.slug).catch(() => {}); - } finally { - this.activeTasks--; - this.taskTracker.removeTask(task.id); - } + await this.runAndReply(prompt, task.id ? { task_id: task.id } : undefined); + this.log.info('└── TASK ASSIGNED handled: %s', task.key || task.id); } + /** + * message.new / chat.message — forward to agent session, reply to same context. + */ private async handleMessageNew(data: Record): Promise { - // Protocol: message.new → { id, chat_id, task_id, author_slug, content, mentions, ... } const content = (data.content as string) || ''; const authorSlug = (data.author_slug as string) || (data.sender_slug as string) || ''; const taskId = data.task_id as string | undefined; const chatId = data.chat_id as string | undefined; - const mentions = (data.mentions as string[]) || []; // Don't respond to own messages if (authorSlug === this.config.slug) { @@ -160,13 +98,24 @@ export class EventRouter { return; } - // Check if agent is mentioned (for filtered modes) - const isMentioned = mentions.includes(this.config.slug); this.log.info('│ MESSAGE from @%s: "%s"', authorSlug, content.slice(0, 200)); - this.log.info('│ Context: %s | Mentioned: %s', taskId ? `task=${taskId}` : chatId ? `chat=${chatId}` : 'none', isMentioned); + const replyCtx = taskId ? { task_id: taskId } : chatId ? { chat_id: chatId } : undefined; + await this.runAndReply(content, replyCtx); + this.log.info('└── MESSAGE handled'); + } + + /** + * Run agent with prompt and send reply to the appropriate context. + * No side effects — agent controls everything via tools. + */ + private async runAndReply( + prompt: string, + replyCtx?: { task_id?: string; chat_id?: string }, + ): Promise { let collectedText = ''; - for await (const msg of runAgent(content, { + + for await (const msg of runAgent(prompt, { workDir: this.config.workDir, sessionId: this.config.sessionId, model: this.config.model, @@ -179,47 +128,21 @@ export class EventRouter { })) { if (msg.type === 'text') { collectedText += msg.content; + } else if (msg.type === 'error') { + this.log.error({ error: msg.content }, 'Agent error'); } } - // Reply to the same context (task comment or chat message) - if (collectedText.trim()) { - this.log.info('│ → Sending reply (%d chars): %s', collectedText.trim().length, collectedText.trim().slice(0, 200)); - if (taskId) { - await this.client.sendMessage({ task_id: taskId, content: collectedText.trim() }, this.config.slug).catch((err) => { - this.log.error({ err, taskId }, 'Failed to send task comment reply'); - }); - } else if (chatId) { - await this.client.sendMessage({ chat_id: chatId, content: collectedText.trim() }, this.config.slug).catch((err) => { - this.log.error({ err, chatId }, 'Failed to send chat reply'); - }); - } - this.log.info('└── MESSAGE REPLIED'); - } else { - this.log.info('└── MESSAGE PROCESSED (no reply)'); + if (collectedText.trim() && replyCtx) { + const payload = { + content: collectedText.trim(), + task_id: replyCtx.task_id, + chat_id: replyCtx.chat_id, + }; + + await this.client.sendMessage(payload, this.config.slug).catch((err) => { + this.log.error({ err, replyCtx }, 'Failed to send reply'); + }); } } } - -function buildPromptFromTask(task: TrackerTask): string { - const parts = [`# Задача: ${task.key} — ${task.title}`, '']; - - if (task.description) { - parts.push(task.description, ''); - } - - if (task.priority) { - parts.push(`Приоритет: ${task.priority}`); - } - - if (task.files?.length) { - parts.push('', 'Прикреплённые файлы:'); - for (const f of task.files) { - parts.push(`- ${f.name} (${f.url})`); - } - } - - parts.push('', 'Выполни задачу. После завершения опиши что было сделано.'); - - return parts.join('\n'); -}