@teamlearners/clawops 0.8.1 → 0.11.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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkMOH4FRJZ_cjs = require('../chunk-MOH4FRJZ.cjs');
3
+ var chunkCOBPAA3N_cjs = require('../chunk-COBPAA3N.cjs');
4
4
  var pino = require('pino');
5
5
  var fs = require('fs');
6
6
  var path = require('path');
@@ -384,6 +384,7 @@ var ControlWebSocket = class {
384
384
  _url;
385
385
  _ws = null;
386
386
  _handlers = /* @__PURE__ */ new Map();
387
+ _transferResolvers = /* @__PURE__ */ new Map();
387
388
  _reconnectDelay = INITIAL_RECONNECT_DELAY;
388
389
  _closed = false;
389
390
  _connectedResolve = null;
@@ -411,6 +412,31 @@ var ControlWebSocket = class {
411
412
  async waitConnected() {
412
413
  return this._connectedPromise;
413
414
  }
415
+ /** Request a call transfer and wait for the result. */
416
+ async requestTransfer(callId, params) {
417
+ return new Promise((resolve, reject) => {
418
+ const timeout = (params.timeout || 30) + 10;
419
+ const timer = setTimeout(() => {
420
+ this._transferResolvers.delete(callId);
421
+ reject(new Error("transfer timeout"));
422
+ }, timeout * 1e3);
423
+ this._transferResolvers.set(callId, {
424
+ resolve: (value) => {
425
+ clearTimeout(timer);
426
+ resolve(value);
427
+ },
428
+ reject: (reason) => {
429
+ clearTimeout(timer);
430
+ reject(reason);
431
+ }
432
+ });
433
+ this.send({
434
+ event: "call.transfer",
435
+ callId,
436
+ transfer: params
437
+ });
438
+ });
439
+ }
414
440
  /** Send a JSON message over the control WebSocket. */
415
441
  send(message) {
416
442
  if (this._ws && this._ws.readyState === 1) {
@@ -421,6 +447,10 @@ var ControlWebSocket = class {
421
447
  close() {
422
448
  this._closed = true;
423
449
  this._clearPingTimer();
450
+ for (const [, resolver] of this._transferResolvers) {
451
+ resolver.reject(new Error("connection closed"));
452
+ }
453
+ this._transferResolvers.clear();
424
454
  if (this._ws) {
425
455
  this._ws.close();
426
456
  this._ws = null;
@@ -466,6 +496,14 @@ var ControlWebSocket = class {
466
496
  });
467
497
  }
468
498
  _dispatchEvent(event) {
499
+ if (["call.transfer.completed", "call.transfer.failed"].includes(event.event)) {
500
+ const callId = event.callId;
501
+ const resolver = this._transferResolvers.get(callId);
502
+ if (resolver) {
503
+ this._transferResolvers.delete(callId);
504
+ resolver.resolve(event.transfer || {});
505
+ }
506
+ }
469
507
  const handlers = this._handlers.get(event.event);
470
508
  if (handlers) {
471
509
  for (const handler of handlers) {
@@ -935,8 +973,8 @@ var AudioRecorder = class {
935
973
  let gap = trackPos - this._mixWritten;
936
974
  gap = gap - gap % 2;
937
975
  if (gap > 0) {
938
- const silence = Buffer.alloc(gap);
939
- fs__namespace.writeSync(this._fdMix, silence, 0, silence.length, 44 + this._mixWritten);
976
+ const silence2 = Buffer.alloc(gap);
977
+ fs__namespace.writeSync(this._fdMix, silence2, 0, silence2.length, 44 + this._mixWritten);
940
978
  this._mixWritten += gap;
941
979
  }
942
980
  }
@@ -1005,7 +1043,7 @@ var MAX_ERROR_MESSAGE_LENGTH = 200;
1005
1043
  function getSdkInfo() {
1006
1044
  return {
1007
1045
  name: "clawops-node",
1008
- version: chunkMOH4FRJZ_cjs.VERSION,
1046
+ version: chunkCOBPAA3N_cjs.VERSION,
1009
1047
  runtime: `node/${process.versions.node}`,
1010
1048
  os: `${process.platform}/${os__default.default.arch()}`
1011
1049
  };
@@ -1047,6 +1085,8 @@ var CallSession = class {
1047
1085
  /** @internal */
1048
1086
  _sendDtmfFn = null;
1049
1087
  /** @internal */
1088
+ _transferFn = null;
1089
+ /** @internal */
1050
1090
  _isTransportConnected = null;
1051
1091
  _dtmfCollectorActive = false;
1052
1092
  _dtmfResolvers = [];
@@ -1198,6 +1238,22 @@ var CallSession = class {
1198
1238
  }
1199
1239
  }
1200
1240
  }
1241
+ /** Transfer the call to another destination. */
1242
+ async transfer(to, options) {
1243
+ if (!this._transferFn) {
1244
+ throw new Error("transfer not available");
1245
+ }
1246
+ return this._transferFn({
1247
+ to,
1248
+ mode: options?.mode ?? "blind",
1249
+ afterTransfer: options?.afterTransfer ?? "terminate",
1250
+ holdMedia: options?.holdMedia ?? "ringback",
1251
+ whisper: options?.whisper ?? null,
1252
+ context: options?.context ?? null,
1253
+ callerId: options?.callerId ?? null,
1254
+ timeout: options?.timeout ?? 30
1255
+ });
1256
+ }
1201
1257
  /** Register an event handler. */
1202
1258
  on(event, handler) {
1203
1259
  let list = this._handlers.get(event);
@@ -1242,6 +1298,7 @@ var BuiltinTool = /* @__PURE__ */ ((BuiltinTool2) => {
1242
1298
  BuiltinTool2["HANG_UP"] = "hang_up";
1243
1299
  BuiltinTool2["COLLECT_DTMF"] = "collect_dtmf";
1244
1300
  BuiltinTool2["SEND_DTMF"] = "send_dtmf";
1301
+ BuiltinTool2["TRANSFER_CALL"] = "transfer_call";
1245
1302
  BuiltinTool2["ALL"] = "all";
1246
1303
  BuiltinTool2["NONE"] = "none";
1247
1304
  return BuiltinTool2;
@@ -1249,7 +1306,8 @@ var BuiltinTool = /* @__PURE__ */ ((BuiltinTool2) => {
1249
1306
  var INDIVIDUAL_TOOLS = /* @__PURE__ */ new Set([
1250
1307
  "hang_up" /* HANG_UP */,
1251
1308
  "collect_dtmf" /* COLLECT_DTMF */,
1252
- "send_dtmf" /* SEND_DTMF */
1309
+ "send_dtmf" /* SEND_DTMF */,
1310
+ "transfer_call" /* TRANSFER_CALL */
1253
1311
  ]);
1254
1312
  function resolveBuiltinTools(value) {
1255
1313
  if (typeof value === "string") {
@@ -1263,6 +1321,187 @@ function resolveBuiltinTools(value) {
1263
1321
  }
1264
1322
  return new Set(value.filter((t) => INDIVIDUAL_TOOLS.has(t)));
1265
1323
  }
1324
+ var CHUNK_SIZE = 160;
1325
+ var SAMPLE_RATE2 = 8e3;
1326
+ var BELL_PARTIALS = [
1327
+ { freqRatio: 1, amplitude: 1, decayRate: 1.2 },
1328
+ { freqRatio: 2.76, amplitude: 0.5, decayRate: 2.5 },
1329
+ { freqRatio: 5.4, amplitude: 0.25, decayRate: 4 }
1330
+ ];
1331
+ function bellNote(freq, durationMs, volume) {
1332
+ const n = SAMPLE_RATE2 * durationMs / 1e3 | 0;
1333
+ const attackSamples = 3e-3 * SAMPLE_RATE2 | 0;
1334
+ const samples = new Int16Array(n);
1335
+ for (let i = 0; i < n; i++) {
1336
+ const t = i / SAMPLE_RATE2;
1337
+ let val = 0;
1338
+ for (const p of BELL_PARTIALS) {
1339
+ const f = freq * p.freqRatio;
1340
+ if (f >= SAMPLE_RATE2 / 2) continue;
1341
+ const env = p.amplitude * Math.exp(-p.decayRate * t * (1e3 / durationMs));
1342
+ val += env * Math.sin(2 * Math.PI * f * t);
1343
+ }
1344
+ if (i < attackSamples) {
1345
+ val *= i / attackSamples;
1346
+ }
1347
+ samples[i] = Math.max(-32768, Math.min(32767, volume * 32767 * val | 0));
1348
+ }
1349
+ return samples;
1350
+ }
1351
+ function silence(durationMs) {
1352
+ return new Int16Array(SAMPLE_RATE2 * durationMs / 1e3 | 0);
1353
+ }
1354
+ function int16ArrayToBuffer(samples) {
1355
+ const buf = Buffer.alloc(samples.length * 2);
1356
+ for (let i = 0; i < samples.length; i++) {
1357
+ buf.writeInt16LE(samples[i], i * 2);
1358
+ }
1359
+ return buf;
1360
+ }
1361
+ function concatInt16Arrays(...arrays) {
1362
+ let total = 0;
1363
+ for (const a of arrays) total += a.length;
1364
+ const result = new Int16Array(total);
1365
+ let offset = 0;
1366
+ for (const a of arrays) {
1367
+ result.set(a, offset);
1368
+ offset += a.length;
1369
+ }
1370
+ return result;
1371
+ }
1372
+ var PENTATONIC_C5 = {
1373
+ C5: 523.25,
1374
+ D5: 587.33,
1375
+ E5: 659.25,
1376
+ G5: 783.99,
1377
+ A5: 880,
1378
+ C6: 1046.5
1379
+ };
1380
+ function generateComfortTone(volume = 0.12) {
1381
+ const p = PENTATONIC_C5;
1382
+ const melody = [
1383
+ [p.E5, 450],
1384
+ [p.G5, 450],
1385
+ [p.A5, 450],
1386
+ [p.C6, 600],
1387
+ [0, 800],
1388
+ [p.A5, 400],
1389
+ [p.G5, 400],
1390
+ [p.E5, 400],
1391
+ [p.D5, 600],
1392
+ [0, 2500],
1393
+ [p.C5, 500],
1394
+ [p.E5, 500],
1395
+ [p.C6, 700],
1396
+ [0, 2500]
1397
+ ];
1398
+ const parts = [];
1399
+ for (const [freq, durMs] of melody) {
1400
+ if (freq === 0) {
1401
+ parts.push(silence(durMs));
1402
+ } else {
1403
+ parts.push(bellNote(freq, durMs, volume));
1404
+ parts.push(silence(150));
1405
+ }
1406
+ }
1407
+ const pcm = int16ArrayToBuffer(concatInt16Arrays(...parts));
1408
+ const ulaw = pcm16ToUlaw(pcm);
1409
+ const chunks = [];
1410
+ for (let i = 0; i < ulaw.length; i += CHUNK_SIZE) {
1411
+ chunks.push(ulaw.subarray(i, i + CHUNK_SIZE));
1412
+ }
1413
+ return chunks;
1414
+ }
1415
+ function loadHoldAudio(source) {
1416
+ if (source === true) {
1417
+ return generateComfortTone();
1418
+ }
1419
+ if (Buffer.isBuffer(source)) {
1420
+ const chunks = [];
1421
+ for (let i = 0; i < source.length; i += CHUNK_SIZE) {
1422
+ chunks.push(source.subarray(i, i + CHUNK_SIZE));
1423
+ }
1424
+ return chunks;
1425
+ }
1426
+ if (typeof source === "string") {
1427
+ const data = fs__namespace.readFileSync(source);
1428
+ const riff = data.toString("ascii", 0, 4);
1429
+ if (riff !== "RIFF") {
1430
+ throw new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${source}`);
1431
+ }
1432
+ let offset = 12;
1433
+ let channels = 1;
1434
+ let sampleRate = 8e3;
1435
+ let bitsPerSample = 16;
1436
+ let pcmData = null;
1437
+ while (offset < data.length - 8) {
1438
+ const chunkId = data.toString("ascii", offset, offset + 4);
1439
+ const chunkSize = data.readUInt32LE(offset + 4);
1440
+ if (chunkId === "fmt ") {
1441
+ channels = data.readUInt16LE(offset + 10);
1442
+ sampleRate = data.readUInt32LE(offset + 12);
1443
+ bitsPerSample = data.readUInt16LE(offset + 22);
1444
+ } else if (chunkId === "data") {
1445
+ pcmData = data.subarray(offset + 8, offset + 8 + chunkSize);
1446
+ }
1447
+ offset += 8 + chunkSize;
1448
+ if (chunkSize % 2 !== 0) offset++;
1449
+ }
1450
+ if (!pcmData) {
1451
+ throw new Error(`WAV data chunk\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${source}`);
1452
+ }
1453
+ if (bitsPerSample !== 16) {
1454
+ throw new Error(`16-bit PCM wav\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4 (\uD604\uC7AC: ${bitsPerSample}-bit)`);
1455
+ }
1456
+ if (channels === 2) {
1457
+ const monoLen = pcmData.length / 2;
1458
+ const mono = Buffer.alloc(monoLen);
1459
+ for (let i = 0; i < monoLen / 2; i++) {
1460
+ mono.writeInt16LE(pcmData.readInt16LE(i * 4), i * 2);
1461
+ }
1462
+ pcmData = mono;
1463
+ }
1464
+ if (sampleRate !== SAMPLE_RATE2) {
1465
+ pcmData = resamplePcm16(pcmData, sampleRate, SAMPLE_RATE2);
1466
+ }
1467
+ const ulaw = pcm16ToUlaw(pcmData);
1468
+ const chunks = [];
1469
+ for (let i = 0; i < ulaw.length; i += CHUNK_SIZE) {
1470
+ chunks.push(ulaw.subarray(i, i + CHUNK_SIZE));
1471
+ }
1472
+ return chunks;
1473
+ }
1474
+ throw new TypeError(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 holdAudio \uD0C0\uC785: ${typeof source}`);
1475
+ }
1476
+ var HoldAudioPlayer = class {
1477
+ _call;
1478
+ _chunks;
1479
+ _timer = null;
1480
+ _index = 0;
1481
+ constructor(call, chunks) {
1482
+ this._call = call;
1483
+ this._chunks = chunks;
1484
+ }
1485
+ start() {
1486
+ if (this._timer !== null) return;
1487
+ this._index = 0;
1488
+ this._timer = setInterval(() => {
1489
+ if (this._index >= this._chunks.length) {
1490
+ this._index = 0;
1491
+ }
1492
+ const chunk = this._chunks[this._index++];
1493
+ if (chunk) {
1494
+ this._call.sendAudio(chunk);
1495
+ }
1496
+ }, 20);
1497
+ }
1498
+ stop() {
1499
+ if (this._timer === null) return;
1500
+ clearInterval(this._timer);
1501
+ this._timer = null;
1502
+ this._call.clearAudio();
1503
+ }
1504
+ };
1266
1505
 
1267
1506
  // src/agent/tool.ts
1268
1507
  function functionTool(fn) {
@@ -1481,10 +1720,11 @@ var ClawOpsAgent = class {
1481
1720
  _log;
1482
1721
  _pipelineLog;
1483
1722
  _isPipelineSession = false;
1723
+ _holdAudioChunks = null;
1484
1724
  constructor(options) {
1485
1725
  this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
1486
1726
  this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
1487
- this._baseUrl = options.baseUrl ?? chunkMOH4FRJZ_cjs.DEFAULT_BASE_URL;
1727
+ this._baseUrl = options.baseUrl ?? chunkCOBPAA3N_cjs.DEFAULT_BASE_URL;
1488
1728
  this._fromNumber = options.from;
1489
1729
  this._session = options.session;
1490
1730
  this._recording = options.recording ?? false;
@@ -1498,6 +1738,9 @@ var ClawOpsAgent = class {
1498
1738
  this._log = createAgentLogger(options.logger);
1499
1739
  this._pipelineLog = createPipelineLogger(this._log);
1500
1740
  this._isPipelineSession = "_stt" in this._session && "_llm" in this._session;
1741
+ if (options.toolConfig?.holdAudio) {
1742
+ this._holdAudioChunks = loadHoldAudio(options.toolConfig.holdAudio);
1743
+ }
1501
1744
  }
1502
1745
  /**
1503
1746
  * Register a function tool.
@@ -1509,7 +1752,7 @@ var ClawOpsAgent = class {
1509
1752
  tool(nameOrTool, description, parameters, handler) {
1510
1753
  if (typeof nameOrTool === "string") {
1511
1754
  if (!description || !parameters || !handler) {
1512
- throw new chunkMOH4FRJZ_cjs.AgentError(
1755
+ throw new chunkCOBPAA3N_cjs.AgentError(
1513
1756
  "tool(name, description, parameters, handler) requires all arguments."
1514
1757
  );
1515
1758
  }
@@ -1545,10 +1788,10 @@ var ClawOpsAgent = class {
1545
1788
  async connect() {
1546
1789
  if (this._controlWs) return;
1547
1790
  if (!this._apiKey) {
1548
- throw new chunkMOH4FRJZ_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1791
+ throw new chunkCOBPAA3N_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1549
1792
  }
1550
1793
  if (!this._accountId) {
1551
- throw new chunkMOH4FRJZ_cjs.AgentError(
1794
+ throw new chunkCOBPAA3N_cjs.AgentError(
1552
1795
  "Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option."
1553
1796
  );
1554
1797
  }
@@ -1572,7 +1815,7 @@ var ClawOpsAgent = class {
1572
1815
  } catch {
1573
1816
  }
1574
1817
  } catch (err) {
1575
- throw new chunkMOH4FRJZ_cjs.AgentConnectionError(
1818
+ throw new chunkCOBPAA3N_cjs.AgentConnectionError(
1576
1819
  `Failed to connect to ClawOps: ${err instanceof Error ? err.message : String(err)}`
1577
1820
  );
1578
1821
  }
@@ -1623,7 +1866,7 @@ var ClawOpsAgent = class {
1623
1866
  });
1624
1867
  if (resp.status !== 201) {
1625
1868
  const error = await resp.json();
1626
- throw new chunkMOH4FRJZ_cjs.AgentError(`\uBC1C\uC2E0 \uC2E4\uD328 (${resp.status}): ${error["error"] ?? ""}`);
1869
+ throw new chunkCOBPAA3N_cjs.AgentError(`\uBC1C\uC2E0 \uC2E4\uD328 (${resp.status}): ${error["error"] ?? ""}`);
1627
1870
  }
1628
1871
  const data = await resp.json();
1629
1872
  const callSession = new CallSession({
@@ -1802,6 +2045,7 @@ var ClawOpsAgent = class {
1802
2045
  },
1803
2046
  () => mediaWs.isConnected
1804
2047
  );
2048
+ session._transferFn = (params) => this._controlWs.requestTransfer(session.callId, params);
1805
2049
  const sessionHandler = this._session;
1806
2050
  if ("setToolRegistry" in sessionHandler && typeof sessionHandler.setToolRegistry === "function") {
1807
2051
  sessionHandler.setToolRegistry(sessionTools);
@@ -1815,6 +2059,9 @@ var ClawOpsAgent = class {
1815
2059
  if ("setLogger" in sessionHandler && typeof sessionHandler.setLogger === "function") {
1816
2060
  sessionHandler.setLogger(this._isPipelineSession ? this._pipelineLog : this._log);
1817
2061
  }
2062
+ if (this._holdAudioChunks && "setHoldAudio" in sessionHandler && typeof sessionHandler.setHoldAudio === "function") {
2063
+ sessionHandler.setHoldAudio(this._holdAudioChunks);
2064
+ }
1818
2065
  this._callSessions.set(session.callId, sessionHandler);
1819
2066
  mediaWs.onAudio((ulawAudio, _timestamp) => {
1820
2067
  if (sessionHandler) {
@@ -1916,10 +2163,27 @@ var SEND_DTMF = {
1916
2163
  required: ["digits"]
1917
2164
  }
1918
2165
  };
2166
+ var TRANSFER_CALL = {
2167
+ name: "transfer_call",
2168
+ description: "Transfer the current call to another phone number. Use for blind transfer (direct handoff) or warm transfer (with whisper message to the target).",
2169
+ parameters: {
2170
+ type: "object",
2171
+ properties: {
2172
+ to: { type: "string", description: "Phone number to transfer to" },
2173
+ mode: { type: "string", enum: ["blind", "warm"], description: "blind: direct transfer (default), warm: play whisper to target first" },
2174
+ after_transfer: { type: "string", enum: ["terminate", "return"], description: "terminate: end AI session (default), return: AI resumes after transfer ends" },
2175
+ whisper: { type: "string", description: "Message to speak to transfer target before connecting customer (warm mode only)" },
2176
+ caller_id: { type: "string", description: "Override caller ID for the transfer leg" },
2177
+ timeout: { type: "integer", description: "Seconds to wait for transfer target to answer (default 30)" }
2178
+ },
2179
+ required: ["to"]
2180
+ }
2181
+ };
1919
2182
  var TOOL_MAP = /* @__PURE__ */ new Map([
1920
2183
  ["hang_up" /* HANG_UP */, HANG_UP],
1921
2184
  ["collect_dtmf" /* COLLECT_DTMF */, COLLECT_DTMF],
1922
- ["send_dtmf" /* SEND_DTMF */, SEND_DTMF]
2185
+ ["send_dtmf" /* SEND_DTMF */, SEND_DTMF],
2186
+ ["transfer_call" /* TRANSFER_CALL */, TRANSFER_CALL]
1923
2187
  ]);
1924
2188
  var BUILTIN_TOOL_NAMES = new Set(
1925
2189
  Array.from(TOOL_MAP.values()).map((s) => s.name)
@@ -1992,6 +2256,21 @@ async function executeBuiltinTool(funcName, args, call) {
1992
2256
  return `Error: ${e}`;
1993
2257
  }
1994
2258
  }
2259
+ if (funcName === "transfer_call") {
2260
+ try {
2261
+ call.transfer(args["to"], {
2262
+ mode: args["mode"] ?? void 0,
2263
+ afterTransfer: args["after_transfer"] ?? void 0,
2264
+ whisper: args["whisper"] ?? void 0,
2265
+ callerId: args["caller_id"] ?? void 0,
2266
+ timeout: args["timeout"] ?? void 0
2267
+ }).catch(() => {
2268
+ });
2269
+ return JSON.stringify({ status: "transfer_initiated" });
2270
+ } catch (e) {
2271
+ return `Error: ${e}`;
2272
+ }
2273
+ }
1995
2274
  return null;
1996
2275
  }
1997
2276
 
@@ -2015,6 +2294,7 @@ var PipelineSession = class {
2015
2294
  _running = false;
2016
2295
  _speaking = false;
2017
2296
  _builtinTools = null;
2297
+ _holdAudioChunks = null;
2018
2298
  _log = NOOP_LOGGER;
2019
2299
  constructor(options) {
2020
2300
  this._stt = options.stt;
@@ -2039,6 +2319,10 @@ var PipelineSession = class {
2039
2319
  setBuiltinTools(tools) {
2040
2320
  this._builtinTools = tools;
2041
2321
  }
2322
+ /** Tool 실행 중 재생할 hold audio 청크를 설정한다. */
2323
+ setHoldAudio(chunks) {
2324
+ this._holdAudioChunks = chunks;
2325
+ }
2042
2326
  getTelemetry() {
2043
2327
  const llm = this._llm;
2044
2328
  const stt = this._stt;
@@ -2204,7 +2488,14 @@ var PipelineSession = class {
2204
2488
  }
2205
2489
  if (!this._tools) return;
2206
2490
  this._callSession?.recordToolCall();
2207
- const result = await this._tools.call(name, args);
2491
+ const player = this._holdAudioChunks && this._callSession ? new HoldAudioPlayer(this._callSession, this._holdAudioChunks) : null;
2492
+ player?.start();
2493
+ let result;
2494
+ try {
2495
+ result = await this._tools.call(name, args);
2496
+ } finally {
2497
+ player?.stop();
2498
+ }
2208
2499
  this._conversation.push({
2209
2500
  role: "assistant",
2210
2501
  content: ""
@@ -2316,6 +2607,8 @@ var OpenAIRealtime = class {
2316
2607
  // Response state tracking — prevent sending response.create while one is active
2317
2608
  _responseInProgress = false;
2318
2609
  _onResponseDone = null;
2610
+ // Hold audio — tool 실행 중 대기음
2611
+ _holdAudioChunks = null;
2319
2612
  constructor(options = {}) {
2320
2613
  this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
2321
2614
  this._systemPrompt = options.systemPrompt ?? "";
@@ -2333,6 +2626,10 @@ var OpenAIRealtime = class {
2333
2626
  setRecorder(recorder) {
2334
2627
  this._recorder = recorder;
2335
2628
  }
2629
+ /** Tool 실행 중 재생할 hold audio 청크를 설정한다. */
2630
+ setHoldAudio(chunks) {
2631
+ this._holdAudioChunks = chunks;
2632
+ }
2336
2633
  async start(callSession, tools) {
2337
2634
  this._call = callSession;
2338
2635
  if (tools) this._tools = tools;
@@ -2588,6 +2885,8 @@ var OpenAIRealtime = class {
2588
2885
  return;
2589
2886
  }
2590
2887
  let result;
2888
+ const player = this._holdAudioChunks && this._call ? new HoldAudioPlayer(this._call, this._holdAudioChunks) : null;
2889
+ player?.start();
2591
2890
  try {
2592
2891
  const args = JSON.parse(item["arguments"] ?? "{}");
2593
2892
  this._call?.recordToolCall();
@@ -2598,6 +2897,8 @@ var OpenAIRealtime = class {
2598
2897
  this._call?.recordToolError(err);
2599
2898
  }
2600
2899
  result = `Error: ${err}`;
2900
+ } finally {
2901
+ player?.stop();
2601
2902
  }
2602
2903
  if (controller.signal.aborted) {
2603
2904
  this._log.info("Tool call cancelled (user interrupted): %s", funcName);
@@ -2705,7 +3006,7 @@ function sanitizeSchemaForGemini(schema, defs, depth = 0) {
2705
3006
  if (result["type"] === "object" && !result["properties"]) result["properties"] = {};
2706
3007
  return result;
2707
3008
  }
2708
- var GeminiRealtime = class {
3009
+ var GeminiRealtime = class _GeminiRealtime {
2709
3010
  _apiKey;
2710
3011
  _systemPrompt;
2711
3012
  _model;
@@ -2721,12 +3022,15 @@ var GeminiRealtime = class {
2721
3022
  _sentAudioChunks = 0;
2722
3023
  _audioRemainder = Buffer.alloc(0);
2723
3024
  _builtinTools = null;
2724
- _toolCallInProgress = false;
3025
+ _pendingToolCall = null;
3026
+ _toolDrainTimer = null;
3027
+ _lastAudioTime = 0;
3028
+ _holdAudioChunks = null;
2725
3029
  _log = NOOP_LOGGER;
2726
3030
  constructor(options = {}) {
2727
3031
  this._apiKey = options.apiKey ?? process.env["GOOGLE_API_KEY"] ?? "";
2728
3032
  this._systemPrompt = options.systemPrompt ?? "";
2729
- this._model = options.model ?? "gemini-2.5-flash-native-audio-preview-12-2025";
3033
+ this._model = options.model ?? "gemini-3.1-flash-live-preview";
2730
3034
  this._voice = options.voice ?? "Kore";
2731
3035
  this._language = options.language ?? "ko";
2732
3036
  this._greeting = options.greeting ?? true;
@@ -2742,6 +3046,10 @@ var GeminiRealtime = class {
2742
3046
  setBuiltinTools(tools) {
2743
3047
  this._builtinTools = tools;
2744
3048
  }
3049
+ /** Tool 실행 중 재생할 hold audio 청크를 설정한다. */
3050
+ setHoldAudio(chunks) {
3051
+ this._holdAudioChunks = chunks;
3052
+ }
2745
3053
  setLogger(logger) {
2746
3054
  this._log = logger;
2747
3055
  }
@@ -2784,7 +3092,9 @@ var GeminiRealtime = class {
2784
3092
  outputAudioTranscription: {}
2785
3093
  };
2786
3094
  if (this._systemPrompt) {
2787
- config["systemInstruction"] = this._systemPrompt;
3095
+ config["systemInstruction"] = {
3096
+ parts: [{ text: this._systemPrompt }]
3097
+ };
2788
3098
  }
2789
3099
  const toolSchemas = this._buildToolSchemas();
2790
3100
  if (toolSchemas.length > 0) {
@@ -2799,25 +3109,20 @@ var GeminiRealtime = class {
2799
3109
  this._log.error({ err }, "Gemini SDK error");
2800
3110
  },
2801
3111
  onclose: (ev) => {
2802
- this._log.info({ code: ev?.code ?? "unknown" }, "Gemini connection closed");
3112
+ this._log.info(
3113
+ { code: ev?.code ?? "unknown" },
3114
+ "Gemini connection closed"
3115
+ );
2803
3116
  this._closed = true;
2804
3117
  }
2805
3118
  }
2806
3119
  });
2807
3120
  if (this._greeting) {
2808
- this._session.sendClientContent({
2809
- turns: [
2810
- {
2811
- role: "user",
2812
- parts: [{ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." }]
2813
- }
2814
- ],
2815
- turnComplete: true
2816
- });
3121
+ this._session.sendRealtimeInput({ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." });
2817
3122
  }
2818
3123
  }
2819
3124
  feedAudio(audio) {
2820
- if (this._session && !this._closed && !this._toolCallInProgress) {
3125
+ if (this._session && !this._closed) {
2821
3126
  const pcm8k = ulawToPcm16(audio);
2822
3127
  if (this._recorder) {
2823
3128
  this._recorder.writeInbound(pcm8k);
@@ -2833,14 +3138,16 @@ var GeminiRealtime = class {
2833
3138
  }
2834
3139
  async feedDtmf(digits) {
2835
3140
  if (this._session) {
2836
- this._session.sendClientContent({
2837
- turns: [{ role: "user", parts: [{ text: `[DTMF \uC785\uB825: ${digits}]` }] }],
2838
- turnComplete: true
2839
- });
3141
+ this._session.sendRealtimeInput({ text: `[DTMF \uC785\uB825: ${digits}]` });
2840
3142
  }
2841
3143
  }
2842
3144
  async stop() {
2843
3145
  this._closed = true;
3146
+ if (this._toolDrainTimer) {
3147
+ clearTimeout(this._toolDrainTimer);
3148
+ this._toolDrainTimer = null;
3149
+ }
3150
+ this._pendingToolCall = null;
2844
3151
  if (this._session) {
2845
3152
  try {
2846
3153
  this._session.close();
@@ -2872,6 +3179,7 @@ var GeminiRealtime = class {
2872
3179
  const mimeType = inlineData.mimeType ?? "";
2873
3180
  if (mimeType.includes("audio")) {
2874
3181
  this._handleAudioData(inlineData.data);
3182
+ if (this._pendingToolCall) this._lastAudioTime = Date.now();
2875
3183
  }
2876
3184
  }
2877
3185
  }
@@ -2900,11 +3208,17 @@ var GeminiRealtime = class {
2900
3208
  }
2901
3209
  }
2902
3210
  if (msg.toolCall) {
2903
- this._handleToolCall(msg.toolCall);
3211
+ this._pendingToolCall = msg.toolCall;
3212
+ this._scheduleToolExecution();
2904
3213
  }
2905
3214
  const toolCancellation = msg["toolCallCancellation"];
2906
3215
  if (toolCancellation) {
2907
3216
  this._log.info({ ids: toolCancellation.ids }, "Tool call cancelled");
3217
+ this._pendingToolCall = null;
3218
+ if (this._toolDrainTimer) {
3219
+ clearTimeout(this._toolDrainTimer);
3220
+ this._toolDrainTimer = null;
3221
+ }
2908
3222
  }
2909
3223
  }
2910
3224
  _handleAudioData(b64Data) {
@@ -2935,54 +3249,81 @@ var GeminiRealtime = class {
2935
3249
  this._audioRemainder = Buffer.alloc(0);
2936
3250
  }
2937
3251
  }
3252
+ // Gemini는 tool_call 후에도 오디오를 계속 보내므로, 이 시간 내 응답이 없으면 drain 완료로 간주
3253
+ static TOOL_DRAIN_TIMEOUT = 300;
3254
+ _scheduleToolExecution() {
3255
+ if (this._toolDrainTimer) return;
3256
+ this._lastAudioTime = Date.now();
3257
+ this._toolDrainTimer = setTimeout(() => {
3258
+ this._toolDrainTimer = null;
3259
+ if (Date.now() - this._lastAudioTime < _GeminiRealtime.TOOL_DRAIN_TIMEOUT) {
3260
+ this._scheduleToolExecution();
3261
+ return;
3262
+ }
3263
+ if (this._pendingToolCall) {
3264
+ const tc = this._pendingToolCall;
3265
+ this._pendingToolCall = null;
3266
+ this._handleToolCall(tc);
3267
+ }
3268
+ }, _GeminiRealtime.TOOL_DRAIN_TIMEOUT);
3269
+ }
2938
3270
  async _handleToolCall(toolCall) {
2939
3271
  const functionCalls = toolCall.functionCalls;
2940
3272
  if (!functionCalls) return;
2941
- this._toolCallInProgress = true;
2942
3273
  const responses = [];
2943
- for (const fc of functionCalls) {
2944
- const name = fc.name ?? "";
2945
- const fcId = fc.id ?? "";
2946
- const args = fc.args ?? {};
2947
- this._log.info({ tool: name, args }, "Tool call: %s", name);
2948
- if (BUILTIN_TOOL_NAMES.has(name) && this._call) {
2949
- const result = await executeBuiltinTool(name, args, this._call);
2950
- if (result !== null) {
2951
- if (name === "hang_up") {
2952
- this._log.info("hang_up: ending call");
2953
- return;
3274
+ const player = this._holdAudioChunks && this._call ? new HoldAudioPlayer(this._call, this._holdAudioChunks) : null;
3275
+ player?.start();
3276
+ try {
3277
+ for (const fc of functionCalls) {
3278
+ const name = fc.name ?? "";
3279
+ const fcId = fc.id ?? "";
3280
+ const args = fc.args ?? {};
3281
+ this._log.info({ tool: name, args }, "Tool call: %s", name);
3282
+ if (BUILTIN_TOOL_NAMES.has(name) && this._call) {
3283
+ const result = await executeBuiltinTool(
3284
+ name,
3285
+ args,
3286
+ this._call
3287
+ );
3288
+ if (result !== null) {
3289
+ if (name === "hang_up") {
3290
+ this._log.info("hang_up: ending call");
3291
+ return;
3292
+ }
3293
+ this._log.info("Builtin tool result: %s -> %s", name, result);
3294
+ responses.push({ id: fcId, name, response: { result } });
3295
+ continue;
2954
3296
  }
2955
- this._log.info("Builtin tool result: %s -> %s", name, result);
2956
- responses.push({ id: fcId, name, response: { result } });
3297
+ }
3298
+ if (!this._tools || !this._tools.has(name)) {
3299
+ this._log.error("Unknown tool: %s", name);
3300
+ responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
2957
3301
  continue;
2958
3302
  }
2959
- }
2960
- if (!this._tools || !this._tools.has(name)) {
2961
- this._log.error("Unknown tool: %s", name);
2962
- responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
2963
- continue;
2964
- }
2965
- try {
2966
- this._call?.recordToolCall();
2967
- const result = await this._tools.call(name, args);
2968
- const resultStr = typeof result === "string" ? result : JSON.stringify(result);
2969
- this._log.info("Tool result: %s -> %s", name, resultStr.substring(0, 200));
2970
- responses.push({
2971
- id: fcId,
2972
- name,
2973
- response: { result: resultStr }
2974
- });
2975
- } catch (err) {
2976
- this._log.error({ err }, "Tool call failed: %s", name);
2977
- if (err instanceof Error) {
2978
- this._call?.recordToolError(err);
3303
+ try {
3304
+ this._call?.recordToolCall();
3305
+ const result = await this._tools.call(name, args);
3306
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
3307
+ this._log.info("Tool result: %s -> %s", name, resultStr.substring(0, 200));
3308
+ responses.push({
3309
+ id: fcId,
3310
+ name,
3311
+ response: { result: resultStr }
3312
+ });
3313
+ } catch (err) {
3314
+ this._log.error({ err }, "Tool call failed: %s", name);
3315
+ if (err instanceof Error) {
3316
+ this._call?.recordToolError(err);
3317
+ }
3318
+ responses.push({
3319
+ id: fcId,
3320
+ name,
3321
+ response: { error: String(err) }
3322
+ });
2979
3323
  }
2980
- responses.push({
2981
- id: fcId,
2982
- name,
2983
- response: { error: String(err) }
2984
- });
2985
3324
  }
3325
+ } finally {
3326
+ player?.stop();
2986
3327
  }
2987
3328
  if (responses.length > 0 && this._session) {
2988
3329
  this._log.debug("Sending %d tool response(s)", responses.length);
@@ -2990,7 +3331,6 @@ var GeminiRealtime = class {
2990
3331
  functionResponses: responses
2991
3332
  });
2992
3333
  }
2993
- this._toolCallInProgress = false;
2994
3334
  }
2995
3335
  };
2996
3336