Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3022dc6
feat(voice): add realtime master emissary mode
johnmatthewtennant Aug 30, 2026
59b5657
fix(voice): recover master follow-up delivery
johnmatthewtennant Aug 30, 2026
9e99d59
fix(voice): restore completed master transcripts
johnmatthewtennant Aug 30, 2026
477ffb3
fix(voice): hide queued emissary coordination
johnmatthewtennant Aug 31, 2026
54c7083
fix(voice): restore realtime bubbles on replay
johnmatthewtennant Aug 31, 2026
c1f86cf
fix(voice): avoid duplicate master turn text
johnmatthewtennant Aug 31, 2026
00b8780
feat(voice): expose realtime session controls
johnmatthewtennant Aug 31, 2026
edbe611
feat(voice): select chained OpenAI models
johnmatthewtennant Aug 31, 2026
5bd4d19
fix(voice): label default settings options
johnmatthewtennant Aug 31, 2026
90d5bd2
fix(voice): recover malformed realtime tool calls
johnmatthewtennant Sep 1, 2026
17a7d1e
fix(voice): coalesce realtime transcript items
johnmatthewtennant Sep 1, 2026
52b0d58
fix(voice): wake master on emissary speech
johnmatthewtennant Sep 1, 2026
1968524
feat(voice): add explicit master delivery modes
johnmatthewtennant Sep 1, 2026
f50d38e
chore(hooks): avoid duplicate local checks
johnmatthewtennant Sep 1, 2026
eaf32ae
feat(voice): track emissary handoffs
johnmatthewtennant Sep 1, 2026
aaf4b18
fix(voice): share handoff dismissals as context
johnmatthewtennant Sep 1, 2026
7bf5601
fix(voice): order master bridge events
johnmatthewtennant Sep 1, 2026
bb10f13
fix(voice): keep accepted handoffs silent
johnmatthewtennant Sep 2, 2026
81555ee
fix(voice): simplify emissary handoffs
johnmatthewtennant Sep 2, 2026
50da8ce
feat(voice): add realtime presentation modes
johnmatthewtennant Sep 2, 2026
1e93018
refactor(voice): share master emissary prompt
johnmatthewtennant Sep 2, 2026
68da538
fix(chat): hide transient voice no-op text
johnmatthewtennant Sep 2, 2026
b9bf616
feat(voice): adopt expert spokesperson flow
johnmatthewtennant Sep 2, 2026
986e013
fix(voice): preserve realtime call lifecycle
johnmatthewtennant Sep 2, 2026
4b36aec
Revert "chore(hooks): avoid duplicate local checks"
johnmatthewtennant Sep 3, 2026
ac04934
Revert "feat(voice): select chained OpenAI models"
johnmatthewtennant Sep 3, 2026
f97cb66
refactor(voice): remove superseded realtime paths
johnmatthewtennant Sep 3, 2026
29beb92
fix(voice): serialize realtime lifecycle and delivery
johnmatthewtennant Sep 3, 2026
262688b
refactor(voice): trim unused bridge payloads
johnmatthewtennant Sep 3, 2026
081bca3
docs(voice): align realtime eval contract
johnmatthewtennant Sep 3, 2026
c280c52
fix(voice): close realtime review gaps
johnmatthewtennant Sep 3, 2026
4c9d0a2
fix(voice): route realtime completion across renderers
johnmatthewtennant Sep 3, 2026
4454aa2
fix(voice): normalize realtime settings and contract
johnmatthewtennant Sep 3, 2026
7719df8
fix(voice): harden realtime lifecycle
johnmatthewtennant Sep 3, 2026
a497f74
fix(voice): preserve realtime shutdown state
johnmatthewtennant Sep 3, 2026
d98f997
fix(voice): close final realtime review gaps
johnmatthewtennant Sep 3, 2026
06301b9
fix(voice): close realtime startup races
johnmatthewtennant Sep 3, 2026
96be7a0
fix(voice): preserve final transcript after hangup
johnmatthewtennant Sep 3, 2026
e2625dc
fix(voice): isolate realtime shared paths
johnmatthewtennant Sep 3, 2026
09f6b1b
fix(voice): close realtime isolation edge cases
johnmatthewtennant Sep 3, 2026
9fd6643
fix(voice): recover exhausted hidden coordination
johnmatthewtennant Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions docs/app-e2e.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,47 @@ Windows uses the same runtime contract through `scripts/windows/Dev-Windows.ps1`
Set `BERD_E2E_MODE=1`, `BERD_E2E_RUN_ROOT`, and the optional provider bootstrap
environment above before invoking it; the app owns its random driver port and
publishes readiness under the run root.

