fix: align with AGENT-PROTOCOL v1.0 — correct WS types, heartbeat, REST auth

This commit is contained in:
Markov 2026-02-23 13:57:32 +01:00
parent 368b9abf69
commit f97aa64142
3 changed files with 18 additions and 46 deletions

View File

@ -96,7 +96,7 @@ export class EventRouter {
if (collectedText.trim()) { if (collectedText.trim()) {
this.log.info('│ → Sending result comment (%d chars)...', collectedText.trim().length); this.log.info('│ → Sending result comment (%d chars)...', collectedText.trim().length);
this.log.info('│ Result preview: %s', collectedText.trim().slice(0, 300)); this.log.info('│ Result preview: %s', collectedText.trim().slice(0, 300));
await this.client.sendMessage({ task_id: task.id, content: collectedText.trim() }).catch((err) => { 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'); this.log.error({ err, taskId: task.id }, 'Failed to add comment');
}); });
} }
@ -114,7 +114,7 @@ export class EventRouter {
await this.client.sendMessage({ await this.client.sendMessage({
task_id: task.id, task_id: task.id,
content: `Agent error: ${err instanceof Error ? err.message : String(err)}`, content: `Agent error: ${err instanceof Error ? err.message : String(err)}`,
}).catch(() => {}); }, this.config.slug).catch(() => {});
} finally { } finally {
this.activeTasks--; this.activeTasks--;
this.taskTracker.removeTask(task.id); this.taskTracker.removeTask(task.id);
@ -164,11 +164,11 @@ export class EventRouter {
if (collectedText.trim()) { if (collectedText.trim()) {
this.log.info('│ → Sending reply (%d chars): %s', collectedText.trim().length, collectedText.trim().slice(0, 200)); this.log.info('│ → Sending reply (%d chars): %s', collectedText.trim().length, collectedText.trim().slice(0, 200));
if (taskId) { if (taskId) {
await this.client.sendMessage({ task_id: taskId, content: collectedText.trim() }).catch((err) => { 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'); this.log.error({ err, taskId }, 'Failed to send task comment reply');
}); });
} else if (chatId) { } else if (chatId) {
await this.client.sendMessage({ chat_id: chatId, content: collectedText.trim() }).catch((err) => { 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.error({ err, chatId }, 'Failed to send chat reply');
}); });
} }

View File

