@teamlearners/clawops 0.4.0 → 0.5.3

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.
@@ -578,6 +578,20 @@ function buildMediaResponse(audioBase64) {
578
578
  }
579
579
  });
580
580
  }
581
+ var VALID_DTMF_DIGITS = new Set("0123456789*#");
582
+ function parseDtmfEvent(data) {
583
+ const dtmf = data["dtmf"];
584
+ return {
585
+ digit: dtmf["digit"] ?? "",
586
+ track: dtmf["track"] ?? ""
587
+ };
588
+ }
589
+ function buildDtmfMessage(digit) {
590
+ if (!VALID_DTMF_DIGITS.has(digit)) {
591
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 DTMF digit: ${digit}`);
592
+ }
593
+ return JSON.stringify({ event: "dtmf", dtmf: { digit } });
594
+ }
581
595
  var MediaWebSocket = class {
582
596
  _ws = null;
583
597
  _audioQueue = [];
@@ -586,6 +600,8 @@ var MediaWebSocket = class {
586
600
  _onAudio = null;
587
601
  _onStart = null;
588
602
  _onClose = null;
603
+ _onDtmf = null;
604
+ _markWaiters = /* @__PURE__ */ new Map();
589
605
  /** Set the handler for inbound audio data. */
590
606
  onAudio(handler) {
591
607
  this._onAudio = handler;
@@ -598,6 +614,20 @@ var MediaWebSocket = class {
598
614
  onClose(handler) {
599
615
  this._onClose = handler;
600
616
  }
617
+ /** Set the handler for inbound DTMF events. */
618
+ onDtmf(handler) {
619
+ this._onDtmf = handler;
620
+ }
621
+ /** Send a single DTMF digit to the platform. */
622
+ sendDtmf(digit) {
623
+ if (this._ws && this._ws.readyState === 1) {
624
+ this._ws.send(buildDtmfMessage(digit));
625
+ }
626
+ }
627
+ /** Whether the WebSocket is connected. */
628
+ get isConnected() {
629
+ return this._ws !== null && this._ws.readyState === 1 && !this._closed;
630
+ }
601
631
  /** Connect to a media WebSocket URL with Bearer authentication. */
602
632
  async connect(url, apiKey) {
603
633
  const { WebSocket } = await import('ws');
@@ -657,6 +687,34 @@ var MediaWebSocket = class {
657
687
  );
658
688
  }
659
689
  }
690
+ /** Wait for all queued audio to be sent. */
691
+ flush() {
692
+ if (this._audioQueue.length === 0 || this._closed) return Promise.resolve();
693
+ return new Promise((resolve) => {
694
+ const check = () => {
695
+ if (this._audioQueue.length === 0 || this._closed) {
696
+ resolve();
697
+ } else {
698
+ setTimeout(check, 5);
699
+ }
700
+ };
701
+ setTimeout(check, 5);
702
+ });
703
+ }
704
+ /** Wait for a named mark to be echoed back by the server. */
705
+ waitForMark(name, timeoutMs = 5e3) {
706
+ if (this._closed) return Promise.resolve();
707
+ return new Promise((resolve) => {
708
+ const timer = setTimeout(() => {
709
+ this._markWaiters.delete(name);
710
+ resolve();
711
+ }, timeoutMs);
712
+ this._markWaiters.set(name, () => {
713
+ clearTimeout(timer);
714
+ resolve();
715
+ });
716
+ });
717
+ }
660
718
  /** Close the media WebSocket. */
661
719
  close() {
662
720
  this._closed = true;
@@ -682,6 +740,24 @@ var MediaWebSocket = class {
682
740
  }
683
741
  break;
684
742
  }
743
+ case "dtmf": {
744
+ const dtmfEvt = parseDtmfEvent(msg);
745
+ if (this._onDtmf) {
746
+ this._onDtmf(dtmfEvt.digit);
747
+ }
748
+ break;
749
+ }
750
+ case "mark": {
751
+ const markName = msg["mark"]?.["name"];
752
+ if (markName) {
753
+ const resolve = this._markWaiters.get(markName);
754
+ if (resolve) {
755
+ this._markWaiters.delete(markName);
756
+ resolve();
757
+ }
758
+ }
759
+ break;
760
+ }
685
761
  case "stop": {
686
762
  this.close();
687
763
  break;
@@ -870,6 +946,13 @@ var CallSession = class {
870
946
  _sendAudioFn = null;
871
947
  _clearAudioFn = null;
872
948
  _hangupFn = null;
949
+ /** @internal */
950
+ _sendDtmfFn = null;
951
+ /** @internal */
952
+ _isTransportConnected = null;
953
+ _dtmfCollectorActive = false;
954
+ _dtmfResolvers = [];
955
+ _dtmfBuffer = [];
873
956
  _handlers = /* @__PURE__ */ new Map();
874
957
  _endedPromise;
875
958
  _resolveEnded;
@@ -893,10 +976,12 @@ var CallSession = class {
893
976
  return (Date.now() - this.startTime.getTime()) / 1e3;
894
977
  }
895
978
  /** Bind transport functions (called internally by the agent). */
896
- _bindTransport(send, clear, hangup) {
979
+ _bindTransport(send, clear, hangup, sendDtmf, isConnected) {
897
980
  this._sendAudioFn = send;
898
981
  this._clearAudioFn = clear;
899
982
  this._hangupFn = hangup;
983
+ if (sendDtmf) this._sendDtmfFn = sendDtmf;
984
+ if (isConnected) this._isTransportConnected = isConnected;
900
985
  this._status = "active";
901
986
  }
902
987
  /** Send PCM16 or ulaw audio to the caller. */
@@ -911,10 +996,76 @@ var CallSession = class {
911
996
  this._clearAudioFn();
912
997
  }
913
998
  }
914
- /** Hang up the call. */
915
- hangup() {
999
+ /** Hang up the call, waiting for pending audio to finish. */
1000
+ async hangup() {
916
1001
  if (this._hangupFn) {
917
- this._hangupFn();
1002
+ await this._hangupFn();
1003
+ }
1004
+ }
1005
+ /** @internal Route a received DTMF digit to an active collector or buffer. */
1006
+ _routeDtmf(digit) {
1007
+ if (this._dtmfCollectorActive && this._dtmfResolvers.length > 0) {
1008
+ const resolve = this._dtmfResolvers.shift();
1009
+ resolve(digit);
1010
+ } else {
1011
+ this._dtmfBuffer.push(digit);
1012
+ }
1013
+ }
1014
+ /** Collect DTMF digits from the caller. */
1015
+ async collectDtmf(options) {
1016
+ if (this._dtmfCollectorActive) {
1017
+ throw new Error("\uC774\uBBF8 DTMF \uC218\uC9D1 \uC911\uC785\uB2C8\uB2E4");
1018
+ }
1019
+ const { maxDigits, finishOnKey = "#", timeout = 5 } = options;
1020
+ this._dtmfCollectorActive = true;
1021
+ const collected = [];
1022
+ try {
1023
+ while (collected.length < maxDigits) {
1024
+ if (this._dtmfBuffer.length > 0) {
1025
+ const digit2 = this._dtmfBuffer.shift();
1026
+ if (digit2 === finishOnKey) break;
1027
+ collected.push(digit2);
1028
+ continue;
1029
+ }
1030
+ const digit = await Promise.race([
1031
+ new Promise((resolve) => {
1032
+ this._dtmfResolvers.push(resolve);
1033
+ }),
1034
+ new Promise((resolve) => {
1035
+ setTimeout(() => resolve(null), timeout * 1e3);
1036
+ })
1037
+ ]);
1038
+ if (digit === null) break;
1039
+ if (digit === finishOnKey) break;
1040
+ collected.push(digit);
1041
+ }
1042
+ } finally {
1043
+ this._dtmfCollectorActive = false;
1044
+ this._dtmfResolvers = [];
1045
+ this._dtmfBuffer = [];
1046
+ }
1047
+ return collected.join("");
1048
+ }
1049
+ /** Send a sequence of DTMF digits. */
1050
+ async sendDtmfSequence(digits) {
1051
+ if (!this._sendDtmfFn) {
1052
+ throw new Error("DTMF \uC804\uC1A1 \uD568\uC218\uAC00 \uBC14\uC778\uB529\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
1053
+ }
1054
+ for (const ch of digits) {
1055
+ if (this._isTransportConnected && !this._isTransportConnected()) {
1056
+ throw new Error("DTMF \uC804\uC1A1 \uC911 \uC5F0\uACB0\uC774 \uB04A\uC5B4\uC84C\uC2B5\uB2C8\uB2E4");
1057
+ }
1058
+ if (ch === "w") {
1059
+ await new Promise((r) => setTimeout(r, 500));
1060
+ } else if (ch === "W") {
1061
+ await new Promise((r) => setTimeout(r, 1e3));
1062
+ } else if ("0123456789*#".includes(ch)) {
1063
+ if (this._sendDtmfFn) {
1064
+ await this._sendDtmfFn(ch);
1065
+ }
1066
+ } else {
1067
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 DTMF \uBB38\uC790: ${ch}`);
1068
+ }
918
1069
  }
