@guava-ai/guava-sdk 0.28.0 → 0.30.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 (38) hide show
  1. package/dist/examples/multiple-agents.js +2 -1
  2. package/dist/examples/multiple-agents.js.map +1 -1
  3. package/dist/examples/property-insurance.d.ts +2 -0
  4. package/dist/examples/property-insurance.js +8 -7
  5. package/dist/examples/property-insurance.js.map +1 -1
  6. package/dist/examples/restaurant-waitlist.d.ts +2 -0
  7. package/dist/examples/restaurant-waitlist.js +10 -9
  8. package/dist/examples/restaurant-waitlist.js.map +1 -1
  9. package/dist/src/agent.d.ts +2 -2
  10. package/dist/src/agent.js +68 -43
  11. package/dist/src/agent.js.map +1 -1
  12. package/dist/src/health.d.ts +4 -1
  13. package/dist/src/health.js +14 -2
  14. package/dist/src/health.js.map +1 -1
  15. package/dist/src/runner.d.ts +10 -1
  16. package/dist/src/runner.js +16 -5
  17. package/dist/src/runner.js.map +1 -1
  18. package/dist/src/socket/client.js +19 -2
  19. package/dist/src/socket/client.js.map +1 -1
  20. package/dist/src/testing/session.d.ts +2 -1
  21. package/dist/src/testing/session.js +3 -1
  22. package/dist/src/testing/session.js.map +1 -1
  23. package/dist/src/utils.d.ts +4 -0
  24. package/dist/src/utils.js +35 -1
  25. package/dist/src/utils.js.map +1 -1
  26. package/dist/src/version.d.ts +1 -1
  27. package/dist/src/version.js +1 -1
  28. package/examples/multiple-agents.ts +2 -1
  29. package/examples/property-insurance.ts +1 -1
  30. package/examples/restaurant-waitlist.ts +1 -1
  31. package/package.json +11 -2
  32. package/src/agent.ts +43 -18
  33. package/src/health.ts +17 -3
  34. package/src/runner.ts +28 -5
  35. package/src/socket/client.ts +31 -13
  36. package/src/testing/session.ts +3 -1
  37. package/src/utils.ts +38 -0
  38. package/src/version.ts +1 -1
package/src/runner.ts CHANGED
@@ -1,13 +1,27 @@
1
1
  import type { Agent } from "./agent.ts";
2
2
  import { MultiHealthContext, getHealthServer } from "./health.ts";
3
+ import { nullDisposable, onSignal } from "./utils.ts";
4
+ import { getDefaultLogger, type Logger } from "./logging.ts";
5
+
6
+ export interface RunOptions {
7
+ /**
8
+ * Install SIGINT/SIGTERM handlers that drain in-flight calls on the first
9
+ * signal; a second signal terminates the process. Defaults to `true`.
10
+ */
11
+ handleSignals?: boolean;
12
+ }
3
13
 
4
14
  export class Runner {
5
15
  private _healthCtx = new MultiHealthContext();
6
16
  private _tasks: Array<() => Promise<void>> = [];
17
+ private _drainController: AbortController = new AbortController();
18
+ private _logger: Logger = getDefaultLogger();
7
19
 
8
20
  listenPhone(agent: Agent, agentNumber: string): this {
9
21
  const ctx = this._healthCtx.createCtx();
10
- this._tasks.push(() => agent._listenInbound(ctx, { agent_number: agentNumber }));
22
+ this._tasks.push(() =>
23
+ agent._listenInbound(ctx, { agent_number: agentNumber }, {}, this._drainController.signal),
24
+ );
11
25
  return this;
12
26
  }
13
27
 
@@ -15,25 +29,34 @@ export class Runner {
15
29
  const ctx = this._healthCtx.createCtx();
16
30
  this._tasks.push(async () => {
17
31
  const code = webrtcCode ?? (await agent._client.createWebrtcAgent(3600));
18
- return agent._listenInbound(ctx, { webrtc_code: code });
32
+ return agent._listenInbound(ctx, { webrtc_code: code }, {}, this._drainController.signal);
19
33
  });
20
34
  return this;
21
35
  }
22
36
 
23
37
  listenSip(agent: Agent, sipCode: string): this {
24
38
  const ctx = this._healthCtx.createCtx();
25
- this._tasks.push(() => agent._listenInbound(ctx, { sip_code: sipCode }));
39
+ this._tasks.push(() =>
40
+ agent._listenInbound(ctx, { sip_code: sipCode }, {}, this._drainController.signal),
41
+ );
26
42
  return this;
27
43
  }
28
44
 
29
45
  attachCampaign(agent: Agent, campaignCode: string): this {
30
46
  const ctx = this._healthCtx.createCtx();
31
- this._tasks.push(() => agent._serveCampaign(ctx, campaignCode));
47
+ this._tasks.push(() => agent._serveCampaign(ctx, campaignCode, this._drainController.signal));
32
48
  return this;
33
49
  }
34
50
 
35
- async run(): Promise<void> {
51
+ async run({ handleSignals = true }: RunOptions = {}): Promise<void> {
36
52
  await using _server = await getHealthServer(this._healthCtx);
53
+ using _ = handleSignals
54
+ ? onSignal((signal) => {
55
+ this._logger.info(`Received ${signal} - draining calls. Send signal again to terminate.`);
56
+ this._drainController.abort();
57
+ })
58
+ : nullDisposable;
37
59
  await Promise.all(this._tasks.map((task) => task()));
60
+ this._logger.info("All tasks done.");
38
61
  }
39
62
  }
@@ -49,6 +49,16 @@ export class GuavaSocketConnectionFailed extends Error {
49
49
  }
50
50
  }
