agent-lyceum 0.35.0 → 0.36.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.
Files changed (2) hide show
  1. package/dist/prompt.js +38 -33
  2. package/package.json +1 -1
package/dist/prompt.js CHANGED
@@ -13,6 +13,25 @@ function memoryLabel(agent, dir) {
13
13
  const kind = dir === (agent.memory.global ?? agent.sharedMemory?.global) ? "global (all projects" : "project (all tasks of this project";
14
14
  return shared ? `${kind}, read-only for you: ${instance1} maintains it)` : `${kind})`;
15
15
  }
16
+ /** Escape a value for a double-quoted attribute of the tags that wrap injected data. */
17
+ function attr(v) {
18
+ return v.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
19
+ }
20
+ /** One memory directory as a `<memory>` block: its MEMORY.md index, or a hint to create one. */
21
+ function memoryBlock(agent, dir) {
22
+ const idx = readCapped(path.join(dir, "MEMORY.md"), MEMORY_INDEX_LIMIT);
23
+ const scope = memoryLabel(agent, dir).split(" ")[0];
24
+ const out = [`<memory scope="${attr(scope ?? "")}" dir="${attr(dir)}" index="MEMORY.md">`];
25
+ if (!idx)
26
+ out.push("(no MEMORY.md yet — create one when you have something worth keeping)");
27
+ else {
28
+ out.push(idx.text.trim());
29
+ if (idx.truncated)
30
+ out.push(`\n[index truncated at ${MEMORY_INDEX_LIMIT} bytes — trim it]`);
31
+ }
32
+ out.push("</memory>");
33
+ return out;
34
+ }
16
35
  /** Lead-only rules that keep the number of wake-ups down; placed after the progress-tracking block. */
