Skip to content

FE-1570: Optimize spoken Brunch responses for Voice mode - #9496

Open
kostandinang wants to merge 15 commits into
mainfrom
kostandin/fe-1570-voice-optimized-brunch-responses
Open

FE-1570: Optimize spoken Brunch responses for Voice mode#9496
kostandinang wants to merge 15 commits into
mainfrom
kostandin/fe-1570-voice-optimized-brunch-responses

Conversation

@kostandinang

Copy link
Copy Markdown
Contributor

🌟 What is the purpose of this PR?

Improve spoken response quality for the Voice mode introduced by H-6763 without weakening Brunch's authority. Petrinaut continues to render the complete canonical Brunch response, while OpenAI Realtime prepares only shorter explanatory context for disposable audio delivery. Application code then appends every protected brunch_ask question exactly.

This PR is stacked on #9467 so it can reuse the unified Voice session, transcript, and lifecycle contracts without duplicating them.

🔗 Related links

  • FE-1570 (internal)
  • H-6763 (internal) — parent Voice-mode work
  • #9467 — parent unified Voice-mode PR
  • #9399 — merged GPT-Realtime-2 transport foundation

🚫 Blocked by

🔍 What does this change?

  • Splits canonical Brunch output into finalized explanatory context, an exact protected question, and the complete canonical fallback.
  • Uses an out-of-band, text-only Realtime response with conversation: "none", tool_choice: "none", and no tools to prepare context.
  • Reserves a strict 50-word spoken budget and appends the exact canonical question and choices in application code.
  • Validates prepared output, limits preparation to two seconds, and falls back to complete canonical speech after timeout, provider failure, interruption, or invalid output.
  • Caches validated preparation only for the active connection epoch and discards stale completions on stop or reconnect.
  • Serializes preparation and audio work with the existing Realtime response queue while preserving pause, barge-in, mute, reconnect, typed handoff, provenance, and exactly-once answer submission.
  • Adds Repeat question and Read full response Voice-dock actions that bypass preparation and replay canonical text.
  • Correlates replay availability with both terminal response and output-buffer completion so overlapping or stale responses cannot re-enable unsafe replay.
  • Keeps canonical/prepared text, audio, provider transcripts, tool payloads, IDs, diagnostics, and disposable Realtime history out of persistent history and logs.

Boundaries

  • Brunch remains the control plane and sole authority for questions, captures, interview state, completion, and durable history.
  • No Brunch Mission 3 runtime, mission, endpoint, capture/session, or tool-result behavior changes are included.
  • Generated preparation is experimental delivery text, never canonical evidence.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s)

📜 Does this require a change to the docs?

The changes in this PR:

  • require changes to docs which are made as part of this PR

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • Voice is a non-production preview and is disabled by server policy in production.
  • Realtime context preparation is experimental because local validation cannot prove semantic equivalence; canonical fallback is therefore mandatory.
  • Physical microphone quality and interruption behavior still need validation in a preview deployment.

🐾 Next steps

  • Retarget this PR to main after H-6763: Integrate Voice mode into the AI assistant #9467 merges.
  • Validate prepared speech, exact questions, barge-in, pause/resume, reconnect, mute, and canonical replay on a physical device in preview.
  • After Brunch Mission 3 lands, write a separate plan for durable brunch_ask human-answer admission against the landed architecture.

🛡 What tests cover this?

  • yarn workspace @apps/petrinaut-website test:unit — 31 files / 237 tests passed.
  • yarn workspace @hashintel/petrinaut test:unit --run — 46 files / 405 tests passed.
  • yarn workspace @local/petrinaut-arch-docs lint:arch-docs — 57 layers / 259 edges / 579 files passed.
  • Website and Petrinaut builds, TypeScript checks, ESLint, repository formatting, and git diff --check passed.
  • Focused coverage includes canonical selection, preparation validation/timeout/fallback, protected-question assembly, stale epochs, function-call and response correlation, barge-in, pause/reconnect, replay safety, provenance, mute state, and exactly-once submissions.

❓ How to test this?

  1. Check out this branch after configuring a real VITE_BRUNCH_CHAT_ENDPOINT, PETRINAUT_OPENAI_VOICE_ENABLED=true, and dedicated OPENAI_VOICE_API_KEY in apps/petrinaut-website/.env.local.
  2. Run yarn workspace @apps/petrinaut-website dev and open the local URL.
  3. Start Voice mode and progress through a Brunch response containing explanatory context followed by a question with choices.
  4. Confirm the transcript retains the complete canonical response while speech uses concise context followed by the exact question and choices.
  5. Use Repeat question and Read full response; confirm both replay canonical content rather than prepared wording.
  6. Exercise barge-in, pause/resume, reconnect, and mute around preparation and playback; confirm speech does not duplicate and stale work does not resume.
  7. End Voice mode and confirm the canonical transcript and accepted answer remain unchanged.

📹 Demo

A short physical-device preview recording should be added before marking this PR ready for review because the primary behavior is audio-dependent.

