From 56d822dc2c9b78df50fad7928a8d088b7618e671 Mon Sep 17 00:00:00 2001 From: Will Pfleger Date: Sat, 11 Jul 2026 17:19:30 -0400 Subject: [PATCH] feat(desktop): surface Team Instructions as distinct observer section (#1759) Signed-off-by: Will Pfleger Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 --- .../agents/ui/agentSessionTranscript.test.mjs | 24 ++- .../ui/agentSessionTranscriptHelpers.test.mjs | 192 ++++++++++++++++++ .../ui/agentSessionTranscriptHelpers.ts | 57 +++++- .../e2e/observer-feed-screenshots.spec.ts | 32 ++- 4 files changed, 278 insertions(+), 27 deletions(-) diff --git a/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs b/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs index f3412f2e9..fb6b5ab08 100644 --- a/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs +++ b/desktop/src/features/agents/ui/agentSessionTranscript.test.mjs @@ -1634,13 +1634,14 @@ test("buildTranscript same-seq different-timestamp session/new events both produ ); }); -test("buildTranscript four-section system prompt card is standalone with all sections; CheckCheck context contains only Buzz/thread context", () => { - // Production scenario: harness emits [Base]/[System]/[Agent Memory — core]/[Channel Canvas] +test("buildTranscript five-section system prompt card is standalone with all sections; CheckCheck context contains only Buzz/thread context", () => { + // Production scenario: team-pack agent harness emits + // [Base]/[System (with team delimiter)]/[Agent Memory — core]/[Channel Canvas] // in systemPrompt. The display layer must: // (a) Render it as a standalone single block (acpSource "session/new"), // NOT inside any turn's prompt bundle. - // (b) The standalone item must carry all four sections in order: - // Base → System → Core Memory → Channel Canvas. + // (b) The standalone item must carry all five sections in order: + // Base → System → Team Instructions → Core Memory → Channel Canvas. // (c) The prompt segment's context (CheckCheck dialog) must contain only // the session/prompt:context sections (Buzz event + Thread context), // never the system-prompt sections. @@ -1676,6 +1677,10 @@ test("buildTranscript four-section system prompt card is standalone with all sec "[System]", "Custom persona.", "", + "---", + "# Team Instructions", + "Always tag on handoff.", + "", "[Agent Memory — core]", "I am Duncan.", "", @@ -1740,14 +1745,14 @@ test("buildTranscript four-section system prompt card is standalone with all sec "exactly one standalone system-prompt single block", ); - // (b) The standalone item carries all four sections in order. + // (b) The standalone item carries all five sections in order. const spItem = systemPromptBlocks[0].item; assert.ok(spItem, "system-prompt block must have an item"); const titles = (spItem.sections ?? []).map((s) => s.title); assert.deepEqual( titles, - ["Base", "System", "Core Memory", "Channel Canvas"], - "system-prompt standalone card must carry Base → System → Core Memory → Channel Canvas in order", + ["Base", "System", "Team Instructions", "Core Memory", "Channel Canvas"], + "system-prompt standalone card must carry Base → System → Team Instructions → Core Memory → Channel Canvas in order", ); // (c) The system-prompt item must NOT be inside any turn group. @@ -1781,7 +1786,7 @@ test("buildTranscript four-section system prompt card is standalone with all sec const contextSectionTitles = (promptContextItem.sections ?? []).map( (s) => s.title, ); - // Must have Buzz event and Thread context sections, NOT Base/System/Core Memory/Channel Canvas. + // Must have Buzz event and Thread context sections, NOT Base/System/Team Instructions/Core Memory/Channel Canvas. assert.ok( contextSectionTitles.some((t) => t.toLowerCase().includes("buzz")), "prompt context must contain a Buzz event section", @@ -1791,9 +1796,10 @@ test("buildTranscript four-section system prompt card is standalone with all sec (t) => t === "Base" || t === "System" || + t === "Team Instructions" || t === "Core Memory" || t === "Channel Canvas", ), - "prompt context must NOT contain system-prompt sections (Base/System/Core Memory/Channel Canvas)", + "prompt context must NOT contain system-prompt sections (Base/System/Team Instructions/Core Memory/Channel Canvas)", ); }); diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs index 94fd1605a..f9bc089d6 100644 --- a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs +++ b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.test.mjs @@ -489,3 +489,195 @@ test("parseSystemPromptSections keeps an embedded canvas-like line literal when { title: "Channel Canvas", body: "this IS the appended canvas" }, ]); }); + +// ── Team Instructions extraction ─────────────────────────────────────────── + +test("parseSystemPromptSections extracts Team Instructions as its own section after System (Base+System+Team)", () => { + // compose_prompt() appends "\n\n---\n# Team Instructions\n{instructions}" to the persona body. + // The canonical delimiter must split System from Team Instructions. + const framed = [ + "[Base]", + "You are a helpful assistant.", + "", + "[System]", + "You are Agent X.", + "", + "---", + "# Team Instructions", + "Always respond in markdown.", + ].join("\n"); + const sections = parseSystemPromptSections(framed); + assert.deepEqual(sections, [ + { title: "Base", body: "You are a helpful assistant." }, + { title: "System", body: "You are Agent X." }, + { title: "Team Instructions", body: "Always respond in markdown." }, + ]); +}); + +test("parseSystemPromptSections extracts Team Instructions after System-only (no Base)", () => { + // When [Base] is absent the [System] header starts the input. + const framed = [ + "[System]", + "You are Agent Y.", + "", + "---", + "# Team Instructions", + "Keep responses concise.", + ].join("\n"); + const sections = parseSystemPromptSections(framed); + assert.deepEqual(sections, [ + { title: "System", body: "You are Agent Y." }, + { title: "Team Instructions", body: "Keep responses concise." }, + ]); +}); + +test("parseSystemPromptSections extracts Team Instructions with Core Memory and Channel Canvas (full 5-section shape)", () => { + // Full production-shaped system prompt: Base → System → Team Instructions → Core Memory → Channel Canvas. + // compose_prompt() produces the canonical delimiter; with_core() and with_canvas() append their frames. + const framed = [ + "[Base]", + "You are a helpful AI assistant running in Buzz.", + "", + "[System]", + "You are Observer Agent. You coordinate multi-agent workflows.", + "", + "---", + "# Team Instructions", + "Always tag on handoff.", + "Never expand scope without approval.", + "", + "[Agent Memory — core]", + "I am Observer Agent.", + "## Lessons Learned", + "Always tag on handoff.", + "", + "[Channel Canvas]", + "Canvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + "Last modified: 2026-07-11T10:00:00Z", + "Fetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301", + ].join("\n"); + const sections = parseSystemPromptSections(framed); + assert.deepEqual(sections, [ + { title: "Base", body: "You are a helpful AI assistant running in Buzz." }, + { + title: "System", + body: "You are Observer Agent. You coordinate multi-agent workflows.", + }, + { + title: "Team Instructions", + body: "Always tag on handoff.\nNever expand scope without approval.", + }, + { + title: "Core Memory", + body: "I am Observer Agent.\n## Lessons Learned\nAlways tag on handoff.", + }, + { + title: "Channel Canvas", + body: "Canvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\nLast modified: 2026-07-11T10:00:00Z\nFetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301", + }, + ]); +}); + +test("parseSystemPromptSections does NOT split on a bare '---' without the '# Team Instructions' heading", () => { + // A horizontal rule alone inside a persona body is kept literal. + const framed = [ + "[System]", + "Some text.", + "", + "---", + "", + "More text after a separator.", + ].join("\n"); + const sections = parseSystemPromptSections(framed); + assert.deepEqual(sections, [ + { + title: "System", + body: "Some text.\n\n---\n\nMore text after a separator.", + }, + ]); +}); + +test("parseSystemPromptSections does NOT split on '# Team Instructions' with only a single preceding newline", () => { + // The canonical delimiter requires \n\n---\n before the heading. + // A single-newline variant is kept literal inside System. + const framed = [ + "[System]", + "Persona preamble.", + "---", + "# Team Instructions", + "These look canonical but lack the double newline before ---.", + ].join("\n"); + const sections = parseSystemPromptSections(framed); + assert.deepEqual(sections, [ + { + title: "System", + body: "Persona preamble.\n---\n# Team Instructions\nThese look canonical but lack the double newline before ---.", + }, + ]); +}); + +test("parseSystemPromptSections does NOT split on '# Team Instructions' heading without the '---' separator", () => { + // Only the exact composed form \n\n---\n# Team Instructions\n triggers the split. + const framed = [ + "[System]", + "Persona text.", + "", + "# Team Instructions", + "This is just a heading in the persona body.", + ].join("\n"); + const sections = parseSystemPromptSections(framed); + assert.deepEqual(sections, [ + { + title: "System", + body: "Persona text.\n\n# Team Instructions\nThis is just a heading in the persona body.", + }, + ]); +}); + +test("parseSystemPromptSections non-team persona (no delimiter) is unaffected", () => { + // A standard Base+System prompt without any team instructions must produce + // exactly two sections — no Team Instructions row added. + const framed = [ + "[Base]", + "You are a helpful assistant.", + "", + "[System]", + "You are a coding assistant.", + ].join("\n"); + const sections = parseSystemPromptSections(framed); + assert.deepEqual(sections, [ + { title: "Base", body: "You are a helpful assistant." }, + { title: "System", body: "You are a coding assistant." }, + ]); +}); + +test("parseSystemPromptSections splits on the LAST occurrence of the canonical delimiter (embedded lookalike + real appended team suffix)", () => { + // A persona body may itself contain the exact delimiter string verbatim + // (e.g. an example or a quoted earlier instruction set). compose_prompt() + // always APPENDS the real team instructions, so the LAST occurrence is the + // authoritative producer boundary. The earlier embedded occurrence must stay + // inside the System body. + const framed = [ + "[System]", + "Here is an example of team framing:", + "", + "---", + "# Team Instructions", + "These are fake — embedded in the persona prose.", + "", + "---", + "# Team Instructions", + "These are real — appended by compose_prompt().", + ].join("\n"); + const sections = parseSystemPromptSections(framed); + assert.deepEqual(sections, [ + { + title: "System", + body: "Here is an example of team framing:\n\n---\n# Team Instructions\nThese are fake — embedded in the persona prose.", + }, + { + title: "Team Instructions", + body: "These are real — appended by compose_prompt().", + }, + ]); +}); diff --git a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts index 8d53e9820..eeb62c13b 100644 --- a/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts +++ b/desktop/src/features/agents/ui/agentSessionTranscriptHelpers.ts @@ -57,15 +57,19 @@ export function parsePromptText(text: string): { /** * Split the framed `session/new` `systemPrompt` into its `Base`/`System`/ - * `Core Memory`/`Channel Canvas` sub-sections deterministically. + * `Team Instructions`/`Core Memory`/`Channel Canvas` sub-sections + * deterministically. * * The harness composes the value in order: * `[Base]\n{base}\n\n[System]\n{persona}\n\n[Agent Memory — core]\n{core}\n\n[Channel Canvas]\n{canvas}` - * with any section omitted when absent. Extraction runs in reverse producer - * order so that each `lastIndexOf` search operates on the full input and - * each extraction boundary is unambiguous. + * with any section omitted when absent. For team-pack agents the persona body + * already contains the pack-level instructions appended by `compose_prompt()` + * in `buzz-persona/src/resolve.rs`: + * `{persona_body}\n\n---\n# Team Instructions\n{pack_instructions}` + * Extraction runs in reverse producer order so that each `lastIndexOf` search + * operates on the full input and each extraction boundary is unambiguous. * - * Three extraction passes before Base/System parsing: + * Four extraction passes before Base/System parsing: * * 1. **Canvas** (`[Channel Canvas]`): appended last by `with_canvas()`. * - Start-of-string: canvas-only input. @@ -79,6 +83,15 @@ export function parsePromptText(text: string): { * 3. **Base/System**: remainder after canvas and core extraction. * Split on the first `\n[System]\n` boundary; no embedded `[...]` line * inside a body can start a new section. + * + * 4. **Team Instructions**: if the `System` body contains the exact canonical + * delimiter `\n\n---\n# Team Instructions\n` (produced by `compose_prompt()`), + * the body is split at the **last** occurrence of that boundary (same + * last-occurrence guard as canvas and core). The text before becomes the + * `System` body; the text after becomes a `Team Instructions` section + * inserted immediately after `System`. Non-canonical lookalikes (bare `---` + * without the heading, a `# Team Instructions` on a different line, or only + * a single preceding newline) are kept literal inside `System`. */ export function parseSystemPromptSections( systemPrompt: string, @@ -121,11 +134,36 @@ export function parseSystemPromptSections( } // ── 3. Parse Base/System from the remaining prefix ──────────────────────── + // The canonical team-instructions delimiter produced by compose_prompt() in + // buzz-persona/src/resolve.rs: + // format!("{persona_prompt}\n\n---\n# Team Instructions\n{instructions}") + const TEAM_DELIMITER = "\n\n---\n# Team Instructions\n"; + + // splitSystemBody: split a raw [System] body string at the last occurrence + // of the canonical team delimiter, returning { systemBody, teamBody | null }. + // Using lastIndexOf mirrors the canvas/core last-occurrence guard: a persona + // author can embed an exact delimiter-like passage inside the persona body; + // only the final occurrence is the producer boundary appended by compose_prompt(). + function splitSystemBody(raw: string): { + systemBody: string; + teamBody: string | null; + } { + const at = raw.lastIndexOf(TEAM_DELIMITER); + if (at === -1) return { systemBody: raw.trim(), teamBody: null }; + return { + systemBody: raw.slice(0, at).trim(), + teamBody: raw.slice(at + TEAM_DELIMITER.length).trim() || null, + }; + } + const baseAndSystem = remainder; if (baseAndSystem) { if (baseAndSystem.startsWith("[System]\n")) { - const body = baseAndSystem.slice("[System]\n".length).trim(); - if (body) sections.push({ title: "System", body }); + const raw = baseAndSystem.slice("[System]\n".length); + const { systemBody, teamBody } = splitSystemBody(raw); + if (systemBody) sections.push({ title: "System", body: systemBody }); + if (teamBody) + sections.push({ title: "Team Instructions", body: teamBody }); } else { const marker = "\n[System]\n"; const at = baseAndSystem.indexOf(marker); @@ -134,8 +172,11 @@ export function parseSystemPromptSections( if (baseBody) sections.push({ title: "Base", body: baseBody }); if (at !== -1) { - const systemBody = baseAndSystem.slice(at + marker.length).trim(); + const raw = baseAndSystem.slice(at + marker.length); + const { systemBody, teamBody } = splitSystemBody(raw); if (systemBody) sections.push({ title: "System", body: systemBody }); + if (teamBody) + sections.push({ title: "Team Instructions", body: teamBody }); } } } diff --git a/desktop/tests/e2e/observer-feed-screenshots.spec.ts b/desktop/tests/e2e/observer-feed-screenshots.spec.ts index 4f06cd355..730594ffd 100644 --- a/desktop/tests/e2e/observer-feed-screenshots.spec.ts +++ b/desktop/tests/e2e/observer-feed-screenshots.spec.ts @@ -418,7 +418,7 @@ test.describe("observer feed screenshots", () => { method: "session/new", params: { systemPrompt: - "[Base]\nYou are a helpful AI assistant running in Buzz.\n\n[System]\nYou are Observer Agent. You coordinate multi-agent workflows in the #agents channel.\n\n[Agent Memory — core]\nI am Observer Agent.\n## Lessons Learned\nAlways tag on handoff.\n\n[Channel Canvas]\nCanvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\nLast modified: 2026-07-11T10:00:00Z\nFetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301", + "[Base]\nYou are a helpful AI assistant running in Buzz.\n\n[System]\nYou are Observer Agent. You coordinate multi-agent workflows in the #agents channel.\n\n---\n# Team Instructions\nAlways tag on handoff.\n\n[Agent Memory — core]\nI am Observer Agent.\n## Lessons Learned\nAlways tag on handoff.\n\n[Channel Canvas]\nCanvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\nLast modified: 2026-07-11T10:00:00Z\nFetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301", }, }, }, @@ -428,7 +428,7 @@ test.describe("observer feed screenshots", () => { timeout: 5_000, }); - // All four section headings must be present in the card — expand it first. + // All five section headings must be present in the card — expand it first. await feedPanel.getByTestId("transcript-metadata-item").evaluate((el) => { if (el.tagName === "DETAILS") (el as HTMLDetailsElement).open = true; for (const details of el.querySelectorAll("details")) { @@ -439,6 +439,9 @@ test.describe("observer feed screenshots", () => { await expect(feedPanel.getByText("System", { exact: true })).toBeVisible({ timeout: 5_000, }); + await expect(feedPanel.getByText("Team Instructions")).toBeVisible({ + timeout: 5_000, + }); await expect(feedPanel.getByText("Core Memory")).toBeVisible({ timeout: 5_000, }); @@ -664,12 +667,12 @@ test.describe("observer feed screenshots", () => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); - // Full realistic pool.rs first-turn wire sequence: + // Full realistic pool.rs first-turn wire sequence for a team-pack agent: // turn_started → session/new → session_resolved → session/prompt // Verifies the consolidated presentation: session/new.systemPrompt always // renders as a standalone top-level "System prompt" card (never injected into // the CheckCheck bundle). The CheckCheck dialog contains only per-turn context - // (Buzz event / Thread context) — no Base/System/Core Memory/Channel Canvas sections. + // (Buzz event / Thread context) — no Base/System/Team Instructions/Core Memory/Channel Canvas sections. await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, @@ -695,7 +698,7 @@ test.describe("observer feed screenshots", () => { method: "session/new", params: { systemPrompt: - "[Base]\nYou are a helpful AI assistant running in Buzz.\n\n[System]\nYou are Observer Agent. You coordinate multi-agent workflows in the #agents channel.\n\n[Agent Memory — core]\nI am Observer Agent.\n## Lessons Learned\nAlways tag on handoff.\n\n[Channel Canvas]\nCanvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\nLast modified: 2026-07-11T10:00:00Z\nFetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301", + "[Base]\nYou are a helpful AI assistant running in Buzz.\n\n[System]\nYou are Observer Agent. You coordinate multi-agent workflows in the #agents channel.\n\n---\n# Team Instructions\nAlways tag on handoff.\n\n[Agent Memory — core]\nI am Observer Agent.\n## Lessons Learned\nAlways tag on handoff.\n\n[Channel Canvas]\nCanvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\nLast modified: 2026-07-11T10:00:00Z\nFetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301", }, }, }, @@ -752,8 +755,8 @@ test.describe("observer feed screenshots", () => { timeout: 5_000, }); - // The standalone card shows "4 sections" collapsed — expand it to reveal - // the section headings, then assert all four are present. + // The standalone card shows "5 sections" collapsed — expand it to reveal + // the section headings, then assert all five are present. await feedPanel.getByTestId("transcript-metadata-item").evaluate((el) => { if (el.tagName === "DETAILS") (el as HTMLDetailsElement).open = true; for (const details of el.querySelectorAll("details")) { @@ -764,6 +767,9 @@ test.describe("observer feed screenshots", () => { await expect(feedPanel.getByText("System", { exact: true })).toBeVisible({ timeout: 5_000, }); + await expect(feedPanel.getByText("Team Instructions")).toBeVisible({ + timeout: 5_000, + }); await expect(feedPanel.getByText("Core Memory")).toBeVisible({ timeout: 5_000, }); @@ -776,7 +782,7 @@ test.describe("observer feed screenshots", () => { await expect(feedPanel.getByText("Prompt context")).toHaveCount(0); // Open the CheckCheck dialog: it contains ONLY per-turn context sections - // (Buzz event, Thread context). Base/System/Core Memory/Channel Canvas must NOT appear. + // (Buzz event, Thread context). Base/System/Team Instructions/Core Memory/Channel Canvas must NOT appear. await feedPanel.getByTestId("transcript-prompt-context-toggle").click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible({ timeout: 5_000 }); @@ -788,10 +794,16 @@ test.describe("observer feed screenshots", () => { expect(sectionTitles.length).toBe(2); expect(sectionTitles[0]).toContain("Buzz event"); expect(sectionTitles[1]).toContain("Thread context"); - // Collect all article heading text and assert none of the four + // Collect all article heading text and assert none of the five // system-prompt section labels appear — including exact "System" which // would be ambiguous via substring search on the full dialog text. - const forbidden = ["Base", "System", "Core Memory", "Channel Canvas"]; + const forbidden = [ + "Base", + "System", + "Team Instructions", + "Core Memory", + "Channel Canvas", + ]; for (const title of sectionTitles) { for (const label of forbidden) { expect(title).not.toContain(label);