@teamlearners/clawops 0.1.0 → 0.3.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.
@@ -324,13 +324,16 @@ function resamplePcm16(pcm, fromRate, toRate) {
324
324
  }
325
325
 
326
326
  // src/agent/control-ws.ts
327
- var DEFAULT_PATH = "/v1/agent/control";
328
327
  var INITIAL_RECONNECT_DELAY = 1e3;
329
328
  var MAX_RECONNECT_DELAY = 3e4;
330
329
  function buildControlWsUrl(options) {
331
- const base = options.baseUrl.replace(/^http/, "ws").replace(/\/$/, "");
332
- const path2 = options.path ?? DEFAULT_PATH;
333
- return `${base}${path2}?api_key=${encodeURIComponent(options.apiKey)}&agent_id=${encodeURIComponent(options.agentId)}`;
330
+ const scheme = options.baseUrl.startsWith("https") ? "wss" : "ws";
331
+ const host = options.baseUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
332
+ let url = `${scheme}://${host}/v1/accounts/${encodeURIComponent(options.accountId)}/agent/listen`;
333
+ if (options.number) {
334
+ url += `?number=${encodeURIComponent(options.number)}`;
335
+ }
336
+ return url;
334
337
  }
335
338
  var ControlWebSocket = class {
336
339
  constructor(_options) {
@@ -381,7 +384,12 @@ var ControlWebSocket = class {
381
384
  }
382
385
  async _doConnect() {
383
386
  const { WebSocket } = await import('ws');
384
- const ws = new WebSocket(this._url);
387
+ const ws = new WebSocket(this._url, {
388
+ followRedirects: true,
389
+ headers: {
390
+ Authorization: `Bearer ${this._options.apiKey}`
391
+ }
392
+ });
385
393
  this._ws = ws;
386
394
  ws.on("open", () => {
387
395
  this._reconnectDelay = INITIAL_RECONNECT_DELAY;
@@ -524,47 +532,37 @@ var MCPClient = class {
524
532
  // src/agent/media-ws.ts
525
533
  function parseStartEvent(data) {
526
534
  const start = data["start"];
535
+ const fmt = start["mediaFormat"] ?? {};
527
536
  return {
528
- streamSid: start["streamSid"] ?? data["streamSid"] ?? "",
529
- callSid: start["callSid"] ?? "",
530
- accountSid: start["accountSid"] ?? "",
531
- tracks: start["tracks"] ?? [],
532
- customParameters: start["customParameters"] ?? {},
533
- mediaFormat: start["mediaFormat"] ?? {
534
- encoding: "audio/x-mulaw",
535
- sampleRate: 8e3,
536
- channels: 1
537
- }
537
+ streamId: start["streamId"] ?? "",
538
+ callId: start["callId"] ?? "",
539
+ accountId: start["accountId"] ?? "",
540
+ sampleRate: fmt["sampleRate"] ?? 8e3
538
541
  };
539
542
  }
540
543
  function parseMediaEvent(data) {
541
544
  const media = data["media"];
542
545
  return {
543
- track: media["track"] ?? "inbound",
544
- chunk: media["chunk"] ?? media["payload"] ?? "",
545
- timestamp: media["timestamp"] ?? ""
546
+ audio: Buffer.from(media["payload"] ?? "", "base64"),
547
+ timestamp: parseInt(media["timestamp"] ?? "0", 10) || 0
546
548
  };
547
549
  }
548
- function buildMediaResponse(streamSid, payload) {
550
+ function buildMediaResponse(audioBase64) {
549
551
  return JSON.stringify({
550
552
  event: "media",
551
- streamSid,
552
553
  media: {
553
- payload
554
+ payload: audioBase64
554
555
  }
555
556
  });
556
557
  }
557
558
  var MediaWebSocket = class {
558
559
  _ws = null;
559
- _streamSid = null;
560
560
  _audioQueue = [];
561
561
  _sendLoopRunning = false;
562
562
  _closed = false;
563
563
  _onAudio = null;
564
564
  _onStart = null;
565
565
  _onClose = null;
566
- _markSeq = 0;
567
- _markResolves = /* @__PURE__ */ new Map();
568
566
  /** Set the handler for inbound audio data. */
569
567
  onAudio(handler) {
570
568
  this._onAudio = handler;
@@ -577,11 +575,16 @@ var MediaWebSocket = class {
577
575
  onClose(handler) {
578
576
  this._onClose = handler;
579
577
  }
580
- /** Connect to a media WebSocket URL. */
581
- async connect(url) {
578
+ /** Connect to a media WebSocket URL with Bearer authentication. */
579
+ async connect(url, apiKey) {
582
580
  const { WebSocket } = await import('ws');
583
581
  return new Promise((resolve, reject) => {
584
- const ws = new WebSocket(url);
582
+ const ws = new WebSocket(url, {
583
+ followRedirects: true,
584
+ headers: {
585
+ Authorization: `Bearer ${apiKey}`
586
+ }
587
+ });
585
588
  this._ws = ws;
586
589
  this._closed = false;
587
590
  ws.on("open", () => {
@@ -616,31 +619,21 @@ var MediaWebSocket = class {
616
619
  /** Clear all queued outbound audio. */
617
620
  sendClear() {
618
621
  this._audioQueue.length = 0;
619
- if (this._ws && this._streamSid && this._ws.readyState === 1) {
622
+ if (this._ws && this._ws.readyState === 1) {
623
+ this._ws.send(JSON.stringify({ event: "clear" }));
624
+ }
625
+ }
626
+ /** Send a mark event. */
627
+ sendMark(name) {
628
+ if (this._ws && this._ws.readyState === 1) {
620
629
  this._ws.send(
621
630
  JSON.stringify({
622
- event: "clear",
623
- streamSid: this._streamSid
631
+ event: "mark",
632
+ mark: { name }
624
633
  })
625
634
  );
626
635
  }
627
636
  }
628
- /** Send a mark event and return a promise that resolves when the mark is acknowledged. */
629
- sendMark() {
630
- const label = `mark_${++this._markSeq}`;
631
- return new Promise((resolve) => {
632
- this._markResolves.set(label, resolve);
633
- if (this._ws && this._streamSid && this._ws.readyState === 1) {
634
- this._ws.send(
635
- JSON.stringify({
636
- event: "mark",
637
- streamSid: this._streamSid,
638
- mark: { name: label }
639
- })
640
- );
641
- }
642
- });
643
- }
644
637
  /** Close the media WebSocket. */
645
638
  close() {
646
639
  this._closed = true;
@@ -648,17 +641,12 @@ var MediaWebSocket = class {
648
641
  this._ws.close();
649
642
  this._ws = null;
650
643
  }
651
- for (const resolve of this._markResolves.values()) {
652
- resolve();
653
- }
654
- this._markResolves.clear();
655
644
  }
656
645
  _handleMessage(msg) {
657
646
  const event = msg["event"];
658
647
  switch (event) {
659
648
  case "start": {
660
649
  const startEvt = parseStartEvent(msg);
661
- this._streamSid = startEvt.streamSid;
662
650
  if (this._onStart) {
663
651
  this._onStart(startEvt);
664
652
  }
@@ -666,18 +654,8 @@ var MediaWebSocket = class {
666
654
  }
667
655
  case "media": {
668
656
  const mediaEvt = parseMediaEvent(msg);
669
- if (mediaEvt.track === "inbound" && this._onAudio) {
670
- const audioBuf = Buffer.from(mediaEvt.chunk, "base64");
671
- this._onAudio(audioBuf);
672
- }
673
- break;
674
- }
675
- case "mark": {
676
- const mark = msg["mark"];
677
- const name = mark?.["name"];
678
- if (name && this._markResolves.has(name)) {
679
- this._markResolves.get(name)();
680
- this._markResolves.delete(name);
657
+ if (this._onAudio) {
658
+ this._onAudio(mediaEvt.audio, mediaEvt.timestamp);
681
659
  }
682
660
  break;
683
661
  }
@@ -697,98 +675,162 @@ var MediaWebSocket = class {
697
675
  }
698
676
  while (this._audioQueue.length > 0 && this._ws && this._ws.readyState === 1) {
699
677
  const payload = this._audioQueue.shift();
700
- if (this._streamSid) {
701
- this._ws.send(buildMediaResponse(this._streamSid, payload));
702
- }
678
+ this._ws.send(buildMediaResponse(payload));
703
679
  }
704
680
  setTimeout(loop, 20);
705
681
  };
706
682
  loop();
707
683
  }
708
684
  };
709
- var WAV_SAMPLE_RATE = 8e3;
710
- var WAV_CHANNELS = 1;
711
- var WAV_BITS_PER_SAMPLE = 16;
712
- function makeWavHeader(dataSize) {
685
+ var SAMPLE_RATE = 8e3;
686
+ var CHANNELS = 1;
687
+ var BITS_PER_SAMPLE = 16;
688
+ var BYTES_PER_SECOND = SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8);
689
+ function makeWavHeader(dataSize = 0) {
713
690
  const header = Buffer.alloc(44);
714
- const byteRate = WAV_SAMPLE_RATE * WAV_CHANNELS * (WAV_BITS_PER_SAMPLE / 8);
715
- const blockAlign = WAV_CHANNELS * (WAV_BITS_PER_SAMPLE / 8);
716
- header.write("RIFF", 0);
691
+ header.write("RIFF", 0, "ascii");
717
692
  header.writeUInt32LE(36 + dataSize, 4);
718
- header.write("WAVE", 8);
719
- header.write("fmt ", 12);
693
+ header.write("WAVE", 8, "ascii");
694
+ header.write("fmt ", 12, "ascii");
720
695
  header.writeUInt32LE(16, 16);
721
696
  header.writeUInt16LE(1, 20);
722
- header.writeUInt16LE(WAV_CHANNELS, 22);
723
- header.writeUInt32LE(WAV_SAMPLE_RATE, 24);
724
- header.writeUInt32LE(byteRate, 28);
725
- header.writeUInt16LE(blockAlign, 32);
726
- header.writeUInt16LE(WAV_BITS_PER_SAMPLE, 34);
727
- header.write("data", 36);
697
+ header.writeUInt16LE(CHANNELS, 22);
698
+ header.writeUInt32LE(SAMPLE_RATE, 24);
699
+ header.writeUInt32LE(SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8), 28);
700
+ header.writeUInt16LE(CHANNELS * (BITS_PER_SAMPLE / 8), 32);
701
+ header.writeUInt16LE(BITS_PER_SAMPLE, 34);
702
+ header.write("data", 36, "ascii");
728
703
  header.writeUInt32LE(dataSize, 40);
729
704
  return header;
730
705
  }
706
+ function mixSamples(a, b) {
707
+ const n = Math.min(a.length, b.length) >> 1;
708
+ const result = Buffer.alloc(n * 2);
709
+ for (let i = 0; i < n; i++) {
710
+ const sa = a.readInt16LE(i * 2);
711
+ const sb = b.readInt16LE(i * 2);
712
+ result.writeInt16LE(Math.max(-32768, Math.min(32767, sa + sb)), i * 2);
713
+ }
714
+ return result;
715
+ }
731
716
  var AudioRecorder = class {
732
- _outputDir;
733
- _sampleRate;
734
- _inboundChunks = [];
735
- _outboundChunks = [];
736
- _callId = null;
717
+ _dir;
718
+ _fdIn = null;
719
+ _fdOut = null;
720
+ _fdMix = null;
721
+ _inWritten = 0;
722
+ _outWritten = 0;
723
+ _mixWritten = 0;
724
+ _startTime = 0;
737
725
  _started = false;
738
- constructor(options) {
739
- this._outputDir = options.outputDir;
740
- this._sampleRate = options.sampleRate ?? WAV_SAMPLE_RATE;
741
- }
742
- /** Start recording for a call. */
743
- start(callId) {
744
- this._callId = callId;
745
- this._inboundChunks = [];
746
- this._outboundChunks = [];
726
+ constructor(recordingPath, callId) {
727
+ this._dir = path.join(recordingPath, callId);
728
+ }
729
+ start() {
730
+ fs.mkdirSync(this._dir, { recursive: true });
731
+ const header = makeWavHeader();
732
+ this._fdIn = fs.openSync(path.join(this._dir, "in.wav"), "w");
733
+ this._fdOut = fs.openSync(path.join(this._dir, "out.wav"), "w");
734
+ this._fdMix = fs.openSync(path.join(this._dir, "mix.wav"), "w+");
735
+ fs.writeSync(this._fdIn, header);
736
+ fs.writeSync(this._fdOut, header);
737
+ fs.writeSync(this._fdMix, header);
738
+ this._startTime = performance.now();
747
739
  this._started = true;
748
- if (!fs.existsSync(this._outputDir)) {
749
- fs.mkdirSync(this._outputDir, { recursive: true });
740
+ }
741
+ _expectedBytes() {
742
+ const elapsed = (performance.now() - this._startTime) / 1e3;
743
+ return Math.floor(elapsed * BYTES_PER_SECOND);
744
+ }
745
+ _padSilence(fd, written) {
746
+ const expected = this._expectedBytes();
747
+ let gap = expected - written;
748
+ if (gap <= 0) return 0;
749
+ gap = gap - gap % 2;
750
+ if (gap > 0) {
751
+ fs.writeSync(fd, Buffer.alloc(gap));
752
+ }
753
+ return gap;
754
+ }
755
+ _writeToMix(data, trackPos) {
756
+ if (this._fdMix === null) return;
757
+ const filePos = 44 + trackPos;
758
+ if (trackPos < this._mixWritten) {
759
+ const overlap = Math.min(data.length, this._mixWritten - trackPos);
760
+ const existing = Buffer.alloc(overlap);
761
+ fs.readSync(this._fdMix, existing, 0, overlap, filePos);
762
+ const mixed = mixSamples(existing, data.subarray(0, overlap));
763
+ fs.writeSync(this._fdMix, mixed, 0, mixed.length, filePos);
764
+ if (data.length > overlap) {
765
+ fs.writeSync(this._fdMix, data, overlap, data.length - overlap, filePos + overlap);
766
+ this._mixWritten = trackPos + data.length;
767
+ }
768
+ } else {
769
+ if (trackPos > this._mixWritten) {
770
+ let gap = trackPos - this._mixWritten;
771
+ gap = gap - gap % 2;
772
+ if (gap > 0) {
773
+ const silence = Buffer.alloc(gap);
774
+ fs.writeSync(this._fdMix, silence, 0, silence.length, 44 + this._mixWritten);
775
+ this._mixWritten += gap;
776
+ }
777
+ }
778
+ fs.writeSync(this._fdMix, data, 0, data.length, 44 + this._mixWritten);
779
+ this._mixWritten += data.length;
750
780
  }
751
781
  }
752
- /** Write inbound (caller) PCM16 audio. */
753
- writeInbound(pcm) {
754
- if (this._started) {
755
- this._inboundChunks.push(Buffer.from(pcm));
782
+ writeInbound(pcm16_8k) {
783
+ if (!this._started || this._fdIn === null) return;
784
+ try {
785
+ const gap = this._padSilence(this._fdIn, this._inWritten);
786
+ this._inWritten += gap;
787
+ const posBefore = this._inWritten;
788
+ fs.writeSync(this._fdIn, pcm16_8k);
789
+ this._inWritten += pcm16_8k.length;
790
+ this._writeToMix(pcm16_8k, posBefore);
791
+ } catch (err) {
792
+ console.error("Error writing inbound audio:", err);
756
793
  }
757
794
  }
758
- /** Write raw outbound (agent) PCM16 audio. */
759
- writeRawOutbound(pcm) {
760
- if (this._started) {
761
- this._outboundChunks.push(Buffer.from(pcm));
795
+ writeOutbound(pcm16_8k) {
796
+ if (!this._started || this._fdOut === null) return;
797
+ try {
798
+ const gap = this._padSilence(this._fdOut, this._outWritten);
799
+ this._outWritten += gap;
800
+ const posBefore = this._outWritten;
801
+ fs.writeSync(this._fdOut, pcm16_8k);
802
+ this._outWritten += pcm16_8k.length;
803
+ this._writeToMix(pcm16_8k, posBefore);
804
+ } catch (err) {
805
+ console.error("Error writing outbound audio:", err);
762
806
  }
763
807
  }
764
- /** Stop recording and write WAV files to disk. Returns file paths. */
765
808
  stop() {
766
- if (!this._started || !this._callId) {
767
- return {};
768
- }
769
- this._started = false;
770
- const result = {};
771
- if (this._inboundChunks.length > 0) {
772
- const inboundPath = path.join(this._outputDir, `${this._callId}_inbound.wav`);
773
- this._writeWav(inboundPath, this._inboundChunks);
774
- result.inbound = inboundPath;
775
- }
776
- if (this._outboundChunks.length > 0) {
777
- const outboundPath = path.join(this._outputDir, `${this._callId}_outbound.wav`);
778
- this._writeWav(outboundPath, this._outboundChunks);
779
- result.outbound = outboundPath;
780
- }
781
- this._inboundChunks = [];
782
- this._outboundChunks = [];
783
- return result;
784
- }
785
- _writeWav(filePath, chunks) {
786
- const pcmData = Buffer.concat(chunks);
787
- const header = makeWavHeader(pcmData.length);
788
- const fd = fs.openSync(filePath, "w");
789
- fs.writeSync(fd, header);
790
- fs.writeSync(fd, pcmData);
791
- fs.closeSync(fd);
809
+ if (!this._started) return;
810
+ try {
811
+ let maxWritten = Math.max(this._inWritten, this._outWritten, this._mixWritten);
812
+ maxWritten = maxWritten & ~1;
813
+ for (const [fd, written] of [
814
+ [this._fdIn, this._inWritten],
815
+ [this._fdOut, this._outWritten],
816
+ [this._fdMix, this._mixWritten]
817
+ ]) {
818
+ if (fd === null) continue;
819
+ const pad = maxWritten - written;
820
+ if (pad > 0) {
821
+ fs.writeSync(fd, Buffer.alloc(pad), 0, pad, 44 + written);
822
+ }
823
+ fs.writeSync(fd, makeWavHeader(maxWritten), 0, 44, 0);
824
+ fs.closeSync(fd);
825
+ }
826
+ } catch (err) {
827
+ console.error("Error stopping recorder:", err);
828
+ } finally {
829
+ this._fdIn = null;
830
+ this._fdOut = null;
831
+ this._fdMix = null;
832
+ this._started = false;
833
+ }
792
834
  }
793
835
  };
794
836
 
@@ -868,23 +910,23 @@ var CallSession = class {
868
910
  /** Mark the session as ended (called internally). */
869
911
  _markEnded() {
870
912
  this._status = "ended";
871
- this._emit({ type: "ended" });
872
913
  this._resolveEnded();
873
914
  }
874
- /** Emit an event to registered handlers. */
875
- _emit(event) {
876
- const handlers = this._handlers.get(event.type);
915
+ /** Emit an event to registered handlers. Matches Python SDK: _emit(event, ...args) */
916
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
917
+ _emit(event, ...args) {
918
+ const handlers = this._handlers.get(event);
877
919
  if (handlers) {
878
920
  for (const handler of handlers) {
879
921
  try {
880
- const result = handler(event);
922
+ const result = handler(this, ...args);
881
923
  if (result && typeof result.catch === "function") {
882
924
  result.catch((err) => {
883
- console.error(`[CallSession] Error in ${event.type} handler:`, err);
925
+ console.error(`[CallSession] Error in ${event} handler:`, err);
884
926
  });
885
927
  }
886
928
  } catch (err) {
887
- console.error(`[CallSession] Error in ${event.type} handler:`, err);
929
+ console.error(`[CallSession] Error in ${event} handler:`, err);
888
930
  }
889
931
  }
890
932
  }
@@ -1088,44 +1130,61 @@ var ATTR_AGENT_ID = "clawops.agent.id";
1088
1130
  // src/agent/agent.ts
1089
1131
  var ClawOpsAgent = class {
1090
1132
  _apiKey;
1091
- _agentId;
1133
+ _accountId;
1092
1134
  _baseUrl;
1093
- _sessionFactory = null;
1135
+ _fromNumber;
1136
+ _session;
1094
1137
  _tools = new ToolRegistry();
1095
1138
  _handlers = /* @__PURE__ */ new Map();
1096
1139
  _controlWs = null;
1097
- _mcpClient = null;
1098
- _recordingDir;
1140
+ _mcpServers;
1141
+ _recording;
1142
+ _recordingPath;
1099
1143
  _activeSessions = /* @__PURE__ */ new Map();
1100
- constructor(options = {}) {
1144
+ constructor(options) {
1101
1145
  this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
1102
- this._agentId = options.agentId ?? process.env["CLAWOPS_AGENT_ID"] ?? "";
1146
+ this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
1103
1147
  this._baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
1104
- this._recordingDir = options.recordingDir ?? null;
1105
- if (options.session) {
1106
- if (typeof options.session === "function") {
1107
- this._sessionFactory = options.session;
1108
- } else {
1109
- const sessionInstance = options.session;
1110
- this._sessionFactory = () => sessionInstance;
1111
- }
1112
- }
1113
- if (options.mcpServers) {
1114
- this._mcpClient = new MCPClient();
1115
- for (const [name, config] of Object.entries(options.mcpServers)) {
1116
- this._mcpClient.addServer(name, config);
1117
- }
1118
- }
1148
+ this._fromNumber = options.from;
1149
+ this._session = options.session;
1150
+ this._recording = options.recording ?? false;
1151
+ this._recordingPath = options.recordingPath ?? "./recordings";
1152
+ this._mcpServers = options.mcpServers ?? [];
1119
1153
  if (options.tracing) {
1120
1154
  setTracingConfig(options.tracing);
1121
1155
  }
1122
1156
  }
1123
- /** Register a function tool. */
1124
- tool(tool) {
1125
- this._tools.register(tool);
1157
+ /**
1158
+ * Register a function tool.
1159
+ *
1160
+ * Supports two signatures (matching Python SDK):
1161
+ * agent.tool(name, description, parameters, handler)
1162
+ * agent.tool(functionToolObject)
1163
+ */
1164
+ tool(nameOrTool, description, parameters, handler) {
1165
+ if (typeof nameOrTool === "string") {
1166
+ if (!description || !parameters || !handler) {
1167
+ throw new AgentError("tool(name, description, parameters, handler) requires all arguments.");
1168
+ }
1169
+ this._tools.register({
1170
+ name: nameOrTool,
1171
+ description,
1172
+ parameters,
1173
+ required: Object.keys(parameters),
1174
+ handler
1175
+ });
1176
+ } else {
1177
+ this._tools.register(nameOrTool);
1178
+ }
1126
1179
  return this;
1127
1180
  }
1128
- /** Register an event handler. */
1181
+ /**
1182
+ * Register an event handler.
1183
+ *
1184
+ * Matches Python SDK decorator style:
1185
+ * agent.on("call_start", (call) => { ... })
1186
+ * agent.on("transcript", (call, role, text) => { ... })
1187
+ */
1129
1188
  on(event, handler) {
1130
1189
  let list = this._handlers.get(event);
1131
1190
  if (!list) {
@@ -1140,21 +1199,14 @@ var ClawOpsAgent = class {
1140
1199
  if (!this._apiKey) {
1141
1200
  throw new AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1142
1201
  }
1143
- if (!this._agentId) {
1144
- throw new AgentError("Agent ID is required. Set CLAWOPS_AGENT_ID or pass agentId option.");
1145
- }
1146
- if (this._mcpClient) {
1147
- try {
1148
- const mcpTools = await this._mcpClient.connect();
1149
- this._tools.registerMcpTools(mcpTools);
1150
- } catch (err) {
1151
- console.error("[ClawOpsAgent] MCP connection error:", err);
1152
- }
1202
+ if (!this._accountId) {
1203
+ throw new AgentError("Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option.");
1153
1204
  }
1154
1205
  this._controlWs = new ControlWebSocket({
1155
1206
  baseUrl: this._baseUrl,
1156
1207
  apiKey: this._apiKey,
1157
- agentId: this._agentId
1208
+ accountId: this._accountId,
1209
+ number: this._fromNumber
1158
1210
  });
1159
1211
  this._controlWs.on("call.incoming", (event) => this._handleIncoming(event));
1160
1212
  this._controlWs.on("call.ended", (event) => this._handleEnded(event));
@@ -1169,6 +1221,7 @@ var ClawOpsAgent = class {
1169
1221
  `Failed to connect to ClawOps: ${err instanceof Error ? err.message : String(err)}`
1170
1222
  );
1171
1223
  }
1224
+ console.log(`[ClawOpsAgent] Connected on ${this._fromNumber}`);
1172
1225
  }
1173
1226
  /**
1174
1227
  * Connect and block until disconnected.
@@ -1194,83 +1247,118 @@ var ClawOpsAgent = class {
1194
1247
  session._markEnded();
1195
1248
  }
1196
1249
  this._activeSessions.clear();
1197
- if (this._mcpClient) {
1198
- await this._mcpClient.disconnect();
1199
- this._tools.clearMcpTools();
1200
- }
1250
+ console.log("[ClawOpsAgent] Disconnected");
1201
1251
  }
1202
1252
  /**
1203
1253
  * Initiate an outbound call.
1254
+ * Matches Python SDK: agent.call(to, { timeout })
1204
1255
  */
1205
- call(options) {
1206
- if (!this._controlWs) {
1207
- throw new AgentError("Agent is not connected. Call connect() first.");
1208
- }
1209
- this._controlWs.send({
1210
- action: "call.create",
1211
- data: {
1212
- to_number: options.to,
1213
- from_number: options.from,
1214
- metadata: options.metadata
1215
- }
1256
+ async call(to, options) {
1257
+ await this.connect();
1258
+ const url = `${this._baseUrl}/v1/accounts/${this._accountId}/calls`;
1259
+ const body = { To: to, From: this._fromNumber, Timeout: options?.timeout ?? 60 };
1260
+ const resp = await fetch(url, {
1261
+ method: "POST",
1262
+ headers: {
1263
+ Authorization: `Bearer ${this._apiKey}`,
1264
+ "Content-Type": "application/json"
1265
+ },
1266
+ body: JSON.stringify(body)
1267
+ });
1268
+ if (resp.status !== 201) {
1269
+ const error = await resp.json();
1270
+ throw new AgentError(`\uBC1C\uC2E0 \uC2E4\uD328 (${resp.status}): ${error["error"] ?? ""}`);
1271
+ }
1272
+ const data = await resp.json();
1273
+ const callSession = new CallSession({
1274
+ callId: data["callId"],
1275
+ fromNumber: this._fromNumber,
1276
+ toNumber: to,
1277
+ accountId: this._accountId,
1278
+ direction: "outbound"
1216
1279
  });
1280
+ for (const [evt, handlers] of this._handlers) {
1281
+ for (const handler of handlers) {
1282
+ callSession.on(evt, handler);
1283
+ }
1284
+ }
1285
+ this._activeSessions.set(callSession.callId, callSession);
1286
+ console.log(`[ClawOpsAgent] Outbound call initiated: ${this._fromNumber} -> ${to} (${callSession.callId})`);
1287
+ return callSession;
1217
1288
  }
1218
1289
  _handleIncoming(event) {
1219
- const data = event.data;
1290
+ const callId = event["callId"];
1291
+ const fromNumber = event["from"] ?? "";
1292
+ const mediaUrl = event["mediaUrl"] ?? "";
1220
1293
  const session = new CallSession({
1221
- callId: data.call_id,
1222
- fromNumber: data.from_number ?? "",
1223
- toNumber: data.to_number ?? "",
1224
- accountId: data.account_id ?? "",
1225
- direction: data.direction ?? "inbound",
1226
- metadata: data.metadata
1294
+ callId,
1295
+ fromNumber,
1296
+ toNumber: this._fromNumber,
1297
+ accountId: this._accountId,
1298
+ direction: "inbound"
1227
1299
  });
1228
- this._activeSessions.set(data.call_id, session);
1229
- this._emitEvent("call.incoming", session);
1230
- if (data.media_ws_url) {
1231
- this._startCallSession(session, data.media_ws_url).catch((err) => {
1232
- console.error(`[ClawOpsAgent] Error in call session ${data.call_id}:`, err);
1300
+ for (const [evt, handlers] of this._handlers) {
1301
+ for (const handler of handlers) {
1302
+ session.on(evt, handler);
1303
+ }
1304
+ }
1305
+ this._activeSessions.set(callId, session);
1306
+ if (this._controlWs) {
1307
+ this._controlWs.send({ event: "call.accept", callId });
1308
+ }
1309
+ if (mediaUrl) {
1310
+ this._startCallSession(session, mediaUrl).catch((err) => {
1311
+ console.error(`[ClawOpsAgent] Error in call session ${callId}:`, err);
1233
1312
  });
1234
1313
  }
1235
1314
  }
1236
1315
  _handleEnded(event) {
1237
- const session = this._activeSessions.get(event.data.call_id);
1316
+ const callId = event["callId"];
1317
+ const session = this._activeSessions.get(callId);
1238
1318
  if (session) {
1239
1319
  session._markEnded();
1240
- this._activeSessions.delete(event.data.call_id);
1241
- this._emitEvent("call.ended", session);
1320
+ this._activeSessions.delete(callId);
1242
1321
  }
1243
1322
  }
1244
1323
  _handleOutboundReady(event) {
1245
- const data = event.data;
1246
- const session = new CallSession({
1247
- callId: data.call_id,
1248
- fromNumber: data.from_number ?? "",
1249
- toNumber: data.to_number ?? "",
1250
- accountId: data.account_id ?? "",
1251
- direction: "outbound",
1252
- metadata: data.metadata
1253
- });
1254
- this._activeSessions.set(data.call_id, session);
1255
- this._emitEvent("call.outbound_ready", session);
1256
- if (data.media_ws_url) {
1257
- this._startCallSession(session, data.media_ws_url).catch((err) => {
1258
- console.error(`[ClawOpsAgent] Error in call session ${data.call_id}:`, err);
1324
+ const callId = event["callId"];
1325
+ const mediaUrl = event["mediaUrl"] ?? "";
1326
+ let session = this._activeSessions.get(callId);
1327
+ if (!session) {
1328
+ session = new CallSession({
1329
+ callId,
1330
+ fromNumber: this._fromNumber,
1331
+ toNumber: event["to"] ?? "",
1332
+ accountId: this._accountId,
1333
+ direction: "outbound"
1334
+ });
1335
+ for (const [evt, handlers] of this._handlers) {
1336
+ for (const handler of handlers) {
1337
+ session.on(evt, handler);
1338
+ }
1339
+ }
1340
+ this._activeSessions.set(callId, session);
1341
+ }
1342
+ if (mediaUrl) {
1343
+ this._startCallSession(session, mediaUrl).catch((err) => {
1344
+ console.error(`[ClawOpsAgent] Error in call session ${callId}:`, err);
1259
1345
  });
1260
1346
  }
1261
1347
  }
1262
1348
  _handleRinging(event) {
1263
- const session = this._activeSessions.get(event.data.call_id);
1349
+ const callId = event["callId"];
1350
+ const session = this._activeSessions.get(callId);
1264
1351
  if (session) {
1265
- this._emitEvent("call.ringing", session);
1352
+ console.log(`[ClawOpsAgent] Outbound call ringing: ${callId}`);
1266
1353
  }
1267
1354
  }
1268
1355
  _handleFailed(event) {
1269
- const session = this._activeSessions.get(event.data.call_id);
1356
+ const callId = event["callId"];
1357
+ const session = this._activeSessions.get(callId);
1270
1358
  if (session) {
1359
+ session._emit("call_failed", event["reason"] ?? "failed");
1271
1360
  session._markEnded();
1272
- this._activeSessions.delete(event.data.call_id);
1273
- this._emitEvent("call.failed", session);
1361
+ this._activeSessions.delete(callId);
1274
1362
  }
1275
1363
  }
1276
1364
  async _startCallSession(session, mediaWsUrl) {
@@ -1279,23 +1367,33 @@ var ClawOpsAgent = class {
1279
1367
  {
1280
1368
  [ATTR_CALL_ID]: session.callId,
1281
1369
  [ATTR_CALL_DIRECTION]: session.direction,
1282
- [ATTR_AGENT_ID]: this._agentId
1370
+ [ATTR_AGENT_ID]: this._accountId
1283
1371
  },
1284
1372
  async () => {
1285
1373
  const sessionTools = this._tools.fork();
1374
+ const mcpClients = [];
1375
+ if (this._mcpServers.length > 0) {
1376
+ for (const serverConfig of this._mcpServers) {
1377
+ const client = new MCPClient();
1378
+ client.addServer("mcp", serverConfig);
1379
+ try {
1380
+ const tools = await client.connect();
1381
+ sessionTools.registerMcpTools(tools);
1382
+ mcpClients.push(client);
1383
+ } catch (err) {
1384
+ console.error("[ClawOpsAgent] MCP connection error:", err);
1385
+ }
1386
+ }
1387
+ }
1286
1388
  let recorder = null;
1287
- if (this._recordingDir) {
1288
- recorder = new AudioRecorder({ outputDir: this._recordingDir });
1289
- recorder.start(session.callId);
1389
+ if (this._recording) {
1390
+ recorder = new AudioRecorder(this._recordingPath, session.callId);
1391
+ recorder.start();
1290
1392
  }
1291
1393
  const mediaWs = new MediaWebSocket();
1292
1394
  session._bindTransport(
1293
1395
  (audio) => {
1294
- const ulaw = pcm16ToUlaw(audio);
1295
- mediaWs.sendAudio(ulaw.toString("base64"));
1296
- if (recorder) {
1297
- recorder.writeRawOutbound(audio);
1298
- }
1396
+ mediaWs.sendAudio(audio.toString("base64"));
1299
1397
  },
1300
1398
  () => {
1301
1399
  mediaWs.sendClear();
@@ -1304,15 +1402,19 @@ var ClawOpsAgent = class {
1304
1402
  mediaWs.close();
1305
1403
  }
1306
1404
  );
1307
- const sessionHandler = this._sessionFactory ? this._sessionFactory() : null;
1308
- mediaWs.onAudio((ulawAudio) => {
1309
- const pcm = ulawToPcm16(ulawAudio);
1310
- if (recorder) {
1311
- recorder.writeInbound(pcm);
1312
- }
1405
+ const sessionHandler = this._session;
1406
+ if ("setToolRegistry" in sessionHandler && typeof sessionHandler.setToolRegistry === "function") {
1407
+ sessionHandler.setToolRegistry(sessionTools);
1408
+ }
1409
+ if (recorder && "setRecorder" in sessionHandler && typeof sessionHandler.setRecorder === "function") {
1410
+ sessionHandler.setRecorder(recorder);
1411
+ }
1412
+ mediaWs.onAudio((ulawAudio, _timestamp) => {
1313
1413
  if (sessionHandler) {
1314
- const resampled = resamplePcm16(pcm, 8e3, 16e3);
1315
- sessionHandler.feedAudio(resampled);
1414
+ sessionHandler.feedAudio(ulawAudio);
1415
+ }
1416
+ if (recorder) {
1417
+ recorder.writeInbound(ulawToPcm16(ulawAudio));
1316
1418
  }
1317
1419
  });
1318
1420
  mediaWs.onClose(() => {
@@ -1321,74 +1423,93 @@ var ClawOpsAgent = class {
1321
1423
  }
1322
1424
  session._markEnded();
1323
1425
  });
1426
+ session._emit("call_start");
1324
1427
  try {
1325
- await mediaWs.connect(mediaWsUrl);
1326
- if (sessionHandler) {
1327
- await sessionHandler.start(session, sessionTools);
1328
- }
1428
+ await mediaWs.connect(mediaWsUrl, this._apiKey);
1429
+ await sessionHandler.start(session, sessionTools);
1329
1430
  await session.wait();
1330
- if (sessionHandler) {
1331
- await sessionHandler.stop();
1332
- }
1431
+ await sessionHandler.stop();
1333
1432
  } catch (err) {
1334
1433
  console.error(`[ClawOpsAgent] Call session error:`, err);
1335
1434
  } finally {
1435
+ if (mcpClients.length > 0) {
1436
+ sessionTools.clearMcpTools();
1437
+ for (const c of mcpClients) {
1438
+ await c.disconnect();
1439
+ }
1440
+ }
1336
1441
  mediaWs.close();
1337
1442
  if (recorder) {
1338
1443
  recorder.stop();
1339
1444
  }
1445
+ session._emit("call_end");
1446
+ session._markEnded();
1447
+ this._activeSessions.delete(session.callId);
1340
1448
  }
1341
1449
  }
1342
1450
  );
1343
1451
  }
1344
- _emitEvent(event, session) {
1345
- const handlers = this._handlers.get(event);
1346
- if (handlers) {
1347
- for (const handler of handlers) {
1348
- try {
1349
- const result = handler(session);
1350
- if (result && typeof result.catch === "function") {
1351
- result.catch((err) => {
1352
- console.error(`[ClawOpsAgent] Error in ${event} handler:`, err);
1353
- });
1354
- }
1355
- } catch (err) {
1356
- console.error(`[ClawOpsAgent] Error in ${event} handler:`, err);
1357
- }
1358
- }
1359
- }
1360
- }
1361
1452
  };
1362
1453
 
1363
1454
  // src/agent/pipeline/openai-realtime.ts
1455
+ var OPENAI_REALTIME_URL = "wss://api.openai.com/v1/realtime?model=";
1456
+ var HANG_UP_TOOL = {
1457
+ type: "function",
1458
+ name: "hang_up",
1459
+ description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
1460
+ parameters: { type: "object", properties: {}, required: [] }
1461
+ };
1364
1462
  var OpenAIRealtime = class {
1365
- _options;
1463
+ _apiKey;
1464
+ _systemPrompt;
1465
+ _model;
1466
+ _voice;
1467
+ _language;
1468
+ _eagerness;
1469
+ _greeting;
1366
1470
  _ws = null;
1367
- _callSession = null;
1471
+ _call = null;
1368
1472
  _tools = null;
1473
+ _recorder = null;
1369
1474
  _closed = false;
1475
+ // Truncation / barge-in tracking (matching Python SDK)
1476
+ _lastAssistantItem = null;
1477
+ _responseStartTs = null;
1478
+ _sentAudioChunks = 0;
1479
+ _audioRemainder = Buffer.alloc(0);
1370
1480
  constructor(options = {}) {
1371
- this._options = {
1372
- model: "gpt-4o-realtime-preview",
1373
- voice: "alloy",
1374
- inputAudioFormat: "pcm16",
1375
- outputAudioFormat: "pcm16",
1376
- ...options
1377
- };
1481
+ this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
1482
+ this._systemPrompt = options.systemPrompt ?? "";
1483
+ this._model = options.model ?? "gpt-realtime-1.5";
1484
+ this._voice = options.voice ?? "marin";
1485
+ this._language = options.language ?? "ko";
1486
+ this._eagerness = options.eagerness ?? "high";
1487
+ this._greeting = options.greeting ?? true;
1488
+ }
1489
+ /** Inject per-call ToolRegistry. */
1490
+ setToolRegistry(registry) {
1491
+ this._tools = registry;
1492
+ }
1493
+ /** Inject per-call AudioRecorder. */
1494
+ setRecorder(recorder) {
1495
+ this._recorder = recorder;
1378
1496
  }
1379
1497
  async start(callSession, tools) {
1380
- this._callSession = callSession;
1381
- this._tools = tools ?? null;
1498
+ this._call = callSession;
1499
+ if (tools) this._tools = tools;
1382
1500
  this._closed = false;
1383
- const apiKey = this._options.apiKey ?? process.env["OPENAI_API_KEY"];
1384
- if (!apiKey) {
1385
- throw new Error("OpenAI API key is required");
1501
+ this._lastAssistantItem = null;
1502
+ this._responseStartTs = null;
1503
+ this._sentAudioChunks = 0;
1504
+ this._audioRemainder = Buffer.alloc(0);
1505
+ if (!this._apiKey) {
1506
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey option.");
1386
1507
  }
1387
1508
  const { WebSocket } = await import('ws');
1388
- const url = `wss://api.openai.com/v1/realtime?model=${this._options.model}`;
1509
+ const url = `${OPENAI_REALTIME_URL}${this._model}`;
1389
1510
  this._ws = new WebSocket(url, {
1390
1511
  headers: {
1391
- Authorization: `Bearer ${apiKey}`,
1512
+ Authorization: `Bearer ${this._apiKey}`,
1392
1513
  "OpenAI-Beta": "realtime=v1"
1393
1514
  }
1394
1515
  });
@@ -1396,6 +1517,9 @@ var OpenAIRealtime = class {
1396
1517
  const ws = this._ws;
1397
1518
  ws.on("open", () => {
1398
1519
  this._sendSessionUpdate();
1520
+ if (this._greeting) {
1521
+ this._send({ type: "response.create" });
1522
+ }
1399
1523
  resolve();
1400
1524
  });
1401
1525
  ws.on("message", (data) => {
@@ -1418,12 +1542,10 @@ var OpenAIRealtime = class {
1418
1542
  }
1419
1543
  feedAudio(audio) {
1420
1544
  if (this._ws && this._ws.readyState === 1 && !this._closed) {
1421
- this._ws.send(
1422
- JSON.stringify({
1423
- type: "input_audio_buffer.append",
1424
- audio: audio.toString("base64")
1425
- })
1426
- );
1545
+ this._send({
1546
+ type: "input_audio_buffer.append",
1547
+ audio: audio.toString("base64")
1548
+ });
1427
1549
  }
1428
1550
  }
1429
1551
  async stop() {
@@ -1435,52 +1557,71 @@ var OpenAIRealtime = class {
1435
1557
  }
1436
1558
  _sendSessionUpdate() {
1437
1559
  if (!this._ws || this._ws.readyState !== 1) return;
1438
- const sessionConfig = {
1439
- modalities: ["text", "audio"],
1440
- voice: this._options.voice,
1441
- input_audio_format: this._options.inputAudioFormat,
1442
- output_audio_format: this._options.outputAudioFormat
1443
- };
1444
- if (this._options.instructions) {
1445
- sessionConfig["instructions"] = this._options.instructions;
1446
- }
1447
- if (this._options.temperature !== void 0) {
1448
- sessionConfig["temperature"] = this._options.temperature;
1449
- }
1450
- if (this._options.turnDetection !== void 0) {
1451
- sessionConfig["turn_detection"] = this._options.turnDetection;
1452
- }
1453
- if (this._tools && this._tools.size > 0) {
1454
- sessionConfig["tools"] = this._tools.toOpenAITools().map((t) => t.function);
1455
- }
1456
- this._ws.send(
1457
- JSON.stringify({
1458
- type: "session.update",
1459
- session: sessionConfig
1460
- })
1461
- );
1560
+ const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
1561
+ toolSchemas.push(HANG_UP_TOOL);
1562
+ this._send({
1563
+ type: "session.update",
1564
+ session: {
1565
+ modalities: ["text", "audio"],
1566
+ voice: this._voice,
1567
+ instructions: this._systemPrompt,
1568
+ input_audio_format: "g711_ulaw",
1569
+ output_audio_format: "g711_ulaw",
1570
+ input_audio_transcription: {
1571
+ model: "gpt-4o-mini-transcribe",
1572
+ language: this._language
1573
+ },
1574
+ input_audio_noise_reduction: { type: "far_field" },
1575
+ turn_detection: {
1576
+ type: "semantic_vad",
1577
+ interrupt_response: true,
1578
+ eagerness: this._eagerness
1579
+ },
1580
+ tools: toolSchemas
1581
+ }
1582
+ });
1462
1583
  }
1463
1584
  _handleMessage(msg) {
1464
1585
  const type = msg["type"];
1465
1586
  switch (type) {
1466
1587
  case "response.audio.delta": {
1467
- const delta = msg["delta"];
1468
- if (delta && this._callSession) {
1469
- const audio = Buffer.from(delta, "base64");
1470
- this._callSession.sendAudio(audio);
1471
- }
1588
+ this._handleAudioDelta(msg);
1472
1589
  break;
1473
1590
  }
1474
1591
  case "response.audio.done": {
1592
+ if (this._audioRemainder.length > 0) {
1593
+ const padded = Buffer.concat([
1594
+ this._audioRemainder,
1595
+ Buffer.alloc(160 - this._audioRemainder.length, 255)
1596
+ ]);
1597
+ if (this._call) {
1598
+ this._call.sendAudio(padded);
1599
+ }
1600
+ this._sentAudioChunks++;
1601
+ this._audioRemainder = Buffer.alloc(0);
1602
+ }
1475
1603
  break;
1476
1604
  }
1477
- case "response.function_call_arguments.done": {
1478
- this._handleFunctionCall(msg);
1605
+ case "input_audio_buffer.speech_started": {
1606
+ this._handleTruncation();
1479
1607
  break;
1480
1608
  }
1481
- case "input_audio_buffer.speech_started": {
1482
- if (this._callSession) {
1483
- this._callSession.clearAudio();
1609
+ case "response.output_item.done": {
1610
+ const item = msg["item"];
1611
+ if (item && item["type"] === "function_call") {
1612
+ this._handleToolCall(item);
1613
+ }
1614
+ break;
1615
+ }
1616
+ case "conversation.item.input_audio_transcription.completed": {
1617
+ if (this._call) {
1618
+ this._call._emit("transcript", "user", msg["transcript"] ?? "");
1619
+ }
1620
+ break;
1621
+ }
1622
+ case "response.audio_transcript.done": {
1623
+ if (this._call) {
1624
+ this._call._emit("transcript", "assistant", msg["transcript"] ?? "");
1484
1625
  }
1485
1626
  break;
1486
1627
  }
@@ -1490,73 +1631,147 @@ var OpenAIRealtime = class {
1490
1631
  }
1491
1632
  }
1492
1633
  }
1493
- async _handleFunctionCall(msg) {
1494
- const name = msg["name"];
1495
- const callId = msg["call_id"];
1496
- const argsStr = msg["arguments"];
1497
- if (!this._tools || !this._tools.has(name)) {
1498
- console.error(`[OpenAIRealtime] Unknown tool: ${name}`);
1634
+ _handleAudioDelta(msg) {
1635
+ if (this._responseStartTs === null) {
1636
+ this._responseStartTs = Date.now();
1637
+ this._sentAudioChunks = 0;
1638
+ }
1639
+ if (msg["item_id"]) {
1640
+ this._lastAssistantItem = msg["item_id"];
1641
+ }
1642
+ const ulaw = Buffer.from(msg["delta"], "base64");
1643
+ if (this._recorder) {
1644
+ this._recorder.writeOutbound(ulawToPcm16(ulaw));
1645
+ }
1646
+ const combined = Buffer.concat([this._audioRemainder, ulaw]);
1647
+ const chunkSize = 160;
1648
+ const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
1649
+ for (let off = 0; off < fullEnd; off += chunkSize) {
1650
+ if (this._call) {
1651
+ this._call.sendAudio(combined.subarray(off, off + chunkSize));
1652
+ }
1653
+ this._sentAudioChunks++;
1654
+ }
1655
+ this._audioRemainder = combined.subarray(fullEnd);
1656
+ }
1657
+ _handleTruncation() {
1658
+ if (!this._lastAssistantItem || this._responseStartTs === null) {
1499
1659
  return;
1500
1660
  }
1501
- try {
1502
- const args = JSON.parse(argsStr);
1503
- const result = await this._tools.call(name, args);
1504
- if (this._ws && this._ws.readyState === 1) {
1505
- this._ws.send(
1506
- JSON.stringify({
1507
- type: "conversation.item.create",
1508
- item: {
1509
- type: "function_call_output",
1510
- call_id: callId,
1511
- output: typeof result === "string" ? result : JSON.stringify(result)
1512
- }
1513
- })
1514
- );
1515
- this._ws.send(JSON.stringify({ type: "response.create" }));
1661
+ const audioEndMs = Math.max(0, this._sentAudioChunks * 20);
1662
+ this._send({
1663
+ type: "conversation.item.truncate",
1664
+ item_id: this._lastAssistantItem,
1665
+ content_index: 0,
1666
+ audio_end_ms: audioEndMs
1667
+ });
1668
+ if (this._call) {
1669
+ this._call.clearAudio();
1670
+ }
1671
+ this._lastAssistantItem = null;
1672
+ this._responseStartTs = null;
1673
+ this._sentAudioChunks = 0;
1674
+ this._audioRemainder = Buffer.alloc(0);
1675
+ }
1676
+ async _handleToolCall(item) {
1677
+ const funcName = item["name"];
1678
+ const callId = item["call_id"];
1679
+ if (funcName === "hang_up") {
1680
+ if (this._call) {
1681
+ this._call.hangup();
1516
1682
  }
1683
+ return;
1684
+ }
1685
+ if (!this._tools || !this._tools.has(funcName)) {
1686
+ console.error(`[OpenAIRealtime] Unknown tool: ${funcName}`);
1687
+ return;
1688
+ }
1689
+ let result;
1690
+ try {
1691
+ const args = JSON.parse(item["arguments"] ?? "{}");
1692
+ result = await this._tools.call(funcName, args);
1517
1693
  } catch (err) {
1518
- console.error(`[OpenAIRealtime] Tool call error for ${name}:`, err);
1694
+ console.error(`[OpenAIRealtime] Tool call failed: ${funcName}:`, err);
1695
+ result = `Error: ${err}`;
1696
+ }
1697
+ this._send({
1698
+ type: "conversation.item.create",
1699
+ item: {
1700
+ type: "function_call_output",
1701
+ call_id: callId,
1702
+ output: typeof result === "string" ? result : JSON.stringify(result)
1703
+ }
1704
+ });
1705
+ this._send({ type: "response.create" });
1706
+ }
1707
+ _send(data) {
1708
+ if (this._ws && this._ws.readyState === 1 && !this._closed) {
1709
+ this._ws.send(JSON.stringify(data));
1519
1710
  }
1520
1711
  }
1521
1712
  };
1522
1713
 
1523
1714
  // src/agent/pipeline/gemini-realtime.ts
1715
+ var HANG_UP_TOOL2 = {
1716
+ name: "hang_up",
1717
+ description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
1718
+ parameters: { type: "object", properties: {} }
1719
+ };
1524
1720
  var GeminiRealtime = class {
1525
- _options;
1721
+ _apiKey;
1722
+ _systemPrompt;
1723
+ _model;
1724
+ _voice;
1725
+ _language;
1726
+ _greeting;
1727
+ _generationConfig;
1526
1728
  _ws = null;
1527
- _callSession = null;
1729
+ _call = null;
1528
1730
  _tools = null;
1731
+ _recorder = null;
1529
1732
  _closed = false;
1733
+ _sentAudioChunks = 0;
1734
+ _audioRemainder = Buffer.alloc(0);
1530
1735
  constructor(options = {}) {
1531
- this._options = {
1532
- model: "gemini-2.0-flash-exp",
1533
- ...options
1534
- };
1736
+ this._apiKey = options.apiKey ?? process.env["GOOGLE_API_KEY"] ?? "";
1737
+ this._systemPrompt = options.systemPrompt ?? "";
1738
+ this._model = options.model ?? "gemini-2.5-flash-native-audio-preview-12-2025";
1739
+ this._voice = options.voice ?? "Kore";
1740
+ this._language = options.language ?? "ko";
1741
+ this._greeting = options.greeting ?? true;
1742
+ this._generationConfig = options.generationConfig;
1743
+ }
1744
+ /** Inject per-call ToolRegistry. */
1745
+ setToolRegistry(registry) {
1746
+ this._tools = registry;
1747
+ }
1748
+ /** Inject per-call AudioRecorder. */
1749
+ setRecorder(recorder) {
1750
+ this._recorder = recorder;
1535
1751
  }
1536
1752
  async start(callSession, tools) {
1537
- this._callSession = callSession;
1538
- this._tools = tools ?? null;
1753
+ this._call = callSession;
1754
+ if (tools) this._tools = tools;
1539
1755
  this._closed = false;
1540
- const apiKey = this._options.apiKey ?? process.env["GOOGLE_API_KEY"];
1541
- if (!apiKey) {
1542
- throw new Error("Google API key is required");
1756
+ this._sentAudioChunks = 0;
1757
+ this._audioRemainder = Buffer.alloc(0);
1758
+ if (!this._apiKey) {
1759
+ throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey option.");
1543
1760
  }
1544
1761
  const { WebSocket } = await import('ws');
1545
- this._options.model;
1546
- const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${apiKey}`;
1762
+ const url = `wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent?key=${this._apiKey}`;
1547
1763
  this._ws = new WebSocket(url);
1548
1764
  return new Promise((resolve, reject) => {
1549
1765
  const ws = this._ws;
1550
1766
  ws.on("open", () => {
1551
1767
  this._sendSetup();
1552
- resolve();
1553
- });
1554
- ws.on("message", (data) => {
1555
- try {
1556
- const msg = JSON.parse(data.toString());
1557
- this._handleMessage(msg);
1558
- } catch {
1559
- }
1768
+ this._waitSetupComplete().then(() => {
1769
+ if (this._greeting) {
1770
+ this._sendGreeting();
1771
+ }
1772
+ this._receiveLoop();
1773
+ resolve();
1774
+ }).catch(reject);
1560
1775
  });
1561
1776
  ws.on("close", () => {
1562
1777
  this._closed = true;
@@ -1571,13 +1786,15 @@ var GeminiRealtime = class {
1571
1786
  }
1572
1787
  feedAudio(audio) {
1573
1788
  if (this._ws && this._ws.readyState === 1 && !this._closed) {
1789
+ const pcm8k = ulawToPcm16(audio);
1790
+ const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
1574
1791
  this._ws.send(
1575
1792
  JSON.stringify({
1576
1793
  realtimeInput: {
1577
1794
  mediaChunks: [
1578
1795
  {
1579
1796
  mimeType: "audio/pcm;rate=16000",
1580
- data: audio.toString("base64")
1797
+ data: pcm16k.toString("base64")
1581
1798
  }
1582
1799
  ]
1583
1800
  }
@@ -1595,35 +1812,85 @@ var GeminiRealtime = class {
1595
1812
  _sendSetup() {
1596
1813
  if (!this._ws || this._ws.readyState !== 1) return;
1597
1814
  const setupConfig = {
1598
- model: `models/${this._options.model}`,
1815
+ model: `models/${this._model}`,
1599
1816
  generationConfig: {
1600
1817
  responseModalities: ["AUDIO"],
1601
1818
  speechConfig: {
1602
1819
  voiceConfig: {
1603
1820
  prebuiltVoiceConfig: {
1604
- voiceName: this._options.voice ?? "Aoede"
1821
+ voiceName: this._voice
1605
1822
  }
1606
1823
  }
1607
1824
  },
1608
- ...this._options.generationConfig
1825
+ ...this._generationConfig
1826
+ },
1827
+ realtimeInputConfig: {
1828
+ automaticActivityDetection: {
1829
+ disabled: false
1830
+ }
1609
1831
  }
1610
1832
  };
1611
- if (this._options.systemInstruction) {
1833
+ if (this._systemPrompt) {
1612
1834
  setupConfig["systemInstruction"] = {
1613
- parts: [{ text: this._options.systemInstruction }]
1835
+ parts: [{ text: this._systemPrompt }]
1614
1836
  };
1615
1837
  }
1616
- if (this._tools && this._tools.size > 0) {
1617
- const toolDefs = this._tools.toOpenAITools().map((t) => ({
1618
- name: t.function.name,
1619
- description: t.function.description,
1620
- parameters: t.function.parameters
1621
- }));
1622
- setupConfig["tools"] = [{ functionDeclarations: toolDefs }];
1623
- }
1838
+ const toolDefs = this._tools ? this._tools.toOpenAITools().map((t) => ({
1839
+ name: t.function.name,
1840
+ description: t.function.description,
1841
+ parameters: t.function.parameters
1842
+ })) : [];
1843
+ toolDefs.push(HANG_UP_TOOL2);
1844
+ setupConfig["tools"] = [{ functionDeclarations: toolDefs }];
1624
1845
  this._ws.send(JSON.stringify({ setup: setupConfig }));
1625
1846
  }
1847
+ _waitSetupComplete() {
1848
+ return new Promise((resolve, reject) => {
1849
+ if (!this._ws) {
1850
+ reject(new Error("WebSocket not connected"));
1851
+ return;
1852
+ }
1853
+ const onMessage = (data) => {
1854
+ try {
1855
+ const msg = JSON.parse(data.toString());
1856
+ if ("setupComplete" in msg) {
1857
+ this._ws?.removeListener("message", onMessage);
1858
+ resolve();
1859
+ }
1860
+ } catch {
1861
+ }
1862
+ };
1863
+ this._ws.on("message", onMessage);
1864
+ });
1865
+ }
1866
+ _sendGreeting() {
1867
+ if (!this._ws || this._ws.readyState !== 1) return;
1868
+ this._ws.send(
1869
+ JSON.stringify({
1870
+ clientContent: {
1871
+ turns: [
1872
+ {
1873
+ role: "user",
1874
+ parts: [{ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." }]
1875
+ }
1876
+ ],
1877
+ turnComplete: true
1878
+ }
1879
+ })
1880
+ );
1881
+ }
1882
+ _receiveLoop() {
1883
+ if (!this._ws) return;
1884
+ this._ws.on("message", (data) => {
1885
+ try {
1886
+ const msg = JSON.parse(data.toString());
1887
+ this._handleMessage(msg);
1888
+ } catch {
1889
+ }
1890
+ });
1891
+ }
1626
1892
  _handleMessage(msg) {
1893
+ if (!this._call) return;
1627
1894
  const serverContent = msg["serverContent"];
1628
1895
  if (serverContent) {
1629
1896
  const modelTurn = serverContent["modelTurn"];
@@ -1632,35 +1899,108 @@ var GeminiRealtime = class {
1632
1899
  if (parts) {
1633
1900
  for (const part of parts) {
1634
1901
  const inlineData = part["inlineData"];
1635
- if (inlineData && inlineData["data"] && this._callSession) {
1636
- const audio = Buffer.from(inlineData["data"], "base64");
1637
- this._callSession.sendAudio(audio);
1902
+ if (inlineData && inlineData["data"]) {
1903
+ const mimeType = inlineData["mimeType"] ?? "";
1904
+ if (mimeType.includes("audio")) {
1905
+ this._handleAudioData(inlineData["data"]);
1906
+ }
1907
+ }
1908
+ const text = part["text"];
1909
+ if (text && this._call) {
1910
+ this._call._emit("transcript", "assistant", text);
1638
1911
  }
1639
1912
  }
1640
1913
  }
1641
1914
  }
1915
+ if (serverContent["turnComplete"]) {
1916
+ this._flushAudioRemainder();
1917
+ }
1918
+ if (serverContent["interrupted"]) {
1919
+ if (this._call) {
1920
+ this._call.clearAudio();
1921
+ }
1922
+ this._sentAudioChunks = 0;
1923
+ this._audioRemainder = Buffer.alloc(0);
1924
+ }
1925
+ }
1926
+ const inputTranscription = msg["inputTranscription"];
1927
+ if (inputTranscription) {
1928
+ const text = inputTranscription["text"];
1929
+ if (text && this._call) {
1930
+ this._call._emit("transcript", "user", text);
1931
+ }
1932
+ }
1933
+ const outputTranscription = msg["outputTranscription"];
1934
+ if (outputTranscription) {
1935
+ const text = outputTranscription["text"];
1936
+ if (text && this._call) {
1937
+ this._call._emit("transcript", "assistant", text);
1938
+ }
1642
1939
  }
1643
1940
  const toolCall = msg["toolCall"];
1644
1941
  if (toolCall) {
1645
1942
  this._handleToolCall(toolCall);
1646
1943
  }
1944
+ if (msg["toolCallCancellation"]) ;
1945
+ }
1946
+ _handleAudioData(b64Data) {
1947
+ if (!this._call) return;
1948
+ const pcm24k = Buffer.from(b64Data, "base64");
1949
+ if (this._recorder) {
1950
+ this._recorder.writeOutbound(resamplePcm16(pcm24k, 24e3, 8e3));
1951
+ }
1952
+ const pcm8k = resamplePcm16(pcm24k, 24e3, 8e3);
1953
+ const ulaw = pcm16ToUlaw(pcm8k);
1954
+ const combined = Buffer.concat([this._audioRemainder, ulaw]);
1955
+ const chunkSize = 160;
1956
+ const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
1957
+ for (let off = 0; off < fullEnd; off += chunkSize) {
1958
+ this._call.sendAudio(combined.subarray(off, off + chunkSize));
1959
+ this._sentAudioChunks++;
1960
+ }
1961
+ this._audioRemainder = combined.subarray(fullEnd);
1962
+ }
1963
+ _flushAudioRemainder() {
1964
+ if (this._audioRemainder.length > 0 && this._call) {
1965
+ const padded = Buffer.concat([
1966
+ this._audioRemainder,
1967
+ Buffer.alloc(160 - this._audioRemainder.length, 255)
1968
+ ]);
1969
+ this._call.sendAudio(padded);
1970
+ this._sentAudioChunks++;
1971
+ this._audioRemainder = Buffer.alloc(0);
1972
+ }
1647
1973
  }
1648
1974
  async _handleToolCall(toolCall) {
1649
1975
  const functionCalls = toolCall["functionCalls"];
1650
- if (!functionCalls || !this._tools) return;
1976
+ if (!functionCalls) return;
1651
1977
  const responses = [];
1652
1978
  for (const fc of functionCalls) {
1653
1979
  const name = fc["name"];
1980
+ const fcId = fc["id"] ?? "";
1654
1981
  const args = fc["args"] ?? {};
1982
+ if (name === "hang_up") {
1983
+ if (this._call) {
1984
+ this._call.hangup();
1985
+ }
1986
+ return;
1987
+ }
1988
+ if (!this._tools || !this._tools.has(name)) {
1989
+ console.error(`[GeminiRealtime] Unknown tool: ${name}`);
1990
+ responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
1991
+ continue;
1992
+ }
1655
1993
  try {
1656
1994
  const result = await this._tools.call(name, args);
1657
1995
  responses.push({
1996
+ id: fcId,
1658
1997
  name,
1659
1998
  response: { result: typeof result === "string" ? result : JSON.stringify(result) }
1660
1999
  });
1661
2000
  } catch (err) {
1662
2001
  console.error(`[GeminiRealtime] Tool call error for ${name}:`, err);
1663
2002
  responses.push({
2003
+ id: fcId,
1664
2004
  name,
1665
2005
  response: { error: String(err) }
1666
2006
  });
@@ -1684,12 +2024,15 @@ var PipelineSession = class {
1684
2024
  _llm;
1685
2025
  _tts;
1686
2026
  _systemPrompt;
2027
+ _greeting;
2028
+ _language;
1687
2029
  _temperature;
1688
2030
  _maxTokens;
1689
2031
  _sampleRate;
1690
2032
  _interruptOnSpeech;
1691
2033
  _callSession = null;
1692
2034
  _tools = null;
2035
+ _recorder = null;
1693
2036
  _conversation = [];
1694
2037
  _audioBuffer = [];
1695
2038
  _running = false;
@@ -1699,10 +2042,20 @@ var PipelineSession = class {
1699
2042
  this._llm = options.llm;
1700
2043
  this._tts = options.tts;
1701
2044
  this._systemPrompt = options.systemPrompt;
2045
+ this._greeting = options.greeting ?? true;
2046
+ this._language = options.language ?? "ko";
1702
2047
  this._temperature = options.temperature;
1703
2048
  this._maxTokens = options.maxTokens;
1704
2049
  this._sampleRate = options.sampleRate ?? 8e3;
1705
2050
  this._interruptOnSpeech = options.interruptOnSpeech ?? true;
2051
+ if (options.toolRegistry) this._tools = options.toolRegistry;
2052
+ if (options.recorder) this._recorder = options.recorder;
2053
+ }
2054
+ setToolRegistry(registry) {
2055
+ this._tools = registry;
2056
+ }
2057
+ setRecorder(recorder) {
2058
+ this._recorder = recorder;
1706
2059
  }
1707
2060
  async start(callSession, tools) {
1708
2061
  this._callSession = callSession;
@@ -1715,6 +2068,11 @@ var PipelineSession = class {
1715
2068
  content: this._systemPrompt
1716
2069
  });
1717
2070
  }
2071
+ if (this._greeting) {
2072
+ this._generateGreeting().catch((err) => {
2073
+ console.error("[PipelineSession] Greeting error:", err);
2074
+ });
2075
+ }
1718
2076
  this._runSttLoop().catch((err) => {
1719
2077
  console.error("[PipelineSession] STT loop error:", err);
1720
2078
  });
@@ -1748,14 +2106,24 @@ var PipelineSession = class {
1748
2106
  async *_createAudioStream() {
1749
2107
  while (this._running) {
1750
2108
  if (this._audioBuffer.length > 0) {
1751
- yield this._audioBuffer.shift();
2109
+ const ulaw = this._audioBuffer.shift();
2110
+ const pcm8k = ulawToPcm16(ulaw);
2111
+ const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
2112
+ yield pcm16k;
1752
2113
  } else {
1753
2114
  await new Promise((resolve) => setTimeout(resolve, 20));
1754
2115
  }
1755
2116
  }
1756
2117
  }
2118
+ async _generateGreeting() {
2119
+ await new Promise((resolve) => setTimeout(resolve, 500));
2120
+ await this._respond();
2121
+ }
1757
2122
  async _handleUserSpeech(transcript) {
1758
2123
  this._conversation.push({ role: "user", content: transcript });
2124
+ await this._respond();
2125
+ }
2126
+ async _respond() {
1759
2127
  let fullResponse = "";
1760
2128
  const textChunks = [];
1761
2129
  const llmStream = this._llm.generate(this._conversation, {
@@ -1822,7 +2190,19 @@ var PipelineSession = class {
1822
2190
  sampleRate: this._sampleRate
1823
2191
  })) {
1824
2192
  if (!this._running || !this._speaking) break;
1825
- this._callSession.sendAudio(audioChunk);
2193
+ if (this._recorder) {
2194
+ const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2195
+ this._recorder.writeOutbound(pcm8k2);
2196
+ }
2197
+ const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2198
+ const ulaw = pcm16ToUlaw(pcm8k);
2199
+ for (let off = 0; off < ulaw.length; off += 160) {
2200
+ let chunk = ulaw.subarray(off, off + 160);
2201
+ if (chunk.length < 160) {
2202
+ chunk = Buffer.concat([chunk, Buffer.alloc(160 - chunk.length, 255)]);
2203
+ }
2204
+ this._callSession.sendAudio(chunk);
2205
+ }
1826
2206
  }
1827
2207
  } catch (err) {
1828
2208
  console.error("[PipelineSession] TTS error:", err);
@@ -1837,14 +2217,14 @@ var DeepgramSTT = class {
1837
2217
  _options;
1838
2218
  constructor(options = {}) {
1839
2219
  this._options = {
1840
- model: "nova-2",
2220
+ model: "nova-3",
1841
2221
  language: "ko",
1842
- interimResults: true,
1843
- punctuate: true,
1844
- smartFormat: true,
2222
+ sampleRate: 16e3,
1845
2223
  encoding: "linear16",
1846
- sampleRate: 8e3,
1847
- channels: 1,
2224
+ punctuate: true,
2225
+ interimResults: true,
2226
+ endpointing: 300,
2227
+ utteranceEndMs: 1e3,
1848
2228
  ...options
1849
2229
  };
1850
2230
  }
@@ -1861,10 +2241,10 @@ var DeepgramSTT = class {
1861
2241
  language,
1862
2242
  punctuate: String(this._options.punctuate),
1863
2243
  interim_results: String(this._options.interimResults),
1864
- smart_format: String(this._options.smartFormat),
1865
2244
  encoding: this._options.encoding,
1866
2245
  sample_rate: String(sampleRate),
1867
- channels: String(this._options.channels)
2246
+ endpointing: String(this._options.endpointing),
2247
+ utterance_end_ms: String(this._options.utteranceEndMs)
1868
2248
  });
1869
2249
  const url = `wss://api.deepgram.com/v1/listen?${params.toString()}`;
1870
2250
  const ws = new WebSocket(url, {
@@ -1953,13 +2333,12 @@ var ElevenLabsTTS = class {
1953
2333
  _options;
1954
2334
  constructor(options = {}) {
1955
2335
  this._options = {
1956
- voiceId: "21m00Tcm4TlvDq8ikWAM",
1957
- modelId: "eleven_multilingual_v2",
1958
- outputFormat: "pcm_16000",
2336
+ voiceId: "EXAVITQu4vr4xnSDxMaL",
2337
+ model: "eleven_flash_v2_5",
2338
+ outputFormat: "pcm_24000",
1959
2339
  stability: 0.5,
1960
2340
  similarityBoost: 0.75,
1961
- style: 0,
1962
- useSpeakerBoost: true,
2341
+ languageCode: "ko",
1963
2342
  ...options
1964
2343
  };
1965
2344
  }
@@ -1985,12 +2364,10 @@ var ElevenLabsTTS = class {
1985
2364
  },
1986
2365
  body: JSON.stringify({
1987
2366
  text,
1988
- model_id: this._options.modelId,
2367
+ model_id: this._options.model,
1989
2368
  voice_settings: {
1990
2369
  stability: this._options.stability,
1991
- similarity_boost: this._options.similarityBoost,
1992
- style: this._options.style,
1993
- use_speaker_boost: this._options.useSpeakerBoost
2370
+ similarity_boost: this._options.similarityBoost
1994
2371
  }
1995
2372
  })
1996
2373
  });
@@ -2014,7 +2391,7 @@ var ElevenLabsTTS = class {
2014
2391
  }
2015
2392
  async *_synthesizeStreaming(apiKey, voiceId, textStream) {
2016
2393
  const { WebSocket } = await import('ws');
2017
- const url = `wss://api.elevenlabs.io/v1/text-to-speech/${voiceId}/stream-input?model_id=${this._options.modelId}&output_format=${this._options.outputFormat}`;
2394
+ const url = `wss://api.elevenlabs.io/v1/text-to-speech/${voiceId}/stream-input?model_id=${this._options.model}&output_format=${this._options.outputFormat}`;
2018
2395
  const ws = new WebSocket(url);
2019
2396
  const audioQueue = [];
2020
2397
  let resolveWait = null;
@@ -2025,9 +2402,7 @@ var ElevenLabsTTS = class {
2025
2402
  text: " ",
2026
2403
  voice_settings: {
2027
2404
  stability: this._options.stability,
2028
- similarity_boost: this._options.similarityBoost,
2029
- style: this._options.style,
2030
- use_speaker_boost: this._options.useSpeakerBoost
2405
+ similarity_boost: this._options.similarityBoost
2031
2406
  },
2032
2407
  xi_api_key: apiKey
2033
2408
  })
@@ -2104,7 +2479,9 @@ var OpenAILLM = class {
2104
2479
  _options;
2105
2480
  constructor(options = {}) {
2106
2481
  this._options = {
2107
- model: "gpt-4o",
2482
+ model: "gpt-4o-mini",
2483
+ temperature: 0.8,
2484
+ maxTokens: 4096,
2108
2485
  ...options
2109
2486
  };
2110
2487
  }
@@ -2188,8 +2565,9 @@ var AnthropicLLM = class {
2188
2565
  _options;
2189
2566
  constructor(options = {}) {
2190
2567
  this._options = {
2191
- model: "claude-sonnet-4-20250514",
2192
- maxTokens: 1024,
2568
+ model: "claude-sonnet-4-6",
2569
+ temperature: 0.8,
2570
+ maxTokens: 4096,
2193
2571
  ...options
2194
2572
  };
2195
2573
  }
@@ -2292,7 +2670,9 @@ var GeminiLLM = class {
2292
2670
  _options;
2293
2671
  constructor(options = {}) {
2294
2672
  this._options = {
2295
- model: "gemini-2.0-flash",
2673
+ model: "gemini-2.5-flash",
2674
+ temperature: 0.8,
2675
+ maxTokens: 4096,
2296
2676
  ...options
2297
2677
  };
2298
2678
  }
@@ -2456,13 +2836,13 @@ var OpenAICompatLLM = class {
2456
2836
  var OllamaLLM = class {
2457
2837
  _inner;
2458
2838
  constructor(options = {}) {
2459
- const baseUrl = (options.baseUrl ?? "http://localhost:11434").replace(/\/$/, "");
2839
+ const baseUrl = options.baseUrl ?? process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434/v1";
2460
2840
  this._inner = new OpenAICompatLLM({
2461
- baseUrl: `${baseUrl}/v1`,
2462
- model: options.model ?? "llama3.1",
2841
+ baseUrl: baseUrl.replace(/\/$/, ""),
2842
+ model: options.model ?? "llama3.2",
2463
2843
  apiKey: "ollama",
2464
- temperature: options.temperature,
2465
- maxTokens: options.maxTokens
2844
+ temperature: options.temperature ?? 0.8,
2845
+ maxTokens: options.maxTokens ?? 4096
2466
2846
  });
2467
2847
  }
2468
2848
  async *generate(messages, options) {