@teamlearners/clawops 0.8.0 → 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.
- package/README.md +25 -0
- package/dist/agent/index.cjs +413 -73
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +40 -3
- package/dist/agent/index.d.ts +40 -3
- package/dist/agent/index.js +406 -66
- package/dist/agent/index.js.map +1 -1
- package/dist/{chunk-2B74QZZJ.js → chunk-5IY2ZR3F.js} +3 -3
- package/dist/chunk-5IY2ZR3F.js.map +1 -0
- package/dist/{chunk-R742GPVL.cjs → chunk-COBPAA3N.cjs} +3 -3
- package/dist/chunk-COBPAA3N.cjs.map +1 -0
- package/dist/index.cjs +35 -35
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-2B74QZZJ.js.map +0 -1
- package/dist/chunk-R742GPVL.cjs.map +0 -1
package/dist/agent/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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
|
|
939
|
-
fs__namespace.writeSync(this._fdMix,
|
|
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:
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
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"] =
|
|
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(
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
const
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
const
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
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
|
-
|
|
2956
|
-
|
|
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
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
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
|
|