agenthub/src/cli/remoteClient.ts
chahinebrini e5bd28488d feat(work): workTimeoutSec pro Agent — codex und kimi auf dasselbe Niveau
Der Work-Loop blockierte fuer ALLE Agenten 50s. Der Wert stammt von Kimi-Code,
dessen MCP-Client Requests nach gut einer Minute abbricht (-32001). Codex
vertraegt Minuten — musste aber mit demselben Minimum leben und kehrte dadurch
sechsmal so oft leer zurueck wie noetig. Jede Rueckkehr ist eine Gelegenheit,
den Turn zu beenden und aus dem Loop zu fallen; genau daran unterschied sich
codex' Zuverlaessigkeit von kimis.

Ein globaler Default zwingt alle auf die Grenze des schwaechsten Clients. Die
Grenze gehoert aber zum Agenten, nicht zum Hub — deshalb jetzt
`agents[].workTimeoutSec` im Roster, ausgeliefert ueber /agents/:agent/identity.
Aufloesung: explizites Argument > Roster > globaler Default.

Konfiguriert: codex 240s, kimi 50s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 20:57:52 +02:00

217 lines
8.5 KiB
TypeScript

import type { Task, Handoff, Decision, Memory, Message, Ask, ActivityItem } from '../core/schema.js';
import type { IndexEntry } from '../core/index.js';
import type { InboxMessage } from '../core/services/messageService.js';
import type { TaskLogEntry } from '../core/services/taskLogService.js';
import type { HealthReport } from '../core/services/presenceService.js';
export class RemoteError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
async function request<T>(baseUrl: string, method: string, path: string, body?: unknown): Promise<T> {
const url = `${baseUrl}${path}`;
let res: Response;
try {
res = await fetch(url, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new RemoteError(0, `Cannot reach AgentHub server at ${baseUrl}: ${message}`);
}
const text = await res.text();
if (!res.ok) {
throw new RemoteError(res.status, text || `HTTP ${res.status}`);
}
if (!text) {
throw new RemoteError(res.status, 'Empty response from server');
}
return JSON.parse(text) as T;
}
export const remoteClient = {
async getStatus(baseUrl: string): Promise<string> {
return request<{ body: string }>(baseUrl, 'GET', '/status').then((r) => r.body);
},
async getHealth(baseUrl: string): Promise<HealthReport> {
return request<HealthReport>(baseUrl, 'GET', '/health');
},
async getAgentIdentity(baseUrl: string, agent: string): Promise<{ canonical: string; names: string[]; workTimeoutSec?: number }> {
return request<{ canonical: string; names: string[]; workTimeoutSec?: number }>(
baseUrl,
'GET',
`/agents/${encodeURIComponent(agent)}/identity`,
);
},
async announce(baseUrl: string, agent: string, role?: string, action: 'joined' | 'left' = 'joined'): Promise<void> {
await request<{ ok: boolean }>(baseUrl, 'POST', '/announce', { agent, role, action });
},
async setLoop(baseUrl: string, agent: string, active: boolean, reason?: string): Promise<void> {
await request<{ ok: boolean }>(
baseUrl,
'POST',
`/agents/${encodeURIComponent(agent)}/loop`,
{ active, reason },
);
},
async updateStatus(baseUrl: string): Promise<string> {
return request<{ body: string }>(baseUrl, 'POST', '/status/update').then((r) => r.body);
},
async createTask(baseUrl: string, options: Partial<Task>): Promise<Task> {
return request<Task>(baseUrl, 'POST', '/tasks', options);
},
async recordExternalTask(baseUrl: string, options: Partial<Task> & { title: string; doneBy: string }): Promise<Task> {
return request<Task>(baseUrl, 'POST', '/tasks/record', options);
},
async listTasks(baseUrl: string, filters?: { status?: string; role?: string }): Promise<IndexEntry[]> {
const params = new URLSearchParams((filters ?? {}) as Record<string, string>);
const qs = params.toString();
return request<IndexEntry[]>(baseUrl, 'GET', `/tasks${qs ? '?' + qs : ''}`);
},
async getTask(baseUrl: string, id: string): Promise<{ task: Task; body: string }> {
return request<{ task: Task; body: string }>(baseUrl, 'GET', `/tasks/${id}`);
},
async claimTask(baseUrl: string, id: string, agentName: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'in_progress', assignedTo: agentName });
},
async dispatchTask(baseUrl: string, id: string, agent: string): Promise<Task> {
return request<Task>(baseUrl, 'POST', `/tasks/${id}/dispatch`, { agent });
},
async doneTask(
baseUrl: string,
id: string,
meta?: { tokens?: number; duration?: number; by?: string },
): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, {
status: 'done',
doneTokens: meta?.tokens,
doneDuration: meta?.duration,
doneBy: meta?.by,
});
},
async assignTask(baseUrl: string, id: string, agentName: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { assignedTo: agentName });
},
async reviewTask(baseUrl: string, id: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'review' });
},
async reopenTask(baseUrl: string, id: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'open' });
},
async getTaskActivity(baseUrl: string, id: string): Promise<ActivityItem[]> {
return request<ActivityItem[]>(baseUrl, 'GET', `/tasks/${id}/activity`);
},
async appendTaskLog(
baseUrl: string,
id: string,
entry: { text: string; agent?: string; level?: string },
): Promise<TaskLogEntry> {
return request<TaskLogEntry>(baseUrl, 'POST', `/tasks/${id}/log`, entry);
},
/** TSK-0274: was wartet auf diesen Agenten (Check-in-Kanal für Arbeitende). */
async getPending(baseUrl: string, agent: string, taskId?: string): Promise<unknown> {
const query = taskId ? `?taskId=${encodeURIComponent(taskId)}` : '';
return request<unknown>(baseUrl, 'GET', `/agents/${encodeURIComponent(agent)}/pending${query}`);
},
async createHandoff(baseUrl: string, options: Partial<Handoff>): Promise<Handoff> {
return request<Handoff>(baseUrl, 'POST', '/handoffs', options);
},
async listHandoffs(baseUrl: string): Promise<IndexEntry[]> {
return request<IndexEntry[]>(baseUrl, 'GET', '/handoffs');
},
async getHandoff(baseUrl: string, id: string): Promise<{ handoff: Handoff; body: string }> {
return request<{ handoff: Handoff; body: string }>(baseUrl, 'GET', `/handoffs/${id}`);
},
async createDecision(baseUrl: string, options: Partial<Decision>): Promise<Decision> {
return request<Decision>(baseUrl, 'POST', '/decisions', options);
},
async listDecisions(baseUrl: string): Promise<IndexEntry[]> {
return request<IndexEntry[]>(baseUrl, 'GET', '/decisions');
},
async addMemory(baseUrl: string, options: Partial<Memory>): Promise<Memory> {
return request<Memory>(baseUrl, 'POST', '/memory', options);
},
async searchMemory(baseUrl: string, query: string): Promise<IndexEntry[]> {
return request<IndexEntry[]>(baseUrl, 'GET', `/memory/search?q=${encodeURIComponent(query)}`);
},
async listMemory(baseUrl: string): Promise<IndexEntry[]> {
return request<IndexEntry[]>(baseUrl, 'GET', '/memory');
},
async sendMessage(baseUrl: string, options: Partial<Message>): Promise<Message> {
return request<Message>(baseUrl, 'POST', '/messages', options);
},
async getInbox(baseUrl: string, agent: string, unreadOnly = false): Promise<InboxMessage[]> {
const q = `?agent=${encodeURIComponent(agent)}${unreadOnly ? '&unread=1' : ''}`;
return request<InboxMessage[]>(baseUrl, 'GET', `/messages${q}`);
},
async markMessageRead(baseUrl: string, id: string): Promise<Message> {
return request<Message>(baseUrl, 'POST', `/messages/${id}/read`);
},
async ackMessage(baseUrl: string, id: string, by?: string): Promise<Message> {
return request<Message>(baseUrl, 'POST', `/messages/${id}/ack`, by ? { by } : undefined);
},
async getMessage(baseUrl: string, id: string): Promise<{ message: Message; body: string }> {
return request<{ message: Message; body: string }>(baseUrl, 'GET', `/messages/${id}`);
},
async createAsk(baseUrl: string, options: Partial<Ask>): Promise<Ask> {
return request<Ask>(baseUrl, 'POST', '/asks', options);
},
async listAsks(baseUrl: string, filters?: { to?: string; status?: string }): Promise<Ask[]> {
const params = new URLSearchParams((filters ?? {}) as Record<string, string>);
const qs = params.toString();
return request<Ask[]>(baseUrl, 'GET', `/asks${qs ? '?' + qs : ''}`);
},
async getAsk(baseUrl: string, id: string): Promise<{ ask: Ask; body: string }> {
return request<{ ask: Ask; body: string }>(baseUrl, 'GET', `/asks/${id}`);
},
async answerAsk(baseUrl: string, id: string, text: string, by?: string): Promise<Ask> {
return request<Ask>(baseUrl, 'POST', `/asks/${id}/answer`, { text, by });
},
async escalateAsk(baseUrl: string, id: string, note?: string): Promise<Ask> {
return request<Ask>(baseUrl, 'POST', `/asks/${id}/escalate`, note ? { note } : undefined);
},
async delegate(baseUrl: string, auto: boolean): Promise<{ suggestion?: { task: IndexEntry; role: string; preferredAgent: string }; handoff?: Handoff }> {
return request<{ suggestion?: { task: IndexEntry; role: string; preferredAgent: string }; handoff?: Handoff }>(baseUrl, 'POST', `/delegate?auto=${auto}`);
},
};