919
1070
  }
920
1071
  /** Register an event handler. */
@@ -956,6 +1107,33 @@ var CallSession = class {
956
1107
  }
957
1108
  };
958
1109
 
1110
+ // src/agent/builtin-tool.ts
1111
+ var BuiltinTool = /* @__PURE__ */ ((BuiltinTool2) => {
1112
+ BuiltinTool2["HANG_UP"] = "hang_up";
1113
+ BuiltinTool2["COLLECT_DTMF"] = "collect_dtmf";
1114
+ BuiltinTool2["SEND_DTMF"] = "send_dtmf";
1115
+ BuiltinTool2["ALL"] = "all";
1116
+ BuiltinTool2["NONE"] = "none";
1117
+ return BuiltinTool2;
1118
+ })(BuiltinTool || {});
1119
+ var INDIVIDUAL_TOOLS = /* @__PURE__ */ new Set([
1120
+ "hang_up" /* HANG_UP */,
1121
+ "collect_dtmf" /* COLLECT_DTMF */,
1122
+ "send_dtmf" /* SEND_DTMF */
1123
+ ]);
1124
+ function resolveBuiltinTools(value) {
1125
+ if (typeof value === "string") {
1126
+ if (value === "all" /* ALL */) {
1127
+ return new Set(INDIVIDUAL_TOOLS);
1128
+ }
1129
+ if (value === "none" /* NONE */) {
1130
+ return /* @__PURE__ */ new Set();
1131
+ }
1132
+ return /* @__PURE__ */ new Set([value]);
1133
+ }
1134
+ return new Set(value.filter((t) => INDIVIDUAL_TOOLS.has(t)));
1135
+ }
1136
+
959
1137
  // src/agent/tool.ts
