agent-lyceum 0.25.1 → 0.27.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 +6 -2
- package/README.zh-TW.md +6 -2
- package/dist/call.js +2 -0
- package/dist/config.d.ts +29 -1
- package/dist/config.js +34 -3
- 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/duplicate-notices.d.ts +9 -0
- package/dist/duplicate-notices.js +34 -0
- package/dist/fallback.d.ts +4 -2
- package/dist/fallback.js +11 -4
- package/dist/guard.d.ts +17 -1
- package/dist/guard.js +37 -6
- 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 +46 -2
- package/dist/mailbox.js +121 -36
- package/dist/message-store.d.ts +47 -1
- package/dist/message-store.js +75 -7
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +27 -2
- package/dist/policy.d.ts +1 -0
- package/dist/policy.js +2 -2
- package/dist/prompt.d.ts +11 -0
- package/dist/prompt.js +66 -10
- package/dist/run-session.d.ts +35 -2
- package/dist/run-session.js +337 -20
- package/dist/run-store.d.ts +17 -3
- package/dist/run-store.js +17 -7
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.js +10 -1
- package/dist/schema.d.ts +19 -0
- package/dist/schema.js +12 -0
- package/dist/status.d.ts +7 -0
- package/dist/status.js +74 -15
- package/dist/tiers.d.ts +27 -0
- package/dist/tiers.js +49 -0
- package/dist/validate.js +28 -1
- package/dist/worktree.d.ts +6 -0
- package/dist/worktree.js +16 -2
- package/package.json +1 -1
package/dist/prompt.js
CHANGED
|
@@ -2,13 +2,45 @@ import fs from "node:fs";
|
|
|
2
2
|
import { cutUtf8, readFileCapped } from "./fs-util.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { REQUIRED_SECTIONS } from "./format.js";
|
|
5
|
+
import { senderLabel, slotName } from "./instances.js";
|
|
6
|
+
import { modelForTier } from "./tiers.js";
|
|
5
7
|
import { buildRecordsView, RECORDS_BUDGET } from "./records.js";
|
|
6
8
|
import { commonFile, memoryDirs, outboxDir, recordsKindDir, repoDirFor, sharedDir, sharedIndex } from "./policy.js";
|
|
7
9
|
function memoryLabel(agent, dir) {
|
|
8
10
|
if (dir === agent.memory.task)
|
|
9
11
|
return "task (this task only, isolated)";
|
|
10
|
-
|
|
12
|
+
const shared = agent.sharedMemory && Object.values(agent.sharedMemory).includes(dir);
|
|
13
|
+
const instance1 = agent.instance ? slotName(agent.instance.role, 1) : "";
|
|
14
|
+
const kind = dir === (agent.memory.global ?? agent.sharedMemory?.global) ? "global (all projects" : "project (all tasks of this project";
|
|
15
|
+
return shared ? `${kind}, read-only for you: ${instance1} maintains it)` : `${kind})`;
|
|
11
16
|
}
|
|
17
|
+
/** Lead-only rules that keep the number of wake-ups down; placed after the progress-tracking block. */
|
|
18
|
+
export const WAKE_ECONOMY_SECTION = [
|
|
19
|
+
"## Keeping wake-ups few",
|
|
20
|
+
"Every wake-up re-reads your whole context (prompt, memory, mail), so the number of wake-ups, yours and your teammates', is the main cost of a run.",
|
|
21
|
+
"- Send all work that is independent of each other in the same wake-up (for example the implementation and the matching docs), each task naming its reviewer, instead of one task per wake-up.",
|
|
22
|
+
"- When a reply arrives while other work you sent is still out and the reply needs nothing from you yet, send no mail: note it in your task memory and end the wake-up. Decide the next step once, when the rest is back; replies waiting together are handed to you in one wake-up.",
|
|
23
|
+
"- Skip the QA layer (the verification role and its reviewer) for a small fix: a change of a few lines that has a reproduction test which failed before and passes after, and an approving code review. Send QA for large changes, changes across modules, and changes to user-visible behaviour.",
|
|
24
|
+
"- Use the most capable (most expensive) model only at three moments: before the plan is final, when the same error shows up a second time, and before you send `done`. In other rounds prefer teammates on cheaper models; read-only search work suits the cheapest one.",
|
|
25
|
+
];
|
|
26
|
+
/** The lead's system prompt gets this at its very end when same-role instances are possible in the run (spec 23.12). No trailing newline. */
|
|
27
|
+
export const PARALLEL_SECTION = (n) => [
|
|
28
|
+
"## Parallel tasks",
|
|
29
|
+
`A member can work on up to ${n} tasks at the same time, each in its own instance with its own worktree. To use this, send each independent task as its own new \`task\` (not a reply, no \`reply_to\`) with the frontmatter line \`parallel: true\`. Only your own mail can do this. Instance 1 keeps the member's name; the others are named \`<member>--2\`, \`<member>--3\`, and their mail reaches you from those names.`,
|
|
30
|
+
"- Mark tasks parallel only when they are independent of each other and change different files. Before you send them, list in each task's `## Scope` the files it will touch; if two lists overlap, do not mark them parallel.",
|
|
31
|
+
"- Never run in parallel work that changes shared components, types, routes, configuration or design tokens: send that first, without the flag, and send the dependent tasks after it is integrated. Changes to the same file are not merged: the later one is blocked and comes back to you as a failure.",
|
|
32
|
+
"- Revisions go back to the instance that did the work: reply to its mail (`reply_to`). An instance whose work is blocked takes no new task, also not one addressed to it by name, until that work is in the repo; to redo it, reply to the failure about it.",
|
|
33
|
+
"- Without `parallel: true`, mail goes to the member itself, one task at a time.",
|
|
34
|
+
"- When rate limits hit, fewer instances may start; waiting tasks run when an instance is free.",
|
|
35
|
+
"- After parallel work is back, have one member type-check and test the combined result before you report `done`.",
|
|
36
|
+
].join("\n");
|
|
37
|
+
/** The lead's system prompt ends with this when a member can run a task on a cheaper model (spec 24.10); `members` = [name, model of low, mid, high]. No trailing newline. */
|
|
38
|
+
export const TIER_SECTION = (members) => [
|
|
39
|
+
"## Task tiers",
|
|
40
|
+
"Some members can run a task on a cheaper model than their own. Add the frontmatter line `tier: low`, `tier: mid` or `tier: high` to a `task` you send them; without it they use their own model. Pick by difficulty: `low` for mechanical work with an exact spec (renames, a one-line fix, running a given command), `mid` for routine changes in one module, `high` (or no tier) for design, debugging, reviews and anything unclear. A tier lasts for that one task: replies and later mail on the thread run on the member's own model, so put `tier` again on each follow-up task. Each model keeps its own session, so a member that resumes sessions does not see what it did on another model; say in the task what it needs. If a tiered task comes back poor, send it again with a higher tier.",
|
|
41
|
+
"Members and the models each tier uses:",
|
|
42
|
+
...members.map((m) => `- ${m.name}: low ${m.low}, mid ${m.mid}, high ${m.high}`),
|
|
43
|
+
].join("\n");
|
|
12
44
|
export const COMMON_LIMIT = 8 * 1024;
|
|
13
45
|
export const MEMORY_INDEX_LIMIT = 4 * 1024;
|
|
14
46
|
export const HANDOFF_LIMIT = COMMON_LIMIT;
|
|
@@ -81,12 +113,22 @@ export function buildSystemPrompt(project, agent) {
|
|
|
81
113
|
if (project.solo)
|
|
82
114
|
return buildSoloSystemPrompt(project, agent);
|
|
83
115
|
const outbox = outboxDir(project, agent.name);
|
|
84
|
-
|
|
116
|
+
// Instances 2 and up are not listed (the lead is told their names separately), and an instance does not list its own role's other instances.
|
|
117
|
+
const mates = Object.keys(project.agents).filter((n) => n !== agent.name && (project.agents[n]?.instance?.k ?? 1) < 2 && (!agent.instance || project.agents[n]?.instance?.role !== agent.instance.role));
|
|
85
118
|
const recipients = agent.canMessage === "all" ? mates : agent.canMessage.filter((n) => project.agents[n] && n !== agent.name);
|
|
86
119
|
const isLead = agent.name === project.lead;
|
|
87
120
|
const lines = [];
|
|
88
121
|
lines.push(readPersona(agent), "", "---", "", "# Team protocol (injected by agent-lyceum)", "");
|
|
89
|
-
lines.push(
|
|
122
|
+
lines.push(agent.instance && agent.instance.k >= 2
|
|
123
|
+
? `You are **${agent.name}**, instance ${agent.instance.k} of **${agent.instance.role}** in the team "${project.name}". Other instances of ${agent.instance.role} may be working on other tasks at the same time, each in its own worktree. You work in the repository \`${repoDirFor(project, agent.name)}\`.`
|
|
124
|
+
: `You are **${agent.name}**, a member of the team "${project.name}". You work in the repository \`${repoDirFor(project, agent.name)}\`.`, ...(agent.instance?.k === 1
|
|
125
|
+
? [
|
|
126
|
+
`Other instances of you${(() => {
|
|
127
|
+
const names = Object.values(project.agents).filter((a) => a.instance?.role === agent.name && a.instance.k >= 2).map((a) => a.name);
|
|
128
|
+
return names.length ? ` (${names.join(", ")})` : "";
|
|
129
|
+
})()} may work in parallel; only you write your global/project memory.`,
|
|
130
|
+
]
|
|
131
|
+
: []), ...(project.workspaces?.[agent.name]
|
|
90
132
|
? [
|
|
91
133
|
`That directory is your own isolated git worktree (branch \`${project.workspaces?.[agent.name]?.branch}\`): teammates cannot see your changes while you work, and you cannot see theirs. Do not commit, push, switch branches or touch any other checkout; the dispatcher collects your changes from this directory after you finish.`,
|
|
92
134
|
]
|
|
@@ -123,16 +165,19 @@ export function buildSystemPrompt(project, agent) {
|
|
|
123
165
|
"Your final `done` mail must also include the full `## Steps` checklist. Tick only steps that were actually done and verified; leave undone ones",
|
|
124
166
|
"(e.g. build, app start, commit) unticked and say so in the mail instead of reporting them as finished.",
|
|
125
167
|
]
|
|
126
|
-
: []), ...(isLead && sharedDir(project) ? handoffRule(sharedDir(project)) : []), "", "## Memory");
|
|
168
|
+
: []), ...(isLead ? ["", ...WAKE_ECONOMY_SECTION] : []), ...(isLead && sharedDir(project) ? handoffRule(sharedDir(project)) : []), "", "## Memory");
|
|
127
169
|
const mems = memoryDirs(agent);
|
|
170
|
+
const sharedMems = Object.values(agent.sharedMemory ?? {});
|
|
128
171
|
if (mems.length === 0)
|
|
129
172
|
lines.push("You have no long-term memory directory.");
|
|
130
|
-
for (const m of mems) {
|
|
173
|
+
for (const m of [...sharedMems, ...mems]) {
|
|
131
174
|
const label = memoryLabel(agent, m);
|
|
132
175
|
lines.push(`- ${label}: \`${m}\``);
|
|
133
176
|
}
|
|
134
177
|
if (mems.length) {
|
|
135
|
-
lines.push("Keep a `MEMORY.md` index in each memory directory (one line per entry, pointing at a file). Its contents are shown in your prompt", "on every wake-up; read other memory files on demand; you can write only inside these directories.", "Put anything specific to the current task (decisions, progress, findings, branch names) in the **task** memory: it is private to this task and starts empty for every new task.", "Also record approaches you considered but rejected, with the reason, so they can be revisited or reversed later.",
|
|
178
|
+
lines.push("Keep a `MEMORY.md` index in each memory directory (one line per entry, pointing at a file). Its contents are shown in your prompt", "on every wake-up; read other memory files on demand; you can write only inside these directories.", "Put anything specific to the current task (decisions, progress, findings, branch names) in the **task** memory: it is private to this task and starts empty for every new task.", "Also record approaches you considered but rejected, with the reason, so they can be revisited or reversed later.", ...(sharedMems.length
|
|
179
|
+
? ["You cannot write the shared memory: put lessons worth keeping in your reply to the lead."]
|
|
180
|
+
: ["Use the global/project memory only for durable lessons that stay true across tasks; never record task-specific details there."]));
|
|
136
181
|
}
|
|
137
182
|
lines.push("", "## Rules");
|
|
138
183
|
lines.push("- Never edit any `AGENT.md`, `COMMON.md`, or the repository's own `CLAUDE.md`/`AGENTS.md`" +
|
|
@@ -141,7 +186,18 @@ export function buildSystemPrompt(project, agent) {
|
|
|
141
186
|
`- You also keep two kinds of durable records for every future run of this project, one Markdown file per entry: decisions in \`${recordsKindDir(project, "decisions")}/\` (what was decided and why) and lessons in \`${recordsKindDir(project, "lessons")}/\` (heuristics and pitfalls that stay true). The file name without \`.md\` is the entry id (lowercase letters, digits, \`-\`). Each file starts with YAML frontmatter between \`---\` lines: \`status: active|superseded\`, \`date: YYYY-MM-DD\`, \`title: <one line>\`, optionally \`run: <run id>\` and \`supersedes: [<ids of entries of the same kind>]\`; the body is the decision or lesson and its reason. Prompts show only effective-active entries (\`status: active\` and not named in another entry's \`supersedes\`), newest first, up to ${CONTEXT_LIMIT / 1024} KB per kind, and list what does not fit as one line each. When an entry becomes outdated, mark it \`status: superseded\` or name it in a new entry's \`supersedes\`. Add or update entries before you send \`done\`.`,
|
|
142
187
|
]
|
|
143
188
|
: []), PR_ATTRIBUTION_RULE, NO_DAEMONS_RULE);
|
|
144
|
-
|
|
189
|
+
let text = lines.join("\n");
|
|
190
|
+
if (isLead && project.run?.instances)
|
|
191
|
+
text = `${text}\n\n${PARALLEL_SECTION(project.run.instances.max)}`;
|
|
192
|
+
if (isLead) {
|
|
193
|
+
const members = Object.values(project.agents)
|
|
194
|
+
.filter((a) => a.name !== project.lead && (!a.instance || a.instance.k === 1) && (a.tiers?.length ?? 0) >= 2)
|
|
195
|
+
.sort((x, y) => (x.name < y.name ? -1 : x.name > y.name ? 1 : 0))
|
|
196
|
+
.map((a) => ({ name: a.name, low: modelForTier(a.tiers, "low"), mid: modelForTier(a.tiers, "mid"), high: modelForTier(a.tiers, "high") }));
|
|
197
|
+
if (members.length)
|
|
198
|
+
text = `${text}\n\n${TIER_SECTION(members)}`;
|
|
199
|
+
}
|
|
200
|
+
return text;
|
|
145
201
|
}
|
|
146
202
|
/** 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. */
|
|
147
203
|
export function pickMessages(project, agent, queue) {
|
|
@@ -188,7 +244,7 @@ export function buildUserPrompt(project, agent, unread, handleCount = 1, handoff
|
|
|
188
244
|
parts.push(`\n[handoff index truncated at ${HANDOFF_LIMIT} bytes — the lead should condense it.]`);
|
|
189
245
|
parts.push("The other files in that folder are there to read on demand.");
|
|
190
246
|
}
|
|
191
|
-
for (const dir of memoryDirs(agent)) {
|
|
247
|
+
for (const dir of [...Object.values(agent.sharedMemory ?? {}), ...memoryDirs(agent)]) {
|
|
192
248
|
const idx = readCapped(path.join(dir, "MEMORY.md"), MEMORY_INDEX_LIMIT);
|
|
193
249
|
const label = memoryLabel(agent, dir).split(" ")[0];
|
|
194
250
|
parts.push("", `# Your ${label} memory index (${dir}/MEMORY.md)`);
|
|
@@ -216,12 +272,12 @@ export function buildUserPrompt(project, agent, unread, handleCount = 1, handoff
|
|
|
216
272
|
const body = shown.length === raw.length
|
|
217
273
|
? m.body
|
|
218
274
|
: `${shown.toString("utf8")}\n[agent-lyceum: this message was cut at ${shown.length} of ${raw.length} bytes; read the whole message in \`${m.file}\` (it moves to \`read/\` in the same folder after this wake-up)]`;
|
|
219
|
-
parts.push(`id: ${m.meta.id}`, `from: ${m.meta.from}`, `type: ${m.meta.type}`, `thread: ${m.meta.thread}`, `subject: ${m.meta.subject}`, "", body);
|
|
275
|
+
parts.push(`id: ${m.meta.id}`, `from: ${senderLabel(m.meta.from, project.agents)}`, `type: ${m.meta.type}`, `thread: ${m.meta.thread}`, `subject: ${m.meta.subject}`, "", body);
|
|
220
276
|
});
|
|
221
277
|
if (queued.length) {
|
|
222
278
|
parts.push("", "# Queued messages (not handled in this wake-up; each gets its own wake-up later, titles only)");
|
|
223
279
|
for (const m of queued)
|
|
224
|
-
parts.push(`- from ${m.meta.from} [${m.meta.type}] "${m.meta.subject}" -> ${m.file}`);
|
|
280
|
+
parts.push(`- from ${senderLabel(m.meta.from, project.agents)} [${m.meta.type}] "${m.meta.subject}" -> ${m.file}`);
|
|
225
281
|
}
|
|
226
282
|
return parts.join("\n");
|
|
227
283
|
}
|
package/dist/run-session.d.ts
CHANGED
|
@@ -63,6 +63,11 @@ export interface RunOptions {
|
|
|
63
63
|
log?: (line: string) => void;
|
|
64
64
|
/** The wait between retries of a model with a fallback chain; tests pass one that does not wait. Default: a timer cut short by `signal`. */
|
|
65
65
|
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Containment check for an instance (spec 23.11 D-17): returns why instance `k` of `role` must not be opened (it could write the role's
|
|
68
|
+
* shared memory), or undefined. Asked before activation, after an instance is created and when a run is resumed. Tests inject one.
|
|
69
|
+
*/
|
|
70
|
+
instanceCheck?: (role: string, k: number) => string | undefined;
|
|
66
71
|
}
|
|
67
72
|
export declare class RunSession {
|
|
68
73
|
private readonly opts;
|
|
@@ -74,9 +79,21 @@ export declare class RunSession {
|
|
|
74
79
|
private readonly cfg;
|
|
75
80
|
private readonly runId;
|
|
76
81
|
private readonly runDir;
|
|
77
|
-
/** The project bound to this run (own memory dirs and mailboxes). */
|
|
78
|
-
private
|
|
82
|
+
/** The project bound to this run (own memory dirs and mailboxes). Bound again when the run gets instances (spec 23.6). */
|
|
83
|
+
private project;
|
|
84
|
+
/** The project as configured (solo-narrowed), before it is bound to this run. */
|
|
85
|
+
private readonly base;
|
|
86
|
+
private readonly layout;
|
|
87
|
+
/** Roles that get no new instance in this process: a check after an instance was created found a conflict. */
|
|
88
|
+
private readonly noMoreInstances;
|
|
89
|
+
private readonly heldLogged;
|
|
90
|
+
private readonly ignoredNoted;
|
|
91
|
+
/** Held mail the assigner left in `pending/` this round (read by the stall check). */
|
|
92
|
+
private waiting;
|
|
79
93
|
private readonly wsMode;
|
|
94
|
+
/** D-17: the paths an instance would get against the shared memory of its role (writes nothing). */
|
|
95
|
+
private readonly containment;
|
|
96
|
+
private readonly instanceCheck;
|
|
80
97
|
private readonly log;
|
|
81
98
|
private readonly guard;
|
|
82
99
|
private readonly state;
|
|
@@ -110,6 +127,22 @@ export declare class RunSession {
|
|
|
110
127
|
private saveState;
|
|
111
128
|
private journal;
|
|
112
129
|
private note;
|
|
130
|
+
/** Spec 24.5.2: log every tier that was dropped; tell the sender once per reason (`no-tiers` is log-only). */
|
|
131
|
+
private noteTierIgnored;
|
|
132
|
+
/** Bind the project to this run again, after its slot table changed. */
|
|
133
|
+
private rebind;
|
|
134
|
+
/** What the router needs to hold and place mail for roles with several instances (spec 23.7.1). */
|
|
135
|
+
private routing;
|
|
136
|
+
/** Make a new instance durable and give it its folders; false (and the role gets no more instances in this process) when a check refuses it. */
|
|
137
|
+
private openInstance;
|
|
138
|
+
/** The one round of placing held mail, at the top of the loop (spec 23.7.2). Leaves what it could not place in `this.waiting`. */
|
|
139
|
+
private assignHeld;
|
|
140
|
+
private assignRole;
|
|
141
|
+
/**
|
|
142
|
+
* The run is about to end idle but mail is still held (spec D-16). Only a cooldown in the way: wait for it (cut short by a cancel) and
|
|
143
|
+
* return true to go round again. Otherwise tell the lead once per set of waiting mails; true when that mail is new (the lead has work).
|
|
144
|
+
*/
|
|
145
|
+
private stallHeld;
|
|
113
146
|
private pendingAgents;
|
|
114
147
|
private pickBatch;
|
|
115
148
|
private wake;
|