@xema/omni-protocol 0.1.2 → 0.1.4
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/dist/index.d.ts +120 -3
- package/dist/index.js +1 -1
- package/dist/testing.d.ts +4 -0
- package/dist/testing.js +7 -1
- package/dist/validation.js +92 -1
- package/guide.md +211 -4
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -290,7 +290,13 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
|
|
|
290
290
|
mute?: true;
|
|
291
291
|
hold?: true;
|
|
292
292
|
agentDisconnect?: true;
|
|
293
|
+
/** Reach the party again while `completing`; the task returns to `in-progress`. */
|
|
294
|
+
callback?: true;
|
|
293
295
|
blindTransfer?: true | DestinationDirectory;
|
|
296
|
+
/** Park the customer and call a destination first; then `complete` or `cancel`. */
|
|
297
|
+
consultTransfer?: true | DestinationDirectory;
|
|
298
|
+
/** Ask a lead to join this call, with a note. The lead's decision arrives on `Task.lead`. */
|
|
299
|
+
consultLead?: true;
|
|
294
300
|
conference?: true | DestinationDirectory;
|
|
295
301
|
recording?: true;
|
|
296
302
|
} : C extends "chat" ? SharedTaskCapabilities & {
|
|
@@ -390,6 +396,35 @@ export type TaskCompletion = {
|
|
|
390
396
|
completionMode: "provider-automatic";
|
|
391
397
|
completionAllowance: DurationSeconds;
|
|
392
398
|
};
|
|
399
|
+
/**
|
|
400
|
+
* A consultation in progress on a task: who is being consulted, and since when where the provider
|
|
401
|
+
* records it. Present between `transfer` `consult` and whichever of `complete` or `cancel`
|
|
402
|
+
* follows; its presence is what makes those two issuable.
|
|
403
|
+
*/
|
|
404
|
+
export interface TaskConsultation {
|
|
405
|
+
destination: string;
|
|
406
|
+
label?: string;
|
|
407
|
+
since?: IsoTimestamp;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The agent's request for a lead, from asking until the lead leaves or the request ends.
|
|
411
|
+
* `requested` while nobody has joined; `joined`, with `leadId`, once somebody has.
|
|
412
|
+
*/
|
|
413
|
+
export interface TaskLead {
|
|
414
|
+
status: "requested" | "joined";
|
|
415
|
+
leadId?: UserId;
|
|
416
|
+
note?: string;
|
|
417
|
+
since: IsoTimestamp;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* On the lead's own task for a call they joined: which member asked, with their note. Its
|
|
421
|
+
* presence is what makes `lead` `take-over` and `leave` issuable.
|
|
422
|
+
*/
|
|
423
|
+
export interface TaskAssisting {
|
|
424
|
+
memberId: UserId;
|
|
425
|
+
note?: string;
|
|
426
|
+
since: IsoTimestamp;
|
|
427
|
+
}
|
|
393
428
|
export type Task<C extends Channel = Channel> = {
|
|
394
429
|
id: TaskId;
|
|
395
430
|
title: string;
|
|
@@ -404,7 +439,15 @@ export type Task<C extends Channel = Channel> = {
|
|
|
404
439
|
reference?: string;
|
|
405
440
|
attributes?: TaskAttribute[];
|
|
406
441
|
handlingHistory?: TaskHandlingStep[];
|
|
407
|
-
} & TaskCompletion
|
|
442
|
+
} & TaskCompletion & (C extends "voice" ? {
|
|
443
|
+
consultation?: TaskConsultation;
|
|
444
|
+
lead?: TaskLead;
|
|
445
|
+
assisting?: TaskAssisting;
|
|
446
|
+
} : {
|
|
447
|
+
consultation?: never;
|
|
448
|
+
lead?: never;
|
|
449
|
+
assisting?: never;
|
|
450
|
+
});
|
|
408
451
|
/** What the provider wants of Omni's acceptance policy for one offer. */
|
|
409
452
|
export type AcceptanceMode = "no-preference" | "require-agent-acceptance" | "require-automatic-acceptance";
|
|
410
453
|
export type TaskOutcome = {
|
|
@@ -421,12 +464,16 @@ export type TaskOutcome = {
|
|
|
421
464
|
| {
|
|
422
465
|
type: "expired";
|
|
423
466
|
phase: "pending" | "confirmed" | "preparing";
|
|
467
|
+
}
|
|
468
|
+
/** This agent left a call that continues without them: a lead who joined and dropped. */
|
|
469
|
+
| {
|
|
470
|
+
type: "left";
|
|
424
471
|
} | {
|
|
425
472
|
type: "failed";
|
|
426
473
|
failure: ProtocolFailure;
|
|
427
474
|
};
|
|
428
475
|
export declare const TASK_COMMAND_NAMES: {
|
|
429
|
-
readonly voice: readonly ["answer", "decline", "start-call", "mute", "hold", "resume", "disconnect", "transfer", "conference", "recording", "complete"];
|
|
476
|
+
readonly voice: readonly ["answer", "decline", "start-call", "mute", "hold", "resume", "disconnect", "callback", "transfer", "lead", "conference", "recording", "complete"];
|
|
430
477
|
readonly chat: readonly ["accept", "reject", "pause", "resume", "complete"];
|
|
431
478
|
readonly email: readonly ["accept", "reject", "complete"];
|
|
432
479
|
};
|
|
@@ -450,9 +497,53 @@ export type VoiceTaskCommand = {
|
|
|
450
497
|
type: "resume";
|
|
451
498
|
} | {
|
|
452
499
|
type: "disconnect";
|
|
453
|
-
}
|
|
500
|
+
}
|
|
501
|
+
/** Issuable only in `completing`, under the `callback` capability. Carries no destination. */
|
|
502
|
+
| {
|
|
503
|
+
type: "callback";
|
|
504
|
+
}
|
|
505
|
+
/** Blind: hand the customer to `destination` with nobody consulted. Gated by `blindTransfer`. */
|
|
506
|
+
| {
|
|
507
|
+
type: "transfer";
|
|
508
|
+
destination: string;
|
|
509
|
+
action?: never;
|
|
510
|
+
}
|
|
511
|
+
/** Park the customer and call `destination` first. Gated by `consultTransfer`. */
|
|
512
|
+
| {
|
|
454
513
|
type: "transfer";
|
|
514
|
+
action: "consult";
|
|
455
515
|
destination: string;
|
|
516
|
+
}
|
|
517
|
+
/** Hand the customer to the consulted destination and leave. Needs `Task.consultation`. */
|
|
518
|
+
| {
|
|
519
|
+
type: "transfer";
|
|
520
|
+
action: "complete";
|
|
521
|
+
}
|
|
522
|
+
/** Drop the consulted destination and return to the customer. Needs `Task.consultation`. */
|
|
523
|
+
| {
|
|
524
|
+
type: "transfer";
|
|
525
|
+
action: "cancel";
|
|
526
|
+
}
|
|
527
|
+
/** Ask a lead to join, with a note. Gated by `consultLead`. */
|
|
528
|
+
| {
|
|
529
|
+
type: "lead";
|
|
530
|
+
action: "request";
|
|
531
|
+
note?: string;
|
|
532
|
+
}
|
|
533
|
+
/** Withdraw a standing request. Needs `Task.lead` with status `requested`. */
|
|
534
|
+
| {
|
|
535
|
+
type: "lead";
|
|
536
|
+
action: "cancel";
|
|
537
|
+
}
|
|
538
|
+
/** The lead keeps the customer; the agent's task ends `transferred`. Needs `Task.assisting`. */
|
|
539
|
+
| {
|
|
540
|
+
type: "lead";
|
|
541
|
+
action: "take-over";
|
|
542
|
+
}
|
|
543
|
+
/** The lead drops; the agent continues. The lead's task ends `left`. Needs `Task.assisting`. */
|
|
544
|
+
| {
|
|
545
|
+
type: "lead";
|
|
546
|
+
action: "leave";
|
|
456
547
|
} | {
|
|
457
548
|
type: "conference";
|
|
458
549
|
participant: string;
|
|
@@ -630,9 +721,33 @@ export interface TeamMember {
|
|
|
630
721
|
* Published only to an agent entitled to one. Its presence is the permission -- nothing else
|
|
631
722
|
* makes somebody a lead, and there is no separate flag to fall out of step with the data.
|
|
632
723
|
*/
|
|
724
|
+
/** A member asking this lead to join their call. */
|
|
725
|
+
export interface LeadRequest {
|
|
726
|
+
id: string;
|
|
727
|
+
memberId: UserId;
|
|
728
|
+
taskId: TaskId;
|
|
729
|
+
note?: string;
|
|
730
|
+
since: IsoTimestamp;
|
|
731
|
+
}
|
|
633
732
|
export interface TeamRoster {
|
|
634
733
|
members: TeamMember[];
|
|
635
734
|
breakControl?: true;
|
|
735
|
+
/** Present when this lead may join a member's call on request. */
|
|
736
|
+
consultControl?: true;
|
|
737
|
+
/** Omitted when the lead may not be asked; `[]` when nobody is asking. */
|
|
738
|
+
requests?: LeadRequest[];
|
|
739
|
+
}
|
|
740
|
+
export type TeamConsultCommand = {
|
|
741
|
+
type: "join";
|
|
742
|
+
requestId: string;
|
|
743
|
+
} | {
|
|
744
|
+
type: "decline";
|
|
745
|
+
requestId: string;
|
|
746
|
+
reason?: string;
|
|
747
|
+
};
|
|
748
|
+
export interface TeamConsultCommandRequest {
|
|
749
|
+
commandId: string;
|
|
750
|
+
command: TeamConsultCommand;
|
|
636
751
|
}
|
|
637
752
|
export type TeamBreakCommand = {
|
|
638
753
|
type: "decide";
|
|
@@ -799,6 +914,8 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
799
914
|
endBreak?(): Promise<BreakEndResult>;
|
|
800
915
|
/** Required when the adapter publishes a `TeamRoster` carrying `breakControl`. */
|
|
801
916
|
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
|
917
|
+
/** Required when the adapter publishes a `TeamRoster` carrying `consultControl`. */
|
|
918
|
+
executeTeamConsult?(request: TeamConsultCommandRequest): Promise<TeamCommandResult>;
|
|
802
919
|
/** Required of every voice adapter: all voice audio lands in Omni. */
|
|
803
920
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
804
921
|
}
|
package/dist/index.js
CHANGED
|
@@ -55,7 +55,7 @@ export function isAllowedBrowserUrl(url) {
|
|
|
55
55
|
// ---------------------------------------------------------------------------
|
|
56
56
|
export const TASK_COMMAND_NAMES = {
|
|
57
57
|
voice: ["answer", "decline", "start-call", "mute", "hold", "resume", "disconnect",
|
|
58
|
-
"transfer", "conference", "recording", "complete"],
|
|
58
|
+
"callback", "transfer", "lead", "conference", "recording", "complete"],
|
|
59
59
|
chat: ["accept", "reject", "pause", "resume", "complete"],
|
|
60
60
|
email: ["accept", "reject", "complete"],
|
|
61
61
|
};
|
package/dist/testing.d.ts
CHANGED
|
@@ -39,6 +39,10 @@ export declare function assertReconnectWithMissedAssignments<C extends Channel>(
|
|
|
39
39
|
* There is no `denied` approval: a refusal returns the agent to `not-requested`, because a
|
|
40
40
|
* pending request nobody is coming to decide is worse than none. So the scenario is a request
|
|
41
41
|
* that goes back to not-requested, and a later one that is granted.
|
|
42
|
+
*
|
|
43
|
+
* A request shows as `awaiting-decision` where a person decides, and as `granted` at once where
|
|
44
|
+
* the provider decides alone; either is the request being made, and the rule is the same for
|
|
45
|
+
* both.
|
|
42
46
|
*/
|
|
43
47
|
export declare function assertDeniedAndRetriedBreak(approvals: readonly BreakApproval[]): void;
|
|
44
48
|
/** Verifies that retrying one dial command cannot place a second call. */
|
package/dist/testing.js
CHANGED
|
@@ -76,6 +76,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
if (snapshot?.team?.breakControl === true)
|
|
78
78
|
requireMethod("executeTeamBreak", "the roster carries breakControl");
|
|
79
|
+
if (snapshot?.team?.consultControl === true)
|
|
80
|
+
requireMethod("executeTeamConsult", "the roster carries consultControl");
|
|
79
81
|
if (publishesUserIds(snapshot))
|
|
80
82
|
requireMethod("describeUsers", "the snapshot publishes a UserId");
|
|
81
83
|
// Capacity is stated, not requested: nothing may be allocated until it is, so a connection
|
|
@@ -209,9 +211,13 @@ export function assertReconnectWithMissedAssignments(before, reconnect, missedTa
|
|
|
209
211
|
* There is no `denied` approval: a refusal returns the agent to `not-requested`, because a
|
|
210
212
|
* pending request nobody is coming to decide is worse than none. So the scenario is a request
|
|
211
213
|
* that goes back to not-requested, and a later one that is granted.
|
|
214
|
+
*
|
|
215
|
+
* A request shows as `awaiting-decision` where a person decides, and as `granted` at once where
|
|
216
|
+
* the provider decides alone; either is the request being made, and the rule is the same for
|
|
217
|
+
* both.
|
|
212
218
|
*/
|
|
213
219
|
export function assertDeniedAndRetriedBreak(approvals) {
|
|
214
|
-
const asked = approvals.
|
|
220
|
+
const asked = approvals.findIndex(approval => approval === "awaiting-decision" || approval === "granted");
|
|
215
221
|
if (asked < 0)
|
|
216
222
|
throw new Error("Break retry scenario requires an initial request");
|
|
217
223
|
const refused = approvals.indexOf("not-requested", asked + 1);
|
package/dist/validation.js
CHANGED
|
@@ -79,7 +79,7 @@ const ISOLATION_SCHEME_VALUES = Object.values(BROWSER_ISOLATION_SCHEMES);
|
|
|
79
79
|
const TASK_CAPABILITIES = {
|
|
80
80
|
voice: membersOf({
|
|
81
81
|
browsers: true, dispositions: true, custom: true, decline: true, mute: true, hold: true,
|
|
82
|
-
agentDisconnect: true, blindTransfer: true, conference: true, recording: true,
|
|
82
|
+
agentDisconnect: true, callback: true, blindTransfer: true, consultTransfer: true, consultLead: true, conference: true, recording: true,
|
|
83
83
|
}),
|
|
84
84
|
chat: membersOf({ browsers: true, dispositions: true, custom: true, reject: true, hold: true }),
|
|
85
85
|
email: membersOf({ browsers: true, dispositions: true, custom: true, reject: true }),
|
|
@@ -502,6 +502,61 @@ function validateHandlingHistory(value, path, into) {
|
|
|
502
502
|
}
|
|
503
503
|
});
|
|
504
504
|
}
|
|
505
|
+
/** Present only while consulting, and only on voice: elsewhere there is nobody to consult. */
|
|
506
|
+
function validateConsultation(value, channel, path, into) {
|
|
507
|
+
if (value === undefined)
|
|
508
|
+
return;
|
|
509
|
+
if (!into.require(channel === "voice", "task.consultation.channel", path, `a ${channel} task cannot carry a consultation`))
|
|
510
|
+
return;
|
|
511
|
+
if (!isPlainObject(value)) {
|
|
512
|
+
into.add("task.consultation.shape", path, "a consultation must be an object when present");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
into.filled(value.destination, "task.consultation.destination", `${path}.destination`, "a consultation names the destination being consulted");
|
|
516
|
+
if (value.label !== undefined) {
|
|
517
|
+
into.filled(value.label, "task.consultation.label", `${path}.label`, "a label must not be empty when present");
|
|
518
|
+
}
|
|
519
|
+
if (value.since !== undefined)
|
|
520
|
+
into.timestamp(value.since, "task.consultation.since", `${path}.since`);
|
|
521
|
+
}
|
|
522
|
+
const LEAD_STATUSES = membersOf({ requested: true, joined: true });
|
|
523
|
+
/** The agent's request for a lead. Voice only; `joined` names the lead, `requested` cannot. */
|
|
524
|
+
function validateLead(value, channel, path, into) {
|
|
525
|
+
if (value === undefined)
|
|
526
|
+
return;
|
|
527
|
+
if (!into.require(channel === "voice", "task.lead.channel", path, `a ${channel} task cannot carry a lead request`))
|
|
528
|
+
return;
|
|
529
|
+
if (!isPlainObject(value)) {
|
|
530
|
+
into.add("task.lead.shape", path, "lead must be an object when present");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (into.oneOf(value.status, LEAD_STATUSES, "task.lead.status", `${path}.status`)) {
|
|
534
|
+
if (value.status === "joined") {
|
|
535
|
+
into.require(isUserId(value.leadId), "task.lead.leadId", `${path}.leadId`, "a joined lead is named by their user id");
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
into.require(value.leadId === undefined, "task.lead.leadId.unexpected", `${path}.leadId`, "nobody has joined a requested lead, so there is no lead to name");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (value.note !== undefined)
|
|
542
|
+
into.filled(value.note, "task.lead.note", `${path}.note`, "a note must not be empty when present");
|
|
543
|
+
into.timestamp(value.since, "task.lead.since", `${path}.since`);
|
|
544
|
+
}
|
|
545
|
+
/** The lead's own task for a call they joined. Voice only. */
|
|
546
|
+
function validateAssisting(value, channel, path, into) {
|
|
547
|
+
if (value === undefined)
|
|
548
|
+
return;
|
|
549
|
+
if (!into.require(channel === "voice", "task.assisting.channel", path, `a ${channel} task cannot be a joined call`))
|
|
550
|
+
return;
|
|
551
|
+
if (!isPlainObject(value)) {
|
|
552
|
+
into.add("task.assisting.shape", path, "assisting must be an object when present");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
into.require(isUserId(value.memberId), "task.assisting.memberId", `${path}.memberId`, "a joined call names the member who asked");
|
|
556
|
+
if (value.note !== undefined)
|
|
557
|
+
into.filled(value.note, "task.assisting.note", `${path}.note`, "a note must not be empty when present");
|
|
558
|
+
into.timestamp(value.since, "task.assisting.since", `${path}.since`);
|
|
559
|
+
}
|
|
505
560
|
export function validateTask(task, context, path = "task") {
|
|
506
561
|
const into = new Collector();
|
|
507
562
|
validateTaskInto(task, context, path, into);
|
|
@@ -537,6 +592,9 @@ function validateTaskInto(task, context, path, into) {
|
|
|
537
592
|
validateBrowsers(task.browsers, `${path}.browsers`, into);
|
|
538
593
|
validateTaskAttributes(task.attributes, `${path}.attributes`, into);
|
|
539
594
|
validateHandlingHistory(task.handlingHistory, `${path}.handlingHistory`, into);
|
|
595
|
+
validateConsultation(task.consultation, context.channel, `${path}.consultation`, into);
|
|
596
|
+
validateLead(task.lead, context.channel, `${path}.lead`, into);
|
|
597
|
+
validateAssisting(task.assisting, context.channel, `${path}.assisting`, into);
|
|
540
598
|
const capabilities = task.capabilities;
|
|
541
599
|
if (!isPlainObject(capabilities)) {
|
|
542
600
|
into.add("task.capabilities.shape", `${path}.capabilities`, "a task needs a capabilities object");
|
|
@@ -556,6 +614,7 @@ function validateTaskInto(task, context, path, into) {
|
|
|
556
614
|
validateCustomCapabilities(declared, `${path}.capabilities.custom`, into);
|
|
557
615
|
break;
|
|
558
616
|
case "blindTransfer":
|
|
617
|
+
case "consultTransfer":
|
|
559
618
|
case "conference":
|
|
560
619
|
validateDestinationDirectory(declared, `${path}.capabilities.${name}`, into);
|
|
561
620
|
break;
|
|
@@ -650,6 +709,36 @@ function validateTeamRosterInto(roster, path, into) {
|
|
|
650
709
|
if (roster.breakControl !== undefined) {
|
|
651
710
|
into.require(roster.breakControl === true, "team.breakControl", `${path}.breakControl`, "breakControl is declared by presence: send true or omit it");
|
|
652
711
|
}
|
|
712
|
+
if (roster.consultControl !== undefined) {
|
|
713
|
+
into.require(roster.consultControl === true, "team.consultControl", `${path}.consultControl`, "consultControl is declared by presence: send true or omit it");
|
|
714
|
+
}
|
|
715
|
+
if (roster.requests !== undefined) {
|
|
716
|
+
// Requests are what a lead acts on, so a lead who may not act has no business receiving them.
|
|
717
|
+
into.require(roster.consultControl === true, "team.requests.capability", `${path}.requests`, "requests require consultControl: a lead who may not join has nothing to decide");
|
|
718
|
+
if (!Array.isArray(roster.requests)) {
|
|
719
|
+
into.add("team.requests.shape", `${path}.requests`, "requests must be an array when present");
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
const seenRequests = new Set();
|
|
723
|
+
roster.requests.forEach((request, index) => {
|
|
724
|
+
const at = `${path}.requests[${index}]`;
|
|
725
|
+
if (!isPlainObject(request)) {
|
|
726
|
+
into.add("team.request.shape", at, "each request must be an object");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (into.filled(request.id, "team.request.id", `${at}.id`, "a request needs an id")) {
|
|
730
|
+
if (seenRequests.has(request.id))
|
|
731
|
+
into.add("team.request.unique", `${at}.id`, `duplicate request id: ${request.id}`);
|
|
732
|
+
seenRequests.add(request.id);
|
|
733
|
+
}
|
|
734
|
+
into.require(isUserId(request.memberId), "team.request.memberId", `${at}.memberId`, "a request names the member asking");
|
|
735
|
+
into.require(isTaskId(request.taskId), "team.request.taskId", `${at}.taskId`, "a request names the task the lead would join");
|
|
736
|
+
if (request.note !== undefined)
|
|
737
|
+
into.filled(request.note, "team.request.note", `${at}.note`, "a note must not be empty when present");
|
|
738
|
+
into.timestamp(request.since, "team.request.since", `${at}.since`);
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
653
742
|
if (!Array.isArray(roster.members)) {
|
|
654
743
|
into.add("team.members.shape", `${path}.members`, "a roster must carry a members array");
|
|
655
744
|
return;
|
|
@@ -770,6 +859,8 @@ function validateTaskOutcome(value, path, into) {
|
|
|
770
859
|
// Only the phases in which a task is still waiting on somebody can expire.
|
|
771
860
|
into.oneOf(value.phase, EXPIRABLE_PHASES, "event.taskEnded.outcome.expired", `${path}.phase`);
|
|
772
861
|
break;
|
|
862
|
+
case "left":
|
|
863
|
+
break;
|
|
773
864
|
case "failed":
|
|
774
865
|
if (!isPlainObject(value.failure)) {
|
|
775
866
|
into.add("event.taskEnded.outcome.failed", `${path}.failure`, "a failed outcome must carry a failure");
|
package/guide.md
CHANGED
|
@@ -335,7 +335,10 @@ type TaskCapabilities<C extends Channel = Channel> =
|
|
|
335
335
|
mute?: true;
|
|
336
336
|
hold?: true;
|
|
337
337
|
agentDisconnect?: true;
|
|
338
|
+
callback?: true;
|
|
338
339
|
blindTransfer?: true | DestinationDirectory;
|
|
340
|
+
consultTransfer?: true | DestinationDirectory;
|
|
341
|
+
consultLead?: true;
|
|
339
342
|
conference?: true | DestinationDirectory;
|
|
340
343
|
recording?: true;
|
|
341
344
|
}
|
|
@@ -420,6 +423,25 @@ type TaskCompletion =
|
|
|
420
423
|
| { completionMode: "agent-command"; completionAllowance?: DurationSeconds }
|
|
421
424
|
| { completionMode: "provider-automatic"; completionAllowance: DurationSeconds };
|
|
422
425
|
|
|
426
|
+
type TaskConsultation = {
|
|
427
|
+
destination: string;
|
|
428
|
+
label?: string;
|
|
429
|
+
since?: IsoTimestamp;
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
type TaskLead = {
|
|
433
|
+
status: "requested" | "joined";
|
|
434
|
+
leadId?: UserId;
|
|
435
|
+
note?: string;
|
|
436
|
+
since: IsoTimestamp;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
type TaskAssisting = {
|
|
440
|
+
memberId: UserId;
|
|
441
|
+
note?: string;
|
|
442
|
+
since: IsoTimestamp;
|
|
443
|
+
};
|
|
444
|
+
|
|
423
445
|
type Task<C extends Channel = Channel> = {
|
|
424
446
|
id: TaskId;
|
|
425
447
|
title: string;
|
|
@@ -432,7 +454,11 @@ type Task<C extends Channel = Channel> = {
|
|
|
432
454
|
reference?: string;
|
|
433
455
|
attributes?: TaskAttribute[];
|
|
434
456
|
handlingHistory?: TaskHandlingStep[];
|
|
435
|
-
} & TaskCompletion
|
|
457
|
+
} & TaskCompletion & (
|
|
458
|
+
C extends "voice"
|
|
459
|
+
? { consultation?: TaskConsultation; lead?: TaskLead; assisting?: TaskAssisting }
|
|
460
|
+
: { consultation?: never; lead?: never; assisting?: never }
|
|
461
|
+
);
|
|
436
462
|
|
|
437
463
|
type AcceptanceMode =
|
|
438
464
|
| "no-preference"
|
|
@@ -444,6 +470,7 @@ type TaskOutcome =
|
|
|
444
470
|
| { type: "transferred"; destination?: string }
|
|
445
471
|
| { type: "cancelled"; reason?: string }
|
|
446
472
|
| { type: "expired"; phase: "pending" | "confirmed" | "preparing" }
|
|
473
|
+
| { type: "left" }
|
|
447
474
|
| { type: "failed"; failure: ProtocolFailure };
|
|
448
475
|
```
|
|
449
476
|
|
|
@@ -459,7 +486,9 @@ const TASK_COMMAND_NAMES = {
|
|
|
459
486
|
"hold",
|
|
460
487
|
"resume",
|
|
461
488
|
"disconnect",
|
|
489
|
+
"callback",
|
|
462
490
|
"transfer",
|
|
491
|
+
"lead",
|
|
463
492
|
"conference",
|
|
464
493
|
"recording",
|
|
465
494
|
"complete",
|
|
@@ -481,7 +510,15 @@ type VoiceTaskCommand =
|
|
|
481
510
|
| { type: "hold" }
|
|
482
511
|
| { type: "resume" }
|
|
483
512
|
| { type: "disconnect" }
|
|
484
|
-
| { type: "
|
|
513
|
+
| { type: "callback" }
|
|
514
|
+
| { type: "transfer"; destination: string; action?: never }
|
|
515
|
+
| { type: "transfer"; action: "consult"; destination: string }
|
|
516
|
+
| { type: "transfer"; action: "complete" }
|
|
517
|
+
| { type: "transfer"; action: "cancel" }
|
|
518
|
+
| { type: "lead"; action: "request"; note?: string }
|
|
519
|
+
| { type: "lead"; action: "cancel" }
|
|
520
|
+
| { type: "lead"; action: "take-over" }
|
|
521
|
+
| { type: "lead"; action: "leave" }
|
|
485
522
|
| { type: "conference"; participant: string; action: "add" | "remove" }
|
|
486
523
|
| { type: "recording"; action: "start" | "pause" | "resume" | "stop" }
|
|
487
524
|
| ({ type: "complete" } & DispositionPayload);
|
|
@@ -564,11 +601,25 @@ type TeamMember = {
|
|
|
564
601
|
break?: BreakApproval;
|
|
565
602
|
};
|
|
566
603
|
|
|
604
|
+
type LeadRequest = {
|
|
605
|
+
id: string;
|
|
606
|
+
memberId: UserId;
|
|
607
|
+
taskId: TaskId;
|
|
608
|
+
note?: string;
|
|
609
|
+
since: IsoTimestamp;
|
|
610
|
+
};
|
|
611
|
+
|
|
567
612
|
type TeamRoster = {
|
|
568
613
|
members: TeamMember[];
|
|
569
614
|
breakControl?: true;
|
|
615
|
+
consultControl?: true;
|
|
616
|
+
requests?: LeadRequest[];
|
|
570
617
|
};
|
|
571
618
|
|
|
619
|
+
type TeamConsultCommand =
|
|
620
|
+
| { type: "join"; requestId: string }
|
|
621
|
+
| { type: "decline"; requestId: string; reason?: string };
|
|
622
|
+
|
|
572
623
|
type TeamBreakCommand =
|
|
573
624
|
| { type: "decide"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
|
|
574
625
|
| { type: "policy"; policy: "ask" | "auto-approve" | "suspended" }
|
|
@@ -1424,6 +1475,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
1424
1475
|
| `cancelBreak(requestId)` | `sessionCapabilities.breaks` is declared. |
|
|
1425
1476
|
| `endBreak()` | `sessionCapabilities.breaks` is declared. |
|
|
1426
1477
|
| `executeTeamBreak(command)` | The adapter publishes a `TeamRoster` carrying `breakControl`. |
|
|
1478
|
+
| `executeTeamConsult(command)` | The adapter publishes a `TeamRoster` carrying `consultControl`. |
|
|
1427
1479
|
| `openMedia(request)` | The manifest channel is `voice`. Every voice task's audio lands in Omni, so there is no voice adapter that does not implement it. |
|
|
1428
1480
|
|
|
1429
1481
|
**The four break methods stand or fall together.** Declaring `sessionCapabilities.breaks` and then
|
|
@@ -1621,6 +1673,9 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
1621
1673
|
| `completionAllowance` | Fixed time allowed to complete the task after primary handling ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
|
|
1622
1674
|
| `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
|
|
1623
1675
|
| `handlingHistory` | Optional ordered handling history for this currently open task. It is live task data, not a permanent archive. |
|
|
1676
|
+
| `consultation` | Voice only. Present while the agent is consulting a transfer destination: who is being consulted, and since when where the provider records it. Its presence is what makes `transfer` `complete` and `cancel` issuable. See **Consult transfer**. |
|
|
1677
|
+
| `lead` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Consulting a lead**. |
|
|
1678
|
+
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead` `take-over` and `leave` issuable. See **Consulting a lead**. |
|
|
1624
1679
|
|
|
1625
1680
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
1626
1681
|
|
|
@@ -1656,8 +1711,11 @@ The canonical task transitions are:
|
|
|
1656
1711
|
| `pending` | Provider withdraws the allocation | Removed by `task-ended` with `cancelled` outcome |
|
|
1657
1712
|
| No task | Snapshot reports work already underway | `in-progress` |
|
|
1658
1713
|
| `in-progress` | Provider or agent pauses the task | `paused` |
|
|
1714
|
+
| `in-progress` | Agent consults a transfer destination (`transfer` `consult`); the customer is parked | `paused` |
|
|
1659
1715
|
| `paused` | Provider or agent resumes the task | `in-progress` |
|
|
1716
|
+
| `paused` | Agent cancels a consultation (`transfer` `cancel`) | `in-progress` |
|
|
1660
1717
|
| `in-progress` or `paused` | Contact handling ends and follow-up work remains | `completing` |
|
|
1718
|
+
| `completing` | Agent calls the party back (`callback`) | `in-progress` |
|
|
1661
1719
|
| Any phase | Provider emits `task-ended` | Removed |
|
|
1662
1720
|
|
|
1663
1721
|
Allocation, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
@@ -1712,6 +1770,42 @@ and the agent takes as long as the work needs. Moving the task to `completing` l
|
|
|
1712
1770
|
deadline is not an alternative on a media channel: the clock starts at a real event, and delaying
|
|
1713
1771
|
that event would falsify the phase and everything timed from it.
|
|
1714
1772
|
|
|
1773
|
+
#### Calling back during completion
|
|
1774
|
+
|
|
1775
|
+
A task that declares `callback` lets the agent reach the party again while the task is
|
|
1776
|
+
`completing` -- to finish what the call left unfinished, on the same task rather than a new one.
|
|
1777
|
+
Omni issues `{ type: "callback" }`; it is issuable only in `completing`, and only where the
|
|
1778
|
+
capability is declared. The provider knows who the party is; the command carries no destination.
|
|
1779
|
+
|
|
1780
|
+
On `applied` the provider is placing the call and the task returns to `in-progress`: the agent is
|
|
1781
|
+
working again, and the completion allowance is **discarded, not paused**. From there the call is
|
|
1782
|
+
reported as any call is -- `paused`, `in-progress`, and when its media ends, `task-media-ended`
|
|
1783
|
+
again, which starts a fresh allowance from that instant. A party who does not answer is a call
|
|
1784
|
+
whose media ended: the task returns to `completing` through the same event and the clock starts
|
|
1785
|
+
again from there. At no point is an agent dialling against a deadline.
|
|
1786
|
+
|
|
1787
|
+
**The control exists only while there is a window to use it in.** Under `agent-command` the task
|
|
1788
|
+
stays `completing` until the agent completes it, so the window is open for as long as they need.
|
|
1789
|
+
Under `provider-automatic` the window is the allowance -- and with `completionAllowance: 0` there
|
|
1790
|
+
is none: the provider disposes the task at provider end, and Omni does not offer Call back, whatever
|
|
1791
|
+
the task declares. A capability names a control that can be used; on a task with no `completing`
|
|
1792
|
+
window it cannot, and declaring it there changes nothing.
|
|
1793
|
+
|
|
1794
|
+
```ts
|
|
1795
|
+
const callbackCapable = {
|
|
1796
|
+
channel: "voice",
|
|
1797
|
+
capabilities: { hold: true, callback: true, dispositions: true },
|
|
1798
|
+
phase: "completing",
|
|
1799
|
+
completionMode: "provider-automatic",
|
|
1800
|
+
completionAllowance: 30,
|
|
1801
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "completionAllowance">;
|
|
1802
|
+
```
|
|
1803
|
+
|
|
1804
|
+
With ten seconds of the thirty left, the agent presses Call back: `execute({ command: { type:
|
|
1805
|
+
"callback" } })` returns `applied`, the task is `in-progress`, and the thirty seconds are gone.
|
|
1806
|
+
The second call ends: `task-media-ended`, the task is `completing`, and a new thirty seconds runs
|
|
1807
|
+
from that instant.
|
|
1808
|
+
|
|
1715
1809
|
```ts
|
|
1716
1810
|
const immediateProviderCompletion = {
|
|
1717
1811
|
completionMode: "provider-automatic",
|
|
@@ -1905,14 +1999,17 @@ const taskCapabilities = {
|
|
|
1905
1999
|
| `mute` | Primary toggle: Mute | Omni may mute and unmute the agent's outbound audio. |
|
|
1906
2000
|
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
1907
2001
|
| `agentDisconnect` | Primary button: Disconnect | Omni may disconnect real-time media without disposing the task. |
|
|
2002
|
+
| `callback` | Completing-task button: Call back | Omni may have the provider call the task's party back while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance disposes at provider end. See **Calling back during completion**. |
|
|
1908
2003
|
| `blindTransfer` | Secondary menu item: Blind transfer | Omni may transfer the caller directly to a destination. |
|
|
2004
|
+
| `consultTransfer` | Secondary menu item: Consult transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Consult transfer**. |
|
|
2005
|
+
| `consultLead` | Secondary menu item: Consult lead | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.lead`. See **Consulting a lead**. |
|
|
1909
2006
|
| `conference` | Secondary button: Conference | Omni may add or remove participants from the active call. |
|
|
1910
2007
|
| `recording` | Overflow menu item: Recording | Omni may expose start, pause, resume, and stop recording controls. |
|
|
1911
2008
|
| `dispositions` | Primary button: Complete | Omni may request task disposal with a provider disposition and notes. |
|
|
1912
2009
|
|
|
1913
2010
|
### Publishing codes and destinations
|
|
1914
2011
|
|
|
1915
|
-
|
|
2012
|
+
Four capabilities accept an object instead of `true` when the provider wants Omni to render real
|
|
1916
2013
|
choices. `true` remains valid and means "offer the control with nothing published".
|
|
1917
2014
|
|
|
1918
2015
|
#### `dispositions`
|
|
@@ -1939,7 +2036,7 @@ capabilities: {
|
|
|
1939
2036
|
With `dispositions: true` Omni shows a Complete control and sends `complete` with no code, because
|
|
1940
2037
|
the provider published none.
|
|
1941
2038
|
|
|
1942
|
-
#### `blindTransfer` and `conference`
|
|
2039
|
+
#### `blindTransfer`, `consultTransfer` and `conference`
|
|
1943
2040
|
|
|
1944
2041
|
```ts
|
|
1945
2042
|
capabilities: {
|
|
@@ -1971,6 +2068,42 @@ A destination the agent types is not in the directory and has no `kind`. Omni tr
|
|
|
1971
2068
|
`external` unless the provider says otherwise in its response, because that is the assumption that
|
|
1972
2069
|
does not overstate what the provider can still see.
|
|
1973
2070
|
|
|
2071
|
+
#### Consult transfer
|
|
2072
|
+
|
|
2073
|
+
A consult transfer parks the customer, calls the destination so the agent can speak to it first,
|
|
2074
|
+
and then either hands the customer over or returns to them. It is its own capability, distinct
|
|
2075
|
+
from `blindTransfer` (a hand-over with nobody consulted) and from `conference` (everybody on one
|
|
2076
|
+
call): a queue may offer any of the three without the others, and each is declared on its own.
|
|
2077
|
+
|
|
2078
|
+
```ts
|
|
2079
|
+
// 1. Consult. The provider parks the customer and calls the destination; the task reports
|
|
2080
|
+
// `paused` and carries `consultation` while the call to the destination stands.
|
|
2081
|
+
{ type: "transfer", action: "consult", destination: "+14155550111" }
|
|
2082
|
+
|
|
2083
|
+
// 2a. Hand the customer to the consulted destination and leave.
|
|
2084
|
+
{ type: "transfer", action: "complete" }
|
|
2085
|
+
|
|
2086
|
+
// 2b. Or drop the destination and return to the customer.
|
|
2087
|
+
{ type: "transfer", action: "cancel" }
|
|
2088
|
+
```
|
|
2089
|
+
|
|
2090
|
+
`consult` is gated by the `consultTransfer` capability and takes a destination exactly as a blind
|
|
2091
|
+
transfer does, from the same kind of directory. While the consultation stands the task carries
|
|
2092
|
+
`consultation`, and that presence is what makes `complete` and `cancel` issuable -- they name no
|
|
2093
|
+
destination because there is exactly one they could mean. A consultation that could be started
|
|
2094
|
+
but not finished would strand the customer and the destination both, which is why all three are
|
|
2095
|
+
commands and a provider that offers `consultTransfer` implements all three.
|
|
2096
|
+
|
|
2097
|
+
`applied` on `complete` says the provider is bridging the customer to the destination and
|
|
2098
|
+
dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
|
|
2099
|
+
the provider reports `task-media-ended`, any completion allowance runs, and the task ends with a
|
|
2100
|
+
`transferred` outcome naming the destination. `applied` on `cancel` says the destination is
|
|
2101
|
+
dropped; the task returns to `in-progress` with `consultation` gone. Omni waits for the
|
|
2102
|
+
provider's report of both, as it does for every command.
|
|
2103
|
+
|
|
2104
|
+
A destination that does not answer is a consultation that ended: the provider clears
|
|
2105
|
+
`consultation`, returns the task to `in-progress`, and the agent is back with the customer.
|
|
2106
|
+
|
|
1974
2107
|
### Chat capabilities
|
|
1975
2108
|
|
|
1976
2109
|
| Capability | Omni UI | Contract |
|
|
@@ -2403,6 +2536,8 @@ A lead who also takes calls sees their team on the idle dashboard. `Snapshot.tea
|
|
|
2403
2536
|
| --- | --- |
|
|
2404
2537
|
| `members` | Every member of this lead's team, whatever their state. `[]` says the lead has a team with nobody in it; omitting the roster says something else entirely — see **Its presence is the permission** below. |
|
|
2405
2538
|
| `breakControl` | Present when this lead decides their team's breaks, absent when they do not. |
|
|
2539
|
+
| `consultControl` | Present when this lead may join a member's call on request, absent when they may not. |
|
|
2540
|
+
| `requests` | The members currently asking this lead to join a call, each with the task and the note. Omitted when the lead may not be asked; `[]` when nobody is asking. See **Consulting a lead**. |
|
|
2406
2541
|
|
|
2407
2542
|
| `TeamMember` field | Contract |
|
|
2408
2543
|
| --- | --- |
|
|
@@ -2460,6 +2595,70 @@ on their own `BreakState`, or they are stopped from working with no way to see w
|
|
|
2460
2595
|
What happens when no lead is online — auto-approving, for instance — is the provider's decision and is
|
|
2461
2596
|
never expressed here.
|
|
2462
2597
|
|
|
2598
|
+
### Consulting a lead
|
|
2599
|
+
|
|
2600
|
+
An agent on a call may ask a lead to join it -- a dispute that needs approval, a customer who
|
|
2601
|
+
asks for a manager, a moment the agent wants a second pair of ears. The capability is
|
|
2602
|
+
`consultLead` on the task; the lead's side is the roster, which is already the lead's view of the
|
|
2603
|
+
team, and a second lead method beside `executeTeamBreak`:
|
|
2604
|
+
|
|
2605
|
+
```ts
|
|
2606
|
+
executeTeamConsult({ commandId, command: TeamConsultCommand }): Promise<TeamCommandResult>
|
|
2607
|
+
```
|
|
2608
|
+
|
|
2609
|
+
Required when the roster carries `consultControl`, and gated by it exactly as `executeTeamBreak`
|
|
2610
|
+
is by `breakControl`. The flow, in order:
|
|
2611
|
+
|
|
2612
|
+
```ts
|
|
2613
|
+
// 1. The agent asks, with a small note. Their task carries `lead` from here on.
|
|
2614
|
+
execute({ commandId, taskId: "call-42", command: { type: "lead", action: "request", note: "Refund dispute, needs approval" } })
|
|
2615
|
+
// task.lead = { status: "requested", note: "Refund dispute, needs approval", since }
|
|
2616
|
+
|
|
2617
|
+
// 2. Every lead entitled to it sees the request on their roster.
|
|
2618
|
+
// team-updated: requests: [{ id: "req-7", memberId: "A-1", taskId: "call-42", note, since }]
|
|
2619
|
+
|
|
2620
|
+
// 3. A lead joins, or declines.
|
|
2621
|
+
executeTeamConsult({ commandId, command: { type: "join", requestId: "req-7" } })
|
|
2622
|
+
executeTeamConsult({ commandId, command: { type: "decline", requestId: "req-7", reason: "In a call" } })
|
|
2623
|
+
```
|
|
2624
|
+
|
|
2625
|
+
**On `join` the provider bridges three parties and the lead is on a task of their own**, on the
|
|
2626
|
+
same task id, arriving on the lead's connection as `task-offered` with `require-automatic-acceptance`
|
|
2627
|
+
-- the way a call an agent placed themselves arrives -- and carrying `assisting`. The agent's task
|
|
2628
|
+
moves to `lead: { status: "joined", leadId }`. A join is the lead's own act, so capacity does not
|
|
2629
|
+
trigger it; but from then on it is an outstanding task the provider counts against the lead's
|
|
2630
|
+
stated ceiling like any other, nothing more is allocated to the lead while it stands, and a
|
|
2631
|
+
provider whose lead is already at the ceiling answers the join `failed`.
|
|
2632
|
+
|
|
2633
|
+
**On `decline`, or a request the agent withdraws with `{ type: "lead", action: "cancel" }`, the
|
|
2634
|
+
provider clears `lead` from the agent's task** and drops the request from every roster. Nothing
|
|
2635
|
+
else changes; the agent is still on the call.
|
|
2636
|
+
|
|
2637
|
+
The lead then has two commands on their copy, gated by `assisting` being present, and a third
|
|
2638
|
+
choice that is no command at all:
|
|
2639
|
+
|
|
2640
|
+
| The lead | The agent's task | The lead's task |
|
|
2641
|
+
| --- | --- | --- |
|
|
2642
|
+
| `{ type: "lead", action: "take-over" }` | `task-ended` with `{ type: "transferred", destination: leadId }`, straight from `in-progress`: **no `completing` window**, the agent is idle at once | Continues alone, and ends as any call does |
|
|
2643
|
+
| `{ type: "lead", action: "leave" }` | Continues; `lead` is cleared | `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
2644
|
+
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own disposition | The same, independently: **both have the disposal window** |
|
|
2645
|
+
|
|
2646
|
+
`left` is the one outcome that ends a task without ending the call: this agent left a call that
|
|
2647
|
+
continues without them. It reads as neither a completion nor a cancellation, because it is
|
|
2648
|
+
neither.
|
|
2649
|
+
|
|
2650
|
+
```ts
|
|
2651
|
+
const consultLeadCapable = {
|
|
2652
|
+
channel: "voice",
|
|
2653
|
+
capabilities: { hold: true, consultLead: true, dispositions: true },
|
|
2654
|
+
phase: "in-progress",
|
|
2655
|
+
lead: { status: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
|
|
2656
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "lead">;
|
|
2657
|
+
```
|
|
2658
|
+
|
|
2659
|
+
Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
|
|
2660
|
+
`describeUsers()`; names never travel on a task or a roster.
|
|
2661
|
+
|
|
2463
2662
|
### A member waiting for a break
|
|
2464
2663
|
|
|
2465
2664
|
A member who has asked for a break **keeps working** until Omni commits it, so asking is not an
|
|
@@ -2619,6 +2818,11 @@ declared:
|
|
|
2619
2818
|
| `decline`, `reject` | The channel's decline or reject capability, **and** Omni provisioning permitting rejection. |
|
|
2620
2819
|
| `start-call` | The `preparing` phase. It starts the contact a preview gave the agent time to read, so the phase is the gate and there is no capability. |
|
|
2621
2820
|
| `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. |
|
|
2821
|
+
| `callback` | The `callback` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
|
|
2822
|
+
| `transfer` with `action: "consult"` | The `consultTransfer` capability. Blind `transfer` is gated by `blindTransfer`; the two are declared and offered separately. |
|
|
2823
|
+
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- `Task.consultation` present. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
|
|
2824
|
+
| `lead` with `action: "request"` or `"cancel"` | The `consultLead` capability. `cancel` needs a request standing -- `Task.lead` with status `requested`. |
|
|
2825
|
+
| `lead` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
2622
2826
|
| Everything else | Its own named capability. |
|
|
2623
2827
|
|
|
2624
2828
|
Declining or rejecting a pending offer ends it without accepting or completing it. The provider
|
|
@@ -2799,6 +3003,9 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
|
|
|
2799
3003
|
- releases task-scoped resources; and
|
|
2800
3004
|
- selects another task or returns to the idle workspace.
|
|
2801
3005
|
|
|
3006
|
+
A `left` outcome ends the task for this agent alone: the call continues without them, as it does
|
|
3007
|
+
when a lead who joined it leaves -- see **Consulting a lead**.
|
|
3008
|
+
|
|
2802
3009
|
A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`.
|
|
2803
3010
|
The `task-media-ended` event and the `completing` phase are likewise non-terminal. A replacement
|
|
2804
3011
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|