@jambonz/schema 0.3.17 → 0.3.19

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.
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://jambonz.org/schema/components/hangup",
4
+ "title": "Hangup",
5
+ "description": "Enable the built-in hangup tool for conversational verbs (agent and s2s). When present, the runtime injects a 'hangup' tool; when the model calls it, the call is ended with a reason placed in an X-Reason SIP header. The LLM-supplied reason wins, falling back to the app-supplied default below.",
6
+ "type": "object",
7
+ "properties": {
8
+ "reason": {
9
+ "type": "string",
10
+ "description": "Default reason placed in the X-Reason SIP header when the call is hung up. Used as a fallback when the LLM does not supply its own reason."
11
+ }
12
+ },
13
+ "additionalProperties": false
14
+ }
@@ -89,6 +89,10 @@
89
89
  "$ref": "handoff",
90
90
  "description": "Declarative transfer-to-human config for s2s verbs. When present, the runtime injects a 'transfer_to_human' tool and runs the packaged transfer choreography when the model calls it."
91
91
  },
92
+ "hangup": {
93
+ "$ref": "hangup",
94
+ "description": "Enable the built-in hangup tool for s2s verbs. When present, the runtime injects a 'hangup' tool; when the model calls it, the call is ended with a reason placed in an X-Reason SIP header (LLM-supplied reason wins, falling back to hangup.reason)."
95
+ },
92
96
  "events": {
93
97
  "type": "array",
94
98
  "items": { "type": "string" },
@@ -0,0 +1,186 @@
1
+ # Conference Monitoring, Coaching & Transcription
2
+
3
+ This guide covers building supervision features on top of jambonz conferences:
4
+ silently monitoring a live room, whisper-coaching the agents in it, barging in
5
+ as a full participant, and tapping the room's audio for transcription or AI
6
+ analysis — all without interrupting or re-dialing any call.
7
+
8
+ A complete, working reference application (React console + Node backend) built
9
+ on these patterns is at **https://github.com/jambonz/room-monitor** — start
10
+ with its `docs/ADAPTING.md`. The `conference-supervision` SDK example
11
+ (`get_sdk_example`) is a distilled version of the same patterns.
12
+
13
+ ## The building blocks
14
+
15
+ | Capability | Mechanism |
16
+ |---|---|
17
+ | Classify participants (who is an "agent") | `memberTag` on each conference member — set at join or changed mid-call |
18
+ | Silent monitor | join the conference muted (`joinMuted: true`) |
19
+ | Coach / whisper | the supervisor's audio is delivered **only** to members carrying a given tag (`speakOnlyTo` at join, or the `coach` participant action mid-call) |
20
+ | Barge-in | `uncoach` + unmute — the supervisor is heard by everyone |
21
+ | Room audio out (transcription / AI / recording) | a **conference listen fork**: jambonz streams the room's mixed audio (L16 PCM) to a WebSocket you host |
22
+ | Discover live conferences | `GET /Accounts/{sid}/Conferences` (add `?expand=participants` for members + tags on systems that support it) |
23
+
24
+ Mode changes are mid-call commands on an existing leg — a supervisor moves
25
+ between monitor → coach → barge instantly, with no re-INVITE and no
26
+ interruption to the room.
27
+
28
+ ## Tagging members
29
+
30
+ Tags drive everything: coaching targets, UI classification, and per-tag audio
31
+ flows. Set a tag when a member joins:
32
+
33
+ ```typescript
34
+ session.conference({
35
+ name: 'support-room-42',
36
+ memberTag: 'agent',
37
+ }).send();
38
+ ```
39
+
40
+ Tags are **fully dynamic** — add or remove them on a participant who is already
41
+ in the conference (no re-join). From the application controlling that leg:
42
+
43
+ ```typescript
44
+ // promote a live participant (e.g. warm transfer completes, human takes over from AI)
45
+ session.injectCommand('conf:participant-action', { action: 'tag', tag: 'agent' });
46
+
47
+ // demote — clears the member's tag
48
+ session.injectCommand('conf:participant-action', { action: 'untag' });
49
+
50
+ // third argument targets another leg this session controls (e.g. a dialed B-leg)
51
+ session.injectCommand('conf:participant-action', { action: 'tag', tag: 'agent' }, bLegCallSid);
52
+ ```
53
+
54
+ Or from anywhere via the REST client:
55
+
56
+ ```typescript
57
+ import { JambonzClient } from '@jambonz/sdk/client';
58
+
59
+ const client = new JambonzClient({ baseUrl, accountSid, apiKey });
60
+ await client.calls.update(callSid, {
61
+ conferenceParticipantAction: { action: 'tag', tag: 'agent' },
62
+ });
63
+ ```
64
+
65
+ The full mid-call action set is `tag | untag | coach | uncoach | mute | unmute
66
+ | hold | unhold`. Tag changes take effect live: an active coach automatically
67
+ starts/stops reaching the member as its tag changes.
68
+
69
+ ## The supervisor leg
70
+
71
+ The supervisor is just another conference member whose join options and
72
+ participant actions produce the three modes. Join for silent monitoring:
73
+
74
+ ```typescript
75
+ session
76
+ .answer()
77
+ .conference({
78
+ name: roomName,
79
+ joinMuted: true, // hears everything, heard by no one
80
+ memberTag: 'supervisor', // lets other tooling filter this leg out
81
+ startConferenceOnEnter: false, // never create/destroy the room being watched
82
+ endConferenceOnExit: false,
83
+ actionHook: '/conf-done',
84
+ })
85
+ .send();
86
+ ```
87
+
88
+ To join directly in coach mode instead, add `speakOnlyTo: 'agent'` and omit
89
+ `joinMuted`. Then switch modes mid-call (from any service, via REST):
90
+
91
+ ```typescript
92
+ // coach: audio delivered only to members tagged 'agent'
93
+ await client.calls.update(supervisorCallSid, {
94
+ conferenceParticipantAction: { action: 'coach', tag: 'agent' },
95
+ });
96
+ await client.calls.update(supervisorCallSid, { conf_mute_status: 'unmute' });
97
+
98
+ // barge-in: heard by everyone
99
+ await client.calls.update(supervisorCallSid, {
100
+ conferenceParticipantAction: { action: 'uncoach' },
101
+ });
102
+
103
+ // back to silent monitoring
104
+ await client.calls.update(supervisorCallSid, { conf_mute_status: 'mute' });
105
+ ```
106
+
107
+ Coach semantics to rely on:
108
+
109
+ - Coached audio reaches **only** members carrying the target tag — including
110
+ members (and audio forks) that join *after* coaching started.
111
+ - Audio forks are never tagged, so a transcription or recording tap **cannot
112
+ hear private coaching**; it hears the coach again after `uncoach`.
113
+ - If the last tagged member leaves, fall back to monitoring in your
114
+ application logic (the reference app polls participants and downgrades
115
+ coach → monitor automatically).
116
+
117
+ ## Tapping the room's audio (transcription, AI, recording)
118
+
119
+ A **conference listen fork** streams the room's mixed audio to a WebSocket you
120
+ operate. jambonz only transports audio — what you do with it (STT, sentiment,
121
+ archiving) is entirely your side of the socket.
122
+
123
+ ```bash
124
+ # start — jambonz dials out to your wsUrl and streams L16 PCM
125
+ curl -X POST "$BASE_URL/v1/Accounts/$ACCOUNT_SID/Conferences/support-room-42/listen" \
126
+ -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
127
+ -d '{"url": "wss://your-host/fork", "sampleRate": 16000,
128
+ "metadata": {"room": "support-room-42", "sampleRate": 16000}}'
129
+
130
+ # stop
131
+ curl -X DELETE "$BASE_URL/v1/Accounts/$ACCOUNT_SID/Conferences/support-room-42/listen" \
132
+ -H "Authorization: Bearer $API_KEY"
133
+ ```
134
+
135
+ Consume it in the same `@jambonz/sdk` app that runs your call logic:
136
+
137
+ ```typescript
138
+ const makeService = createEndpoint({ server, port: 3000 });
139
+ const audio = makeService.audio({ path: '/fork' });
140
+
141
+ audio.on('connection', (stream) => {
142
+ // your `metadata` arrives verbatim as the fork's first text frame and is
143
+ // exposed as stream.metadata — make it self-describing (include sampleRate)
144
+ console.log('fork connected', stream.metadata);
145
+ stream.on('audio', (pcm: Buffer) => {
146
+ // feed your STT / AI / recorder here
147
+ });
148
+ });
149
+ ```
150
+
151
+ Lifecycle guarantees: the fork is a media-server-owned bot member — it is
152
+ **excluded from participant counts, never keeps a room alive, and is torn down
153
+ automatically when the conference ends**. Starting a fork requires no
154
+ participant leg (you can transcribe a room nobody is monitoring), and repeated
155
+ starts for the same conference are idempotent.
156
+
157
+ > Conference-level listen (`/Conferences/{name}/listen`) and the
158
+ > `?expand=participants` listing require a jambonz release with MediaJam-based
159
+ > conferencing that includes these endpoints.
160
+
161
+ ## Discovering live rooms
162
+
163
+ ```typescript
164
+ // names only (all releases)
165
+ const rooms = await client.conferences.list();
166
+
167
+ // with participants + tags (releases with the enriched listing)
168
+ const res = await fetch(
169
+ `${baseUrl}/v1/Accounts/${accountSid}/Conferences?expand=participants`,
170
+ { headers: { Authorization: `Bearer ${apiKey}` } });
171
+ // → [{ id, name, durationSec, participants: [{ call_sid, label, memberTag, isAgent }] }]
172
+ ```
173
+
174
+ Filter out your own supervisor legs (`memberTag === 'supervisor'`) before
175
+ displaying counts, and derive "can I coach?" from agent presence.
176
+
177
+ ## Putting it together
178
+
179
+ A typical supervision backend is three small pieces: a poller on the
180
+ conferences listing (rooms → your UI), a jambonz application that joins
181
+ supervisor legs and applies participant actions, and an audio-fork consumer
182
+ feeding your STT. The reference app implements exactly this — and ships a
183
+ closed-loop end-to-end test (`tools/e2e/`) that verifies the audio-visibility
184
+ contracts, including coach privacy, using scripted fake-microphone browsers.
185
+ If you adapt the patterns, adapt the test too: it will keep verifying who can
186
+ hear whom as your code evolves.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/schema",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "description": "JSON Schema definitions and validation for jambonz verb applications",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -140,7 +140,9 @@
140
140
  "baseten",
141
141
  "azure-openai",
142
142
  "groq",
143
- "huggingface"
143
+ "huggingface",
144
+ "moonshot",
145
+ "zai"
144
146
  ],
145
147
  "description": "LLM vendor id. Must match a `@jambonz/llm` registered adapter."
146
148
  },
@@ -305,6 +307,10 @@
305
307
  "$ref": "../components/handoff",
306
308
  "description": "Declarative transfer-to-human config. When present, the runtime injects a 'transfer_to_human' tool into the agent's toolset and runs the packaged transfer choreography (blind/warm) when the LLM calls it."
307
309
  },
310
+ "hangup": {
311
+ "$ref": "../components/hangup",
312
+ "description": "Enable the built-in hangup tool. When present, the runtime injects a 'hangup' tool into the agent's toolset; when the LLM calls it, the call is ended with a reason placed in an X-Reason SIP header (LLM-supplied reason wins, falling back to hangup.reason)."
313
+ },
308
314
  "toolFiller": {
309
315
  "oneOf": [
310
316
  { "type": "boolean", "const": false },