## Live Realtime Expert–Spokesperson evaluation

`tests/app-e2e/realtime-expert-spokesperson.eval.test.ts` is an opt-in live
evaluation driven by typed chat messages. It starts a fresh Realtime voice
conversation, mutes its microphone so ambient audio cannot affect the run, asks
how many repositories are in the user's Development folder, then asks whether
any are symbolic links. It verifies that each typed question is followed in
order by visible Expert-to-Spokesperson coordination and a visible terminal Expert
turn. Each turn may contain one finalized Spokesperson answer, or an
acknowledgement and waiting update before the answer. More than three finalized
utterances fails the evaluation as a likely coordination loop.

The app-test-driver protocol serves one command per TCP connection, so
the client opens a fresh authenticated connection for every command. Home
navigation and promotion of its composer draft may temporarily replace the app
webview; this eval waits for the expected destination after those two known
boundaries without replaying the navigation or call-button click. Mutating
actions remain single-shot.

This scenario intentionally uses the normal local dev profile, not isolated
E2E mode: Realtime needs the Berd-owned API key stored from Voice settings, and
the Expert needs the normal configured agent/tool environment for inspecting the
real Development folder. Before running it, select **OpenAI Realtime** as the
Voice mode and save the Realtime API key in Berd.

Start the instrumented app in one terminal:

```bash
APP_TEST_DRIVER_TOKEN=local-realtime-eval just dev-e2e
```

Then run only the live scenario in another terminal:

```bash
APP_TEST_DRIVER_TOKEN=local-realtime-eval \
BERD_E2E_REALTIME_EVAL=1 \
pnpm exec vitest run --config vitest.app-e2e.config.ts \
tests/app-e2e/realtime-expert-spokesperson.eval.test.ts
```

Without `BERD_E2E_REALTIME_EVAL=1`, the live scenario is skipped so the normal
app E2E suite never makes network/model calls or depends on a developer's local
filesystem and credentials.
8 changes: 2 additions & 6 deletions scripts/block-feature-gates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ done
[[ "${VITE_MANAGED_CONNECTIONS:-0}" == "1" ]] && features+=(block-managed-connections)
[[ "${VITE_SKILL_DISCOVERY:-0}" == "1" ]] && features+=(block-skill-discovery)
[[ "${VITE_TELEMETRY_ENFORCED:-0}" == "1" ]] && features+=(block-telemetry-enforced)
if [[ "${VITE_VOICE_DICTATION:-0}" == "1" ]]; then
features+=(block-voice-dictation)
else
features+=(no-voice-dictation)
fi
[[ "${VITE_VOICE_DICTATION:-0}" == "1" ]] && features+=(block-voice-dictation)

(IFS=,; echo "${features[*]}")
(IFS=,; echo "${features[*]:-}")
6 changes: 1 addition & 5 deletions scripts/release/build-macos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -420,11 +420,7 @@ done
# the user setting in Gate A and hides the toggle, the feature does the same
# for the native Gate B in export_otel_logs.
[[ "$VITE_TELEMETRY_ENFORCED_VALUE" == "1" ]] && CARGO_FEATURES="$CARGO_FEATURES,block-telemetry-enforced"
if [[ "$VITE_VOICE_DICTATION_VALUE" == "1" ]]; then
CARGO_FEATURES="$CARGO_FEATURES,block-voice-dictation"
else
CARGO_FEATURES="$CARGO_FEATURES,no-voice-dictation"
fi
[[ "$VITE_VOICE_DICTATION_VALUE" == "1" ]] && CARGO_FEATURES="$CARGO_FEATURES,block-voice-dictation"