51
51
 
52
+ /** Raised when the server rejects the websocket handshake with an HTTP response. */
53
+ class UnexpectedResponseError extends Error {
54
+ constructor(
55
+ readonly statusCode: number | undefined,
56
+ readonly body: string,
57
+ ) {
58
+ super(`Unexpected server response: ${statusCode ?? "unknown"}. Body: ${body || "<empty>"}`);
59
+ }
60
+ }
61
+
52
62
  const HandshakeResponse = z.union([GuavaOpenAck, GuavaClose]);
53
63
 
54
64
  function _sendMessage(ws: WebSocket, message: GuavaClientMessage): void {
@@ -160,13 +170,7 @@ export class GuavaSocket<SendT, RecvT> {
160
170
  body += chunk.toString();
161
171
  });
162
172
  res.on("end", () => {
163
- settle(() =>
164
- reject(
165
- new Error(
166
- `Unexpected server response: ${res.statusCode}. Body: ${body || "<empty>"}`,
167
- ),
168
- ),
169
- );
173
+ settle(() => reject(new UnexpectedResponseError(res.statusCode, body)));
170
174
  });
171
175
  });
172
176
  socket.once("close", () =>
@@ -225,6 +229,7 @@ export class GuavaSocket<SendT, RecvT> {
225
229
  if (e instanceof GuavaSocketClosedError) throw e;
226
230
 
227
231
  if (attempt >= MAX_RECONNECT_ATTEMPTS) {
232
+ this._logger.error("Couldn't connect to server after %d attempts.", attempt);
228
233
  this._setCloseReason(
229
234
  "reconnection-failed",
230
235
  `Couldn't connect after ${attempt} attempts.`,
@@ -232,12 +237,25 @@ export class GuavaSocket<SendT, RecvT> {
232
237
  throw new GuavaSocketConnectionFailed();
233
238
  }
234
239
 
235
- this._logger.error(
236
- "Failed to connect (attempt %d/%d). Retrying in a few seconds...\n",
237
- attempt,
238
- MAX_RECONNECT_ATTEMPTS,
239
- e,
240
- );
240
+ if (e instanceof UnexpectedResponseError) {
241
+ const interpretation =
242
+ e.statusCode === 401 || e.statusCode === 403 ? "Authentication Failure" : "HTTP error";
243
+ this._logger.error(
244
+ "Failed to connect to websocket endpoint %s due to %s. HTTP %s, Body: %s. Retrying in a few seconds...",
245
+ this._connectionUrl,
246
+ interpretation,
247
+ e.statusCode ?? "unknown",
248
+ e.body || "No response body.",
249
+ );
250
+ } else {
251
+ this._logger.error(
252
+ "Failed to connect to %s (attempt %d/%d). Retrying in a few seconds...\n",
253
+ this._connectionUrl,
254
+ attempt,
255
+ MAX_RECONNECT_ATTEMPTS,
256
+ e,
257
+ );
258
+ }
241
259
  await sleep(reconnectDelay(attempt));
242
260
  }
243
261
  }
@@ -29,15 +29,17 @@ export class TestSession {
29
29
 
30
30
  executedActions: string[] = [];
31
31
  terminationReason: string | null = null;
32
+ readonly id: string;
32
33
 
33
34
  /** @internal */
34
35
  get _events(): ReadonlyArray<InjectASR | BotTTS> {
35
36
  return this._sessionEvents;
36
37
  }
37
38
 
38
- constructor(ws: WebSocket, client: Client) {
39
+ constructor(ws: WebSocket, client: Client, id: string) {
39
40
  this._ws = ws;
40
41
  this._client = client;
42
+ this.id = id;
41
43
 
42
44
  ws.on("message", (data) => {
43
45
  let event: TestingEvent;
package/src/utils.ts CHANGED
@@ -63,3 +63,41 @@ export async function fetchOrThrow(
63
63
 
64
64
  return res;
65
65
  }
66
+
67
+ /** A Disposable whose disposal does nothing. Useful as a conditional no-op. */
68
+ export const nullDisposable: Disposable = { [Symbol.dispose]() {} };
69
+
70
+ export function onAbort(signal: AbortSignal, listener: () => void): Disposable {
71
+ signal.throwIfAborted();
72
+ signal.addEventListener("abort", listener, { once: true });
73
+
74
+ return {
75
+ [Symbol.dispose]() {
76
+ signal.removeEventListener("abort", listener);
77
+ },
78
+ };
79
+ }
80
+
81
+ export function onSignal(handler: (signal: "SIGINT" | "SIGTERM") => void): Disposable {
82
+ // Remove both handlers on the first signal so any second signal (of either
83
+ // type) falls through to Node's default behavior and terminates the process.
84
+ const remove = () => {
85
+ process.off("SIGINT", sigint);
86
+ process.off("SIGTERM", sigterm);
87
+ };
88
+ const sigint = () => {
89
+ remove();
90
+ handler("SIGINT");
91
+ };
92
+ const sigterm = () => {
93
+ remove();
94
+ handler("SIGTERM");
95
+ };
96
+
97
+ process.on("SIGINT", sigint);
98
+ process.on("SIGTERM", sigterm);
99
+
100
+ return {
101
+ [Symbol.dispose]: remove,
102
+ };
103
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = "0.28.0";
1
+ export const SDK_VERSION = "0.30.0";