@guava-ai/guava-sdk 0.23.0 → 0.24.0

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.
Files changed (57) hide show
  1. package/dist/examples/help-desk.js +22 -8
  2. package/dist/examples/help-desk.js.map +1 -1
  3. package/dist/examples/property-insurance.js +14 -3
  4. package/dist/examples/property-insurance.js.map +1 -1
  5. package/dist/examples/restaurant-waitlist.js +9 -1
  6. package/dist/examples/restaurant-waitlist.js.map +1 -1
  7. package/dist/examples/scheduling-outbound.js +2 -4
  8. package/dist/examples/scheduling-outbound.js.map +1 -1
  9. package/dist/src/action-item.d.ts +4 -4
  10. package/dist/src/action-item.js +1 -1
  11. package/dist/src/action-item.js.map +1 -1
  12. package/dist/src/agent.d.ts +18 -6
  13. package/dist/src/agent.js +269 -121
  14. package/dist/src/agent.js.map +1 -1
  15. package/dist/src/call.d.ts +9 -1
  16. package/dist/src/call.js +24 -1
  17. package/dist/src/call.js.map +1 -1
  18. package/dist/src/commands.d.ts +37 -9
  19. package/dist/src/commands.js +13 -3
  20. package/dist/src/commands.js.map +1 -1
  21. package/dist/src/events.d.ts +28 -1
  22. package/dist/src/events.js +10 -2
  23. package/dist/src/events.js.map +1 -1
  24. package/dist/src/health.d.ts +35 -0
  25. package/dist/src/health.js +99 -0
  26. package/dist/src/health.js.map +1 -0
  27. package/dist/src/helpers/index.d.ts +1 -0
  28. package/dist/src/helpers/index.js +9 -0
  29. package/dist/src/helpers/index.js.map +1 -0
  30. package/dist/src/helpers/llm.d.ts +33 -1
  31. package/dist/src/helpers/llm.js +220 -0
  32. package/dist/src/helpers/llm.js.map +1 -1
  33. package/dist/src/helpers/server-rag.d.ts +25 -0
  34. package/dist/src/helpers/server-rag.js +119 -0
  35. package/dist/src/helpers/server-rag.js.map +1 -0
  36. package/dist/src/socket/protocol.d.ts +4 -4
  37. package/dist/src/testing/mocks.d.ts +6 -0
  38. package/dist/src/testing/mocks.js +12 -0
  39. package/dist/src/testing/mocks.js.map +1 -1
  40. package/dist/src/version.d.ts +1 -1
  41. package/dist/src/version.js +1 -1
  42. package/examples/help-desk.ts +21 -12
  43. package/examples/property-insurance.ts +15 -3
  44. package/examples/restaurant-waitlist.ts +10 -1
  45. package/examples/scheduling-outbound.ts +2 -4
  46. package/package.json +5 -1
  47. package/src/action-item.ts +1 -1
  48. package/src/agent.ts +261 -127
  49. package/src/call.ts +37 -1
  50. package/src/commands.ts +16 -2
  51. package/src/events.ts +11 -1
  52. package/src/health.ts +120 -0
  53. package/src/helpers/index.ts +2 -0
  54. package/src/helpers/llm.ts +178 -1
  55. package/src/helpers/server-rag.ts +132 -0
  56. package/src/testing/mocks.ts +17 -0
  57. package/src/version.ts +1 -1
package/src/agent.ts CHANGED
@@ -1,36 +1,38 @@
1
1
  import WebSocket from "ws";
2
- import { Call, Client } from "./index.ts";
3
- import { getDefaultLogger, type Logger } from "./logging.ts";
4
- import { getBaseUrl, fetchOrThrow } from "./utils.ts";
5
- import { runWebrtcHelper } from "./webrtc-helper.ts";
6
2
  import {
3
+ ActionSuggestionCommand,
7
4
  AnswerQuestionCommand,
8
5
  ChoiceResultCommand,
9
- RegisteredHooksCommand,
10
- ActionSuggestionCommand,
11
6
  type Command,
7
+ RegisteredHooksCommand,
12
8
  } from "./commands.ts";
