@calloop/agent-sdk 0.2.0 → 0.2.1
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/CHANGELOG.md +8 -0
- package/LICENSE +21 -21
- package/README.md +231 -231
- package/package.json +46 -45
- package/src/Call.js +296 -296
- package/src/CalloopAgent.js +483 -483
- package/src/Monitor.js +173 -173
- package/src/emitter.js +25 -25
- package/src/index.js +5 -5
package/CHANGELOG.md
ADDED
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 PCGLOBALCO
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PCGLOBALCO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,231 +1,231 @@
|
|
|
1
|
-
# @calloop/agent-sdk
|
|
2
|
-
|
|
3
|
-
The agent side of Calloop's [agent protocol](https://docs.calloop.net/agents/protocol) for
|
|
4
|
-
browsers and Node: connect, go available, ring, answer, hang up, transfer,
|
|
5
|
-
and keep calls alive across network drops, page reloads and device switches.
|
|
6
|
-
The app keeps its own UI, ringtones and routing; the SDK gives it events,
|
|
7
|
-
state and the remote audio stream.
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
npm install @calloop/agent-sdk
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
```js
|
|
14
|
-
import { connect } from '@calloop/agent-sdk';
|
|
15
|
-
|
|
16
|
-
const agent = await connect({
|
|
17
|
-
url: 'https://api.calloop.net',
|
|
18
|
-
getToken: () => fetch('/api/calloop-token').then((r) => r.text()), // your backend signs it
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
agent.setAvailability('AVAILABLE');
|
|
22
|
-
|
|
23
|
-
agent.on('incoming', (call) => {
|
|
24
|
-
ring(call.customer); // your UI
|
|
25
|
-
call.on('remoteStream', (stream) => { audioEl.srcObject = stream; });
|
|
26
|
-
call.on('ended', ({ reason }) => stopRinging());
|
|
27
|
-
acceptButton.onclick = () => call.accept(); // asks for the microphone
|
|
28
|
-
declineButton.onclick = () => call.decline();
|
|
29
|
-
});
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Connecting
|
|
33
|
-
|
|
34
|
-
`connect(options)` resolves once Calloop has identified the agent (`session:ready`).
|
|
35
|
-
|
|
36
|
-
| Option | |
|
|
37
|
-
|---|---|
|
|
38
|
-
| `url` | Calloop's base URL |
|
|
39
|
-
| `getToken` | `async () => jwt`, called on every (re)connect — agent tokens are short-lived. Or `token` for a fixed one. |
|
|
40
|
-
| `deviceId` | Stable id for this device (default: one kept in `localStorage`). Calloop uses it to tell "this device" from "my other device". |
|
|
41
|
-
| `webrtc` | `{ RTCPeerConnection, MediaStream }` outside a browser (e.g. `@roamhq/wrtc` in Node) |
|
|
42
|
-
| `getUserMedia` | `async () => MediaStream` for the microphone (default: `navigator.mediaDevices.getUserMedia`) |
|
|
43
|
-
| `io` | socket.io-client's `io` (default: the bundled one) — to use the app's own socket.io-client |
|
|
44
|
-
| `purpose` | `'session'` (default): the agent's main connection. Anything else (e.g. `'decline'`) is an auxiliary connection: no presence, no redelivered ringing calls, no resync on connect |
|
|
45
|
-
|
|
46
|
-
ICE/TURN servers come from Calloop; the app never configures them.
|
|
47
|
-
|
|
48
|
-
The SDK reconnects by itself after a network drop (`disconnected`, then
|
|
49
|
-
`ready`). When Calloop itself ends the connection — the token's signing key was
|
|
50
|
-
revoked — socket.io doesn't retry: the agent emits `closed` ({ reason }) and
|
|
51
|
-
sets `agent.closedReason`. Call `agent.close()` and `connect()` again with a
|
|
52
|
-
token signed by a current key. The same happens when the agent's role changes,
|
|
53
|
-
the agent is deleted, or its tenant or consumer is suspended.
|
|
54
|
-
|
|
55
|
-
## Security
|
|
56
|
-
|
|
57
|
-
- **Only agent tokens in the client.** The browser or app gets a short-lived
|
|
58
|
-
agent token from your backend (5–15 minutes; Calloop refuses one valid for
|
|
59
|
-
more than 24 hours). Never put a Calloop API key or a signing key in client
|
|
60
|
-
code: they're your backend's alone.
|
|
61
|
-
- **A fresh token per connect.** Give `getToken`, not a fixed `token`: it runs
|
|
62
|
-
on every (re)connect. Keep the token in memory, not `localStorage`.
|
|
63
|
-
- **`role` on every token.** A token without `role` connects as `AGENT`; send
|
|
64
|
-
`role: 'SUPERVISOR'` on a supervisor's tokens every time.
|
|
65
|
-
- **https only.** `url` is `https://…` outside local development (the SDK warns
|
|
66
|
-
in the console otherwise): the token and the call's signalling travel over it.
|
|
67
|
-
- **What ends a session.** Calloop disconnects an agent's sockets when its
|
|
68
|
-
signing key is revoked, its role changes, it's deleted, or its tenant or
|
|
69
|
-
consumer is suspended (`closed`, below). Reconnect with a fresh token, or not
|
|
70
|
-
at all.
|
|
71
|
-
|
|
72
|
-
## The agent
|
|
73
|
-
|
|
74
|
-
| | |
|
|
75
|
-
|---|---|
|
|
76
|
-
| `agent.setAvailability('AVAILABLE' \| 'OFFLINE', { agentId? })` | `ON_CALL` is set by Calloop; `agentId`: a supervisor setting someone else |
|
|
77
|
-
| `agent.startOutbound(callId, { stream? })` | start an outbound call the consumer created (`POST /v1/tenants/{t}/calls`) |
|
|
78
|
-
| `agent.calls`, `agent.call(id)` | this agent's calls |
|
|
79
|
-
| `agent.availability` | this agent's availability (`AVAILABLE` · `ON_CALL` · `OFFLINE`), as Calloop reports it |
|
|
80
|
-
| `agent.queues` | queue snapshots by queue id (see below) |
|
|
81
|
-
| `agent.team` | the tenant's agents' availability, as it changes (see below) |
|
|
82
|
-
| `agent.sync()` | re-read the calls and queue snapshots now (done on every connect) |
|
|
83
|
-
| `agent.refreshSession()` | new ICE/TURN credentials (done automatically before they expire) |
|
|
84
|
-
| `agent.ready` | a promise that resolves with the agent once it is first identified |
|
|
85
|
-
| `agent.agent`, `agent.session` | identity from `session:ready` |
|
|
86
|
-
| `agent.iceServers`, `agent.deviceId` | the ICE servers Calloop gave this session; this device's id |
|
|
87
|
-
| `agent.close()` | |
|
|
88
|
-
|
|
89
|
-
Queue snapshots and the team's availability come only to supervisors and to
|
|
90
|
-
agents whose tenant allows the team view (the tenant's `team_view` setting).
|
|
91
|
-
Without it, a plain agent gets no queues, and `agent.team` holds only itself.
|
|
92
|
-
|
|
93
|
-
`on(event, fn)` returns a function that removes the listener; `once()` and
|
|
94
|
-
`off()` work as usual, on the agent, a call and a monitor.
|
|
95
|
-
|
|
96
|
-
Events: `incoming` (call), `elsewhere` (call), `callState` (call, state, previous),
|
|
97
|
-
`callEnded` (call, info), `availability`, `team`, `queue`, `ready` (after each reconnect),
|
|
98
|
-
`sessionRefreshed`, `disconnected`, `closed` ({ reason }: Calloop ended the
|
|
99
|
-
connection, see above), `error`.
|
|
100
|
-
|
|
101
|
-
## A call
|
|
102
|
-
|
|
103
|
-
`call.state`: `ringing` → `connecting` → `active` → `ended`; `dialing` for an
|
|
104
|
-
outbound call before the customer answers; `elsewhere` when its media is on
|
|
105
|
-
another of the agent's devices.
|
|
106
|
-
|
|
107
|
-
| | |
|
|
108
|
-
|---|---|
|
|
109
|
-
| `call.accept({ stream? })` | answer (default microphone) |
|
|
110
|
-
| `call.decline()` | in a queue, it passes to the next agent |
|
|
111
|
-
| `call.hangup()` | |
|
|
112
|
-
| `call.mute(bool)` | |
|
|
113
|
-
| `call.transfer({ agentId } \| { queueId })` | |
|
|
114
|
-
| `call.switchHere({ stream? })` | take a call that's active on another device |
|
|
115
|
-
| `call.setPrivate(bool)` | talk privately to the monitoring supervisor |
|
|
116
|
-
| `call.customer`, `call.direction`, `call.channel`, `call.callUuid`, `call.queueId`, `call.data` | |
|
|
117
|
-
| `call.isRingAll` | offered to every member of a `RING_ALL` queue at once (first accept wins) |
|
|
118
|
-
| `call.muted`, `call.endReason` | as set by `mute()`; why the call ended (`null` until then) |
|
|
119
|
-
| `call.remoteStream`, `call.localStream`, `call.pc` | |
|
|
120
|
-
|
|
121
|
-
Events: `state` (state, previous), `remoteStream` (stream), `ended` ({ reason }),
|
|
122
|
-
`updated`, `dtmf`, `customerMedia`, `networkTerminating`, `supervisorMode`,
|
|
123
|
-
`privateChanged`.
|
|
124
|
-
|
|
125
|
-
`ended` reasons: `hangup`, `declined`, `terminated` (see
|
|
126
|
-
`terminationReason`: the call ended — or, with `transferred`, this agent's part
|
|
127
|
-
of it did: the previous agent of a transfer gets it while the call goes on with
|
|
128
|
-
the next one), `withdrawn` (the offer went to someone else),
|
|
129
|
-
`answered_elsewhere`, `media_failed`, `accept_failed`, `error`, `gone`.
|
|
130
|
-
A failed call's `terminated` also carries `failure`: `{ code, title, details? }`,
|
|
131
|
-
what the provider said (a SIP trunk: `404`, `Not Found`, its `Reason` header).
|
|
132
|
-
Other details by reason:
|
|
133
|
-
|
|
134
|
-
| `reason` | Also in the `ended` info |
|
|
135
|
-
|---|---|
|
|
136
|
-
| `terminated` | `terminationReason`, `terminatedBy`, `failure` |
|
|
137
|
-
| `withdrawn` | `withdrawnReason`: `declined` · `taken` · `timeout` · `overflow` |
|
|
138
|
-
| `answered_elsewhere` | `by` (the agent id), `agentName` |
|
|
139
|
-
| `error` | `code`, `message` |
|
|
140
|
-
| `media_failed`, `accept_failed` | `error` (the message), when there is one |
|
|
141
|
-
|
|
142
|
-
## Supervisors
|
|
143
|
-
|
|
144
|
-
An agent whose token has `role: 'SUPERVISOR'` (`agent.isSupervisor`) also gets:
|
|
145
|
-
|
|
146
|
-
| | |
|
|
147
|
-
|---|---|
|
|
148
|
-
| `agent.board` | the tenant's live calls (`Map` callId → call view: status, direction, customer, agent, queue, `ivr` position), rebuilt on every sync. `ivr` is filled by live IVR events only: after a sync it is missing until the call's next IVR step |
|
|
149
|
-
| `agent.monitor(callId, { stream? })` | listen to a call → a `Monitor` |
|
|
150
|
-
| `agent.monitors` | the calls being monitored (`Map` callId → `Monitor`) |
|
|
151
|
-
| `agent.transferCall(callId, { agentId } \| { queueId })` | move any call |
|
|
152
|
-
| `agent.setAvailability(value, { agentId })` | set an agent's availability |
|
|
153
|
-
|
|
154
|
-
Board events: `board` (the whole board, after each sync), `boardCall` (a call
|
|
155
|
-
appeared or changed), `boardCallEnded` (view, { terminationReason, terminatedBy }),
|
|
156
|
-
`counters` (the tenant's live counts, pushed at most every 2 s).
|
|
157
|
-
|
|
158
|
-
`agent.board` and the `board` / `boardCall` / `boardCallEnded` events are
|
|
159
|
-
supervisors' only. An agent whose tenant has the team view can page the board
|
|
160
|
-
with `boardCalls()`, but the SDK gives it no live board events.
|
|
161
|
-
|
|
162
|
-
A dashboard on a large tenant narrows and pages the board instead of holding
|
|
163
|
-
all of it:
|
|
164
|
-
|
|
165
|
-
| | |
|
|
166
|
-
|---|---|
|
|
167
|
-
| `agent.subscribeBoard({ channelIds?, queueIds?, agentIds? })` | only these lines / queues / agents from now on; `{}` = the whole tenant |
|
|
168
|
-
| `agent.unsubscribeBoard()` | no board on this connection |
|
|
169
|
-
| `agent.boardCalls({ …filter, cursor?, limit? })` | `{ calls, nextCursor }`, newest first |
|
|
170
|
-
| `agent.boardCounters()` | supervisors: the counters now |
|
|
171
|
-
|
|
172
|
-
`agent.board` is still rebuilt from `sync()` (the whole tenant); a narrowed
|
|
173
|
-
dashboard keeps its own list from `boardCalls()` and the board events.
|
|
174
|
-
|
|
175
|
-
A filter lasts for one connection: after a reconnect Calloop subscribes the new
|
|
176
|
-
connection to the whole tenant again. Re-apply it on `ready`:
|
|
177
|
-
|
|
178
|
-
```js
|
|
179
|
-
agent.on('ready', () => agent.subscribeBoard({ queueIds: [3] }));
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
```js
|
|
183
|
-
const monitor = await agent.monitor(callId); // asks for the microphone
|
|
184
|
-
monitor.on('stream', (s) => { callAudio.srcObject = s; }); // the customer and the agent, mixed
|
|
185
|
-
monitor.setMode('whisper'); // 'listen' | 'whisper' | 'barge'
|
|
186
|
-
monitor.mute(true); // your microphone
|
|
187
|
-
monitor.stop();
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
A monitor has `callId`, `state` (`connecting` · `active` · `ended`), `mode`,
|
|
191
|
-
`muted` and `stream`.
|
|
192
|
-
|
|
193
|
-
`listen`: the supervisor hears the call, nobody hears the supervisor ·
|
|
194
|
-
`whisper`: the agent hears the supervisor · `barge`: both do. Calloop mixes, so
|
|
195
|
-
switching modes is instant. Monitor events: `state`, `stream` (the call as you
|
|
196
|
-
hear it), `mode`, `modeRefused` ({ mode, code, message }: Calloop refused a
|
|
197
|
-
`setMode()` — e.g. whisper on a direct call, or a mode the tenant doesn't allow
|
|
198
|
-
— and monitoring goes on in the current mode), `agentPrivate` (the agent
|
|
199
|
-
talking privately to you), `agentReconnected`, `ended` ({ reason, code?,
|
|
200
|
-
message? }). Reasons: `stopped`, `call_ended`, `ended` (Calloop ended it),
|
|
201
|
-
`disconnected`, `failed` (Calloop couldn't start it; `code`/`message` say why),
|
|
202
|
-
`not_allowed` (the tenant doesn't allow monitoring), `closed` (`agent.close()`).
|
|
203
|
-
A reconnect of the supervisor's socket ends monitoring — start it again.
|
|
204
|
-
|
|
205
|
-
## TypeScript
|
|
206
|
-
|
|
207
|
-
Types ship with the package: `connect`, `CalloopAgent`,
|
|
208
|
-
`Call`, `Monitor` and the payload shapes.
|
|
209
|
-
|
|
210
|
-
## What it handles
|
|
211
|
-
|
|
212
|
-
- Resync on every connect (`calls:sync`): ringing calls, calls on this device
|
|
213
|
-
(reconnected automatically after a reload), calls on another device.
|
|
214
|
-
- ICE candidates buffered both ways until each side is ready.
|
|
215
|
-
- Media recovery: no media within 15 s or a failed connection → one reconnect.
|
|
216
|
-
If that fails too, the call ends locally with `media_failed` and the SDK asks
|
|
217
|
-
Calloop to end it (`system_failed`), so Calloop records it `FAILED` /
|
|
218
|
-
`NETWORK_ERROR`.
|
|
219
|
-
- Direct calls (`call.isDirect`: a personal line's 1:1 call that Calloop relays
|
|
220
|
-
without a room) reconnect like any other call (the SDK sends its own offer); `agent.monitor()` listens without
|
|
221
|
-
the microphone — the stream carries the customer and the agent as two tracks
|
|
222
|
-
(listen-only).
|
|
223
|
-
- Offers withdrawn (taken, timed out, declined elsewhere, an unaccepted
|
|
224
|
-
transfer returned to its queue) and redelivered offers.
|
|
225
|
-
- TURN credentials refreshed on the live connection before they expire, for
|
|
226
|
-
long shifts.
|
|
227
|
-
|
|
228
|
-
## A first test call
|
|
229
|
-
|
|
230
|
-
Sign a test token with your signing key ([the agent token](https://docs.calloop.net/agents/protocol#the-agent-token)),
|
|
231
|
-
connect as in [Connecting](#connecting), go available, and call your line.
|
|
1
|
+
# @calloop/agent-sdk
|
|
2
|
+
|
|
3
|
+
The agent side of Calloop's [agent protocol](https://docs.calloop.net/agents/protocol) for
|
|
4
|
+
browsers and Node: connect, go available, ring, answer, hang up, transfer,
|
|
5
|
+
and keep calls alive across network drops, page reloads and device switches.
|
|
6
|
+
The app keeps its own UI, ringtones and routing; the SDK gives it events,
|
|
7
|
+
state and the remote audio stream.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @calloop/agent-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { connect } from '@calloop/agent-sdk';
|
|
15
|
+
|
|
16
|
+
const agent = await connect({
|
|
17
|
+
url: 'https://api.calloop.net',
|
|
18
|
+
getToken: () => fetch('/api/calloop-token').then((r) => r.text()), // your backend signs it
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
agent.setAvailability('AVAILABLE');
|
|
22
|
+
|
|
23
|
+
agent.on('incoming', (call) => {
|
|
24
|
+
ring(call.customer); // your UI
|
|
25
|
+
call.on('remoteStream', (stream) => { audioEl.srcObject = stream; });
|
|
26
|
+
call.on('ended', ({ reason }) => stopRinging());
|
|
27
|
+
acceptButton.onclick = () => call.accept(); // asks for the microphone
|
|
28
|
+
declineButton.onclick = () => call.decline();
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Connecting
|
|
33
|
+
|
|
34
|
+
`connect(options)` resolves once Calloop has identified the agent (`session:ready`).
|
|
35
|
+
|
|
36
|
+
| Option | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `url` | Calloop's base URL |
|
|
39
|
+
| `getToken` | `async () => jwt`, called on every (re)connect — agent tokens are short-lived. Or `token` for a fixed one. |
|
|
40
|
+
| `deviceId` | Stable id for this device (default: one kept in `localStorage`). Calloop uses it to tell "this device" from "my other device". |
|
|
41
|
+
| `webrtc` | `{ RTCPeerConnection, MediaStream }` outside a browser (e.g. `@roamhq/wrtc` in Node) |
|
|
42
|
+
| `getUserMedia` | `async () => MediaStream` for the microphone (default: `navigator.mediaDevices.getUserMedia`) |
|
|
43
|
+
| `io` | socket.io-client's `io` (default: the bundled one) — to use the app's own socket.io-client |
|
|
44
|
+
| `purpose` | `'session'` (default): the agent's main connection. Anything else (e.g. `'decline'`) is an auxiliary connection: no presence, no redelivered ringing calls, no resync on connect |
|
|
45
|
+
|
|
46
|
+
ICE/TURN servers come from Calloop; the app never configures them.
|
|
47
|
+
|
|
48
|
+
The SDK reconnects by itself after a network drop (`disconnected`, then
|
|
49
|
+
`ready`). When Calloop itself ends the connection — the token's signing key was
|
|
50
|
+
revoked — socket.io doesn't retry: the agent emits `closed` ({ reason }) and
|
|
51
|
+
sets `agent.closedReason`. Call `agent.close()` and `connect()` again with a
|
|
52
|
+
token signed by a current key. The same happens when the agent's role changes,
|
|
53
|
+
the agent is deleted, or its tenant or consumer is suspended.
|
|
54
|
+
|
|
55
|
+
## Security
|
|
56
|
+
|
|
57
|
+
- **Only agent tokens in the client.** The browser or app gets a short-lived
|
|
58
|
+
agent token from your backend (5–15 minutes; Calloop refuses one valid for
|
|
59
|
+
more than 24 hours). Never put a Calloop API key or a signing key in client
|
|
60
|
+
code: they're your backend's alone.
|
|
61
|
+
- **A fresh token per connect.** Give `getToken`, not a fixed `token`: it runs
|
|
62
|
+
on every (re)connect. Keep the token in memory, not `localStorage`.
|
|
63
|
+
- **`role` on every token.** A token without `role` connects as `AGENT`; send
|
|
64
|
+
`role: 'SUPERVISOR'` on a supervisor's tokens every time.
|
|
65
|
+
- **https only.** `url` is `https://…` outside local development (the SDK warns
|
|
66
|
+
in the console otherwise): the token and the call's signalling travel over it.
|
|
67
|
+
- **What ends a session.** Calloop disconnects an agent's sockets when its
|
|
68
|
+
signing key is revoked, its role changes, it's deleted, or its tenant or
|
|
69
|
+
consumer is suspended (`closed`, below). Reconnect with a fresh token, or not
|
|
70
|
+
at all.
|
|
71
|
+
|
|
72
|
+
## The agent
|
|
73
|
+
|
|
74
|
+
| | |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `agent.setAvailability('AVAILABLE' \| 'OFFLINE', { agentId? })` | `ON_CALL` is set by Calloop; `agentId`: a supervisor setting someone else |
|
|
77
|
+
| `agent.startOutbound(callId, { stream? })` | start an outbound call the consumer created (`POST /v1/tenants/{t}/calls`) |
|
|
78
|
+
| `agent.calls`, `agent.call(id)` | this agent's calls |
|
|
79
|
+
| `agent.availability` | this agent's availability (`AVAILABLE` · `ON_CALL` · `OFFLINE`), as Calloop reports it |
|
|
80
|
+
| `agent.queues` | queue snapshots by queue id (see below) |
|
|
81
|
+
| `agent.team` | the tenant's agents' availability, as it changes (see below) |
|
|
82
|
+
| `agent.sync()` | re-read the calls and queue snapshots now (done on every connect) |
|
|
83
|
+
| `agent.refreshSession()` | new ICE/TURN credentials (done automatically before they expire) |
|
|
84
|
+
| `agent.ready` | a promise that resolves with the agent once it is first identified |
|
|
85
|
+
| `agent.agent`, `agent.session` | identity from `session:ready` |
|
|
86
|
+
| `agent.iceServers`, `agent.deviceId` | the ICE servers Calloop gave this session; this device's id |
|
|
87
|
+
| `agent.close()` | |
|
|
88
|
+
|
|
89
|
+
Queue snapshots and the team's availability come only to supervisors and to
|
|
90
|
+
agents whose tenant allows the team view (the tenant's `team_view` setting).
|
|
91
|
+
Without it, a plain agent gets no queues, and `agent.team` holds only itself.
|
|
92
|
+
|
|
93
|
+
`on(event, fn)` returns a function that removes the listener; `once()` and
|
|
94
|
+
`off()` work as usual, on the agent, a call and a monitor.
|
|
95
|
+
|
|
96
|
+
Events: `incoming` (call), `elsewhere` (call), `callState` (call, state, previous),
|
|
97
|
+
`callEnded` (call, info), `availability`, `team`, `queue`, `ready` (after each reconnect),
|
|
98
|
+
`sessionRefreshed`, `disconnected`, `closed` ({ reason }: Calloop ended the
|
|
99
|
+
connection, see above), `error`.
|
|
100
|
+
|
|
101
|
+
## A call
|
|
102
|
+
|
|
103
|
+
`call.state`: `ringing` → `connecting` → `active` → `ended`; `dialing` for an
|
|
104
|
+
outbound call before the customer answers; `elsewhere` when its media is on
|
|
105
|
+
another of the agent's devices.
|
|
106
|
+
|
|
107
|
+
| | |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `call.accept({ stream? })` | answer (default microphone) |
|
|
110
|
+
| `call.decline()` | in a queue, it passes to the next agent |
|
|
111
|
+
| `call.hangup()` | |
|
|
112
|
+
| `call.mute(bool)` | |
|
|
113
|
+
| `call.transfer({ agentId } \| { queueId })` | |
|
|
114
|
+
| `call.switchHere({ stream? })` | take a call that's active on another device |
|
|
115
|
+
| `call.setPrivate(bool)` | talk privately to the monitoring supervisor |
|
|
116
|
+
| `call.customer`, `call.direction`, `call.channel`, `call.callUuid`, `call.queueId`, `call.data` | |
|
|
117
|
+
| `call.isRingAll` | offered to every member of a `RING_ALL` queue at once (first accept wins) |
|
|
118
|
+
| `call.muted`, `call.endReason` | as set by `mute()`; why the call ended (`null` until then) |
|
|
119
|
+
| `call.remoteStream`, `call.localStream`, `call.pc` | |
|
|
120
|
+
|
|
121
|
+
Events: `state` (state, previous), `remoteStream` (stream), `ended` ({ reason }),
|
|
122
|
+
`updated`, `dtmf`, `customerMedia`, `networkTerminating`, `supervisorMode`,
|
|
123
|
+
`privateChanged`.
|
|
124
|
+
|
|
125
|
+
`ended` reasons: `hangup`, `declined`, `terminated` (see
|
|
126
|
+
`terminationReason`: the call ended — or, with `transferred`, this agent's part
|
|
127
|
+
of it did: the previous agent of a transfer gets it while the call goes on with
|
|
128
|
+
the next one), `withdrawn` (the offer went to someone else),
|
|
129
|
+
`answered_elsewhere`, `media_failed`, `accept_failed`, `error`, `gone`.
|
|
130
|
+
A failed call's `terminated` also carries `failure`: `{ code, title, details? }`,
|
|
131
|
+
what the provider said (a SIP trunk: `404`, `Not Found`, its `Reason` header).
|
|
132
|
+
Other details by reason:
|
|
133
|
+
|
|
134
|
+
| `reason` | Also in the `ended` info |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `terminated` | `terminationReason`, `terminatedBy`, `failure` |
|
|
137
|
+
| `withdrawn` | `withdrawnReason`: `declined` · `taken` · `timeout` · `overflow` |
|
|
138
|
+
| `answered_elsewhere` | `by` (the agent id), `agentName` |
|
|
139
|
+
| `error` | `code`, `message` |
|
|
140
|
+
| `media_failed`, `accept_failed` | `error` (the message), when there is one |
|
|
141
|
+
|
|
142
|
+
## Supervisors
|
|
143
|
+
|
|
144
|
+
An agent whose token has `role: 'SUPERVISOR'` (`agent.isSupervisor`) also gets:
|
|
145
|
+
|
|
146
|
+
| | |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `agent.board` | the tenant's live calls (`Map` callId → call view: status, direction, customer, agent, queue, `ivr` position), rebuilt on every sync. `ivr` is filled by live IVR events only: after a sync it is missing until the call's next IVR step |
|
|
149
|
+
| `agent.monitor(callId, { stream? })` | listen to a call → a `Monitor` |
|
|
150
|
+
| `agent.monitors` | the calls being monitored (`Map` callId → `Monitor`) |
|
|
151
|
+
| `agent.transferCall(callId, { agentId } \| { queueId })` | move any call |
|
|
152
|
+
| `agent.setAvailability(value, { agentId })` | set an agent's availability |
|
|
153
|
+
|
|
154
|
+
Board events: `board` (the whole board, after each sync), `boardCall` (a call
|
|
155
|
+
appeared or changed), `boardCallEnded` (view, { terminationReason, terminatedBy }),
|
|
156
|
+
`counters` (the tenant's live counts, pushed at most every 2 s).
|
|
157
|
+
|
|
158
|
+
`agent.board` and the `board` / `boardCall` / `boardCallEnded` events are
|
|
159
|
+
supervisors' only. An agent whose tenant has the team view can page the board
|
|
160
|
+
with `boardCalls()`, but the SDK gives it no live board events.
|
|
161
|
+
|
|
162
|
+
A dashboard on a large tenant narrows and pages the board instead of holding
|
|
163
|
+
all of it:
|
|
164
|
+
|
|
165
|
+
| | |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `agent.subscribeBoard({ channelIds?, queueIds?, agentIds? })` | only these lines / queues / agents from now on; `{}` = the whole tenant |
|
|
168
|
+
| `agent.unsubscribeBoard()` | no board on this connection |
|
|
169
|
+
| `agent.boardCalls({ …filter, cursor?, limit? })` | `{ calls, nextCursor }`, newest first |
|
|
170
|
+
| `agent.boardCounters()` | supervisors: the counters now |
|
|
171
|
+
|
|
172
|
+
`agent.board` is still rebuilt from `sync()` (the whole tenant); a narrowed
|
|
173
|
+
dashboard keeps its own list from `boardCalls()` and the board events.
|
|
174
|
+
|
|
175
|
+
A filter lasts for one connection: after a reconnect Calloop subscribes the new
|
|
176
|
+
connection to the whole tenant again. Re-apply it on `ready`:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
agent.on('ready', () => agent.subscribeBoard({ queueIds: [3] }));
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
const monitor = await agent.monitor(callId); // asks for the microphone
|
|
184
|
+
monitor.on('stream', (s) => { callAudio.srcObject = s; }); // the customer and the agent, mixed
|
|
185
|
+
monitor.setMode('whisper'); // 'listen' | 'whisper' | 'barge'
|
|
186
|
+
monitor.mute(true); // your microphone
|
|
187
|
+
monitor.stop();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
A monitor has `callId`, `state` (`connecting` · `active` · `ended`), `mode`,
|
|
191
|
+
`muted` and `stream`.
|
|
192
|
+
|
|
193
|
+
`listen`: the supervisor hears the call, nobody hears the supervisor ·
|
|
194
|
+
`whisper`: the agent hears the supervisor · `barge`: both do. Calloop mixes, so
|
|
195
|
+
switching modes is instant. Monitor events: `state`, `stream` (the call as you
|
|
196
|
+
hear it), `mode`, `modeRefused` ({ mode, code, message }: Calloop refused a
|
|
197
|
+
`setMode()` — e.g. whisper on a direct call, or a mode the tenant doesn't allow
|
|
198
|
+
— and monitoring goes on in the current mode), `agentPrivate` (the agent
|
|
199
|
+
talking privately to you), `agentReconnected`, `ended` ({ reason, code?,
|
|
200
|
+
message? }). Reasons: `stopped`, `call_ended`, `ended` (Calloop ended it),
|
|
201
|
+
`disconnected`, `failed` (Calloop couldn't start it; `code`/`message` say why),
|
|
202
|
+
`not_allowed` (the tenant doesn't allow monitoring), `closed` (`agent.close()`).
|
|
203
|
+
A reconnect of the supervisor's socket ends monitoring — start it again.
|
|
204
|
+
|
|
205
|
+
## TypeScript
|
|
206
|
+
|
|
207
|
+
Types ship with the package: `connect`, `CalloopAgent`,
|
|
208
|
+
`Call`, `Monitor` and the payload shapes.
|
|
209
|
+
|
|
210
|
+
## What it handles
|
|
211
|
+
|
|
212
|
+
- Resync on every connect (`calls:sync`): ringing calls, calls on this device
|
|
213
|
+
(reconnected automatically after a reload), calls on another device.
|
|
214
|
+
- ICE candidates buffered both ways until each side is ready.
|
|
215
|
+
- Media recovery: no media within 15 s or a failed connection → one reconnect.
|
|
216
|
+
If that fails too, the call ends locally with `media_failed` and the SDK asks
|
|
217
|
+
Calloop to end it (`system_failed`), so Calloop records it `FAILED` /
|
|
218
|
+
`NETWORK_ERROR`.
|
|
219
|
+
- Direct calls (`call.isDirect`: a personal line's 1:1 call that Calloop relays
|
|
220
|
+
without a room) reconnect like any other call (the SDK sends its own offer); `agent.monitor()` listens without
|
|
221
|
+
the microphone — the stream carries the customer and the agent as two tracks
|
|
222
|
+
(listen-only).
|
|
223
|
+
- Offers withdrawn (taken, timed out, declined elsewhere, an unaccepted
|
|
224
|
+
transfer returned to its queue) and redelivered offers.
|
|
225
|
+
- TURN credentials refreshed on the live connection before they expire, for
|
|
226
|
+
long shifts.
|
|
227
|
+
|
|
228
|
+
## A first test call
|
|
229
|
+
|
|
230
|
+
Sign a test token with your signing key ([the agent token](https://docs.calloop.net/agents/protocol#the-agent-token)),
|
|
231
|
+
connect as in [Connecting](#connecting), go available, and call your line.
|
package/package.json
CHANGED
|
@@ -1,45 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@calloop/agent-sdk",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Agent client for Calloop: ring, answer, hang up and transfer calls over Calloop's agent gateway (Socket.IO + WebRTC).",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"calloop",
|
|
7
|
-
"call-center",
|
|
8
|
-
"webrtc",
|
|
9
|
-
"voip",
|
|
10
|
-
"whatsapp",
|
|
11
|
-
"sip",
|
|
12
|
-
"agent"
|
|
13
|
-
],
|
|
14
|
-
"homepage": "https://docs.calloop.net/sdks/js",
|
|
15
|
-
"repository": {
|
|
16
|
-
"type": "git",
|
|
17
|
-
"url": "git+https://github.com/PCGlobalCo/calloop-sdks.git",
|
|
18
|
-
"directory": "agent-js"
|
|
19
|
-
},
|
|
20
|
-
"bugs": {
|
|
21
|
-
"url": "https://github.com/PCGlobalCo/calloop-sdks/issues"
|
|
22
|
-
},
|
|
23
|
-
"type": "module",
|
|
24
|
-
"main": "src/index.js",
|
|
25
|
-
"types": "src/index.d.ts",
|
|
26
|
-
"exports": {
|
|
27
|
-
".": {
|
|
28
|
-
"types": "./src/index.d.ts",
|
|
29
|
-
"default": "./src/index.js"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"files": [
|
|
33
|
-
"src"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@calloop/agent-sdk",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Agent client for Calloop: ring, answer, hang up and transfer calls over Calloop's agent gateway (Socket.IO + WebRTC).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"calloop",
|
|
7
|
+
"call-center",
|
|
8
|
+
"webrtc",
|
|
9
|
+
"voip",
|
|
10
|
+
"whatsapp",
|
|
11
|
+
"sip",
|
|
12
|
+
"agent"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://docs.calloop.net/sdks/js",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/PCGlobalCo/calloop-sdks.git",
|
|
18
|
+
"directory": "agent-js"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/PCGlobalCo/calloop-sdks/issues"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "src/index.js",
|
|
25
|
+
"types": "src/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./src/index.d.ts",
|
|
29
|
+
"default": "./src/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"src",
|
|
34
|
+
"CHANGELOG.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"demo": "node examples/serve.mjs"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"socket.io-client": "^4.8.1"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|