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>
This commit is contained in:
parent
6a4cd32312
commit
e5bd28488d
@ -43,8 +43,8 @@ export const remoteClient = {
|
||||
return request<HealthReport>(baseUrl, 'GET', '/health');
|
||||
},
|
||||
|
||||
async getAgentIdentity(baseUrl: string, agent: string): Promise<{ canonical: string; names: string[] }> {
|
||||
return request<{ canonical: string; names: string[] }>(
|
||||
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`,
|
||||
|
||||
@ -162,6 +162,17 @@ export const AgentConfigSchema = z.object({
|
||||
dispatch: AgentDispatch.default('loop'),
|
||||
/** Previous/alternate names that resolve to this canonical roster key. */
|
||||
aliases: z.array(z.string().min(1)).default([]),
|
||||
/**
|
||||
* Wie lange `agenthub_work` fuer DIESEN Agenten blockieren darf.
|
||||
*
|
||||
* Der Wert haengt am MCP-CLIENT, nicht am Hub: Kimi-Code bricht Requests nach
|
||||
* gut einer Minute ab (-32001), Codex vertraegt mehrere Minuten. Ein globaler
|
||||
* Default zwingt beide auf das Minimum des schwaechsten Clients — der
|
||||
* robustere Agent kehrt dann unnoetig oft leer zurueck, und JEDE Rueckkehr
|
||||
* ist eine Gelegenheit, aus dem Loop zu fallen. Genau daran unterschied sich
|
||||
* codex' Zuverlaessigkeit von kimis.
|
||||
*/
|
||||
workTimeoutSec: z.number().int().positive().max(600).optional(),
|
||||
/** Display model, e.g. "Opus 4.8", "Sonnet 4.6", "Kimi K2", "GPT-5 Codex". */
|
||||
model: z.string().optional(),
|
||||
/** Provider/company for the logo: "anthropic" | "openai" | "moonshot" | … */
|
||||
|
||||
@ -301,7 +301,19 @@ export async function startMcpServer(cwd: string): Promise<void> {
|
||||
? `No task in review for ${agent}, and no hub server to wait on.`
|
||||
: `No open task addressed to ${agent}, and no hub server to wait on.`;
|
||||
if (!remote) return asText(emptyMsg);
|
||||
const effectiveTimeout = timeoutSec ?? DEFAULT_WORK_TIMEOUT_SEC;
|
||||
// Timeout-Aufloesung: explizites Argument > Roster-Einstellung des Agenten
|
||||
// > globaler Default. Der Roster-Wert existiert, weil die Grenze am
|
||||
// MCP-CLIENT haengt: Kimi-Code bricht nach gut einer Minute ab, Codex
|
||||
// vertraegt Minuten. Ein globaler Default zwingt alle auf das Minimum des
|
||||
// schwaechsten Clients — der robustere Agent kehrt dann unnoetig oft leer
|
||||
// zurueck, und jede Rueckkehr ist eine Gelegenheit, aus dem Loop zu fallen.
|
||||
let rosterTimeout: number | undefined;
|
||||
if (timeoutSec === undefined) {
|
||||
try {
|
||||
rosterTimeout = (await remoteClient.getAgentIdentity(serverUrl!, agent)).workTimeoutSec;
|
||||
} catch { /* Roster nicht erreichbar — Default greift */ }
|
||||
}
|
||||
const effectiveTimeout = timeoutSec ?? rosterTimeout ?? DEFAULT_WORK_TIMEOUT_SEC;
|
||||
let result;
|
||||
try {
|
||||
result = await waitForTask(serverUrl!, finder, effectiveTimeout);
|
||||
|
||||
@ -148,7 +148,14 @@ export async function registerRoutes(app: FastifyInstance, cwd: string): Promise
|
||||
app.get('/agents/:agent/identity', async (request, reply) => {
|
||||
try {
|
||||
const identity = agentIdentity(cwd, (request.params as { agent: string }).agent);
|
||||
return { canonical: identity.canonical, names: [...identity.names] };
|
||||
// workTimeoutSec gehoert fachlich zur Agenten-Identitaet: "wer bin ich und
|
||||
// wie lange darf ich blockieren". Der MCP-Server holt es sich hier, statt
|
||||
// dass jeder Client es raten muss.
|
||||
let workTimeoutSec: number | undefined;
|
||||
try {
|
||||
workTimeoutSec = loadConfig(cwd).agents?.[identity.canonical]?.workTimeoutSec;
|
||||
} catch { /* ohne Config bleibt der Default */ }
|
||||
return { canonical: identity.canonical, names: [...identity.names], workTimeoutSec };
|
||||
} catch (err) {
|
||||
return badRequest(reply, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user