960
1138
  function functionTool(fn) {
961
1139
  return fn;
@@ -1164,6 +1342,12 @@ var ClawOpsAgent = class {
1164
1342
  _recording;
1165
1343
  _recordingPath;
1166
1344
  _activeSessions = /* @__PURE__ */ new Map();
1345
+ _builtinTools;
1346
+ _passiveDtmfDebounceMs;
1347
+ _passiveDtmfBuffer = [];
1348
+ _passiveDtmfTimer = null;
1349
+ _passiveDtmfCallId = null;
1350
+ _callSessions = /* @__PURE__ */ new Map();
1167
1351
  constructor(options) {
1168
1352
  this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
1169
1353
  this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
@@ -1173,6 +1357,8 @@ var ClawOpsAgent = class {
1173
1357
  this._recording = options.recording ?? false;
1174
1358
  this._recordingPath = options.recordingPath ?? "./recordings";
1175
1359
  this._mcpServers = options.mcpServers ?? [];
1360
+ this._builtinTools = resolveBuiltinTools(options.builtinTools ?? "all" /* ALL */);
1361
+ this._passiveDtmfDebounceMs = options.passiveDtmfDebounceMs ?? 500;
1176
1362
  if (options.tracing) {
1177
1363
  setTracingConfig(options.tracing);
1178
1364
  }
@@ -1187,7 +1373,9 @@ var ClawOpsAgent = class {
1187
1373
  tool(nameOrTool, description, parameters, handler) {
1188
1374
  if (typeof nameOrTool === "string") {
1189
1375
  if (!description || !parameters || !handler) {
1190
- throw new chunk6IQN5RQD_cjs.AgentError("tool(name, description, parameters, handler) requires all arguments.");
1376
+ throw new chunk6IQN5RQD_cjs.AgentError(
1377
+ "tool(name, description, parameters, handler) requires all arguments."
1378
+ );
1191
1379
  }
1192
1380
  this._tools.register({
1193
1381
  name: nameOrTool,
@@ -1223,7 +1411,9 @@ var ClawOpsAgent = class {
1223
1411
  throw new chunk6IQN5RQD_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1224
1412
  }
1225
1413
  if (!this._accountId) {
1226
- throw new chunk6IQN5RQD_cjs.AgentError("Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option.");
1414
+ throw new chunk6IQN5RQD_cjs.AgentError(
1415
+ "Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option."
1416
+ );
1227
1417
  }
1228
1418
  this._controlWs = new ControlWebSocket({
1229
1419
  baseUrl: this._baseUrl,
@@ -1270,6 +1460,7 @@ var ClawOpsAgent = class {
1270
1460
  session._markEnded();
1271
1461
  }
1272
1462
  this._activeSessions.clear();
1463
+ this._callSessions.clear();
1273
1464
  console.log("[ClawOpsAgent] Disconnected");
1274
1465
  }
1275
1466
  /**
@@ -1306,7 +1497,9 @@ var ClawOpsAgent = class {
1306
1497
  }
1307
1498
  }
1308
1499
  this._activeSessions.set(callSession.callId, callSession);
1309
- console.log(`[ClawOpsAgent] Outbound call initiated: ${this._fromNumber} -> ${to} (${callSession.callId})`);
1500
+ console.log(
1501
+ `[ClawOpsAgent] Outbound call initiated: ${this._fromNumber} -> ${to} (${callSession.callId})`
1502
+ );
1310
1503
  return callSession;
1311
1504
  }
1312
1505
  _handleIncoming(event) {
@@ -1384,6 +1577,30 @@ var ClawOpsAgent = class {
1384
1577
  this._activeSessions.delete(callId);
1385
1578
  }
1386
1579
  }
1580
+ _onDtmfEvent(callSession, digit) {
1581
+ callSession._emit("dtmf", digit);
1582
+ callSession._routeDtmf(digit);
1583
+ if (callSession._dtmfCollectorActive) {
1584
+ callSession.clearAudio();
1585
+ return;
1586
+ }
1587
+ this._passiveDtmfBuffer.push(digit);
1588
+ this._passiveDtmfCallId = callSession.callId;
1589
+ if (this._passiveDtmfTimer) {
1590
+ clearTimeout(this._passiveDtmfTimer);
1591
+ }
1592
+ this._passiveDtmfTimer = setTimeout(() => {
1593
+ const digits = this._passiveDtmfBuffer.join("");
1594
+ this._passiveDtmfBuffer = [];
1595
+ const sessionHandler = this._passiveDtmfCallId ? this._callSessions.get(this._passiveDtmfCallId) : null;
1596
+ this._passiveDtmfCallId = null;
1597
+ if (digits && sessionHandler && sessionHandler.feedDtmf) {
1598
+ sessionHandler.feedDtmf(digits).catch((err) => {
1599
+ console.error("[ClawOpsAgent] feedDtmf error:", err);
1600
+ });
1601
+ }
1602
+ }, this._passiveDtmfDebounceMs);
1603
+ }
1387
1604
  async _startCallSession(session, mediaWsUrl) {
1388
1605
  await withSpan(
1389
1606
  "clawops.call_session",
@@ -1421,9 +1638,17 @@ var ClawOpsAgent = class {
1421
1638
  () => {
1422
1639
  mediaWs.sendClear();
1423
1640
  },
1424
- () => {
1641
+ async () => {
1642
+ await mediaWs.flush();
1643
+ const markName = `hangup-${Date.now()}`;
1644
+ mediaWs.sendMark(markName);
1645
+ await mediaWs.waitForMark(markName, 5e3);
1425
1646
  mediaWs.close();
1426
- }
1647
+ },
1648
+ async (digit) => {
1649
+ mediaWs.sendDtmf(digit);
1650
+ },
1651
+ () => mediaWs.isConnected
1427
1652
  );
1428
1653
  const sessionHandler = this._session;
1429
1654
  if ("setToolRegistry" in sessionHandler && typeof sessionHandler.setToolRegistry === "function") {
@@ -1432,6 +1657,10 @@ var ClawOpsAgent = class {
1432
1657
  if (recorder && "setRecorder" in sessionHandler && typeof sessionHandler.setRecorder === "function") {
1433
1658
  sessionHandler.setRecorder(recorder);
1434
1659
  }
1660
+ if ("setBuiltinTools" in sessionHandler && typeof sessionHandler.setBuiltinTools === "function") {
1661
+ sessionHandler.setBuiltinTools(this._builtinTools);
1662
+ }
1663
+ this._callSessions.set(session.callId, sessionHandler);
1435
1664
  mediaWs.onAudio((ulawAudio, _timestamp) => {
1436
1665
  if (sessionHandler) {
1437
1666
  sessionHandler.feedAudio(ulawAudio);
@@ -1440,6 +1669,9 @@ var ClawOpsAgent = class {
1440
1669
  recorder.writeInbound(ulawToPcm16(ulawAudio));
1441
1670
  }
1442
1671
  });
1672
+ mediaWs.onDtmf((digit) => {
1673
+ this._onDtmfEvent(session, digit);
1674
+ });
1443
1675
  mediaWs.onClose(() => {
1444
1676
  if (recorder) {
1445
1677
  recorder.stop();
@@ -1468,6 +1700,7 @@ var ClawOpsAgent = class {
1468
1700
  session._emit("call_end");
1469
1701
  session._markEnded();
1470
1702
  this._activeSessions.delete(session.callId);
1703
+ this._callSessions.delete(session.callId);
1471
1704
  }
1472
1705
  }
1473
1706
  );
@@ -1482,6 +1715,32 @@ var HANG_UP_TOOL = {
1482
1715
  description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
1483
1716
  parameters: { type: "object", properties: {}, required: [] }
1484
1717
  };
1718
+ var COLLECT_DTMF_TOOL = {
1719
+ type: "function",
1720
+ name: "collect_dtmf",
1721
+ description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4. \uBC18\uB4DC\uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB34\uC5C7\uC744 \uC785\uB825\uD574\uC57C \uD558\uB294\uC9C0 \uC548\uB0B4\uD55C \uD6C4 \uD638\uCD9C\uD558\uC138\uC694.",
1722
+ parameters: {
1723
+ type: "object",
1724
+ properties: {
1725
+ max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
1726
+ finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
1727
+ timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
1728
+ },
1729
+ required: ["max_digits"]
1730
+ }
1731
+ };
1732
+ var SEND_DTMF_TOOL = {
1733
+ type: "function",
1734
+ name: "send_dtmf",
1735
+ description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
1736
+ parameters: {
1737
+ type: "object",
1738
+ properties: {
1739
+ digits: { type: "string", description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30." }
1740
+ },
1741
+ required: ["digits"]
1742
+ }
1743
+ };
1485
1744
  var OpenAIRealtime = class {
1486
1745
  _apiKey;
1487
1746
  _systemPrompt;
@@ -1490,6 +1749,10 @@ var OpenAIRealtime = class {
1490
1749
  _language;
1491
1750
  _eagerness;
1492
1751
  _greeting;
1752
+ _builtinTools = null;
1753
+ setBuiltinTools(tools) {
1754
+ this._builtinTools = tools;
1755
+ }
1493
1756
  _ws = null;
1494
1757
  _call = null;
1495
1758
  _tools = null;
@@ -1500,6 +1763,9 @@ var OpenAIRealtime = class {
1500
1763
  _responseStartTs = null;
1501
1764
  _sentAudioChunks = 0;
1502
1765
  _audioRemainder = Buffer.alloc(0);
1766
+ // Response state tracking — prevent sending response.create while one is active
1767
+ _responseInProgress = false;
1768
+ _onResponseDone = null;
1503
1769
  constructor(options = {}) {
1504
1770
  this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
1505
1771
  this._systemPrompt = options.systemPrompt ?? "";
@@ -1563,6 +1829,18 @@ var OpenAIRealtime = class {
1563
1829
  });
1564
1830
  });
1565
1831
  }
1832
+ async feedDtmf(digits) {
1833
+ await this._waitForResponseDone();
1834
+ this._send({
1835
+ type: "conversation.item.create",
1836
+ item: {
1837
+ type: "message",
1838
+ role: "user",
1839
+ content: [{ type: "input_text", text: `[DTMF \uC785\uB825: ${digits}]` }]
1840
+ }
1841
+ });
1842
+ this._send({ type: "response.create" });
1843
+ }
1566
1844
  feedAudio(audio) {
1567
1845
  if (this._ws && this._ws.readyState === 1 && !this._closed) {
1568
1846
  this._send({
@@ -1581,7 +1859,9 @@ var OpenAIRealtime = class {
1581
1859
  _sendSessionUpdate() {
1582
1860
  if (!this._ws || this._ws.readyState !== 1) return;
1583
1861
  const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
1584
- toolSchemas.push(HANG_UP_TOOL);
1862
+ if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */)) toolSchemas.push(HANG_UP_TOOL);
1863
+ if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */)) toolSchemas.push(COLLECT_DTMF_TOOL);
1864
+ if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */)) toolSchemas.push(SEND_DTMF_TOOL);
1585
1865
  this._send({
1586
1866
  type: "session.update",
1587
1867
  session: {
@@ -1648,6 +1928,19 @@ var OpenAIRealtime = class {
1648
1928
  }
1649
1929
  break;
1650
1930
  }
1931
+ case "response.created": {
1932
+ this._responseInProgress = true;
1933
+ break;
1934
+ }
1935
+ case "response.done": {
1936
+ this._responseInProgress = false;
1937
+ if (this._onResponseDone) {
1938
+ const cb = this._onResponseDone;
1939
+ this._onResponseDone = null;
1940
+ cb();
1941
+ }
1942
+ break;
1943
+ }
1651
1944
  case "error": {
1652
1945
  console.error("[OpenAIRealtime] API error:", msg["error"]);
1653
1946
  break;
@@ -1701,7 +1994,56 @@ var OpenAIRealtime = class {
1701
1994
  const callId = item["call_id"];
1702
1995
  if (funcName === "hang_up") {
1703
1996
  if (this._call) {
1704
- this._call.hangup();
1997
+ await this._call.hangup();
1998
+ }
1999
+ return;
2000
+ }
2001
+ if (funcName === "collect_dtmf") {
2002
+ if (this._call) {
2003
+ let result2;
2004
+ try {
2005
+ const args = JSON.parse(item["arguments"] ?? "{}");
2006
+ result2 = await this._call.collectDtmf({
2007
+ maxDigits: args["max_digits"] ?? 4,
2008
+ finishOnKey: args["finish_on_key"] ?? "#",
2009
+ timeout: args["timeout"] ?? 5
2010
+ });
2011
+ } catch (err) {
2012
+ result2 = `Error: ${err}`;
2013
+ }
2014
+ await this._waitForResponseDone();
2015
+ this._send({
2016
+ type: "conversation.item.create",
2017
+ item: {
2018
+ type: "function_call_output",
2019
+ call_id: callId,
2020
+ output: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)"
2021
+ }
2022
+ });
2023
+ this._send({ type: "response.create" });
2024
+ }
2025
+ return;
2026
+ }
2027
+ if (funcName === "send_dtmf") {
2028
+ if (this._call) {
2029
+ let result2;
2030
+ try {
2031
+ const args = JSON.parse(item["arguments"] ?? "{}");
2032
+ await this._call.sendDtmfSequence(args["digits"] ?? "");
2033
+ result2 = "sent";
2034
+ } catch (err) {
2035
+ result2 = `Error: ${err}`;
2036
+ }
2037
+ await this._waitForResponseDone();
2038
+ this._send({
2039
+ type: "conversation.item.create",
2040
+ item: {
2041
+ type: "function_call_output",
2042
+ call_id: callId,
2043
+ output: result2
2044
+ }
2045
+ });
2046
+ this._send({ type: "response.create" });
1705
2047
  }
1706
2048
  return;
1707
2049
  }
@@ -1717,6 +2059,7 @@ var OpenAIRealtime = class {
1717
2059
  console.error(`[OpenAIRealtime] Tool call failed: ${funcName}:`, err);
1718
2060
  result = `Error: ${err}`;
1719
2061
  }
2062
+ await this._waitForResponseDone();
1720
2063
  this._send({
1721
2064
  type: "conversation.item.create",
1722
2065
  item: {
@@ -1727,6 +2070,12 @@ var OpenAIRealtime = class {
1727
2070
  });
1728
2071
  this._send({ type: "response.create" });
1729
2072
  }
2073
+ _waitForResponseDone() {
2074
+ if (!this._responseInProgress) return Promise.resolve();
2075
+ return new Promise((resolve) => {
2076
+ this._onResponseDone = resolve;
2077
+ });
2078
+ }
1730
2079
  _send(data) {
1731
2080
  if (this._ws && this._ws.readyState === 1 && !this._closed) {
1732
2081
  this._ws.send(JSON.stringify(data));
@@ -1740,6 +2089,33 @@ var HANG_UP_TOOL2 = {
1740
2089
  description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
1741
2090
  parameters: { type: "object", properties: {} }
1742
2091
  };
2092
+ var COLLECT_DTMF_TOOL2 = {
2093
+ name: "collect_dtmf",
2094
+ description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4. \uBC18\uB4DC\uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB34\uC5C7\uC744 \uC785\uB825\uD574\uC57C \uD558\uB294\uC9C0 \uC548\uB0B4\uD55C \uD6C4 \uD638\uCD9C\uD558\uC138\uC694.",
2095
+ parameters: {
2096
+ type: "object",
2097
+ properties: {
2098
+ max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
2099
+ finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
2100
+ timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
2101
+ },
2102
+ required: ["max_digits"]
2103
+ }
2104
+ };
2105
+ var SEND_DTMF_TOOL2 = {
2106
+ name: "send_dtmf",
2107
+ description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
2108
+ parameters: {
2109
+ type: "object",
2110
+ properties: {
2111
+ digits: {
2112
+ type: "string",
2113
+ description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
2114
+ }
2115
+ },
2116
+ required: ["digits"]
2117
+ }
2118
+ };
1743
2119
  function resolveRef(ref, defs) {
1744
2120
  const parts = ref.replace(/^#\//, "").split("/");
1745
2121
  let result = defs;
@@ -1802,7 +2178,11 @@ function sanitizeSchemaForGemini(schema, defs, depth = 0) {
1802
2178
  result["properties"] = props;
1803
2179
  }
1804
2180
  if (schema["items"] && typeof schema["items"] === "object" && !Array.isArray(schema["items"])) {
1805
- result["items"] = sanitizeSchemaForGemini(schema["items"], defs, depth + 1);
2181
+ result["items"] = sanitizeSchemaForGemini(
2182
+ schema["items"],
2183
+ defs,
2184
+ depth + 1
2185
+ );
1806
2186
  }
1807
2187
  if (!result["type"] && result["properties"]) result["type"] = "object";
1808
2188
  if (result["type"] === "object" && !result["properties"]) result["properties"] = {};
@@ -1815,14 +2195,16 @@ var GeminiRealtime = class {
1815
2195
  _voice;
1816
2196
  _language;
1817
2197
  _greeting;
1818
- _generationConfig;
1819
- _ws = null;
2198
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2199
+ _session = null;
1820
2200
  _call = null;
1821
2201
  _tools = null;
1822
2202
  _recorder = null;
1823
2203
  _closed = false;
1824
2204
  _sentAudioChunks = 0;
1825
2205
  _audioRemainder = Buffer.alloc(0);
2206
+ _builtinTools = null;
2207
+ _toolCallInProgress = false;
1826
2208
  constructor(options = {}) {
1827
2209
  this._apiKey = options.apiKey ?? process.env["GOOGLE_API_KEY"] ?? "";
1828
2210
  this._systemPrompt = options.systemPrompt ?? "";
@@ -1830,7 +2212,6 @@ var GeminiRealtime = class {
1830
2212
  this._voice = options.voice ?? "Kore";
1831
2213
  this._language = options.language ?? "ko";
1832
2214
  this._greeting = options.greeting ?? true;
1833
- this._generationConfig = options.generationConfig;
1834
2215
  }
1835
2216
  /** Inject per-call ToolRegistry. */
1836
2217
  setToolRegistry(registry) {
@@ -1840,6 +2221,9 @@ var GeminiRealtime = class {
1840
2221
  setRecorder(recorder) {
1841
2222
  this._recorder = recorder;
1842
2223
  }
2224
+ setBuiltinTools(tools) {
2225
+ this._builtinTools = tools;
2226
+ }
1843
2227
  async start(callSession, tools) {
1844
2228
  this._call = callSession;
1845
2229
  if (tools) this._tools = tools;
@@ -1849,83 +2233,89 @@ var GeminiRealtime = class {
1849
2233
  if (!this._apiKey) {
1850
2234
  throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey option.");
1851
2235
  }
1852
- const { WebSocket } = await import('ws');
1853
- const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${this._apiKey}`;
1854
- this._ws = new WebSocket(url);
1855
- return new Promise((resolve, reject) => {
1856
- const ws = this._ws;
1857
- ws.on("open", () => {
1858
- this._sendSetup();
1859
- this._waitSetupComplete().then(() => {
1860
- if (this._greeting) {
1861
- this._sendGreeting();
2236
+ const { GoogleGenAI } = await import('@google/genai/node');
2237
+ const client = new GoogleGenAI({ apiKey: this._apiKey });
2238
+ const config = {
2239
+ responseModalities: ["AUDIO"],
2240
+ speechConfig: {
2241
+ voiceConfig: {
2242
+ prebuiltVoiceConfig: {
2243
+ voiceName: this._voice
1862
2244
  }
1863
- this._receiveLoop();
1864
- resolve();
1865
- }).catch(reject);
1866
- });
1867
- ws.on("close", () => {
1868
- this._closed = true;
1869
- });
1870
- ws.on("error", (err) => {
1871
- if (!this._ws) {
1872
- reject(err);
1873
2245
  }
1874
- console.error("[GeminiRealtime] WebSocket error:", err.message);
1875
- });
2246
+ },
2247
+ inputAudioTranscription: {},
2248
+ outputAudioTranscription: {}
2249
+ };
2250
+ if (this._systemPrompt) {
2251
+ config["systemInstruction"] = this._systemPrompt;
2252
+ }
2253
+ const toolSchemas = this._buildToolSchemas();
2254
+ if (toolSchemas.length > 0) {
2255
+ config["tools"] = [{ functionDeclarations: toolSchemas }];
2256
+ }
2257
+ this._session = await client.live.connect({
2258
+ model: this._model,
2259
+ config,
2260
+ callbacks: {
2261
+ onmessage: (msg) => this._handleMessage(msg),
2262
+ onerror: (err) => {
2263
+ console.error("[GeminiRealtime] SDK error:", err);
2264
+ },
2265
+ onclose: (ev) => {
2266
+ console.log(
2267
+ `[GeminiRealtime] Connection closed: code=${ev?.code ?? "unknown"}`
2268
+ );
2269
+ this._closed = true;
2270
+ }
2271
+ }
1876
2272
  });
2273
+ if (this._greeting) {
2274
+ this._session.sendClientContent({
2275
+ turns: [
2276
+ {
2277
+ role: "user",
2278
+ parts: [{ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." }]
2279
+ }
2280
+ ],
2281
+ turnComplete: true
2282
+ });
2283
+ }
1877
2284
  }
1878
2285
  feedAudio(audio) {
1879
- if (this._ws && this._ws.readyState === 1 && !this._closed) {
2286
+ if (this._session && !this._closed && !this._toolCallInProgress) {
1880
2287
  const pcm8k = ulawToPcm16(audio);
2288
+ if (this._recorder) {
2289
+ this._recorder.writeInbound(pcm8k);
2290
+ }
1881
2291
  const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
1882
- this._ws.send(
1883
- JSON.stringify({
1884
- realtimeInput: {
1885
- mediaChunks: [
1886
- {
1887
- mimeType: "audio/pcm;rate=16000",
1888
- data: pcm16k.toString("base64")
1889
- }
1890
- ]
1891
- }
1892
- })
1893
- );
2292
+ this._session.sendRealtimeInput({
2293
+ audio: {
2294
+ data: Buffer.from(pcm16k).toString("base64"),
2295
+ mimeType: "audio/pcm;rate=16000"
2296
+ }
2297
+ });
1894
2298
  }
1895
2299
  }
1896
- async stop() {
1897
- this._closed = true;
1898
- if (this._ws) {
1899
- this._ws.close();
1900
- this._ws = null;
2300
+ async feedDtmf(digits) {
2301
+ if (this._session) {
2302
+ this._session.sendClientContent({
2303
+ turns: [{ role: "user", parts: [{ text: `[DTMF \uC785\uB825: ${digits}]` }] }],
2304
+ turnComplete: true
2305
+ });
1901
2306
  }
1902
2307
  }
1903
- _sendSetup() {
1904
- if (!this._ws || this._ws.readyState !== 1) return;
1905
- const setupConfig = {
1906
- model: `models/${this._model}`,
1907
- generationConfig: {
1908
- responseModalities: ["AUDIO"],
1909
- speechConfig: {
1910
- voiceConfig: {
1911
- prebuiltVoiceConfig: {
1912
- voiceName: this._voice
1913
- }
1914
- }
1915
- },
1916
- ...this._generationConfig
1917
- },
1918
- realtimeInputConfig: {
1919
- automaticActivityDetection: {
1920
- disabled: false
1921
- }
2308
+ async stop() {
2309
+ this._closed = true;
2310
+ if (this._session) {
2311
+ try {
2312
+ this._session.close();
2313
+ } catch {
1922
2314
  }
1923
- };
1924
- if (this._systemPrompt) {
1925
- setupConfig["systemInstruction"] = {
1926
- parts: [{ text: this._systemPrompt }]
1927
- };
2315
+ this._session = null;
1928
2316
  }
2317
+ }
2318
+ _buildToolSchemas() {
1929
2319
  const toolDefs = this._tools ? this._tools.toOpenAITools().map((t) => ({
1930
2320
  name: t.function.name,
1931
2321
  description: t.function.description,
@@ -1933,108 +2323,59 @@ var GeminiRealtime = class {
1933
2323
  t.function.parameters ?? { type: "object", properties: {} }
1934
2324
  )
1935
2325
  })) : [];
1936
- toolDefs.push(HANG_UP_TOOL2);
1937
- setupConfig["tools"] = [{ functionDeclarations: toolDefs }];
1938
- this._ws.send(JSON.stringify({ setup: setupConfig }));
1939
- }
1940
- _waitSetupComplete() {
1941
- return new Promise((resolve, reject) => {
1942
- if (!this._ws) {
1943
- reject(new Error("WebSocket not connected"));
1944
- return;
1945
- }
1946
- const onMessage = (data) => {
1947
- try {
1948
- const msg = JSON.parse(data.toString());
1949
- if ("setupComplete" in msg) {
1950
- this._ws?.removeListener("message", onMessage);
1951
- resolve();
1952
- }
1953
- } catch {
1954
- }
1955
- };
1956
- this._ws.on("message", onMessage);
1957
- });
1958
- }
1959
- _sendGreeting() {
1960
- if (!this._ws || this._ws.readyState !== 1) return;
1961
- this._ws.send(
1962
- JSON.stringify({
1963
- clientContent: {
1964
- turns: [
1965
- {
1966
- role: "user",
1967
- parts: [{ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." }]
1968
- }
1969
- ],
1970
- turnComplete: true
1971
- }
1972
- })
1973
- );
1974
- }
1975
- _receiveLoop() {
1976
- if (!this._ws) return;
1977
- this._ws.on("message", (data) => {
1978
- try {
1979
- const msg = JSON.parse(data.toString());
1980
- this._handleMessage(msg);
1981
- } catch {
1982
- }
1983
- });
2326
+ if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */)) toolDefs.push(HANG_UP_TOOL2);
2327
+ if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */)) toolDefs.push(COLLECT_DTMF_TOOL2);
2328
+ if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */)) toolDefs.push(SEND_DTMF_TOOL2);
2329
+ return toolDefs;
1984
2330
  }
1985
2331
  _handleMessage(msg) {
1986
2332
  if (!this._call) return;
1987
- const serverContent = msg["serverContent"];
2333
+ const serverContent = msg.serverContent;
1988
2334
  if (serverContent) {
1989
- const modelTurn = serverContent["modelTurn"];
2335
+ const modelTurn = serverContent.modelTurn;
1990
2336
  if (modelTurn) {
1991
- const parts = modelTurn["parts"];
1992
- if (parts) {
1993
- for (const part of parts) {
1994
- const inlineData = part["inlineData"];
1995
- if (inlineData && inlineData["data"]) {
1996
- const mimeType = inlineData["mimeType"] ?? "";
1997
- if (mimeType.includes("audio")) {
1998
- this._handleAudioData(inlineData["data"]);
1999
- }
2000
- }
2001
- const text = part["text"];
2002
- if (text && this._call) {
2003
- this._call._emit("transcript", "assistant", text);
2337
+ for (const part of modelTurn.parts ?? []) {
2338
+ const inlineData = part.inlineData;
2339
+ if (inlineData?.data) {
2340
+ const mimeType = inlineData.mimeType ?? "";
2341
+ if (mimeType.includes("audio")) {
2342
+ this._handleAudioData(inlineData.data);
2004
2343
  }
2005
2344
  }
2006
2345
  }
2007
2346
  }
2008
- if (serverContent["turnComplete"]) {
2347
+ if (serverContent.turnComplete) {
2348
+ console.log("[GeminiRealtime] Turn complete");
2009
2349
  this._flushAudioRemainder();
2010
2350
  }
2011
- if (serverContent["interrupted"]) {
2351
+ if (serverContent.interrupted) {
2352
+ console.log("[GeminiRealtime] Barge-in detected");
2012
2353
  if (this._call) {
2013
2354
  this._call.clearAudio();
2014
2355
  }
2015
2356
  this._sentAudioChunks = 0;
2016
2357
  this._audioRemainder = Buffer.alloc(0);
2017
2358
  }
2018
- }
2019
- const inputTranscription = msg["inputTranscription"];
2020
- if (inputTranscription) {
2021
- const text = inputTranscription["text"];
2022
- if (text && this._call) {
2023
- this._call._emit("transcript", "user", text);
2359
+ const inputText = serverContent.inputTranscription?.text;
2360
+ if (inputText && this._call) {
2361
+ console.log(`[GeminiRealtime] [TRANSCRIPT-USER] ${inputText}`);
2362
+ this._call._emit("transcript", "user", inputText);
2024
2363
  }
2025
- }
2026
- const outputTranscription = msg["outputTranscription"];
2027
- if (outputTranscription) {
2028
- const text = outputTranscription["text"];
2029
- if (text && this._call) {
2030
- this._call._emit("transcript", "assistant", text);
2364
+ const outputText = serverContent.outputTranscription?.text;
2365
+ if (outputText && this._call) {
2366
+ console.log(`[GeminiRealtime] [TRANSCRIPT-ASSISTANT] ${outputText}`);
2367
+ this._call._emit("transcript", "assistant", outputText);
2031
2368
  }
2032
2369
  }
2033
- const toolCall = msg["toolCall"];
2034
- if (toolCall) {
2035
- this._handleToolCall(toolCall);
2370
+ if (msg.toolCall) {
2371
+ this._handleToolCall(msg.toolCall);
2372
+ }
2373
+ const toolCancellation = msg["toolCallCancellation"];
2374
+ if (toolCancellation) {
2375
+ console.log(
2376
+ `[GeminiRealtime] Tool call cancelled: ${(toolCancellation.ids ?? []).join(", ")}`
2377
+ );
2036
2378
  }
2037
- if (msg["toolCallCancellation"]) ;
2038
2379
  }
2039
2380
  _handleAudioData(b64Data) {
2040
2381
  if (!this._call) return;
@@ -2047,9 +2388,9 @@ var GeminiRealtime = class {
2047
2388
  const combined = Buffer.concat([this._audioRemainder, ulaw]);
2048
2389
  const chunkSize = 160;
2049
2390
  const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
2050
- for (let off = 0; off < fullEnd; off += chunkSize) {
2051
- this._call.sendAudio(combined.subarray(off, off + chunkSize));
2052
- this._sentAudioChunks++;
2391
+ if (fullEnd > 0) {
2392
+ this._call.sendAudio(combined.subarray(0, fullEnd));
2393
+ this._sentAudioChunks += fullEnd / chunkSize;
2053
2394
  }
2054
2395
  this._audioRemainder = combined.subarray(fullEnd);
2055
2396
  }
@@ -2065,19 +2406,66 @@ var GeminiRealtime = class {
2065
2406
  }
2066
2407
  }
2067
2408
  async _handleToolCall(toolCall) {
2068
- const functionCalls = toolCall["functionCalls"];
2409
+ const functionCalls = toolCall.functionCalls;
2069
2410
  if (!functionCalls) return;
2411
+ this._toolCallInProgress = true;
2412
+ console.log(
2413
+ `[GeminiRealtime] toolCall: ${functionCalls.map((fc) => fc.name).join(", ")}`
2414
+ );
2070
2415
  const responses = [];
2071
2416
  for (const fc of functionCalls) {
2072
- const name = fc["name"];
2073
- const fcId = fc["id"] ?? "";
2074
- const args = fc["args"] ?? {};
2417
+ const name = fc.name ?? "";
2418
+ const fcId = fc.id ?? "";
2419
+ const args = fc.args ?? {};
2420
+ console.log(`[GeminiRealtime] Tool call: ${name}(${JSON.stringify(args)})`);
2075
2421
  if (name === "hang_up") {
2422
+ console.log("[GeminiRealtime] hang_up: ending call");
2076
2423
  if (this._call) {
2077
- this._call.hangup();
2424
+ await this._call.hangup();
2078
2425
  }
2079
2426
  return;
2080
2427
  }
2428
+ if (name === "collect_dtmf") {
2429
+ if (this._call) {
2430
+ let result;
2431
+ try {
2432
+ console.log(
2433
+ `[GeminiRealtime] collect_dtmf: waiting for digits (maxDigits=${args["max_digits"] ?? 4}, timeout=${args["timeout"] ?? 5})`
2434
+ );
2435
+ result = await this._call.collectDtmf({
2436
+ maxDigits: args["max_digits"] ?? 4,
2437
+ finishOnKey: args["finish_on_key"] ?? "#",
2438
+ timeout: args["timeout"] ?? 5
2439
+ });
2440
+ console.log(`[GeminiRealtime] DTMF collected: ${result || "(empty)"}`);
2441
+ } catch (err) {
2442
+ console.error(`[GeminiRealtime] collect_dtmf error:`, err);
2443
+ result = `Error: ${err}`;
2444
+ }
2445
+ responses.push({
2446
+ id: fcId,
2447
+ name,
2448
+ response: { result: result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)" }
2449
+ });
2450
+ }
2451
+ continue;
2452
+ }
2453
+ if (name === "send_dtmf") {
2454
+ if (this._call) {
2455
+ let result;
2456
+ try {
2457
+ console.log(`[GeminiRealtime] send_dtmf: digits="${args["digits"] ?? ""}"`);
2458
+ await this._call.sendDtmfSequence(args["digits"] ?? "");
2459
+ result = "sent";
2460
+ console.log(`[GeminiRealtime] send_dtmf: sent`);
2461
+ } catch (err) {
2462
+ console.error(`[GeminiRealtime] send_dtmf error:`, err);
2463
+ result = `Error: ${err}`;
2464
+ }
2465
+ responses.push({ id: fcId, name, response: { result } });
2466
+ }
2467
+ continue;
2468
+ }
2081
2469
  if (!this._tools || !this._tools.has(name)) {
2082
2470
  console.error(`[GeminiRealtime] Unknown tool: ${name}`);
2083
2471
  responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
@@ -2085,13 +2473,15 @@ var GeminiRealtime = class {
2085
2473
  }
2086
2474
  try {
2087
2475
  const result = await this._tools.call(name, args);
2476
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
2477
+ console.log(`[GeminiRealtime] Tool result: ${name} -> ${resultStr.substring(0, 200)}`);
2088
2478
  responses.push({
2089
2479
  id: fcId,
2090
2480
  name,
2091
- response: { result: typeof result === "string" ? result : JSON.stringify(result) }
2481
+ response: { result: resultStr }
2092
2482
  });
2093
2483
  } catch (err) {
2094
- console.error(`[GeminiRealtime] Tool call error for ${name}:`, err);
2484
+ console.error(`[GeminiRealtime] Tool call failed: ${name}:`, err);
2095
2485
  responses.push({
2096
2486
  id: fcId,
2097
2487
  name,
@@ -2099,19 +2489,41 @@ var GeminiRealtime = class {
2099
2489
  });
2100
2490
  }
2101
2491
  }
2102
- if (this._ws && this._ws.readyState === 1) {
2103
- this._ws.send(
2104
- JSON.stringify({
2105
- toolResponse: {
2106
- functionResponses: responses
2107
- }
2108
- })
2109
- );
2492
+ if (responses.length > 0 && this._session) {
2493
+ console.log(`[GeminiRealtime] Sending ${responses.length} tool response(s)`);
2494
+ this._session.sendToolResponse({
2495
+ functionResponses: responses
2496
+ });
2110
2497
  }
2498
+ this._toolCallInProgress = false;
2111
2499
  }
2112
2500
  };
2113
2501
 
2114
2502
  // src/agent/pipeline/pipeline-session.ts
2503
+ var COLLECT_DTMF_TOOL3 = {
2504
+ function: {
2505
+ description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4.",
2506
+ parameters: {
2507
+ properties: {
2508
+ max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
2509
+ finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
2510
+ timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
2511
+ },
2512
+ required: ["max_digits"]
2513
+ }
2514
+ }
2515
+ };
2516
+ var SEND_DTMF_TOOL3 = {
2517
+ function: {
2518
+ description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
2519
+ parameters: {
2520
+ properties: {
2521
+ digits: { type: "string", description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30." }
2522
+ },
2523
+ required: ["digits"]
2524
+ }
2525
+ }
2526
+ };
2115
2527
  var PipelineSession = class {
2116
2528
  _stt;
2117
2529
  _llm;
@@ -2130,6 +2542,7 @@ var PipelineSession = class {
2130
2542
  _audioBuffer = [];
2131
2543
  _running = false;
2132
2544
  _speaking = false;
2545
+ _builtinTools = null;
2133
2546
  constructor(options) {
2134
2547
  this._stt = options.stt;
2135
2548
  this._llm = options.llm;
@@ -2150,6 +2563,9 @@ var PipelineSession = class {
2150
2563
  setRecorder(recorder) {
2151
2564
  this._recorder = recorder;
2152
2565
  }
2566
+ setBuiltinTools(tools) {
2567
+ this._builtinTools = tools;
2568
+ }
2153
2569
  async start(callSession, tools) {
2154
2570
  this._callSession = callSession;
2155
2571
  this._tools = tools ?? null;
@@ -2175,6 +2591,13 @@ var PipelineSession = class {
2175
2591
  this._audioBuffer.push(audio);
2176
2592
  }
2177
2593
  }
2594
+ async feedDtmf(digits) {
2595
+ this._conversation.push({
2596
+ role: "user",
2597
+ content: `[DTMF \uC785\uB825: ${digits}]`
2598
+ });
2599
+ await this._respond();
2600
+ }
2178
2601
  async stop() {
2179
2602
  this._running = false;
2180
2603
  this._audioBuffer = [];
@@ -2216,11 +2639,37 @@ var PipelineSession = class {
2216
2639
  this._conversation.push({ role: "user", content: transcript });
2217
2640
  await this._respond();
2218
2641
  }
2642
+ _buildEffectiveTools() {
2643
+ const includeCollectDtmf = !this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */);
2644
+ const includeSendDtmf = !this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */);
2645
+ if (!includeCollectDtmf && !includeSendDtmf) return this._tools ?? void 0;
2646
+ const base = this._tools ? this._tools.fork() : new ToolRegistry();
2647
+ if (includeCollectDtmf) {
2648
+ base.register({
2649
+ name: "collect_dtmf",
2650
+ description: COLLECT_DTMF_TOOL3.function.description,
2651
+ parameters: COLLECT_DTMF_TOOL3.function.parameters.properties,
2652
+ required: COLLECT_DTMF_TOOL3.function.parameters.required,
2653
+ handler: async () => ""
2654
+ });
2655
+ }
2656
+ if (includeSendDtmf) {
2657
+ base.register({
2658
+ name: "send_dtmf",
2659
+ description: SEND_DTMF_TOOL3.function.description,
2660
+ parameters: SEND_DTMF_TOOL3.function.parameters.properties,
2661
+ required: SEND_DTMF_TOOL3.function.parameters.required,
2662
+ handler: async () => ""
2663
+ });
2664
+ }
2665
+ return base;
2666
+ }
2219
2667
  async _respond() {
2220
2668
  let fullResponse = "";
2221
2669
  const textChunks = [];
2670
+ const effectiveTools = this._buildEffectiveTools();
2222
2671
  const llmStream = this._llm.generate(this._conversation, {
2223
- tools: this._tools ?? void 0,
2672
+ tools: effectiveTools,
2224
2673
  temperature: this._temperature,
2225
2674
  maxTokens: this._maxTokens
2226
2675
  });
@@ -2239,10 +2688,38 @@ var PipelineSession = class {
2239
2688
  }
2240
2689
  }
2241
2690
  async _handleToolCall(chunk) {
2242
- if (!chunk.toolCall || !this._tools) return;
2691
+ if (!chunk.toolCall) return;
2243
2692
  const { id, name, arguments: argsStr } = chunk.toolCall;
2244
2693
  try {
2245
2694
  const args = JSON.parse(argsStr);
2695
+ if (name === "collect_dtmf" && this._callSession) {
2696
+ let result2;
2697
+ try {
2698
+ result2 = await this._callSession.collectDtmf({
2699
+ maxDigits: args["max_digits"] ?? 4,
2700
+ finishOnKey: args["finish_on_key"] ?? "#",
2701
+ timeout: args["timeout"] ?? 5
2702
+ });
2703
+ } catch (err) {
2704
+ result2 = `Error: ${err}`;
2705
+ }
2706
+ this._conversation.push({ role: "tool", content: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)", tool_call_id: id, name });
2707
+ await this._respond();
2708
+ return;
2709
+ }
2710
+ if (name === "send_dtmf" && this._callSession) {
2711
+ let result2;
2712
+ try {
2713
+ await this._callSession.sendDtmfSequence(args["digits"] ?? "");
2714
+ result2 = "sent";
2715
+ } catch (err) {
2716
+ result2 = `Error: ${err}`;
2717
+ }
2718
+ this._conversation.push({ role: "tool", content: result2, tool_call_id: id, name });
2719
+ await this._respond();
2720
+ return;
2721
+ }
2722
+ if (!this._tools) return;
2246
2723
  const result = await this._tools.call(name, args);
2247
2724
  this._conversation.push({
2248
2725
  role: "assistant",
@@ -2255,9 +2732,10 @@ var PipelineSession = class {
2255
2732
  tool_call_id: id,
2256
2733
  name
2257
2734
  });
2735
+ const effectiveTools = this._buildEffectiveTools();
2258
2736
  let followUpText = "";
2259
2737
  const followUpStream = this._llm.generate(this._conversation, {
2260
- tools: this._tools ?? void 0,
2738
+ tools: effectiveTools,
2261
2739
  temperature: this._temperature,
2262
2740
  maxTokens: this._maxTokens
2263
2741
  });
@@ -3083,6 +3561,7 @@ function mcpServerHTTP(options) {
3083
3561
 
3084
3562
  exports.AnthropicLLM = AnthropicLLM;
3085
3563
  exports.AudioRecorder = AudioRecorder;
3564
+ exports.BuiltinTool = BuiltinTool;
3086
3565
  exports.CallSession = CallSession;
3087
3566
  exports.ClawOpsAgent = ClawOpsAgent;
3088
3567
  exports.DECODE_TABLE = DECODE_TABLE;