@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 ADDED
@@ -0,0 +1,8 @@
1
+ ## 0.2.1
2
+
3
+ * Published from [PCGlobalCo/calloop-sdks](https://github.com/PCGlobalCo/calloop-sdks)
4
+ by GitHub Actions, with npm provenance. No code changes.
5
+
6
+ ## 0.2.0
7
+
8
+ * First public release.
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.0",
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
- "scripts": {
36
- "demo": "node examples/serve.mjs"
37
- },
38
- "dependencies": {
39
- "socket.io-client": "^4.8.1"
40
- },
41
- "license": "MIT",
42
- "publishConfig": {
43
- "access": "public"
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
+ }