@ -43,15 +43,8 @@ export class TrackerClient {
} }
// --- Agent lifecycle --- // --- Agent lifecycle ---
// Registration: done via UI (Settings → Agents) or POST /api/v1/members
async register(payload: RegistrationPayload): Promise<void> { // Heartbeat: done via WebSocket only ({"type": "heartbeat", "status": "online"})
this.log.info({ slug: payload.slug }, 'Registering agent');
await this.request('POST', '/api/v1/agents/register', payload);
}
async heartbeat(payload: HeartbeatPayload): Promise<void> {
await this.request('POST', '/api/v1/agents/heartbeat', payload);
}
// --- Tasks --- // --- Tasks ---
@ -104,9 +97,13 @@ export class TrackerClient {
// --- Messages (unified: chat + task comments) --- // --- Messages (unified: chat + task comments) ---
async sendMessage(payload: { chat_id?: string; task_id?: string; content: string; mentions?: string[] }): Promise<Record<string, unknown>> { async sendMessage(payload: { chat_id?: string; task_id?: string; content: string; mentions?: string[] }, agentSlug?: string): Promise<Record<string, unknown>> {
this.log.info({ chatId: payload.chat_id, taskId: payload.task_id, contentLength: payload.content.length }, 'Sending message'); this.log.info({ chatId: payload.chat_id, taskId: payload.task_id, contentLength: payload.content.length }, 'Sending message');
return this.request('POST', '/api/v1/messages', payload); return this.request('POST', '/api/v1/messages', {
...payload,
author_type: 'agent',
author_slug: agentSlug || 'agent',
});
} }
async listMessages(params: Record<string, string>): Promise<Record<string, unknown>[]> { async listMessages(params: Record<string, string>): Promise<Record<string, unknown>[]> {
@ -115,14 +112,7 @@ export class TrackerClient {
} }
// --- Files --- // --- Files ---
// TODO: file upload/download not implemented yet in Tracker
async uploadFile(taskId: string, filename: string, content: string): Promise<void> {
await this.request('POST', `/api/v1/tasks/${taskId}/files`, { filename, content });
}
async listTaskFiles(taskId: string): Promise<Record<string, unknown>[]> {
return this.request('GET', `/api/v1/tasks/${taskId}/files`);
}
// --- Projects --- // --- Projects ---

View File

@ -98,12 +98,9 @@ export class WsClientTransport implements TaskTracker {
ws.on('open', () => { ws.on('open', () => {
this.log.info('━━━ WS CONNECTED ━━━'); this.log.info('━━━ WS CONNECTED ━━━');
// Send auth — use BOTH "event" and "type" for compatibility // Send auth — Tracker expects "type" field only
this.send('auth', { this.send('auth', {
token: this.config.token, token: this.config.token,
name: this.config.name,
slug: this.config.slug,
capabilities: this.config.capabilities,
}); });
}); });
@ -139,11 +136,10 @@ export class WsClientTransport implements TaskTracker {
} }
/** /**
* Send a JSON message with both "event" and "type" fields set, * Send a JSON message with "type" field (Tracker protocol).
* so the tracker picks up whichever field it uses.
*/ */
private send(eventType: string, payload: Record<string, unknown> = {}): void { private send(eventType: string, payload: Record<string, unknown> = {}): void {
const msg = { event: eventType, type: eventType, ...payload }; const msg = { type: eventType, ...payload };
const json = JSON.stringify(msg); const json = JSON.stringify(msg);
this.log.info('→ SEND: %s', json.slice(0, 500)); this.log.info('→ SEND: %s', json.slice(0, 500));
if (this.ws?.readyState === WebSocket.OPEN) { if (this.ws?.readyState === WebSocket.OPEN) {
@ -219,24 +215,10 @@ export class WsClientTransport implements TaskTracker {
this.reconnectDelay = 1000; this.reconnectDelay = 1000;
this.startHeartbeat(); this.startHeartbeat();
// Subscribe to lobby chat // Subscribe to all projects
if (this.lobbyChatId) {
this.log.info('→ Subscribing to lobby chat: %s', this.lobbyChatId);
this.send('chat.subscribe', { chat_id: this.lobbyChatId });
}
// Subscribe to projects (try both protocols)
for (const project of this.projects) { for (const project of this.projects) {
this.log.info('→ Subscribing to project: %s (%s)', project.slug, project.id); this.log.info('→ Subscribing to project: %s (%s)', project.slug, project.id);
// WEBSOCKET-PROTOCOL.md style: subscribe with channels
this.send('subscribe', { channels: [`project:${project.slug}`] });
// TRACKER-PROTOCOL.md style: project.subscribe
this.send('project.subscribe', { project_id: project.id }); this.send('project.subscribe', { project_id: project.id });
// If project has a chat_id, subscribe to it
if (project.chat_id) {
this.log.info('→ Subscribing to project chat: %s', project.chat_id);
this.send('chat.subscribe', { chat_id: project.chat_id });
}
} }
if (this.resolveStart) { if (this.resolveStart) {
@ -303,7 +285,7 @@ export class WsClientTransport implements TaskTracker {
private sendHeartbeat(): void { private sendHeartbeat(): void {
const status = this.currentTasks.size > 0 ? 'busy' : 'online'; const status = this.currentTasks.size > 0 ? 'busy' : 'online';
this.send('agent.heartbeat', { status, current_tasks: [...this.currentTasks] }); this.send('heartbeat', { status });
} }
private scheduleReconnect(): void { private scheduleReconnect(): void {