# bb CLI PATH install has no runtime-config representation; the custom pipeline
# exposes a dedicated select that disables it via the Cargo feature.
Expand Down
9 changes: 7 additions & 2 deletions scripts/release/tests/release-scripts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,6 @@ describe("build-macos Block-service feature seam", () => {
expect(script).not.toContain(
'VITE_AUTH_GATE_VALUE="$VITE_BUILDERBOT_VALUE"',
);
expect(script).toContain("no-voice-dictation");
expect(script).toContain('if [[ "$VITE_AGENT_TOOLS_VALUE" == "1" ]]; then');
expect(script).toContain(
'jq \'.bundle.resources["../resources/bb"] = "bb"\'',
Expand Down Expand Up @@ -2155,10 +2154,16 @@ async function canonicalGates() {
}

describe("Block feature gate propagation", () => {
it("supports an empty base feature set", () => {
const result = run("bash", ["scripts/block-feature-gates.sh"]);
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("");
});

it("maps every updater-off default to the fail-closed Cargo posture", () => {
const result = run("bash", ["scripts/block-feature-gates.sh", "berdctl"]);
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("berdctl,no-voice-dictation");
expect(result.stdout.trim()).toBe("berdctl");
});

it("maps every renderer gate to its matching Cargo feature", () => {
Expand Down
2 changes: 1 addition & 1 deletion scripts/windows/Test-WindowsDev.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ try {
Assert-Equal "process args: trailing backslash doubled inside quotes" (Join-WindowsProcessArguments -Arguments @("C:\Program Files\")) '"C:\Program Files\\"'
Assert-Equal "process args: embedded quote escaped" (Join-WindowsProcessArguments -Arguments @('say "hi"')) '"say \"hi\""'

Assert-Equal "public app feature defaults fail closed" (Get-BerdAppFeatures) "berdctl,app-test-driver,no-voice-dictation"
Assert-Equal "public app feature defaults fail closed" (Get-BerdAppFeatures) "berdctl,app-test-driver"
$featureGateNames = @("VITE_AGENT_TOOLS", "VITE_AUTOMATIONS", "VITE_BUILDERBOT", "VITE_FEEDBACK", "VITE_MANAGED_CONNECTIONS", "VITE_SKILL_DISCOVERY", "VITE_TELEMETRY_ENFORCED", "VITE_VOICE_DICTATION")
$savedFeatureGates = @{}
foreach ($name in $featureGateNames) {
Expand Down
3 changes: 0 additions & 3 deletions scripts/windows/WindowsDev.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -513,9 +513,6 @@ function Get-BerdAppFeatures {
}
if ($value -eq "1") { $features.Add($gate.Feature) }
}
if ([Environment]::GetEnvironmentVariable("VITE_VOICE_DICTATION", "Process") -ne "1") {
$features.Add("no-voice-dictation")
}
return ($features -join ",")
}

Expand Down
5 changes: 2 additions & 3 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,6 @@ devtools = ["tauri/devtools"]
# restricted build is expressed by ADDING these (never with
# --no-default-features + re-listing everything you want to keep).
#
# no-voice-dictation: the realtime client secret is never requested
# (`get_openai_realtime_status` reports `configured: false`).
no-voice-dictation = []
# no-bb-cli-install: the app never auto-installs or offers to symlink the
# bundled berdctl into /usr/local/bin/bb. The bundled binary still ships for
# direct `berdctl` use (gated separately by the `berdctl` protocol-server
Expand Down Expand Up @@ -177,6 +174,8 @@ block-skill-discovery = []
# For managed internal distributions where consent is an employment-policy
# fact, not a per-user choice.
block-telemetry-enforced = []
# Enables Block-service-backed voice dictation. Public Realtime voice is a
# separate, user-configured path and does not depend on Block services.
block-voice-dictation = []
# Admin runtime-config endpoint fetch. Default-OFF: a normal build never
# compiles the kgoose-backed fetch/cache path and instead loads the bundled
Expand Down
155 changes: 154 additions & 1 deletion src-tauri/crates/berdctl/api-surface-feedback.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"protocolVersion": 4,
"groups": {
"sessions": {
"description": "Manage the user's chat sessions: create (fire-and-forget, on any installed agent harness), send, open, list, get, rename, move, move to group, clear project, fork, archive.",
"description": "Manage the user's chat sessions: create (fire-and-forget, on any installed agent harness), send, open, list, get, rename, move, move to group, clear project, send to a live voice Spokesperson, dismiss voice handoffs, fork, archive.",
"actions": {
"create": {
"description": "Create a new chat session on any installed agent harness and send the prompt in it. Fire-and-forget: returns the session id immediately and the session runs in the background without changing what the user sees; the user can open it themselves. Use --from to give the delegating session or tool a concise visible label on the initial message. Only check on it later (action \"get\") if the user asks.",
Expand Down Expand Up @@ -427,6 +427,159 @@
"additionalProperties": false
}
},
"send_to_spokesperson": {
"description": "Inject a private coordination message into the OpenAI Realtime voice Spokesperson owned by an existing Berd session. The Spokesperson receives the message either as silent context for future turns or as a request to speak now. The command fails when the target session has no live Realtime voice conversation.",
"fields": [
{
"name": "session_id",
"required": true,
"kind": "string",
"description": "Id of the session that owns the live Realtime Spokesperson.",
"min": 1
},
{
"name": "message",
"required": true,
"kind": "string",
"description": "Private coordination message to inject into the Spokesperson.",
"min": 1,
"max": 20000
},
{
"name": "cursor",
"required": true,
"kind": "number",
"description": "Newest cursor from any Expert-bound voice transcript, handoff, reminder, or bridge result.",
"min": 0,
"max": 4294967295
},
{
"name": "mode",
"required": false,
"kind": "string",
"description": "Delivery mode: context updates future turns silently; say asks the Spokesperson to speak now.",
"values": ["context", "say"]
},
{
"name": "resolves",
"required": false,
"kind": "string_array",
"description": "Open handoff id resolved by this say message; repeat for multiple handoffs."
}
],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"session_id": {
"type": "string",
"minLength": 1,
"description": "Id of the session that owns the live Realtime Spokesperson."
},
"message": {
"type": "string",
"minLength": 1,
"maxLength": 20000,
"description": "Private coordination message to inject into the Spokesperson."
},
"cursor": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"description": "Newest cursor from any Expert-bound voice transcript, handoff, reminder, or bridge result."
},
"mode": {
"default": "say",
"description": "Delivery mode: context updates future turns silently; say asks the Spokesperson to speak now.",
"type": "string",
"enum": ["context", "say"]
},
"resolves": {
"default": [],
"description": "Open handoff id resolved by this say message; repeat for multiple handoffs.",
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 100
}
}
},
"required": ["session_id", "message", "cursor"],
"additionalProperties": false
}
},
"dismiss_handoffs": {
"description": "Explicitly close one or more open Realtime Spokesperson handoffs and deliver the reason as silent context without waking the Spokesperson. Use this only when a spoken response is obsolete, superseded, or already handled. The command and its reason remain visible in the Expert's normal Berd activity.",
"fields": [
{
"name": "session_id",
"required": true,
"kind": "string",
"description": "Id of the session that owns the live Realtime Spokesperson.",
"min": 1
},
{
"name": "cursor",
"required": true,
"kind": "number",
"description": "Newest cursor from any Expert-bound voice transcript, handoff, reminder, or bridge result.",
"min": 0,
"max": 4294967295
},
{
"name": "handoff_id",
"required": true,
"kind": "string_array",
"description": "Open handoff id to dismiss; repeat for multiple handoffs."
},
{
"name": "reason",
"required": true,
"kind": "string",
"description": "Why no spoken response is needed for these handoffs.",
"min": 1,
"max": 2000
}
],
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"session_id": {
"type": "string",
"minLength": 1,
"description": "Id of the session that owns the live Realtime Spokesperson."
},
"cursor": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"description": "Newest cursor from any Expert-bound voice transcript, handoff, reminder, or bridge result."
},
"handoff_id": {
"minItems": 1,
"maxItems": 100,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"description": "Open handoff id to dismiss; repeat for multiple handoffs."
},
"reason": {
"type": "string",
"minLength": 1,
"maxLength": 2000,
"description": "Why no spoken response is needed for these handoffs."
}
},
"required": ["session_id", "cursor", "handoff_id", "reason"],
"additionalProperties": false
}
},
"fork": {
"description": "Duplicate an existing chat session, copying its full conversation history into a new session the user can continue down an independent path. The fork appears in the app's session list; the user's current view does not change.",
"fields": [
Expand Down
Loading