From e3c62d6f69c6c05d299d8b4b39a1ce7cdda97fb5 Mon Sep 17 00:00:00 2001 From: Kostandin Angjellari Date: Tue, 15 Sep 2026 16:51:30 +0200 Subject: [PATCH 1/2] Remove the model-facing Brunch question marker Co-authored-by: Cursor --- .../src/agents/chat-agent/tool-catalogue.ts | 6 - .../runbook/schema-carrier-probe.ts | 7 +- .../admission-controls.integration.ts | 23 +--- .../integration/admission-controls.test.ts | 16 +-- .../history-retention.integration.ts | 29 +--- .../test/integration/petrinaut-chat-result.ts | 1 + .../integration/petrinaut-chat.integration.ts | 35 ++--- .../test/integration/petrinaut-chat.test.ts | 28 ++-- .../workpiece-revisions.integration.ts | 4 +- .../brunch-panel-transport.ts | 4 +- .../use-flue-chat-history.ts | 4 +- .../voice-interview/canonical-speech.test.ts | 14 ++ .../docs/reference/architecture/topology.md | 1 - .../brunch-agent/packages/core/src/flue.ts | 29 +--- .../brunch-agent/packages/core/src/index.ts | 3 + .../packages/core/src/prompts/SYSTEM.md | 2 - .../packages/core/src/question-marker.ts | 9 +- .../core/test/question-marker.test.ts | 41 ++---- .../core/test/update-workpiece.test.ts | 9 +- .../transport-aisdk/test/ui-stream.test.ts | 125 +++++++++--------- 20 files changed, 166 insertions(+), 224 deletions(-) diff --git a/apps/brunch-agent/src/agents/chat-agent/tool-catalogue.ts b/apps/brunch-agent/src/agents/chat-agent/tool-catalogue.ts index c50545a6eac..4bae474f583 100644 --- a/apps/brunch-agent/src/agents/chat-agent/tool-catalogue.ts +++ b/apps/brunch-agent/src/agents/chat-agent/tool-catalogue.ts @@ -47,12 +47,6 @@ export const ordinaryBrunchToolCatalogue: readonly OrdinaryBrunchToolCatalogueEn executionOwner: "flue", role: "substrate", }, - { - name: "brunch_mark_question", - definitionOwner: "brunch-core", - executionOwner: "brunch-app", - role: "workpiece", - }, { name: "mutate_workpiece", definitionOwner: "brunch-core", diff --git a/apps/brunch-agent/src/evaluations/runbook/schema-carrier-probe.ts b/apps/brunch-agent/src/evaluations/runbook/schema-carrier-probe.ts index 484681301fa..e4fb6960ba7 100644 --- a/apps/brunch-agent/src/evaluations/runbook/schema-carrier-probe.ts +++ b/apps/brunch-agent/src/evaluations/runbook/schema-carrier-probe.ts @@ -17,6 +17,7 @@ import { clientToolHistoryFrom, clientToolResultSignal, } from "@hashintel/brunch-agent-transport-aisdk"; +import { BRUNCH_QUESTION_TOOL_NAMES } from "@hashintel/brunch-agent/question-marker"; import { petrinautAiTools, type PetrinautAiToolInput, @@ -159,8 +160,10 @@ try { }); assert.deepEqual(generatedAddType.parameters, canonicalSchema); assert( - generatedTools.some((tool) => tool.name === "brunch_mark_question"), - "Question marker missing", + !generatedTools.some((tool) => + BRUNCH_QUESTION_TOOL_NAMES.some((name) => name === tool.name), + ), + "Legacy question marker must not be mounted", ); assert.deepEqual(headless.definition().types, [ petrinautAiTools.addType.inputSchema.parse(nestedType), diff --git a/apps/brunch-agent/test/integration/admission-controls.integration.ts b/apps/brunch-agent/test/integration/admission-controls.integration.ts index bcfb5e88734..e20c81ad2cc 100644 --- a/apps/brunch-agent/test/integration/admission-controls.integration.ts +++ b/apps/brunch-agent/test/integration/admission-controls.integration.ts @@ -25,7 +25,7 @@ import { VALIDATED_CONSTRUCTION_MODE, } from "@hashintel/brunch-agent-plugin-sdcpn/flue"; import { snapshotToUiMessages } from "@hashintel/brunch-agent-transport-aisdk"; -import { BRUNCH_QUESTION_TOOL_NAME } from "@hashintel/brunch-agent/question-marker"; +import { BRUNCH_QUESTION_TOOL_NAMES } from "@hashintel/brunch-agent/question-marker"; import { CLIENT_TOOL_RESULT_SIGNAL, @@ -70,7 +70,7 @@ const browserNames: ReadonlySet = new Set([ const project = (history: FlueConversationSnapshot) => snapshotToUiMessages(history, { clientToolNames: browserNames, - hiddenToolNames: new Set([BRUNCH_QUESTION_TOOL_NAME]), + hiddenToolNames: new Set(BRUNCH_QUESTION_TOOL_NAMES), }); const faux = fauxProvider({ provider: "anthropic", @@ -152,20 +152,10 @@ const run = async () => { const observations = []; try { for (const names of [ - [BRUNCH_QUESTION_TOOL_NAME, "addType"], - ["addType", BRUNCH_QUESTION_TOOL_NAME], ["mutate_workpiece", "addType"], ["addType", "mutate_workpiece"], - [BRUNCH_QUESTION_TOOL_NAME, "mutate_workpiece", "addType"], - [BRUNCH_QUESTION_TOOL_NAME, "addType", "mutate_workpiece"], - ["mutate_workpiece", BRUNCH_QUESTION_TOOL_NAME, "addType"], - ["mutate_workpiece", "addType", BRUNCH_QUESTION_TOOL_NAME], - ["addType", BRUNCH_QUESTION_TOOL_NAME, "mutate_workpiece"], - ["addType", "mutate_workpiece", BRUNCH_QUESTION_TOOL_NAME], ["addType", "unmounted_admission_probe"], ["addType"], - [BRUNCH_QUESTION_TOOL_NAME], - ["mutate_workpiece", BRUNCH_QUESTION_TOOL_NAME], ]) { caseId = names.join("-"); const client = clientFor(); @@ -310,12 +300,7 @@ const run = async () => { const message = fauxAssistantMessage( [ fauxText(text), - ...(abort - ? [makeCall("addType")] - : [ - makeCall("mutate_workpiece"), - makeCall(BRUNCH_QUESTION_TOOL_NAME), - ]), + ...(abort ? [makeCall("addType")] : [makeCall("mutate_workpiece")]), ], { stopReason: "toolUse" }, ); @@ -391,7 +376,7 @@ const run = async () => { }); } const rejected = observations.find( - (observation) => observation.caseId === "brunch_mark_question-addType", + (observation) => observation.caseId === "mutate_workpiece-addType", )!; const priorIds = new Set( rejected.seeded.messages.map((message) => message.id), diff --git a/apps/brunch-agent/test/integration/admission-controls.test.ts b/apps/brunch-agent/test/integration/admission-controls.test.ts index 1cd7be59968..7171af4337f 100644 --- a/apps/brunch-agent/test/integration/admission-controls.test.ts +++ b/apps/brunch-agent/test/integration/admission-controls.test.ts @@ -31,12 +31,12 @@ beforeAll(async () => { }); test("production rejects every mixed proposal before publishing or partially executing it", () => { - expect(result.observations).toHaveLength(14); + expect(result.observations).toHaveLength(4); const mixed = result.observations.filter( ({ generated }) => generated.length > 1 && generated.some((call) => call.name === "addType"), ); - expect(mixed).toHaveLength(11); + expect(mixed).toHaveLength(3); for (const observation of mixed) { expect(observation.pendingMutationIds).toEqual([]); expect(observation.after).toEqual(observation.before); @@ -138,7 +138,7 @@ test("every failed submission is attributable from the server output by stage, s } }); -test("production still settles revisions and noninteractive markers without browser results", () => { +test("production settles revisions without browser results", () => { for (const observation of result.observations) { expect(observation.seed.error).toBeNull(); const revision = observation.seeded.messages @@ -151,16 +151,6 @@ test("production still settles revisions and noninteractive markers without brow output: { revisionId: `${observation.caseId}-old-revision`, ordinal: 1 }, }); } - for (const caseId of [ - "brunch_mark_question", - "mutate_workpiece-brunch_mark_question", - ]) { - const observation = result.observations.find( - (entry) => entry.caseId === caseId, - )!; - expect(observation.attempt.error).toBeNull(); - expect(observation.providerCallsBeforeClientResult).toBe(2); - } }); test("an independently admitted browser mutation waits for its correlated result and does not reapply", () => { diff --git a/apps/brunch-agent/test/integration/history-retention.integration.ts b/apps/brunch-agent/test/integration/history-retention.integration.ts index cef953e8fcd..4514c165687 100644 --- a/apps/brunch-agent/test/integration/history-retention.integration.ts +++ b/apps/brunch-agent/test/integration/history-retention.integration.ts @@ -20,7 +20,7 @@ import { snapshotToUiMessages, CLIENT_TOOL_RESULT_SIGNAL, } from "@hashintel/brunch-agent-transport-aisdk"; -import { BRUNCH_QUESTION_TOOL_NAME } from "@hashintel/brunch-agent/question-marker"; +import { BRUNCH_QUESTION_TOOL_NAMES } from "@hashintel/brunch-agent/question-marker"; import { agentOwnershipHeaders, @@ -350,7 +350,7 @@ const tools = (name: string, input: Record, id: string) => const project = (snapshot: FlueConversationSnapshot) => snapshotToUiMessages(snapshot, { clientToolNames: new Set([READ_PETRINAUT_DOCS_TOOL_NAME]), - hiddenToolNames: new Set([BRUNCH_QUESTION_TOOL_NAME]), + hiddenToolNames: new Set(BRUNCH_QUESTION_TOOL_NAMES), }); const status = async (operation: () => Promise) => { try { @@ -431,11 +431,6 @@ try { ); responses.push( tools("ping", { note: "a4-early-ping" }, "a4-ping-early"), - tools( - BRUNCH_QUESTION_TOOL_NAME, - { question: "Which synthetic record follows?" }, - "a4-question", - ), tools( READ_PETRINAUT_DOCS_TOOL_NAME, { doc: "ai-assistant" }, @@ -515,13 +510,7 @@ try { .filter((part) => part.type === "dynamic-tool"); assert.deepEqual( publicTools.map((part) => part.toolCallId), - [ - "a4-ping-early", - "a4-question", - "a4-doc-early", - "a4-ping-middle", - "a4-doc-late", - ], + ["a4-ping-early", "a4-doc-early", "a4-ping-middle", "a4-doc-late"], ); for (const suffix of ["early", "middle"]) { const ping = publicTools.find( @@ -531,15 +520,11 @@ try { assert.deepEqual(ping.input, { note: `a4-${suffix}-ping` }); assert.deepEqual(ping.output, { ok: true, note: `a4-${suffix}-ping` }); } - const marker = publicTools.find( - (part) => part.toolCallId === "a4-question", - ); - assert(marker?.state === "output-available"); - assert.deepEqual(marker.output, { marked: true }); assert( - before.messages + !before.messages .flatMap((message) => message.parts) .some((part) => part.type === "data-brunch-question"), + "New responses must not create question markers", ); const clientResults = clientToolHistoryFrom(before.messages).results; assert.deepEqual( @@ -702,7 +687,7 @@ try { ? compactions.some( (event) => !event.isError && - event.messagesBefore === 20 && + event.messagesBefore === 18 && event.messagesAfter === 3, ) : compactions.some( @@ -710,7 +695,7 @@ try { !event.isError && event.messagesAfter < event.messagesBefore, ), silentOverflow - ? "Silent overflow must fold the known 20-message window to 3" + ? "Silent overflow must fold the known 18-message window to 3" : "Actual successful folding must reduce runtime context messages", ); assert( diff --git a/apps/brunch-agent/test/integration/petrinaut-chat-result.ts b/apps/brunch-agent/test/integration/petrinaut-chat-result.ts index b1a70cb413c..3dd04e44c41 100644 --- a/apps/brunch-agent/test/integration/petrinaut-chat-result.ts +++ b/apps/brunch-agent/test/integration/petrinaut-chat-result.ts @@ -21,6 +21,7 @@ export interface PetrinautChatResult { readonly resumedStatus: number; readonly resumedText: string; readonly resumedFinish: UIMessageChunk | undefined; + readonly questionResponseProviderCalls: number; readonly questionMarkerLive: unknown; readonly questionMarkerHistory: unknown; readonly questionToolVisibleLive: boolean; diff --git a/apps/brunch-agent/test/integration/petrinaut-chat.integration.ts b/apps/brunch-agent/test/integration/petrinaut-chat.integration.ts index f0db8e9b64b..c2f706756c4 100644 --- a/apps/brunch-agent/test/integration/petrinaut-chat.integration.ts +++ b/apps/brunch-agent/test/integration/petrinaut-chat.integration.ts @@ -10,6 +10,7 @@ import { fauxText, fauxThinking, fauxToolCall, + type Provider, } from "@earendil-works/pi-ai"; import { createFlueClient, FlueApiError } from "@flue/sdk"; @@ -21,7 +22,7 @@ import { import { ELICITATION_SKILL_NAME } from "@hashintel/brunch-agent/flue"; import { BRUNCH_QUESTION_DATA_NAME, - BRUNCH_QUESTION_TOOL_NAME, + BRUNCH_QUESTION_TOOL_NAMES, } from "@hashintel/brunch-agent/question-marker"; import { PING_TOOL_NAME } from "../../src/agents/chat-agent/tools/ping.ts"; @@ -113,13 +114,22 @@ const questionToolVisibleInHistory = ( ): boolean => messages .flatMap((message) => message.parts) - .some((part) => part.type === `tool-${BRUNCH_QUESTION_TOOL_NAME}`); + .some((part) => + BRUNCH_QUESTION_TOOL_NAMES.some((name) => part.type === `tool-${name}`), + ); const faux = fauxProvider({ provider: "anthropic", models: [{ id: CHAT_MODEL_ID, reasoning: true }], }); -installFauxProvider(faux.provider); +let providerCallCount = 0; +installFauxProvider({ + ...faux.provider, + streamSimple(model, context, options) { + providerCallCount += 1; + return faux.provider.streamSimple(model, context, options); + }, +} satisfies Provider); const application = await loadBuiltBrunchApplication(); try { @@ -134,14 +144,14 @@ try { const panelTransport = createFlueChatTransport({ client: historyClient, clientToolNames, - hiddenToolNames: new Set([BRUNCH_QUESTION_TOOL_NAME]), + hiddenToolNames: new Set(BRUNCH_QUESTION_TOOL_NAMES), }); const projectHistory = ( snapshot: Awaited>, ) => snapshotToUiMessages(snapshot, { clientToolNames, - hiddenToolNames: new Set([BRUNCH_QUESTION_TOOL_NAME]), + hiddenToolNames: new Set(BRUNCH_QUESTION_TOOL_NAMES), }); if (process.env.BRUNCH_RESUME_PHASE === "1") { @@ -237,16 +247,6 @@ try { ], { stopReason: "toolUse" }, ), - fauxAssistantMessage( - [ - fauxToolCall( - BRUNCH_QUESTION_TOOL_NAME, - { question }, - { id: "tool-question-1" }, - ), - ], - { stopReason: "toolUse" }, - ), fauxAssistantMessage([ fauxText( `The guide says the assistant can read its own documentation pages. ${question}`, @@ -343,6 +343,7 @@ try { ], }, ] as UIMessage[]; + const questionResponseCallStart = providerCallCount; const resumedChunks = await chunksFrom( await panelTransport.sendMessages({ trigger: "submit-message", @@ -451,12 +452,14 @@ try { .map((chunk) => chunk.delta) .join(""), resumedFinish: resumedChunks.at(-1), + questionResponseProviderCalls: + providerCallCount - questionResponseCallStart, questionMarkerLive: questionMarkerFromChunks(resumedChunks), questionMarkerHistory: questionMarkerFromHistory(historyMessages), questionToolVisibleLive: resumedChunks.some( (chunk) => chunk.type === "tool-input-available" && - chunk.toolName === BRUNCH_QUESTION_TOOL_NAME, + BRUNCH_QUESTION_TOOL_NAMES.some((name) => name === chunk.toolName), ), questionToolVisibleHistory: questionToolVisibleInHistory(historyMessages), historyUserEntryCount: userEntryIds.length, diff --git a/apps/brunch-agent/test/integration/petrinaut-chat.test.ts b/apps/brunch-agent/test/integration/petrinaut-chat.test.ts index c507aeef030..4ae71d981f7 100644 --- a/apps/brunch-agent/test/integration/petrinaut-chat.test.ts +++ b/apps/brunch-agent/test/integration/petrinaut-chat.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { expect, test } from "vitest"; import { READ_PETRINAUT_DOCS_TOOL_NAME } from "@hashintel/brunch-agent-plugin-sdcpn/flue"; +import { BRUNCH_QUESTION_TOOL_NAMES } from "@hashintel/brunch-agent/question-marker"; import { runNodeScript } from "./run-node-script"; @@ -74,15 +75,13 @@ test("the browser transport streams the mounted Flue agent through server and cl type: "finish", finishReason: "stop", }); - expect(result.questionMarkerLive).toEqual({ - question: "Which documentation page should we inspect next?", - toolCallId: "tool-question-1", - }); + expect(result.resumedText).toContain( + "Which documentation page should we inspect next?", + ); + expect(result.questionResponseProviderCalls).toBe(1); + expect(result.questionMarkerLive).toBeUndefined(); expect(result.questionToolVisibleLive).toBe(false); - expect(result.questionMarkerHistory).toEqual({ - question: "Which documentation page should we inspect next?", - toolCallId: "tool-question-1", - }); + expect(result.questionMarkerHistory).toBeUndefined(); expect(result.questionToolVisibleHistory).toBe(false); expect(result.historyUserEntryCount).toBe(1); expect(result.historyClientToolResultCount).toBe(1); @@ -123,7 +122,9 @@ test("the browser transport streams the mounted Flue agent through server and cl expect(result.interviewerToolNames).toContain( READ_PETRINAUT_DOCS_TOOL_NAME, ); - expect(result.interviewerToolNames).toContain("brunch_mark_question"); + expect(result.interviewerToolNames).not.toEqual( + expect.arrayContaining([...BRUNCH_QUESTION_TOOL_NAMES]), + ); expect(result.interviewerToolNames).not.toContain("brunch_ask"); expect(result.interviewerToolNames).not.toContain("sweep"); expect(result.interviewerToolNames).not.toContain("brunch_sweep"); @@ -168,10 +169,7 @@ test("the browser transport streams the mounted Flue agent through server and cl expect(resumeResult.historyUserText).toContain( "Run the FE-1435 transport probe.", ); - expect(resumeResult.questionMarkerHistory).toEqual({ - question: "Which documentation page should we inspect next?", - toolCallId: "tool-question-1", - }); + expect(resumeResult.questionMarkerHistory).toBeUndefined(); expect(resumeResult.questionToolVisibleHistory).toBe(false); expect(resumeResult.transcript).toContain("tool ping"); expect(resumeResult.transcript).toContain( @@ -179,7 +177,9 @@ test("the browser transport streams the mounted Flue agent through server and cl ); expect(resumeResult.transcript).toContain("tool activate_skill"); expect(resumeResult.transcript).toContain("tool read_skill_resource"); - expect(resumeResult.transcript).toContain("tool brunch_mark_question"); + for (const markerName of BRUNCH_QUESTION_TOOL_NAMES) { + expect(resumeResult.transcript).not.toContain(`tool ${markerName}`); + } } finally { await rm(dbDirectory, { recursive: true, force: true }); } diff --git a/apps/brunch-agent/test/integration/workpiece-revisions.integration.ts b/apps/brunch-agent/test/integration/workpiece-revisions.integration.ts index d0c454318f0..35d51f2f4d7 100644 --- a/apps/brunch-agent/test/integration/workpiece-revisions.integration.ts +++ b/apps/brunch-agent/test/integration/workpiece-revisions.integration.ts @@ -132,10 +132,8 @@ const probe = async () => { const mixed = []; for (const names of [ - ["brunch_mark_question", "addType"], ["mutate_workpiece", "addType"], - ["brunch_mark_question", "mutate_workpiece", "addType"], - ["addType", "mutate_workpiece", "brunch_mark_question"], + ["addType", "mutate_workpiece"], ]) { const caseId = names.join("-"); const typeInput = { diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-panel-transport.ts b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-panel-transport.ts index be6ffd06d48..d88974d81c4 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-panel-transport.ts +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/brunch-panel-transport.ts @@ -3,7 +3,7 @@ import { FlueChatAdmissionError, } from "@hashintel/brunch-agent-transport-aisdk"; import { SWEEP_TOOL_NAME } from "@hashintel/brunch-agent/client-tools"; -import { BRUNCH_QUESTION_TOOL_NAME } from "@hashintel/brunch-agent/question-marker"; +import { BRUNCH_QUESTION_TOOL_NAMES } from "@hashintel/brunch-agent/question-marker"; import { sweepOutputSchema } from "../brunch-sweep-output"; import { brunchClientToolNames } from "./brunch-client-tools"; @@ -373,7 +373,7 @@ export const createBrunchPanelTransport = ( ...(options?.mapClientToolInput === undefined ? {} : { mapClientToolInput: options.mapClientToolInput }), - hiddenToolNames: new Set([BRUNCH_QUESTION_TOOL_NAME]), + hiddenToolNames: new Set(BRUNCH_QUESTION_TOOL_NAMES), onAdmission: (event) => { tracker.recordAdmission(event); options?.onAdmission?.(event.admission); diff --git a/apps/petrinaut-website/src/main/app/local-storage-demo/use-flue-chat-history.ts b/apps/petrinaut-website/src/main/app/local-storage-demo/use-flue-chat-history.ts index cff00f5cc8d..abd58441b59 100644 --- a/apps/petrinaut-website/src/main/app/local-storage-demo/use-flue-chat-history.ts +++ b/apps/petrinaut-website/src/main/app/local-storage-demo/use-flue-chat-history.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { snapshotToUiMessages } from "@hashintel/brunch-agent-transport-aisdk"; -import { BRUNCH_QUESTION_TOOL_NAME } from "@hashintel/brunch-agent/question-marker"; +import { BRUNCH_QUESTION_TOOL_NAMES } from "@hashintel/brunch-agent/question-marker"; import { brunchClientToolNames } from "./brunch-client-tools"; @@ -46,7 +46,7 @@ const projectPetrinautMessages = ( dynamicClientToolNames, validatedClientToolNames, ...(mapClientToolInput === undefined ? {} : { mapClientToolInput }), - hiddenToolNames: new Set([BRUNCH_QUESTION_TOOL_NAME]), + hiddenToolNames: new Set(BRUNCH_QUESTION_TOOL_NAMES), }) as PetrinautAiMessage[]; export const useFlueChatHistory = ( diff --git a/apps/petrinaut-website/src/main/app/voice-interview/canonical-speech.test.ts b/apps/petrinaut-website/src/main/app/voice-interview/canonical-speech.test.ts index 50807c4ba97..4be82493ca6 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/canonical-speech.test.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/canonical-speech.test.ts @@ -164,6 +164,20 @@ describe("canonical speech selection", () => { }); }); + test("keeps an unmarked question in full-response speech without enabling repeat", () => { + const text = "The batch is ready. Which operator confirms the batch next?"; + const selection = selectCanonicalSpeech([ + { + id: "assistant-unmarked-question", + role: "assistant", + parts: [{ type: "text", text, state: "done" }], + }, + ]); + + expect(selection.segments.map((segment) => segment.text)).toEqual([text]); + expect(selection.questionSegment).toBeUndefined(); + }); + test.each([ { name: "missing exact finalized prose", diff --git a/libs/@hashintel/brunch-agent/docs/reference/architecture/topology.md b/libs/@hashintel/brunch-agent/docs/reference/architecture/topology.md index 6eae2ff12e0..6d1ed3de675 100644 --- a/libs/@hashintel/brunch-agent/docs/reference/architecture/topology.md +++ b/libs/@hashintel/brunch-agent/docs/reference/architecture/topology.md @@ -150,7 +150,6 @@ those remain with their definition owners. | `task` | Flue | Flue | substrate delegation | | `activate_skill` | Flue | Flue | skill activation | | `read_skill_resource` | Flue | Flue | skill resource read | -| `brunch_mark_question` | Brunch core | Brunch app | question relay metadata | | `mutate_workpiece` | Brunch core | Brunch app | durable full-revision write with recorded delta | | `read_petrinaut_net` | Petrinaut Core | Petrinaut website | current document read | | `read_petrinaut_docs` | Petrinaut Core | Petrinaut website | user-guide read | diff --git a/libs/@hashintel/brunch-agent/packages/core/src/flue.ts b/libs/@hashintel/brunch-agent/packages/core/src/flue.ts index 2e944556eeb..5457acabb61 100644 --- a/libs/@hashintel/brunch-agent/packages/core/src/flue.ts +++ b/libs/@hashintel/brunch-agent/packages/core/src/flue.ts @@ -2,7 +2,6 @@ import { type AgentDispatchRequest, type CompactionConfig, defineTool, - useDataWriter, useModel, usePersistentState, useSkill, @@ -12,13 +11,6 @@ import { import * as v from "valibot"; import systemPrompt from "./prompts/SYSTEM.md?raw"; -import { - BRUNCH_QUESTION_DATA_NAME, - BRUNCH_QUESTION_TOOL_NAME, - BrunchQuestionDataSchema, - BrunchQuestionInputSchema, - type BrunchQuestionData, -} from "./question-marker"; import { ELICITATION_SKILL_NAME, elicitationSkill, @@ -61,7 +53,7 @@ const _preparedWorkpieceDeliveryIsDispatchable = ( * Mount the contributions owned by Brunch core and return its system prompt. * * Core contributes the always-on universal prompt, one `elicitation` - * capability skill, the question marker, and durable workpiece revisions. + * capability skill and durable workpiece revisions. */ export function useBrunchAgent( model: string, @@ -73,10 +65,6 @@ export function useBrunchAgent( ): string { useModel(model, compaction === undefined ? undefined : { compaction }); useSkill(elicitationSkill); - const writeQuestion = useDataWriter(BRUNCH_QUESTION_DATA_NAME, { - schema: BrunchQuestionDataSchema, - }); - useTool(createBrunchQuestionMarkerTool(writeQuestion)); const [revision, setRevision] = usePersistentState( workpieceRevisionStateKey, null, @@ -92,21 +80,6 @@ export function useBrunchAgent( return systemPrompt.replace(/^\s+|\s+$/gu, ""); } -export const createBrunchQuestionMarkerTool = ( - writeQuestion: (question: BrunchQuestionData) => void, -) => - defineTool({ - name: BRUNCH_QUESTION_TOOL_NAME, - description: - "Mark the exact text of a direct question for accessible replay. Call this immediately before including that exact question in ordinary assistant prose. This marker does not ask or answer the question itself.", - input: BrunchQuestionInputSchema, - output: v.object({ marked: v.literal(true) }), - run({ data, toolCallId }) { - writeQuestion({ question: data.question, toolCallId }); - return { output: { marked: true as const } }; - }, - }); - /** Successful settlement carriage; optional Markdown admits retained pointer-only results. */ export const updateWorkpieceOutputSchema = v.object({ ...workpieceRevisionPointerSchema.entries, diff --git a/libs/@hashintel/brunch-agent/packages/core/src/index.ts b/libs/@hashintel/brunch-agent/packages/core/src/index.ts index 4cfd52cd8d9..cfbd5f04cc0 100644 --- a/libs/@hashintel/brunch-agent/packages/core/src/index.ts +++ b/libs/@hashintel/brunch-agent/packages/core/src/index.ts @@ -30,8 +30,11 @@ export { export { BRUNCH_QUESTION_DATA_NAME, BRUNCH_QUESTION_TOOL_NAME, + BRUNCH_QUESTION_TOOL_NAMES, BrunchQuestionDataSchema, BrunchQuestionInputSchema, + LEGACY_BRUNCH_QUESTION_TOOL_NAME, + LEGACY_QUESTION_REPLAY_TOOL_NAME, parseBrunchQuestionData, type BrunchQuestionData, } from "./question-marker"; diff --git a/libs/@hashintel/brunch-agent/packages/core/src/prompts/SYSTEM.md b/libs/@hashintel/brunch-agent/packages/core/src/prompts/SYSTEM.md index 1e973409b54..c0cd5b3e862 100644 --- a/libs/@hashintel/brunch-agent/packages/core/src/prompts/SYSTEM.md +++ b/libs/@hashintel/brunch-agent/packages/core/src/prompts/SYSTEM.md @@ -10,8 +10,6 @@ Establish what the result must help the person decide, answer, compare, explain, Use the person's vocabulary and follow their active account rather than traversing a schema, template, or target representation. For practice-based accounts, prefer concrete remembered cases. Do not open with a battery of independent questions; deepen one answerable thread at a time and group questions only when they share one frame. -Before asking the person a direct question, call `brunch_mark_question` with the exact question text. Then include the exact same question text in ordinary assistant prose. The marker only makes that text available for accessible replay; it does not wait for or accept the answer, so continue the same response normally after calling it. Do not mark headings, rhetorical questions, or prose that you will not present verbatim. - Activate `elicitation` when progress requires source-side knowledge that cannot be responsibly inferred from the available account, including substantive interviewing, consequential corrections, or consulting a source. In a non-interactive conversation, use the supplied account as the complete input: report a blocking gap and the smallest question a later interactive conversation must answer, without asking it or inventing an answer. ## Authorship and uncertainty diff --git a/libs/@hashintel/brunch-agent/packages/core/src/question-marker.ts b/libs/@hashintel/brunch-agent/packages/core/src/question-marker.ts index ba6194c63f5..0f07838d2a7 100644 --- a/libs/@hashintel/brunch-agent/packages/core/src/question-marker.ts +++ b/libs/@hashintel/brunch-agent/packages/core/src/question-marker.ts @@ -1,6 +1,13 @@ import * as v from "valibot"; -export const BRUNCH_QUESTION_TOOL_NAME = "brunch_mark_question"; +export const LEGACY_BRUNCH_QUESTION_TOOL_NAME = "brunch_mark_question"; +export const LEGACY_QUESTION_REPLAY_TOOL_NAME = "mark_question_for_replay"; +/** @deprecated Retained only for source compatibility with historical projections. */ +export const BRUNCH_QUESTION_TOOL_NAME = LEGACY_BRUNCH_QUESTION_TOOL_NAME; +export const BRUNCH_QUESTION_TOOL_NAMES = [ + LEGACY_BRUNCH_QUESTION_TOOL_NAME, + LEGACY_QUESTION_REPLAY_TOOL_NAME, +] as const; export const BRUNCH_QUESTION_DATA_NAME = "brunch-question"; const NonBlankStringSchema = v.pipe( diff --git a/libs/@hashintel/brunch-agent/packages/core/test/question-marker.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/question-marker.test.ts index 42e0f59d22b..1102a668f43 100644 --- a/libs/@hashintel/brunch-agent/packages/core/test/question-marker.test.ts +++ b/libs/@hashintel/brunch-agent/packages/core/test/question-marker.test.ts @@ -1,21 +1,26 @@ import * as v from "valibot"; -import { describe, expect, test, vi } from "vitest"; +import { describe, expect, test } from "vitest"; -import { createBrunchQuestionMarkerTool } from "../src/flue"; import { BRUNCH_QUESTION_DATA_NAME, BRUNCH_QUESTION_TOOL_NAME, + BRUNCH_QUESTION_TOOL_NAMES, BrunchQuestionDataSchema, BrunchQuestionInputSchema, + LEGACY_BRUNCH_QUESTION_TOOL_NAME, + LEGACY_QUESTION_REPLAY_TOOL_NAME, parseBrunchQuestionData, - type BrunchQuestionData, } from "../src/question-marker"; -import type { FlueLogger } from "@flue/runtime"; - -describe("the Brunch question marker", () => { - test("defines one non-interactive tool and data-part identity", () => { +describe("legacy Brunch question markers", () => { + test("preserves historical tool and data identities for projection compatibility", () => { expect(BRUNCH_QUESTION_TOOL_NAME).toBe("brunch_mark_question"); + expect(LEGACY_BRUNCH_QUESTION_TOOL_NAME).toBe("brunch_mark_question"); + expect(LEGACY_QUESTION_REPLAY_TOOL_NAME).toBe("mark_question_for_replay"); + expect(BRUNCH_QUESTION_TOOL_NAMES).toEqual([ + "brunch_mark_question", + "mark_question_for_replay", + ]); expect(BRUNCH_QUESTION_DATA_NAME).toBe("brunch-question"); }); @@ -35,28 +40,6 @@ describe("the Brunch question marker", () => { ).toEqual({ question, toolCallId: "tool-question-1" }); }); - test("writes the exact marker without terminating or waiting for an answer", async () => { - const writeQuestion = vi.fn<(question: BrunchQuestionData) => void>(); - const tool = createBrunchQuestionMarkerTool(writeQuestion); - - const result = await tool.run({ - data: { question: "Which line should run this order?" }, - log: { - error: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - }, - toolCallId: "tool-question-1", - }); - - expect(writeQuestion).toHaveBeenCalledOnce(); - expect(writeQuestion).toHaveBeenCalledWith({ - question: "Which line should run this order?", - toolCallId: "tool-question-1", - }); - expect(result).toEqual({ output: { marked: true } }); - }); - test.each([ { question: "" }, { question: " " }, diff --git a/libs/@hashintel/brunch-agent/packages/core/test/update-workpiece.test.ts b/libs/@hashintel/brunch-agent/packages/core/test/update-workpiece.test.ts index b4ef9364eb2..a1226cc0388 100644 --- a/libs/@hashintel/brunch-agent/packages/core/test/update-workpiece.test.ts +++ b/libs/@hashintel/brunch-agent/packages/core/test/update-workpiece.test.ts @@ -13,6 +13,7 @@ import { elicitationSkill, workpieceMarkdownByteCeiling, } from "../src/flue"; +import { BRUNCH_QUESTION_TOOL_NAMES } from "../src/question-marker"; import { deriveWorkpieceMutation } from "../src/update-workpiece"; import { workpieceRevisionStateKey, @@ -176,9 +177,11 @@ test("captures the persistent-state setter at render and writes from run", async const mounted = vi .mocked(useTool) .mock.calls.map(([definition]) => definition); - expect(mounted.map((definition) => definition.name)).toContain( - "brunch_mark_question", - ); + const mountedNames = mounted.map((definition) => definition.name); + for (const markerName of BRUNCH_QUESTION_TOOL_NAMES) { + expect(mountedNames).not.toContain(markerName); + expect(prompt).not.toContain(markerName); + } const revisionTool = mounted.find( (definition) => definition.name === MUTATE_WORKPIECE_TOOL_NAME, ); diff --git a/libs/@hashintel/brunch-agent/packages/transport-aisdk/test/ui-stream.test.ts b/libs/@hashintel/brunch-agent/packages/transport-aisdk/test/ui-stream.test.ts index df2f8166f21..87d12871f81 100644 --- a/libs/@hashintel/brunch-agent/packages/transport-aisdk/test/ui-stream.test.ts +++ b/libs/@hashintel/brunch-agent/packages/transport-aisdk/test/ui-stream.test.ts @@ -127,71 +127,74 @@ test("projects data and metadata onto the AI SDK stream", () => { }); }); -test("hides an implementation tool while preserving its data marker", () => { - const written = project( - [ - { - type: "message-started", - conversationId: "conversation-1", - messageId: "message-1", - submissionId: "submission-1", - turnId: "turn-1", - position: position(0), - }, - { - type: "tool-input", - conversationId: "conversation-1", - messageId: "message-1", - toolCallId: "tool-question-1", - toolName: "brunch_mark_question", - input: { question: "Which line should run this order?" }, - position: position(1), - }, - { - type: "data-part", - conversationId: "conversation-1", - messageId: "message-1", - name: "brunch-question", - data: { - question: "Which line should run this order?", +test.each(["brunch_mark_question", "mark_question_for_replay"])( + "hides historical implementation tool $markerToolName while preserving its data marker", + (markerToolName) => { + const written = project( + [ + { + type: "message-started", + conversationId: "conversation-1", + messageId: "message-1", + submissionId: "submission-1", + turnId: "turn-1", + position: position(0), + }, + { + type: "tool-input", + conversationId: "conversation-1", + messageId: "message-1", toolCallId: "tool-question-1", + toolName: markerToolName, + input: { question: "Which line should run this order?" }, + position: position(1), }, - position: position(2), - }, - { - type: "tool-output", - conversationId: "conversation-1", + { + type: "data-part", + conversationId: "conversation-1", + messageId: "message-1", + name: "brunch-question", + data: { + question: "Which line should run this order?", + toolCallId: "tool-question-1", + }, + position: position(2), + }, + { + type: "tool-output", + conversationId: "conversation-1", + toolCallId: "tool-question-1", + output: { marked: true }, + position: position(3), + }, + { + type: "submission-settled", + conversationId: "conversation-1", + submissionId: "submission-1", + outcome: "completed", + position: position(4), + }, + ], + new Set([markerToolName]), + ); + + expect(written).toContainEqual({ + type: "data-brunch-question", + data: { + question: "Which line should run this order?", toolCallId: "tool-question-1", - output: { marked: true }, - position: position(3), }, - { - type: "submission-settled", - conversationId: "conversation-1", - submissionId: "submission-1", - outcome: "completed", - position: position(4), - }, - ], - new Set(["brunch_mark_question"]), - ); - - expect(written).toContainEqual({ - type: "data-brunch-question", - data: { - question: "Which line should run this order?", - toolCallId: "tool-question-1", - }, - }); - expect( - written.some( - (chunk) => - chunk.type === "tool-input-available" || - chunk.type === "tool-output-available" || - chunk.type === "tool-output-error", - ), - ).toBe(false); -}); + }); + expect( + written.some( + (chunk) => + chunk.type === "tool-input-available" || + chunk.type === "tool-output-available" || + chunk.type === "tool-output-error", + ), + ).toBe(false); + }, +); test("ignores observation catch-up chunks in a submission stream", () => { const written = project([ From 8173c53b2807c685dfd769a255b3bfd712254179 Mon Sep 17 00:00:00 2001 From: Kostandin Angjellari Date: Tue, 15 Sep 2026 16:59:55 +0200 Subject: [PATCH 2/2] Update buffered Voice marker expectation Co-authored-by: Cursor --- .../voice-interview/buffered-admission.integration.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/petrinaut-website/src/main/app/voice-interview/buffered-admission.integration.test.ts b/apps/petrinaut-website/src/main/app/voice-interview/buffered-admission.integration.test.ts index adc06eb7ec9..ed7630cb60f 100644 --- a/apps/petrinaut-website/src/main/app/voice-interview/buffered-admission.integration.test.ts +++ b/apps/petrinaut-website/src/main/app/voice-interview/buffered-admission.integration.test.ts @@ -52,7 +52,7 @@ const voice = () => { return { bridge, speakCanonical }; }; -test("buffered production output remains silent until approved; marker and ordinary prose survive without speaking tool payloads", () => { +test("buffered production output remains silent until approved; ordinary prose survives without a repeatable question or spoken tool payloads", () => { const sample = result.buffering.find( ({ caseId }) => caseId === "buffered-valid", )!; @@ -68,7 +68,7 @@ test("buffered production output remains silent until approved; marker and ordin ]); expect(speakCanonical).not.toHaveBeenCalled(); const completed = speechFrom(sample.projectedAfter); - expect(completed.questionSegment?.text).toBe(result.question); + expect(completed.questionSegment).toBeUndefined(); expect(completed.segments.map((segment) => segment.text)).toEqual([ sample.text, "Timing remains unknown.",