Live copilot ingress and shared page sessions¶
Status: Draft | Last reviewed: 2026-08-12
How to let a Salesforce, ServiceNow, or custom-web component select a copilot, start or attach a live audio source, and drive transcript, Agent Assist, and copilot widgets from one shared conversation. Read this before adding browser media capture to a host component or exposing a copilot media endpoint.
Decision¶
Build one media-owning session controller. Every visible widget subscribes to the controller's conversation events; no transcript, suggestions, sentiment, or copilot-chat widget opens its own media connection.
Use three protocols with separate responsibilities:
| Plane | Protocol | Purpose |
|---|---|---|
| Control | HTTPS JSON | List permitted copilots, create a logical session, attach context, and end the session |
| Media | WebRTC with WHIP-compatible signaling | Send low-latency browser audio to TrueServe |
| Events | Existing conversation-api SSE | Fan transcript, suggestions, insights, status, and tool events out to any number of widgets |
Use browser WebRTC only when the browser is the audio source. Treat a combined room microphone as a mixed mono source and use speaker diarization to separate in-person participants. If a Salesforce VoiceCall or another CCaaS call already exists, attach the selected copilot to the server-side call tap instead. A Salesforce-page microphone can hear the room, but it does not give reliable access to the remote party's isolated softphone audio.
Warning: Do not market microphone-only capture as two-party Agent Assist. Use a server-side SCV, Genesys, Five9, Amazon Connect, SIPREC, or TrueServe media tap when speaker roles matter.
Why WebRTC and WHIP¶
WebRTC already gives the browser encrypted Opus media, congestion control, NAT traversal, and ICE/TURN. WHIP standardizes the small HTTP offer/answer and teardown layer needed for one-way media ingest. It matches this use case more closely than a bidirectional voice-call protocol.
| Option | Decision |
|---|---|
| Direct Pion plus WHIP | Use for the first browser-ingest release; it reuses media-proxy and keeps the client small |
| LiveKit room | Reconsider when a session needs multiple browser participants, SFU routing, or room semantics; the current TrueServe provider is avatar-oriented |
| WebSocket PCM | Do not use as the primary media path; it recreates codec, congestion, jitter, and backpressure behavior already provided by WebRTC |
| Janus/SIP | Keep for telephony; a single browser producer does not need a SIP bridge |
Existing building blocks¶
| Building block | Reuse |
|---|---|
| surface-app embed display modes | Render the existing SSE stream without knowing how media arrived |
| CRM embed components | Salesforce and ServiceNow wrappers that frame those display modes, with server-side token minting already solved |
| Copilot API design | Supplies the /copilot/* facade and normalized suggestions contract |
| conversation-api SSE | Carries transcript, suggestion, custom-insight, tool, and status events |
| Direct WebRTC handler | Pion peer, Opus transport, ICE, and gRPC bridge |
| Agent Assist sidecar linking | Routes caller and agent audio into a copilot session for platform-managed calls |
| Workflow Transcribe node | Already normalizes batch diarization from Deepgram, AWS, Azure, and Google into speaker-labelled segments |
embed_public_clients.allowed_agent_ids |
Limits which agents a host may offer in its selector |
| Agent Studio for Salesforce | Apex token minting, record binding, context Flow, and App Builder exposure |
The current direct-WebRTC implementation is voice-agent shaped, not copilot-ingest shaped:
session.goopensSessionType.AGENTwith one input channel and an outbound TTS track.opus.godecodes stereo Opus and downmixes it to mono.- The first inbound track wins; later audio tracks are discarded.
/api/proxy/webrtc/offerrequires an ai-manager user or emulator session and is not an external surface contract.- The current
<trueserve-session>owns REST and SSE state, but noMediaStream,RTCPeerConnection, or media-session lifecycle. - The live Deepgram Nova-3 definition does not expose diarization, and its wrapper does not convert word-level speaker tags into speaker-aware transcript frames.
Preserve the existing voice-test route. Add an ingest-specific path instead of weakening its authorization or changing its behavior.
Audio source modes¶
| Source mode | Component action | Server action | Speaker quality | Priority |
|---|---|---|---|---|
existing_conversation |
Select a copilot and attach to the current record or external call id | Link a new assist sidecar to the existing media sessions | Best; roles already separated when the connector supplies them | Production default |
browser_dictation |
Prompt for microphone access and publish one track | Run an ingest-only assistant session with diarization off | One known speaker | First browser MVP |
browser_meeting |
Prompt for microphone access and publish the mixed room track | Run streaming diarization and emit participant-labelled utterances | Model-estimated speakers from one combined source | Primary meeting mode |
host_stream |
A custom in-DOM host sets a MediaStream property |
Accept one or two labeled tracks | Best when the host supplies separate customer and agent tracks | Custom-app phase |
display_audio |
Ask the operator to share a tab/window with audio | Treat as a mixed source | Browser-dependent, mixed, and privacy-sensitive | Unsupported by default |
existing_conversation must fail explicitly with 409 source_unavailable if the call transport has no tap. Do not silently fall back to the page microphone.
Mixed-microphone diarization¶
Diarization answers "which anonymous speaker said this?" It does not normally identify a person's real name. Preserve provider labels such as speaker_0 and let the operator map them to names after hearing the first few turns. Voice-reference enrollment is an optional enhancement, not a requirement for the live MVP.
Use a two-pass pipeline for meeting mode:
| Pass | Default | Purpose |
|---|---|---|
| Live | Deepgram Nova-3 streaming with diarize_model=latest |
Low-latency captions and copilot input; the current streaming diarizer is v1 |
| Final | Deepgram Nova-3 batch with diarize_model=latest |
Recompute the meeting with the newer v2 batch diarizer and publish a refined transcript |
| Known-speaker finalization | gpt-4o-transcribe-diarize |
Optional final pass when the tenant supplies short reference clips and names for known participants |
Azure Conversation Transcription is the strongest existing-cloud alternative for real-time meeting diarization. AWS Transcribe and Google Cloud STT also support streaming diarization, but TrueServe does not currently have live speaker-aware wrappers for them. Evaluate providers on the tenant's actual rooms, accents, speaker counts, and microphones before making one the global default.
For dictation mode, disable diarization and stamp the known operator identity. Running diarization on one expected speaker adds label instability without adding value.
Represent a live diarized utterance with fields that permit correction:
{
"turn_id": "turn-42",
"text": "Let's move the launch to Friday.",
"speaker_id": "speaker_1",
"speaker_label": "Speaker 2",
"speaker_confidence": null,
"is_final": true,
"revision": 1
}
Live speaker assignments may change as more audio arrives. Feed only finalized utterances into suggestions and other agent actions. The final pass may publish refined_transcript events with a higher revision; clients replace the earlier segment by turn_id instead of appending a duplicate.
Target architecture¶
flowchart LR
subgraph HOST[Host page]
control[Live copilot controller]
bus[Host session bus]
transcript[Transcript]
suggestions[Agent Assist]
chat[Copilot chat]
other[Sentiment and future widgets]
end
subgraph EDGE[TrueServe public APIs]
catalog[Copilot catalog]
session[Copilot session control]
whip[WHIP signaling]
sse[Conversation SSE]
end
subgraph MEDIA[Media runtime]
proxy[media-proxy ingest or existing call tap]
grpc[gRPC audio stream]
orch[ai-orchestrator copilot pipeline]
redis[(conversation event stream)]
end
control --> catalog
control --> session
control -->|browser audio only| whip --> proxy
session -->|existing call| proxy
proxy --> grpc --> orch --> redis --> sse
control --> bus
bus --> transcript
bus --> suggestions
bus --> chat
bus --> other
sse --> transcript
sse --> suggestions
sse --> chat
sse --> other
The shared value is a safe session descriptor, not audio and not a bearer token:
{
"sessionKey": "case-assist",
"conversationId": "7b13d5d8-...",
"sessionId": "7b13d5d8-...",
"agentId": "3021",
"agentName": "Claims Copilot",
"sourceMode": "browser_meeting",
"captureProfile": "meeting",
"state": "live"
}
Each cross-origin widget gets its own short-lived token bound to that conversation. Never put a token on the host message bus.
Component contract¶
Add two framework-free elements and expose them as one host component:
| Element | Responsibility |
|---|---|
<trueserve-live-session> |
Headless state machine, selected agent, logical session, optional WebRTC ingest, and registration of the resulting TrueServeSDKClient |
<trueserve-copilot-launcher> |
Accessible selector plus Start, Mute, and Stop controls bound to a live session |
The Salesforce wrapper is c-true-serve-live-copilot. It composes both elements and remains the single App Builder component an admin places. Custom apps may use the elements separately.
Recommended inputs:
| Input | Meaning |
|---|---|
client-id |
Public embed-client/config id whose allowed_agent_ids forms the selector catalog |
agent-id |
Optional fixed agent; hides the selector |
source-mode |
existing_conversation, browser_dictation, browser_meeting, or host_stream |
expected-speakers |
Optional minimum/maximum hint for meeting diarization |
conversation-id / external-id |
Existing call binding for attach mode |
session-key |
Stable page-local name used by sibling widgets |
context |
CRM context copied into the logical session before media starts |
media-stream property |
Custom in-DOM source; never serialized through an iframe bridge |
Do not support automatic microphone start. A visible operator gesture must start capture and the component must keep a persistent live/muted indicator on screen.
Recommended outward events:
| Event | Detail |
|---|---|
trueserve-session-starting |
{sessionKey, agentId, sourceMode} |
trueserve-session-started |
Safe session descriptor |
trueserve-session-state |
Descriptor plus state and optional safe error code |
trueserve-session-ended |
Descriptor plus reason |
trueserve-session-error |
{code, message, retryable} with no credentials or provider secrets |
Host fan-out¶
| Host | Fan-out mechanism |
|---|---|
| Same DOM | Composed DOM events between the capture controller and the widgets sharing its page |
| Salesforce | A TrueServeLiveSession__c Lightning Message Channel; wrappers subscribe, mint a conversation-bound token through Apex, then connect their own iframe |
| ServiceNow | UI Builder action plus the existing origin-checked iframe bridge |
| Plain iframe | Origin-checked postMessage carrying only the safe descriptor |
Lightning Message Service belongs outside the surface-app iframe. The iframe emits a session event to its wrapper; the wrapper publishes it to the page. An LMS message does not cross an iframe boundary by itself.
Whichever component owns capture needs allow="microphone; autoplay" on its iframe: microphone access in a cross-origin iframe requires permission delegation. The CRM wrappers use a plain sandboxed iframe, so they can set it. Passive transcript and suggestion components need nothing.
Public API shape¶
Extend the draft Copilot API with the following surface.
Catalog¶
GET /copilot/agents?client_id=<id> returns safe summaries only:
{
"items": [
{
"id": "3021",
"name": "Claims Copilot",
"description": "Live claims guidance",
"capabilities": ["transcription", "suggestions", "query"],
"audio": {"minChannels": 1, "preferredChannels": 2}
}
]
}
Filter to the caller's tenant, allowed_agent_ids, published versions, and agent_type="agent_assistant". An empty allowlist returns no agents. Do not expose prompts, integrations, provider credentials, draft versions, or full agent_versions.config.
Salesforce calls the catalog through Apex and its Named Credential. After selection, Apex mints the normal agent-scoped embed token. A custom host with a backend follows the same pattern. A browser-only catalog token is a later PKCE enhancement, not required for the MVP.
Logical session¶
POST /copilot/sessions accepts:
{
"agent_id": "3021",
"source": {
"kind": "browser_meeting",
"tracks": [{"role": "participant", "channels": 1}],
"diarization": {"enabled": true, "min_speakers": 2, "max_speakers": 6}
},
"external_id": "salesforce:0LQ...",
"context": {"case_id": "500..."},
"idempotency_key": "c25b2e2d-..."
}
It validates the exact agent capability and tenant, resolves the published version, creates the conversation, applies context, and returns:
{
"conversation_id": "7b13d5d8-...",
"session_id": "7b13d5d8-...",
"state": "created",
"events_url": "/sse/conversations/7b13d5d8-...",
"media": {
"protocol": "whip",
"endpoint": "/copilot/sessions/7b13d5d8-.../whip",
"token": "short-lived-session-capability",
"expires_at": "2026-07-15T12:01:00Z"
}
}
For existing_conversation, return no media block. The server links the selected copilot to the existing call tap and publishes the same status events.
WebRTC ingest¶
Make /copilot/sessions/{id}/whip conform to the basic WHIP exchange:
POSTan SDP offer asapplication/sdpwith the session capability inAuthorization.- Return
201 Created, the SDP answer, and aLocationfor the media resource. DELETEthe returned resource to stop ingest.
The capability authorizes one session, agent, origin, and limited setup window. It is not a tenant API token. Return ephemeral TURN credentials through the WHIP ICE-server relation or the logical-session response; never ship the static coturn credential to a browser.
Keep live output on GET /sse/conversations/{id}. Do not create a second WebSocket or WebRTC data-channel vocabulary for transcript and suggestions.
Media-runtime changes¶
Add an ingest-only Pion session alongside the existing voice-agent session:
- Open gRPC with
SessionType.ASSISTANT, no outbound TTS track, and published-version-only metadata. - Preserve mono input as one channel and stamp its configured role.
- Add a speaker-aware live STT adapter that groups contiguous word-level speaker tags into utterances and emits stable
turn_id,speaker_id, finality, and revision fields. - Add
diarize_modelto the Deepgram service definition and wrapper. Uselatest; do not set both the deprecateddiarizeflag anddiarize_model. - Record meeting audio once when policy permits, then submit it to the selected finalizer and publish the result as
refined_transcriptrather than overwriting audit history in place. - For dual-track input, decode each track independently, align 20 ms PCM frames, interleave customer and agent channels, and advertise
num_channels=2to the orchestrator. - Bound per-track queues and record packet loss, late-frame drops, jitter, queue depth, and role mapping.
- Reject an unexpected third track or a track that does not match the declared role map.
- Close the peer, gRPC stream, Redis ownership entry, and conversation status from any teardown path.
Do not modify the existing decoder to return stereo for every voice call. Add an explicit ingest decoder/mixer so voice-agent behavior and recordings stay stable.
Store transient ownership and lifecycle in Redis with a TTL instead of an ai-manager process-local map. The durable audit record remains the conversation and its messages. The Redis record contains tenant, agent, conversation, media resource id, owner node, state, and last heartbeat so DELETE and timeout cleanup work across replicas.
Security and compliance¶
- Bind catalog access to
embed_public_clients.allowed_agent_ids; deny an empty or missing allowlist. - Bind every session and WHIP capability to tenant, agent, origin, conversation, expiry, and a maximum concurrent-session count.
- Keep the media-proxy API key server-side. The browser talks only to the authenticated public facade and to ICE candidates negotiated for its session.
- Enforce conversation-api allowed origins and Salesforce CSP Trusted URLs.
- Use a visible operator gesture, microphone indicator, mute state, and stop control.
- Default recording off for ad-hoc browser capture until the tenant supplies a recording/retention policy and the host captures any required consent.
- Redact bearer values, SDP ICE passwords, TURN credentials, provider tokens, and raw CRM context from logs.
- Treat audio and transcripts as tenant data for retention, deletion, audit, and telemetry routing.
- Rate-limit catalog, session creation, and SDP setup separately. Cap session duration and idle time.
Delivery phases¶
Phase 0 — transport and host spike¶
- Prove microphone capture in a normal Salesforce iframe on supported Lightning browsers.
- Verify that the wrapper's sandboxed iframe delegates microphone access via
allow="microphone; autoplay"on supported Lightning browsers. - Adapt a Pion ingest session to the WHIP POST/DELETE exchange.
- Prove a mono
SessionType.ASSISTANTstream creates one conversation and emits transcript plus one suggestion over existing SSE. - Prove two people on one room microphone produce distinct speaker-labelled final utterances and that a later correction replaces rather than duplicates a turn.
Exit when a Salesforce sandbox can select a fixed copilot, start microphone capture by gesture, see an SSE transcript, and stop with no leaked peer or gRPC session.
Phase 1 — composable microphone MVP¶
- Add catalog and logical-session endpoints with allowlist enforcement and idempotency.
- Extract the reusable WebRTC state machine from the React-only test hooks into a framework-free client.
- Add explicit Dictation and Meeting capture profiles; only Meeting enables diarization.
- Add
<trueserve-live-session>,<trueserve-copilot-launcher>, and the Salesforce wrapper. - Publish the safe session descriptor over Salesforce LMS and connect existing transcript/suggestions/copilot wrappers to it.
- Add one-use media capabilities, ephemeral TURN credentials, Redis ownership, timeouts, and explicit error codes.
Exit when one media stream drives at least three independent widgets without any widget opening another media connection.
Phase 2 — real-call attach and speaker separation¶
- Add dynamic attach/detach for TrueServe, SCV, Genesys, Five9, and other transports that expose a tap.
- Preserve separate caller and agent channels through media-proxy and the copilot pipeline.
- Add
host_streamsupport for custom apps that can provide labeled tracks. - Resolve Salesforce VoiceCall/external ids through the existing record-binding flow and persist the chosen conversation id where page reloads can recover it.
Exit when the same component can attach to a live Salesforce call, shows correct customer/agent transcript roles, and returns source_unavailable for unsupported telephony instead of using ambient audio.
Phase 3 — hardening and additional hosts¶
- Exercise ICE/TURN across enterprise NAT, failover, page navigation, utility-bar pop-out, and multi-replica routing.
- Add ServiceNow and plain-iframe wrappers with the same safe session event contract.
- Load-test concurrent ingest, Redis cleanup, SSE fan-out, and provider quotas.
- Add dashboards for setup latency, active sessions, media gaps, transcript latency, suggestion latency, and abnormal teardown.
Acceptance criteria¶
- Only explicitly allowed, published copilot agents appear in the selector.
- Start always requires an operator gesture in browser-capture mode.
- One logical conversation id ties media, transcript, suggestions, query, status, and audit together.
- Multiple widgets receive the same ordered SSE events and none duplicates media ingest.
- Dictation mode produces one known speaker without unnecessary diarization.
- Meeting mode emits anonymous speaker labels, supports operator renaming, and keeps suggestions behind finalized utterances.
- Mono mode labels its single declared role honestly; dual-channel mode preserves customer and agent roles.
- A final diarization pass revises existing turns by id and never creates duplicate transcript text.
- An existing-call attach never falls back to microphone capture.
- Stop, page unload, idle timeout, and network failure all release media and gRPC resources.
- No long-lived API token, media-proxy key, static TURN credential, or provider token reaches the browser.
Decisions to confirm before implementation¶
| Decision | Default |
|---|---|
| Primary Salesforce use case | Dictation or an in-person meeting through one combined microphone |
| Selector scope | embed_public_clients.allowed_agent_ids, fail closed |
| Browser media protocol | Audio-only WebRTC with WHIP-compatible signaling |
| First channel shape | Mono; known identity for dictation and model-derived speakers for meetings |
| Live meeting diarizer | Deepgram Nova-3 streaming with diarize_model=latest |
| Final transcript | Optional batch rediarization; publish as refined_transcript |
| Salesforce delivery | The CRM wrapper iframe, with allow="microphone" added on the capture-owning component only |
| Recording | Off until tenant policy and consent are configured |
| Event transport | Existing conversation-api SSE only |