@jambonz/schema 0.4.31 → 0.4.32

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.
@@ -291,6 +291,44 @@ session.updateAgent({
291
291
  });
292
292
  ```
293
293
 
294
+ ## Your own harness (`llm.vendor: "harness"`)
295
+
296
+ With `vendor: "harness"` the customer's own code decides what the agent says, while jambonz keeps STT, turn-taking, barge-in, TTS and the turn record. feature-server opens one WebSocket per call to the URL in the account's **Harness** LLM credential (fields `url` and `api_key`, looked up by vendor and `label` like any LLM credential), with subprotocol `harness.jambonz.org` and `Authorization: Bearer <api_key>`.
297
+
298
+ ```json
299
+ {
300
+ "verb": "agent",
301
+ "stt": {"vendor": "deepgramflux", "language": "en-US"},
302
+ "tts": {"vendor": "cartesia", "voice": "..."},
303
+ "llm": {
304
+ "vendor": "harness",
305
+ "label": "my-harness",
306
+ "llmOptions": {"tools": [{"name": "lookup_account", "description": "...", "parameters": {"type": "object"}}]},
307
+ "harness": {
308
+ "metadata": {"agent": "outage-line"},
309
+ "firstChunkTimeoutMs": 4000,
310
+ "holdingPhrase": "One moment.",
311
+ "onHarnessFailure": {"say": "Sorry, I'm having trouble. Transferring you now.", "transfer": "+15085550100"}
312
+ }
313
+ },
314
+ "toolHook": "/tool",
315
+ "eventHook": "/event",
316
+ "actionHook": "/done"
317
+ }
318
+ ```
319
+
320
+ The harness owns prompts, context, fillers and reprompts, so with it `llmOptions` may carry only `tools`, and `model`, `auth`, `connectOptions`, `mcpServers`, `toolFiller`, `earlyGeneration: true`, `speechCleanup: true` and `noResponseMaxPrompts` are rejected by the schema. The tools are sent to the harness in `session:start` as OpenAI function definitions; when the harness sends `tool:call`, jambonz runs it through the `toolHook` exactly as for a model (`agent:tool-call`, answered with `llm:tool-output`) and returns `tool:result`. `handoff`, when present, gives the defaults for transfers the harness asks for.
321
+
322
+ What jambonz sends: `session:start`, `speech:start`, `transcript`, `turn:start` (`input` is `speech`, `greeting` or `no_input`), `turn:cancel`, `event` (`user_interruption`, `reply_spoken`, `reply_dropped`, `silence`, `transfer`, `recording`, `application_update`, `error`), `command:result`, `tool:result`, `session:end`. What the harness sends: `reply:chunk` / `reply:done` (OpenAI `ChatCompletionChunk`s; `choices[0].delta.content` is spoken), `tool:call`, and `command` (`hangup`, `transfer`, `record`, `barge_in`, `listen`). The full protocol is the harness protocol reference.
323
+
324
+ Notes on this implementation:
325
+ - `application_update` carries the application's `agent:update` type as `update_type` (the message's own `type` is `event`), with the rest of the update in `data`.
326
+ - `user_interruption.spoken` is exact with TTS vendors that report alignment, and estimated from playout time otherwise.
327
+ - `noResponseTimeout` sends a `silence` event (`seconds`, `streak`) each time it elapses; jambonz does not reprompt.
328
+ - `play` and `listen` with `hints` are not supported yet and are refused in `command:result`; `listen` with `language` works where the STT vendor can change language mid-call (Deepgram Flux).
329
+ - Failures (socket cannot open, no reply twice in `firstChunkTimeoutMs`, a dropped socket that cannot be reopened within 2 s) run `onHarnessFailure`; the actionHook reports `completionReason: "harness_failure"` with `harness_error: {reason, message}` and the eventHook gets `harness_failure`.
330
+ - Each `turn_end` carries `engine: "harness"`, `harness.turn_record` (the harness's `reply:done.turn_record`, up to 16 KB) and `latency.harness_first_chunk_ms` in place of `llm_ms`.
331
+
294
332
  ## LLM configuration
295
333
 
296
334
  The `llm` property is the only required field. It configures the text-to-text LLM:
@@ -2,6 +2,17 @@
2
2
 
3
3
  The dialogflow verb connects the caller to a Google Dialogflow agent (ES, CX, or CES). See the verb schema for all properties.
4
4
 
5
+ ## Credentials without a key in the verb
6
+
7
+ `credentials` may name a Google speech credential on the account instead of carrying a service account key:
8
+
9
+ ```json
10
+ {"verb": "dialogflow", "model": "ces", "project": "my-project", "region": "us", "agent": "<CES app id>",
11
+ "lang": "en-US", "credentials": {"vendor": "google", "label": "my-ces"}}
12
+ ```
13
+
14
+ An object with only `vendor: "google"` and optionally `label` is a reference. feature-server looks up the Google speech credential with that label (no label, or null, means the unlabelled one), account first and then service provider, and uses its service key. The credential does not have to be enabled or tested for STT or TTS. If none is found the verb ends at once: the actionHook gets `dialogflowResult: "error"` with `error.code: "credential_not_found"`, and feature-server logs the account sid, vendor and label. An inline service account works as before.
15
+
5
16
  ## Sending input into a CES conversation mid-call
6
17
 
7
18
  With `model: "ces"`, an application can send input into the running conversation without restarting the verb. Updates can be sent via:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jambonz/schema",
3
- "version": "0.4.31",
3
+ "version": "0.4.32",
4
4
  "description": "JSON Schema definitions and validation for jambonz verb applications",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -130,19 +130,19 @@
130
130
  },
131
131
  "llm": {
132
132
  "type": "object",
133
- "description": "LLM configuration for the agent. Optional: when omitted, or when `vendor`/`model` are omitted, they fall back to the application's configured default LLM (like `stt`/`tts`).",
133
+ "description": "LLM configuration for the agent. Optional: when omitted, or when `vendor`/`model` are omitted, they fall back to the application's configured default LLM (like `stt`/`tts`). With `vendor: \"harness\"` a customer-run harness decides what to say instead of a model, over one WebSocket per call opened by jambonz; jambonz keeps STT, turn-taking, barge-in and TTS. See `harness`.",
134
134
  "properties": {
135
135
  "vendor": {
136
136
  "type": "string",
137
- "description": "LLM vendor id. Must match a `@jambonz/llm` registered adapter. Optional: falls back to the application's default LLM vendor."
137
+ "description": "LLM vendor id. Must match a `@jambonz/llm` registered adapter, or be \"harness\" for a customer-run harness (the account's Harness credential, looked up by vendor and label, gives its WebSocket URL and key). Optional: falls back to the application's default LLM vendor."
138
138
  },
139
139
  "model": {
140
140
  "type": "string",
141
141
  "description": "Vendor-specific model id (e.g. 'gpt-4o', 'claude-sonnet-4-5-20250929'). Optional: falls back to the application's default LLM model."
142
142
  },
143
143
  "label": {
144
- "type": "string",
145
- "description": "Optional label to disambiguate when the account has multiple credentials for the same vendor."
144
+ "type": ["string", "null"],
145
+ "description": "Optional label to disambiguate when the account has multiple credentials for the same vendor. null or omitted selects the unlabelled credential."
146
146
  },
147
147
  "auth": {
148
148
  "type": "object",
@@ -204,13 +204,50 @@
204
204
  },
205
205
  "tools": {
206
206
  "type": "array",
207
- "description": "Tool / function definitions available to the model. The MCP-flat shape `{name, description, parameters}` is canonical; the OpenAI-wrapped form `{type:'function', function:{...}}` is also accepted.",
207
+ "description": "Tool / function definitions available to the model. The MCP-flat shape `{name, description, parameters}` is canonical; the OpenAI-wrapped form `{type:'function', function:{...}}` is also accepted. With vendor \"harness\" these are sent to the harness in session:start as OpenAI function definitions, and the harness runs them with tool:call through the toolHook.",
208
208
  "items": {
209
209
  "type": "object"
210
210
  }
211
211
  }
212
212
  },
213
213
  "additionalProperties": false
214
+ },
215
+ "harness": {
216
+ "type": "object",
217
+ "description": "Harness settings, only with vendor \"harness\". The harness owns prompts, context, fillers and reprompts, so with it `llmOptions` may carry only `tools`, `model`/`auth`/`connectOptions` are not allowed, and `mcpServers`, `toolFiller`, `earlyGeneration`, `speechCleanup: true` and `noResponseMaxPrompts` are rejected. `noResponseTimeout` still sets when the harness is sent a `silence` event; jambonz does not reprompt on its own. `handoff`, when present, supplies the defaults (mode, disposition, timeout, callerId) for transfers the harness asks for.",
218
+ "properties": {
219
+ "metadata": {
220
+ "type": "object",
221
+ "additionalProperties": true,
222
+ "description": "Free-form JSON passed to the harness in session:start (for example agent id, version and per-call variables)."
223
+ },
224
+ "firstChunkTimeoutMs": {
225
+ "type": "integer",
226
+ "minimum": 500,
227
+ "default": 4000,
228
+ "description": "Milliseconds from the end of the caller's turn to the harness's first reply:chunk (or reply:done). On the first timeout holdingPhrase is spoken once; on a second timeout onHarnessFailure runs. While a tool:call from the harness is outstanding, the second timeout waits for it. Default: 4000."
229
+ },
230
+ "holdingPhrase": {
231
+ "type": "string",
232
+ "description": "Spoken once when the harness has not started its reply within firstChunkTimeoutMs, for example 'One moment.'. When omitted nothing is spoken and the second timeout still applies."
233
+ },
234
+ "onHarnessFailure": {
235
+ "type": "object",
236
+ "description": "What jambonz does when the harness cannot be reached, stops answering, or drops and cannot be reconnected within 2 seconds: speak `say`, then transfer to `transfer` if given, otherwise hang up. The actionHook reports completionReason 'harness_failure' with a harness_error object.",
237
+ "properties": {
238
+ "say": {
239
+ "type": "string",
240
+ "description": "Spoken to the caller before the transfer or hangup."
241
+ },
242
+ "transfer": {
243
+ "type": "string",
244
+ "description": "Where to transfer the caller: an E.164 number, a sip: URI, or a registered user name. Uses the verb's `handoff` settings when present, otherwise a blind transfer by dial."
245
+ }
246
+ },
247
+ "additionalProperties": false
248
+ }
249
+ },
250
+ "additionalProperties": false
214
251
  }
