agent-lyceum 0.21.2 → 0.22.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/dist/adapters/claude.d.ts +2 -0
- package/dist/adapters/claude.js +2 -0
- package/dist/adapters/types.d.ts +5 -0
- package/dist/format-rejections.d.ts +6 -0
- package/dist/format-rejections.js +29 -0
- package/dist/guard.js +3 -2
- package/dist/mailbox.js +10 -1
- package/dist/policy.d.ts +7 -0
- package/dist/policy.js +19 -2
- package/dist/prompt.d.ts +3 -1
- package/dist/prompt.js +26 -5
- package/dist/run-session.js +4 -0
- package/dist/run-store.d.ts +6 -0
- package/dist/run-store.js +4 -0
- package/dist/scaffold.js +2 -1
- package/dist/status-watch.d.ts +5 -4
- package/dist/status-watch.js +9 -8
- package/dist/status.d.ts +7 -0
- package/dist/status.js +11 -2
- package/package.json +1 -1
package/dist/adapters/claude.js
CHANGED
|
@@ -133,6 +133,8 @@ function interpretClaude(j, stdout, stderr, code) {
|
|
|
133
133
|
text: String(j.result ?? ""),
|
|
134
134
|
sessionId: j.session_id,
|
|
135
135
|
outputTokens: j.usage?.output_tokens,
|
|
136
|
+
cacheReadTokens: j.usage?.cache_read_input_tokens,
|
|
137
|
+
cacheWriteTokens: j.usage?.cache_creation_input_tokens,
|
|
136
138
|
error: ok ? undefined : String(j.result ?? stderr).slice(0, 500),
|
|
137
139
|
kind: ok ? undefined : classifyClaudeError({ code, stdout, stderr, result: j.is_error && j.result !== undefined ? String(j.result) : undefined }),
|
|
138
140
|
};
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -22,6 +22,9 @@ export interface WakeResult {
|
|
|
22
22
|
text: string;
|
|
23
23
|
sessionId?: string;
|
|
24
24
|
outputTokens?: number;
|
|
25
|
+
/** Prompt-cache usage (claude only): input tokens read from / written to the cache. */
|
|
26
|
+
cacheReadTokens?: number;
|
|
27
|
+
cacheWriteTokens?: number;
|
|
25
28
|
exitCode: number | null;
|
|
26
29
|
timedOut: boolean;
|
|
27
30
|
error?: string;
|
|
@@ -35,6 +38,8 @@ export type Parsed = {
|
|
|
35
38
|
text: string;
|
|
36
39
|
sessionId?: string;
|
|
37
40
|
outputTokens?: number;
|
|
41
|
+
cacheReadTokens?: number;
|
|
42
|
+
cacheWriteTokens?: number;
|
|
38
43
|
error?: string;
|
|
39
44
|
/** Set on failure only; `unknown` when no rule recognizes the output. */
|
|
40
45
|
kind?: ErrorKind;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** How many times one sender's mail (same type and subject) is sent back for missing sections before it is delivered with a warning instead. */
|
|
2
|
+
export declare const MAX_FORMAT_REJECTIONS = 2;
|
|
3
|
+
export declare function formatRejectionKey(from: string, type: string, subject: string): string;
|
|
4
|
+
export declare function formatRejections(runDir: string, key: string): number;
|
|
5
|
+
/** Record one more rejection for `key` and return the new count. */
|
|
6
|
+
export declare function countFormatRejection(runDir: string, key: string): number;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { atomicWrite } from "./fs-util.js";
|
|
4
|
+
/** How many times one sender's mail (same type and subject) is sent back for missing sections before it is delivered with a warning instead. */
|
|
5
|
+
export const MAX_FORMAT_REJECTIONS = 2;
|
|
6
|
+
const FILE = "format-rejections.json";
|
|
7
|
+
export function formatRejectionKey(from, type, subject) {
|
|
8
|
+
return `${from}|${type}|${subject.trim().toLowerCase()}`;
|
|
9
|
+
}
|
|
10
|
+
function load(runDir) {
|
|
11
|
+
try {
|
|
12
|
+
const data = JSON.parse(fs.readFileSync(path.join(runDir, FILE), "utf8"));
|
|
13
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function formatRejections(runDir, key) {
|
|
20
|
+
return load(runDir)[key] ?? 0;
|
|
21
|
+
}
|
|
22
|
+
/** Record one more rejection for `key` and return the new count. */
|
|
23
|
+
export function countFormatRejection(runDir, key) {
|
|
24
|
+
const all = load(runDir);
|
|
25
|
+
const n = (all[key] ?? 0) + 1;
|
|
26
|
+
all[key] = n;
|
|
27
|
+
atomicWrite(path.join(runDir, FILE), `${JSON.stringify(all, null, 2)}\n`);
|
|
28
|
+
return n;
|
|
29
|
+
}
|
package/dist/guard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { mayEditProtected, repoInstructionFiles, sharedContextFiles, sharedIndex } from "./policy.js";
|
|
5
5
|
export const MAX_KEEP_BYTES = 1024 * 1024;
|
|
6
6
|
function safeName(file) {
|
|
7
7
|
return file.replace(/[\\/:]/g, "_").replace(/^_+/, "");
|
|
@@ -29,7 +29,8 @@ export class ProtectedGuard {
|
|
|
29
29
|
const files = new Set();
|
|
30
30
|
for (const a of Object.values(project.agents))
|
|
31
31
|
files.add(a.agentMd);
|
|
32
|
-
|
|
32
|
+
for (const f of sharedContextFiles(project))
|
|
33
|
+
files.add(f);
|
|
33
34
|
for (const f of repoInstructionFiles(project))
|
|
34
35
|
files.add(f);
|
|
35
36
|
const handoff = sharedIndex(project);
|
package/dist/mailbox.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import { formatWarning, missingSections, parseSteps } from "./format.js";
|
|
6
6
|
import { atomicWrite } from "./fs-util.js";
|
|
7
|
+
import { countFormatRejection, formatRejectionKey, formatRejections, MAX_FORMAT_REJECTIONS } from "./format-rejections.js";
|
|
7
8
|
import { deterministicId, fault, RouteJournal, sourceIdOf } from "./message-store.js";
|
|
8
9
|
import { inboxDir, outboxDir, sharedDir } from "./policy.js";
|
|
9
10
|
import { must } from "./assert.js";
|
|
@@ -279,6 +280,15 @@ export function routeOutboxes(project) {
|
|
|
279
280
|
reject(why + ".");
|
|
280
281
|
continue;
|
|
281
282
|
}
|
|
283
|
+
const missing = missingSections(type, parsed.body);
|
|
284
|
+
if (missing.length && project.run?.layout === "run") {
|
|
285
|
+
const key = formatRejectionKey(sender.name, type, subject);
|
|
286
|
+
if (formatRejections(project.run.dir, key) < MAX_FORMAT_REJECTIONS) {
|
|
287
|
+
const n = countFormatRejection(project.run.dir, key);
|
|
288
|
+
reject(`this ${type} is missing required section(s): ${missing.map((s) => `\`## ${s}\``).join(", ")}. Add them (write \`None\` where a section is empty) and send it again. (Attempt ${n} of ${MAX_FORMAT_REJECTIONS}; after that it is delivered with a warning.)`);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
282
292
|
const replyTo = d.reply_to ? String(d.reply_to) : undefined;
|
|
283
293
|
const thread = d.thread ? String(d.thread) : undefined;
|
|
284
294
|
// Mail whose delivery was already planned (a replay after a crash) is not a repeat of itself.
|
|
@@ -308,7 +318,6 @@ export function routeOutboxes(project) {
|
|
|
308
318
|
});
|
|
309
319
|
res.rejected.push({ from: sender.name, file, reason: `duplicate: ${repeatNote()}` });
|
|
310
320
|
}
|
|
311
|
-
const missing = missingSections(type, parsed.body);
|
|
312
321
|
const body = missing.length ? `${formatWarning(type, missing)}\n\n${parsed.body}` : parsed.body;
|
|
313
322
|
if (sender.name === project.lead) {
|
|
314
323
|
const st = parseSteps(parsed.body);
|
package/dist/policy.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface WritePolicy {
|
|
|
23
23
|
deny: string[];
|
|
24
24
|
}
|
|
25
25
|
export declare function commonFile(project: ResolvedProject): string;
|
|
26
|
+
/** Durable decisions and lessons of the project, kept next to COMMON.md: the lead writes them, everyone else reads them. */
|
|
27
|
+
export declare function decisionsFile(project: ResolvedProject): string;
|
|
28
|
+
export declare function lessonsFile(project: ResolvedProject): string;
|
|
29
|
+
/** Where the lead moves entries that no longer fit under the prompt cap; read on demand, never injected. */
|
|
30
|
+
export declare function archiveFile(file: string): string;
|
|
31
|
+
/** The lead-maintained files shared by every run of the project. */
|
|
32
|
+
export declare function sharedContextFiles(project: ResolvedProject): string[];
|
|
26
33
|
export declare function outboxDir(project: ResolvedProject, agent: string): string;
|
|
27
34
|
export declare function inboxDir(project: ResolvedProject, agent: string): string;
|
|
28
35
|
/** The repo's own CLAUDE.md/AGENTS.md: in the lead's worktree when it has one (concurrent runs), else in the repo. */
|
package/dist/policy.js
CHANGED
|
@@ -4,6 +4,22 @@ import { findOnPath } from "./fs-util.js";
|
|
|
4
4
|
export function commonFile(project) {
|
|
5
5
|
return path.join(project.paths.shared, "common", "COMMON.md");
|
|
6
6
|
}
|
|
7
|
+
/** Durable decisions and lessons of the project, kept next to COMMON.md: the lead writes them, everyone else reads them. */
|
|
8
|
+
export function decisionsFile(project) {
|
|
9
|
+
return path.join(project.paths.shared, "common", "DECISIONS.md");
|
|
10
|
+
}
|
|
11
|
+
export function lessonsFile(project) {
|
|
12
|
+
return path.join(project.paths.shared, "common", "LESSONS.md");
|
|
13
|
+
}
|
|
14
|
+
/** Where the lead moves entries that no longer fit under the prompt cap; read on demand, never injected. */
|
|
15
|
+
export function archiveFile(file) {
|
|
16
|
+
return file.replace(/\.md$/, "_ARCHIVE.md");
|
|
17
|
+
}
|
|
18
|
+
/** The lead-maintained files shared by every run of the project. */
|
|
19
|
+
export function sharedContextFiles(project) {
|
|
20
|
+
const [d, l] = [decisionsFile(project), lessonsFile(project)];
|
|
21
|
+
return [commonFile(project), d, l, archiveFile(d), archiveFile(l)];
|
|
22
|
+
}
|
|
7
23
|
export function outboxDir(project, agent) {
|
|
8
24
|
return path.join(project.paths.outboxRoot, agent);
|
|
9
25
|
}
|
|
@@ -77,7 +93,7 @@ export function writePolicy(project, agent) {
|
|
|
77
93
|
];
|
|
78
94
|
const allowFiles = [];
|
|
79
95
|
if (isLead)
|
|
80
|
-
allowFiles.push(
|
|
96
|
+
allowFiles.push(...sharedContextFiles(project));
|
|
81
97
|
if (mayWriteShared && sharedIdx)
|
|
82
98
|
allowFiles.push(sharedIdx);
|
|
83
99
|
if (agent.canEditAgentMd) {
|
|
@@ -100,7 +116,8 @@ export function writePolicy(project, agent) {
|
|
|
100
116
|
deny.push(outboxDir(project, a.name));
|
|
101
117
|
deny.push(inboxDir(project, a.name));
|
|
102
118
|
}
|
|
103
|
-
|
|
119
|
+
for (const f of sharedContextFiles(project))
|
|
120
|
+
addDeny(f);
|
|
104
121
|
for (const f of repoInstructionFiles(project))
|
|
105
122
|
addDeny(f);
|
|
106
123
|
if (shared && !mayWriteShared)
|
package/dist/prompt.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Layer, MemoryFile, MissingPath, ProjectChanges } from "./memory-ti
|
|
|
4
4
|
export declare const COMMON_LIMIT: number;
|
|
5
5
|
export declare const MEMORY_INDEX_LIMIT: number;
|
|
6
6
|
export declare const HANDOFF_LIMIT: number;
|
|
7
|
+
/** Cap of each of DECISIONS.md and LESSONS.md in a prompt. */
|
|
8
|
+
export declare const CONTEXT_LIMIT: number;
|
|
7
9
|
export declare const LEAD_BATCH_LIMIT = 5;
|
|
8
10
|
export declare function readPersona(agent: ResolvedAgent): string;
|
|
9
11
|
/** The protocol of an agent working alone: no teammates, no ask. `run --agent` still ends with a `done` mail; `call` just answers. */
|
|
@@ -12,7 +14,7 @@ export declare function buildSoloSystemPrompt(project: ResolvedProject, agent: R
|
|
|
12
14
|
export declare function buildCallUserPrompt(agent: ResolvedAgent, task: string): string;
|
|
13
15
|
/** Everything that goes to the runtime's system prompt: the persona plus the team protocol. */
|
|
14
16
|
export declare function buildSystemPrompt(project: ResolvedProject, agent: ResolvedAgent): string;
|
|
15
|
-
/** Messages one wake-up handles: the oldest one, or
|
|
17
|
+
/** Messages one wake-up handles: the oldest one, or a run of `reply`/`failure` mail (up to LEAD_BATCH_LIMIT) so the agent decides once on all of it; the lead takes any such run, other agents only same-thread ones. */
|
|
16
18
|
export declare function pickMessages(project: ResolvedProject, agent: ResolvedAgent, queue: Message[]): Message[];
|
|
17
19
|
export declare function buildUserPrompt(project: ResolvedProject, agent: ResolvedAgent, unread: Message[], handleCount?: number, handoff?: boolean): string;
|
|
18
20
|
export interface TidyContext {
|
package/dist/prompt.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import { readFileCapped } from "./fs-util.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { REQUIRED_SECTIONS } from "./format.js";
|
|
5
|
-
import { commonFile, memoryDirs, outboxDir, repoDirFor, sharedDir, sharedIndex } from "./policy.js";
|
|
5
|
+
import { archiveFile, commonFile, decisionsFile, lessonsFile, memoryDirs, outboxDir, repoDirFor, sharedDir, sharedIndex } from "./policy.js";
|
|
6
6
|
function memoryLabel(agent, dir) {
|
|
7
7
|
if (dir === agent.memory.task)
|
|
8
8
|
return "task (this task only, isolated)";
|
|
@@ -11,6 +11,8 @@ function memoryLabel(agent, dir) {
|
|
|
11
11
|
export const COMMON_LIMIT = 8 * 1024;
|
|
12
12
|
export const MEMORY_INDEX_LIMIT = 4 * 1024;
|
|
13
13
|
export const HANDOFF_LIMIT = COMMON_LIMIT;
|
|
14
|
+
/** Cap of each of DECISIONS.md and LESSONS.md in a prompt. */
|
|
15
|
+
export const CONTEXT_LIMIT = 6 * 1024;
|
|
14
16
|
export const LEAD_BATCH_LIMIT = 5;
|
|
15
17
|
const BATCHABLE = new Set(["reply", "failure"]);
|
|
16
18
|
const readCapped = readFileCapped;
|
|
@@ -88,7 +90,7 @@ export function buildSystemPrompt(project, agent) {
|
|
|
88
90
|
? `When the whole job is finished, send a message with \`type: done\` (no \`to\` needed). That ends the run. A summary in your reply text is never delivered and does not end the run: after the last teammate reply is verified you MUST send \`done\` (or a \`task\` for the remaining work), or the run stops as "idle" with no final report.`
|
|
89
91
|
: `Report results to the lead by mail; the lead decides what happens next.`, isLead
|
|
90
92
|
? "Each wake-up handles one message, except that several `reply`/`failure` messages waiting together are handed to you in one wake-up: weigh them together, then decide the next step once."
|
|
91
|
-
: "Each wake-up handles one message. Do the work it asks, then send the replies that are needed, then stop.", "Mail that repeats an earlier mail of yours to the same agent (same type, same text) is not sent again; you are told, and should wait for the answer or ask for something different instead of resending.", "", "## Message format", "The body of every `task` and `reply` must contain these `##` headings, exactly as written (write `None` under a heading if there is nothing to say).", "
|
|
93
|
+
: "Each wake-up handles one message, except that consecutive `reply`/`failure` messages on the same thread may be handed to you together: handle them together. Do the work it asks, then send the replies that are needed, then stop.", "Mail that repeats an earlier mail of yours to the same agent (same type, same text) is not sent again; you are told, and should wait for the answer or ask for something different instead of resending.", "", "## Message format", "The body of every `task` and `reply` must contain these `##` headings, exactly as written (write `None` under a heading if there is nothing to say).", "A `task` or `reply` missing a heading is sent back to you (at most twice per sender + type + subject); after that it is delivered with a warning flagged to the recipient.", "", `- \`task\`: ${REQUIRED_SECTIONS.task.map((h) => `\`## ${h}\``).join(", ")} (Upstream = id of the mail this task derives from, or \`None\`).`, `- \`reply\`: ${REQUIRED_SECTIONS.reply.map((h) => `\`## ${h}\``).join(", ")}.`, isLead
|
|
92
94
|
? "- `done`: your final report to the user (shown when the run ends, saved as `result.md`). Its frontmatter must contain `outcome: completed|partial|blocked|failed`, and the body the headings `## Result` (the actual deliverable or conclusion, not just \"done\"), `## Files` (paths created or changed), `## Verification` (what you ran or checked, and what it showed) and `## Not done` (anything skipped or unverified, or `None`). Only `completed` means success, and it needs all four headings and every checklist step ticked; if work remains or is blocked, say `partial`/`blocked`/`failed` (those need only `## Result` and `## Not done`). A `done` that breaks this is sent back to you and does not end the run. " + OPEN_PR_RULE
|
|
93
95
|
: "- `done` has no required headings.", ...(agent.canAskUser
|
|
94
96
|
? [
|
|
@@ -130,18 +132,26 @@ export function buildSystemPrompt(project, agent) {
|
|
|
130
132
|
}
|
|
131
133
|
lines.push("", "## Rules");
|
|
132
134
|
lines.push("- Never edit any `AGENT.md`, `COMMON.md`, or the repository's own `CLAUDE.md`/`AGENTS.md`" +
|
|
133
|
-
(isLead || agent.canEditAgentMd ? " unless you need to as lead (changes are logged)." : "."), "- Never touch another agent's memory, inbox or outbox, or the team's config files.", `- The shared team context is in \`${commonFile(project)}\`${isLead ? " (you maintain it; keep it short)" : " (read-only)"}.`,
|
|
135
|
+
(isLead || agent.canEditAgentMd ? " unless you need to as lead (changes are logged)." : "."), "- Never touch another agent's memory, inbox or outbox, or the team's config files.", `- The shared team context is in \`${commonFile(project)}\`${isLead ? " (you maintain it; keep it short)" : " (read-only)"}.`, ...(isLead
|
|
136
|
+
? [
|
|
137
|
+
`- You also keep two durable records for every future run of this project: \`${decisionsFile(project)}\` (what was decided and why; a new decision replaces the one it contradicts) and \`${lessonsFile(project)}\` (heuristics and pitfalls that stay true). Update them before you send \`done\`. Each is shown in prompts only up to ${CONTEXT_LIMIT / 1024} KB: move the oldest entries into \`${archiveFile(decisionsFile(project))}\` / \`${archiveFile(lessonsFile(project))}\` (read on demand) to keep them under it.`,
|
|
138
|
+
]
|
|
139
|
+
: []), PR_ATTRIBUTION_RULE, NO_DAEMONS_RULE);
|
|
134
140
|
return lines.join("\n");
|
|
135
141
|
}
|
|
136
|
-
/** Messages one wake-up handles: the oldest one, or
|
|
142
|
+
/** Messages one wake-up handles: the oldest one, or a run of `reply`/`failure` mail (up to LEAD_BATCH_LIMIT) so the agent decides once on all of it; the lead takes any such run, other agents only same-thread ones. */
|
|
137
143
|
export function pickMessages(project, agent, queue) {
|
|
138
144
|
const sorted = [...queue].sort((a, b) => path.basename(a.file).localeCompare(path.basename(b.file)));
|
|
139
|
-
|
|
145
|
+
const first = sorted[0];
|
|
146
|
+
if (!first || !BATCHABLE.has(first.meta.type))
|
|
140
147
|
return sorted.slice(0, 1);
|
|
148
|
+
const isLead = agent.name === project.lead;
|
|
141
149
|
const run = [];
|
|
142
150
|
for (const m of sorted) {
|
|
143
151
|
if (!BATCHABLE.has(m.meta.type) || run.length >= LEAD_BATCH_LIMIT)
|
|
144
152
|
break;
|
|
153
|
+
if (!isLead && m.meta.thread !== first.meta.thread)
|
|
154
|
+
break;
|
|
145
155
|
run.push(m);
|
|
146
156
|
}
|
|
147
157
|
return run;
|
|
@@ -158,6 +168,17 @@ export function buildUserPrompt(project, agent, unread, handleCount = 1, handoff
|
|
|
158
168
|
parts.push(`\n[COMMON.md was truncated at ${COMMON_LIMIT} bytes — the lead should condense it.]`);
|
|
159
169
|
}
|
|
160
170
|
}
|
|
171
|
+
for (const [title, file] of [
|
|
172
|
+
["Project decisions (DECISIONS.md)", decisionsFile(project)],
|
|
173
|
+
["Project lessons (LESSONS.md)", lessonsFile(project)],
|
|
174
|
+
]) {
|
|
175
|
+
const rec = readCapped(file, CONTEXT_LIMIT);
|
|
176
|
+
if (!rec?.text.trim())
|
|
177
|
+
continue; // nothing recorded yet: no heading, no tokens
|
|
178
|
+
parts.push("", `# ${title}`, rec.text.trim());
|
|
179
|
+
if (rec.truncated)
|
|
180
|
+
parts.push(`\n[${path.basename(file)} was truncated at ${CONTEXT_LIMIT} bytes — the lead should move the oldest entries to ${path.basename(archiveFile(file))}.]`);
|
|
181
|
+
}
|
|
161
182
|
const handoffFile = handoff ? sharedIndex(project) : undefined;
|
|
162
183
|
const notes = handoffFile ? readCapped(handoffFile, HANDOFF_LIMIT) : undefined;
|
|
163
184
|
if (handoffFile && notes?.text.trim()) {
|
package/dist/run-session.js
CHANGED
|
@@ -382,6 +382,8 @@ export class RunSession {
|
|
|
382
382
|
state.sessions[sessionKey] = result.sessionId;
|
|
383
383
|
if (result.outputTokens)
|
|
384
384
|
state.output_tokens += result.outputTokens;
|
|
385
|
+
state.cache_read_tokens += result.cacheReadTokens ?? 0;
|
|
386
|
+
state.cache_write_tokens += result.cacheWriteTokens ?? 0;
|
|
385
387
|
delete state.active[agent.name];
|
|
386
388
|
// With a chain only: which model ran, what it replaced, why it failed.
|
|
387
389
|
const which = chain ? { runtime: c.runtime, model: c.model, fallback_from: c.primary || !replaced ? undefined : modelLabel(replaced), kind: result.kind } : {};
|
|
@@ -392,6 +394,8 @@ export class RunSession {
|
|
|
392
394
|
duration_ms: Date.now() - started,
|
|
393
395
|
ok: result.ok,
|
|
394
396
|
output_tokens: result.outputTokens,
|
|
397
|
+
cache_read_tokens: result.cacheReadTokens,
|
|
398
|
+
cache_write_tokens: result.cacheWriteTokens,
|
|
395
399
|
handling,
|
|
396
400
|
error: result.error,
|
|
397
401
|
...which,
|
package/dist/run-store.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface WakeRecord {
|
|
|
32
32
|
duration_ms: number;
|
|
33
33
|
ok: boolean;
|
|
34
34
|
output_tokens?: number;
|
|
35
|
+
/** Prompt-cache input tokens read / written by this wake (claude only). */
|
|
36
|
+
cache_read_tokens?: number;
|
|
37
|
+
cache_write_tokens?: number;
|
|
35
38
|
handling: WakeTopic[];
|
|
36
39
|
/** Mail this wake handed to others; `to` is "(done)" for the lead's final message. */
|
|
37
40
|
sent?: SentTopic[];
|
|
@@ -105,6 +108,9 @@ export interface RunState {
|
|
|
105
108
|
pr_url?: string;
|
|
106
109
|
sessions: Record<string, string>;
|
|
107
110
|
output_tokens: number;
|
|
111
|
+
/** Run totals of the wakes' prompt-cache input tokens (0 for runs before this was recorded, and for codex). */
|
|
112
|
+
cache_read_tokens: number;
|
|
113
|
+
cache_write_tokens: number;
|
|
108
114
|
/** `runtime`/`model` only for agents with a fallback chain. */
|
|
109
115
|
last_wake: Record<string, {
|
|
110
116
|
at: string;
|
package/dist/run-store.js
CHANGED
|
@@ -46,6 +46,8 @@ const WireState = z
|
|
|
46
46
|
ask_rejections: z.record(z.string(), nonNeg).optional(),
|
|
47
47
|
sessions: z.record(z.string(), z.string()).optional(),
|
|
48
48
|
output_tokens: nonNeg.optional(),
|
|
49
|
+
cache_read_tokens: nonNeg.optional(),
|
|
50
|
+
cache_write_tokens: nonNeg.optional(),
|
|
49
51
|
last_wake: z.record(z.string(), z.any()).optional(),
|
|
50
52
|
pid: z.number().optional(),
|
|
51
53
|
task_summary: z.string().optional(),
|
|
@@ -86,6 +88,8 @@ function normalize(w) {
|
|
|
86
88
|
max_rounds: w.max_rounds ?? 0,
|
|
87
89
|
sessions: w.sessions ?? {},
|
|
88
90
|
output_tokens: w.output_tokens ?? 0,
|
|
91
|
+
cache_read_tokens: w.cache_read_tokens ?? 0,
|
|
92
|
+
cache_write_tokens: w.cache_write_tokens ?? 0,
|
|
89
93
|
last_wake: w.last_wake ?? {},
|
|
90
94
|
pid: w.pid ?? 0,
|
|
91
95
|
task_summary: w.task_summary ?? "",
|
package/dist/scaffold.js
CHANGED
|
@@ -6,7 +6,8 @@ const PERSONAS = {
|
|
|
6
6
|
|
|
7
7
|
You coordinate the team. You receive the user's task, break it into concrete pieces, and delegate each piece to the right teammate by mail.
|
|
8
8
|
|
|
9
|
-
- Keep \`COMMON.md\` (shared team context: goal,
|
|
9
|
+
- Keep \`COMMON.md\` (shared team context: goal, conventions) accurate and short.
|
|
10
|
+
- Before you send \`done\`, record what was decided and why in \`DECISIONS.md\` and what the project learned in \`LESSONS.md\` (next to \`COMMON.md\`); later runs start from them.
|
|
10
11
|
- Write every task with the standard task headings, and cite the mail it derives from under Upstream so teammates can trace it.
|
|
11
12
|
- When a teammate reports back, decide the next step: assign follow-up work, ask another teammate to verify, or accept the result.
|
|
12
13
|
- Teammates cannot message each other by default; route everything through yourself.
|
package/dist/status-watch.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResolvedProject } from "./config.js";
|
|
2
2
|
import { type StatusReport } from "./status.js";
|
|
3
|
-
/** What `status --json --watch` prints, one JSON object per line: the project's report plus one per
|
|
3
|
+
/** What `status --json --watch` prints, one JSON object per line: the project's report plus one per task that is not completed. */
|
|
4
4
|
export type WatchFrame = {
|
|
5
5
|
schema_version: 1;
|
|
6
6
|
report?: StatusReport;
|
|
@@ -8,9 +8,10 @@ export type WatchFrame = {
|
|
|
8
8
|
error?: string;
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* The frame for now. Every
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The frame for now. Every task that is not completed (running, interrupted, waiting for answers, or ended partial,
|
|
12
|
+
* blocked, failed or cancelled) gets its own report, so it stays listed until it is resumed to completion or cleared.
|
|
13
|
+
* A task that was running in the previous frame and has completed since is included once more, so the reader sees
|
|
14
|
+
* it finish (and can toast) before it is dropped. `tracked` is that previous set; it is replaced by the ids running now.
|
|
14
15
|
*/
|
|
15
16
|
export declare function buildWatchFrame(project: ResolvedProject, tracked: Set<string>): WatchFrame;
|
|
16
17
|
export interface WatchDeps {
|
package/dist/status-watch.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { buildStatusReport, buildTaskListReport } from "./status.js";
|
|
2
|
+
import { buildStatusReport, buildTaskListReport, isCompletedRun } from "./status.js";
|
|
3
3
|
/**
|
|
4
|
-
* The frame for now. Every
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The frame for now. Every task that is not completed (running, interrupted, waiting for answers, or ended partial,
|
|
5
|
+
* blocked, failed or cancelled) gets its own report, so it stays listed until it is resumed to completion or cleared.
|
|
6
|
+
* A task that was running in the previous frame and has completed since is included once more, so the reader sees
|
|
7
|
+
* it finish (and can toast) before it is dropped. `tracked` is that previous set; it is replaced by the ids running now.
|
|
7
8
|
*/
|
|
8
9
|
export function buildWatchFrame(project, tracked) {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const all = buildTaskListReport(project).runs;
|
|
11
|
+
const running = all.filter((r) => r.state === "running").map((r) => r.run_id);
|
|
12
|
+
const open = all.filter((r) => !isCompletedRun(r)).map((r) => r.run_id);
|
|
12
13
|
const runs = {};
|
|
13
|
-
for (const id of new Set([...
|
|
14
|
+
for (const id of new Set([...open, ...tracked]))
|
|
14
15
|
runs[id] = buildStatusReport(project, id);
|
|
15
16
|
const report = buildStatusReport(project);
|
|
16
17
|
tracked.clear();
|
package/dist/status.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface Paint {
|
|
|
43
43
|
cyan: (t: string) => string;
|
|
44
44
|
}
|
|
45
45
|
export declare function paint(on: boolean): Paint;
|
|
46
|
+
/** "快取 讀 288,124 寫 21,364 命中 93%": the share of cached input that was read, not written. Empty when nothing was recorded. */
|
|
47
|
+
export declare function cacheText(read: number | undefined, write: number | undefined): string;
|
|
46
48
|
export declare function progressBar(done: number, total: number, width?: number, c?: Paint): string;
|
|
47
49
|
export interface QueueItem {
|
|
48
50
|
agent: string;
|
|
@@ -74,6 +76,9 @@ export interface RunReport {
|
|
|
74
76
|
/** Set when an agent kept failing and the run stopped: `resume` after fixing the cause. */
|
|
75
77
|
failure?: RunFailure;
|
|
76
78
|
output_tokens: number;
|
|
79
|
+
/** Prompt-cache input tokens read / written over the run (claude only; 0 when unknown). */
|
|
80
|
+
cache_read_tokens: number;
|
|
81
|
+
cache_write_tokens: number;
|
|
77
82
|
task_source: "text" | "file";
|
|
78
83
|
task_path?: string;
|
|
79
84
|
task_summary: string;
|
|
@@ -161,6 +166,8 @@ export declare function buildRunReport(run: {
|
|
|
161
166
|
}, project?: ResolvedProject, opts?: {
|
|
162
167
|
detail?: boolean;
|
|
163
168
|
}): RunReport;
|
|
169
|
+
/** A run that ended with a verified-or-inferred `completed` outcome: the only kind the monitor stops listing. */
|
|
170
|
+
export declare const isCompletedRun: (r: Pick<RunReport, "state" | "outcome">) => boolean;
|
|
164
171
|
/** The status of a project: its agents and one run (the newest running one, else the newest, or the one named by `runId`). */
|
|
165
172
|
export declare function buildStatusReport(base: ResolvedProject, runId?: string, now?: number): StatusReport;
|
|
166
173
|
/** Every run of the project, newest first. */
|
package/dist/status.js
CHANGED
|
@@ -120,6 +120,11 @@ const cols = (t) => [...t].reduce((n, ch) => n + ((ch.codePointAt(0) ?? 0) >= 0x
|
|
|
120
120
|
const padCols = (t, n) => t + " ".repeat(Math.max(0, n - cols(t)));
|
|
121
121
|
const clip = (t, n) => (t.length > n ? t.slice(0, n - 1) + "…" : t);
|
|
122
122
|
const tokens = (n) => (n ? n.toLocaleString("en-US") : "0");
|
|
123
|
+
/** "快取 讀 288,124 寫 21,364 命中 93%": the share of cached input that was read, not written. Empty when nothing was recorded. */
|
|
124
|
+
export function cacheText(read, write) {
|
|
125
|
+
const [r, w] = [read ?? 0, write ?? 0];
|
|
126
|
+
return r + w > 0 ? `快取 讀 ${tokens(r)} 寫 ${tokens(w)} 命中 ${Math.round((100 * r) / (r + w))}%` : "";
|
|
127
|
+
}
|
|
123
128
|
function since(iso, now) {
|
|
124
129
|
const sec = Math.max(0, Math.round((now - Date.parse(iso)) / 1000));
|
|
125
130
|
return sec < 60 ? `${sec}s` : `${Math.floor(sec / 60)}m${String(sec % 60).padStart(2, "0")}s`;
|
|
@@ -207,6 +212,8 @@ export function buildRunReport(run, project, opts = {}) {
|
|
|
207
212
|
max_rounds: s.max_rounds,
|
|
208
213
|
...(s.failure ? { failure: s.failure } : {}),
|
|
209
214
|
output_tokens: s.output_tokens,
|
|
215
|
+
cache_read_tokens: s.cache_read_tokens ?? 0,
|
|
216
|
+
cache_write_tokens: s.cache_write_tokens ?? 0,
|
|
210
217
|
task_source: s.task_source,
|
|
211
218
|
task_path: s.task_path,
|
|
212
219
|
task_summary: s.task_summary,
|
|
@@ -226,6 +233,8 @@ export function buildRunReport(run, project, opts = {}) {
|
|
|
226
233
|
...(ask ? { ask } : {}),
|
|
227
234
|
};
|
|
228
235
|
}
|
|
236
|
+
/** A run that ended with a verified-or-inferred `completed` outcome: the only kind the monitor stops listing. */
|
|
237
|
+
export const isCompletedRun = (r) => r.state === "ended" && r.outcome === "completed";
|
|
229
238
|
const projectInfo = (p) => ({ name: p.name, dir: p.dir, lead: p.lead });
|
|
230
239
|
/** The fallback part of an agent's report: nothing at all for an agent without a chain. */
|
|
231
240
|
function fallbackView(a, state, now) {
|
|
@@ -362,7 +371,7 @@ function summaryBlock(run, now, c) {
|
|
|
362
371
|
/** What a run is doing / did, one line per wake. */
|
|
363
372
|
function describeRun(s, now, c, limit = Infinity) {
|
|
364
373
|
const lines = [
|
|
365
|
-
`${c.bold("執行 " + runLabel(s))} [${stateColor(c, reportLabel(s))}] 輪次 ${roundsLabel(s.rounds, s.max_rounds)} 輸出 tokens ${tokens(s.output_tokens)}`,
|
|
374
|
+
`${c.bold("執行 " + runLabel(s))} [${stateColor(c, reportLabel(s))}] 輪次 ${roundsLabel(s.rounds, s.max_rounds)} 輸出 tokens ${tokens(s.output_tokens)}${cacheText(s.cache_read_tokens, s.cache_write_tokens) ? ` ${cacheText(s.cache_read_tokens, s.cache_write_tokens)}` : ""}`,
|
|
366
375
|
` 任務:${taskText(s)}`,
|
|
367
376
|
];
|
|
368
377
|
const steps = stepsLine(s.steps, c);
|
|
@@ -501,7 +510,7 @@ export function formatStatusReport(report, now = Date.now(), color = false) {
|
|
|
501
510
|
if (fb.length)
|
|
502
511
|
lines.push("", ...fb);
|
|
503
512
|
if (run)
|
|
504
|
-
lines.push("", `輸出 tokens:${tokens(run.output_tokens)}`);
|
|
513
|
+
lines.push("", `輸出 tokens:${tokens(run.output_tokens)}${cacheText(run.cache_read_tokens, run.cache_write_tokens) ? ` ${cacheText(run.cache_read_tokens, run.cache_write_tokens)}` : ""}`);
|
|
505
514
|
if (report.memory_advice?.length)
|
|
506
515
|
lines.push("", `記憶:${report.memory_advice.join(";")}(建議執行 agent-lyceum tidy -p ${report.project.name};status 只提示,不會自動整理)`);
|
|
507
516
|
return lines.join("\n");
|