Human-controlled customer components¶
Status: Current | Last reviewed: 2026-10-07
The shared customer-components API sends published catalogue screens from a representative's conversation to its linked customer conversation. The original voice transport may have ended. Both conversations retain sent-screen and response events, and Conversation offers View shared screen for each saved snapshot.
Ownership and linking¶
A trusted integration establishes the relationship after validating its handoff:
POST /api/conversations/{representativeConversationId}/customer-components/link
This ai-manager route requires AIManager:ManageAgents / agents:manage.
Its body contains customerConversationId, catalogIds, and an optional
registered business handler. Server-side TypeScript integrations can call
bindCustomerComponents in their existing transaction.
Both conversations must belong to the same tenant, have matching test/live
classification, and be different records. The representative must be active.
An active representative cannot be replaced. The customer record stores
metadata.customer_components; the representative stores the reciprocal
metadata.customer_component_target. Send and response requests cannot choose
another tenant, target, or handler.
The link endpoint is an integration authority, not a patient or representative button. The integration remains responsible for proving its real handoff. Surgery Partners establishes this link only after its patient-verification, accepted-transfer and Salesforce relationship checks.
Catalogue action contract¶
Catalogue components use the existing awaitable dispatchAction:
await ctx.dispatchAction({
name: "customer_component.send",
context: {
actionId, // retain this ID when retrying the same request
expectedRevision,
screen: {
catalogId: "t/<tenant-id>/customer-care",
component: "ContactPreference",
surfaceId: "customer-plan",
title: "contact preferences",
props: { heading: "How should we contact you?" },
responses: {
choose: {
title: "Customer chose a contact method",
fields: { method: ["Phone", "Email"] }
}
}
}
}
});
The shared and Surface SDKs route these actions to
POST /api/conversations/{conversationId}/customer-components.
sendCustomerComponentAction(command) is the equivalent SDK method; include
kind: "send" or "respond". Surface shares use
/api/share/{code}/customer-components, and the native Studio phone uses its
registered-owner BFF. Embed credentials remain in the server-side cookie jar.
Native Studio checks the logged-in owner and registered test conversation for
both call legs; the trusted link determines which participant may send or reply.
GET on the same route returns the actor role, active status, revision, current
screen and persisted interaction history. The core enforces the link's catalogue
allowlist and validates component props against the published catalogue schema.
Requests are limited to 64 KiB.
The renderer supplies customerInteraction.id, .revision, .response and
.readOnly on the root component. A customer submits:
await ctx.dispatchAction({
name: "customer_component.respond",
context: {
actionId,
expectedRevision: ctx.component.customerInteraction.revision,
interactionId: ctx.component.customerInteraction.id,
name: "choose",
values: { method: "Phone" }
}
});
Response definitions can use either fields for enumerated string choices or
schema for structured JSON values. Schemas must describe an object with
additionalProperties: false; references and regular-expression keywords are
not accepted. A response must match the current interaction and its declared
response definition. Caller-supplied fields cannot turn a selection into a
booking or another business operation.
Persistence and delivery¶
The core locks the customer conversation and commits its revision, receipt and events together. Reusing an action ID with changed data fails. An identical retry returns its receipt; a late retry cannot broadcast an obsolete screen over the current one.
Sending publishes native A2UI operations to the existing customer surface.
Both conversations receive a chat_event containing the serialized screen
snapshot. Responding records the chosen values and another snapshot without
changing earlier snapshots. Events persist in conversation_events and stream
to both canonical and transport-session aliases.
Responses also send customer_component.response through the existing Copilot
application-event channel. Configure interested workers with input_mode: both
or events and include that name in event_names. The event is structured data,
not a fabricated spoken transcript turn. delivery.copilotNotified reports a
subscriber, not completion of a model turn or an external transaction.
Conversation shows the title, response summary and an expandable read-only screen. The catalogue manifest includes the linked customer catalogues using the reciprocal tenant-scoped relationship. Snapshot data is fixed at the event; catalogue rendering still follows the manifest's bundle resolution.
Linked customer catalogues resolve their current published versions, matching server-side props validation. This takes precedence over the completed voice agent's older catalogue pin. Surface refreshes the manifest when the shared revision changes, including share pages whose conversation ID stays private. Unrelated voice catalogue pins retain their configured versions.
The customer share screen refreshes the shared state independently of voice
transport, so it can recover after a disconnected stream. Human workspaces
receive a customer-component-updated browser event from live interaction
events and retain polling as recovery. GET and Conversation history remain
readable after the representative call ends; mutations fail.
Business handlers¶
The default service sends catalogue descriptors and validates replies without
a demo-specific handler. A registered handler may instead resolve named
template requests and apply domain rules within the same transaction.
Surgery Partners uses surgery-partners for verified estimate/coverage snapshots
and callback selection. Its old API retains insurance collection, Salesforce
booking and wrap-up. Successful booking calls recordComponentResult to publish
the confirmation through the shared service. It no longer implements a separate
human send/response transport.
Validation¶
From the repository root:
bash .devcontainer/scripts/with-env.sh -- bash -c \
'cd apps/ai-manager && RUN_CUSTOMER_COMPONENT_DB_TESTS=1 node --import tsx --test lib/customer-components/*.test.ts'
The integration test uses disposable test conversations, real Postgres and Redis, and a standard published component without a Surgery Partners handler. It checks both histories, replies, aliases, stale revisions, replay safety, tenant/role denials and ended-call behavior, then removes its fixtures.
scripts/surgery-partners/rehearsal.ts additionally exercises the real catalogue
buttons, customer response, native Copilot event processing, Conversation
snapshots/reload and Salesforce booking. See the
Surgery Partners runbook and
presenter guide.
After that call ends, scripts/surgery-partners/check-component-replay.ts
opens a fresh Conversation browser session and checks all four events, disabled
historical controls and unchanged selections without a live business-data fetch.
Add RUN_CUSTOMER_COMPONENT_HTTP_TESTS=1 to the test command above to exercise
the embedded and native Studio HTTP boundaries against the local running apps.