@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
petrinaut Ready Ready Preview Sep 2, 2026 8:06pm UTC
petrinaut-docs Ready Ready Preview Sep 2, 2026 8:06pm UTC
2 Skipped Deployments
Project Deployment Actions Updated
hash Ignored Ignored Preview Sep 2, 2026 8:06pm UTC
hashdotdesign-tokens Ignored Ignored Preview Sep 2, 2026 8:06pm UTC

Request Review

kostandinang and others added 8 commits September 2, 2026 17:19
Keep child-only replay fixtures aligned with the mute state contract now owned by the parent PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
Separate speech cancellation from bridge lifecycle invalidation so prepared turns still close their tool call without starting unwanted audio.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Sep 2, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes Realtime orchestration, speech timing, and interview UX around canonical vs prepared audio; fallbacks and replay gating reduce but do not eliminate risk of wrong or duplicated spoken content.

Overview
Voice mode now uses an Approach D pipeline: after Brunch finishes a turn, OpenAI Realtime may run a text-only, out-of-band step to condense finalized assistant context within a 50-word budget (reserving words for the protected brunch_ask question), then plays verbatim tool-disabled audio with the exact question appended in application code. Invalid, slow (>2s), interrupted, or failed preparation falls back to full canonical context plus question; Brunch transcript text stays authoritative and unchanged.

selectInterviewSpeech separates context segments, the pending question, and full-response segments for automatic delivery. The Realtime session queues preparation and speech together, validates prepared text, caches successful prep per connection epoch, and supports speakPrepared / optional speakResponse: false on function-call completion when speech is cancelled.

The Voice dock gains Voice playback options (Repeat question, Read full response) wired through Petrinaut session state (canRepeatQuestion, canReadFullResponse) so replay uses speakCanonical and skips preparation. Barge-in, pause, and disconnect invalidate pending preparation; replay stays disabled until the active response is safely terminal.

Docs and changesets describe the dock replacing the composer during voice, typed send requiring End voice mode first, and expanded voice diagnostics for the preparation operation.

Reviewed by Cursor Bugbot for commit c7fe8a2. Bugbot is set up for automated code reviews on this repo. Configure here.

Skip automatic speech for answered ask turns and keep replay actions disabled while a barge-in turn remains unresolved.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 943dfc2. Configure here.

Treat a completed transcript as the end of the input guard so replay actions recover when no answer submission follows.

Co-authored-by: Cursor <cursoragent@cursor.com>
@lunelson

lunelson commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

The Brunch-side premise has changed enough since this work was designed that we now intend to validate a different Voice integration in FE-1574. This is an architectural coordination note, not a claim that the careful correlation, fallback, cancellation, and replay work in this PR is incorrect.

🏗️ Agent notes

What changed

Mission 4 replaced the older Brunch agent shape with the package-composed useBrunchAgent() + useSdcpnPlugin() agent and showed focused conversational behavior on its successful interactive probes. That does not prove every canonical reply is already suitable for speech, but it removes the premise that a secondary semantic rewrite is necessarily required; we should measure the current agent before making that rewrite a permanent layer.

We also no longer expect the existing Petrinaut AI assistant or its AI SDK useChat request/response format to be the authority for Voice. Flue already exposes the native conversation operations Voice needs: durable send, submission-correlated read, resumable/live observe, canonical history, and abort. The intended first experiment is therefore:

finalized STT
→ FlueClient.send to the owning Brunch conversation
→ canonical Flue response via observe/read
→ visible Voice text + ordinary TTS

A remotely exposed browser path may still need a thin authenticated same-origin proxy, but it should preserve Flue semantics rather than translate Voice through AI SDK UI messages.

Why this matters for this PR

The preparation path here is defensive: it excludes tools/reasoning, protects the exact brunch_ask question, times out, falls back canonically, and keeps generated speech ephemeral. Those are good properties. Even so, prepareInterviewSpeech() adds another semantic actor, up to two seconds before delivery, more cancellation/race state, and possible divergence between what Brunch canonically said and what the user heard. The 50-word result cannot be proven semantically equivalent by the structural validation in this branch.

Mission 5 will initially remove that variable and test exact canonical speech. Its visible proof target is:

  • one finalized spoken answer appears exactly once in canonical Flue history;
  • the corresponding canonical Brunch response reaches visible Voice text and TTS without the AI SDK chat composer or a secondary generation call;
  • local playback cancellation and durable Flue abort remain distinct and visible; and
  • reopening the conversation reconstructs the settled turn without duplicate submission or playback.

The draft contract is here.

What we should preserve

The reusable work in this stack—exactly-once finalized answer submission, turn correlation, interruption/barge-in behavior, stale-work rejection, playback controls, answer provenance, and explicit canonical fallback—should inform or be ported into that experiment. For now, please treat the AI SDK/Petrinaut-assistant dependency and secondary 50-word preparation as experimental choices rather than the integration seam we intend to stabilize.

If canonical speech proves unusably long, the next least-lossy options are deterministic question-focused presentation or Brunch-owned spoken-mode output. We should retain a secondary generative preparation step only if the direct experiment demonstrates that those are insufficient and the latency/semantic trade-off is accepted explicitly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash.design Affects the `hash.design` design site (app) area/apps area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants