agent-lyceum 0.25.0 → 0.26.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 +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/config.d.ts +15 -1
- package/dist/config.js +2 -0
- package/dist/doctor-instances.d.ts +4 -0
- package/dist/doctor-instances.js +63 -0
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +17 -1
- package/dist/instance-containment.d.ts +15 -0
- package/dist/instance-containment.js +36 -0
- package/dist/instances.d.ts +183 -0
- package/dist/instances.js +268 -0
- package/dist/mailbox.d.ts +32 -2
- package/dist/mailbox.js +76 -18
- package/dist/message-store.d.ts +47 -1
- package/dist/message-store.js +75 -7
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +23 -1
- package/dist/policy.d.ts +1 -0
- package/dist/policy.js +2 -2
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +38 -9
- package/dist/run-session.d.ts +33 -2
- package/dist/run-session.js +280 -12
- package/dist/run-store.d.ts +13 -2
- package/dist/run-store.js +17 -7
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +3 -0
- package/dist/schema.d.ts +4 -0
- package/dist/schema.js +2 -0
- package/dist/status.d.ts +7 -0
- package/dist/status.js +48 -14
- package/dist/worktree.d.ts +6 -0
- package/dist/worktree.js +14 -1
- package/package.json +1 -1
package/dist/status.js
CHANGED
|
@@ -5,7 +5,9 @@ import { RESULT_FILE } from "./dispatcher.js";
|
|
|
5
5
|
import { bindRunProject, loadRunState, outcomeOf, roundsLabel, runLabel } from "./run-store.js";
|
|
6
6
|
import { candidatesFor } from "./fallback.js";
|
|
7
7
|
import { briefOf, digestOf } from "./format.js";
|
|
8
|
-
import { listUnread } from "./mailbox.js";
|
|
8
|
+
import { PENDING_DIR, listUnread, readMessageNoFollow } from "./mailbox.js";
|
|
9
|
+
import { parseSlotName } from "./instances.js";
|
|
10
|
+
import { inboxDir } from "./policy.js";
|
|
9
11
|
import { tidyAdvice } from "./memory-tidy.js";
|
|
10
12
|
import { inspectRunLock, lockHolderAlive } from "./project-lock.js";
|
|
11
13
|
/** The run to show: the newest one still running (a resumed run keeps its old id, so newer runs may sit above it), else the newest run. */
|
|
@@ -154,7 +156,7 @@ function stepsLine(steps, c, finished = false) {
|
|
|
154
156
|
return `目前:${clip(cur.text, 60)}`;
|
|
155
157
|
}
|
|
156
158
|
/** Order the dispatcher will wake agents: the lead first, otherwise the oldest mail. */
|
|
157
|
-
function queueOf(project) {
|
|
159
|
+
function queueOf(project, instances) {
|
|
158
160
|
const rows = Object.values(project.agents)
|
|
159
161
|
.map((a) => ({ agent: a.name, msg: listUnread(project, a.name)[0] }))
|
|
160
162
|
.flatMap((r) => (r.msg ? [{ agent: r.agent, msg: r.msg }] : []));
|
|
@@ -163,7 +165,7 @@ function queueOf(project) {
|
|
|
163
165
|
return x.agent === project.lead ? -1 : 1;
|
|
164
166
|
return path.basename(x.msg.file).localeCompare(path.basename(y.msg.file));
|
|
165
167
|
});
|
|
166
|
-
|
|
168
|
+
const items = rows.map((r) => ({
|
|
167
169
|
agent: r.agent,
|
|
168
170
|
from: r.msg.meta.from,
|
|
169
171
|
type: r.msg.meta.type,
|
|
@@ -171,14 +173,44 @@ function queueOf(project) {
|
|
|
171
173
|
brief: briefOf(r.msg.body),
|
|
172
174
|
digest: digestOf(r.msg.meta.type, r.msg.body),
|
|
173
175
|
}));
|
|
176
|
+
return [...items, ...heldQueue(project, instances)];
|
|
174
177
|
}
|
|
178
|
+
/** Mail the assigner still holds for roles with instances, oldest first per role. Unreadable files are skipped. */
|
|
179
|
+
function heldQueue(project, instances) {
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const role of Object.keys(instances?.roles ?? {}).sort()) {
|
|
182
|
+
const dir = path.join(inboxDir(project, role), PENDING_DIR);
|
|
183
|
+
let files = [];
|
|
184
|
+
try {
|
|
185
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith(".md") && !f.startsWith(".")).sort();
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
for (const f of files) {
|
|
191
|
+
try {
|
|
192
|
+
const m = readMessageNoFollow(path.join(dir, f));
|
|
193
|
+
out.push({ agent: role, held: true, from: m.meta.from, type: m.meta.type, subject: m.meta.subject, brief: briefOf(m.body), digest: digestOf(m.meta.type, m.body) });
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* a half-written file: the next poll sees it */
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
/** How an agent is named on the text pages: an instance of a role shows as `role #k`. */
|
|
203
|
+
const shownName = (name, slots) => {
|
|
204
|
+
const i = parseSlotName(name);
|
|
205
|
+
return i && slots?.[i.role]?.[i.k - 1] === name ? `${i.role} #${i.k}` : name;
|
|
206
|
+
};
|
|
175
207
|
/** One run as `status` sees it. Without `project`, liveness falls back to the recorded pid and there is no queue. */
|
|
176
208
|
export function buildRunReport(run, project, opts = {}) {
|
|
177
209
|
const s = run.state;
|
|
178
210
|
const detail = opts.detail ?? true;
|
|
179
211
|
const live = runIsAlive(s, project) && !s.end_reason;
|
|
180
212
|
const o = outcomeOf(s);
|
|
181
|
-
const bound = project ? bindRunProject(project, run.dir, s.mail_layout) : undefined;
|
|
213
|
+
const bound = project ? bindRunProject(project, run.dir, s.mail_layout, s.instances) : undefined;
|
|
182
214
|
const result = path.join(run.dir, RESULT_FILE);
|
|
183
215
|
const hasResult = detail && s.end_reason === "done" && fs.existsSync(result);
|
|
184
216
|
const cur = s.steps?.find((x) => !x.done);
|
|
@@ -221,7 +253,7 @@ export function buildRunReport(run, project, opts = {}) {
|
|
|
221
253
|
current_step: cur?.text ?? null,
|
|
222
254
|
active: s.active,
|
|
223
255
|
// mail still waiting is what `resume` will pick up, so it is shown for every run that did not finish with a done
|
|
224
|
-
queue: detail && bound && s.end_reason !== "done" ? queueOf(bound) : [],
|
|
256
|
+
queue: detail && bound && s.end_reason !== "done" ? queueOf(bound, s.instances) : [],
|
|
225
257
|
wakes: s.wakes,
|
|
226
258
|
notes: s.notes ?? [],
|
|
227
259
|
blocked_integrations: s.blocked_integrations ?? [],
|
|
@@ -230,6 +262,7 @@ export function buildRunReport(run, project, opts = {}) {
|
|
|
230
262
|
mail_layout: s.mail_layout,
|
|
231
263
|
workspace_mode: s.workspace_mode,
|
|
232
264
|
last_wake: s.last_wake,
|
|
265
|
+
...(s.instances ? { instances: s.instances.roles } : {}),
|
|
233
266
|
...(ask ? { ask } : {}),
|
|
234
267
|
};
|
|
235
268
|
}
|
|
@@ -277,7 +310,7 @@ export function buildStatusReport(base, runId, now = Date.now()) {
|
|
|
277
310
|
else
|
|
278
311
|
run = currentRun(base);
|
|
279
312
|
// Mail is per run: count the unread mail of the run being shown.
|
|
280
|
-
const project = run ? bindRunProject(base, run.dir, run.state.mail_layout) : base;
|
|
313
|
+
const project = run ? bindRunProject(base, run.dir, run.state.mail_layout, run.state.instances) : base;
|
|
281
314
|
const report = run ? buildRunReport(run, base) : undefined;
|
|
282
315
|
let advice = [];
|
|
283
316
|
try {
|
|
@@ -293,6 +326,7 @@ export function buildStatusReport(base, runId, now = Date.now()) {
|
|
|
293
326
|
name: a.name,
|
|
294
327
|
runtime: a.runtime,
|
|
295
328
|
lead: a.name === project.lead,
|
|
329
|
+
...(a.instance ? { role: a.instance.role, instance: a.instance.k } : {}),
|
|
296
330
|
unread: listUnread(project, a.name).length,
|
|
297
331
|
can_message: (a.canMessage === "all" ? Object.keys(project.agents) : a.canMessage).filter((n) => n !== a.name),
|
|
298
332
|
last_wake: run?.state.last_wake[a.name],
|
|
@@ -309,10 +343,10 @@ export function buildTaskListReport(project) {
|
|
|
309
343
|
// ---- text pages ----
|
|
310
344
|
/** Agents in wake order, newest last; the ones working now are bracketed. */
|
|
311
345
|
function flowLine(r, c) {
|
|
312
|
-
const names = (r.wakes ?? []).map((w) => w.agent);
|
|
346
|
+
const names = (r.wakes ?? []).map((w) => shownName(w.agent, r.instances));
|
|
313
347
|
const shown = names.slice(-7);
|
|
314
348
|
const parts = shown.map((n) => c.cyan(n));
|
|
315
|
-
const activeNames = r.live ? Object.keys(r.active ?? {}) : [];
|
|
349
|
+
const activeNames = r.live ? Object.keys(r.active ?? {}).map((n) => shownName(n, r.instances)) : [];
|
|
316
350
|
const out = (names.length > shown.length ? "… → " : "") + parts.join(" → ");
|
|
317
351
|
if (!activeNames.length)
|
|
318
352
|
return out || "(尚無)";
|
|
@@ -320,7 +354,7 @@ function flowLine(r, c) {
|
|
|
320
354
|
}
|
|
321
355
|
/** The state label of a report: an outcome that was only inferred (old runs) is not shown as a checked result. */
|
|
322
356
|
const reportLabel = (r) => runStateLabel({ end_reason: r.end_reason, outcome: r.outcome_verified ? r.outcome : undefined }, r.live);
|
|
323
|
-
const mailText = (m) => `來自 ${m.from} 的 ${m.type}:「${clip(m.subject, 60)}
|
|
357
|
+
const mailText = (m, slots) => `來自 ${shownName(m.from, slots)} 的 ${m.type}:「${clip(m.subject, 60)}」${m.held ? "(保留中,等待空閒實例)" : ""}`;
|
|
324
358
|
const taskText = (s) => s.task_summary ?? (s.task_source === "file" ? `檔案 ${s.task_path}` : "文字");
|
|
325
359
|
/** The readable top block: what is happening, what is next, how far along the task is. */
|
|
326
360
|
function summaryBlock(run, now, c) {
|
|
@@ -338,7 +372,7 @@ function summaryBlock(run, now, c) {
|
|
|
338
372
|
const active = Object.entries(s.active ?? {});
|
|
339
373
|
if (active.length) {
|
|
340
374
|
for (const [i, [agent, a]] of active.entries()) {
|
|
341
|
-
lines.push(`${i === 0 ? L("現在:") : " "}${c.cyan(agent)} ${live ? c.green(`工作中 ${since(a.since, now)}`) : c.yellow("曾在工作(已中斷)")},處理 ${handlingText(a.handling)}`);
|
|
375
|
+
lines.push(`${i === 0 ? L("現在:") : " "}${c.cyan(shownName(agent, s.instances))} ${live ? c.green(`工作中 ${since(a.since, now)}`) : c.yellow("曾在工作(已中斷)")},處理 ${handlingText(a.handling)}`);
|
|
342
376
|
}
|
|
343
377
|
}
|
|
344
378
|
else
|
|
@@ -346,7 +380,7 @@ function summaryBlock(run, now, c) {
|
|
|
346
380
|
const queue = s.queue;
|
|
347
381
|
if (queue.length) {
|
|
348
382
|
for (const [i, q] of queue.slice(0, 3).entries())
|
|
349
|
-
lines.push(`${i === 0 ? L("下一個:") : " "}${c.cyan(q.agent)} ← ${mailText(q)}`);
|
|
383
|
+
lines.push(`${i === 0 ? L("下一個:") : " "}${c.cyan(q.agent)} ← ${mailText(q, s.instances)}`);
|
|
350
384
|
if (queue.length > 3)
|
|
351
385
|
lines.push(c.dim(` … 還有 ${queue.length - 3} 筆`));
|
|
352
386
|
}
|
|
@@ -387,10 +421,10 @@ function describeRun(s, now, c, limit = Infinity) {
|
|
|
387
421
|
if (wakes.length > limit)
|
|
388
422
|
lines.push(c.dim(` … 省略較早的 ${wakes.length - limit} 筆(不帶 --monitor 可看完整紀錄)`));
|
|
389
423
|
for (const w of limit < wakes.length ? wakes.slice(-limit) : wakes) {
|
|
390
|
-
lines.push(` #${w.round} ${c.cyan(w.agent)} ${okText(c, w.ok)} (${Math.round(w.duration_ms / 1000)}s, ${tokens(w.output_tokens)} tok) — ${handlingText(w.handling)}`, ` → ${sentText(w.sent)}`);
|
|
424
|
+
lines.push(` #${w.round} ${c.cyan(shownName(w.agent, s.instances))} ${okText(c, w.ok)} (${Math.round(w.duration_ms / 1000)}s, ${tokens(w.output_tokens)} tok) — ${handlingText(w.handling)}`, ` → ${sentText(w.sent)}`);
|
|
391
425
|
}
|
|
392
426
|
for (const [agent, a] of Object.entries(s.active ?? {})) {
|
|
393
|
-
lines.push(` #${a.round} ${c.cyan(agent)} ${c.green(`工作中 ${since(a.since, now)}`)} — ${handlingText(a.handling)}`);
|
|
427
|
+
lines.push(` #${a.round} ${c.cyan(shownName(agent, s.instances))} ${c.green(`工作中 ${since(a.since, now)}`)} — ${handlingText(a.handling)}`);
|
|
394
428
|
}
|
|
395
429
|
return lines;
|
|
396
430
|
}
|
|
@@ -486,7 +520,7 @@ export function formatStatusReport(report, now = Date.now(), color = false) {
|
|
|
486
520
|
const rows = [header];
|
|
487
521
|
for (const a of report.agents) {
|
|
488
522
|
const lw = a.last_wake;
|
|
489
|
-
rows.push([a.name + (a.lead ? " (lead)" : ""), a.runtime ?? "?", String(a.unread), lw ? `${lw.ok ? "成功" : "失敗"} ${lw.at}` : "-"]);
|
|
523
|
+
rows.push([(a.instance && a.instance >= 2 && a.role ? `${a.role} #${a.instance}` : a.name) + (a.lead ? " (lead)" : ""), a.runtime ?? "?", String(a.unread), lw ? `${lw.ok ? "成功" : "失敗"} ${lw.at}` : "-"]);
|
|
490
524
|
}
|
|
491
525
|
const w = header.map((_, i) => Math.max(...rows.map((r) => cols(r[i] ?? ""))));
|
|
492
526
|
// Pad on the plain text first so ANSI codes never skew the column widths.
|
package/dist/worktree.d.ts
CHANGED
|
@@ -81,6 +81,7 @@ export declare function snapshotHead(project: ResolvedProject, runId: string): s
|
|
|
81
81
|
* `from`: the checkout to snapshot — the lead's worktree in a concurrent run; by default your repo's working tree.
|
|
82
82
|
*/
|
|
83
83
|
export declare function snapshotBase(project: ResolvedProject, runId: string, n: number, from?: AgentWorkspace): string;
|
|
84
|
+
export declare const workspaceRoot: (project: ResolvedProject, runId: string, agent: string) => string;
|
|
84
85
|
/**
|
|
85
86
|
* The agent's own checkout of the snapshot `baseRef`, at `<project home>/worktrees/<run>/<agent>`.
|
|
86
87
|
* An existing checkout is reused: untouched, when it already is on this snapshot (resume) or holds unfinished
|
|
@@ -144,6 +145,11 @@ export declare function integrateAgentChanges(project: ResolvedProject, changes:
|
|
|
144
145
|
* Throws when the metadata predates recording the git dir and the git dir cannot be told for certain.
|
|
145
146
|
*/
|
|
146
147
|
export declare function existingAgentWorkspace(project: ResolvedProject, runId: string, agent: string): AgentWorkspace | undefined;
|
|
148
|
+
/**
|
|
149
|
+
* The git dir recorded in an agent's worktree metadata, read-only (never completes or rewrites legacy metadata). undefined when there
|
|
150
|
+
* is no readable metadata or it predates recording the git dir.
|
|
151
|
+
*/
|
|
152
|
+
export declare function recordedGitDir(project: ResolvedProject, runId: string, agent: string): string | undefined;
|
|
147
153
|
export interface WorkspaceWork {
|
|
148
154
|
agent: string;
|
|
149
155
|
root: string;
|
package/dist/worktree.js
CHANGED
|
@@ -290,7 +290,7 @@ export function snapshotBase(project, runId, n, from) {
|
|
|
290
290
|
fs.rmSync(index, { force: true });
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
|
-
const workspaceRoot = (project, runId, agent) => path.join(project.paths.root, "worktrees", runId, agent);
|
|
293
|
+
export const workspaceRoot = (project, runId, agent) => path.join(project.paths.root, "worktrees", runId, agent);
|
|
294
294
|
/** The user's repo's git common dir, as a real absolute path. Asked of the user's repo, never of a worktree. */
|
|
295
295
|
const commonDirOf = (top) => fs.realpathSync(git(top, ["rev-parse", "--path-format=absolute", "--git-common-dir"]));
|
|
296
296
|
/** The admin dirs under `<commonDir>/worktrees/` whose `gitdir` file names `<root>/.git` (the file may hold a relative path). */
|
|
@@ -594,6 +594,19 @@ export function existingAgentWorkspace(project, runId, agent) {
|
|
|
594
594
|
}
|
|
595
595
|
return fs.existsSync(root) ? describeWorkspace(project, root, agent, meta) : undefined;
|
|
596
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* The git dir recorded in an agent's worktree metadata, read-only (never completes or rewrites legacy metadata). undefined when there
|
|
599
|
+
* is no readable metadata or it predates recording the git dir.
|
|
600
|
+
*/
|
|
601
|
+
export function recordedGitDir(project, runId, agent) {
|
|
602
|
+
try {
|
|
603
|
+
const meta = JSON.parse(fs.readFileSync(`${workspaceRoot(project, runId, agent)}.json`, "utf8"));
|
|
604
|
+
return typeof meta.gitDir === "string" && meta.gitDir ? meta.gitDir : undefined;
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
597
610
|
/** What is in an agent's worktree that exists nowhere else: uncommitted changes, and commits whose files are not in the main repo. */
|
|
598
611
|
export function inspectWorkspace(project, runId, agent, target) {
|
|
599
612
|
const root = workspaceRoot(project, runId, agent);
|