feat(contact-center): centralize consult transfer list policy (CAI-8354) - #5179
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78d9b379db
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| public async getConsultTransferQueues( | ||
| params: ConsultTransferQueueSearchParams = {} | ||
| ): Promise<ContactServiceQueuesResponse> { |
There was a problem hiding this comment.
Register the new public APIs in the contract catalogs
This adds public façade methods and exported request types, but neither ai-docs/CONTRACTS.md nor .sdd/manifest.json contains the new consult/transfer APIs, so the package's authoritative contract and routing metadata will omit this public surface during conformance checks. Add the corresponding catalog and manifest entries in this change.
AGENTS.md reference: packages/@webex/contact-center/AGENTS.md:L64-L70
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 61b3ada8cf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| /** Desktop-profile filtering changes the returned result set */ | ||
| desktopProfileFilter?: boolean; | ||
| /** Provisioning-view filtering changes the returned result set */ | ||
| provisioningView?: boolean; | ||
| /** Single-object response mode changes the response shape */ | ||
| singleObjectResponse?: boolean; |
There was a problem hiding this comment.
Update the scoped PageCache contract guide
These new cache-validation fields and bypass semantics are absent from src/utils/AGENTS.md, which still documents only search, filter, attributes, and sortBy as CacheValidationParams and canUseCache() inputs. Update that scoped contract so maintainers do not follow stale cache-safety guidance; its prescribed workflow explicitly requires contract changes to update the utils guide and pass a drift check.
AGENTS.md reference: packages/@webex/contact-center/src/utils/AGENTS.md:L247-L253
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e2ec8310a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| destinations.push(CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE); | ||
| } | ||
| if (isVoice) destinations.push(CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER); | ||
| if (canUseEntryPoints) destinations.push(CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT); |
There was a problem hiding this comment.
Exclude entry points from blind-transfer destinations
When a voice task has entry-point access, this shared builder appends entryPoint to both the Consult and Transfer arrays. The existing destination contract explicitly marks DESTINATION_TYPE.ENTRYPOINT as supported only for consult operations (src/services/task/types.ts:36-37), while Task.transfer() sends every non-queue destination through blindTransfer() (src/services/task/Task.ts:675-684). A consumer following the new transfer controls can therefore offer an unsupported entry-point transfer that is rejected by the backend; build the two arrays separately so entry points are included only for Consult.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9aa9dc7af
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| filter: 'queueType==INBOUND;channelType==TELEPHONY;active==true', | ||
| sortBy: 'name', | ||
| sortOrder: 'asc', | ||
| ...params, |
There was a problem hiding this comment.
Preserve queue defaults when optional fields are undefined
When callers build this optional parameter object with fields whose values are undefined (for example, getQueues({filter: selectedFilter, sortBy: selectedSort}) before either selection exists), ...params overwrites the default filter and sort values. fetchQueues() then emits neither the inbound-active-telephony filter nor sort=name,ASC, returning a broader and unordered queue list even though the caller supplied no effective override. Normalize undefined fields before merging, while retaining an explicit empty filter if that is the supported way to disable the default.
Useful? React with 👍 / 👎.
| filter: DEFAULT_ENTRY_POINT_FILTER, | ||
| sortBy: 'name', | ||
| sortOrder: 'asc', | ||
| ...params, |
There was a problem hiding this comment.
Preserve the entry-point filter when its override is undefined
If an options object contains filter: undefined, which is common when forwarding an optional typed filter, this spread replaces DEFAULT_ENTRY_POINT_FILTER with undefined. The request consequently omits the active inbound telephony restriction and can return destinations outside the documented default policy; only a defined caller filter should override this default.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd0664183e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| function applyTaskDestinationTypes(dropdown, action) { | ||
| const destinations = currentTask?.uiControls?.consultTransferDestinations?.[action]; | ||
| if (!Array.isArray(destinations) || destinations.length === 0) return; |
There was a problem hiding this comment.
Clear destination choices when policy returns none
When a task legitimately exposes an empty destination array—for example, a digital task whose profile denies both buddy-team and queue access—this early return leaves the static HTML options (queue, agent, dialNumber, and entryPoint) in the dropdown. The sample can then submit a destination category that the SDK explicitly omitted, defeating the centralized policy; distinguish a missing policy field used for backward compatibility from an explicitly empty array, and clear or disable the dropdown for the latter.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 54560d7e30
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| applyTaskDestinationTypes(document.querySelector('#transfer-destination-type'), 'transfer'); | ||
| transferOptions.style.display = 'block'; | ||
| onTransferTypeSelectionChanged(); | ||
| await onTransferTypeSelectionChanged(); |
There was a problem hiding this comment.
Stop transfer setup when the destination policy is empty
When a connected digital task denies both buddy-team and queue access, consultTransferDestinations.transfer is explicitly empty. applyTaskDestinationTypes() clears and disables the type selector, but this unconditional call then handles its empty value as the fallback case and creates an enabled free-text destination input; initiateTransfer() subsequently accepts that text and submits a transfer with destinationType: ''. Fresh evidence in the current revision is this post-clear handler call, so return before building transfer inputs when the policy array is empty.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 08acad6fc0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const isSearchRequest = !!(search || filter || attributes || sortBy); | ||
| const hasCustomSort = sortBy !== 'name' || sortOrder !== 'asc'; | ||
| const isSearchRequest = !!(search || filter || attributes || hasCustomSort); | ||
| const effectiveSortBy = sortBy === 'name' ? DEFAULT_ENTRY_POINT_SORT_FIELD : sortBy; |
There was a problem hiding this comment.
Translate the public entry-point ID sort field
When an existing caller uses getEntryPoints({sortBy: 'id'}), this forwards id to the new dial-number endpoint, where it identifies the dial-number mapping, while the returned EntryPointRecord.id is populated from entryPointId. The response is therefore ordered by an internal identifier that is discarded rather than by the public ID requested by the caller, regressing the previous /v2/entry-point behavior. Map the public id sort field to entryPointId, as is already done for name.
AGENTS.md reference: packages/@webex/contact-center/AGENTS.md:L81-L85
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 14fe9c5ca0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| dropdown.innerHTML = ''; | ||
| dropdown.disabled = destinations.length === 0; | ||
|
|
||
| if (destinations.length === 0) return; |
There was a problem hiding this comment.
Disable submission when no destinations are allowed
When the task policy explicitly returns an empty array, this branch disables only the destination-type selector; the initial or previously rendered destination input and the Initiate button remain enabled. Although the current revision now avoids rebuilding the input, initiateConsult() and initiateTransfer() can still read that editable/stale input and submit a task operation with destinationType: '', bypassing the empty policy. Clear or disable the destination controls and submission action before returning.
Useful? React with 👍 / 👎.
rarajes2
left a comment
There was a problem hiding this comment.
Please attach a vidcast to showcase the feature.
| * const url = endPointMap.entryPointList('org123', 'page=0&pageSize=10'); | ||
| * @ignore | ||
| */ | ||
| entryPointList: (orgId: string, queryParams: string) => | ||
| `/organization/${orgId}/v2/entry-point?${queryParams}`, | ||
| entryPointDialNumberList: (orgId: string, queryParams: string) => | ||
| `/organization/${orgId}/v3/dial-number?${queryParams}`, |
There was a problem hiding this comment.
Do we not need the v2 at all ?
There was a problem hiding this comment.
The v3 dial-number endpoint is intentional for this flow because it supports desktopProfileFilter, returns the entry-point mapping and dialled number, and matches the list required by the consumer. The previous v2 entry-point endpoint has no remaining call site. If we later need raw entry-point entities, that should be handled as a separate use case.
| accessQueue: agentProfileData.accessQueue as 'ALL' | 'SPECIFIC' | 'NONE', | ||
| accessEntryPoint: agentProfileData.accessEntryPoint as 'ALL' | 'SPECIFIC' | 'NONE', | ||
| accessBuddyTeam: agentProfileData.accessBuddyTeam as 'ALL' | 'SPECIFIC' | 'NONE', |
There was a problem hiding this comment.
Since we have created types for these, we should use them.
There was a problem hiding this comment.
Agreed. The collaboration-access type should be defined once and reused by DesktopProfileResponse, Profile, the task destination configuration, and this mapping instead of repeating the literal union. addressed
| resource, | ||
| method, | ||
| body, | ||
| ...(headers ? {headers} : {}), |
There was a problem hiding this comment.
Check once if the spread is working as expected
There was a problem hiding this comment.
Confirmed. When headers is provided, the spread adds {headers} to the request; when it is undefined, no headers property is added. The WebexRequest unit test verifies forwarding the header object, and the EntryPoint tests verify the required headers reach webex.request()
| transfer: [], | ||
| }; | ||
|
|
||
| if (destinationConfig.accessBuddyTeam?.toUpperCase() !== 'NONE') { |
There was a problem hiding this comment.
Instead of using the hardcoded strings here, we should use the created constants. Applicable everywhere
There was a problem hiding this comment.
Agreed. I’ll use a shared collaboration-access constant for NONE and the corresponding direction constants instead of spelling these values inline.
| * Desktop Profile access level for a consult/transfer destination category. | ||
| * @internal | ||
| */ | ||
| export type ConsultTransferCollaborationAccess = 'ALL' | 'SPECIFIC' | 'NONE'; |
There was a problem hiding this comment.
This is repeated in this file as well - packages/@webex/contact-center/src/services/config/types.ts
Please search through the codebase and centralize it
There was a problem hiding this comment.
Agreed. This is the same access domain used by the profile and task configuration. I’ll centralize the constant/type in the configuration types and reuse it from the task state-machine types.
| attributes | ||
| ?.split(',') | ||
| .map((attribute) => attribute.trim()) | ||
| .filter(Boolean) | ||
| .forEach((attribute) => mergedAttributes.add(attribute)); |
There was a problem hiding this comment.
Q. How does the .filter(Boolean) work ?
There was a problem hiding this comment.
After splitting and trimming the attributes, .filter(Boolean) removes empty strings. For example, "id, ,name," becomes ["id", "name"] before the values are added to the Set.
| * The media type channel to filter buddy agents. Defaults to telephony when omitted. | ||
| */ | ||
| mediaType: 'telephony' | 'chat' | 'social' | 'email'; | ||
| mediaType?: 'telephony' | 'chat' | 'social' | 'email'; |
There was a problem hiding this comment.
Is this not a required field anymore ?
There was a problem hiding this comment.
Correct—it is intentionally optional now. getBuddyAgents() defaults it to telephony, allowing calls such as getBuddyAgents({action: 'Consult'}). Digital consumers can still pass chat, email, or social explicitly. The default behavior is covered by a unit test.
…4-expose-desktop-profile-collaboration-flags # Conflicts: # packages/@webex/contact-center/ai-docs/CONTRACTS.md # packages/@webex/contact-center/src/services/task/ai-docs/task-spec.md
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 03d6aae36e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const mapDialNumberEntryPoint = (item: DialNumberEntryPointRecord): EntryPointRecord => ({ | ||
| id: item.entryPointId, | ||
| name: item.entryPointName, | ||
| ...(item.dialledNumber ? {number: item.dialledNumber} : {}), | ||
| }); |
There was a problem hiding this comment.
Preserve requested entry-point attributes
When a caller uses the retained attributes option—for example, requesting description or another compatible dial-number field—the request includes that field, but this mapper unconditionally discards every property except the remapped ID, name, and number. Consequently, getEntryPoints({attributes: ...}) cannot return the attributes it explicitly requested, including fields still represented by EntryPointRecord; preserve compatible requested properties while overriding only the SDK-owned mapped fields.
Useful? React with 👍 / 👎.
COMPLETES CAI-8354
This pull request addresses
Contact Center consumers need consistent consult and transfer destination eligibility, visibility, and backend ordering through the existing SDK surfaces.
Changes
Demo
Watch the Vidcast
Impact
Consumers can use the existing list methods and read one root Task control field without recreating reusable destination decisions. Existing explicit list options remain available for consumers that need supported overrides.
Validation
Change Type
GAI Coding Policy