17
36
  export const WAKE_ECONOMY_SECTION = [
18
37
  "## Keeping wake-ups few",
@@ -132,15 +151,9 @@ export function buildSoloSystemPrompt(project, agent) {
132
151
  /** The prompt of a `call`: the memory indexes the agent is used to, then the task. */
133
152
  export function buildCallUserPrompt(agent, task) {
134
153
  const parts = [];
135
- for (const dir of memoryDirs(agent)) {
136
- const idx = readCapped(path.join(dir, "MEMORY.md"), MEMORY_INDEX_LIMIT);
137
- parts.push(`# Your ${memoryLabel(agent, dir).split(" ")[0]} memory index (${dir}/MEMORY.md)`);
138
- if (!idx)
139
- parts.push("(no MEMORY.md yet — create one when you have something worth keeping)", "");
140
- else
141
- parts.push(idx.text.trim(), ...(idx.truncated ? [`[index truncated at ${MEMORY_INDEX_LIMIT} bytes — trim it]`] : []), "");
142
- }
143
- parts.push("# Task", task.trim());
154
+ for (const dir of memoryDirs(agent))
155
+ parts.push(...memoryBlock(agent, dir), "");
156
+ parts.push("<task>", task.trim(), "</task>");
144
157
  return parts.join("\n");
145
158
  }
146
159
  /** Everything that goes to the runtime's system prompt: the persona plus the team protocol. */
@@ -269,7 +282,7 @@ export function pickMessages(project, agent, queue) {
269
282
  export function buildUserPrompt(project, agent, unread, handleCount = 1, handoff = false) {
270
283
  const parts = [];
271
284
  const common = readCapped(commonFile(project), COMMON_LIMIT);
272
- parts.push("# Shared team context (COMMON.md)");
285
+ parts.push('<shared_context source="COMMON.md">');
273
286
  if (!common)
274
287
  parts.push("(empty)");
275
288
  else {
@@ -278,56 +291,48 @@ export function buildUserPrompt(project, agent, unread, handleCount = 1, handoff
278
291
  parts.push(`\n[COMMON.md was truncated at ${COMMON_LIMIT} bytes — the lead should condense it.]`);
279
292
  }
280
293
  }
281
- for (const [title, kind] of [
282
- ["Project decisions", "decisions"],
283
- ["Project lessons", "lessons"],
294
+ parts.push("</shared_context>");
295
+ for (const [tag, kind] of [
296
+ ["project_decisions", "decisions"],
297
+ ["project_lessons", "lessons"],
284
298
  ]) {
285
299
  const view = buildRecordsView(project, kind, CONTEXT_LIMIT);
286
300
  if (view)
287
- parts.push("", `# ${title}`, view); // nothing recorded yet: no heading, no tokens
301
+ parts.push("", `<${tag}>`, view, `</${tag}>`); // nothing recorded yet: no tag, no tokens
288
302
  }
289
303
  const handoffFile = handoff ? sharedIndex(project) : undefined;
290
304
  const notes = handoffFile ? readCapped(handoffFile, HANDOFF_LIMIT) : undefined;
291
305
  if (handoffFile && notes?.text.trim()) {
292
- parts.push("", `# Handoff notes from earlier segments of this task (${handoffFile})`, notes.text.trim());
306
+ parts.push("", `<handoff_notes from="earlier segments of this task" file="${attr(handoffFile)}">`, notes.text.trim());
293
307
  if (notes.truncated)
294
308
  parts.push(`\n[handoff index truncated at ${HANDOFF_LIMIT} bytes — the lead should condense it.]`);
295
- parts.push("The other files in that folder are there to read on demand.");
296
- }
297
- for (const dir of [...Object.values(agent.sharedMemory ?? {}), ...memoryDirs(agent)]) {
298
- const idx = readCapped(path.join(dir, "MEMORY.md"), MEMORY_INDEX_LIMIT);
299
- const label = memoryLabel(agent, dir).split(" ")[0];
300
- parts.push("", `# Your ${label} memory index (${dir}/MEMORY.md)`);
301
- if (!idx)
302
- parts.push("(no MEMORY.md yet — create one when you have something worth keeping)");
303
- else {
304
- parts.push(idx.text.trim());
305
- if (idx.truncated)
306
- parts.push(`\n[index truncated at ${MEMORY_INDEX_LIMIT} bytes — trim it]`);
307
- }
309
+ parts.push("The other files in that folder are there to read on demand.", "</handoff_notes>");
308
310
  }
311
+ for (const dir of [...Object.values(agent.sharedMemory ?? {}), ...memoryDirs(agent)])
312
+ parts.push("", ...memoryBlock(agent, dir));
309
313
  const sorted = [...unread].sort((a, b) => path.basename(a.file).localeCompare(path.basename(b.file)));
310
314
  const current = sorted.slice(0, handleCount);
311
315
  const queued = sorted.slice(handleCount);
312
- parts.push("", current.length > 1 ? `# Messages to handle now (${current.length}, oldest first — decide on them together)` : "# Message to handle now");
316
+ parts.push("", current.length > 1 ? `<messages count="${current.length}" order="oldest first" note="decide on them together">` : `<messages count="${current.length}">`);
313
317
  if (current.length === 0)
314
318
  parts.push("(no message)");
315
319
  let remaining = MAIL_BATCH_LIMIT;
316
320
  current.forEach((m, i) => {
317
- if (current.length > 1)
318
- parts.push("", `## Message ${i + 1} of ${current.length}`);
321
+ parts.push(current.length > 1 ? `<message n="${i + 1}" of="${current.length}">` : "<message>");
319
322
  const raw = Buffer.from(m.body, "utf8");
320
323
  const shown = cutUtf8(raw, Math.min(MAIL_BODY_LIMIT, remaining));
321
324
  remaining -= shown.length;
322
325
  const body = shown.length === raw.length
323
326
  ? m.body
324
327
  : `${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)]`;
325
- 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);
328
+ 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, "</message>");
326
329
  });
330
+ parts.push("</messages>");
327
331
  if (queued.length) {
328
- parts.push("", "# Queued messages (not handled in this wake-up; each gets its own wake-up later, titles only)");
332
+ parts.push("", '<queued_messages note="not handled in this wake-up; each gets its own wake-up later, titles only">');
329
333
  for (const m of queued)
330
334
  parts.push(`- from ${senderLabel(m.meta.from, project.agents)} [${m.meta.type}] "${m.meta.subject}" -> ${m.file}`);
335
+ parts.push("</queued_messages>");
331
336
  }
332
337
  return parts.join("\n");
333
338
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-lyceum",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Config + dispatcher for teams of Claude Code / Codex agents with scoped context and file-based mailboxes",
5
5
  "keywords": [
6
6
  "ai",