13
9
  import {
14
- type GuavaEvent,
15
- type CallerSpeechEvent,
16
10
  type AgentSpeechEvent,
17
11
  type BotSessionEnded,
12
+ type CallerSpeechEvent,
18
13
  type DTMFPressedEvent,
14
+ type EscalateEvent,
15
+ type GuavaEvent,
19
16
  decodeEventDict,
20
17
  } from "./events.ts";
21
- import { telemetryClient } from "./telemetry.ts";
22
- import { GuavaSocket, GuavaSocketClosedError } from "./socket/client.ts";
23
18
  import {
24
19
  type ClientMessage as DialerClientMessage,
25
20
  type ServerMessage as DialerServerMessage,
26
21
  decodeServerMessage as decodeDialerServerMessage,
27
22
  } from "./guavadialer-events.ts";
28
- import * as ListenInbound from "./socket/listen-inbound.ts";
23
+ import { HealthContext, getHealthServer } from "./health.ts";
24
+ import { _generate } from "./helpers/llm.ts";
25
+ import { Call, Client } from "./index.ts";
26
+ import { type Logger, getDefaultLogger } from "./logging.ts";
29
27
  import type { CallInfo } from "./socket/call-info.ts";
30
- import { TestSession } from "./testing/session.ts";
31
- import { SessionStarted } from "./testing/protocol.ts";
28
+ import { GuavaSocket, GuavaSocketClosedError } from "./socket/client.ts";
29
+ import * as ListenInbound from "./socket/listen-inbound.ts";
30
+ import { telemetryClient } from "./telemetry.ts";
32
31
  import { runChat } from "./testing/chat.ts";
33
- import { _generate } from "./helpers/llm.ts";
32
+ import { SessionStarted } from "./testing/protocol.ts";
33
+ import { TestSession } from "./testing/session.ts";
34
+ import { fetchOrThrow, getBaseUrl } from "./utils.ts";
35
+ import { runWebrtcHelper } from "./webrtc-helper.ts";
34
36
 
35
37
  export type { CallInfo } from "./socket/call-info.ts";
36
38
 
@@ -41,13 +43,18 @@ export interface SuggestedAction {
41
43
  description?: string;
42
44
  }
43
45
 
44
- export type InboundConnection = { agent_number: string } | { webrtc_code: string };
46
+ export type InboundConnection =
47
+ | { agent_number: string }
48
+ | { webrtc_code: string }
49
+ | { sip_code: string };
45
50
 
46
51
  @telemetryClient.trackClass()
