@guava-ai/guava-sdk 0.19.0 → 0.21.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 (45) hide show
  1. package/bin/example-runner.ts +1 -0
  2. package/dist/bin/example-runner.d.ts +1 -0
  3. package/dist/bin/example-runner.js +1 -0
  4. package/dist/bin/example-runner.js.map +1 -1
  5. package/dist/examples/polling-campaign.d.ts +1 -0
  6. package/dist/examples/polling-campaign.js +106 -0
  7. package/dist/examples/polling-campaign.js.map +1 -0
  8. package/dist/src/action-item.d.ts +8 -4
  9. package/dist/src/action-item.js +5 -2
  10. package/dist/src/action-item.js.map +1 -1
  11. package/dist/src/agent.d.ts +19 -4
  12. package/dist/src/agent.js +79 -6
  13. package/dist/src/agent.js.map +1 -1
  14. package/dist/src/client.d.ts +34 -1
  15. package/dist/src/client.js +66 -2
  16. package/dist/src/client.js.map +1 -1
  17. package/dist/src/commands.d.ts +6 -3
  18. package/dist/src/events.d.ts +65 -0
  19. package/dist/src/events.js +6 -1
  20. package/dist/src/events.js.map +1 -1
  21. package/dist/src/guavadialer-events.d.ts +49 -0
  22. package/dist/src/guavadialer-events.js +76 -0
  23. package/dist/src/guavadialer-events.js.map +1 -0
  24. package/dist/src/index.d.ts +2 -0
  25. package/dist/src/index.js.map +1 -1
  26. package/dist/src/sms.d.ts +19 -0
  27. package/dist/src/sms.js +52 -0
  28. package/dist/src/sms.js.map +1 -0
  29. package/dist/src/utils.d.ts +1 -0
  30. package/dist/src/utils.js +4 -0
  31. package/dist/src/utils.js.map +1 -1
  32. package/dist/src/version.d.ts +1 -1
  33. package/dist/src/version.js +1 -1
  34. package/examples/README.md +6 -4
  35. package/examples/polling-campaign.ts +79 -0
  36. package/package.json +2 -2
  37. package/src/action-item.ts +7 -3
  38. package/src/agent.ts +92 -7
  39. package/src/client.ts +87 -3
  40. package/src/events.ts +28 -0
  41. package/src/guavadialer-events.ts +51 -0
  42. package/src/index.ts +2 -0
  43. package/src/sms.ts +17 -0
  44. package/src/utils.ts +4 -0
  45. package/src/version.ts +1 -1