215
252
  },
216
253
  "additionalProperties": false
@@ -221,7 +258,7 @@
221
258
  },
222
259
  "eventHook": {
223
260
  "$ref": "../components/actionHook",
224
- "description": "A webhook invoked for agent events. Receives event types: 'user_transcript' (user speech recognized), 'llm_response' (assistant reply), 'user_interruption' (barge-in detected), and 'turn_end' (end-of-turn summary with transcript, response, and latency metrics)."
261
+ "description": "A webhook invoked for agent events. Receives event types: 'user_transcript' (user speech recognized), 'llm_response' (assistant reply), 'user_interruption' (barge-in detected), and 'turn_end' (end-of-turn summary with transcript, response, and latency metrics). With vendor \"harness\" it also receives 'harness_failure' ({reason, message}) when onHarnessFailure runs, and turn_end carries `engine: \"harness\"`, `harness.turn_record` and `latency.harness_first_chunk_ms`."
225
262
  },
226
263
  "toolHook": {
227
264
  "$ref": "../components/actionHook",
@@ -343,6 +380,41 @@
343
380
  "description": "Configuration for playing filler audio while tool calls are in progress. Prevents silence during long-running tool executions."
344
381
  }
345
382
  },
383
+ "allOf": [
384
+ {
385
+ "if": {
386
+ "properties": {
387
+ "llm": {
388
+ "properties": { "vendor": { "const": "harness" } },
389
+ "required": ["vendor"]
390
+ }
391
+ },
392
+ "required": ["llm"]
393
+ },
394
+ "then": {
395
+ "properties": {
396
+ "llm": {
397
+ "properties": {
398
+ "model": false,
399
+ "auth": false,
400
+ "connectOptions": false,
401
+ "llmOptions": { "propertyNames": { "enum": ["tools"] } }
402
+ }
403
+ },
404
+ "mcpServers": { "maxItems": 0 },
405
+ "toolFiller": { "const": false },
406
+ "earlyGeneration": { "const": false },
407
+ "speechCleanup": { "const": false },
408
+ "noResponseMaxPrompts": { "const": 0 }
409
+ }
410
+ },
411
+ "else": {
412
+ "properties": {
413
+ "llm": { "not": { "required": ["harness"] } }
414
+ }
415
+ }
416
+ }
417
+ ],
346
418
  "$defs": {
347
419
  "llmMessage": {
348
420
  "type": "object",
@@ -454,6 +526,48 @@
454
526
  "sticky": false
455
527
  },
456
528
  "actionHook": "/agent-complete"
529
+ },
530
+ {
531
+ "verb": "agent",
532
+ "stt": {
533
+ "vendor": "deepgramflux",
534
+ "language": "en-US"
535
+ },
536
+ "tts": {
537
+ "vendor": "cartesia",
538
+ "voice": "sonic-english"
539
+ },
540
+ "llm": {
541
+ "vendor": "harness",
542
+ "label": "my-harness",
543
+ "llmOptions": {
544
+ "tools": [
545
+ {
546
+ "name": "lookup_account",
547
+ "description": "Look up the caller's account by phone number.",
548
+ "parameters": {
549
+ "type": "object",
550
+ "properties": { "phone": { "type": "string" } },
551
+ "required": ["phone"]
552
+ }
553
+ }
554
+ ]
555
+ },
556
+ "harness": {
557
+ "metadata": { "agent": "outage-line", "tenant": "acme" },
558
+ "firstChunkTimeoutMs": 4000,
559
+ "holdingPhrase": "One moment.",
560
+ "onHarnessFailure": {
561
+ "say": "Sorry, I'm having trouble. Transferring you now.",
562
+ "transfer": "+15085550100"
563
+ }
564
+ }
565
+ },
566
+ "turnDetection": "stt",
567
+ "bargeIn": { "enable": true },
568
+ "toolHook": "/tool",
569
+ "eventHook": "/event",
570
+ "actionHook": "/done"
457
571
  }
458
572
  ]
459
573
  }
@@ -24,7 +24,7 @@
24
24
  "type": "string"
25
25
  }
26
26
  ],
27
- "description": "Google service account credentials as a JSON object or stringified JSON."
27
+ "description": "Google service account credentials as a JSON object or stringified JSON. Alternatively a reference to a Google speech credential on the account, so no key is carried in the verb: {\"vendor\": \"google\", \"label\": \"my-ces\"} (label optional; omit it or use null for the unlabelled Google credential). An object with only `vendor` (\"google\") and optionally `label` is treated as a reference; feature-server looks the credential up by vendor and label, account first then service provider, and uses its service key. If none is found the verb ends at once and its actionHook reports dialogflowResult 'error' with error.code 'credential_not_found'."
28
28
  },
29
29
  "project": {
30
30
  "type": "string",