47
52
  export class Agent {
48
53
  private _name?: string;
49
54
  private _organization?: string;
50
55
  private _purpose?: string;
56
+ private _voice?: string;
57
+ private _acceptDtmfForNumbers: boolean;
51
58
  private _logger: Logger;
52
59
 
53
60
  private _client: Client = new Client();
@@ -68,16 +75,29 @@ export class Agent {
68
75
  private _onActionRequested?: (
69
76
  call: Call,
70
77
  intentSummary: string,
71
- ) => Promise<SuggestedAction | undefined>;
78
+ ) => Promise<SuggestedAction | SuggestedAction[] | null | undefined>;
72
79
  private _onActionGeneric?: (call: Call, actionKey: string) => Promise<void>;
73
80
  private _onActionHandlers: Record<string, (call: Call) => Promise<void>> = {};
81
+ private _onValidateHandlers: Record<
82
+ string,
83
+ (call: Call, value: unknown) => Promise<true | [false, string]>
84
+ > = {};
74
85
  private _onSessionEnd?: (call: Call, event: BotSessionEnded) => Promise<void>;
86
+ private _onEscalate?: (call: Call, event: EscalateEvent) => Promise<void>;
75
87
  private _onDtmf?: (call: Call, event: DTMFPressedEvent) => Promise<void>;
76
88
 
77
- constructor(args?: { name?: string; organization?: string; purpose?: string }) {
89
+ constructor(args?: {
90
+ name?: string;
91
+ organization?: string;
92
+ purpose?: string;
93
+ voice?: string;
94
+ acceptDtmf?: boolean;
95
+ }) {
78
96
  this._name = args?.name;
79
97
  this._organization = args?.organization;
80
98
  this._purpose = args?.purpose;
99
+ this._voice = args?.voice;
100
+ this._acceptDtmfForNumbers = args?.acceptDtmf ?? true;
81
101
  this._logger = getDefaultLogger();
82
102
  }
83
103
 
@@ -117,6 +137,13 @@ export class Agent {
117
137
  }
118
138
  }
119
139
 
140
+ onValidate(
141
+ fieldKey: string,
142
+ callback: (call: Call, value: unknown) => Promise<true | [false, string]>,
143
+ ): void {
144
+ this._onValidateHandlers[fieldKey] = callback;
145
+ }
146
+
120
147
  onSearchQuery(
121
148
  fieldKey: string,
122
149
  callback: (call: Call, query: string) => Promise<[string[], string[]]>,
@@ -125,7 +152,10 @@ export class Agent {
125
152
  }
126
153
 
127
154
  onActionRequest(
128
- callback: (call: Call, intentSummary: string) => Promise<SuggestedAction | undefined>,
155
+ callback: (
156
+ call: Call,
157
+ intentSummary: string,
158
+ ) => Promise<SuggestedAction | SuggestedAction[] | null | undefined>,
129
159
  ): void {
130
160
  this._onActionRequested = callback;
131
161
  }
@@ -150,6 +180,10 @@ export class Agent {
150
180
  this._onSessionEnd = callback;
151
181
  }
152
182
 
183
+ onEscalate(callback: (call: Call, event: EscalateEvent) => Promise<void>): void {
184
+ this._onEscalate = callback;
185
+ }
186
+
153
187
  onDtmf(callback: (call: Call, event: DTMFPressedEvent) => Promise<void>): void {
154
188
  this._onDtmf = callback;
155
189
  }
@@ -208,7 +242,9 @@ export class Agent {
208
242
  }
209
243
 
210
244
  async listenPhone(phoneNumber: string): Promise<void> {
211
- return this._listenInbound({ agent_number: phoneNumber });
245
+ const healthCtx = new HealthContext();
246
+ await using _server = await getHealthServer(healthCtx);
247
+ return this._listenInbound(healthCtx, { agent_number: phoneNumber });
212
248
  }
213
249
 
214
250
  async listenWebrtc(webrtcCode?: string): Promise<void> {
@@ -216,12 +252,21 @@ export class Agent {
216
252
  this._logger.info("No WebRTC code provided. Creating a temporary one.");
217
253
  webrtcCode = await this._client.createWebrtcAgent(3600);
218
254
  }
219
- return this._listenInbound({ webrtc_code: webrtcCode });
255
+ const healthCtx = new HealthContext();
256
+ await using _server = await getHealthServer(healthCtx);
257
+ return this._listenInbound(healthCtx, { webrtc_code: webrtcCode });
258
+ }
259
+
260
+ async listenSip(sipCode: string): Promise<void> {
261
+ const healthCtx = new HealthContext();
262
+ await using _server = await getHealthServer(healthCtx);
263
+ return this._listenInbound(healthCtx, { sip_code: sipCode });
220
264
  }
221
265
 
222
266
  async callLocal(): Promise<void> {
223
267
  const webrtcCode = await this._client.createWebrtcAgent(300);
224
- this._listenInbound({ webrtc_code: webrtcCode }).catch((err) => {
268
+ // No health-server for call_local.
269
+ this._listenInbound(new HealthContext(), { webrtc_code: webrtcCode }).catch((err) => {
225
270
  this._logger.error("Listen loop error: %s", err);
226
271
  });
227
272
  await runWebrtcHelper(webrtcCode, getBaseUrl());
@@ -237,13 +282,29 @@ export class Agent {
237
282
  await this._onAgentSpeech(call, event);
238
283
  }
239
284
  } else if (event.event_type === "task-done") {
240
- this._logger.info(`Task ${event.task_id} completed.`);
241
- if (this._onTaskCompleteGeneric !== undefined) {
242
- await this._onTaskCompleteGeneric(call, event.task_id);
243
- } else if (event.task_id in this._onTaskCompleteHandlers) {
244
- await this._onTaskCompleteHandlers[event.task_id](call);
285
+ const errors: string[] = [];
286
+ for (const fieldKey of call._fieldKeysByTaskId[event.task_id] ?? []) {
287
+ const handler = this._onValidateHandlers[fieldKey];
288
+ if (handler) {
289
+ const result = await handler(call, await call.getField(fieldKey));
290
+ if (result !== true) {
291
+ const [, error] = result;
292
+ errors.push(error);
293
+ }
294
+ }
295
+ }
296
+
297
+ if (errors.length > 0) {
298
+ await call.retryTask(errors.join(" "));
245
299
  } else {
246
- this._logger.warn(`No handler registered for completion of task '${event.task_id}'`);
300
+ this._logger.info(`Task ${event.task_id} completed.`);
301
+ if (this._onTaskCompleteGeneric !== undefined) {
302
+ await this._onTaskCompleteGeneric(call, event.task_id);
303
+ } else if (event.task_id in this._onTaskCompleteHandlers) {
304
+ await this._onTaskCompleteHandlers[event.task_id](call);
305
+ } else {
306
+ this._logger.warn(`No handler registered for completion of task '${event.task_id}'`);
307
+ }
247
308
  }
248
309
  } else if (event.event_type === "agent-question") {
249
310
  if (this._onQuestion !== undefined) {
@@ -291,15 +352,19 @@ export class Agent {
291
352
  }
292
353
  } else if (event.event_type === "action-request") {
293
354
  this._logger.info(`Received action request ${event.intent_id}: ${event.intent_summary}`);
294
- let suggestion: SuggestedAction | undefined;
355
+ let suggestions: SuggestedAction[] = [];
295
356
  if (this._onActionRequested !== undefined) {
296
- suggestion = await this._onActionRequested(call, event.intent_summary);
357
+ const result = await this._onActionRequested(call, event.intent_summary);
358
+ if (Array.isArray(result)) {
359
+ suggestions = result;
360
+ } else if (result !== null && result !== undefined) {
361
+ suggestions = [result];
362
+ }
297
363
  }
298
364
  await call.sendCommand(ActionSuggestionCommand, {
299
365
  command_type: "action-suggestion",
300
366
  intent_id: event.intent_id,
301
- action_key: suggestion?.key ?? null,
302
- action_description: suggestion?.description ?? "",
367
+ actions: suggestions.map((s) => ({ key: s.key, description: s.description ?? "" })),
303
368
  });
304
369
  } else if (event.event_type === "execute-action") {
305
370
  this._logger.info(`Executing action '${event.action_key}'`);
@@ -327,6 +392,18 @@ export class Agent {
327
392
  if (this._onDtmf !== undefined) {
328
393
  await this._onDtmf(call, event);
329
394
  }
395
+ } else if (event.event_type === "escalate") {
396
+ if (this._onEscalate !== undefined) {
397
+ await this._onEscalate(call, event);
398
+ } else if (event.requested_by === "agent") {
399
+ await call.sendInstruction(
400
+ "No escalation target set. Apologize for not being able to help, ask them to try calling another time, and hang up the call immediately.",
401
+ );
402
+ } else {
403
+ await call.sendInstruction(
404
+ "Let them know there are no representatives available to take their call. Ask them if they would prefer to continue or to call another time.",
405
+ );
406
+ }
330
407
  } else if (event.event_type === "error") {
331
408
  this._logger.error(`The Guava agent reported an error: ${event.content}`);
332
409
  } else if (event.event_type === "warning") {
@@ -334,18 +411,25 @@ export class Agent {
334
411
  }
335
412
  }
336
413
 
337
- async _initCall(variables: Record<string, any> = {}): Promise<Call> {
338
- const call = new Call(variables);
414
+ async _initCall(
415
+ callId: string,
416
+ callInfo: CallInfo,
417
+ variables: Record<string, any> = {},
418
+ ): Promise<Call> {
419
+ const call = new Call(callId, callInfo, variables);
339
420
  await call.setPersona({
340
421
  agentName: this._name,
341
422
  agentPurpose: this._purpose,
342
423
  organizationName: this._organization,
424
+ voice: this._voice,
343
425
  });
344
426
  await call.sendCommand(RegisteredHooksCommand, {
345
427
  command_type: "registered-hooks",
346
428
  has_on_question: this._onQuestion !== undefined,
347
429
  has_on_intent: false,
348
430
  has_on_action_requested: this._onActionRequested !== undefined,
431
+ has_on_escalate: this._onEscalate !== undefined,
432
+ accept_dtmf_for_numbers: this._acceptDtmfForNumbers,
349
433
  });
350
434
  if (this._onCallStart !== undefined) {
351
435
  await this._onCallStart(call);
@@ -355,11 +439,12 @@ export class Agent {
355
439
 
356
440
  async _attachToCall(
357
441
  callId: string,
442
+ callInfo: CallInfo,
358
443
  initialVariables: Record<string, any> = {},
359
444
  testSession?: TestSession,
360
445
  route: string = "v2/connect-call",
361
446
  ): Promise<void> {
362
- const call = await this._initCall(initialVariables);
447
+ const call = await this._initCall(callId, callInfo, initialVariables);
363
448
 
364
449
  const url = new URL(`${route}/${callId}`, this._client.getWebsocketBase());
365
450
  await using socket = await new GuavaSocket<Command, GuavaEvent | null>(
@@ -393,63 +478,76 @@ export class Agent {
393
478
  }
394
479
  }
395
480
 
396
- async _listenInbound(conn: InboundConnection): Promise<void> {
481
+ async _listenInbound(healthCtx: HealthContext, conn: InboundConnection): Promise<void> {
397
482
  const url = new URL("v2/listen-inbound", this._client.getWebsocketBase());
398
483
  if ("agent_number" in conn) {
399
484
  url.searchParams.set("phone_number", conn.agent_number);
400
- } else {
485
+ } else if ("webrtc_code" in conn) {
401
486
  url.searchParams.set("webrtc_code", conn.webrtc_code);
487
+ } else {
488
+ url.searchParams.set("sip_code", conn.sip_code);
402
489
  }
403
490
 
404
- await using socket = await new GuavaSocket<
405
- ListenInbound.ClientMessage,
406
- ListenInbound.ServerMessage
407
- >(
408
- "listen-inbound",
409
- url.toString(),
410
- this._client,
411
- (msg) => msg as unknown as Record<string, unknown>,
412
- ListenInbound.decodeServerMessage,
413
- ).connect();
414
-
415
491
  try {
416
- for await (const msg of socket) {
417
- switch (msg.message_type) {
418
- case "listen-started":
419
- if ("agent_number" in conn) {
420
- this._logger.info(
421
- "Listening on %s (%d other listeners).",
422
- conn.agent_number,
423
- msg.other_listeners,
424
- );
425
- } else {
426
- this._logger.info(
427
- "Listening on WebRTC code %s (%d other listeners).",
428
- conn.webrtc_code,
429
- msg.other_listeners,
430
- );
431
- const debugUrl = new URL(
432
- `debug-webrtc?webrtc_code=${conn.webrtc_code}`,
433
- this._client.getHttpBase(),
434
- );
435
- this._logger.info("Call your agent at: %s", debugUrl);
492
+ await using socket = await new GuavaSocket<
493
+ ListenInbound.ClientMessage,
494
+ ListenInbound.ServerMessage
495
+ >(
496
+ "listen-inbound",
497
+ url.toString(),
498
+ this._client,
499
+ (msg) => msg as unknown as Record<string, unknown>,
500
+ ListenInbound.decodeServerMessage,
501
+ ).connect();
502
+
503
+ try {
504
+ for await (const msg of socket) {
505
+ switch (msg.message_type) {
506
+ case "listen-started":
507
+ healthCtx.ready();
508
+ if ("agent_number" in conn) {
509
+ this._logger.info(
510
+ "Listening on %s (%d other listeners).",
511
+ conn.agent_number,
512
+ msg.other_listeners,
513
+ );
514
+ } else if ("webrtc_code" in conn) {
515
+ this._logger.info(
516
+ "Listening on WebRTC code %s (%d other listeners).",
517
+ conn.webrtc_code,
518
+ msg.other_listeners,
519
+ );
520
+ const debugUrl = new URL(
521
+ `debug-webrtc?webrtc_code=${conn.webrtc_code}`,
522
+ this._client.getHttpBase(),
523
+ );
524
+ this._logger.info("Call your agent at: %s", debugUrl);
525
+ } else {
526
+ this._logger.info(
527
+ "Listening on SIP code %s (%d other listeners).",
528
+ conn.sip_code,
529
+ msg.other_listeners,
530
+ );
531
+ }
532
+ break;
533
+ case "incoming-call":
534
+ socket.send({ message_type: "claim-call", call_id: msg.call_id });
535
+ break;
536
+ case "assign-call": {
537
+ const { call_id, call_info } = msg;
538
+ this._logger.info("Received call (session ID: %s)", call_id);
539
+ this._handleAssignedCall(call_id, call_info, socket).catch((err) => {
540
+ this._logger.error("Error handling assigned call %s: %s", call_id, err);
541
+ });
542
+ break;
436
543
  }
437
- break;
438
- case "incoming-call":
439
- socket.send({ message_type: "claim-call", call_id: msg.call_id });
440
- break;
441
- case "assign-call": {
442
- const { call_id, call_info } = msg;
443
- this._logger.info("Received call (session ID: %s)", call_id);
444
- this._handleAssignedCall(call_id, call_info, socket).catch((err) => {
445
- this._logger.error("Error handling assigned call %s: %s", call_id, err);
446
- });
447
- break;
448
544
  }
449
545
  }
546
+ } catch (e) {
547
+ if (!(e instanceof GuavaSocketClosedError)) throw e;
450
548
  }
451
- } catch (e) {
452
- if (!(e instanceof GuavaSocketClosedError)) throw e;
549
+ } finally {
550
+ healthCtx.stopped();
453
551
  }
454
552
  }
455
553
 
@@ -466,7 +564,7 @@ export class Agent {
466
564
  } else {
467
565
  this._logger.info("Accepting call %s.", callId);
468
566
  socket.send({ message_type: "answer-call", call_id: callId });
469
- await this._attachToCall(callId, initialVariables);
567
+ await this._attachToCall(callId, callInfo, initialVariables);
470
568
  }
471
569
  }
472
570
 
@@ -489,7 +587,13 @@ export class Agent {
489
587
  const { call_id } = (await response.json()) as { call_id: string };
490
588
 
491
589
  this._logger.info("Outbound call created with session ID: %s", call_id);
492
- await this._attachToCall(call_id, variables);
590
+ const callInfo: CallInfo = {
591
+ call_type: "pstn",
592
+ from_number: fromNumber ?? null,
593
+ to_number: toNumber,
594
+ caller_id: null,
595
+ };
596
+ await this._attachToCall(call_id, callInfo, variables);
493
597
  }
494
598
 
495
599
  /**
@@ -529,8 +633,15 @@ export class Agent {
529
633
  const sessionStarted = SessionStarted.parse(JSON.parse(rawFirst));
530
634
  const testSession = new TestSession(ws, this._client);
531
635
 
636
+ const testCallInfo: CallInfo = {
637
+ call_type: "pstn",
638
+ from_number: null,
639
+ to_number: "+15555555555",
640
+ caller_id: null,
641
+ };
532
642
  const attachPromise = this._attachToCall(
533
643
  sessionStarted.session_id,
644
+ testCallInfo,
534
645
  variables,
535
646
  testSession,
536
647
  ).catch((err: unknown) => {
@@ -671,6 +782,8 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
671
782
  name: this._name,
672
783
  organization: this._organization,
673
784
  purpose: this._purpose,
785
+ voice: this._voice,
786
+ acceptDtmf: this._acceptDtmfForNumbers,
674
787
  });
675
788
  cloned._onCallReceived = this._onCallReceived;
676
789
  cloned._onCallStart = this._onCallStart;
@@ -680,66 +793,85 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
680
793
  cloned._onTaskCompleteGeneric = this._onTaskCompleteGeneric;
681
794
  cloned._onTaskCompleteHandlers = { ...this._onTaskCompleteHandlers };
682
795
  cloned._searchQueryHandlers = { ...this._searchQueryHandlers };
796
+ cloned._onValidateHandlers = { ...this._onValidateHandlers };
683
797
  cloned._onActionRequested = this._onActionRequested;
684
798
  cloned._onActionGeneric = this._onActionGeneric;
685
799
  cloned._onActionHandlers = { ...this._onActionHandlers };
686
800
  cloned._onSessionEnd = this._onSessionEnd;
801
+ cloned._onEscalate = this._onEscalate;
687
802
  cloned._onDtmf = this._onDtmf;
688
803
  return cloned;
689
804
  }
690
805
 
691
- private async _serveCampaign(campaignCode: string): Promise<void> {
692
- const campaignUrl = new URL(`v1/campaigns/${campaignCode}`, this._client.getHttpBase());
693
- const campaignResponse = await fetchOrThrow(campaignUrl, {
694
- headers: await this._client.headers(),
695
- });
696
- const campaign = (await campaignResponse.json()) as { id: string; name: string };
806
+ private async _serveCampaign(healthCtx: HealthContext, campaignCode: string): Promise<void> {
807
+ try {
808
+ const campaignUrl = new URL(`v1/campaigns/${campaignCode}`, this._client.getHttpBase());
809
+ const campaignResponse = await fetchOrThrow(campaignUrl, {
810
+ headers: await this._client.headers(),
811
+ });
812
+ const campaign = (await campaignResponse.json()) as { id: string; name: string };
697
813
 
698
- const wsUrl = new URL(`v1/serve-campaign/${campaign.id}`, this._client.getWebsocketBase());
699
- this._logger.info("Connecting to campaign '%s' (id: %s).", campaign.name, campaign.id);
814
+ const wsUrl = new URL(`v1/serve-campaign/${campaign.id}`, this._client.getWebsocketBase());
815
+ this._logger.info("Connecting to campaign '%s' (id: %s).", campaign.name, campaign.id);
700
816
 
701
- await using socket = await new GuavaSocket<DialerClientMessage, DialerServerMessage>(
702
- "serve-campaign",
703
- wsUrl.toString(),
704
- this._client,
705
- (msg) => msg as unknown as Record<string, unknown>,
706
- decodeDialerServerMessage,
707
- ).connect();
817
+ await using socket = await new GuavaSocket<DialerClientMessage, DialerServerMessage>(
818
+ "serve-campaign",
819
+ wsUrl.toString(),
820
+ this._client,
821
+ (msg) => msg as unknown as Record<string, unknown>,
822
+ decodeDialerServerMessage,
823
+ ).connect();
708
824
 
709
- const activeCalls: Promise<void>[] = [];
825
+ const activeCalls: Promise<void>[] = [];
710
826
 
711
- try {
712
- for await (const msg of socket) {
713
- switch (msg.message_type) {
714
- case "listen-started":
715
- this._logger.info("Listening for calls on campaign '%s'. Ready.", campaign.name);
716
- break;
717
- case "initiate-and-assign-call": {
718
- const { call_id, contact_data } = msg;
719
- const data = contact_data as Record<string, unknown> | null;
720
- const logPhone = (data?.phone_number as string | undefined) ?? "?";
721
- this._logger.info(
722
- "Ready to make call, id %s — initiating call setup and dispatch for contact %s.",
723
- call_id,
724
- logPhone,
725
- );
726
- activeCalls.push(
727
- (async () => {
728
- socket.send({ message_type: "controller-ready", call_id });
729
- const variables = (data?.data as Record<string, unknown>) ?? {};
730
- await this._attachToCall(call_id, variables, undefined, "v2/connect-campaign-call");
731
- })(),
732
- );
733
- break;
827
+ try {
828
+ for await (const msg of socket) {
829
+ switch (msg.message_type) {
830
+ case "listen-started":
831
+ this._logger.info("Listening for calls on campaign '%s'. Ready.", campaign.name);
832
+ healthCtx.ready();
833
+ break;
834
+ case "initiate-and-assign-call": {
835
+ const { call_id, contact_data } = msg;
836
+ const data = contact_data as Record<string, unknown> | null;
837
+ const logPhone = (data?.phone_number as string | undefined) ?? "?";
838
+ this._logger.info(
839
+ "Ready to make call, id %s — initiating call setup and dispatch for contact %s.",
840
+ call_id,
841
+ logPhone,
842
+ );
843
+ activeCalls.push(
844
+ (async () => {
845
+ socket.send({ message_type: "controller-ready", call_id });
846
+ const variables = (data?.data as Record<string, unknown>) ?? {};
847
+ const campaignCallInfo: CallInfo = {
848
+ call_type: "pstn",
849
+ from_number: null,
850
+ to_number: (data?.phone_number as string) ?? "",
851
+ caller_id: null,
852
+ };
853
+ await this._attachToCall(
854
+ call_id,
855
+ campaignCallInfo,
856
+ variables,
857
+ undefined,
858
+ "v2/connect-campaign-call",
859
+ );
860
+ })(),
861
+ );
862
+ break;
863
+ }
734
864
  }
735
865
  }
866
+ } catch (e) {
867
+ if (!(e instanceof GuavaSocketClosedError)) throw e;
868
+ this._logger.info("Campaign '%s' disconnected.", campaign.name);
736
869
  }
737
- } catch (e) {
738
- if (!(e instanceof GuavaSocketClosedError)) throw e;
739
- this._logger.info("Campaign '%s' disconnected.", campaign.name);
740
- }
741
870
 
742
- await Promise.all(activeCalls);
871
+ await Promise.all(activeCalls);
872
+ } finally {
873
+ healthCtx.stopped();
874
+ }
743
875
  }
744
876
 
745
877
  /**
@@ -754,7 +886,9 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
754
886
  * await agent.attachCampaign("gcmp-abc123");
755
887
  */
756
888
  async attachCampaign(campaignCode: string): Promise<void> {
757
- return this._serveCampaign(campaignCode);
889
+ const healthCtx = new HealthContext();
890
+ await using _server = await getHealthServer(healthCtx);
891
+ return this._serveCampaign(healthCtx, campaignCode);
758
892
  }
759
893
 
760
894
  /* ===== Aliases to be removed at some point. ===== */