agent-lyceum 0.17.3 → 0.19.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 +4 -3
- package/README.zh-TW.md +4 -3
- package/dist/adapters/claude.d.ts +12 -0
- package/dist/adapters/claude.js +39 -8
- 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 +35 -13
- package/dist/cli.js +17 -9
- package/dist/config.d.ts +36 -7
- package/dist/config.js +75 -8
- package/dist/demo.js +0 -1
- package/dist/dispatcher.d.ts +2 -1
- package/dist/dispatcher.js +3 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +25 -17
- package/dist/fallback.d.ts +111 -0
- package/dist/fallback.js +157 -0
- package/dist/paths.d.ts +0 -2
- package/dist/paths.js +0 -11
- package/dist/policy.d.ts +2 -6
- package/dist/policy.js +6 -10
- package/dist/process-runner.js +4 -2
- package/dist/project-lock.d.ts +10 -2
- package/dist/project-lock.js +23 -12
- package/dist/prompt.js +0 -3
- package/dist/run-outcome.d.ts +3 -11
- package/dist/run-outcome.js +5 -0
- package/dist/run-session.d.ts +11 -7
- package/dist/run-session.js +184 -113
- package/dist/run-store.d.ts +32 -7
- package/dist/run-store.js +23 -1
- package/dist/scaffold.js +0 -5
- package/dist/schema.d.ts +117 -22
- package/dist/schema.js +22 -7
- package/dist/solo.js +1 -2
- package/dist/status.d.ts +16 -7
- package/dist/status.js +49 -1
- package/dist/validate.js +41 -38
- package/dist/worktree.d.ts +19 -12
- package/dist/worktree.js +35 -74
- package/package.json +1 -1
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,23 +65,53 @@ export declare const AgentPartial: z.ZodObject<{
|
|
|
39
65
|
none: "none";
|
|
40
66
|
search: "search";
|
|
41
67
|
}>>;
|
|
42
|
-
|
|
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>>>;
|
|
83
|
+
owns: z.ZodOptional<z.ZodUnknown>;
|
|
43
84
|
}, z.core.$strict>;
|
|
44
85
|
export type AgentPartialT = z.infer<typeof AgentPartial>;
|
|
45
86
|
export declare const DispatcherPartial: z.ZodObject<{
|
|
46
87
|
max_rounds: z.ZodOptional<z.ZodNumber>;
|
|
47
|
-
max_parallel: z.ZodOptional<z.
|
|
48
|
-
max_concurrent_runs: z.ZodOptional<z.
|
|
88
|
+
max_parallel: z.ZodOptional<z.ZodUnknown>;
|
|
89
|
+
max_concurrent_runs: z.ZodOptional<z.ZodUnknown>;
|
|
49
90
|
wake_timeout_sec: z.ZodOptional<z.ZodNumber>;
|
|
50
91
|
retry: z.ZodOptional<z.ZodNumber>;
|
|
51
92
|
strict: z.ZodOptional<z.ZodBoolean>;
|
|
52
|
-
workspace_mode: z.ZodOptional<z.
|
|
53
|
-
auto: "auto";
|
|
54
|
-
shared: "shared";
|
|
55
|
-
worktree: "worktree";
|
|
56
|
-
}>>;
|
|
93
|
+
workspace_mode: z.ZodOptional<z.ZodUnknown>;
|
|
57
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>;
|
|
58
111
|
}, z.core.$strict>;
|
|
112
|
+
/** Settings that no longer exist; files that still carry them load, and `validate` warns. */
|
|
113
|
+
export declare const REMOVED_DISPATCHER_KEYS: readonly ["max_parallel", "max_concurrent_runs", "workspace_mode"];
|
|
114
|
+
export declare const REMOVED_AGENT_KEYS: readonly ["owns"];
|
|
59
115
|
export declare const GlobalConfig: z.ZodObject<{
|
|
60
116
|
agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
61
117
|
runtime: z.ZodOptional<z.ZodEnum<{
|
|
@@ -85,7 +141,22 @@ export declare const GlobalConfig: z.ZodObject<{
|
|
|
85
141
|
none: "none";
|
|
86
142
|
search: "search";
|
|
87
143
|
}>>;
|
|
88
|
-
|
|
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>>>;
|
|
159
|
+
owns: z.ZodOptional<z.ZodUnknown>;
|
|
89
160
|
}, z.core.$strict>>>;
|
|
90
161
|
}, z.core.$strict>;
|
|
91
162
|
export declare const ProjectConfig: z.ZodObject<{
|
|
@@ -95,17 +166,29 @@ export declare const ProjectConfig: z.ZodObject<{
|
|
|
95
166
|
}, z.core.$strict>;
|
|
96
167
|
dispatcher: z.ZodDefault<z.ZodObject<{
|
|
97
168
|
max_rounds: z.ZodOptional<z.ZodNumber>;
|
|
98
|
-
max_parallel: z.ZodOptional<z.
|
|
99
|
-
max_concurrent_runs: z.ZodOptional<z.
|
|
169
|
+
max_parallel: z.ZodOptional<z.ZodUnknown>;
|
|
170
|
+
max_concurrent_runs: z.ZodOptional<z.ZodUnknown>;
|
|
100
171
|
wake_timeout_sec: z.ZodOptional<z.ZodNumber>;
|
|
101
172
|
retry: z.ZodOptional<z.ZodNumber>;
|
|
102
173
|
strict: z.ZodOptional<z.ZodBoolean>;
|
|
103
|
-
workspace_mode: z.ZodOptional<z.
|
|
104
|
-
auto: "auto";
|
|
105
|
-
shared: "shared";
|
|
106
|
-
worktree: "worktree";
|
|
107
|
-
}>>;
|
|
174
|
+
workspace_mode: z.ZodOptional<z.ZodUnknown>;
|
|
108
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>;
|
|
109
192
|
}, z.core.$strict>>;
|
|
110
193
|
agents: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
111
194
|
runtime: z.ZodOptional<z.ZodEnum<{
|
|
@@ -135,21 +218,33 @@ export declare const ProjectConfig: z.ZodObject<{
|
|
|
135
218
|
none: "none";
|
|
136
219
|
search: "search";
|
|
137
220
|
}>>;
|
|
138
|
-
|
|
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>>>;
|
|
236
|
+
owns: z.ZodOptional<z.ZodUnknown>;
|
|
139
237
|
}, z.core.$strict>>>;
|
|
140
238
|
}, z.core.$strict>;
|
|
141
239
|
export interface DispatcherSettings {
|
|
142
240
|
max_rounds: number;
|
|
143
|
-
max_parallel: number;
|
|
144
|
-
/** How many runs of one project may be active at once. Above 1, every run works in its own git worktree. */
|
|
145
|
-
max_concurrent_runs: number;
|
|
146
241
|
wake_timeout_sec: number;
|
|
147
242
|
retry: number;
|
|
148
243
|
strict: boolean;
|
|
149
|
-
/** "auto": a git worktree per non-lead agent when agents can run in parallel, otherwise one shared checkout. */
|
|
150
|
-
workspace_mode: "auto" | "shared" | "worktree";
|
|
151
244
|
/** log.jsonl is rotated to log.1.jsonl past this size; 0 = never. */
|
|
152
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;
|
|
153
248
|
}
|
|
154
249
|
export declare const DISPATCHER_DEFAULTS: DispatcherSettings;
|
|
155
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,21 +48,28 @@ 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(),
|
|
41
|
-
|
|
51
|
+
fallback: z.array(FallbackEntryPartial).optional(),
|
|
52
|
+
/** Removed setting: still accepted (any value) so old files load; ignored, with a warning from `validate`. */
|
|
53
|
+
owns: z.unknown().optional(),
|
|
42
54
|
})
|
|
43
55
|
.strict();
|
|
44
56
|
export const DispatcherPartial = z
|
|
45
57
|
.object({
|
|
46
58
|
max_rounds: z.number().int().min(0).optional(),
|
|
47
|
-
max_parallel: z.
|
|
48
|
-
max_concurrent_runs: z.
|
|
59
|
+
max_parallel: z.unknown().optional(),
|
|
60
|
+
max_concurrent_runs: z.unknown().optional(),
|
|
49
61
|
wake_timeout_sec: z.number().int().positive().optional(),
|
|
50
62
|
retry: z.number().int().min(0).optional(),
|
|
51
63
|
strict: z.boolean().optional(),
|
|
52
|
-
workspace_mode: z.
|
|
64
|
+
workspace_mode: z.unknown().optional(),
|
|
53
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(),
|
|
54
68
|
})
|
|
55
69
|
.strict();
|
|
70
|
+
/** Settings that no longer exist; files that still carry them load, and `validate` warns. */
|
|
71
|
+
export const REMOVED_DISPATCHER_KEYS = ["max_parallel", "max_concurrent_runs", "workspace_mode"];
|
|
72
|
+
export const REMOVED_AGENT_KEYS = ["owns"];
|
|
56
73
|
export const GlobalConfig = z
|
|
57
74
|
.object({
|
|
58
75
|
agents: z.record(z.string(), AgentPartial).default({}),
|
|
@@ -68,13 +85,11 @@ export const ProjectConfig = z
|
|
|
68
85
|
.strict();
|
|
69
86
|
export const DISPATCHER_DEFAULTS = {
|
|
70
87
|
max_rounds: 0,
|
|
71
|
-
max_parallel: 1,
|
|
72
|
-
max_concurrent_runs: 1,
|
|
73
88
|
wake_timeout_sec: 600,
|
|
74
89
|
retry: 2,
|
|
75
90
|
strict: false,
|
|
76
|
-
workspace_mode: "auto",
|
|
77
91
|
log_max_bytes: 8 * 1024 * 1024,
|
|
92
|
+
fallback_cooldown_sec: 300,
|
|
78
93
|
};
|
|
79
94
|
/** How a run turned out, independent of why it stopped (`EndReason`). Only "completed" means success. */
|
|
80
95
|
export const RUN_OUTCOMES = ["completed", "partial", "blocked", "failed", "waiting", "cancelled"];
|
package/dist/solo.js
CHANGED
|
@@ -15,8 +15,7 @@ export function soloProject(project, name, hints = {}) {
|
|
|
15
15
|
...project,
|
|
16
16
|
solo: "run",
|
|
17
17
|
lead: name,
|
|
18
|
-
|
|
19
|
-
agents: { ...project.agents, [name]: { ...a, canMessage: [], canAskUser: false, canEditAgentMd: false, owns: [] } },
|
|
18
|
+
agents: { ...project.agents, [name]: { ...a, canMessage: [], canAskUser: false, canEditAgentMd: false } },
|
|
20
19
|
};
|
|
21
20
|
}
|
|
22
21
|
/** `project` with only `name` listed: what a preflight of a solo run needs to look at. */
|
package/dist/status.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ActiveWake, type EndReason, type RunState, type WakeRecord } from "./run-store.js";
|
|
1
|
+
import { type ActiveWake, type EndReason, type RunFailure, type RunState, type WakeRecord } from "./run-store.js";
|
|
2
2
|
import type { RunOutcome } from "./schema.js";
|
|
3
3
|
import type { ResolvedProject } from "./config.js";
|
|
4
4
|
import { type DigestSection, type Step } from "./format.js";
|
|
@@ -72,11 +72,7 @@ export interface RunReport {
|
|
|
72
72
|
/** 0 = no limit. */
|
|
73
73
|
max_rounds: number;
|
|
74
74
|
/** Set when an agent kept failing and the run stopped: `resume` after fixing the cause. */
|
|
75
|
-
failure?:
|
|
76
|
-
agent: string;
|
|
77
|
-
error?: string;
|
|
78
|
-
attempts: number;
|
|
79
|
-
};
|
|
75
|
+
failure?: RunFailure;
|
|
80
76
|
output_tokens: number;
|
|
81
77
|
task_source: "text" | "file";
|
|
82
78
|
task_path?: string;
|
|
@@ -123,6 +119,19 @@ export interface AgentReport {
|
|
|
123
119
|
ok: boolean;
|
|
124
120
|
error?: string;
|
|
125
121
|
};
|
|
122
|
+
/** Only for an agent with a fallback chain: the model it last ran on (or its own, before any wake). */
|
|
123
|
+
model?: string;
|
|
124
|
+
/** Only when the agent's latest wake ran on a backup: what it replaced and why. */
|
|
125
|
+
fallback?: {
|
|
126
|
+
from: string;
|
|
127
|
+
kind: string;
|
|
128
|
+
};
|
|
129
|
+
/** Only for an agent with a fallback chain: its models that are cooling down now (`model` is `<runtime>/<model>`). */
|
|
130
|
+
cooldowns?: {
|
|
131
|
+
model: string;
|
|
132
|
+
kind: string;
|
|
133
|
+
remaining_sec: number;
|
|
134
|
+
}[];
|
|
126
135
|
}
|
|
127
136
|
export interface StatusReport {
|
|
128
137
|
schema_version: 1;
|
|
@@ -153,7 +162,7 @@ export declare function buildRunReport(run: {
|
|
|
153
162
|
detail?: boolean;
|
|
154
163
|
}): RunReport;
|
|
155
164
|
/** The status of a project: its agents and one run (the newest running one, else the newest, or the one named by `runId`). */
|
|
156
|
-
export declare function buildStatusReport(base: ResolvedProject, runId?: string): StatusReport;
|
|
165
|
+
export declare function buildStatusReport(base: ResolvedProject, runId?: string, now?: number): StatusReport;
|
|
157
166
|
/** Every run of the project, newest first. */
|
|
158
167
|
export declare function buildTaskListReport(project: ResolvedProject): TaskListReport;
|
|
159
168
|
/** Live view: summary and agent table, plus the latest run and any run still going, wake by wake. */
|
package/dist/status.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { askReplyPath, readAskReply } from "./ask-reply.js";
|
|
4
4
|
import { RESULT_FILE } from "./dispatcher.js";
|
|
5
5
|
import { bindRunProject, loadRunState, outcomeOf, roundsLabel, runLabel } from "./run-store.js";
|
|
6
|
+
import { candidatesFor } from "./fallback.js";
|
|
6
7
|
import { briefOf, digestOf } from "./format.js";
|
|
7
8
|
import { listUnread } from "./mailbox.js";
|
|
8
9
|
import { tidyAdvice } from "./memory-tidy.js";
|
|
@@ -226,8 +227,37 @@ export function buildRunReport(run, project, opts = {}) {
|
|
|
226
227
|
};
|
|
227
228
|
}
|
|
228
229
|
const projectInfo = (p) => ({ name: p.name, dir: p.dir, lead: p.lead });
|
|
230
|
+
/** The fallback part of an agent's report: nothing at all for an agent without a chain. */
|
|
231
|
+
function fallbackView(a, state, now) {
|
|
232
|
+
if (!a.fallback.length || !a.runtime)
|
|
233
|
+
return {};
|
|
234
|
+
const lw = state?.last_wake[a.name];
|
|
235
|
+
const ran = lw?.runtime ? lw : undefined;
|
|
236
|
+
const model = ran ? ran.model : a.model;
|
|
237
|
+
const mine = (state?.wakes ?? []).filter((w) => w.agent === a.name);
|
|
238
|
+
const last = mine.at(-1);
|
|
239
|
+
let fallback;
|
|
240
|
+
if (last?.fallback_from) {
|
|
241
|
+
// The reason is on the failed wake just before: the nearest earlier wake of this agent that has a kind.
|
|
242
|
+
let kind = "unknown";
|
|
243
|
+
for (let i = mine.length - 2; i >= 0; i--) {
|
|
244
|
+
const k = mine[i].kind;
|
|
245
|
+
if (k) {
|
|
246
|
+
kind = k;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
fallback = { from: last.fallback_from, kind };
|
|
251
|
+
}
|
|
252
|
+
const keys = new Set(candidatesFor(a).map((c) => c.key));
|
|
253
|
+
const cooldowns = Object.entries(state?.cooldowns ?? {})
|
|
254
|
+
.filter(([k]) => keys.has(k))
|
|
255
|
+
.map(([k, v]) => ({ model: k, kind: v.kind, remaining_sec: Math.ceil((Date.parse(v.until) - now) / 1000) }))
|
|
256
|
+
.filter((c) => Number.isFinite(c.remaining_sec) && c.remaining_sec > 0);
|
|
257
|
+
return { runtime: ran?.runtime ?? a.runtime, ...(model ? { model } : {}), ...(fallback ? { fallback } : {}), ...(cooldowns.length ? { cooldowns } : {}) };
|
|
258
|
+
}
|
|
229
259
|
/** The status of a project: its agents and one run (the newest running one, else the newest, or the one named by `runId`). */
|
|
230
|
-
export function buildStatusReport(base, runId) {
|
|
260
|
+
export function buildStatusReport(base, runId, now = Date.now()) {
|
|
231
261
|
let run;
|
|
232
262
|
if (runId) {
|
|
233
263
|
const dir = path.join(base.paths.runs, runId);
|
|
@@ -257,6 +287,7 @@ export function buildStatusReport(base, runId) {
|
|
|
257
287
|
unread: listUnread(project, a.name).length,
|
|
258
288
|
can_message: (a.canMessage === "all" ? Object.keys(project.agents) : a.canMessage).filter((n) => n !== a.name),
|
|
259
289
|
last_wake: run?.state.last_wake[a.name],
|
|
290
|
+
...fallbackView(a, run?.state, now),
|
|
260
291
|
})),
|
|
261
292
|
run: report,
|
|
262
293
|
...(advice.length ? { memory_advice: advice } : {}),
|
|
@@ -425,6 +456,20 @@ export function formatTaskListReport(report, color = false) {
|
|
|
425
456
|
export function formatStatus(base, now = Date.now(), color = false) {
|
|
426
457
|
return formatStatusReport(buildStatusReport(base), now, color);
|
|
427
458
|
}
|
|
459
|
+
const shortSec = (sec) => (sec < 60 ? `${sec}s` : `${Math.floor(sec / 60)}m${String(sec % 60).padStart(2, "0")}s`);
|
|
460
|
+
/** `runtime/model`, or the runtime alone for its default model (cooldown keys end in `/` then). */
|
|
461
|
+
const modelName = (key) => key.replace(/\/$/, "");
|
|
462
|
+
/** Agents running on a backup and models cooling down; empty for a run without fallback. */
|
|
463
|
+
function fallbackLines(agents, c) {
|
|
464
|
+
const out = [];
|
|
465
|
+
for (const a of agents) {
|
|
466
|
+
if (a.fallback)
|
|
467
|
+
out.push(` ${c.cyan(a.name)} ${c.yellow(`備援中 fallback: ${a.fallback.kind}`)} (${modelName(`${a.runtime ?? "?"}/${a.model ?? ""}`)})`);
|
|
468
|
+
for (const k of a.cooldowns ?? [])
|
|
469
|
+
out.push(` ${c.cyan(a.name)} ${c.yellow(`冷卻中:${modelName(k.model)} 剩 ${shortSec(k.remaining_sec)}`)}(${k.kind})`);
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
428
473
|
export function formatStatusReport(report, now = Date.now(), color = false) {
|
|
429
474
|
const c = paint(color);
|
|
430
475
|
const run = report.run;
|
|
@@ -452,6 +497,9 @@ export function formatStatusReport(report, now = Date.now(), color = false) {
|
|
|
452
497
|
const table = rows.map((r, ri) => r.map((_, i) => cell(r, ri, i)).join(" ").trimEnd());
|
|
453
498
|
table.splice(1, 0, c.dim(w.map((n) => "-".repeat(n)).join(" ")));
|
|
454
499
|
const lines = [`${c.bold("專案:")}${report.project.name} ${c.dim(`(repo:${report.project.dir})`)}`, "", ...summaryBlock(run, now, c), "", ...table];
|
|
500
|
+
const fb = fallbackLines(report.agents, c);
|
|
501
|
+
if (fb.length)
|
|
502
|
+
lines.push("", ...fb);
|
|
455
503
|
if (run)
|
|
456
504
|
lines.push("", `輸出 tokens:${tokens(run.output_tokens)}`);
|
|
457
505
|
if (report.memory_advice?.length)
|
package/dist/validate.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { enforcementFor, memoryDirs
|
|
2
|
+
import { enforcementFor, memoryDirs } from "./policy.js";
|
|
3
3
|
import { isInside } from "./paths.js";
|
|
4
4
|
import { inferRuntime, RUNTIME_EFFORTS } from "./schema.js";
|
|
5
|
-
import { isGitRepo, resolveWorkspaceMode } from "./worktree.js";
|
|
6
5
|
import { must } from "./assert.js";
|
|
7
6
|
export function validateProject(project) {
|
|
8
7
|
const issues = [];
|
|
@@ -26,6 +25,7 @@ export function validateProject(project) {
|
|
|
26
25
|
if (a.effort && a.runtime && !RUNTIME_EFFORTS[a.runtime].includes(a.effort)) {
|
|
27
26
|
err(`Agent "${a.name}": effort "${a.effort}" is not supported by ${a.runtime} (use ${RUNTIME_EFFORTS[a.runtime].join(", ")}).`);
|
|
28
27
|
}
|
|
28
|
+
validateFallback(project, a, err);
|
|
29
29
|
if (!fs.existsSync(a.agentMd))
|
|
30
30
|
err(`Agent "${a.name}": AGENT.md not found at ${a.agentMd}`);
|
|
31
31
|
if (!a.memory.global && !a.memory.project)
|
|
@@ -55,40 +55,8 @@ export function validateProject(project) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const withOwns = Object.values(project.agents).filter((a) => a.name !== project.lead);
|
|
61
|
-
for (const a of withOwns) {
|
|
62
|
-
if (a.owns.length === 0)
|
|
63
|
-
err(`max_parallel > 1 requires "owns" for agent "${a.name}".`);
|
|
64
|
-
}
|
|
65
|
-
for (let i = 0; i < withOwns.length; i++) {
|
|
66
|
-
for (let j = i + 1; j < withOwns.length; j++) {
|
|
67
|
-
const ai = must(withOwns[i], "agent");
|
|
68
|
-
const aj = must(withOwns[j], "agent");
|
|
69
|
-
for (const x of ownsDirs(project, ai)) {
|
|
70
|
-
for (const y of ownsDirs(project, aj)) {
|
|
71
|
-
if (isInside(x, y) || isInside(y, x)) {
|
|
72
|
-
err(`"owns" overlap: ${ai.name} (${x}) vs ${aj.name} (${y})`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// Parallel agents and concurrent runs each need their own checkout, which needs git.
|
|
80
|
-
const concurrent = project.dispatcher.max_concurrent_runs > 1;
|
|
81
|
-
if (concurrent && project.dispatcher.workspace_mode === "shared") {
|
|
82
|
-
err(`dispatcher.workspace_mode "shared" cannot be combined with max_concurrent_runs > 1: every run needs its own git worktree (use "auto" or "worktree").`);
|
|
83
|
-
}
|
|
84
|
-
else if (concurrent && fs.existsSync(project.dir) && !isGitRepo(project.dir)) {
|
|
85
|
-
err(`max_concurrent_runs > 1 needs ${project.dir} to be a git repository (each run works in its own git worktree). Set max_concurrent_runs: 1.`);
|
|
86
|
-
}
|
|
87
|
-
else if (project.dispatcher.workspace_mode === "shared" && project.dispatcher.max_parallel > 1) {
|
|
88
|
-
err(`dispatcher.workspace_mode "shared" cannot be combined with max_parallel > 1: parallel agents need separate git worktrees (use "auto" or "worktree").`);
|
|
89
|
-
}
|
|
90
|
-
else if (!concurrent && resolveWorkspaceMode(project.dispatcher) === "worktree" && fs.existsSync(project.dir) && !isGitRepo(project.dir)) {
|
|
91
|
-
err(`Parallel runs (worktree workspaces) need ${project.dir} to be a git repository. Run sequentially instead (max_parallel: 1, workspace_mode: auto or shared).`);
|
|
58
|
+
for (const ig of project.ignoredSettings ?? []) {
|
|
59
|
+
warn(`${ig.file}: ignoring removed setting(s) ${ig.keys.join(", ")}. Concurrency needs no configuration now: in a git repo with a commit every run and every agent gets its own worktree. Delete these lines.`);
|
|
92
60
|
}
|
|
93
61
|
// Context paths should live outside the repo (the tool never writes into it).
|
|
94
62
|
for (const a of Object.values(project.agents)) {
|
|
@@ -108,11 +76,46 @@ export function validateProject(project) {
|
|
|
108
76
|
}
|
|
109
77
|
return { issues, enforcement, ok: !issues.some((i) => i.level === "error") };
|
|
110
78
|
}
|
|
79
|
+
const PARITY_FIELDS = ["memory", "agentMd", "otherContext", "shared", "repoInstructions"];
|
|
80
|
+
const norm = (m) => (m ?? "").trim().toLowerCase();
|
|
81
|
+
/** Load-time (fail closed) checks of one agent's fallback chain: well-formed, not a no-op, and no weaker at write-scope than the primary. */
|
|
82
|
+
function validateFallback(project, a, err) {
|
|
83
|
+
for (const bad of a.fallbackInvalid) {
|
|
84
|
+
err(bad.reason === "empty"
|
|
85
|
+
? `agent ${a.name}: fallback #${bad.index + 1} needs a model or a runtime.`
|
|
86
|
+
: `agent ${a.name}: fallback #${bad.index + 1} has no recognizable runtime (set runtime, or use a recognizable model).`);
|
|
87
|
+
}
|
|
88
|
+
const primary = a.runtime ? enforcementFor(project, a) : undefined;
|
|
89
|
+
for (const f of a.fallback) {
|
|
90
|
+
const label = `${f.runtime}/${f.model || "(default model)"}`;
|
|
91
|
+
if (f.runtime === a.runtime && norm(f.model) === norm(a.model)) {
|
|
92
|
+
// A chain shared through dispatcher.fallback may already contain this agent's primary: just skip it then.
|
|
93
|
+
if (!a.fallbackShared)
|
|
94
|
+
err(`agent ${a.name}: fallback ${label} is the same as the primary runtime and model.`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (f.effort && !RUNTIME_EFFORTS[f.runtime].includes(f.effort)) {
|
|
98
|
+
err(`agent ${a.name}: fallback ${label}: effort "${f.effort}" is not supported by ${f.runtime} (use ${RUNTIME_EFFORTS[f.runtime].join(", ")}).`);
|
|
99
|
+
}
|
|
100
|
+
if (!primary)
|
|
101
|
+
continue;
|
|
102
|
+
// `prompt-only` is the one level that is no enforcement at all; everything else (os, tool-rules, post-hoc) still holds the line.
|
|
103
|
+
const swapped = enforcementFor(project, { ...a, runtime: f.runtime, model: f.model || undefined, effort: f.effort });
|
|
104
|
+
for (const k of PARITY_FIELDS) {
|
|
105
|
+
const from = primary[k];
|
|
106
|
+
const to = swapped[k];
|
|
107
|
+
if (from === "n/a" || to === "n/a")
|
|
108
|
+
continue;
|
|
109
|
+
if (to === "prompt-only" && from !== "prompt-only")
|
|
110
|
+
err(`agent ${a.name}: fallback ${label} cannot enforce ${k} (${from} -> ${to})`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
111
114
|
export function formatEnforcement(list) {
|
|
112
|
-
const header = ["agent", "runtime", "memory", "AGENT.md", "others' ctx", "handoff", "repo CLAUDE/AGENTS"
|
|
115
|
+
const header = ["agent", "runtime", "memory", "AGENT.md", "others' ctx", "handoff", "repo CLAUDE/AGENTS"];
|
|
113
116
|
const rows = [header];
|
|
114
117
|
for (const e of list)
|
|
115
|
-
rows.push([e.agent, e.runtime, e.memory, e.agentMd, e.otherContext, e.shared, e.repoInstructions
|
|
118
|
+
rows.push([e.agent, e.runtime, e.memory, e.agentMd, e.otherContext, e.shared, e.repoInstructions]);
|
|
116
119
|
const w = header.map((_, i) => Math.max(...rows.map((r) => (r[i] ?? "").length)));
|
|
117
120
|
const lines = rows.map((r) => r.map((c, i) => c.padEnd(w[i] ?? 0)).join(" ").trimEnd());
|
|
118
121
|
lines.splice(1, 0, w.map((n) => "-".repeat(n)).join(" "));
|
package/dist/worktree.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { ResolvedProject } from "./config.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { RunState } from "./run-store.js";
|
|
3
3
|
export type WorkspaceMode = "shared" | "worktree";
|
|
4
|
-
/** Agents share the repo, or each non-lead agent gets its own git worktree. `auto` picks worktrees as soon as agents can run in parallel; several concurrent runs always do. */
|
|
5
|
-
export declare function resolveWorkspaceMode(d: Pick<DispatcherSettings, "workspace_mode" | "max_parallel"> & Partial<Pick<DispatcherSettings, "max_concurrent_runs">>): WorkspaceMode;
|
|
6
4
|
export interface AgentWorkspace {
|
|
7
5
|
agent: string;
|
|
8
6
|
/** Root of the git worktree. */
|
|
@@ -58,11 +56,22 @@ export declare function isGitRepo(dir: string): boolean;
|
|
|
58
56
|
export declare const toplevel: (dir: string) => string;
|
|
59
57
|
/** Files in the repo with changes nobody committed (modified, staged or new and not ignored). Paths are relative to the repo root. */
|
|
60
58
|
export declare function dirtyPaths(dir: string): string[];
|
|
61
|
-
/**
|
|
62
|
-
export declare function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
/** Why `dir` cannot give every run and agent its own git worktree, or undefined when it can. */
|
|
60
|
+
export declare function isolationBlocker(dir: string): string | undefined;
|
|
61
|
+
export interface WorkspacePlan {
|
|
62
|
+
mode: WorkspaceMode;
|
|
63
|
+
/** The lead works in its own worktree (isolation mode); otherwise it works in the checkout. */
|
|
64
|
+
leadWorktree: boolean;
|
|
65
|
+
/** Why the run works in the checkout, when it was probed. */
|
|
66
|
+
reason?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Where a run's agents work. A new run is isolated when the project dir can be (see `isolationBlocker`), otherwise it shares the
|
|
70
|
+
* checkout. A resumed run keeps what its state recorded, whatever the repo looks like now. Decided once, before the run's lock is taken.
|
|
71
|
+
*/
|
|
72
|
+
export declare function planWorkspace(project: ResolvedProject, resume?: RunState): WorkspacePlan;
|
|
73
|
+
/** Refuse to resume a worktree run when its repo can no longer be isolated; returns nothing when it is fine. */
|
|
74
|
+
export declare function assertWorktreeRunnable(project: ResolvedProject): void;
|
|
66
75
|
/** `refs/agent-lyceum/<run>/base-0` pointing at the repo's HEAD: where a concurrent run starts. Nothing in the repo changes. */
|
|
67
76
|
export declare function snapshotHead(project: ResolvedProject, runId: string): string;
|
|
68
77
|
/**
|
|
@@ -97,8 +106,6 @@ export interface ChangeSet {
|
|
|
97
106
|
/** Why these changes may not be integrated (empty when they may). */
|
|
98
107
|
violations: string[];
|
|
99
108
|
}
|
|
100
|
-
/** Glob as used in `owns`: `**` crosses folders, `*` and `?` stay within one, `{a,b}` alternates. */
|
|
101
|
-
export declare function globMatch(glob: string, file: string): boolean;
|
|
102
109
|
/** Commit whatever is in an agent's worktree (no hooks, no signing, dispatcher identity). Returns the HEAD afterwards and whether a commit was made. */
|
|
103
110
|
export declare function commitWorkspace(ws: AgentWorkspace, message: string): {
|
|
104
111
|
head: string;
|
|
@@ -106,10 +113,10 @@ export declare function commitWorkspace(ws: AgentWorkspace, message: string): {
|
|
|
106
113
|
};
|
|
107
114
|
/**
|
|
108
115
|
* Commit whatever the agent left in its worktree and list what changed since its snapshot, checking every
|
|
109
|
-
* changed path (modified, added, deleted, or either end of a rename) against the
|
|
116
|
+
* changed path (modified, added, deleted, or either end of a rename) against the project
|
|
110
117
|
* folder, the protected instruction files, and symlink/submodule rules. Nothing is applied here.
|
|
111
118
|
*/
|
|
112
|
-
export declare function collectAgentChanges(workspace: AgentWorkspace
|
|
119
|
+
export declare function collectAgentChanges(workspace: AgentWorkspace): ChangeSet;
|
|
113
120
|
export type IntegrationResult = {
|
|
114
121
|
status: "integrated";
|
|
115
122
|
files: ChangedFile[];
|