agent-lyceum 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/README.zh-TW.md +1 -0
- package/dist/adapters/claude.d.ts +12 -0
- package/dist/adapters/claude.js +38 -1
- package/dist/adapters/codex.d.ts +7 -0
- package/dist/adapters/codex.js +42 -1
- package/dist/adapters/types.d.ts +5 -0
- package/dist/call.d.ts +5 -0
- package/dist/call.js +34 -11
- package/dist/cli.js +3 -0
- package/dist/config.d.ts +26 -1
- package/dist/config.js +53 -3
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +14 -5
- package/dist/fallback.d.ts +111 -0
- package/dist/fallback.js +157 -0
- package/dist/policy.d.ts +2 -0
- package/dist/policy.js +4 -0
- package/dist/process-runner.d.ts +10 -0
- package/dist/process-runner.js +126 -14
- package/dist/run-outcome.d.ts +3 -11
- package/dist/run-outcome.js +5 -0
- package/dist/run-session.d.ts +8 -6
- package/dist/run-session.js +164 -83
- package/dist/run-store.d.ts +31 -6
- package/dist/run-store.js +23 -1
- package/dist/schema.d.ts +105 -0
- package/dist/schema.js +14 -0
- package/dist/status.d.ts +16 -7
- package/dist/status.js +49 -1
- package/dist/validate.js +36 -0
- package/package.json +1 -1
package/dist/run-session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { realInvoker } from "./adapters/index.js";
|
|
4
|
+
import { agentForCandidate, candidatesFor, Cooldowns, planAttempts, runWithFallback } from "./fallback.js";
|
|
4
5
|
import { briefOf, digestOf, doneContract, mergeSteps, parseSteps } from "./format.js";
|
|
5
6
|
import { ProtectedGuard } from "./guard.js";
|
|
6
7
|
import { addBatch, askersWithAnswers, askReplyPath, checkAnswers, describeCheck, isComplete, markDelivered, parseAskBody, readAskReply, renderAnswers, resolveOptionLetters, writeAskReply } from "./ask-reply.js";
|
|
@@ -35,6 +36,21 @@ function readIndex(file) {
|
|
|
35
36
|
function retryPrompt(prompt, error) {
|
|
36
37
|
return `${prompt}\n\n# Retry notice\nYour previous attempt at this wake-up failed${error ? ` (${error})` : ""} after possibly changing files already. Its mail was discarded. Check the current state of the repository before redoing the work, and send the mail again.`;
|
|
37
38
|
}
|
|
39
|
+
/** `runtime/model`, or the runtime alone for its default model. */
|
|
40
|
+
const modelLabel = (c) => (c.model ? `${c.runtime}/${c.model}` : c.runtime);
|
|
41
|
+
/** Each model given up on once, in order, with its last failure (retries on the same model are one entry). */
|
|
42
|
+
function lastPerModel(tried) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const t of tried) {
|
|
45
|
+
const last = out.at(-1);
|
|
46
|
+
const entry = { runtime: t.runtime, ...(t.model ? { model: t.model } : {}), ...(t.kind ? { kind: t.kind } : {}), ...(t.error ? { error: t.error } : {}) };
|
|
47
|
+
if (last && last.runtime === t.runtime && last.model === t.model)
|
|
48
|
+
out[out.length - 1] = entry;
|
|
49
|
+
else
|
|
50
|
+
out.push(entry);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
38
54
|
export class RunSession {
|
|
39
55
|
opts;
|
|
40
56
|
resume;
|
|
@@ -58,6 +74,8 @@ export class RunSession {
|
|
|
58
74
|
doneOutcome;
|
|
59
75
|
doneNote;
|
|
60
76
|
failure;
|
|
77
|
+
/** Models cooling down after a rate limit or quota error; one table for the run, shared by parallel wake-ups. */
|
|
78
|
+
cooldowns;
|
|
61
79
|
doneVerification;
|
|
62
80
|
/** The accepted `done` carried `open_pr: true` (only the lead can send `done`). */
|
|
63
81
|
openPrRequested = false;
|
|
@@ -225,6 +243,7 @@ export class RunSession {
|
|
|
225
243
|
this.log = log;
|
|
226
244
|
this.guard = guard;
|
|
227
245
|
this.state = state;
|
|
246
|
+
this.cooldowns = Cooldowns.from(state.cooldowns);
|
|
228
247
|
// Only a segment that starts here has a baseline: a fresh run or a follow-up reopen. A plain resume continues one begun by an earlier process.
|
|
229
248
|
this.sharedAtStart = !resume || opts.followup ? readIndex(sharedIndex(project)) : undefined;
|
|
230
249
|
}
|
|
@@ -273,7 +292,7 @@ export class RunSession {
|
|
|
273
292
|
return pending;
|
|
274
293
|
}
|
|
275
294
|
async wake(agent) {
|
|
276
|
-
const { invoke, say, cfg, runDir, project, log, state, claims, attempts, noSessionNoted, opts } = this;
|
|
295
|
+
const { invoke, say, cfg, runDir, project, log, state, claims, attempts, noSessionNoted, opts, cooldowns } = this;
|
|
277
296
|
const queue = listUnread(project, agent.name);
|
|
278
297
|
if (queue.length === 0)
|
|
279
298
|
return { ok: true };
|
|
@@ -293,92 +312,153 @@ export class RunSession {
|
|
|
293
312
|
userPrompt: buildUserPrompt(project, agent, queue, unread.length, (state.reopened?.length ?? 0) > 0),
|
|
294
313
|
timeoutSec: cfg.wake_timeout_sec,
|
|
295
314
|
};
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
315
|
+
// Without a fallback chain (one candidate) the loop, its output and its records are exactly those of plain retries (spec 20.2 decision 11).
|
|
316
|
+
const all = candidatesFor(agent);
|
|
317
|
+
const chain = all.length > 1;
|
|
318
|
+
const candidates = chain ? planAttempts(agent, cooldowns, Date.now()) : all;
|
|
319
|
+
// The model a backup's calls stand in for: the one switched away from in this wake, or the cooling-down primary it started past.
|
|
320
|
+
let replaced = candidates[0]?.primary ? undefined : all[0];
|
|
321
|
+
let calls = 0;
|
|
322
|
+
let stoppedByCancel = false;
|
|
323
|
+
const outcome = await runWithFallback({
|
|
324
|
+
candidates,
|
|
325
|
+
legacy: !chain,
|
|
326
|
+
retry: cfg.retry,
|
|
327
|
+
cooldowns,
|
|
328
|
+
cooldownSec: cfg.fallback_cooldown_sec,
|
|
329
|
+
signal: opts.signal,
|
|
330
|
+
sleep: opts.sleep,
|
|
331
|
+
beforeAttempt: (_c, ctx) => {
|
|
332
|
+
// A cancel that came during the previous call: no new model is tried (without a chain the call still goes ahead, as before).
|
|
333
|
+
if (chain && ctx.attempt > 0 && opts.signal?.aborted) {
|
|
334
|
+
stoppedByCancel = true;
|
|
335
|
+
return "stop";
|
|
336
|
+
}
|
|
299
337
|
// The round limit (when one is set) ends the run; this message stays unread for the resume.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
state.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
round,
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
338
|
+
return state.max_rounds > 0 && state.rounds >= state.max_rounds ? "stop" : undefined;
|
|
339
|
+
},
|
|
340
|
+
onSwitch: (from, to, kind) => {
|
|
341
|
+
replaced = from;
|
|
342
|
+
this.syncCooldowns();
|
|
343
|
+
if (opts.signal?.aborted)
|
|
344
|
+
return;
|
|
345
|
+
say(` ${agent.name} falling back to ${modelLabel(to)} (${kind ?? "unknown error"})`);
|
|
346
|
+
this.note(`${agent.name} fell back from ${modelLabel(from)} to ${modelLabel(to)} (${kind ?? "unknown error"}); the backup starts a fresh session and redoes this wake-up. ` +
|
|
347
|
+
`Anything the failed attempts already did outside the mailbox (file edits, commands) was not undone: side effects may have happened.`);
|
|
348
|
+
},
|
|
349
|
+
invoke: async (c, ctx) => {
|
|
350
|
+
calls++;
|
|
351
|
+
this.syncCooldowns();
|
|
352
|
+
const attempt = ctx.attempt;
|
|
353
|
+
// A backup runs as the same agent on another runtime/model, in a session of its own.
|
|
354
|
+
const runAs = agentForCandidate(agent, c);
|
|
355
|
+
const sessionKey = c.primary ? agent.name : `${agent.name}@${c.key}`;
|
|
356
|
+
const round = ++state.rounds;
|
|
357
|
+
const started = Date.now();
|
|
358
|
+
const att = beginAttempt(runDir, claim.id);
|
|
359
|
+
const retryN = chain ? ctx.candidateAttempt : attempt;
|
|
360
|
+
say(`[round ${roundsLabel(state.rounds, state.max_rounds)}] waking ${agent.name} (${chain ? modelLabel(c) : agent.runtime})${retryN ? ` — retry ${retryN}` : ""}`);
|
|
361
|
+
const sessionId = agent.resume ? state.sessions[sessionKey] : undefined;
|
|
362
|
+
const handling = unread.map((m) => ({ from: m.meta.from, type: m.meta.type, subject: m.meta.subject, brief: briefOf(m.body), digest: digestOf(m.meta.type, m.body) }));
|
|
363
|
+
// What the panel shows for the message being worked on is uncut; the wake record below keeps the short digest.
|
|
364
|
+
const activeHandling = unread.map((m, i) => ({ ...handling[i], digest: digestOf(m.meta.type, m.body, true) }));
|
|
365
|
+
state.active[agent.name] = { round, since: new Date(started).toISOString(), handling: activeHandling };
|
|
366
|
+
this.saveState();
|
|
367
|
+
const result = await invoke({
|
|
368
|
+
...base,
|
|
369
|
+
agent: runAs,
|
|
370
|
+
signal: opts.signal,
|
|
371
|
+
logDir: attemptLogDir(runDir, att.id),
|
|
372
|
+
userPrompt: attempt ? retryPrompt(base.userPrompt, ctx.previous?.error) : base.userPrompt,
|
|
373
|
+
sessionId,
|
|
374
|
+
}).catch((e) => ({
|
|
375
|
+
ok: false,
|
|
376
|
+
text: "",
|
|
377
|
+
exitCode: null,
|
|
378
|
+
timedOut: false,
|
|
379
|
+
error: e.message,
|
|
380
|
+
}));
|
|
381
|
+
if (result.sessionId && agent.resume)
|
|
382
|
+
state.sessions[sessionKey] = result.sessionId;
|
|
383
|
+
if (result.outputTokens)
|
|
384
|
+
state.output_tokens += result.outputTokens;
|
|
385
|
+
delete state.active[agent.name];
|
|
386
|
+
// With a chain only: which model ran, what it replaced, why it failed.
|
|
387
|
+
const which = chain ? { runtime: c.runtime, model: c.model, fallback_from: c.primary || !replaced ? undefined : modelLabel(replaced), kind: result.kind } : {};
|
|
388
|
+
state.wakes.push({
|
|
389
|
+
round,
|
|
390
|
+
agent: agent.name,
|
|
391
|
+
at: new Date().toISOString(),
|
|
392
|
+
duration_ms: Date.now() - started,
|
|
393
|
+
ok: result.ok,
|
|
394
|
+
output_tokens: result.outputTokens,
|
|
395
|
+
handling,
|
|
396
|
+
error: result.error,
|
|
397
|
+
...which,
|
|
398
|
+
});
|
|
399
|
+
state.last_wake[agent.name] = { at: new Date().toISOString(), ok: result.ok, error: result.error, ...(chain ? { runtime: c.runtime, model: c.model } : {}) };
|
|
400
|
+
log("wake", {
|
|
401
|
+
agent: agent.name,
|
|
402
|
+
runtime: c.runtime,
|
|
403
|
+
attempt,
|
|
404
|
+
ok: result.ok,
|
|
405
|
+
exit_code: result.exitCode,
|
|
406
|
+
timed_out: result.timedOut,
|
|
407
|
+
duration_ms: Date.now() - started,
|
|
408
|
+
messages: unread.map((m) => m.meta.id),
|
|
409
|
+
error: result.error,
|
|
410
|
+
...(chain ? { model: c.model, fallback_from: which.fallback_from, kind: result.kind } : {}),
|
|
411
|
+
});
|
|
412
|
+
this.saveState();
|
|
413
|
+
if (result.ok) {
|
|
414
|
+
attempts.set(agent.name, finishAttempt(runDir, att.id, "output_ready"));
|
|
415
|
+
if (agent.resume && !result.sessionId && !state.sessions[sessionKey] && !noSessionNoted.has(agent.name)) {
|
|
416
|
+
noSessionNoted.add(agent.name);
|
|
417
|
+
this.note(`${agent.name} has resume: true but its ${c.runtime} CLI reported no session id, so every wake-up starts a fresh session.`);
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
354
420
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
421
|
+
if (result.cancelled) {
|
|
422
|
+
// Stopped on purpose: not a failure to report. Its output is set aside (its input stays unread for the resume, below).
|
|
423
|
+
const parked = parkOutbox(runDir, outboxDir(project, agent.name), att.id);
|
|
424
|
+
finishAttempt(runDir, att.id, "failed", { error: "cancelled", parked });
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
// A failed attempt's output is never trusted: park it under the attempt, away from the next attempt's result.
|
|
359
428
|
const parked = parkOutbox(runDir, outboxDir(project, agent.name), att.id);
|
|
360
|
-
finishAttempt(runDir, att.id, "failed", { error:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
return
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
`Anything it already did outside its mailbox (file edits, commands) was not undone: side effects may have happened.`);
|
|
372
|
-
}
|
|
373
|
-
if (result?.ok)
|
|
429
|
+
finishAttempt(runDir, att.id, "failed", { error: result.error, parked });
|
|
430
|
+
say(` ${agent.name} failed: ${result.error ?? "unknown error"}`);
|
|
431
|
+
this.note(`${agent.name} attempt ${att.n} failed (${result.error ?? "unknown error"})${parked.length ? `; ${parked.length} output file(s) set aside in ${path.join("mail", "attempts", att.id, "outbox")}` : ""}. ` +
|
|
432
|
+
`Anything it already did outside its mailbox (file edits, commands) was not undone: side effects may have happened.`);
|
|
433
|
+
return result;
|
|
434
|
+
},
|
|
435
|
+
});
|
|
436
|
+
if (this.syncCooldowns())
|
|
437
|
+
this.saveState();
|
|
438
|
+
const { result } = outcome;
|
|
439
|
+
if (result?.ok) {
|
|
374
440
|
markOutputReady(runDir, claim.id);
|
|
375
|
-
|
|
376
|
-
// The agent failed every time it was called: the run stops here (see settle) and its message stays unread for the resume.
|
|
377
|
-
abandonClaim(runDir, claim.id);
|
|
378
|
-
claims.delete(agent.name);
|
|
379
|
-
return { ok: false, error: result?.error, attempts: cfg.retry + 1 };
|
|
441
|
+
return { ok: true };
|
|
380
442
|
}
|
|
381
|
-
|
|
443
|
+
// The input stays unread for the resume: the round limit, or a cancel (during a call or a backoff).
|
|
444
|
+
abandonClaim(runDir, claim.id);
|
|
445
|
+
claims.delete(agent.name);
|
|
446
|
+
if (outcome.stopped === "beforeAttempt" && !stoppedByCancel)
|
|
447
|
+
return { ok: false, cancelled: true };
|
|
448
|
+
if (result?.cancelled || outcome.stopped) {
|
|
449
|
+
say(` ${agent.name} stopped (cancelled)`);
|
|
450
|
+
return { ok: false, cancelled: true };
|
|
451
|
+
}
|
|
452
|
+
// The agent failed every time it was called: the run stops here (see settle) and its message stays unread for the resume.
|
|
453
|
+
return { ok: false, error: result?.error, attempts: calls, ...(chain ? { tried: lastPerModel(outcome.tried) } : {}) };
|
|
454
|
+
}
|
|
455
|
+
/** Copy the shared cooldown table into the state (only once something has cooled down); true when it was. */
|
|
456
|
+
syncCooldowns() {
|
|
457
|
+
const table = this.cooldowns.toJSON();
|
|
458
|
+
if (!Object.keys(table).length)
|
|
459
|
+
return false;
|
|
460
|
+
this.state.cooldowns = table;
|
|
461
|
+
return true;
|
|
382
462
|
}
|
|
383
463
|
/**
|
|
384
464
|
* A finished member's files go from its worktree into the repo before anyone reads its mail, so the lead
|
|
@@ -543,7 +623,8 @@ export class RunSession {
|
|
|
543
623
|
const failedIdx = results.findIndex((r) => !r.ok && !r.cancelled);
|
|
544
624
|
if (failedIdx >= 0) {
|
|
545
625
|
const failed = must(batch[failedIdx], "failed agent");
|
|
546
|
-
|
|
626
|
+
const r = results[failedIdx];
|
|
627
|
+
this.failure = { agent: failed.name, error: r?.error, attempts: r?.attempts ?? this.cfg.retry + 1, ...(r?.tried ? { tried: r.tried } : {}) };
|
|
547
628
|
state.failure = this.failure;
|
|
548
629
|
this.endReason = failed.name === project.lead ? "lead_failed" : "agent_failed";
|
|
549
630
|
return;
|
package/dist/run-store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResolvedProject } from "./config.js";
|
|
2
2
|
import type { DigestSection, Step } from "./format.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type ErrorKind, type Runtime, type RunOutcome } from "./schema.js";
|
|
4
4
|
import type { ResultBranch } from "./worktree.js";
|
|
5
5
|
export declare const STATE_FILE = "state.json";
|
|
6
6
|
export declare const STATE_SCHEMA_VERSION = 2;
|
|
@@ -36,6 +36,27 @@ export interface WakeRecord {
|
|
|
36
36
|
/** Mail this wake handed to others; `to` is "(done)" for the lead's final message. */
|
|
37
37
|
sent?: SentTopic[];
|
|
38
38
|
error?: string;
|
|
39
|
+
/** With a fallback chain only: the runtime and model this call ran on. */
|
|
40
|
+
runtime?: Runtime;
|
|
41
|
+
model?: string;
|
|
42
|
+
/** With a fallback chain only: the `runtime/model` this call replaced (set on a backup's calls). */
|
|
43
|
+
fallback_from?: string;
|
|
44
|
+
/** With a fallback chain only: why the call failed. */
|
|
45
|
+
kind?: ErrorKind;
|
|
46
|
+
}
|
|
47
|
+
/** A model given up on during a wake with a fallback chain, for `failure.tried`. */
|
|
48
|
+
export interface TriedModel {
|
|
49
|
+
runtime: Runtime;
|
|
50
|
+
model?: string;
|
|
51
|
+
kind?: ErrorKind;
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
/** The run stopped because an agent kept failing: who, the last error, how many times it was called; `tried` only with a fallback chain. */
|
|
55
|
+
export interface RunFailure {
|
|
56
|
+
agent: string;
|
|
57
|
+
error?: string;
|
|
58
|
+
attempts: number;
|
|
59
|
+
tried?: TriedModel[];
|
|
39
60
|
}
|
|
40
61
|
export interface RunState {
|
|
41
62
|
/** 1 for runs written before versioning; 2 for runs with their own mailboxes. */
|
|
@@ -84,10 +105,13 @@ export interface RunState {
|
|
|
84
105
|
pr_url?: string;
|
|
85
106
|
sessions: Record<string, string>;
|
|
86
107
|
output_tokens: number;
|
|
108
|
+
/** `runtime`/`model` only for agents with a fallback chain. */
|
|
87
109
|
last_wake: Record<string, {
|
|
88
110
|
at: string;
|
|
89
111
|
ok: boolean;
|
|
90
112
|
error?: string;
|
|
113
|
+
runtime?: Runtime;
|
|
114
|
+
model?: string;
|
|
91
115
|
}>;
|
|
92
116
|
/** Process running this dispatcher, so a monitor can tell "running" from "interrupted". */
|
|
93
117
|
pid: number;
|
|
@@ -98,11 +122,12 @@ export interface RunState {
|
|
|
98
122
|
/** Finished wakes, oldest first. */
|
|
99
123
|
wakes: WakeRecord[];
|
|
100
124
|
/** Set when the run stopped because an agent kept failing: who, the last error, how many times it was called. */
|
|
101
|
-
failure?:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
125
|
+
failure?: RunFailure;
|
|
126
|
+
/** Models cooling down after `rate_limit`/`quota` (`<runtime>/<model>` → until when); absent until one is. */
|
|
127
|
+
cooldowns?: Record<string, {
|
|
128
|
+
until: string;
|
|
129
|
+
kind: ErrorKind;
|
|
130
|
+
}>;
|
|
106
131
|
/** Checklist progress: seeded from the task, then replaced by the lead's latest `## Steps` (after a reopen, added to `step_base`). */
|
|
107
132
|
steps?: Step[];
|
|
108
133
|
/** The checklist as it stood at the last reopen: the follow-up's steps are shown after it, and its open steps do not hold up the follow-up's done. */
|
package/dist/run-store.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { atomicWrite } from "./fs-util.js";
|
|
6
|
+
import { ERROR_KINDS, RUNTIMES } from "./schema.js";
|
|
6
7
|
export const STATE_FILE = "state.json";
|
|
7
8
|
export const STATE_SCHEMA_VERSION = 2;
|
|
8
9
|
/** `20261006T010203456Z-<128 random bits>`: sorts by start time and cannot collide, even for two starts in one millisecond. */
|
|
@@ -11,6 +12,19 @@ export function newRunId(d = new Date()) {
|
|
|
11
12
|
return `${t}-${crypto.randomBytes(16).toString("hex")}`;
|
|
12
13
|
}
|
|
13
14
|
const nonNeg = z.number().finite();
|
|
15
|
+
const CooldownEntry = z.object({ until: z.string(), kind: z.enum(ERROR_KINDS) });
|
|
16
|
+
/** `state.cooldowns` is advisory: keep the entries that parse and drop the rest, so one bad entry never makes the run unreadable. */
|
|
17
|
+
function lenientCooldowns(raw) {
|
|
18
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
19
|
+
return undefined;
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
22
|
+
const e = CooldownEntry.safeParse(v);
|
|
23
|
+
if (e.success)
|
|
24
|
+
out[k] = e.data;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
14
28
|
const WireState = z
|
|
15
29
|
.object({
|
|
16
30
|
schema_version: z.number().int().optional(),
|
|
@@ -39,7 +53,15 @@ const WireState = z
|
|
|
39
53
|
wakes: z.array(z.any()).optional(),
|
|
40
54
|
solo_agent: z.string().optional(),
|
|
41
55
|
steps: z.array(z.object({ text: z.string(), done: z.boolean() })).optional(),
|
|
42
|
-
failure: z
|
|
56
|
+
failure: z
|
|
57
|
+
.object({
|
|
58
|
+
agent: z.string(),
|
|
59
|
+
error: z.string().optional(),
|
|
60
|
+
attempts: z.number(),
|
|
61
|
+
tried: z.array(z.object({ runtime: z.enum(RUNTIMES), model: z.string().optional(), kind: z.enum(ERROR_KINDS).optional(), error: z.string().optional() })).optional(),
|
|
62
|
+
})
|
|
63
|
+
.optional(),
|
|
64
|
+
cooldowns: z.unknown().transform(lenientCooldowns).optional(),
|
|
43
65
|
step_base: z.array(z.object({ text: z.string(), done: z.boolean() })).optional(),
|
|
44
66
|
name: z.string().optional(),
|
|
45
67
|
reopened: z.array(z.object({ at: z.string() })).optional(),
|
package/dist/schema.d.ts
CHANGED
|
@@ -11,6 +11,32 @@ export declare function inferRuntime(model: string | undefined): Runtime | undef
|
|
|
11
11
|
/** How much of the web an agent may use: nothing, search only, or search plus fetching pages. Unset leaves each runtime's own default alone. */
|
|
12
12
|
export declare const WEB_LEVELS: readonly ["none", "search", "fetch"];
|
|
13
13
|
export type WebLevel = (typeof WEB_LEVELS)[number];
|
|
14
|
+
/** Why a wake failed, as far as the CLI's output reveals; decides whether to retry, fall back, or stop. */
|
|
15
|
+
export declare const ERROR_KINDS: readonly ["rate_limit", "quota", "timeout", "network", "crash", "auth", "unknown"];
|
|
16
|
+
export type ErrorKind = (typeof ERROR_KINDS)[number];
|
|
17
|
+
/** One link of a fallback chain after resolution: runtime and model are filled in (model "" = the runtime's own default). */
|
|
18
|
+
export interface FallbackEntry {
|
|
19
|
+
runtime: Runtime;
|
|
20
|
+
model: string;
|
|
21
|
+
effort?: Effort;
|
|
22
|
+
}
|
|
23
|
+
/** A fallback entry as written in a file: `runtime` may be omitted (inferred from `model`, else the agent's). */
|
|
24
|
+
export declare const FallbackEntryPartial: z.ZodObject<{
|
|
25
|
+
runtime: z.ZodOptional<z.ZodEnum<{
|
|
26
|
+
"claude-code": "claude-code";
|
|
27
|
+
codex: "codex";
|
|
28
|
+
}>>;
|
|
29
|
+
model: z.ZodOptional<z.ZodString>;
|
|
30
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
31
|
+
high: "high";
|
|
32
|
+
low: "low";
|
|
33
|
+
max: "max";
|
|
34
|
+
medium: "medium";
|
|
35
|
+
minimal: "minimal";
|
|
36
|
+
xhigh: "xhigh";
|
|
37
|
+
}>>;
|
|
38
|
+
}, z.core.$strict>;
|
|
39
|
+
export type FallbackEntryPartialT = z.infer<typeof FallbackEntryPartial>;
|
|
14
40
|
export declare const AgentPartial: z.ZodObject<{
|
|
15
41
|
runtime: z.ZodOptional<z.ZodEnum<{
|
|
16
42
|
"claude-code": "claude-code";
|
|
@@ -39,6 +65,21 @@ export declare const AgentPartial: z.ZodObject<{
|
|
|
39
65
|
none: "none";
|
|
40
66
|
search: "search";
|
|
41
67
|
}>>;
|
|
68
|
+
fallback: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
69
|
+
runtime: z.ZodOptional<z.ZodEnum<{
|
|
70
|
+
"claude-code": "claude-code";
|
|
71
|
+
codex: "codex";
|
|
72
|
+
}>>;
|
|
73
|
+
model: z.ZodOptional<z.ZodString>;
|
|
74
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
75
|
+
high: "high";
|
|
76
|
+
low: "low";
|
|
77
|
+
max: "max";
|
|
78
|
+
medium: "medium";
|
|
79
|
+
minimal: "minimal";
|
|
80
|
+
xhigh: "xhigh";
|
|
81
|
+
}>>;
|
|
82
|
+
}, z.core.$strict>>>;
|
|
42
83
|
owns: z.ZodOptional<z.ZodUnknown>;
|
|
43
84
|
}, z.core.$strict>;
|
|
44
85
|
export type AgentPartialT = z.infer<typeof AgentPartial>;
|
|
@@ -51,6 +92,22 @@ export declare const DispatcherPartial: z.ZodObject<{
|
|
|
51
92
|
strict: z.ZodOptional<z.ZodBoolean>;
|
|
52
93
|
workspace_mode: z.ZodOptional<z.ZodUnknown>;
|
|
53
94
|
log_max_bytes: z.ZodOptional<z.ZodNumber>;
|
|
95
|
+
fallback: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
96
|
+
runtime: z.ZodOptional<z.ZodEnum<{
|
|
97
|
+
"claude-code": "claude-code";
|
|
98
|
+
codex: "codex";
|
|
99
|
+
}>>;
|
|
100
|
+
model: z.ZodOptional<z.ZodString>;
|
|
101
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
102
|
+
high: "high";
|
|
103
|
+
low: "low";
|
|
104
|
+
max: "max";
|
|
105
|
+
medium: "medium";
|
|
106
|
+
minimal: "minimal";
|
|
107
|
+
xhigh: "xhigh";
|
|
108
|
+
}>>;
|
|
109
|
+
}, z.core.$strict>>>;
|
|
110
|
+
fallback_cooldown_sec: z.ZodOptional<z.ZodNumber>;
|
|
54
111
|
}, z.core.$strict>;
|
|
55
112
|
/** Settings that no longer exist; files that still carry them load, and `validate` warns. */
|
|
56
113
|
export declare const REMOVED_DISPATCHER_KEYS: readonly ["max_parallel", "max_concurrent_runs", "workspace_mode"];
|
|
@@ -84,6 +141,21 @@ export declare const GlobalConfig: z.ZodObject<{
|
|
|
84
141
|
none: "none";
|
|
85
142
|
search: "search";
|
|
86
143
|
}>>;
|
|
144
|
+
fallback: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
145
|
+
runtime: z.ZodOptional<z.ZodEnum<{
|
|
146
|
+
"claude-code": "claude-code";
|
|
147
|
+
codex: "codex";
|
|
148
|
+
}>>;
|
|
149
|
+
model: z.ZodOptional<z.ZodString>;
|
|
150
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
151
|
+
high: "high";
|
|
152
|
+
low: "low";
|
|
153
|
+
max: "max";
|
|
154
|
+
medium: "medium";
|
|
155
|
+
minimal: "minimal";
|
|
156
|
+
xhigh: "xhigh";
|
|
157
|
+
}>>;
|
|
158
|
+
}, z.core.$strict>>>;
|
|
87
159
|
owns: z.ZodOptional<z.ZodUnknown>;
|
|
88
160
|
}, z.core.$strict>>>;
|
|
89
161
|
}, z.core.$strict>;
|
|
@@ -101,6 +173,22 @@ export declare const ProjectConfig: z.ZodObject<{
|
|
|
101
173
|
strict: z.ZodOptional<z.ZodBoolean>;
|
|
102
174
|
workspace_mode: z.ZodOptional<z.ZodUnknown>;
|
|
103
175
|
log_max_bytes: z.ZodOptional<z.ZodNumber>;
|
|
176
|
+
fallback: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
177
|
+
runtime: z.ZodOptional<z.ZodEnum<{
|
|
178
|
+
"claude-code": "claude-code";
|
|
179
|
+
codex: "codex";
|
|
180
|
+
}>>;
|
|
181
|
+
model: z.ZodOptional<z.ZodString>;
|
|
182
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
183
|
+
high: "high";
|
|
184
|
+
low: "low";
|
|
185
|
+
max: "max";
|
|
186
|
+
medium: "medium";
|
|
187
|
+
minimal: "minimal";
|
|
188
|
+
xhigh: "xhigh";
|
|
189
|
+
}>>;
|
|
190
|
+
}, z.core.$strict>>>;
|
|
191
|
+
fallback_cooldown_sec: z.ZodOptional<z.ZodNumber>;
|
|
104
192
|
}, z.core.$strict>>;
|
|
105
193
|
agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
106
194
|
runtime: z.ZodOptional<z.ZodEnum<{
|
|
@@ -130,6 +218,21 @@ export declare const ProjectConfig: z.ZodObject<{
|
|
|
130
218
|
none: "none";
|
|
131
219
|
search: "search";
|
|
132
220
|
}>>;
|
|
221
|
+
fallback: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
222
|
+
runtime: z.ZodOptional<z.ZodEnum<{
|
|
223
|
+
"claude-code": "claude-code";
|
|
224
|
+
codex: "codex";
|
|
225
|
+
}>>;
|
|
226
|
+
model: z.ZodOptional<z.ZodString>;
|
|
227
|
+
effort: z.ZodOptional<z.ZodEnum<{
|
|
228
|
+
high: "high";
|
|
229
|
+
low: "low";
|
|
230
|
+
max: "max";
|
|
231
|
+
medium: "medium";
|
|
232
|
+
minimal: "minimal";
|
|
233
|
+
xhigh: "xhigh";
|
|
234
|
+
}>>;
|
|
235
|
+
}, z.core.$strict>>>;
|
|
133
236
|
owns: z.ZodOptional<z.ZodUnknown>;
|
|
134
237
|
}, z.core.$strict>>>;
|
|
135
238
|
}, z.core.$strict>;
|
|
@@ -140,6 +243,8 @@ export interface DispatcherSettings {
|
|
|
140
243
|
strict: boolean;
|
|
141
244
|
/** log.jsonl is rotated to log.1.jsonl past this size; 0 = never. */
|
|
142
245
|
log_max_bytes: number;
|
|
246
|
+
/** How long a primary that hit rate_limit/quota is skipped within a run, unless the CLI says when to come back. */
|
|
247
|
+
fallback_cooldown_sec: number;
|
|
143
248
|
}
|
|
144
249
|
export declare const DISPATCHER_DEFAULTS: DispatcherSettings;
|
|
145
250
|
/** How a run turned out, independent of why it stopped (`EndReason`). Only "completed" means success. */
|
package/dist/schema.js
CHANGED
|
@@ -20,12 +20,22 @@ export function inferRuntime(model) {
|
|
|
20
20
|
}
|
|
21
21
|
/** How much of the web an agent may use: nothing, search only, or search plus fetching pages. Unset leaves each runtime's own default alone. */
|
|
22
22
|
export const WEB_LEVELS = ["none", "search", "fetch"];
|
|
23
|
+
/** Why a wake failed, as far as the CLI's output reveals; decides whether to retry, fall back, or stop. */
|
|
24
|
+
export const ERROR_KINDS = ["rate_limit", "quota", "timeout", "network", "crash", "auth", "unknown"];
|
|
23
25
|
const Memory = z
|
|
24
26
|
.object({
|
|
25
27
|
global: z.string().optional(),
|
|
26
28
|
project: z.string().optional(),
|
|
27
29
|
})
|
|
28
30
|
.strict();
|
|
31
|
+
/** A fallback entry as written in a file: `runtime` may be omitted (inferred from `model`, else the agent's). */
|
|
32
|
+
export const FallbackEntryPartial = z
|
|
33
|
+
.object({
|
|
34
|
+
runtime: z.enum(RUNTIMES).optional(),
|
|
35
|
+
model: z.string().optional(),
|
|
36
|
+
effort: z.enum(EFFORTS).optional(),
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
29
39
|
export const AgentPartial = z
|
|
30
40
|
.object({
|
|
31
41
|
runtime: z.enum(RUNTIMES).optional(),
|
|
@@ -38,6 +48,7 @@ export const AgentPartial = z
|
|
|
38
48
|
can_edit_agent_md: z.boolean().optional(),
|
|
39
49
|
can_ask_user: z.boolean().optional(),
|
|
40
50
|
allow_web: z.enum(WEB_LEVELS).optional(),
|
|
51
|
+
fallback: z.array(FallbackEntryPartial).optional(),
|
|
41
52
|
/** Removed setting: still accepted (any value) so old files load; ignored, with a warning from `validate`. */
|
|
42
53
|
owns: z.unknown().optional(),
|
|
43
54
|
})
|
|
@@ -52,6 +63,8 @@ export const DispatcherPartial = z
|
|
|
52
63
|
strict: z.boolean().optional(),
|
|
53
64
|
workspace_mode: z.unknown().optional(),
|
|
54
65
|
log_max_bytes: z.number().int().min(0).optional(),
|
|
66
|
+
fallback: z.array(FallbackEntryPartial).optional(),
|
|
67
|
+
fallback_cooldown_sec: z.number().int().min(0).optional(),
|
|
55
68
|
})
|
|
56
69
|
.strict();
|
|
57
70
|
/** Settings that no longer exist; files that still carry them load, and `validate` warns. */
|
|
@@ -76,6 +89,7 @@ export const DISPATCHER_DEFAULTS = {
|
|
|
76
89
|
retry: 2,
|
|
77
90
|
strict: false,
|
|
78
91
|
log_max_bytes: 8 * 1024 * 1024,
|
|
92
|
+
fallback_cooldown_sec: 300,
|
|
79
93
|
};
|
|
80
94
|
/** How a run turned out, independent of why it stopped (`EndReason`). Only "completed" means success. */
|
|
81
95
|
export const RUN_OUTCOMES = ["completed", "partial", "blocked", "failed", "waiting", "cancelled"];
|