@webex/contact-center 3.12.0-next.84 → 3.12.0-next.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.sdd/manifest.json +876 -0
- package/AGENTS.md +65 -409
- package/ai-docs/ARCHITECTURE.md +168 -0
- package/ai-docs/CONTRACTS.md +46 -0
- package/ai-docs/GETTING_STARTED.md +168 -0
- package/ai-docs/GLOSSARY.md +43 -0
- package/ai-docs/README.md +25 -18
- package/ai-docs/REVIEW_CHECKLIST.md +41 -0
- package/ai-docs/RULES.md +187 -198
- package/ai-docs/SECURITY.md +52 -0
- package/ai-docs/SERVICE_STATE.md +48 -0
- package/ai-docs/SPEC_INDEX.md +65 -0
- package/ai-docs/adr/0001-spec-source-policy.md +55 -0
- package/ai-docs/adr/README.md +8 -0
- package/ai-docs/adr/_adr-template.md +31 -0
- package/ai-docs/contact-center-spec.md +341 -0
- package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
- package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
- package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
- package/ai-docs/templates/README.md +1 -1
- package/ai-docs/templates/new-method/04-validation.md +1 -1
- package/ai-docs/templates/new-service/05-validation.md +1 -1
- package/dist/webex.js +1 -1
- package/package.json +2 -2
- package/src/metrics/ai-docs/AGENTS.md +2 -0
- package/src/metrics/ai-docs/ARCHITECTURE.md +2 -0
- package/src/metrics/ai-docs/metrics-spec.md +854 -0
- package/src/services/agent/ai-docs/AGENTS.md +3 -1
- package/src/services/agent/ai-docs/ARCHITECTURE.md +2 -0
- package/src/services/agent/ai-docs/agent-spec.md +504 -0
- package/src/services/ai-docs/AGENTS.md +4 -2
- package/src/services/ai-docs/services-spec.md +492 -0
- package/src/services/config/ai-docs/AGENTS.md +3 -1
- package/src/services/config/ai-docs/ARCHITECTURE.md +2 -0
- package/src/services/config/ai-docs/config-spec.md +669 -0
- package/src/services/core/ai-docs/AGENTS.md +4 -2
- package/src/services/core/ai-docs/ARCHITECTURE.md +2 -0
- package/src/services/core/ai-docs/core-spec.md +783 -0
- package/src/services/task/ai-docs/AGENTS.md +2 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +11 -2
- package/src/services/task/ai-docs/task-spec.md +1319 -0
- package/src/services/task/state-machine/ai-docs/AGENTS.md +38 -75
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +6 -4
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2115 -0
- package/src/utils/AGENTS.md +3 -1
- package/src/utils/ai-docs/utils-spec.md +381 -0
- package/umd/contact-center.min.js +2 -2
|
@@ -0,0 +1,2115 @@
|
|
|
1
|
+
# Task State Machine — SPEC
|
|
2
|
+
|
|
3
|
+
> Start here → root [`AGENTS.md`](../../../../../AGENTS.md) · router [`SPEC_INDEX.md`](../../../../../ai-docs/SPEC_INDEX.md) · system [`ARCHITECTURE.md`](../../../../../ai-docs/ARCHITECTURE.md). This is the module's canonical specification.
|
|
4
|
+
|
|
5
|
+
## Metadata
|
|
6
|
+
|
|
7
|
+
| Field | Value |
|
|
8
|
+
|---|---|
|
|
9
|
+
| Module id | `task-state-machine` |
|
|
10
|
+
| Source path(s) | `src/services/task/state-machine` |
|
|
11
|
+
| Doc kind | Module spec |
|
|
12
|
+
| Coverage score | Partial (manifest-authoritative); 15/15 required document fields present |
|
|
13
|
+
| Generated from | `module-spec` @ SDLC template library `0.2.1` |
|
|
14
|
+
| generated_by / approved_by / updated_at | Codex generator / developer-approved follow-up review remediation / 2026-07-21 |
|
|
15
|
+
| Validation status | Follow-up validation passed (independent Claude fallback, 2026-07-21); coverage remains Partial |
|
|
16
|
+
|
|
17
|
+
## Evidence Rules
|
|
18
|
+
Every requirement cites stable source and test file paths. Code/tests are the behavioral referee; routed source text supplies explicit intent and rationale. Missing or contradictory evidence blocks promotion.
|
|
19
|
+
|
|
20
|
+
## Source Material Register
|
|
21
|
+
| Source material | Scope | Decision | Detail location or disposition |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| Reviewed prior module guides and architecture material | overview / architecture / API / tests | used and code-checked | Content is placed by meaning throughout this specification; exact routing remains in the manifest. |
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
Task State Machine is one of nine confirmed Contact Center SDK modules. Own deterministic task lifecycle states, transition guards/actions, typed internal events, and state-derived UI-control availability. Existing reviewed documentation is migrated by meaning and code/tests remain the behavioral referee.
|
|
27
|
+
|
|
28
|
+
## Purpose / Responsibility
|
|
29
|
+
Own deterministic task lifecycle states, transition guards/actions, typed internal events, and state-derived UI-control availability.
|
|
30
|
+
|
|
31
|
+
## Stack
|
|
32
|
+
TypeScript 5.4, XState 5 actors, pure guards, assign actions, Jest 27.
|
|
33
|
+
|
|
34
|
+
## Folder / Package Structure
|
|
35
|
+
```text
|
|
36
|
+
src/services/task/state-machine/
|
|
37
|
+
├── TaskStateMachine.ts
|
|
38
|
+
├── actions.ts
|
|
39
|
+
├── constants.ts
|
|
40
|
+
├── guards.ts
|
|
41
|
+
├── index.ts
|
|
42
|
+
├── types.ts
|
|
43
|
+
├── uiControlsComputer.ts
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Key Files (source of truth)
|
|
47
|
+
| File | Holds |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `src/services/task/state-machine/TaskStateMachine.ts` | Authoritative Task State Machine implementation or contract source. |
|
|
50
|
+
| `src/services/task/state-machine/constants.ts` | Authoritative Task State Machine implementation or contract source. |
|
|
51
|
+
| `src/services/task/state-machine/types.ts` | Authoritative Task State Machine implementation or contract source. |
|
|
52
|
+
| `src/services/task/state-machine/guards.ts` | Authoritative Task State Machine implementation or contract source. |
|
|
53
|
+
| `src/services/task/state-machine/actions.ts` | Authoritative Task State Machine implementation or contract source. |
|
|
54
|
+
| `src/services/task/state-machine/uiControlsComputer.ts` | Authoritative Task State Machine implementation or contract source. |
|
|
55
|
+
|
|
56
|
+
## Public Surface
|
|
57
|
+
| Contract | Availability | Source |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| Task state-machine factory/types | module-internal Task integration | `src/services/task/state-machine/index.ts`, `src/services/task/state-machine/TaskStateMachine.ts` |
|
|
60
|
+
| Guards/actions | internal state-graph implementations | `src/services/task/state-machine/guards.ts`, `src/services/task/state-machine/actions.ts` |
|
|
61
|
+
| `getDefaultUIControls` | exported from `uiControlsComputer.ts` and re-exported directly by package root; not exported by `state-machine/index.ts` | `src/services/task/state-machine/uiControlsComputer.ts`, `src/index.ts` |
|
|
62
|
+
| `computeVoiceInteractionUIControls` / `computeDigitalInteractionUIControls` | private helpers; not public APIs | `src/services/task/state-machine/uiControlsComputer.ts` |
|
|
63
|
+
|
|
64
|
+
See root `CONTRACTS.md` for the package-level state-control export.
|
|
65
|
+
|
|
66
|
+
## Requires (dependencies)
|
|
67
|
+
- XState
|
|
68
|
+
- TaskData and task-event contracts
|
|
69
|
+
- Task and TaskManager event/action integration
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence |
|
|
73
|
+
|---|---|---|---|---|---|---|
|
|
74
|
+
| TASK_STATE_MACHINE-R-001 | Map typed Task events through the XState graph and preserve guards/actions for offer, assignment, consult, conference, transfer, wrapup, termination, and hydration. | A deterministic event vocabulary isolates lifecycle policy from transport payloads. | `src/services/task/state-machine/TaskStateMachine.ts` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
|
|
75
|
+
| TASK_STATE_MACHINE-R-002 | Keep `handleConferenceFailed`, `handleSwitchToMainCall`, and `handleSwitchToConsult` wired where the graph invokes them; retain `forceConsultInitiator` as defined-but-currently-unwired. | Incorrect absence/wiring claims cause maintainers to duplicate or remove real actions. | `src/services/task/state-machine/actions.ts` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
|
|
76
|
+
| TASK_STATE_MACHINE-R-003 | Treat `syncTaskDataFromEvent` as a Task-supplied machine implementation, not a default action in `actions.ts`. | The reusable graph declares the action name while Task owns integration-specific data synchronization. | `src/services/task/Task.ts` | `test/unit/spec/services/task/Task.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
|
|
77
|
+
| TASK_STATE_MACHINE-R-004 | Compute UI controls through the real private voice/digital helper names and preserve the public `getDefaultUIControls` shape. | Applications depend on stable control state while implementation helpers remain private. | `src/services/task/state-machine/uiControlsComputer.ts` | `test/unit/spec/services/task/state-machine/uiControlsComputer.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
|
|
78
|
+
| TASK_STATE_MACHINE-R-005 | Keep authentication and credentials outside the state-machine layer; it receives typed Task data/events and never invokes authenticated transport. | Pure transition logic remains reusable and cannot leak or mutate host authentication state. | `src/services/task/state-machine/TaskStateMachine.ts`, `src/services/task/state-machine/types.ts` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | None; security/auth applicability is explicitly N/A. | PRESENT |
|
|
79
|
+
| TASK_STATE_MACHINE-R-006 | Treat `UIControlConfig` values as Task-supplied capability configuration, not rollout flags evaluated or owned by the state machine. | Rollout and profile policy must be resolved before actor construction so transitions remain deterministic. | `src/services/task/state-machine/types.ts`, `src/services/task/Task.ts` | `test/unit/spec/services/task/Task.ts` | None; rollout ownership is explicit. | PRESENT |
|
|
80
|
+
| TASK_STATE_MACHINE-R-007 | Keep logging and metrics in Task/TaskManager integration; the state-machine implementation has no LoggerProxy or MetricsManager dependency. | Separating observability side effects from guards/actions preserves deterministic transition tests. | `src/services/task/state-machine/TaskStateMachine.ts`, `src/services/task/Task.ts` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/Task.ts` | None; observability ownership is explicit. | PRESENT |
|
|
81
|
+
|
|
82
|
+
## Design Overview
|
|
83
|
+
TaskManager maps Contact Center notifications to `TaskEvent` values. Each Task sends those events to its XState actor built by `createTaskStateMachine()`. The configuration applies guards and named actions, updates `TaskContext`, and computes UI controls. Task supplies the integration-specific `syncTaskDataFromEvent` implementation through machine options; it is not a default action in `actions.ts`.
|
|
84
|
+
|
|
85
|
+
## Data Flow
|
|
86
|
+
```mermaid
|
|
87
|
+
flowchart LR
|
|
88
|
+
WS[Contact Center notification] --> TM[TaskManager maps to TaskEvent]
|
|
89
|
+
TM --> Task[Owning Task instance]
|
|
90
|
+
Task --> Actor[XState task actor]
|
|
91
|
+
Actor --> Guards[guards.ts evaluates transition]
|
|
92
|
+
Guards --> Actions[actions.ts mutates TaskContext and emits]
|
|
93
|
+
Task --> Sync[Task-provided syncTaskDataFromEvent]
|
|
94
|
+
Actions --> UI[computeUIControls]
|
|
95
|
+
UI --> Events[Task UI controls and TASK_EVENTS]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Sequence Diagram(s)
|
|
99
|
+
Sequence coverage:
|
|
100
|
+
|
|
101
|
+
| Operation group | Diagram | Failure / recovery coverage |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| Offer and assignment | `TASK_INCOMING` enters OFFERED; `TASK_OFFERED` updates that state and `ASSIGN` selects CONNECTED or CONSULTING. | RONA/invite/assign failures terminate the offered task; outbound failure may wrap up instead. |
|
|
104
|
+
| Hold/resume | `HOLD_INITIATED`/`UNHOLD_INITIATED` enter intermediate states; success/failure selects HELD or CONNECTED. | Failure transitions update task data and restore the stable state; Voice owns the thrown error and failure metric. |
|
|
105
|
+
| Consult | `CONSULT` enters `CONSULT_INITIATING`; local and backend success/failure/end events select CONSULTING, HELD, CONNECTED, CONFERENCING, WRAPPING_UP, or TERMINATED. | Consult failure/end actions update and clear consult context according to guards. |
|
|
106
|
+
| Conference/transfer | `MERGE_TO_CONFERENCE` and `CONFERENCE_START` use `CONF_INITIATING`/`CONFERENCING`; transfer uses the `TRANSFER_*` events. | `handleConferenceFailed` and transfer failure paths preserve or clear the relevant conference context. |
|
|
107
|
+
| Wrapup/termination | End/wrapup events select `WRAPPING_UP`, `COMPLETED`, or `TERMINATED`. | Terminal states are final and cannot accept normal interaction transitions. |
|
|
108
|
+
| Hydrate/recovery | IDLE `HYDRATE` guards restore WRAPPING_UP, CONSULTING, HELD, CONNECTED, or CONFERENCING; otherwise remain IDLE. | Root-level HYDRATE on an active task updates data without incorrectly re-entering a child state. |
|
|
109
|
+
|
|
110
|
+
### Offer and assignment
|
|
111
|
+
|
|
112
|
+
```mermaid
|
|
113
|
+
sequenceDiagram
|
|
114
|
+
participant TM as TaskManager
|
|
115
|
+
participant Task
|
|
116
|
+
participant Actor as XState actor
|
|
117
|
+
participant Guard as guards.ts
|
|
118
|
+
participant Action as actions.ts / Task override
|
|
119
|
+
TM->>Task: AgentContactReserved mapping
|
|
120
|
+
Task->>Actor: send(TASK_INCOMING)
|
|
121
|
+
Actor->>Action: initializeTask + emit incoming/reservation
|
|
122
|
+
Actor-->>Task: OFFERED
|
|
123
|
+
alt offer update then assignment
|
|
124
|
+
Task->>Actor: TASK_OFFERED then ASSIGN
|
|
125
|
+
Actor->>Action: update data + emit offer/assignment
|
|
126
|
+
Actor-->>Task: CONNECTED or guarded CONSULTING
|
|
127
|
+
else RONA/invite/assign failure
|
|
128
|
+
Actor->>Action: failure cleanup/event
|
|
129
|
+
Actor-->>Task: TERMINATED
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Hold and resume
|
|
134
|
+
|
|
135
|
+
```mermaid
|
|
136
|
+
sequenceDiagram
|
|
137
|
+
participant Task
|
|
138
|
+
participant Actor as XState actor
|
|
139
|
+
participant Action as actions.ts
|
|
140
|
+
participant UI as uiControlsComputer
|
|
141
|
+
Task->>Actor: HOLD_INITIATED or UNHOLD_INITIATED
|
|
142
|
+
Actor-->>Task: HOLD_INITIATING or RESUME_INITIATING
|
|
143
|
+
alt success event
|
|
144
|
+
Task->>Actor: HOLD_SUCCESS or UNHOLD_SUCCESS
|
|
145
|
+
Actor->>Action: update media hold state; emit task event
|
|
146
|
+
Actor->>UI: compute controls for HELD/CONNECTED
|
|
147
|
+
Actor-->>Task: HELD or CONNECTED
|
|
148
|
+
else failure event
|
|
149
|
+
Task->>Actor: HOLD_FAILED or UNHOLD_FAILED
|
|
150
|
+
Actor->>Action: update task data
|
|
151
|
+
Actor-->>Task: CONNECTED or HELD
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Consult
|
|
156
|
+
|
|
157
|
+
```mermaid
|
|
158
|
+
sequenceDiagram
|
|
159
|
+
participant Task
|
|
160
|
+
participant Actor as XState actor
|
|
161
|
+
participant Guard as guards.ts
|
|
162
|
+
participant Action as actions.ts / Task override
|
|
163
|
+
Task->>Actor: CONSULT
|
|
164
|
+
Actor->>Guard: validate consult capability/state
|
|
165
|
+
Actor-->>Task: CONSULT_INITIATING
|
|
166
|
+
alt local request or backend consult succeeds
|
|
167
|
+
Task->>Actor: CONSULT_SUCCESS / CONSULT_CREATED / CONSULTING_ACTIVE
|
|
168
|
+
Actor->>Action: syncTaskDataFromEvent + consult context actions
|
|
169
|
+
Actor-->>Task: CONSULTING
|
|
170
|
+
else consult failed/ended/cancelled
|
|
171
|
+
Task->>Actor: failure/end event
|
|
172
|
+
Actor->>Action: clear/update consult context
|
|
173
|
+
Actor-->>Task: HELD, CONNECTED, or termination path
|
|
174
|
+
end
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Conference and transfer
|
|
178
|
+
|
|
179
|
+
```mermaid
|
|
180
|
+
sequenceDiagram
|
|
181
|
+
participant Task
|
|
182
|
+
participant Actor as XState actor
|
|
183
|
+
participant Action as actions.ts
|
|
184
|
+
Task->>Actor: MERGE_TO_CONFERENCE or TRANSFER_CONFERENCE
|
|
185
|
+
Actor-->>Task: CONF_INITIATING
|
|
186
|
+
alt conference/transfer succeeds
|
|
187
|
+
Task->>Actor: CONFERENCE_START / TRANSFER_CONFERENCE_SUCCESS
|
|
188
|
+
Actor->>Action: handleSwitchToMainCall or handleSwitchToConsult where wired
|
|
189
|
+
Actor-->>Task: CONFERENCING or final transferred context
|
|
190
|
+
else conference/transfer fails
|
|
191
|
+
Task->>Actor: CONFERENCE_FAILED / TRANSFER_CONFERENCE_FAILED
|
|
192
|
+
Actor->>Action: handleConferenceFailed / matching failure action
|
|
193
|
+
Actor-->>Task: preserve correct main/consult call and stable state
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Wrapup and termination
|
|
198
|
+
|
|
199
|
+
```mermaid
|
|
200
|
+
sequenceDiagram
|
|
201
|
+
participant TM as TaskManager
|
|
202
|
+
participant Task
|
|
203
|
+
participant Actor as XState actor
|
|
204
|
+
TM->>Task: end/wrapup/backend terminal event
|
|
205
|
+
Task->>Actor: mapped TaskEvent
|
|
206
|
+
alt wrapup required
|
|
207
|
+
Actor-->>Task: WRAPPING_UP
|
|
208
|
+
Task->>Actor: WRAPUP_COMPLETE
|
|
209
|
+
Actor-->>Task: COMPLETED
|
|
210
|
+
else contact terminated/no wrapup
|
|
211
|
+
Actor-->>Task: TERMINATED or COMPLETED
|
|
212
|
+
end
|
|
213
|
+
Note over Actor: terminal states do not accept normal interaction transitions
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Hydrate and recovery
|
|
217
|
+
|
|
218
|
+
```mermaid
|
|
219
|
+
sequenceDiagram
|
|
220
|
+
participant TM as TaskManager
|
|
221
|
+
participant Task
|
|
222
|
+
participant Actor as XState actor
|
|
223
|
+
participant Guard as guards.ts
|
|
224
|
+
participant Action as Task sync override
|
|
225
|
+
TM->>Task: HYDRATE(taskData)
|
|
226
|
+
Task->>Actor: send(HYDRATE)
|
|
227
|
+
alt actor is IDLE
|
|
228
|
+
Actor->>Guard: choose backend-represented state
|
|
229
|
+
Guard-->>Actor: WRAPPING_UP/CONSULTING/HELD/CONNECTED/CONFERENCING or default IDLE
|
|
230
|
+
Actor->>Action: initialize synchronized task data
|
|
231
|
+
else actor already active
|
|
232
|
+
Actor->>Action: syncTaskDataFromEvent
|
|
233
|
+
Actor-->>Task: retain current child state with updated context
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Class / Component Relationships
|
|
238
|
+
```mermaid
|
|
239
|
+
classDiagram
|
|
240
|
+
class TaskManager
|
|
241
|
+
class Task
|
|
242
|
+
class TaskStateMachine
|
|
243
|
+
class Guards
|
|
244
|
+
class Actions
|
|
245
|
+
class UIControlsComputer
|
|
246
|
+
TaskManager --> Task : sends mapped events
|
|
247
|
+
Task --> TaskStateMachine : owns configured actor
|
|
248
|
+
TaskStateMachine --> Guards : evaluates
|
|
249
|
+
TaskStateMachine --> Actions : executes
|
|
250
|
+
Task --> Actions : provides syncTaskDataFromEvent
|
|
251
|
+
TaskStateMachine --> UIControlsComputer : recomputes controls
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Use Cases
|
|
255
|
+
- **UC-1 Offer and assignment:** map reserved/offer/assigned notifications to typed events and enter OFFERED or CONNECTED with updated task data. Evidence: `src/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/state-machine`.
|
|
256
|
+
- **UC-2 Hold/resume:** represent the initiating operation separately, then settle into HELD or CONNECTED from success/failure events. Evidence: `src/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/state-machine`.
|
|
257
|
+
- **UC-3 Consult:** retain main/consult context while moving through CONSULT_INITIATING and CONSULTING; use the Task-supplied synchronization action for ownership changes. Evidence: `src/services/task/Task.ts`, `test/unit/spec/services/task/Task.ts`.
|
|
258
|
+
- **UC-4 Conference/transfer:** execute conference and switch actions only where the graph wires them; `forceConsultInitiator` remains defined but unwired. Evidence: `src/services/task/state-machine/actions.ts`, `src/services/task/state-machine/TaskStateMachine.ts`.
|
|
259
|
+
- **UC-5 Wrapup/termination:** select WRAPPING_UP, COMPLETED, or TERMINATED from backend state and wrapup requirements. Evidence: `src/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/state-machine`.
|
|
260
|
+
- **UC-6 Hydrate/recovery:** use IDLE HYDRATE guards to restore backend state while active-task hydration updates context without a child-state transition. Evidence: `src/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/state-machine`.
|
|
261
|
+
|
|
262
|
+
## State Model
|
|
263
|
+
The actor starts at `IDLE`. Normal interaction paths cover `OFFERED`, `CONNECTED`, `HELD`, `CONSULTING`, and `CONFERENCING`, with explicit initiating states for hold, resume, consult, and conference. `WRAPPING_UP` precedes final `COMPLETED` or `TERMINATED` outcomes. Backend task data remains authoritative for HYDRATE recovery.
|
|
264
|
+
|
|
265
|
+
## Business Rules & Invariants
|
|
266
|
+
- Every transition event is a declared `TaskEvent`; raw Contact Center messages are mapped by TaskManager before actor delivery.
|
|
267
|
+
- `syncTaskDataFromEvent` is supplied by Task machine options; it must not be claimed as a default `actions.ts` implementation.
|
|
268
|
+
- `handleConferenceFailed`, `handleSwitchToMainCall`, and `handleSwitchToConsult` are wired actions. `forceConsultInitiator` is defined but currently unwired.
|
|
269
|
+
- `didInitiateConsult` is defined but currently unwired and must not be described as an active guard.
|
|
270
|
+
- Security/auth applicability is N/A inside the pure state-machine layer: it has no credential or transport dependency.
|
|
271
|
+
- `UIControlConfig` is supplied by Task as resolved capability configuration; the state machine owns no rollout/feature-flag evaluation.
|
|
272
|
+
- Observability is owned by Task/TaskManager; state-machine guards/actions remain free of LoggerProxy and MetricsManager dependencies.
|
|
273
|
+
|
|
274
|
+
## Concurrency & Reactive Flow
|
|
275
|
+
- TaskManager serially maps each backend notification to a Task event; the actor applies guards/actions synchronously for that event, while remote operation completion arrives as later mapped events.
|
|
276
|
+
- UI controls are recomputed from the resulting context/state and emitted only through the owning Task integration.
|
|
277
|
+
|
|
278
|
+
## State Machine
|
|
279
|
+
```mermaid
|
|
280
|
+
stateDiagram-v2
|
|
281
|
+
[*] --> IDLE
|
|
282
|
+
IDLE --> OFFERED: TASK_OFFERED / OFFER_CONSULT
|
|
283
|
+
IDLE --> CONNECTED: ASSIGN or HYDRATE connected
|
|
284
|
+
OFFERED --> CONNECTED: ASSIGN
|
|
285
|
+
CONNECTED --> HOLD_INITIATING: HOLD_INITIATED
|
|
286
|
+
HOLD_INITIATING --> HELD: HOLD_SUCCESS
|
|
287
|
+
HOLD_INITIATING --> CONNECTED: HOLD_FAILED
|
|
288
|
+
HELD --> RESUME_INITIATING: UNHOLD_INITIATED
|
|
289
|
+
RESUME_INITIATING --> CONNECTED: UNHOLD_SUCCESS
|
|
290
|
+
HELD --> CONSULT_INITIATING: CONSULT
|
|
291
|
+
CONNECTED --> CONSULT_INITIATING: CONSULT
|
|
292
|
+
CONSULT_INITIATING --> CONSULTING: CONSULT_SUCCESS / CONSULTING_ACTIVE
|
|
293
|
+
CONSULTING --> CONF_INITIATING: MERGE_TO_CONFERENCE
|
|
294
|
+
CONF_INITIATING --> CONFERENCING: CONFERENCE_START
|
|
295
|
+
CONNECTED --> WRAPPING_UP: TASK_WRAPUP when required
|
|
296
|
+
HELD --> WRAPPING_UP: TASK_WRAPUP when required
|
|
297
|
+
WRAPPING_UP --> COMPLETED: WRAPUP_COMPLETE
|
|
298
|
+
CONNECTED --> TERMINATED: CONTACT_ENDED without wrapup
|
|
299
|
+
COMPLETED --> [*]
|
|
300
|
+
TERMINATED --> [*]
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Guide AI agents working on task lifecycle transitions, guard logic, executable actions and UI control computation in the XState-based task state machine.
|
|
304
|
+
|
|
305
|
+
This guide is for internal state management for the task lifecycle in:
|
|
306
|
+
|
|
307
|
+
- State machine configuration: `TaskStateMachine.ts`
|
|
308
|
+
|
|
309
|
+
- Actions and context mutation: `actions.ts`
|
|
310
|
+
|
|
311
|
+
- Guard logic: `guards.ts`
|
|
312
|
+
|
|
313
|
+
- UI control computation: `uiControlsComputer.ts`
|
|
314
|
+
|
|
315
|
+
- Event types and payloads: `constants.ts`, `types.ts`
|
|
316
|
+
|
|
317
|
+
Use this doc when implementing:
|
|
318
|
+
|
|
319
|
+
- new state transitions
|
|
320
|
+
|
|
321
|
+
- event mapping and payload extensions
|
|
322
|
+
|
|
323
|
+
- guard/action fixes
|
|
324
|
+
|
|
325
|
+
- UI control behavior changes tied to task state
|
|
326
|
+
|
|
327
|
+
```text
|
|
328
|
+
state-machine/
|
|
329
|
+
├── TaskStateMachine.ts # State graph and transition configuration
|
|
330
|
+
├── actions.ts # Assign actions and emitter placeholders
|
|
331
|
+
├── guards.ts # Pure guard predicates
|
|
332
|
+
├── uiControlsComputer.ts # Voice/Digital UI control computation
|
|
333
|
+
├── constants.ts # TaskState, TaskEvent, machine constants
|
|
334
|
+
├── types.ts # Context and typed event payload map
|
|
335
|
+
├── index.ts # Public exports
|
|
336
|
+
└── ai-docs/
|
|
337
|
+
├── AGENTS.md # AI coding guide
|
|
338
|
+
└── ARCHITECTURE.md # State machine architecture guide
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
- **State Graph and Transition Rules**: `TaskStateMachine.ts` defines all states, transition tables, and event handlers that drive the task lifecycle.
|
|
342
|
+
|
|
343
|
+
- **Deterministic Context Updates**: `actions.ts` implements XState actions for task context mutation and provides emitter placeholders that `Task` overrides to surface SDK events.
|
|
344
|
+
|
|
345
|
+
- **Transition Eligibility**: `guards.ts` contains pure predicates that gate transitions based on current context, task data, and backend state.
|
|
346
|
+
|
|
347
|
+
- **UI Controls Computation**: `uiControlsComputer.ts` derives `TaskUIControls` from state and context for voice/digital channels, keeping UI enablement centralized.
|
|
348
|
+
|
|
349
|
+
- **Typed Event Contracts**: `constants.ts` and `types.ts` define `TaskState`, `TaskEvent`, and the `TaskEventPayloadMap` so transitions and payloads stay type-safe.
|
|
350
|
+
|
|
351
|
+
- **Public Exports**: `index.ts` exposes the state machine factory, event enums, and types for consumption by the task layer.
|
|
352
|
+
|
|
353
|
+
**Transition Source**: `getTaskStateMachineConfig()` in `TaskStateMachine.ts`
|
|
354
|
+
|
|
355
|
+
API-driven transition from `voice/Voice.ts`:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
// task.hold() / task.resume() -> holdResume()
|
|
359
|
+
stateMachineService.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId});
|
|
360
|
+
// ... backend call succeeds
|
|
361
|
+
stateMachineService.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Backend-driven transition from `TaskManager.ts`:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
const eventPayload = TaskManager.mapEventToTaskStateMachineEvent(
|
|
368
|
+
CC_EVENTS.AGENT_CONTACT_RESERVED,
|
|
369
|
+
taskData
|
|
370
|
+
);
|
|
371
|
+
if (eventPayload) {
|
|
372
|
+
task.sendStateMachineEvent(eventPayload);
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Backend CC events from WebSocket are mapped to `TaskEvent` in `TaskManager.mapEventToTaskStateMachineEvent`.
|
|
377
|
+
The state machine consumes only `TaskEvent` and never raw CC events.
|
|
378
|
+
|
|
379
|
+
Source of truth: `TaskEventPayloadMap` in `types.ts`.
|
|
380
|
+
All new events must add a typed payload entry in `TaskEventPayloadMap`.
|
|
381
|
+
|
|
382
|
+
- API contracts for external services.
|
|
383
|
+
|
|
384
|
+
- Mercury or CC WebSocket protocols (see `TaskManager.ts` mapping).
|
|
385
|
+
|
|
386
|
+
Guards are boolean conditions that determine determine if a state transition is allowed. These functions validate the current context before allowing transitions.
|
|
387
|
+
|
|
388
|
+
- Guards must be pure and must return boolean only
|
|
389
|
+
|
|
390
|
+
- No mutation or side-effects.
|
|
391
|
+
|
|
392
|
+
- Reuse helper accessors (e.g., `getTaskDataFromEvent`).
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
// Check if interaction is in terminated state
|
|
396
|
+
isInteractionTerminated(context, event) {
|
|
397
|
+
return event.taskData?.interaction?.isTerminated === true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Check if interaction is consulting
|
|
401
|
+
isInteractionConsulting(context, event) {
|
|
402
|
+
return event.taskData?.interaction?.state === 'consulting';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Check if interaction is held
|
|
406
|
+
isInteractionHeld(context, event) {
|
|
407
|
+
return event.taskData?.interaction?.state === 'hold';
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Check if interaction is connected
|
|
411
|
+
isInteractionConnected(context, event) {
|
|
412
|
+
return event.taskData?.interaction?.state === 'connected';
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// Check if current agent initiated consult
|
|
418
|
+
didInitiateConsult(context, event) {
|
|
419
|
+
if (event.taskData?.isConsulted === true) return false;
|
|
420
|
+
return event.taskData?.consultingAgentId
|
|
421
|
+
? isSelfConsultingAgent(context, event.taskData)
|
|
422
|
+
: context.consultInitiator === true;
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
// Check if conference is in progress from event taskData
|
|
428
|
+
conferenceInProgressFromEvent(context, event) {
|
|
429
|
+
const taskData = event.taskData;
|
|
430
|
+
if (!taskData?.interaction) return false;
|
|
431
|
+
return getIsConferenceInProgress(taskData);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check if conference is in progress by participants
|
|
435
|
+
isConferencingByParticipants(context, event) {
|
|
436
|
+
const taskData = event.taskData;
|
|
437
|
+
if (!taskData) return false;
|
|
438
|
+
|
|
439
|
+
const mainCallId = taskData.interaction?.mainInteractionId || taskData.interactionId;
|
|
440
|
+
const media = taskData.interaction?.media?.[mainCallId];
|
|
441
|
+
const participants = taskData.interaction?.participants;
|
|
442
|
+
if (!media?.participants || !participants) return false;
|
|
443
|
+
|
|
444
|
+
let agentCount = 0;
|
|
445
|
+
for (const pId of media.participants) {
|
|
446
|
+
const p = participants[pId];
|
|
447
|
+
if (p && p.pType !== 'Customer' && p.pType !== 'Supervisor' && !p.hasLeft) {
|
|
448
|
+
agentCount += 1;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return agentCount >= 2;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Check if conference should downgrade to connected
|
|
456
|
+
shouldDowngradeConferenceToConnected(context, event) {
|
|
457
|
+
const taskData = event.taskData ?? context.taskData;
|
|
458
|
+
if (!taskData?.interaction) return false;
|
|
459
|
+
|
|
460
|
+
const selfAgentId = getSelfAgentId(context, taskData);
|
|
461
|
+
if (!selfAgentId) return false;
|
|
462
|
+
|
|
463
|
+
const mainCallId = taskData?.interaction?.mainInteractionId || taskData?.interactionId;
|
|
464
|
+
if (!mainCallId) return false;
|
|
465
|
+
|
|
466
|
+
// Do not downgrade while backend still reports active conference state
|
|
467
|
+
if (taskData.interaction.state === 'conference') return false;
|
|
468
|
+
|
|
469
|
+
const agentParticipantsCount = getConferenceParticipantsCount(taskData?.interaction, mainCallId);
|
|
470
|
+
if (agentParticipantsCount >= 2) return false;
|
|
471
|
+
|
|
472
|
+
const customerInCall = getIsCustomerInCall(taskData?.interaction, mainCallId);
|
|
473
|
+
if (!customerInCall) return false;
|
|
474
|
+
|
|
475
|
+
const selfInMainCall = Boolean(
|
|
476
|
+
taskData?.interaction?.media?.[mainCallId]?.participants?.includes(selfAgentId)
|
|
477
|
+
);
|
|
478
|
+
return selfInMainCall;
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
// Check if this agent should move to wrapup
|
|
484
|
+
shouldWrapUp(context, event) {
|
|
485
|
+
const taskData = event.taskData;
|
|
486
|
+
if (!taskData) return false;
|
|
487
|
+
|
|
488
|
+
if (event.type === TaskEvent.CONFERENCE_END) {
|
|
489
|
+
const selfAgentId = getSelfAgentId(context, taskData);
|
|
490
|
+
if (!selfAgentId) return false;
|
|
491
|
+
|
|
492
|
+
const pending = taskData.agentsPendingWrapUp;
|
|
493
|
+
if (Array.isArray(pending) && pending.length > 0) {
|
|
494
|
+
return pending.includes(selfAgentId);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const participantWrapUp = taskData.interaction?.participants?.[selfAgentId]?.isWrapUp === true;
|
|
498
|
+
const wrapUpRequired = taskData.wrapUpRequired === true;
|
|
499
|
+
return wrapUpRequired || participantWrapUp;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return shouldWrapUpForThisAgent(context, taskData);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Check if wrapup is required OR current agent is consult initiator
|
|
506
|
+
shouldWrapUpOrIsInitiator(context, event) {
|
|
507
|
+
return Boolean(event.taskData?.wrapUpRequired || context.consultInitiator);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Check whether the leaving participant is the current agent
|
|
511
|
+
didCurrentAgentLeaveConference(context, event) {
|
|
512
|
+
const selfAgentId = getSelfAgentId(context, event.taskData);
|
|
513
|
+
if (!selfAgentId) return false;
|
|
514
|
+
|
|
515
|
+
const participantIdFromEvent = 'participantId' in event ? event.participantId : undefined;
|
|
516
|
+
const participantId = participantIdFromEvent ?? event.taskData?.participantId;
|
|
517
|
+
return Boolean(participantId) && participantId === selfAgentId;
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
// Check if primary media leg is on hold
|
|
523
|
+
isPrimaryMediaOnHold(context, event) {
|
|
524
|
+
const taskData = event.taskData;
|
|
525
|
+
if (!taskData || !taskData.mediaResourceId) return false;
|
|
526
|
+
|
|
527
|
+
return taskData.interaction?.media?.[taskData.mediaResourceId]?.isHold === true;
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Actions are side effects executed during state machine transitions from current state to target state(next state).
|
|
532
|
+
Actions contain:
|
|
533
|
+
|
|
534
|
+
- Context synchronization (`initializeTask`, `updateTaskData`, `syncTaskDataFromEvent`)
|
|
535
|
+
|
|
536
|
+
- Lifecycle mutations (`clearConsultState`, `markEnded`, consult/conference flags)
|
|
537
|
+
|
|
538
|
+
- Integration hooks (`requestAutoAnswer`, `requestCleanup`, emitter placeholders)
|
|
539
|
+
|
|
540
|
+
- Context mutations should be centralized in `assign(...)` actions
|
|
541
|
+
|
|
542
|
+
- Emitter actions intentionally no-op defaults and overridden by `Task` to bridge machine transitions to SDK events.
|
|
543
|
+
|
|
544
|
+
- Deterministic updates from `taskData`.
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
// Initialize context for incoming task
|
|
548
|
+
initializeTask(context, event) {
|
|
549
|
+
return {
|
|
550
|
+
consultInitiator: false,
|
|
551
|
+
exitingConference: false,
|
|
552
|
+
consultDestinationType: null,
|
|
553
|
+
consultDestinationAgentJoined: false,
|
|
554
|
+
...deriveTaskDataUpdates(context, event.taskData),
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Update taskData + derived recording/consult fields
|
|
559
|
+
updateTaskData(context, event) {
|
|
560
|
+
return deriveTaskDataUpdates(context, event.taskData);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Keep Task instance data in sync (Task.ts action override)
|
|
564
|
+
syncTaskDataFromEvent(event) {
|
|
565
|
+
this.updateTaskFromEvent(event);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Update hold flag on specific media leg in context.taskData.interaction.media
|
|
569
|
+
setHoldState(context, event) {
|
|
570
|
+
// Handles HOLD_SUCCESS and UNHOLD_SUCCESS for event.mediaResourceId
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Conference/consult lifecycle mutators
|
|
574
|
+
handleConferenceStarted() { return {consultInitiator: false}; }
|
|
575
|
+
handleConsultFailed() { return {consultDestinationAgentJoined: false, consultInitiator: false}; }
|
|
576
|
+
handleParticipantLeft(event) { return event.taskData ? {taskData: event.taskData} : {}; }
|
|
577
|
+
handleTransferConferenceSuccess(event) { return event.taskData ? {taskData: event.taskData} : {}; }
|
|
578
|
+
|
|
579
|
+
// Consult destination and mode flags
|
|
580
|
+
setConsultDestination(event) { /* sets consultDestinationType and resets consult flags */ }
|
|
581
|
+
setConsultFromConference() { return {consultFromConference: true}; }
|
|
582
|
+
setConsultAgentJoined(event) { /* sets consultDestinationAgentJoined on CONSULTING_ACTIVE */ }
|
|
583
|
+
setExitingConference() { return {exitingConference: true}; }
|
|
584
|
+
|
|
585
|
+
// Conference transfer flags
|
|
586
|
+
setTransferConferenceRequested() { return {transferConferenceRequested: true}; }
|
|
587
|
+
clearTransferConferenceRequested() { return {transferConferenceRequested: false}; }
|
|
588
|
+
|
|
589
|
+
// Consult call hold flags
|
|
590
|
+
setConsultCallHeld() { return {consultCallHeld: true}; }
|
|
591
|
+
clearConsultCallHeld() { return {consultCallHeld: false}; }
|
|
592
|
+
|
|
593
|
+
// Recording state mutator for pause/resume events
|
|
594
|
+
setRecordingState(event) {
|
|
595
|
+
// PAUSE_RECORDING => recordingInProgress false
|
|
596
|
+
// RESUME_RECORDING => recordingInProgress true
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Reset consult/conference-related context
|
|
600
|
+
clearConsultState() {
|
|
601
|
+
return {
|
|
602
|
+
consultDestinationType: null,
|
|
603
|
+
consultDestinationAgentJoined: false,
|
|
604
|
+
consultInitiator: false,
|
|
605
|
+
exitingConference: false,
|
|
606
|
+
consultCallHeld: false,
|
|
607
|
+
consultFromConference: false,
|
|
608
|
+
transferConferenceRequested: false,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// End-of-task cleanup for recording flags
|
|
613
|
+
markEnded() {
|
|
614
|
+
return {recordingControlsAvailable: false, recordingInProgress: false};
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
> `handleConferenceFailed`, `handleSwitchToMainCall`, and `handleSwitchToConsult` are defined in `actions.ts` and wired by `TaskStateMachine.ts`. `forceConsultInitiator` is defined in `actions.ts` but is not wired in the current graph.
|
|
619
|
+
|
|
620
|
+
```typescript
|
|
621
|
+
// Emit task incoming
|
|
622
|
+
emitTaskIncoming(context, event) {
|
|
623
|
+
task.emit(TASK_EVENTS.TASK_INCOMING, task);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Emit task assigned
|
|
627
|
+
emitTaskAssigned(context, event) {
|
|
628
|
+
task.emit(TASK_EVENTS.TASK_ASSIGNED, task);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Emit task hold
|
|
632
|
+
emitTaskHold(context, event) {
|
|
633
|
+
task.emit(TASK_EVENTS.TASK_HOLD, task);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Emit task wrapup
|
|
637
|
+
emitTaskWrapup(context, event) {
|
|
638
|
+
if (context.taskData.wrapUpRequired) {
|
|
639
|
+
task.emit(TASK_EVENTS.TASK_WRAPUP, task);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ... more emission actions for each event type
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
// NOTE: These are no-op placeholders in actions.ts and are overridden in Task.ts.
|
|
648
|
+
|
|
649
|
+
// Request cleanup (remove from collection, keep task object)
|
|
650
|
+
requestCleanup(context, event) {
|
|
651
|
+
task.emit(TASK_EVENTS.TASK_CLEANUP, task, {removeFromCollection: false});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Cleanup resources (remove from collection)
|
|
655
|
+
cleanupResources(context, event) {
|
|
656
|
+
task.emit(TASK_EVENTS.TASK_CLEANUP, task, {removeFromCollection: true});
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
// NOTE: requestAutoAnswer is a placeholder in actions.ts and is overridden in Task.ts.
|
|
662
|
+
|
|
663
|
+
// Request auto-answer
|
|
664
|
+
requestAutoAnswer(context, event) {
|
|
665
|
+
if (event.taskData?.isAutoAnswering) {
|
|
666
|
+
// Trigger accept() method
|
|
667
|
+
autoAnswerIfNeeded();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
`uiControlsComputer.ts` computes `TaskUIControls` from:
|
|
673
|
+
|
|
674
|
+
- current machine state
|
|
675
|
+
|
|
676
|
+
- current context
|
|
677
|
+
|
|
678
|
+
- channel type (voice vs digital)
|
|
679
|
+
|
|
680
|
+
- call/participant metadata from `taskData`
|
|
681
|
+
|
|
682
|
+
- config flags (`isEndTaskEnabled`, recording toggles, voice variant)
|
|
683
|
+
|
|
684
|
+
This keeps all control enablement/visibility logic centralized and testable.
|
|
685
|
+
|
|
686
|
+
- `TaskState`
|
|
687
|
+
|
|
688
|
+
- `TaskContext` (including `taskData`)
|
|
689
|
+
|
|
690
|
+
- `UIControlConfig` (channel type, agentId, voice variant, recording flags)
|
|
691
|
+
|
|
692
|
+
- `TaskUIControls` with per-control visibility and enabled state.
|
|
693
|
+
|
|
694
|
+
1. Add event in `TaskEvent` (`constants.ts`)
|
|
695
|
+
|
|
696
|
+
2. Add typed payload in `TaskEventPayloadMap` (`types.ts`)
|
|
697
|
+
|
|
698
|
+
3. Wire transitions in `TaskStateMachine.ts`
|
|
699
|
+
|
|
700
|
+
4. Add/adjust actions in `actions.ts`
|
|
701
|
+
|
|
702
|
+
5. Add guard(s) in `guards.ts` if needed
|
|
703
|
+
|
|
704
|
+
6. Update `TaskManager` event mapping and unit tests
|
|
705
|
+
|
|
706
|
+
1. Implement pure guard in `guards.ts`
|
|
707
|
+
|
|
708
|
+
2. Use guard in `TaskStateMachine.ts` transition array
|
|
709
|
+
|
|
710
|
+
3. Keep side-effects in actions only (no side-effects in guards)
|
|
711
|
+
|
|
712
|
+
4. Add tests for positive and negative transition paths
|
|
713
|
+
|
|
714
|
+
1. Update control logic in `computeVoiceInteractionUIControls()` or `computeDigitalInteractionUIControls()`
|
|
715
|
+
|
|
716
|
+
2. Preserve `getDefaultUIControls()` shape compatibility
|
|
717
|
+
|
|
718
|
+
3. Verify behavior across `CONNECTED`, `HELD`, `CONSULTING`, `CONFERENCING`, `WRAPPING_UP`
|
|
719
|
+
|
|
720
|
+
4. Add or update UI-control unit coverage
|
|
721
|
+
|
|
722
|
+
Technical reference for the complete state machine using XState to drive state transitions and UI control behavior. It orchestrates state transitions, guards, and actions for task lifecycle management.
|
|
723
|
+
|
|
724
|
+
The task state machine is built with `xstate` and organized into:
|
|
725
|
+
|
|
726
|
+
- **State graph** (`TaskStateMachine.ts`)
|
|
727
|
+
|
|
728
|
+
- **Context mutators** (`actions.ts`)
|
|
729
|
+
|
|
730
|
+
- **Guard predicates** (`guards.ts`)
|
|
731
|
+
|
|
732
|
+
- **UI control derivation** (`uiControlsComputer.ts`)
|
|
733
|
+
|
|
734
|
+
- **Event/context contracts** (`types.ts`)
|
|
735
|
+
|
|
736
|
+
It is instantiated by `Task` and receives mapped backend/user events through `sendStateMachineEvent(...)`.
|
|
737
|
+
|
|
738
|
+
`Task` bootstraps and owns the actor lifecycle:
|
|
739
|
+
|
|
740
|
+
1. `createTaskStateMachine(uiControlConfig, {actions: overrides})`
|
|
741
|
+
|
|
742
|
+
2. `createActor(machine).start()`
|
|
743
|
+
|
|
744
|
+
3. `TaskManager` and task APIs map external signals to `TaskEvent`
|
|
745
|
+
|
|
746
|
+
4. Actor transitions update context and execute action overrides
|
|
747
|
+
|
|
748
|
+
5. `Task` recomputes UI controls and emits task-level events
|
|
749
|
+
|
|
750
|
+
**Description**: Initial state before a task is offered or restored.
|
|
751
|
+
|
|
752
|
+
**How this state is reached (incoming transitions)**:
|
|
753
|
+
|
|
754
|
+
- Machine start -> `IDLE` (no event, no actions)
|
|
755
|
+
|
|
756
|
+
**Valid transitions from `IDLE` state**:
|
|
757
|
+
|
|
758
|
+
- `TASK_INCOMING` -> `OFFERED`
|
|
759
|
+
|
|
760
|
+
- Guard: none
|
|
761
|
+
|
|
762
|
+
- Actions: `initializeTask`, `emitTaskIncoming`
|
|
763
|
+
|
|
764
|
+
- `HYDRATE` -> `WRAPPING_UP`
|
|
765
|
+
|
|
766
|
+
- Guard: `guards.isInteractionTerminated`
|
|
767
|
+
|
|
768
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskHydrate`
|
|
769
|
+
|
|
770
|
+
- `HYDRATE` -> `CONSULTING`
|
|
771
|
+
|
|
772
|
+
- Guard: `guards.isInteractionConsulting`
|
|
773
|
+
|
|
774
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
775
|
+
|
|
776
|
+
- `HYDRATE` -> `HELD`
|
|
777
|
+
|
|
778
|
+
- Guard: `guards.isInteractionHeld`
|
|
779
|
+
|
|
780
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
781
|
+
|
|
782
|
+
- `HYDRATE` -> `CONNECTED`
|
|
783
|
+
|
|
784
|
+
- Guard: `guards.isInteractionConnected`
|
|
785
|
+
|
|
786
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
787
|
+
|
|
788
|
+
- `HYDRATE` -> `CONFERENCING`
|
|
789
|
+
|
|
790
|
+
- Guard: `guards.isConferencingByParticipants`
|
|
791
|
+
|
|
792
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
793
|
+
|
|
794
|
+
- `HYDRATE` -> stay `IDLE` (default hydrate branch)
|
|
795
|
+
|
|
796
|
+
- Guard: default
|
|
797
|
+
|
|
798
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
799
|
+
|
|
800
|
+
**Description**: Task has been offered/reserved and is waiting for assignment or termination paths.
|
|
801
|
+
|
|
802
|
+
**How this state is reached (incoming transitions)**:
|
|
803
|
+
|
|
804
|
+
- `IDLE --TASK_INCOMING--> OFFERED`
|
|
805
|
+
|
|
806
|
+
- Guard: none
|
|
807
|
+
|
|
808
|
+
- Actions: `initializeTask`, `emitTaskIncoming`
|
|
809
|
+
|
|
810
|
+
**Valid transitions from `OFFERED`**:
|
|
811
|
+
|
|
812
|
+
- `TASK_OFFERED` -> Stay `OFFERED`
|
|
813
|
+
|
|
814
|
+
- Guard: none
|
|
815
|
+
|
|
816
|
+
- Actions: `updateTaskData`, `emitTaskOfferContact`, `requestAutoAnswer`
|
|
817
|
+
|
|
818
|
+
- `OFFER_CONSULT` -> Stay `OFFERED`
|
|
819
|
+
|
|
820
|
+
- Guard: none
|
|
821
|
+
|
|
822
|
+
- Actions: `updateTaskData`, `emitTaskOfferConsult`, `requestAutoAnswer`
|
|
823
|
+
|
|
824
|
+
- `ASSIGN` -> `CONNECTED`
|
|
825
|
+
|
|
826
|
+
- Guard: none
|
|
827
|
+
|
|
828
|
+
- Actions: `updateTaskData`, `emitTaskAssigned`
|
|
829
|
+
|
|
830
|
+
- `CONSULTING_ACTIVE` -> `CONSULTING`
|
|
831
|
+
|
|
832
|
+
- Guard: none
|
|
833
|
+
|
|
834
|
+
- Actions: `updateTaskData`, `setConsultAgentJoined`, `emitTaskConsultAccepted`, `emitTaskConsulting`
|
|
835
|
+
|
|
836
|
+
- `TASK_WRAPUP` -> `TERMINATED`
|
|
837
|
+
|
|
838
|
+
- Guard: none
|
|
839
|
+
|
|
840
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskEnd`
|
|
841
|
+
|
|
842
|
+
- `RONA` / `ASSIGN_FAILED` / `INVITE_FAILED` / `OUTBOUND_FAILED` -> `TERMINATED`
|
|
843
|
+
|
|
844
|
+
- Guard: none
|
|
845
|
+
|
|
846
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskReject`
|
|
847
|
+
|
|
848
|
+
**Description**: Agent is connected on the main customer interaction leg.
|
|
849
|
+
|
|
850
|
+
**How this state is reached (incoming transitions)**:
|
|
851
|
+
|
|
852
|
+
- `OFFERED --ASSIGN--> CONNECTED`
|
|
853
|
+
|
|
854
|
+
- Guard: none
|
|
855
|
+
|
|
856
|
+
- Actions: `updateTaskData`, `emitTaskAssigned`
|
|
857
|
+
|
|
858
|
+
- `IDLE --HYDRATE--> CONNECTED`
|
|
859
|
+
|
|
860
|
+
- Guard: `guards.isInteractionConnected`
|
|
861
|
+
|
|
862
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
863
|
+
|
|
864
|
+
- `RESUME_INITIATING --UNHOLD_SUCCESS--> CONNECTED`
|
|
865
|
+
|
|
866
|
+
- Guard: none
|
|
867
|
+
|
|
868
|
+
- Actions: `updateTaskData`, `setHoldState`, `emitTaskResume`
|
|
869
|
+
|
|
870
|
+
- `HELD --TRANSFER_SUCCESS--> CONNECTED` (receiver/default branch)
|
|
871
|
+
|
|
872
|
+
- Guard: default branch when `guards.shouldWrapUpOrIsInitiator` is false
|
|
873
|
+
|
|
874
|
+
- Actions: `updateTaskData`, `clearConsultState`
|
|
875
|
+
|
|
876
|
+
- `CONSULTING --ASSIGN--> CONNECTED`
|
|
877
|
+
|
|
878
|
+
- Guard: none
|
|
879
|
+
|
|
880
|
+
- Actions: `updateTaskData`, `emitTaskAssigned`
|
|
881
|
+
|
|
882
|
+
**Valid transitions from `CONNECTED`**:
|
|
883
|
+
|
|
884
|
+
- `ASSIGN` -> `CONNECTED` (self-transition)
|
|
885
|
+
|
|
886
|
+
- Guard: none
|
|
887
|
+
|
|
888
|
+
- Actions: `updateTaskData`, `emitTaskAssigned`
|
|
889
|
+
|
|
890
|
+
- `HOLD_INITIATED` -> `HOLD_INITIATING`
|
|
891
|
+
|
|
892
|
+
- Guard: none
|
|
893
|
+
|
|
894
|
+
- Actions: none
|
|
895
|
+
|
|
896
|
+
- `CONSULT` -> `CONSULT_INITIATING`
|
|
897
|
+
|
|
898
|
+
- Guard: none
|
|
899
|
+
|
|
900
|
+
- Actions: `setConsultInitiator`, `setConsultDestination`
|
|
901
|
+
|
|
902
|
+
- `CONSULTING_ACTIVE` -> `CONSULTING`
|
|
903
|
+
|
|
904
|
+
- Guard: none
|
|
905
|
+
|
|
906
|
+
- Actions: `updateTaskData`, `setConsultAgentJoined`, `emitTaskConsultAccepted`, `emitTaskConsulting`
|
|
907
|
+
|
|
908
|
+
- `TRANSFER_SUCCESS` -> `WRAPPING_UP`
|
|
909
|
+
|
|
910
|
+
- Guard: `guards.shouldWrapUpOrIsInitiator`
|
|
911
|
+
|
|
912
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskWrapup`
|
|
913
|
+
|
|
914
|
+
- `TRANSFER_SUCCESS` -> Stay `CONNECTED` (receiver/default branch)
|
|
915
|
+
|
|
916
|
+
- Guard: default
|
|
917
|
+
|
|
918
|
+
- Actions: `updateTaskData`, `clearConsultState`
|
|
919
|
+
|
|
920
|
+
- `TRANSFER_FAILED` -> Stay `CONNECTED`
|
|
921
|
+
|
|
922
|
+
- Guard: none
|
|
923
|
+
|
|
924
|
+
- Actions: `updateTaskData`
|
|
925
|
+
|
|
926
|
+
- `CONTACT_ENDED` -> `CONFERENCING`
|
|
927
|
+
|
|
928
|
+
- Guard: `guards.conferenceInProgressFromEvent`
|
|
929
|
+
|
|
930
|
+
- Actions: `updateTaskData`, `emitTaskConferenceStarted`, `requestCleanup`
|
|
931
|
+
|
|
932
|
+
- `CONTACT_ENDED` -> `WRAPPING_UP`
|
|
933
|
+
|
|
934
|
+
- Guard: `guards.shouldWrapUp`
|
|
935
|
+
|
|
936
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskWrapup`, `requestCleanup`
|
|
937
|
+
|
|
938
|
+
- `CONTACT_ENDED` -> `TERMINATED` (default branch)
|
|
939
|
+
|
|
940
|
+
- Guard: default
|
|
941
|
+
|
|
942
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskEnd`
|
|
943
|
+
|
|
944
|
+
- `TASK_WRAPUP` -> `WRAPPING_UP`
|
|
945
|
+
|
|
946
|
+
- Guard: none
|
|
947
|
+
|
|
948
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskWrapup`
|
|
949
|
+
|
|
950
|
+
- `PAUSE_RECORDING` / `RESUME_RECORDING` -> Stay `CONNECTED`
|
|
951
|
+
|
|
952
|
+
- Guard: none
|
|
953
|
+
|
|
954
|
+
- Actions: `updateTaskData`, `setRecordingState`, `emitTaskRecordingPaused` / `emitTaskRecordingResumed`
|
|
955
|
+
|
|
956
|
+
**Description**: Main call is on hold.
|
|
957
|
+
|
|
958
|
+
**How this state is reached (incoming transitions)**:
|
|
959
|
+
|
|
960
|
+
- `IDLE --HYDRATE--> HELD`
|
|
961
|
+
|
|
962
|
+
- Guard: `guards.isInteractionHeld`
|
|
963
|
+
|
|
964
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
965
|
+
|
|
966
|
+
- `HOLD_INITIATING --HOLD_SUCCESS--> HELD`
|
|
967
|
+
|
|
968
|
+
- Guard: none
|
|
969
|
+
|
|
970
|
+
- Actions: `updateTaskData`, `setHoldState`, `emitTaskHold`
|
|
971
|
+
|
|
972
|
+
- `RESUME_INITIATING --UNHOLD_FAILED--> HELD`
|
|
973
|
+
|
|
974
|
+
- Guard: none
|
|
975
|
+
|
|
976
|
+
- Actions: none
|
|
977
|
+
|
|
978
|
+
- `CONSULTING --CONSULT_END--> HELD`
|
|
979
|
+
|
|
980
|
+
- Guard: inline `context.consultInitiator === true`
|
|
981
|
+
|
|
982
|
+
- Actions: `updateTaskData`, `clearConsultState`, `emitTaskConsultEnd`
|
|
983
|
+
|
|
984
|
+
- `CONSULT_INITIATING --CONSULT_FAILED--> HELD`
|
|
985
|
+
|
|
986
|
+
- Guard: `guards.isPrimaryMediaOnHold`
|
|
987
|
+
|
|
988
|
+
- Actions: `updateTaskData`, `handleConsultFailed`
|
|
989
|
+
|
|
990
|
+
- `CONSULT_INITIATING --CTQ_CANCEL--> HELD`
|
|
991
|
+
|
|
992
|
+
- Guard: `guards.isPrimaryMediaOnHold`
|
|
993
|
+
|
|
994
|
+
- Actions: `updateTaskData`, `clearConsultState`
|
|
995
|
+
|
|
996
|
+
**Valid transitions from `HELD`**:
|
|
997
|
+
|
|
998
|
+
- `UNHOLD_INITIATED` -> `RESUME_INITIATING`
|
|
999
|
+
|
|
1000
|
+
- Guard: none
|
|
1001
|
+
|
|
1002
|
+
- Actions: none
|
|
1003
|
+
|
|
1004
|
+
- `CONSULT` -> `CONSULT_INITIATING`
|
|
1005
|
+
|
|
1006
|
+
- Guard: none
|
|
1007
|
+
|
|
1008
|
+
- Actions: `setConsultInitiator`, `setConsultDestination`
|
|
1009
|
+
|
|
1010
|
+
- `TRANSFER_SUCCESS` -> `WRAPPING_UP`
|
|
1011
|
+
|
|
1012
|
+
- Guard: `guards.shouldWrapUpOrIsInitiator`
|
|
1013
|
+
|
|
1014
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskWrapup`
|
|
1015
|
+
|
|
1016
|
+
- `TRANSFER_SUCCESS` -> `CONNECTED` (receiver/default branch)
|
|
1017
|
+
|
|
1018
|
+
- Guard: default
|
|
1019
|
+
|
|
1020
|
+
- Actions: `updateTaskData`, `clearConsultState`
|
|
1021
|
+
|
|
1022
|
+
- `TRANSFER_FAILED` -> stay `HELD`
|
|
1023
|
+
|
|
1024
|
+
- Guard: none
|
|
1025
|
+
|
|
1026
|
+
- Actions: `updateTaskData`
|
|
1027
|
+
|
|
1028
|
+
- `CONTACT_ENDED` -> `CONFERENCING`
|
|
1029
|
+
|
|
1030
|
+
- Guard: `guards.conferenceInProgressFromEvent`
|
|
1031
|
+
|
|
1032
|
+
- Actions: `updateTaskData`, `emitTaskConferenceStarted`, `requestCleanup`
|
|
1033
|
+
|
|
1034
|
+
- `CONTACT_ENDED` -> `WRAPPING_UP`
|
|
1035
|
+
|
|
1036
|
+
- Guard: `guards.shouldWrapUp`
|
|
1037
|
+
|
|
1038
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskWrapup`, `requestCleanup`
|
|
1039
|
+
|
|
1040
|
+
- `CONTACT_ENDED` -> `TERMINATED` (default branch)
|
|
1041
|
+
|
|
1042
|
+
- Guard: default
|
|
1043
|
+
|
|
1044
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskEnd`
|
|
1045
|
+
|
|
1046
|
+
- `TASK_WRAPUP` -> `WRAPPING_UP`
|
|
1047
|
+
|
|
1048
|
+
- Guard: none
|
|
1049
|
+
|
|
1050
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskWrapup`
|
|
1051
|
+
|
|
1052
|
+
**Description**: Hold request has been sent and is awaiting backend confirmation.
|
|
1053
|
+
|
|
1054
|
+
**How this state is reached (incoming transitions)**:
|
|
1055
|
+
|
|
1056
|
+
- `CONNECTED --HOLD_INITIATED--> HOLD_INITIATING`
|
|
1057
|
+
|
|
1058
|
+
- Guard: none
|
|
1059
|
+
|
|
1060
|
+
- Actions: none
|
|
1061
|
+
|
|
1062
|
+
**Valid transitions from `HOLD_INITIATING`**:
|
|
1063
|
+
|
|
1064
|
+
- `HOLD_SUCCESS` -> `HELD`
|
|
1065
|
+
|
|
1066
|
+
- Guard: none
|
|
1067
|
+
|
|
1068
|
+
- Actions: `updateTaskData`, `setHoldState`, `emitTaskHold`
|
|
1069
|
+
|
|
1070
|
+
- `HOLD_FAILED` -> `CONNECTED`
|
|
1071
|
+
|
|
1072
|
+
- Guard: none
|
|
1073
|
+
|
|
1074
|
+
- Actions: `updateTaskData`
|
|
1075
|
+
|
|
1076
|
+
**Description**: Resume/unhold request has been sent and is awaiting backend confirmation.
|
|
1077
|
+
|
|
1078
|
+
**How this state is reached (incoming transitions)**:
|
|
1079
|
+
|
|
1080
|
+
- `HELD --UNHOLD_INITIATED--> RESUME_INITIATING`
|
|
1081
|
+
|
|
1082
|
+
- Guard: none
|
|
1083
|
+
|
|
1084
|
+
- Actions: none
|
|
1085
|
+
|
|
1086
|
+
**Valid transitions from `RESUME_INITIATING`**:
|
|
1087
|
+
|
|
1088
|
+
- `UNHOLD_SUCCESS` -> `CONNECTED`
|
|
1089
|
+
|
|
1090
|
+
- Guard: none
|
|
1091
|
+
|
|
1092
|
+
- Actions: `updateTaskData`, `setHoldState`, `emitTaskResume`
|
|
1093
|
+
|
|
1094
|
+
- `UNHOLD_FAILED` -> `HELD`
|
|
1095
|
+
|
|
1096
|
+
- Guard: none
|
|
1097
|
+
|
|
1098
|
+
- Actions: none
|
|
1099
|
+
|
|
1100
|
+
**Description**: Consult request is in-flight.
|
|
1101
|
+
|
|
1102
|
+
**How this state is reached (incoming transitions)**:
|
|
1103
|
+
|
|
1104
|
+
- `CONNECTED --CONSULT--> CONSULT_INITIATING`
|
|
1105
|
+
|
|
1106
|
+
- Guard: none
|
|
1107
|
+
|
|
1108
|
+
- Actions: `setConsultInitiator`, `setConsultDestination`
|
|
1109
|
+
|
|
1110
|
+
- `HELD --CONSULT--> CONSULT_INITIATING`
|
|
1111
|
+
|
|
1112
|
+
- Guard: none
|
|
1113
|
+
|
|
1114
|
+
- Actions: `setConsultInitiator`, `setConsultDestination`
|
|
1115
|
+
|
|
1116
|
+
- `CONFERENCING --CONSULT--> CONSULT_INITIATING`
|
|
1117
|
+
|
|
1118
|
+
- Guard: none
|
|
1119
|
+
|
|
1120
|
+
- Actions: `setConsultInitiator`, `setConsultDestination`, `setConsultFromConference`
|
|
1121
|
+
|
|
1122
|
+
**Valid transitions from `CONSULT_INITIATING`**:
|
|
1123
|
+
|
|
1124
|
+
- `CONSULT_SUCCESS` -> `CONSULTING`
|
|
1125
|
+
|
|
1126
|
+
- Guard: none
|
|
1127
|
+
|
|
1128
|
+
- Actions: `updateTaskData`, `setConsultInitiator`
|
|
1129
|
+
|
|
1130
|
+
- `CONSULT_FAILED` -> `CONFERENCING`
|
|
1131
|
+
|
|
1132
|
+
- Guard: inline `context.consultFromConference === true`
|
|
1133
|
+
|
|
1134
|
+
- Actions: `updateTaskData`, `handleConsultFailed`
|
|
1135
|
+
|
|
1136
|
+
- `CONSULT_FAILED` -> `HELD`
|
|
1137
|
+
|
|
1138
|
+
- Guard: `guards.isPrimaryMediaOnHold`
|
|
1139
|
+
|
|
1140
|
+
- Actions: `updateTaskData`, `handleConsultFailed`
|
|
1141
|
+
|
|
1142
|
+
- `CONSULT_FAILED` -> `CONNECTED` (default branch)
|
|
1143
|
+
|
|
1144
|
+
- Guard: default
|
|
1145
|
+
|
|
1146
|
+
- Actions: `updateTaskData`, `handleConsultFailed`
|
|
1147
|
+
|
|
1148
|
+
- `CTQ_CANCEL` -> `HELD`
|
|
1149
|
+
|
|
1150
|
+
- Guard: `guards.isPrimaryMediaOnHold`
|
|
1151
|
+
|
|
1152
|
+
- Actions: `updateTaskData`, `clearConsultState`
|
|
1153
|
+
|
|
1154
|
+
- `CTQ_CANCEL` -> `CONNECTED` (default branch)
|
|
1155
|
+
|
|
1156
|
+
- Guard: default
|
|
1157
|
+
|
|
1158
|
+
- Actions: `updateTaskData`, `clearConsultState`
|
|
1159
|
+
|
|
1160
|
+
- `HOLD_SUCCESS` -> stay `CONSULT_INITIATING`
|
|
1161
|
+
|
|
1162
|
+
- Guard: none
|
|
1163
|
+
|
|
1164
|
+
- Actions: `updateTaskData`
|
|
1165
|
+
|
|
1166
|
+
- `HOLD_FAILED` -> `CONNECTED`
|
|
1167
|
+
|
|
1168
|
+
- Guard: none
|
|
1169
|
+
|
|
1170
|
+
- Actions: `updateTaskData`, `handleConsultFailed`
|
|
1171
|
+
|
|
1172
|
+
**Description**: Agent is in active consult leg.
|
|
1173
|
+
|
|
1174
|
+
**How this state is reached (incoming transitions)**:
|
|
1175
|
+
|
|
1176
|
+
- `IDLE --HYDRATE--> CONSULTING`
|
|
1177
|
+
|
|
1178
|
+
- Guard: `guards.isInteractionConsulting`
|
|
1179
|
+
|
|
1180
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
1181
|
+
|
|
1182
|
+
- `OFFERED --CONSULTING_ACTIVE--> CONSULTING`
|
|
1183
|
+
|
|
1184
|
+
- Guard: none
|
|
1185
|
+
|
|
1186
|
+
- Actions: `updateTaskData`, `setConsultAgentJoined`, `emitTaskConsultAccepted`, `emitTaskConsulting`
|
|
1187
|
+
|
|
1188
|
+
- `CONSULT_INITIATING --CONSULT_SUCCESS--> CONSULTING`
|
|
1189
|
+
|
|
1190
|
+
- Guard: none
|
|
1191
|
+
|
|
1192
|
+
- Actions: `updateTaskData`, `setConsultInitiator`
|
|
1193
|
+
|
|
1194
|
+
- `CONF_INITIATING --CONFERENCE_FAILED--> CONSULTING`
|
|
1195
|
+
|
|
1196
|
+
- Guard: none
|
|
1197
|
+
|
|
1198
|
+
- Actions: none
|
|
1199
|
+
|
|
1200
|
+
**Valid transitions from `CONSULTING`**:
|
|
1201
|
+
|
|
1202
|
+
- `CONSULTING_ACTIVE` -> stay `CONSULTING`
|
|
1203
|
+
|
|
1204
|
+
- Guard: none
|
|
1205
|
+
|
|
1206
|
+
- Actions: `updateTaskData`, `setConsultAgentJoined`, `emitTaskConsulting`
|
|
1207
|
+
|
|
1208
|
+
- `CONSULT_END` -> `CONFERENCING`
|
|
1209
|
+
|
|
1210
|
+
- Guard: inline `context.consultInitiator === true && context.consultFromConference === true`
|
|
1211
|
+
|
|
1212
|
+
- Actions: `updateTaskData`, `clearConsultState`, `emitTaskConsultEnd`
|
|
1213
|
+
|
|
1214
|
+
- `CONSULT_END` -> `HELD`
|
|
1215
|
+
|
|
1216
|
+
- Guard: inline `context.consultInitiator === true`
|
|
1217
|
+
|
|
1218
|
+
- Actions: `updateTaskData`, `clearConsultState`, `emitTaskConsultEnd`
|
|
1219
|
+
|
|
1220
|
+
- `CONSULT_END` -> `TERMINATED` (default branch)
|
|
1221
|
+
|
|
1222
|
+
- Guard: default
|
|
1223
|
+
|
|
1224
|
+
- Actions: `updateTaskData`
|
|
1225
|
+
|
|
1226
|
+
- `HOLD_SUCCESS` -> stay `CONSULTING`
|
|
1227
|
+
|
|
1228
|
+
- Guard: none
|
|
1229
|
+
|
|
1230
|
+
- Actions: `updateTaskData`, `setHoldState`, `setConsultCallHeld`
|
|
1231
|
+
|
|
1232
|
+
- `UNHOLD_SUCCESS` -> stay `CONSULTING`
|
|
1233
|
+
|
|
1234
|
+
- Guard: none
|
|
1235
|
+
|
|
1236
|
+
- Actions: `updateTaskData`, `setHoldState`, `clearConsultCallHeld`
|
|
1237
|
+
|
|
1238
|
+
- `TRANSFER_SUCCESS` -> `WRAPPING_UP`
|
|
1239
|
+
|
|
1240
|
+
- Guard: `guards.shouldWrapUpOrIsInitiator`
|
|
1241
|
+
|
|
1242
|
+
- Actions: `updateTaskData`, `markEnded`, `emitTaskWrapup`
|
|
1243
|
+
|
|
1244
|
+
- `TRANSFER_SUCCESS` -> `CONNECTED` (receiver/default branch)
|
|
1245
|
+
|
|
1246
|
+
- Guard: default
|
|
1247
|
+
|
|
1248
|
+
- Actions: `updateTaskData`, `clearConsultState`
|
|
1249
|
+
|
|
1250
|
+
- `TRANSFER_FAILED` -> stay `CONSULTING`
|
|
1251
|
+
|
|
1252
|
+
- Guard: none
|
|
1253
|
+
|
|
1254
|
+
- Actions: `updateTaskData`
|
|
1255
|
+
|
|
1256
|
+
- `TRANSFER_CONFERENCE` -> stay `CONSULTING`
|
|
1257
|
+
|
|
1258
|
+
- Guard: none
|
|
1259
|
+
|
|
1260
|
+
- Actions: `setTransferConferenceRequested`, `emitTaskTransferConference`
|
|
1261
|
+
|
|
1262
|
+
- `TRANSFER_CONFERENCE_SUCCESS` -> stay `CONSULTING`
|
|
1263
|
+
|
|
1264
|
+
- Guard: inline `context.transferConferenceRequested !== true`
|
|
1265
|
+
|
|
1266
|
+
- Actions: `updateTaskData`, `handleTransferConferenceSuccess`, `clearTransferConferenceRequested`
|
|
1267
|
+
|
|
1268
|
+
- `TRANSFER_CONFERENCE_SUCCESS` -> `WRAPPING_UP`
|
|
1269
|
+
|
|
1270
|
+
- Guard: `guards.shouldWrapUp`
|
|
1271
|
+
|
|
1272
|
+
- Actions: `updateTaskData`, `markEnded`, `clearConsultState`, `handleTransferConferenceSuccess`, `clearTransferConferenceRequested`, `emitTaskWrapup`
|
|
1273
|
+
|
|
1274
|
+
- `TRANSFER_CONFERENCE_SUCCESS` -> `CONFERENCING`
|
|
1275
|
+
|
|
1276
|
+
- Guard: inline `!context.consultInitiator`
|
|
1277
|
+
|
|
1278
|
+
- Actions: `updateTaskData`, `clearConsultState`, `handleTransferConferenceSuccess`, `clearTransferConferenceRequested`
|
|
1279
|
+
|
|
1280
|
+
- `TRANSFER_CONFERENCE_SUCCESS` -> `TERMINATED` (default branch)
|
|
1281
|
+
|
|
1282
|
+
- Guard: default
|
|
1283
|
+
|
|
1284
|
+
- Actions: `updateTaskData`, `markEnded`, `clearConsultState`, `handleTransferConferenceSuccess`, `clearTransferConferenceRequested`, `emitTaskEnd`
|
|
1285
|
+
|
|
1286
|
+
- `TRANSFER_CONFERENCE_FAILED` -> stay `CONSULTING`
|
|
1287
|
+
|
|
1288
|
+
- Guard: none
|
|
1289
|
+
|
|
1290
|
+
- Actions: `clearTransferConferenceRequested`
|
|
1291
|
+
|
|
1292
|
+
- `ASSIGN` -> `CONNECTED`
|
|
1293
|
+
|
|
1294
|
+
- Guard: none
|
|
1295
|
+
|
|
1296
|
+
- Actions: `updateTaskData`, `emitTaskAssigned`
|
|
1297
|
+
|
|
1298
|
+
- `CONTACT_ENDED` -> `WRAPPING_UP`
|
|
1299
|
+
|
|
1300
|
+
- Guard: none
|
|
1301
|
+
|
|
1302
|
+
- Actions: `updateTaskData`, `markEnded`, `clearConsultState`, `emitTaskWrapup`, `requestCleanup`
|
|
1303
|
+
|
|
1304
|
+
- `TASK_WRAPUP` -> `WRAPPING_UP`
|
|
1305
|
+
|
|
1306
|
+
- Guard: none
|
|
1307
|
+
|
|
1308
|
+
- Actions: `updateTaskData`, `markEnded`, `clearConsultState`, `emitTaskWrapup`
|
|
1309
|
+
|
|
1310
|
+
- `MERGE_TO_CONFERENCE` -> `CONF_INITIATING`
|
|
1311
|
+
|
|
1312
|
+
- Guard: none
|
|
1313
|
+
|
|
1314
|
+
- Actions: none
|
|
1315
|
+
|
|
1316
|
+
- `CONFERENCE_START` -> `CONFERENCING`
|
|
1317
|
+
|
|
1318
|
+
- Guard: none
|
|
1319
|
+
|
|
1320
|
+
- Actions: `handleConferenceStarted`, `clearConsultState`
|
|
1321
|
+
|
|
1322
|
+
**Description**: Conference merge is being established.
|
|
1323
|
+
|
|
1324
|
+
**How this state is reached (incoming transitions)**:
|
|
1325
|
+
|
|
1326
|
+
- `CONSULTING --MERGE_TO_CONFERENCE--> CONF_INITIATING`
|
|
1327
|
+
|
|
1328
|
+
- Guard: none
|
|
1329
|
+
|
|
1330
|
+
- Actions: none
|
|
1331
|
+
|
|
1332
|
+
**Valid transitions from `CONF_INITIATING`**:
|
|
1333
|
+
|
|
1334
|
+
- `CONFERENCE_START` -> `CONFERENCING`
|
|
1335
|
+
|
|
1336
|
+
- Guard: none
|
|
1337
|
+
|
|
1338
|
+
- Actions: `handleConferenceStarted`
|
|
1339
|
+
|
|
1340
|
+
- `CONFERENCE_FAILED` -> `CONSULTING`
|
|
1341
|
+
|
|
1342
|
+
- Guard: none
|
|
1343
|
+
|
|
1344
|
+
- Actions: none
|
|
1345
|
+
|
|
1346
|
+
**Description**: Active conference call state.
|
|
1347
|
+
|
|
1348
|
+
**How this state is reached (incoming transitions)**:
|
|
1349
|
+
|
|
1350
|
+
- `IDLE --HYDRATE--> CONFERENCING`
|
|
1351
|
+
|
|
1352
|
+
- Guard: `guards.isConferencingByParticipants`
|
|
1353
|
+
|
|
1354
|
+
- Actions: `updateTaskData`, `emitTaskHydrate`
|
|
1355
|
+
|
|
1356
|
+
- `CONNECTED --CONTACT_ENDED--> CONFERENCING`
|
|
1357
|
+
|
|
1358
|
+
- Guard: `guards.conferenceInProgressFromEvent`
|
|
1359
|
+
|
|
1360
|
+
- Actions: `updateTaskData`, `emitTaskConferenceStarted`, `requestCleanup`
|
|
1361
|
+
|
|
1362
|
+
- `HELD --CONTACT_ENDED--> CONFERENCING`
|
|
1363
|
+
|
|
1364
|
+
- Guard: `guards.conferenceInProgressFromEvent`
|
|
1365
|
+
|
|
1366
|
+
- Actions: `updateTaskData`, `emitTaskConferenceStarted`, `requestCleanup`
|
|
1367
|
+
|
|
1368
|
+
- `CONSULTING --CONFERENCE_START--> CONFERENCING`
|
|
1369
|
+
|
|
1370
|
+
- Guard: none
|
|
1371
|
+
|
|
1372
|
+
- Actions: `handleConferenceStarted`, `clearConsultState`
|
|
1373
|
+
|
|
1374
|
+
- `CONF_INITIATING --CONFERENCE_START--> CONFERENCING`
|
|
1375
|
+
|
|
1376
|
+
- Guard: none
|
|
1377
|
+
|
|
1378
|
+
- Actions: `handleConferenceStarted`
|
|
1379
|
+
|
|
1380
|
+
- `CONSULTING --TRANSFER_CONFERENCE_SUCCESS--> CONFERENCING`
|
|
1381
|
+
|
|
1382
|
+
- Guard: inline `!context.consultInitiator`
|
|
1383
|
+
|
|
1384
|
+
- Actions: `updateTaskData`, `clearConsultState`, `handleTransferConferenceSuccess`, `clearTransferConferenceRequested`
|
|
1385
|
+
|
|
1386
|
+
**Valid transitions from `CONFERENCING`**:
|
|
1387
|
+
|
|
1388
|
+
- `CONSULT` -> `CONSULT_INITIATING`
|
|
1389
|
+
|
|
1390
|
+
- Guard: none
|
|
1391
|
+
|
|
1392
|
+
- Actions: `setConsultInitiator`, `setConsultDestination`, `setConsultFromConference`
|
|
1393
|
+
|
|
1394
|
+
- `CONFERENCE_START` -> stay `CONFERENCING`
|
|
1395
|
+
|
|
1396
|
+
- Guard: none
|
|
1397
|
+
|
|
1398
|
+
- Actions: `updateTaskData`, `clearConsultState`, `emitTaskConferenceStarted`
|
|
1399
|
+
|
|
1400
|
+
- `CONSULT_END` -> stay `CONFERENCING`
|
|
1401
|
+
|
|
1402
|
+
- Guard: none
|
|
1403
|
+
|
|
1404
|
+
- Actions: `updateTaskData`, `clearConsultState`
|
|
1405
|
+
|
|
1406
|
+
- `HOLD_SUCCESS` / `UNHOLD_SUCCESS` -> stay `CONFERENCING`
|
|
1407
|
+
|
|
1408
|
+
- Guard: none
|
|
1409
|
+
|
|
1410
|
+
- Actions: `updateTaskData`, `setHoldState`, `emitTaskHold` / `emitTaskResume`
|
|
1411
|
+
|
|
1412
|
+
- `TRANSFER_CONFERENCE` -> stay `CONFERENCING`
|
|
1413
|
+
|
|
1414
|
+
- Guard: none
|
|
1415
|
+
|
|
1416
|
+
- Actions: `setTransferConferenceRequested`, `emitTaskTransferConference`
|
|
1417
|
+
|
|
1418
|
+
- `TRANSFER_CONFERENCE_SUCCESS` -> stay `CONFERENCING`
|
|
1419
|
+
|
|
1420
|
+
- Guard: inline `context.transferConferenceRequested !== true`
|
|
1421
|
+
|
|
1422
|
+
- Actions: `updateTaskData`, `handleTransferConferenceSuccess`, `clearTransferConferenceRequested`
|
|
1423
|
+
|
|
1424
|
+
- `TRANSFER_CONFERENCE_FAILED` -> stay `CONFERENCING`
|
|
1425
|
+
|
|
1426
|
+
- Guard: none
|
|
1427
|
+
|
|
1428
|
+
- Actions: `clearTransferConferenceRequested`
|
|
1429
|
+
|
|
1430
|
+
- `PARTICIPANT_LEAVE` -> `WRAPPING_UP`
|
|
1431
|
+
|
|
1432
|
+
- Guard: `guards.didCurrentAgentLeaveConference && guards.shouldWrapUp`
|
|
1433
|
+
|
|
1434
|
+
- Actions: `updateTaskData`, `handleParticipantLeft`, `markEnded`, `clearConsultState`, `emitTaskParticipantLeft`, `emitTaskWrapup`
|
|
1435
|
+
|
|
1436
|
+
- `PARTICIPANT_LEAVE` -> `TERMINATED`
|
|
1437
|
+
|
|
1438
|
+
- Guard: `guards.didCurrentAgentLeaveConference`
|
|
1439
|
+
|
|
1440
|
+
- Actions: `updateTaskData`, `handleParticipantLeft`, `markEnded`, `clearConsultState`, `emitTaskParticipantLeft`, `emitTaskEnd`
|
|
1441
|
+
|
|
1442
|
+
- `PARTICIPANT_LEAVE` -> `CONNECTED`
|
|
1443
|
+
|
|
1444
|
+
- Guard: `guards.shouldDowngradeConferenceToConnected`
|
|
1445
|
+
|
|
1446
|
+
- Actions: `updateTaskData`, `handleParticipantLeft`, `clearConsultState`, `emitTaskParticipantLeft`, `emitTaskConferenceEnded`
|
|
1447
|
+
|
|
1448
|
+
- `PARTICIPANT_LEAVE` -> stay `CONFERENCING` (default)
|
|
1449
|
+
|
|
1450
|
+
- Guard: default
|
|
1451
|
+
|
|
1452
|
+
- Actions: `updateTaskData`, `handleParticipantLeft`, `emitTaskParticipantLeft`
|
|
1453
|
+
|
|
1454
|
+
- `CONFERENCE_END` -> `WRAPPING_UP`
|
|
1455
|
+
|
|
1456
|
+
- Guard: `guards.shouldWrapUp`
|
|
1457
|
+
|
|
1458
|
+
- Actions: `updateTaskData`, `markEnded`, `clearConsultState`, `emitTaskWrapup`
|
|
1459
|
+
|
|
1460
|
+
- `CONFERENCE_END` -> `CONNECTED`
|
|
1461
|
+
|
|
1462
|
+
- Guard: inline `!context.exitingConference && customerInCall`
|
|
1463
|
+
|
|
1464
|
+
- Actions: `updateTaskData`, `clearConsultState`, `emitTaskConferenceEnded`
|
|
1465
|
+
|
|
1466
|
+
- `CONFERENCE_END` -> `TERMINATED` (default branch)
|
|
1467
|
+
|
|
1468
|
+
- Guard: default
|
|
1469
|
+
|
|
1470
|
+
- Actions: `updateTaskData`, `markEnded`, `clearConsultState`, `emitTaskEnd`
|
|
1471
|
+
|
|
1472
|
+
- `CONTACT_ENDED` -> stay `CONFERENCING`
|
|
1473
|
+
|
|
1474
|
+
- Guard: none
|
|
1475
|
+
|
|
1476
|
+
- Actions: `updateTaskData`, `requestCleanup`
|
|
1477
|
+
|
|
1478
|
+
**Description**: Post-interaction work (ACW) is in progress.
|
|
1479
|
+
|
|
1480
|
+
**How this state is reached (incoming transitions)**:
|
|
1481
|
+
|
|
1482
|
+
- Reached from `CONNECTED`, `HELD`, `CONSULTING`, or `CONFERENCING` via `CONTACT_ENDED`, `TASK_WRAPUP`, `TRANSFER_SUCCESS`, `TRANSFER_CONFERENCE_SUCCESS`, `PARTICIPANT_LEAVE`, or `CONFERENCE_END` branches
|
|
1483
|
+
|
|
1484
|
+
- Entry always emits wrapup event after transition
|
|
1485
|
+
|
|
1486
|
+
**Entry Actions**:
|
|
1487
|
+
|
|
1488
|
+
- `emitTaskWrapup`
|
|
1489
|
+
|
|
1490
|
+
**Valid transitions from `WRAPPING_UP`**:
|
|
1491
|
+
|
|
1492
|
+
- `WRAPUP_COMPLETE` -> `COMPLETED`
|
|
1493
|
+
|
|
1494
|
+
- Guard: none
|
|
1495
|
+
|
|
1496
|
+
- Actions: `updateTaskData`
|
|
1497
|
+
|
|
1498
|
+
**Guards**: None
|
|
1499
|
+
|
|
1500
|
+
**Description**: Final wrapped-up terminal state.
|
|
1501
|
+
|
|
1502
|
+
**Entry Actions**:
|
|
1503
|
+
|
|
1504
|
+
- `emitTaskWrappedup`
|
|
1505
|
+
|
|
1506
|
+
- `cleanupResources`
|
|
1507
|
+
|
|
1508
|
+
**How this state is reached (incoming transitions)**:
|
|
1509
|
+
|
|
1510
|
+
- `WRAPPING_UP --WRAPUP_COMPLETE--> COMPLETED`
|
|
1511
|
+
|
|
1512
|
+
- Guard: none
|
|
1513
|
+
|
|
1514
|
+
- Actions: `updateTaskData`
|
|
1515
|
+
|
|
1516
|
+
**Valid transitions from `COMPLETED`**: None (final state)
|
|
1517
|
+
|
|
1518
|
+
**Guards**: None
|
|
1519
|
+
|
|
1520
|
+
**Description**: Final terminated terminal state.
|
|
1521
|
+
|
|
1522
|
+
**Entry Actions**:
|
|
1523
|
+
|
|
1524
|
+
- `cleanupResources`
|
|
1525
|
+
|
|
1526
|
+
**How this state is reached (incoming transitions)**:
|
|
1527
|
+
|
|
1528
|
+
- Reached from `OFFERED`, `CONNECTED`, `HELD`, `CONSULTING`, and `CONFERENCING` via terminating branches (`TASK_WRAPUP`, failure paths, and default end-of-contact/conference branches)
|
|
1529
|
+
|
|
1530
|
+
**Valid transitions from `TERMINATED`**: None (final state)
|
|
1531
|
+
|
|
1532
|
+
**Guards**: None
|
|
1533
|
+
|
|
1534
|
+
Event names below are from `TaskEvent` in `constants.ts`.
|
|
1535
|
+
|
|
1536
|
+
- `TASK_INCOMING`, `TASK_OFFERED`, `HYDRATE`
|
|
1537
|
+
|
|
1538
|
+
- `CONTACT_UPDATED`, `CONTACT_OWNER_CHANGED`
|
|
1539
|
+
|
|
1540
|
+
- `ASSIGN`, `CONTACT_ENDED`, `TASK_WRAPUP`, `WRAPUP_COMPLETE`
|
|
1541
|
+
|
|
1542
|
+
- `HOLD_INITIATED`, `HOLD_SUCCESS`, `HOLD_FAILED`
|
|
1543
|
+
|
|
1544
|
+
- `UNHOLD_INITIATED`, `UNHOLD_SUCCESS`, `UNHOLD_FAILED`
|
|
1545
|
+
|
|
1546
|
+
- `OFFER_CONSULT`, `CONSULT`, `CONSULT_SUCCESS`, `CONSULT_CREATED`
|
|
1547
|
+
|
|
1548
|
+
- `CONSULTING_ACTIVE`, `CONSULT_END`, `CONSULT_FAILED`
|
|
1549
|
+
|
|
1550
|
+
- `CTQ_CANCEL`, `CTQ_CANCEL_FAILED`
|
|
1551
|
+
|
|
1552
|
+
- `MERGE_TO_CONFERENCE`, `CONFERENCE_START`, `CONFERENCE_FAILED`, `CONFERENCE_END`
|
|
1553
|
+
|
|
1554
|
+
- `PARTICIPANT_LEAVE`
|
|
1555
|
+
|
|
1556
|
+
- `TRANSFER_CONFERENCE`, `TRANSFER_CONFERENCE_SUCCESS`, `TRANSFER_CONFERENCE_FAILED`
|
|
1557
|
+
|
|
1558
|
+
- `EXIT_CONFERENCE`, `EXIT_CONFERENCE_SUCCESS`, `EXIT_CONFERENCE_FAILED`
|
|
1559
|
+
|
|
1560
|
+
- `TRANSFER_SUCCESS`, `TRANSFER_FAILED`
|
|
1561
|
+
|
|
1562
|
+
- `RECORDING_STARTED`, `PAUSE_RECORDING`, `RESUME_RECORDING`
|
|
1563
|
+
|
|
1564
|
+
The flow below shows a single transition in the requested form:
|
|
1565
|
+
|
|
1566
|
+
```mermaid
|
|
1567
|
+
flowchart LR
|
|
1568
|
+
A[User Action/CC Event Mapping] --> B[State Machine Event Trigger]
|
|
1569
|
+
B --> C{Check against Current State: Valid Transition?}
|
|
1570
|
+
C -- No --> X[Ignore/No-op]
|
|
1571
|
+
C -- Yes --> D[StateMachine evaluates guards]
|
|
1572
|
+
D -- No --> Y[Blocked by Guard]
|
|
1573
|
+
D -- Yes --> E[Execute Associated Actions]
|
|
1574
|
+
E --> F[Context updated]
|
|
1575
|
+
F --> G[UI Controls Recomputed]
|
|
1576
|
+
G --> H[Transition to Target State]
|
|
1577
|
+
```
|
|
1578
|
+
|
|
1579
|
+
```mermaid
|
|
1580
|
+
flowchart LR
|
|
1581
|
+
A[User invoked hold API] --> B[Event Trigger: HOLD_INITIATED]
|
|
1582
|
+
B --> C{State = CONNECTED?}
|
|
1583
|
+
C -- No --> X[Ignore/No-op]
|
|
1584
|
+
C -- Yes --> D[Guards: none]
|
|
1585
|
+
D -- Yes --> E[Actions: setHoldInitiated + updateTaskData]
|
|
1586
|
+
E --> F[Context updated]
|
|
1587
|
+
F --> G[UI controls recomputed]
|
|
1588
|
+
G --> H[Transition: CONNECTED -> HOLD_INITIATING]
|
|
1589
|
+
H --> I[CC Event: AGENT_CONTACT_HELD]
|
|
1590
|
+
I --> J[Mapped: HOLD_SUCCESS]
|
|
1591
|
+
J --> K{State = HOLD_INITIATING?}
|
|
1592
|
+
K -- No --> X
|
|
1593
|
+
K -- Yes --> L[Guards: none]
|
|
1594
|
+
L -- Yes --> M[Actions: setHoldSuccess + updateTaskData]
|
|
1595
|
+
M --> N[Context updated]
|
|
1596
|
+
N --> O[UI controls recomputed]
|
|
1597
|
+
O --> P[Transition: HOLD_INITIATING -> HELD]
|
|
1598
|
+
```
|
|
1599
|
+
|
|
1600
|
+
Manage complex task lifecycle with clear states, transitions, guards, and actions.
|
|
1601
|
+
|
|
1602
|
+
```typescript
|
|
1603
|
+
// Task.ts
|
|
1604
|
+
export default abstract class Task extends EventEmitter {
|
|
1605
|
+
public stateMachineService?: ActorRefFrom<TaskStateMachine>;
|
|
1606
|
+
|
|
1607
|
+
private initializeStateMachine(): void {
|
|
1608
|
+
const machine: TaskStateMachine = createTaskStateMachine(this.uiControlConfig, {
|
|
1609
|
+
actions: this.getStateMachineActionOverrides(),
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
this.stateMachineService = createActor(machine);
|
|
1613
|
+
|
|
1614
|
+
// Subscribe to state changes
|
|
1615
|
+
this.stateMachineService.subscribe((snapshot) => {
|
|
1616
|
+
const currentState = snapshot.value as TaskState;
|
|
1617
|
+
this.state = snapshot;
|
|
1618
|
+
this.updateUiControls(previousState !== currentState);
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
this.stateMachineService.start();
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
public sendStateMachineEvent(event: TaskEventPayload): void {
|
|
1625
|
+
this.stateMachineService?.send(event);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
```
|
|
1629
|
+
|
|
1630
|
+
```typescript
|
|
1631
|
+
// state-machine/TaskStateMachine.ts
|
|
1632
|
+
export function getTaskStateMachineConfig(uiControlConfig: UIControlConfig) {
|
|
1633
|
+
return {
|
|
1634
|
+
id: 'taskStateMachine',
|
|
1635
|
+
initial: TaskState.IDLE,
|
|
1636
|
+
context: createInitialContext(uiControlConfig, TaskState.IDLE),
|
|
1637
|
+
states: {
|
|
1638
|
+
[TaskState.IDLE]: {
|
|
1639
|
+
on: {
|
|
1640
|
+
[TaskEvent.TASK_INCOMING]: {
|
|
1641
|
+
target: TaskState.OFFERED,
|
|
1642
|
+
actions: ['initializeTask', 'emitTaskIncoming'],
|
|
1643
|
+
},
|
|
1644
|
+
},
|
|
1645
|
+
},
|
|
1646
|
+
[TaskState.OFFERED]: {
|
|
1647
|
+
on: {
|
|
1648
|
+
[TaskEvent.ASSIGN]: {
|
|
1649
|
+
target: TaskState.CONNECTED,
|
|
1650
|
+
actions: ['updateTaskData', 'emitTaskAssigned'],
|
|
1651
|
+
},
|
|
1652
|
+
[TaskEvent.TASK_WRAPUP]: {
|
|
1653
|
+
target: TaskState.TERMINATED,
|
|
1654
|
+
actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
|
|
1655
|
+
},
|
|
1656
|
+
},
|
|
1657
|
+
},
|
|
1658
|
+
// ... more states
|
|
1659
|
+
},
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
```
|
|
1663
|
+
|
|
1664
|
+
Complete mapping from backend CC_EVENTS to internal TaskEvent types.
|
|
1665
|
+
|
|
1666
|
+
| Backend CC Event | TaskEvent | Typical From State(s) | Target State | Notes / Guards |
|
|
1667
|
+
|---|---|---|---|---|
|
|
1668
|
+
| `AGENT_CONTACT_RESERVED` | `TASK_INCOMING` | `IDLE` | `OFFERED` | Incoming task entry |
|
|
1669
|
+
| `AGENT_OFFER_CONTACT` | `TASK_OFFERED` | `OFFERED` | `OFFERED` | Offer payload refresh |
|
|
1670
|
+
| `AGENT_CONTACT` | `HYDRATE` | `IDLE` | `WRAPPING_UP` / `CONSULTING` / `HELD` / `CONNECTED` / `CONFERENCING` / `IDLE` | Guard-based restore |
|
|
1671
|
+
| `CONTACT_UPDATED` | `CONTACT_UPDATED` | any | same | Context sync |
|
|
1672
|
+
| `CONTACT_OWNER_CHANGED` | `CONTACT_OWNER_CHANGED` | any | same | Context sync |
|
|
1673
|
+
| `AGENT_OFFER_CONSULT` | `OFFER_CONSULT` | `OFFERED` | `OFFERED` | Receiver-side consult offer |
|
|
1674
|
+
| `AGENT_CONTACT_ASSIGNED` | `ASSIGN` | `OFFERED` / `CONNECTED` / `CONSULTING` | `CONNECTED` | Assign/reassign |
|
|
1675
|
+
| `AGENT_CONTACT_HELD` | `HOLD_SUCCESS` | `HOLD_INITIATING` | `HELD` | Includes `mediaResourceId` |
|
|
1676
|
+
| `AGENT_CONTACT_UNHELD` | `UNHOLD_SUCCESS` | `RESUME_INITIATING` | `CONNECTED` | Includes `mediaResourceId` |
|
|
1677
|
+
| `AGENT_CONSULT_CREATED` | `CONSULT_CREATED` | varies | same | Context + emitter action |
|
|
1678
|
+
| `AGENT_CONSULTING` | `CONSULTING_ACTIVE` | `OFFERED` / `CONSULTING` | `CONSULTING` | Sets consult joined flag |
|
|
1679
|
+
| `AGENT_CONSULT_ENDED` | `CONSULT_END` | `CONSULTING` | `CONFERENCING` / `HELD` / `TERMINATED` | Depends on initiator flags |
|
|
1680
|
+
| `AGENT_CONSULT_FAILED` | `CONSULT_FAILED` | `CONSULT_INITIATING` | `CONFERENCING` / `HELD` / `CONNECTED` | Guard-based fallback |
|
|
1681
|
+
| `AGENT_CTQ_FAILED` | `CONSULT_FAILED` | `CONSULT_INITIATING` | `CONFERENCING` / `HELD` / `CONNECTED` | Same as consult failed |
|
|
1682
|
+
| `AGENT_CTQ_CANCELLED` | `CTQ_CANCEL` | `CONSULT_INITIATING` | `HELD` / `CONNECTED` | Guarded by hold state |
|
|
1683
|
+
| `AGENT_CTQ_CANCEL_FAILED` | `CTQ_CANCEL_FAILED` | varies | same | No transition mapping |
|
|
1684
|
+
| `AGENT_BLIND_TRANSFERRED` | `TRANSFER_SUCCESS` | `CONNECTED` / `HELD` / `CONSULTING` | `WRAPPING_UP` / `CONNECTED` | `shouldWrapUpOrIsInitiator` |
|
|
1685
|
+
| `AGENT_CONSULT_TRANSFERRED` | `TRANSFER_SUCCESS` | `CONNECTED` / `HELD` / `CONSULTING` | `WRAPPING_UP` / `CONNECTED` | Same path |
|
|
1686
|
+
| `AGENT_VTEAM_TRANSFERRED` | `TRANSFER_SUCCESS` | `CONNECTED` / `HELD` / `CONSULTING` | `WRAPPING_UP` / `CONNECTED` | Same path |
|
|
1687
|
+
| `AGENT_WRAPUP` | `TASK_WRAPUP` | `OFFERED` / `CONNECTED` / `HELD` / `CONSULTING` | `TERMINATED` / `WRAPPING_UP` | `OFFERED` terminates; others wrap |
|
|
1688
|
+
| `AGENT_BLIND_TRANSFER_FAILED` | `TRANSFER_FAILED` | `CONNECTED` / `HELD` / `CONSULTING` | same | Context update |
|
|
1689
|
+
| `AGENT_VTEAM_TRANSFER_FAILED` | `TRANSFER_FAILED` | `CONNECTED` / `HELD` / `CONSULTING` | same | Context update |
|
|
1690
|
+
| `AGENT_CONSULT_TRANSFER_FAILED` | `TRANSFER_FAILED` | `CONNECTED` / `HELD` / `CONSULTING` | same | Context update |
|
|
1691
|
+
| `AGENT_CONFERENCE_TRANSFER_FAILED` | `TRANSFER_FAILED` | `CONNECTED` / `HELD` / `CONSULTING` | same | Context update |
|
|
1692
|
+
| `CONTACT_ENDED` | `CONTACT_ENDED` | `CONNECTED` / `HELD` / `CONSULTING` / `CONFERENCING` | `CONFERENCING` / `WRAPPING_UP` / `TERMINATED` / same | Guard-driven branch |
|
|
1693
|
+
| `AGENT_INVITE_FAILED` | `INVITE_FAILED` | `OFFERED` | `TERMINATED` | Reject path |
|
|
1694
|
+
| `AGENT_CONTACT_ASSIGN_FAILED` | `ASSIGN_FAILED` | `OFFERED` | `TERMINATED` | Reject path |
|
|
1695
|
+
| `AGENT_CONTACT_OFFER_RONA` | `RONA` | `OFFERED` | `TERMINATED` | Timeout path |
|
|
1696
|
+
| `AGENT_OUTBOUND_FAILED` | `OUTBOUND_FAILED` | `OFFERED` | `TERMINATED` | Outbound failure |
|
|
1697
|
+
| `CONTACT_RECORDING_STARTED` | `RECORDING_STARTED` | any | same | Recording state update |
|
|
1698
|
+
| `CONTACT_RECORDING_PAUSED` | `PAUSE_RECORDING` | `CONNECTED` | same | Recording state update |
|
|
1699
|
+
| `CONTACT_RECORDING_RESUMED` | `RESUME_RECORDING` | `CONNECTED` | same | Recording state update |
|
|
1700
|
+
| `AGENT_WRAPPEDUP` | `WRAPUP_COMPLETE` | `WRAPPING_UP` | `COMPLETED` | Final completion |
|
|
1701
|
+
| `AGENT_CONSULT_CONFERENCED` | `CONFERENCE_START` | `CONSULTING` / `CONF_INITIATING` / `CONFERENCING` | `CONFERENCING` / same | Conference established |
|
|
1702
|
+
| `PARTICIPANT_JOINED_CONFERENCE` | `CONFERENCE_START` | `CONSULTING` / `CONF_INITIATING` / `CONFERENCING` | `CONFERENCING` / same | Conference participant joined |
|
|
1703
|
+
| `AGENT_CONSULT_CONFERENCE_FAILED` | `CONFERENCE_FAILED` | `CONF_INITIATING` | `CONSULTING` | Merge fail fallback |
|
|
1704
|
+
| `AGENT_CONSULT_CONFERENCE_ENDED` | `CONFERENCE_END` | `CONFERENCING` | `WRAPPING_UP` / `CONNECTED` / `TERMINATED` | Guard-driven |
|
|
1705
|
+
| `PARTICIPANT_LEFT_CONFERENCE` | `PARTICIPANT_LEAVE` | `CONFERENCING` | `WRAPPING_UP` / `TERMINATED` / `CONNECTED` / same | Ownership + downgrade guards |
|
|
1706
|
+
| `AGENT_CONFERENCE_TRANSFERRED` | `TRANSFER_CONFERENCE_SUCCESS` | `CONSULTING` / `CONFERENCING` | `WRAPPING_UP` / `CONFERENCING` / `TERMINATED` / same | Initiator/receiver dependent |
|
|
1707
|
+
|
|
1708
|
+
- `AGENT_CONTACT_UNASSIGNED` -> returns `null` in mapper (`TaskManager.mapEventToTaskStateMachineEvent`)
|
|
1709
|
+
|
|
1710
|
+
| Backend Event | TaskEvent | State Transition | Notes |
|
|
1711
|
+
|---|---|---|---|
|
|
1712
|
+
| `AgentContactReserved` | `TASK_INCOMING` | IDLE → OFFERED | New task offered |
|
|
1713
|
+
| `AgentOfferContact` | `TASK_OFFERED` | Stay in OFFERED | Offer confirmation |
|
|
1714
|
+
| `AgentContact` | `HYDRATE` | Various | State restoration |
|
|
1715
|
+
| `AgentContactAssigned` | `ASSIGN` | OFFERED → CONNECTED (also CONNECTED/CONSULTING refresh paths) | Task accepted/reassigned |
|
|
1716
|
+
| `ContactUpdated` | `CONTACT_UPDATED` | No change | Data update only |
|
|
1717
|
+
| `ContactOwnerChanged` | `CONTACT_OWNER_CHANGED` | No change | Owner update only |
|
|
1718
|
+
| `ContactEnded` | `CONTACT_ENDED` | Guard-based branch | CONFERENCING / WRAPPING_UP / TERMINATED / stay |
|
|
1719
|
+
| `AgentContactUnassigned` | None | N/A | Handled by other events |
|
|
1720
|
+
|
|
1721
|
+
| Backend Event | TaskEvent | State Transition | Context Update |
|
|
1722
|
+
|---|---|---|---|
|
|
1723
|
+
| `AgentContactHeld` | `HOLD_SUCCESS` | HOLD_INITIATING → HELD | `setHoldState` updates `taskData.interaction.media[mediaResourceId].isHold = true` |
|
|
1724
|
+
| `AgentContactUnheld` | `UNHOLD_SUCCESS` | RESUME_INITIATING → CONNECTED | `setHoldState` updates `taskData.interaction.media[mediaResourceId].isHold = false` |
|
|
1725
|
+
| `AgentContactHoldFailed` | `HOLD_FAILED` | HOLD_INITIATING → CONNECTED | Context refreshed |
|
|
1726
|
+
| `AgentContactUnholdFailed` | `UNHOLD_FAILED` | RESUME_INITIATING → HELD | No transition action |
|
|
1727
|
+
|
|
1728
|
+
| Backend Event / API | TaskEvent | State Transition | Context Update |
|
|
1729
|
+
|---|---|---|---|
|
|
1730
|
+
| API `task.consult(...)` | `CONSULT` | CONNECTED/HELD/CONFERENCING → CONSULT_INITIATING | Sets consult initiator + destination (and `consultFromConference` in conference flow) |
|
|
1731
|
+
| `AgentOfferConsult` | `OFFER_CONSULT` | OFFERED → OFFERED | Offer-only path |
|
|
1732
|
+
| `AgentConsultCreated` | `CONSULT_CREATED` | No state transition wiring | Event exists but not consumed by transition table |
|
|
1733
|
+
| `AgentConsulting` | `CONSULTING_ACTIVE` | OFFERED → CONSULTING, CONSULTING → CONSULTING | Sets `consultDestinationAgentJoined` |
|
|
1734
|
+
| `AgentConsultEnded` | `CONSULT_END` | CONSULTING → CONFERENCING / HELD / TERMINATED | Depends on initiator and consult-from-conference |
|
|
1735
|
+
| `AgentConsultFailed` / `AgentCtqFailed` | `CONSULT_FAILED` | CONSULT_INITIATING → CONFERENCING / HELD / CONNECTED | Guard-based fallback |
|
|
1736
|
+
| `AgentCtqCancelled` | `CTQ_CANCEL` | CONSULT_INITIATING → HELD / CONNECTED | Guarded by `isPrimaryMediaOnHold` |
|
|
1737
|
+
| `AgentCtqCancelFailed` | `CTQ_CANCEL_FAILED` | No state transition wiring | Event mapped but not consumed |
|
|
1738
|
+
|
|
1739
|
+
| Backend Event | TaskEvent | State Transition | Wrapup Logic |
|
|
1740
|
+
|---|---|---|---|
|
|
1741
|
+
| `AgentBlindTransferred` | `TRANSFER_SUCCESS` | → WRAPPING_UP/CONNECTED | Guard `shouldWrapUpOrIsInitiator` decides wrapup vs receiver/default path |
|
|
1742
|
+
| `AgentVTeamTransferred` | `TRANSFER_SUCCESS` | → WRAPPING_UP/CONNECTED | Same transition logic as blind transfer |
|
|
1743
|
+
| `AgentConsultTransferred` | `TRANSFER_SUCCESS` | CONSULTING → WRAPPING_UP/CONNECTED | Initiator/wrapup path vs receiver/default path |
|
|
1744
|
+
| `AgentBlindTransferFailed` | `TRANSFER_FAILED` | No change | Emit failure, stay in current state |
|
|
1745
|
+
| `AgentVTeamTransferFailed` | `TRANSFER_FAILED` | No change | Queue transfer failed |
|
|
1746
|
+
| `AgentConsultTransferFailed` | `TRANSFER_FAILED` | No change | Consult transfer failed |
|
|
1747
|
+
|
|
1748
|
+
| Backend Event / API | TaskEvent | State Transition | Context Update |
|
|
1749
|
+
|---|---|---|---|
|
|
1750
|
+
| API `task.consultConference()` | `MERGE_TO_CONFERENCE` | CONSULTING → CONF_INITIATING | Starts merge flow |
|
|
1751
|
+
| `AgentConsultConferenced` | `CONFERENCE_START` | CONSULTING/CONF_INITIATING → CONFERENCING | `handleConferenceStarted` path |
|
|
1752
|
+
| `ParticipantJoinedConference` | `CONFERENCE_START` | CONFERENCING → CONFERENCING | Refresh + emit conference started |
|
|
1753
|
+
| `ParticipantLeftConference` | `PARTICIPANT_LEAVE` | CONFERENCING → WRAPPING_UP / TERMINATED / CONNECTED / stay | Uses `didCurrentAgentLeaveConference`, `shouldWrapUp`, `shouldDowngradeConferenceToConnected` |
|
|
1754
|
+
| `AgentConsultConferenceEnded` | `CONFERENCE_END` | CONFERENCING → WRAPPING_UP / CONNECTED / TERMINATED | Guard-based branch |
|
|
1755
|
+
| `AgentConsultConferenceFailed` | `CONFERENCE_FAILED` | CONF_INITIATING → CONSULTING | Merge failed fallback |
|
|
1756
|
+
| `AgentConferenceTransferred` | `TRANSFER_CONFERENCE_SUCCESS` | CONSULTING/CONFERENCING branch logic | Initiator/receiver dependent |
|
|
1757
|
+
| API/SDK conference transfer fail | `TRANSFER_CONFERENCE_FAILED` | CONSULTING/CONFERENCING stay | Clears transfer request flag |
|
|
1758
|
+
|
|
1759
|
+
| Backend Event | TaskEvent | State Transition | Context Update |
|
|
1760
|
+
|---|---|---|---|
|
|
1761
|
+
| `ContactRecordingStarted` | `RECORDING_STARTED` | No change | Update recording state |
|
|
1762
|
+
| `ContactRecordingPaused` | `PAUSE_RECORDING` | No change | Mark recording paused |
|
|
1763
|
+
| `ContactRecordingResumed` | `RESUME_RECORDING` | No change | Mark recording active |
|
|
1764
|
+
|
|
1765
|
+
| Backend Event | TaskEvent | State Transition | Notes |
|
|
1766
|
+
|---|---|---|---|
|
|
1767
|
+
| `AgentWrapup` | `TASK_WRAPUP` | → WRAPPING_UP | Enter ACW |
|
|
1768
|
+
| `AgentWrappedup` | `WRAPUP_COMPLETE` | WRAPPING_UP → COMPLETED | Complete wrapup |
|
|
1769
|
+
|
|
1770
|
+
This diagram represents state-to-state lifecycle transitions for the task state machine.
|
|
1771
|
+
|
|
1772
|
+
```mermaid
|
|
1773
|
+
stateDiagram-v2
|
|
1774
|
+
[*] --> IDLE
|
|
1775
|
+
|
|
1776
|
+
%% IDLE
|
|
1777
|
+
IDLE --> OFFERED: AGENT_CONTACT_RESERVED (CC Event) -> TASK_INCOMING (State Machine Event)
|
|
1778
|
+
|
|
1779
|
+
%% OFFERED
|
|
1780
|
+
OFFERED --> OFFERED: AGENT_OFFER_CONTACT (CC Event) -> TASK_OFFERED (State Machine Event)
|
|
1781
|
+
OFFERED --> OFFERED: AGENT_OFFER_CONSULT (CC Event) -> OFFER_CONSULT (State Machine Event)
|
|
1782
|
+
OFFERED --> CONNECTED: AGENT_CONTACT_ASSIGNED (CC Event) -> ASSIGN (State Machine Event)
|
|
1783
|
+
OFFERED --> CONSULTING: AGENT_CONSULTING (CC Event) -> CONSULTING_ACTIVE (State Machine Event)
|
|
1784
|
+
OFFERED --> TERMINATED: AGENT_CONTACT_OFFER_RONA (CC Event) -> RONA (State Machine Event)
|
|
1785
|
+
OFFERED --> TERMINATED: AGENT_CONTACT_ASSIGN_FAILED (CC Event) -> ASSIGN_FAILED (State Machine Event)
|
|
1786
|
+
OFFERED --> TERMINATED: AGENT_INVITE_FAILED (CC Event) -> INVITE_FAILED (State Machine Event)
|
|
1787
|
+
OFFERED --> TERMINATED: AGENT_OUTBOUND_FAILED (CC Event) -> OUTBOUND_FAILED (State Machine Event)
|
|
1788
|
+
OFFERED --> TERMINATED: AGENT_WRAPUP (CC Event) -> TASK_WRAPUP (State Machine Event)
|
|
1789
|
+
|
|
1790
|
+
%% CONNECTED
|
|
1791
|
+
CONNECTED --> HOLD_INITIATING: API hold() -> HOLD_INITIATED (State Machine Event)
|
|
1792
|
+
CONNECTED --> CONSULT_INITIATING: API consult() -> CONSULT (State Machine Event)
|
|
1793
|
+
CONNECTED --> WRAPPING_UP: AGENT_*TRANSFERRED (CC Event) -> TRANSFER_SUCCESS (State Machine Event) [shouldWrapUpOrIsInitiator]
|
|
1794
|
+
CONNECTED --> CONNECTED: AGENT_*TRANSFERRED (CC Event) -> TRANSFER_SUCCESS (State Machine Event) [receiver]
|
|
1795
|
+
CONNECTED --> CONNECTED: AGENT_*TRANSFER_FAILED (CC Event) -> TRANSFER_FAILED (State Machine Event)
|
|
1796
|
+
CONNECTED --> CONFERENCING: CONTACT_ENDED (CC Event) -> CONTACT_ENDED (State Machine Event) [conferenceInProgressFromEvent]
|
|
1797
|
+
CONNECTED --> WRAPPING_UP: CONTACT_ENDED (CC Event) -> CONTACT_ENDED (State Machine Event) [shouldWrapUp]
|
|
1798
|
+
CONNECTED --> TERMINATED: CONTACT_ENDED (CC Event) -> CONTACT_ENDED (State Machine Event) [default]
|
|
1799
|
+
CONNECTED --> WRAPPING_UP: AGENT_WRAPUP (CC Event) -> TASK_WRAPUP (State Machine Event)
|
|
1800
|
+
|
|
1801
|
+
%% HOLD_INITIATING
|
|
1802
|
+
HOLD_INITIATING --> HELD: HOLD_SUCCESS (State Machine Event)
|
|
1803
|
+
HOLD_INITIATING --> CONNECTED: HOLD_FAILED (State Machine Event)
|
|
1804
|
+
|
|
1805
|
+
%% HELD
|
|
1806
|
+
HELD --> RESUME_INITIATING: UNHOLD_INITIATED (State Machine Event)
|
|
1807
|
+
HELD --> CONSULT_INITIATING: CONSULT (State Machine Event)
|
|
1808
|
+
HELD --> WRAPPING_UP: TRANSFER_SUCCESS (State Machine Event) [shouldWrapUpOrIsInitiator]
|
|
1809
|
+
HELD --> CONNECTED: TRANSFER_SUCCESS (State Machine Event) [receiver]
|
|
1810
|
+
HELD --> CONFERENCING: CONTACT_ENDED (CC Event) -> CONTACT_ENDED (State Machine Event) [conferenceInProgressFromEvent]
|
|
1811
|
+
HELD --> WRAPPING_UP: CONTACT_ENDED (CC Event) -> CONTACT_ENDED (State Machine Event) [shouldWrapUp]
|
|
1812
|
+
HELD --> TERMINATED: CONTACT_ENDED (CC Event) -> CONTACT_ENDED (State Machine Event) [default]
|
|
1813
|
+
HELD --> WRAPPING_UP: TASK_WRAPUP (State Machine Event)
|
|
1814
|
+
HELD --> HELD: TRANSFER_FAILED (State Machine Event)
|
|
1815
|
+
|
|
1816
|
+
%% RESUME_INITIATING
|
|
1817
|
+
RESUME_INITIATING --> CONNECTED: UNHOLD_SUCCESS (State Machine Event)
|
|
1818
|
+
RESUME_INITIATING --> HELD: UNHOLD_FAILED (State Machine Event)
|
|
1819
|
+
|
|
1820
|
+
%% CONSULT_INITIATING
|
|
1821
|
+
CONSULT_INITIATING --> CONSULTING: CONSULT_SUCCESS (State Machine Event)
|
|
1822
|
+
CONSULT_INITIATING --> CONFERENCING: CONSULT_FAILED (State Machine Event) [consultFromConference]
|
|
1823
|
+
CONSULT_INITIATING --> HELD: CONSULT_FAILED / CTQ_CANCEL (State Machine Event) [isPrimaryMediaOnHold]
|
|
1824
|
+
CONSULT_INITIATING --> CONNECTED: HOLD_FAILED / CONSULT_FAILED / CTQ_CANCEL (State Machine Event) [default]
|
|
1825
|
+
CONSULT_INITIATING --> CONSULT_INITIATING: HOLD_SUCCESS (State Machine Event)
|
|
1826
|
+
|
|
1827
|
+
%% CONSULTING
|
|
1828
|
+
CONSULTING --> CONFERENCING: CONSULT_END (State Machine Event) [consultInitiator && consultFromConference]
|
|
1829
|
+
CONSULTING --> HELD: CONSULT_END (State Machine Event) [consultInitiator]
|
|
1830
|
+
CONSULTING --> TERMINATED: CONSULT_END (State Machine Event) [consulted agent]
|
|
1831
|
+
CONSULTING --> WRAPPING_UP: TRANSFER_SUCCESS (State Machine Event) [shouldWrapUpOrIsInitiator]
|
|
1832
|
+
CONSULTING --> CONNECTED: TRANSFER_SUCCESS (State Machine Event) [receiver]
|
|
1833
|
+
CONSULTING --> WRAPPING_UP: CONTACT_ENDED (CC Event) -> CONTACT_ENDED (State Machine Event) / TASK_WRAPUP (State Machine Event)
|
|
1834
|
+
CONSULTING --> CONNECTED: ASSIGN (State Machine Event)
|
|
1835
|
+
CONSULTING --> CONF_INITIATING: MERGE_TO_CONFERENCE (State Machine Event)
|
|
1836
|
+
CONSULTING --> CONFERENCING: CONFERENCE_START (State Machine Event)
|
|
1837
|
+
CONSULTING --> CONFERENCING: TRANSFER_CONFERENCE_SUCCESS (State Machine Event) [!consultInitiator]
|
|
1838
|
+
CONSULTING --> WRAPPING_UP: TRANSFER_CONFERENCE_SUCCESS (State Machine Event) [shouldWrapUp]
|
|
1839
|
+
CONSULTING --> TERMINATED: TRANSFER_CONFERENCE_SUCCESS (State Machine Event) [default]
|
|
1840
|
+
CONSULTING --> CONSULTING: CONSULTING_ACTIVE / HOLD_SUCCESS / UNHOLD_SUCCESS / TRANSFER_FAILED / TRANSFER_CONFERENCE / TRANSFER_CONFERENCE_FAILED (State Machine Event)
|
|
1841
|
+
|
|
1842
|
+
%% CONF_INITIATING
|
|
1843
|
+
CONF_INITIATING --> CONFERENCING: CONFERENCE_START (State Machine Event)
|
|
1844
|
+
CONF_INITIATING --> CONSULTING: CONFERENCE_FAILED (State Machine Event)
|
|
1845
|
+
|
|
1846
|
+
%% CONFERENCING
|
|
1847
|
+
CONFERENCING --> CONSULT_INITIATING: CONSULT (State Machine Event)
|
|
1848
|
+
CONFERENCING --> WRAPPING_UP: PARTICIPANT_LEAVE (CC Event) -> PARTICIPANT_LEAVE (State Machine Event) [didCurrentAgentLeaveConference && shouldWrapUp]
|
|
1849
|
+
CONFERENCING --> TERMINATED: PARTICIPANT_LEAVE (CC Event) -> PARTICIPANT_LEAVE (State Machine Event) [didCurrentAgentLeaveConference]
|
|
1850
|
+
CONFERENCING --> CONNECTED: PARTICIPANT_LEAVE (CC Event) -> PARTICIPANT_LEAVE (State Machine Event) [shouldDowngradeConferenceToConnected]
|
|
1851
|
+
CONFERENCING --> WRAPPING_UP: CONFERENCE_END (CC Event) -> CONFERENCE_END (State Machine Event) [shouldWrapUp]
|
|
1852
|
+
CONFERENCING --> CONNECTED: CONFERENCE_END (CC Event) -> CONFERENCE_END (State Machine Event) [customerInCall]
|
|
1853
|
+
CONFERENCING --> TERMINATED: CONFERENCE_END (CC Event) -> CONFERENCE_END (State Machine Event) [default]
|
|
1854
|
+
CONFERENCING --> CONFERENCING: CONFERENCE_START / CONSULT_END / HOLD_SUCCESS / UNHOLD_SUCCESS / PARTICIPANT_LEAVE(other) / TRANSFER_CONFERENCE / TRANSFER_CONFERENCE_SUCCESS / TRANSFER_CONFERENCE_FAILED / CONTACT_ENDED (State Machine Event)
|
|
1855
|
+
|
|
1856
|
+
%% WRAPPING_UP / FINAL
|
|
1857
|
+
WRAPPING_UP --> COMPLETED: WRAPUP_COMPLETE (State Machine Event)
|
|
1858
|
+
COMPLETED --> [*]
|
|
1859
|
+
TERMINATED --> [*]
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
The full diagram above is the source of truth. The diagrams below split above flows based on each feature.
|
|
1863
|
+
|
|
1864
|
+
```mermaid
|
|
1865
|
+
stateDiagram-v2
|
|
1866
|
+
[*] --> IDLE
|
|
1867
|
+
IDLE --> OFFERED: AGENT_CONTACT_RESERVED -> TASK_INCOMING
|
|
1868
|
+
OFFERED --> OFFERED: AGENT_CONTACT_OFFER -> TASK_OFFERED
|
|
1869
|
+
OFFERED --> OFFERED: AGENT_CONSULT_OFFER -> OFFER_CONSULT
|
|
1870
|
+
OFFERED --> CONNECTED: AGENT_CONTACT_ASSIGNED -> ASSIGN
|
|
1871
|
+
OFFERED --> TERMINATED: AGENT_CONTACT_OFFER_RONA/AGENT_CONTACT_ASSIGN_FAILED/AGENT_INVITE_FAILED/AGENT_OUTBOUND_FAILED/AGENT_WRAPUP -> RONA/ASSIGN_FAILED/INVITE_FAILED/OUTBOUND_FAILED/TASK_WRAPUP
|
|
1872
|
+
|
|
1873
|
+
CONNECTED --> HOLD_INITIATING: task.hold() -> HOLD_INITIATED
|
|
1874
|
+
CONNECTED --> CONSULT_INITIATING: task.consult() -> CONSULT
|
|
1875
|
+
|
|
1876
|
+
CONNECTED --> CONNECTED: PAUSE_RECORDING/RESUME_RECORDING
|
|
1877
|
+
CONNECTED --> CONNECTED: AGENT_BLIND_TRANSFER_FAILED/AGENT_VTEAM_TRANSFER_FAILED -> TRANSFER_FAILED
|
|
1878
|
+
|
|
1879
|
+
CONNECTED --> WRAPPING_UP: AGENT_BLIND_TRANSFERRED/AGENT_VTEAM_TRANSFERRED -> TRANSFER_SUCCESS [shouldWrapUpOrIsInitiator]
|
|
1880
|
+
CONNECTED --> WRAPPING_UP: CONTACT_ENDED -> CONTACT_ENDED [shouldWrapUp]
|
|
1881
|
+
CONNECTED --> WRAPPING_UP: AGENT_WRAPUP -> TASK_WRAPUP
|
|
1882
|
+
CONNECTED --> TERMINATED: CONTACT_ENDED -> CONTACT_ENDED [default]
|
|
1883
|
+
|
|
1884
|
+
WRAPPING_UP --> COMPLETED: AGENT_WRAPPEDUP -> WRAPUP_COMPLETE
|
|
1885
|
+
COMPLETED --> [*]
|
|
1886
|
+
TERMINATED --> [*]
|
|
1887
|
+
```
|
|
1888
|
+
|
|
1889
|
+
```mermaid
|
|
1890
|
+
stateDiagram-v2
|
|
1891
|
+
[*] --> CONNECTED
|
|
1892
|
+
CONNECTED --> HOLD_INITIATING: task.hold() -> HOLD_INITIATED
|
|
1893
|
+
HOLD_INITIATING --> HELD: AGENT_CONTACT_HELD -> HOLD_SUCCESS
|
|
1894
|
+
HOLD_INITIATING --> CONNECTED: AGENT_CONTACT_HOLD_FAILED -> HOLD_FAILED
|
|
1895
|
+
HELD --> RESUME_INITIATING: task.resume() -> UNHOLD_INITIATED
|
|
1896
|
+
RESUME_INITIATING --> CONNECTED: AGENT_CONTACT_UNHELD -> UNHOLD_SUCCESS
|
|
1897
|
+
RESUME_INITIATING --> HELD: AGENT_CONTACT_UNHOLD_FAILED -> UNHOLD_FAILED
|
|
1898
|
+
HELD --> CONSULT_INITIATING: task.consult() -> Consult
|
|
1899
|
+
CONSULT_INITIATING --> HELD: CONSULT_FAILED / CTQ_CANCEL [isPrimaryMediaOnHold]
|
|
1900
|
+
HELD --> WRAPPING_UP: CONTACT_ENDED -> CONTACT_ENDED [shouldWrapUp]
|
|
1901
|
+
HELD --> TERMINATED: CONTACT_ENDED -> CONTACT_ENDED [default]
|
|
1902
|
+
|
|
1903
|
+
WRAPPING_UP --> COMPLETED: AGENT_WRAPPEDUP -> WRAPUP_COMPLETE
|
|
1904
|
+
COMPLETED --> [*]
|
|
1905
|
+
TERMINATED --> [*]
|
|
1906
|
+
```
|
|
1907
|
+
|
|
1908
|
+
```mermaid
|
|
1909
|
+
stateDiagram-v2
|
|
1910
|
+
stateDiagram-v2
|
|
1911
|
+
[*] --> CONNECTED
|
|
1912
|
+
CONNECTED --> CONSULT_INITIATING: task.consult() -> CONSULT
|
|
1913
|
+
HELD --> CONSULT_INITIATING: task.consult() -> CONSULT
|
|
1914
|
+
CONFERENCING --> CONSULT_INITIATING: task.consult() -> CONSULT
|
|
1915
|
+
|
|
1916
|
+
CONSULT_INITIATING --> CONSULT_INITIATING: AGENT_CONTACT_HELD -> HOLD_SUCCESS
|
|
1917
|
+
CONSULT_INITIATING --> CONNECTED: AGENT_CONTACT_HOLD_FAILED -> HOLD_FAILED
|
|
1918
|
+
CONSULT_INITIATING --> CONSULTING: API consult success -> CONSULT_SUCCESS
|
|
1919
|
+
CONSULT_INITIATING --> HELD: AGENT_CONSULT_FAILED/AGENT_CTQ_FAILED -> CONSULT_FAILED [isPrimaryMediaOnHold]
|
|
1920
|
+
CONSULT_INITIATING --> CONNECTED: AGENT_CONSULT_FAILED/AGENT_CTQ_FAILED -> CONSULT_FAILED [default]
|
|
1921
|
+
CONSULT_INITIATING --> CONFERENCING: AGENT_CONSULT_FAILED -> CONSULT_FAILED [consultFromConference]
|
|
1922
|
+
CONSULT_INITIATING --> HELD: AGENT_CTQ_CANCELLED -> CTQ_CANCEL [isPrimaryMediaOnHold]
|
|
1923
|
+
CONSULT_INITIATING --> CONNECTED: AGENT_CTQ_CANCELLED -> CTQ_CANCEL [default]
|
|
1924
|
+
|
|
1925
|
+
CONSULTING --> HELD: AGENT_CONSULT_ENDED -> CONSULT_END [consultInitiator]
|
|
1926
|
+
CONSULTING --> TERMINATED: AGENT_CONSULT_ENDED -> CONSULT_END [consulted agent]
|
|
1927
|
+
CONSULTING --> CONFERENCING: AGENT_CONSULT_ENDED -> CONSULT_END [consultInitiator && consultFromConference]
|
|
1928
|
+
CONSULTING --> CONNECTED: AGENT_CONSULT_TRANSFERRED/AGENT_CONTACT_ASSIGNED -> TRANSFER_SUCCESS/ASSIGN
|
|
1929
|
+
CONSULTING --> WRAPPING_UP: AGENT_CONSULT_TRANSFERRED -> TRANSFER_SUCCESS [shouldWrapUpOrIsInitiator]
|
|
1930
|
+
CONSULTING --> CONSULTING: AGENT_CONSULT_TRANSFER_FAILED -> TRANSFER_FAILED
|
|
1931
|
+
CONSULTING --> CONF_INITIATING: task.consultConference() -> MERGE_TO_CONFERENCE
|
|
1932
|
+
CONSULTING --> WRAPPING_UP: AGENT_CONTACT_ENDED -> CONTACT_ENDED
|
|
1933
|
+
|
|
1934
|
+
WRAPPING_UP --> COMPLETED: AGENT_WRAPPEDUP -> WRAPUP_COMPLETE
|
|
1935
|
+
COMPLETED --> [*]
|
|
1936
|
+
TERMINATED --> [*]
|
|
1937
|
+
```
|
|
1938
|
+
|
|
1939
|
+
```mermaid
|
|
1940
|
+
stateDiagram-v2
|
|
1941
|
+
[*] --> CONSULTING
|
|
1942
|
+
CONSULTING --> CONF_INITIATING: task.consultConference() -> MERGE_TO_CONFERENCE
|
|
1943
|
+
CONF_INITIATING --> CONFERENCING: AGENT_CONSULT_CONFERENCED -> CONFERENCE_START
|
|
1944
|
+
CONF_INITIATING --> CONSULTING: AGENT_CONSULT_CONFERENCE_FAILED -> CONFERENCE_FAILED
|
|
1945
|
+
CONFERENCING --> CONSULT_INITIATING: task.consult() -> CONSULT
|
|
1946
|
+
CONSULTING --> CONFERENCING: AGENT_CONFERENCE_TRANSFERRED -> TRANSFER_CONFERENCE_SUCCESS [!consultInitiator]
|
|
1947
|
+
CONSULTING --> WRAPPING_UP: AGENT_CONFERENCE_TRANSFERRED -> TRANSFER_CONFERENCE_SUCCESS [shouldWrapUp]
|
|
1948
|
+
CONSULTING --> TERMINATED: AGENT_CONFERENCE_TRANSFERRED -> TRANSFER_CONFERENCE_SUCCESS [default]
|
|
1949
|
+
CONSULTING --> CONSULTING: AGENT_CONFERENCE_TRANSFER_FAILED -> TRANSFER_CONFERENCE_FAILED
|
|
1950
|
+
CONFERENCING --> CONFERENCING: AGENT_CONFERENCE_TRANSFERRED -> TRANSFER_CONFERENCE_SUCCESS
|
|
1951
|
+
CONFERENCING --> CONFERENCING: AGENT_CONFERENCE_TRANSFER_FAILED-> TRANSFER_CONFERENCE_FAILED
|
|
1952
|
+
|
|
1953
|
+
CONFERENCING --> WRAPPING_UP: PARTICIPANT_LEFT_CONFERENCE -> PARTICIPANT_LEAVE [didCurrentAgentLeaveConference && shouldWrapUp]
|
|
1954
|
+
CONFERENCING --> TERMINATED: PARTICIPANT_LEFT_CONFERENCE -> PARTICIPANT_LEAVE [didCurrentAgentLeaveConference]
|
|
1955
|
+
CONFERENCING --> CONNECTED: PARTICIPANT_LEFT_CONFERENCE -> PARTICIPANT_LEAVE [shouldDowngradeConferenceToConnected]
|
|
1956
|
+
CONFERENCING --> WRAPPING_UP: AGENT_CONSULT_CONFERENCE_ENDED -> CONFERENCE_END [shouldWrapUp]
|
|
1957
|
+
CONFERENCING --> CONNECTED: AGENT_CONSULT_CONFERENCE_ENDED -> CONFERENCE_END [customerInCall]
|
|
1958
|
+
CONFERENCING --> TERMINATED: AGENT_CONSULT_CONFERENCE_ENDED -> CONFERENCE_END [default]
|
|
1959
|
+
WRAPPING_UP --> COMPLETED: WRAPUP_COMPLETE
|
|
1960
|
+
COMPLETED --> [*]
|
|
1961
|
+
TERMINATED --> [*]
|
|
1962
|
+
```
|
|
1963
|
+
|
|
1964
|
+
```typescript
|
|
1965
|
+
import {setup} from 'xstate';
|
|
1966
|
+
import {actions} from './actions';
|
|
1967
|
+
import {guards} from './guards';
|
|
1968
|
+
|
|
1969
|
+
const taskStateMachine = setup({
|
|
1970
|
+
types: {
|
|
1971
|
+
context: {} as TaskContext,
|
|
1972
|
+
events: {} as TaskEventPayload,
|
|
1973
|
+
},
|
|
1974
|
+
})
|
|
1975
|
+
.createMachine({
|
|
1976
|
+
id: 'taskStateMachine',
|
|
1977
|
+
initial: TaskState.IDLE,
|
|
1978
|
+
context: createInitialContext(uiControlConfig, TaskState.IDLE),
|
|
1979
|
+
states: {
|
|
1980
|
+
[TaskState.IDLE]: {
|
|
1981
|
+
on: {
|
|
1982
|
+
[TaskEvent.TASK_INCOMING]: {
|
|
1983
|
+
target: TaskState.OFFERED,
|
|
1984
|
+
actions: ['initializeTask', 'emitTaskIncoming'],
|
|
1985
|
+
},
|
|
1986
|
+
[TaskEvent.HYDRATE]: [
|
|
1987
|
+
{
|
|
1988
|
+
guard: guards.isInteractionTerminated,
|
|
1989
|
+
target: TaskState.WRAPPING_UP,
|
|
1990
|
+
actions: ['updateTaskData', 'markEnded', 'emitTaskHydrate'],
|
|
1991
|
+
},
|
|
1992
|
+
// ... more hydrate cases
|
|
1993
|
+
],
|
|
1994
|
+
},
|
|
1995
|
+
},
|
|
1996
|
+
[TaskState.OFFERED]: {
|
|
1997
|
+
on: {
|
|
1998
|
+
[TaskEvent.ASSIGN]: {
|
|
1999
|
+
target: TaskState.CONNECTED,
|
|
2000
|
+
actions: ['updateTaskData', 'emitTaskAssigned'],
|
|
2001
|
+
},
|
|
2002
|
+
[TaskEvent.TASK_WRAPUP]: {
|
|
2003
|
+
target: TaskState.TERMINATED,
|
|
2004
|
+
actions: ['updateTaskData', 'markEnded', 'emitTaskEnd'],
|
|
2005
|
+
},
|
|
2006
|
+
// ... more transitions
|
|
2007
|
+
},
|
|
2008
|
+
},
|
|
2009
|
+
// ... more states
|
|
2010
|
+
},
|
|
2011
|
+
})
|
|
2012
|
+
.provide({actions});
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
The HYDRATE event restores state machine state after page refresh or reconnection.
|
|
2016
|
+
|
|
2017
|
+
**Algorithm**:
|
|
2018
|
+
|
|
2019
|
+
1. Receive HYDRATE event with full task data
|
|
2020
|
+
|
|
2021
|
+
2. Check interaction state and flags in order of precedence:
|
|
2022
|
+
|
|
2023
|
+
- If `taskData.interaction.isTerminated === true` -> WRAPPING_UP
|
|
2024
|
+
|
|
2025
|
+
- If `taskData.interaction.state === 'consulting'` -> CONSULTING
|
|
2026
|
+
|
|
2027
|
+
- If `taskData.interaction.state === 'hold'` -> HELD
|
|
2028
|
+
|
|
2029
|
+
- If `taskData.interaction.state === 'connected'` -> CONNECTED
|
|
2030
|
+
|
|
2031
|
+
- If conferencing-by-participants guard passes (`agentCount >= 2` in main call) -> CONFERENCING
|
|
2032
|
+
|
|
2033
|
+
- Default → Stay in IDLE
|
|
2034
|
+
|
|
2035
|
+
3. Update context with hydrated data
|
|
2036
|
+
|
|
2037
|
+
4. Emit TASK_HYDRATE event
|
|
2038
|
+
|
|
2039
|
+
### Action implementation ownership
|
|
2040
|
+
|
|
2041
|
+
- `syncTaskDataFromEvent` appears in the reusable graph but its implementation is supplied in Task's machine options in `src/services/task/Task.ts`; it is intentionally absent from the default `actions.ts` map.
|
|
2042
|
+
- `didInitiateConsult` exists in `guards.ts` but is not referenced by the current graph. Treat it as defined-but-unwired.
|
|
2043
|
+
- `getDefaultUIControls` is package-public through `src/index.ts`; the voice/digital computation helpers are private implementation details.
|
|
2044
|
+
|
|
2045
|
+
## Pitfalls
|
|
2046
|
+
- `syncTaskDataFromEvent` is declared by the graph but implemented by Task; adding a competing default action loses integration-owned normalization.
|
|
2047
|
+
- `handleConferenceFailed`, `handleSwitchToMainCall`, and `handleSwitchToConsult` are wired actions, while `forceConsultInitiator` remains defined but unwired.
|
|
2048
|
+
- Guards/actions must remain deterministic and transport-free; logging, metrics, authentication, and request side effects belong to Task/TaskManager.
|
|
2049
|
+
|
|
2050
|
+
- `RONA`, `INVITE_FAILED`, `ASSIGN_FAILED`, `OUTBOUND_FAILED`
|
|
2051
|
+
|
|
2052
|
+
| Backend Event | TaskEvent | State Transition | Notes |
|
|
2053
|
+
|---|---|---|---|
|
|
2054
|
+
| `AgentContactOfferRona` | `RONA` | OFFERED → TERMINATED | Redirection on no answer |
|
|
2055
|
+
| `AgentInviteFailed` | `INVITE_FAILED` | OFFERED → TERMINATED | Invite failed |
|
|
2056
|
+
| `AgentContactAssignFailed` | `ASSIGN_FAILED` | OFFERED → TERMINATED | Assignment failed |
|
|
2057
|
+
| `AgentOutboundFailed` | `OUTBOUND_FAILED` | OFFERED → TERMINATED | Outdial failed |
|
|
2058
|
+
|
|
2059
|
+
## Module Do's / Don'ts
|
|
2060
|
+
- DO map backend notifications to typed `TaskEvent` values before sending them to the actor.
|
|
2061
|
+
- DO preserve initiating states separately from success/failure stable states.
|
|
2062
|
+
- DON'T invoke WebexRequest, LoggerProxy, or MetricsManager from the state-machine layer.
|
|
2063
|
+
- DON'T document private UI-control helpers as public APIs or claim an action is absent without checking graph wiring.
|
|
2064
|
+
|
|
2065
|
+
## Key Design Trade-off
|
|
2066
|
+
- Backend events are mapped into a typed internal event vocabulary before transition evaluation, isolating the state graph from transport details.
|
|
2067
|
+
|
|
2068
|
+
## Test-Case Strategy (module)
|
|
2069
|
+
Use `test/unit/spec/services/task/state-machine/TaskStateMachine.ts`, `guards.ts`, and `uiControlsComputer.ts` to cover every documented transition/action/guard/control claim. Use `test/unit/spec/services/task/Task.ts` for injected `syncTaskDataFromEvent` behavior. Explicitly assert defined-but-unwired status for `forceConsultInitiator` and `didInitiateConsult` until source wiring changes.
|
|
2070
|
+
|
|
2071
|
+
| Behavior / Requirement | Existing test evidence | Gap |
|
|
2072
|
+
|---|---|---|
|
|
2073
|
+
| `TASK_STATE_MACHINE-R-001` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | Keep transition coverage synchronized with all mapped TaskEvent groups. |
|
|
2074
|
+
| `TASK_STATE_MACHINE-R-002` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | Retain explicit wired/unwired assertions. |
|
|
2075
|
+
| `TASK_STATE_MACHINE-R-003` | `test/unit/spec/services/task/Task.ts` | None. |
|
|
2076
|
+
| `TASK_STATE_MACHINE-R-004` | `test/unit/spec/services/task/state-machine/uiControlsComputer.ts` | None. |
|
|
2077
|
+
| `TASK_STATE_MACHINE-R-005` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts` | Transport/auth absence is also verified from imports. |
|
|
2078
|
+
| `TASK_STATE_MACHINE-R-006` | `test/unit/spec/services/task/Task.ts` | None. |
|
|
2079
|
+
| `TASK_STATE_MACHINE-R-007` | `test/unit/spec/services/task/state-machine/TaskStateMachine.ts`, `test/unit/spec/services/task/Task.ts` | Observability absence is also verified from imports. |
|
|
2080
|
+
|
|
2081
|
+
## Traceability
|
|
2082
|
+
- Repo architecture: `../../../../../ai-docs/ARCHITECTURE.md` · Registry: `../../../../../ai-docs/SPEC_INDEX.md`
|
|
2083
|
+
- Coverage state and contracts baseline: `../../../../../.sdd/manifest.json`
|
|
2084
|
+
|
|
2085
|
+
- Task lifecycle state machine: `TaskStateMachine.ts`
|
|
2086
|
+
|
|
2087
|
+
- State machine types/events: `constants.ts`, `types.ts`
|
|
2088
|
+
|
|
2089
|
+
- Guard logic: `guards.ts`
|
|
2090
|
+
|
|
2091
|
+
- Actions and context mutation: `actions.ts`
|
|
2092
|
+
|
|
2093
|
+
- UI control computation: `uiControlsComputer.ts`
|
|
2094
|
+
|
|
2095
|
+
`computeUIControls()` in `uiControlsComputer.ts`.
|
|
2096
|
+
|
|
2097
|
+
- [../../ai-docs/task-spec.md](../../ai-docs/task-spec.md) - Task service usage guide
|
|
2098
|
+
|
|
2099
|
+
- [../../ai-docs/task-spec.md](../../ai-docs/task-spec.md) - Task service architecture
|
|
2100
|
+
|
|
2101
|
+
- `../Task.ts` - actor lifecycle, action overrides, event emission
|
|
2102
|
+
|
|
2103
|
+
- `../TaskManager.ts` - maps backend events to state-machine events
|
|
2104
|
+
|
|
2105
|
+
- `../types.ts` - shared task data structures
|
|
2106
|
+
|
|
2107
|
+
- `../../ai-docs/task-spec.md` - broader task service architecture
|
|
2108
|
+
|
|
2109
|
+
- [TaskStateMachine.ts](../TaskStateMachine.ts) - Implementation
|
|
2110
|
+
|
|
2111
|
+
- [guards.ts](../guards.ts) - Guard functions
|
|
2112
|
+
|
|
2113
|
+
- [actions.ts](../actions.ts) - Action functions
|
|
2114
|
+
|
|
2115
|
+
- [constants.ts](../constants.ts) - State and event enums
|