picogent/src/router.ts
Markov db90b64f54 feat: bootstrap context из AGENT.md и memory/ файлов
- loadBootstrapContext() загружает AGENT.md, memory/notes.md, memory/projects/*.md
- Лимит 15K символов, автотрункейт
- agentHome передаётся из router в agent options
- System prompt из agent.json сохранён как fallback
2026-02-24 22:04:29 +01:00

118 lines
3.9 KiB
TypeScript

import path from 'path';
import { logger } from './logger.js';
import { runAgent } from './agent.js';
import { TrackerClient } from './tracker/client.js';
import { createTrackerTools } from './tools/index.js';
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
import type { AgentConfig } from './config.js';
import type { TrackerEvent } from './tracker/types.js';
export interface TaskTracker {
addTask(taskId: string): void;
removeTask(taskId: string): void;
}
export class EventRouter {
private log = logger.child({ component: 'event-router' });
private trackerTools: ToolDefinition[];
constructor(
private config: AgentConfig,
private client: TrackerClient,
) {
this.trackerTools = createTrackerTools({
trackerClient: client,
agentSlug: config.slug,
selfAssignedTasks: new Set(),
});
this.log.info({ toolCount: this.trackerTools.length }, 'Tracker tools registered');
}
async handleEvent(event: TrackerEvent): Promise<void> {
this.log.info('┌── ROUTER: handling %s (id: %s)', event.event, event.id);
switch (event.event) {
case 'message.new':
case 'chat.message':
await this.handleMessageNew(event.data);
break;
case 'task.assigned':
case 'task.created':
case 'task.updated':
case 'agent.status':
case 'agent.online':
case 'agent.offline':
this.log.info({ event: event.event }, 'Informational event, skipping');
break;
default:
this.log.warn({ event: event.event }, 'Unknown event type, ignoring');
}
}
/**
* message.new / chat.message — forward to agent session.
* Agent uses send_message tool to reply when needed. Router posts nothing.
*/
private async handleMessageNew(data: Record<string, unknown>): Promise<void> {
const content = (data.content 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 chatId = data.chat_id as string | undefined;
const taskKey = data.task_key as string | undefined;
// Don't respond to own messages
if (authorSlug === this.config.slug) {
this.log.debug('Ignoring own message');
return;
}
// System messages: only process if agent is mentioned
if (authorType === 'system') {
if (!content.includes(`@${this.config.slug}`)) {
this.log.debug('Ignoring system message (not mentioned): %s', content.slice(0, 100));
return;
}
}
if (!content) {
this.log.warn({ data }, 'message.new event missing content');
return;
}
// Build context-rich prompt for the agent
const ctx = taskId ? `[задача ${taskKey || taskId}]` : chatId ? '[чат]' : '';
const from = authorType === 'system' ? '[система]' : `@${authorSlug}`;
const prompt = `${ctx} ${from}: ${content}`;
this.log.info('│ %s %s: "%s"', ctx, from, content.slice(0, 200));
await this.runAgent(prompt);
this.log.info('└── MESSAGE handled');
}
/**
* Run agent session. Agent controls everything via tools (send_message, update_task, etc.)
*/
private async runAgent(prompt: string): Promise<void> {
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, // fallback if no AGENT.md
skillsDir: this.config.agentHome,
sessionDir: path.join(this.config.agentHome, 'sessions'),
allowedPaths: this.config.allowedPaths.length > 0
? this.config.allowedPaths
: [this.config.agentHome],
customTools: this.trackerTools,
agentHome: this.config.agentHome,
})) {
if (msg.type === 'error') {
this.log.error({ error: msg.content }, 'Agent error');
}
}
}
}