refactor: router is pure relay — no replies, no side effects
Agent sees system messages from Tracker and acts via tools. Router only forwards message.new events to agent session.
This commit is contained in:
parent
64eca81f5a
commit
cbe3f86890
@ -12,7 +12,7 @@ import type { IncomingMessage } from './transport/types.js';
|
|||||||
|
|
||||||
async function startAgentHttp(config: AgentConfig, client: TrackerClient): Promise<void> {
|
async function startAgentHttp(config: AgentConfig, client: TrackerClient): Promise<void> {
|
||||||
const registration = new TrackerRegistration(config);
|
const registration = new TrackerRegistration(config);
|
||||||
const router = new EventRouter(config, client, registration);
|
const router = new EventRouter(config, client);
|
||||||
const http = new HttpTransport(config.listenPort);
|
const http = new HttpTransport(config.listenPort);
|
||||||
|
|
||||||
http.onEvent((event) => router.handleEvent(event));
|
http.onEvent((event) => router.handleEvent(event));
|
||||||
@ -38,7 +38,7 @@ async function startAgentHttp(config: AgentConfig, client: TrackerClient): Promi
|
|||||||
|
|
||||||
async function startAgentWs(config: AgentConfig, client: TrackerClient): Promise<void> {
|
async function startAgentWs(config: AgentConfig, client: TrackerClient): Promise<void> {
|
||||||
const wsTransport = new WsClientTransport(config);
|
const wsTransport = new WsClientTransport(config);
|
||||||
const router = new EventRouter(config, client, wsTransport);
|
const router = new EventRouter(config, client);
|
||||||
|
|
||||||
wsTransport.onEvent((event) => router.handleEvent(event));
|
wsTransport.onEvent((event) => router.handleEvent(event));
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { TrackerClient } from './tracker/client.js';
|
|||||||
import { createTrackerTools } from './tools/index.js';
|
import { createTrackerTools } from './tools/index.js';
|
||||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||||
import type { AgentConfig } from './config.js';
|
import type { AgentConfig } from './config.js';
|
||||||
import type { TrackerEvent, TrackerTask } from './tracker/types.js';
|
import type { TrackerEvent } from './tracker/types.js';
|
||||||
|
|
||||||
export interface TaskTracker {
|
export interface TaskTracker {
|
||||||
addTask(taskId: string): void;
|
addTask(taskId: string): void;
|
||||||
@ -19,12 +19,11 @@ export class EventRouter {
|
|||||||
constructor(
|
constructor(
|
||||||
private config: AgentConfig,
|
private config: AgentConfig,
|
||||||
private client: TrackerClient,
|
private client: TrackerClient,
|
||||||
private taskTracker: TaskTracker,
|
|
||||||
) {
|
) {
|
||||||
this.trackerTools = createTrackerTools({
|
this.trackerTools = createTrackerTools({
|
||||||
trackerClient: client,
|
trackerClient: client,
|
||||||
agentSlug: config.slug,
|
agentSlug: config.slug,
|
||||||
selfAssignedTasks: new Set(), // kept for ToolContext compat, no longer used in router
|
selfAssignedTasks: new Set(),
|
||||||
});
|
});
|
||||||
this.log.info({ toolCount: this.trackerTools.length }, 'Tracker tools registered');
|
this.log.info({ toolCount: this.trackerTools.length }, 'Tracker tools registered');
|
||||||
}
|
}
|
||||||
@ -33,13 +32,11 @@ export class EventRouter {
|
|||||||
this.log.info('┌── ROUTER: handling %s (id: %s)', event.event, event.id);
|
this.log.info('┌── ROUTER: handling %s (id: %s)', event.event, event.id);
|
||||||
|
|
||||||
switch (event.event) {
|
switch (event.event) {
|
||||||
case 'task.assigned':
|
|
||||||
await this.handleTaskAssigned(event.data);
|
|
||||||
break;
|
|
||||||
case 'message.new':
|
case 'message.new':
|
||||||
case 'chat.message':
|
case 'chat.message':
|
||||||
await this.handleMessageNew(event.data);
|
await this.handleMessageNew(event.data);
|
||||||
break;
|
break;
|
||||||
|
case 'task.assigned':
|
||||||
case 'task.created':
|
case 'task.created':
|
||||||
case 'task.updated':
|
case 'task.updated':
|
||||||
case 'agent.status':
|
case 'agent.status':
|
||||||
@ -53,39 +50,16 @@ export class EventRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* task.assigned — notify agent via session, no side effects.
|
* message.new / chat.message — forward to agent session.
|
||||||
* Agent decides what to do (change status, start work, etc.) via tools.
|
* Agent uses send_message tool to reply when needed. Router posts nothing.
|
||||||
*/
|
|
||||||
private async handleTaskAssigned(data: Record<string, unknown>): Promise<void> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.log.info('│ TASK ASSIGNED: %s — %s', task.key || task.id, task.title);
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
|
|
||||||
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<string, unknown>): Promise<void> {
|
private async handleMessageNew(data: Record<string, unknown>): Promise<void> {
|
||||||
const content = (data.content as string) || '';
|
const content = (data.content as string) || '';
|
||||||
const authorSlug = (data.author_slug as string) || (data.sender_slug as string) || '';
|
const authorSlug = (data.author_slug as string) || (data.sender_slug as string) || '';
|
||||||
|
const authorType = (data.author_type as string) || 'member';
|
||||||
const taskId = data.task_id as string | undefined;
|
const taskId = data.task_id as string | undefined;
|
||||||
const chatId = data.chat_id as string | undefined;
|
const chatId = data.chat_id as string | undefined;
|
||||||
|
const taskKey = data.task_key as string | undefined;
|
||||||
|
|
||||||
// Don't respond to own messages
|
// Don't respond to own messages
|
||||||
if (authorSlug === this.config.slug) {
|
if (authorSlug === this.config.slug) {
|
||||||
@ -98,23 +72,21 @@ export class EventRouter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log.info('│ MESSAGE from @%s: "%s"', authorSlug, content.slice(0, 200));
|
// Build context-rich prompt for the agent
|
||||||
|
const ctx = taskId ? `[задача ${taskKey || taskId}]` : chatId ? '[чат]' : '';
|
||||||
|
const from = authorType === 'system' ? '[система]' : `@${authorSlug}`;
|
||||||
|
const prompt = `${ctx} ${from}: ${content}`;
|
||||||
|
|
||||||
const replyCtx = taskId ? { task_id: taskId } : chatId ? { chat_id: chatId } : undefined;
|
this.log.info('│ %s %s: "%s"', ctx, from, content.slice(0, 200));
|
||||||
await this.runAndReply(content, replyCtx);
|
|
||||||
|
await this.runAgent(prompt);
|
||||||
this.log.info('└── MESSAGE handled');
|
this.log.info('└── MESSAGE handled');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run agent with prompt and send reply to the appropriate context.
|
* Run agent session. Agent controls everything via tools (send_message, update_task, etc.)
|
||||||
* No side effects — agent controls everything via tools.
|
|
||||||
*/
|
*/
|
||||||
private async runAndReply(
|
private async runAgent(prompt: string): Promise<void> {
|
||||||
prompt: string,
|
|
||||||
replyCtx?: { task_id?: string; chat_id?: string },
|
|
||||||
): Promise<void> {
|
|
||||||
let collectedText = '';
|
|
||||||
|
|
||||||
for await (const msg of runAgent(prompt, {
|
for await (const msg of runAgent(prompt, {
|
||||||
workDir: this.config.workDir,
|
workDir: this.config.workDir,
|
||||||
sessionId: this.config.sessionId,
|
sessionId: this.config.sessionId,
|
||||||
@ -126,23 +98,9 @@ export class EventRouter {
|
|||||||
allowedPaths: this.config.allowedPaths,
|
allowedPaths: this.config.allowedPaths,
|
||||||
customTools: this.trackerTools,
|
customTools: this.trackerTools,
|
||||||
})) {
|
})) {
|
||||||
if (msg.type === 'text') {
|
if (msg.type === 'error') {
|
||||||
collectedText += msg.content;
|
|
||||||
} else if (msg.type === 'error') {
|
|
||||||
this.log.error({ error: msg.content }, 'Agent error');
|
this.log.error({ error: msg.content }, 'Agent error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user