@@ -0,0 +1,79 @@
1
+ /**
2
+ * This example attaches a political polling agent to an ongoing Guava campaign.
3
+ *
4
+ * To use this, first create a campaign from the dashboard or CLI and add contacts.
5
+ * Then, run this script with the campaign code and the Agent will start making calls
6
+ * to those registered contacts.
7
+ *
8
+ * Usage: guava-example polling-campaign <campaign-code>
9
+ */
10
+ import * as guava from "@guava-ai/guava-sdk";
11
+
12
+ const agent = new guava.Agent({
13
+ name: "Jordan",
14
+ organization: "Harper Valley Research Center",
15
+ purpose: "Conduct a non-partisan political opinion poll",
16
+ });
17
+
18
+ agent.onCallStart(async (call: guava.Call) => {
19
+ const firstName = await call.getVariable("first_name");
20
+ await call.reachPerson(firstName, {
21
+ greeting:
22
+ `Hi, is this ${firstName}? I'm calling from the Harper Valley Research Center. ` +
23
+ "We're conducting a brief, non-partisan poll about issues affecting your State.",
24
+ });
25
+ });
26
+
27
+ agent.onReachPerson(async (call: guava.Call, outcome: string) => {
28
+ if (outcome === "available") {
29
+ const firstName = await call.getVariable("first_name");
30
+ await call.setTask({
31
+ taskId: "political_poll",
32
+ objective:
33
+ `Conduct a brief political opinion poll with ${firstName}. ` +
34
+ "Be polite, non-partisan, and respect their time.",
35
+ checklist: [
36
+ guava.Field({
37
+ key: "top_issue",
38
+ description: "The most important issue to the respondent right now",
39
+ fieldType: "text",
40
+ question: "What would you say is the most important issue facing your state right now?",
41
+ }),
42
+ guava.Field({
43
+ key: "governor_approval",
44
+ description: "Approval rating of the current governor",
45
+ fieldType: "multiple_choice",
46
+ question: "Do you approve or disapprove of the job the current governor is doing?",
47
+ choices: ["approve", "disapprove", "no_opinion"],
48
+ }),
49
+ guava.Field({
50
+ key: "likely_to_vote",
51
+ description: "How likely the respondent is to vote in the next election",
52
+ fieldType: "multiple_choice",
53
+ question: "How likely are you to vote in the upcoming election?",
54
+ choices: ["very_likely", "likely", "unlikely", "very_unlikely"],
55
+ }),
56
+ ],
57
+ });
58
+ } else {
59
+ await call.hangup("Appropriately end the call.");
60
+ }
61
+ });
62
+
63
+ agent.onTaskComplete("political_poll", async (call) => {
64
+ // Here is where you would read the poll results using call.getField(...)
65
+ await call.hangup(
66
+ "Thank them for participating and let them know the results will be published next month.",
67
+ );
68
+ });
69
+
70
+ export async function run(args: string[]) {
71
+ const [campaignCode] = args;
72
+
73
+ if (!campaignCode) {
74
+ console.error("Usage: guava-example polling-campaign <campaign-code>");
75
+ process.exit(1);
76
+ }
77
+
78
+ await agent.attachCampaign(campaignCode);
79
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@guava-ai/guava-sdk",
3
3
  "description": "The official TypeScript SDK for building Guava voice agents.",
4
4
  "homepage": "https://docs.goguava.ai/typescript-sdk/",
5
- "version": "0.19.0",
5
+ "version": "0.21.0",
6
6
  "type": "commonjs",
7
7
  "main": "dist/src/index.js",
8
8
  "types": "dist/src/index.d.ts",
@@ -50,7 +50,7 @@
50
50
  "check-format": "biome format ./src ./examples",
51
51
  "format": "biome format --write ./src ./examples",
52
52
  "test": "jest",
53
- "check": "npm run typecheck && npm audit && npm run lint && npm run check-format",
53
+ "check": "npm run typecheck && npm audit --omit=dev && npm run lint && npm run check-format",
54
54
  "clean": "rm -rf ./dist",
55
55
  "build": "tsx scripts/update-version.ts && tsc && chmod +x ./dist/bin/example-runner.js",
56
56
  "prepare": "npm run clean && npm run build",
@@ -13,7 +13,8 @@ export const FieldItem = z
13
13
  .object({
14
14
  item_type: z.literal("field"),
15
15
  key: z.string(),
16
- description: z.string(),
16
+ description: z.string().default(""),
17
+ question: z.string().default(""),
17
18
  field_type: FieldItemType,
18
19
  required: z.boolean().default(true),
19
20
  choices: z.array(z.string()).default([]),
@@ -34,7 +35,8 @@ export type FieldItem = z.input<typeof FieldItem>;
34
35
  export const SerializableFieldItem = z.object({
35
36
  item_type: z.literal("field"),
36
37
  key: z.string(),
37
- description: z.string(),
38
+ description: z.string().default(""),
39
+ question: z.string().default(""),
38
40
  field_type: FieldItemType,
39
41
  required: z.boolean().default(true),
40
42
  choices: z.array(z.string()).default([]),
@@ -61,7 +63,8 @@ export type ActionItem = z.input<typeof ActionItem>;
61
63
 
62
64
  export function Field(options: {
63
65
  key: string;
64
- description: string;
66
+ description?: string;
67
+ question?: string;
65
68
  fieldType: FieldItemType;
66
69
  required?: boolean;
67
70
  choices?: string[];
@@ -72,6 +75,7 @@ export function Field(options: {
72
75
  item_type: "field",
73
76
  key: options.key,
74
77
  description: options.description,
78
+ question: options.question,
75
79
  field_type: options.fieldType,
76
80
  required: options.required,
77
81
  choices: options.choices,
package/src/agent.ts CHANGED
@@ -14,10 +14,17 @@ import {
14
14
  type GuavaEvent,
15
15
  type CallerSpeechEvent,
16
16
  type AgentSpeechEvent,
17
+ type BotSessionEnded,
18
+ type DTMFPressedEvent,
17
19
  decodeEventDict,
18
20
  } from "./events.ts";
19
21
  import { telemetryClient } from "./telemetry.ts";
20
22
  import { GuavaSocket, GuavaSocketClosedError } from "./socket/client.ts";
23
+ import {
24
+ type ClientMessage as DialerClientMessage,
25
+ type ServerMessage as DialerServerMessage,
26
+ decodeServerMessage as decodeDialerServerMessage,
27
+ } from "./guavadialer-events.ts";
21
28
  import * as ListenInbound from "./socket/listen-inbound.ts";
22
29
  import type { CallInfo } from "./socket/call-info.ts";
23
30
  import { TestSession } from "./testing/session.ts";
@@ -64,7 +71,8 @@ export class Agent {
64
71
  ) => Promise<SuggestedAction | undefined>;
65
72
  private _onActionGeneric?: (call: Call, actionKey: string) => Promise<void>;
66
73
  private _onActionHandlers: Record<string, (call: Call) => Promise<void>> = {};
67
- private _onSessionEnd?: (call: Call) => Promise<void>;
74
+ private _onSessionEnd?: (call: Call, event: BotSessionEnded) => Promise<void>;
75
+ private _onDtmf?: (call: Call, event: DTMFPressedEvent) => Promise<void>;
68
76
 
69
77
  constructor(args?: { name?: string; organization?: string; purpose?: string }) {
70
78
  this._name = args?.name;
@@ -138,10 +146,14 @@ export class Agent {
138
146
  }
139
147
  }
140
148
 
141
- onSessionEnd(callback: (call: Call) => Promise<void>): void {
149
+ onSessionEnd(callback: (call: Call, event: BotSessionEnded) => Promise<void>): void {
142
150
  this._onSessionEnd = callback;
143
151
  }
144
152
 
153
+ onDtmf(callback: (call: Call, event: DTMFPressedEvent) => Promise<void>): void {
154
+ this._onDtmf = callback;
155
+ }
156
+
145
157
  get handlers() {
146
158
  return {
147
159
  onCallReceived: (callInfo: CallInfo) => this._onCallReceived(callInfo),
@@ -181,9 +193,9 @@ export class Agent {
181
193
  if (actionKey in this._onActionHandlers) return this._onActionHandlers[actionKey](call);
182
194
  throw new Error(`No onAction handler registered for action '${actionKey}'.`);
183
195
  },
184
- onSessionEnd: (call: Call) => {
196
+ onSessionEnd: (call: Call, event: BotSessionEnded) => {
185
197
  if (!this._onSessionEnd) throw new Error("No onSessionEnd handler registered.");
186
- return this._onSessionEnd(call);
198
+ return this._onSessionEnd(call, event);
187
199
  },
188
200
  };
189
201
  }
@@ -310,8 +322,10 @@ export class Agent {
310
322
  if (testSession) {
311
323
  testSession.terminationReason = event.termination_reason;
312
324
  }
313
- if (this._onSessionEnd !== undefined) {
314
- await this._onSessionEnd(call);
325
+ await this._onSessionEnd?.(call, event);
326
+ } else if (event.event_type === "dtmf") {
327
+ if (this._onDtmf !== undefined) {
328
+ await this._onDtmf(call, event);
315
329
  }
316
330
  } else if (event.event_type === "error") {
317
331
  this._logger.error(`The Guava agent reported an error: ${event.content}`);
@@ -343,10 +357,11 @@ export class Agent {
343
357
  callId: string,
344
358
  initialVariables: Record<string, any> = {},
345
359
  testSession?: TestSession,
360
+ route: string = "v2/connect-call",
346
361
  ): Promise<void> {
347
362
  const call = await this._initCall(initialVariables);
348
363
 
349
- const url = new URL(`v2/connect-call/${callId}`, this._client.getWebsocketBase());
364
+ const url = new URL(`${route}/${callId}`, this._client.getWebsocketBase());
350
365
  await using socket = await new GuavaSocket<Command, GuavaEvent | null>(
351
366
  `call-connection-${callId}`,
352
367
  url.toString(),
@@ -661,9 +676,79 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
661
676
  cloned._onActionGeneric = this._onActionGeneric;
662
677
  cloned._onActionHandlers = { ...this._onActionHandlers };
663
678
  cloned._onSessionEnd = this._onSessionEnd;
679
+ cloned._onDtmf = this._onDtmf;
664
680
  return cloned;
665
681
  }
666
682
 
683
+ private async _serveCampaign(campaignCode: string): Promise<void> {
684
+ const campaignUrl = new URL(`v1/campaigns/${campaignCode}`, this._client.getHttpBase());
685
+ const campaignResponse = await fetchOrThrow(campaignUrl, {
686
+ headers: await this._client.headers(),
687
+ });
688
+ const campaign = (await campaignResponse.json()) as { id: string; name: string };
689
+
690
+ const wsUrl = new URL(`v1/serve-campaign/${campaign.id}`, this._client.getWebsocketBase());
691
+ this._logger.info("Connecting to campaign '%s' (id: %s).", campaign.name, campaign.id);
692
+
693
+ await using socket = await new GuavaSocket<DialerClientMessage, DialerServerMessage>(
694
+ "serve-campaign",
695
+ wsUrl.toString(),
696
+ this._client,
697
+ (msg) => msg as unknown as Record<string, unknown>,
698
+ decodeDialerServerMessage,
699
+ ).connect();
700
+
701
+ const activeCalls: Promise<void>[] = [];
702
+
703
+ try {
704
+ for await (const msg of socket) {
705
+ switch (msg.message_type) {
706
+ case "listen-started":
707
+ this._logger.info("Listening for calls on campaign '%s'. Ready.", campaign.name);
708
+ break;
709
+ case "initiate-and-assign-call": {
710
+ const { call_id, contact_data } = msg;
711
+ const data = contact_data as Record<string, unknown> | null;
712
+ const logPhone = (data?.phone_number as string | undefined) ?? "?";
713
+ this._logger.info(
714
+ "Ready to make call, id %s — initiating call setup and dispatch for contact %s.",
715
+ call_id,
716
+ logPhone,
717
+ );
718
+ activeCalls.push(
719
+ (async () => {
720
+ socket.send({ message_type: "controller-ready", call_id });
721
+ const variables = (data?.data as Record<string, unknown>) ?? {};
722
+ await this._attachToCall(call_id, variables, undefined, "v2/connect-campaign-call");
723
+ })(),
724
+ );
725
+ break;
726
+ }
727
+ }
728
+ }
729
+ } catch (e) {
730
+ if (!(e instanceof GuavaSocketClosedError)) throw e;
731
+ this._logger.info("Campaign '%s' disconnected.", campaign.name);
732
+ }
733
+
734
+ await Promise.all(activeCalls);
735
+ }
736
+
737
+ /**
738
+ * Attach this agent to an active Guava campaign and handle outbound calls.
739
+ *
740
+ * Blocks until the campaign connection is closed.
741
+ *
742
+ * @param campaignCode - The campaign code (e.g. `gcmp-...`). Create a campaign
743
+ * and upload contacts via the Guava dashboard or CLI before calling this.
744
+ *
745
+ * @example
746
+ * await agent.attachCampaign("gcmp-abc123");
747
+ */
748
+ async attachCampaign(campaignCode: string): Promise<void> {
749
+ return this._serveCampaign(campaignCode);
750
+ }
751
+
667
752
  /* ===== Aliases to be removed at some point. ===== */
668
753
  /** @deprecated Use {@link listenPhone} instead. */
669
754
  async inboundPhone(phoneNumber: string): Promise<void> {
package/src/client.ts CHANGED
@@ -10,7 +10,8 @@ import { ErrorEvent, SessionStartedEvent, decodeEvent, InboundTunnelEvent } from
10
10
  import { SDK_VERSION } from "./version.ts";
11
11
  import os from "node:os";
12
12
  import * as fs from "node:fs";
13
- import { getBaseUrl, fetchOrThrow } from "./utils.ts";
13
+ import { getBaseUrl, fetchOrThrow, sleep } from "./utils.ts";
14
+ import { SmsMessage } from "./sms.ts";
14
15
  import { telemetryClient } from "./telemetry.ts";
15
16
  import type { CallController } from "./call-controller.ts";
16
17
  import {
@@ -24,6 +25,14 @@ import {
24
25
 
25
26
  const SDK_NAME = "typescript-sdk";
26
27
 
28
+ export interface ClientOptions {
29
+ apiKey?: string;
30
+ baseUrl?: string;
31
+ logger?: Logger;
32
+ captureWarnings?: boolean;
33
+ checkDeprecation?: boolean;
34
+ }
35
+
27
36
  let firstClient = false;
28
37
 
29
38
  function stringifyZod<Schema extends z.ZodType>(schema: Schema, data: z.input<Schema>): string {
@@ -44,7 +53,13 @@ export class Client {
44
53
  private _controller?: CallController;
45
54
  private messageHandler?: (_: WebSocket.MessageEvent) => void;
46
55
 
47
- constructor(apiKey?: string, baseUrl?: string, logger?: Logger, captureWarnings: boolean = true) {
56
+ constructor({
57
+ apiKey,
58
+ baseUrl,
59
+ logger,
60
+ captureWarnings = true,
61
+ checkDeprecation = true,
62
+ }: ClientOptions = {}) {
48
63
  // Set up the default logger.
49
64
  if (logger) {
50
65
  this._logger = logger;
@@ -84,7 +99,9 @@ export class Client {
84
99
  }
85
100
 
86
101
  telemetryClient.setSdkClient(this);
87
- this._checkSdkDeprecation();
102
+ if (checkDeprecation) {
103
+ this._checkSdkDeprecation();
104
+ }
88
105
  }
89
106
  }
90
107
 
@@ -155,6 +172,73 @@ export class Client {
155
172
  return body.webrtc_code;
156
173
  }
157
174
 
175
+ /**
176
+ * Sends an SMS message from one of your Guava numbers.
177
+ * @param fromNumber - One of your Guava numbers (E.164). Must have SMS configured.
178
+ * @param toNumber - The recipient's number (E.164).
179
+ * @param message - The message body to send.
180
+ */
181
+ async sendSms(fromNumber: string, toNumber: string, message: string): Promise<void> {
182
+ const url = new URL("v1/send-sms", this.getHttpBase());
183
+ await fetchOrThrow(url, {
184
+ method: "POST",
185
+ headers: { ...(await this.headers()), "Content-Type": "application/json" },
186
+ body: JSON.stringify({
187
+ from_number: fromNumber,
188
+ to_number: toNumber,
189
+ message,
190
+ }),
191
+ });
192
+ }
193
+
194
+ /**
195
+ * Waits for and returns the next inbound SMS sent from `fromNumber` to `toNumber`.
196
+ *
197
+ * Polls the inbox for messages received after this call begins, resolving once one
198
+ * arrives or `timeoutMs` elapses. Note the direction: `fromNumber` is the external
199
+ * number you're waiting to hear from, and `toNumber` is your Guava number — the
200
+ * opposite of {@link sendSms}.
201
+ *
202
+ * @param fromNumber - The external number to wait for a message from (E.164).
203
+ * @param toNumber - Your Guava number that will receive the message (E.164).
204
+ * @param options.timeoutMs - Max time to wait before giving up. Defaults to 60000.
205
+ * @param options.pollIntervalMs - Time between inbox checks. Defaults to 2000.
206
+ * @returns The message, or `null` if `timeoutMs` elapses with no new message.
207
+ */
208
+ async nextSms(
209
+ fromNumber: string,
210
+ toNumber: string,
211
+ options?: { timeoutMs?: number; pollIntervalMs?: number },
212
+ ): Promise<SmsMessage | null> {
213
+ const timeoutMs = options?.timeoutMs ?? 60_000;
214
+ const pollIntervalMs = options?.pollIntervalMs ?? 2_000;
215
+ const start = new Date().toISOString();
216
+ const deadline = Date.now() + timeoutMs;
217
+ while (true) {
218
+ const url = new URL("v1/messages", this.getHttpBase());
219
+ url.searchParams.set("to_number", toNumber);
220
+ url.searchParams.set("from_number", fromNumber);
221
+ url.searchParams.set("modality", "sms");
222
+ url.searchParams.set("start", start);
223
+ const response = await fetchOrThrow(url, {
224
+ method: "GET",
225
+ headers: await this.headers(),
226
+ });
227
+ // The endpoint returns matches oldest-first, so the earliest message after
228
+ // `start` is always the first element — we only need one, so `has_more`
229
+ // (which signals additional *later* messages) is irrelevant here.
230
+ const body = (await response.json()) as { messages: unknown[] };
231
+ if (body.messages?.length) {
232
+ return SmsMessage.parse(body.messages[0]);
233
+ }
234
+ const remaining = deadline - Date.now();
235
+ if (remaining <= 0) {
236
+ return null;
237
+ }
238
+ await sleep(Math.min(pollIntervalMs, remaining));
239
+ }
240
+ }
241
+
158
242
  /**
159
243
  * @description use the Guava API to call out to a number
160
244
  */
package/src/events.ts CHANGED
@@ -99,6 +99,9 @@ export const BotSessionEnded = z.object({
99
99
  });
100
100
  export type BotSessionEnded = z.infer<typeof BotSessionEnded>;
101
101
 
102
+ /** Why a bot session ended. */
103
+ export type TerminationReason = BotSessionEnded["termination_reason"];
104
+
102
105
  export const ChoiceQueryEvent = z.object({
103
106
  event_type: z.literal("choice-query"),
104
107
  field_key: z.string(),
@@ -120,6 +123,30 @@ export const ExecuteActionEvent = z.object({
120
123
  });
121
124
  export type ExecuteActionEvent = z.infer<typeof ExecuteActionEvent>;
122
125
 
126
+ export type DTMFDigit =
127
+ | "0"
128
+ | "1"
129
+ | "2"
130
+ | "3"
131
+ | "4"
132
+ | "5"
133
+ | "6"
134
+ | "7"
135
+ | "8"
136
+ | "9"
137
+ | "*"
138
+ | "#"
139
+ | "A"
140
+ | "B"
141
+ | "C"
142
+ | "D";
143
+
144
+ export const DTMFPressedEvent = z.object({
145
+ event_type: z.literal("dtmf"),
146
+ digit: z.enum(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#", "A", "B", "C", "D"]),
147
+ });
148
+ export type DTMFPressedEvent = z.infer<typeof DTMFPressedEvent>;
149
+
123
150
  export const GuavaEvent = z.discriminatedUnion("event_type", [
124
151
  SessionStartedEvent,
125
152
  InboundCallEvent,
@@ -137,6 +164,7 @@ export const GuavaEvent = z.discriminatedUnion("event_type", [
137
164
  ChoiceQueryEvent,
138
165
  ActionRequestEvent,
139
166
  ExecuteActionEvent,
167
+ DTMFPressedEvent,
140
168
  ]);
141
169
  export type GuavaEvent = z.infer<typeof GuavaEvent>;
142
170
 
@@ -0,0 +1,51 @@
1
+ import * as z from "zod";
2
+
3
+ export const ListenStarted = z.object({
4
+ message_type: z.literal("listen-started"),
5
+ other_listeners: z.number().int(),
6
+ });
7
+ export type ListenStarted = z.infer<typeof ListenStarted>;
8
+
9
+ /**
10
+ * @description Sent from the server when it wants to start a call and has assigned it to the appropriate pod.
11
+ */
12
+ export const InitiateAndAssignCall = z.object({
13
+ message_type: z.literal("initiate-and-assign-call"),
14
+ call_id: z.string(),
15
+ contact_data: z.unknown(),
16
+ });
17
+ export type InitiateAndAssignCall = z.infer<typeof InitiateAndAssignCall>;
18
+
19
+ /**
20
+ * @description Sent from the client when it has initiated a call controller and is ready to connect to the call.
21
+ */
22
+ export const ControllerReady = z.object({
23
+ message_type: z.literal("controller-ready"),
24
+ call_id: z.string(),
25
+ });
26
+ export type ControllerReady = z.infer<typeof ControllerReady>;
27
+
28
+ /**
29
+ * @description Sent from the client when the controller failed to initialize (e.g. timeout). The server should release any resources held for this call.
30
+ */
31
+ export const InitControllerFailed = z.object({
32
+ message_type: z.literal("init-controller-failed"),
33
+ call_id: z.string(),
34
+ });
35
+ export type InitControllerFailed = z.infer<typeof InitControllerFailed>;
36
+
37
+ export const ServerMessage = z.discriminatedUnion("message_type", [
38
+ ListenStarted,
39
+ InitiateAndAssignCall,
40
+ ]);
41
+ export type ServerMessage = z.infer<typeof ServerMessage>;
42
+
43
+ export const ClientMessage = z.discriminatedUnion("message_type", [
44
+ ControllerReady,
45
+ InitControllerFailed,
46
+ ]);
47
+ export type ClientMessage = z.infer<typeof ClientMessage>;
48
+
49
+ export function decodeServerMessage(payload: Record<string, unknown>): ServerMessage {
50
+ return ServerMessage.parse(payload);
51
+ }
package/src/index.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  export { Client, type InboundConnection } from "./client.ts";
2
+ export type { SmsMessage } from "./sms.ts";
2
3
  export { CallController, type TaskObjective } from "./call-controller.ts";
3
4
  export { Say, Field } from "./action-item.ts";
4
5
  export { Logger, getConsoleLogger, getDefaultLogger } from "./logging.ts";
5
6
  export { Agent, CallInfo } from "./agent.ts";
6
7
  export { Call } from "./call.ts";
8
+ export type { BotSessionEnded, TerminationReason, DTMFPressedEvent, DTMFDigit } from "./events.ts";
7
9
  export { TestSession } from "./testing/session.ts";
8
10
  export { MockCall } from "./testing/mocks.ts";
package/src/sms.ts ADDED
@@ -0,0 +1,17 @@
1
+ import * as z from "zod";
2
+
3
+ /**
4
+ * An inbound SMS message received on one of your Guava numbers.
5
+ *
6
+ * Field names mirror the wire format returned by `GET /v1/messages`.
7
+ */
8
+ export const SmsMessage = z.object({
9
+ id: z.string(),
10
+ from_number: z.string(),
11
+ to_number: z.string(),
12
+ content: z.string(),
13
+ received_at: z.string(),
14
+ modality: z.literal("sms"),
15
+ direction: z.enum(["inbound", "outbound"]),
16
+ });
17
+ export type SmsMessage = z.infer<typeof SmsMessage>;
package/src/utils.ts CHANGED
@@ -45,6 +45,10 @@ class HttpStatusError extends Error {
45
45
  }
46
46
  }
47
47
 
48
+ export function sleep(ms: number): Promise<void> {
49
+ return new Promise((resolve) => setTimeout(resolve, ms));
50
+ }
51
+
48
52
  export async function fetchOrThrow(
49
53
  input: RequestInfo | URL,
50
54
  init?: RequestInit,
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = "0.19.0";
1
+ export const SDK_VERSION = "0.21.0";