@teamlearners/clawops 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
- import { DEFAULT_BASE_URL, AgentError, AgentConnectionError } from '../chunk-7S2OEBS6.js';
1
+ import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-2B74QZZJ.js';
2
2
  import pino from 'pino';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
+ import os from 'os';
5
6
 
6
7
  // src/agent/audio.ts
7
8
  var _BIAS = 132;
@@ -972,6 +973,36 @@ var AudioRecorder = class {
972
973
  }
973
974
  }
974
975
  };
976
+ var MAX_ERRORS = 20;
977
+ var MAX_ERROR_MESSAGE_LENGTH = 200;
978
+ function getSdkInfo() {
979
+ return {
980
+ name: "clawops-node",
981
+ version: VERSION,
982
+ runtime: `node/${process.versions.node}`,
983
+ os: `${process.platform}/${os.arch()}`
984
+ };
985
+ }
986
+ function createCallMetrics() {
987
+ return {
988
+ firstResponseMs: null,
989
+ turnCount: 0,
990
+ toolCallCount: 0,
991
+ toolErrorCount: 0,
992
+ bargeInCount: 0,
993
+ endReason: null,
994
+ errors: []
995
+ };
996
+ }
997
+ function addMetricError(metrics, err) {
998
+ metrics.toolErrorCount++;
999
+ if (metrics.errors.length < MAX_ERRORS) {
1000
+ metrics.errors.push({
1001
+ type: err.name || "Error",
1002
+ message: (err.message || "").slice(0, MAX_ERROR_MESSAGE_LENGTH)
1003
+ });
1004
+ }
1005
+ }
975
1006
 
976
1007
  // src/agent/session.ts
977
1008
  var CallSession = class {
@@ -997,6 +1028,8 @@ var CallSession = class {
997
1028
  _handlers = /* @__PURE__ */ new Map();
998
1029
  _endedPromise;
999
1030
  _resolveEnded;
1031
+ _metrics = createCallMetrics();
1032
+ _firstResponseSent = false;
1000
1033
  constructor(options) {
1001
1034
  this.callId = options.callId;
1002
1035
  this.fromNumber = options.fromNumber;
@@ -1019,6 +1052,30 @@ var CallSession = class {
1019
1052
  get duration() {
1020
1053
  return (Date.now() - this.startTime.getTime()) / 1e3;
1021
1054
  }
1055
+ get metrics() {
1056
+ return this._metrics;
1057
+ }
1058
+ recordFirstResponse() {
1059
+ if (!this._firstResponseSent) {
1060
+ this._firstResponseSent = true;
1061
+ this._metrics.firstResponseMs = Date.now() - this.startTime.getTime();
1062
+ }
1063
+ }
1064
+ recordTurn() {
1065
+ this._metrics.turnCount++;
1066
+ }
1067
+ recordToolCall() {
1068
+ this._metrics.toolCallCount++;
1069
+ }
1070
+ recordToolError(err) {
1071
+ addMetricError(this._metrics, err);
1072
+ }
1073
+ recordBargeIn() {
1074
+ this._metrics.bargeInCount++;
1075
+ }
1076
+ recordEndReason(reason) {
1077
+ this._metrics.endReason = reason;
1078
+ }
1022
1079
  /** Bind transport functions (called internally by the agent). */
1023
1080
  _bindTransport(send, clear, hangup, sendDtmf, isConnected) {
1024
1081
  this._sendAudioFn = send;
@@ -1459,6 +1516,7 @@ var ClawOpsAgent = class {
1459
1516
  }
1460
1517
  /** Connect to the ClawOps platform and start listening for calls. */
1461
1518
  async connect() {
1519
+ if (this._controlWs) return;
1462
1520
  if (!this._apiKey) {
1463
1521
  throw new AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1464
1522
  }
@@ -1482,6 +1540,10 @@ var ClawOpsAgent = class {
1482
1540
  try {
1483
1541
  await this._controlWs.connect();
1484
1542
  await this._controlWs.waitConnected();
1543
+ try {
1544
+ this._controlWs.send({ event: "agent.hello", sdk: getSdkInfo() });
1545
+ } catch {
1546
+ }
1485
1547
  } catch (err) {
1486
1548
  throw new AgentConnectionError(
1487
1549
  `Failed to connect to ClawOps: ${err instanceof Error ? err.message : String(err)}`
@@ -1695,9 +1757,11 @@ var ClawOpsAgent = class {
1695
1757
  session._bindTransport(
1696
1758
  (audio) => {
1697
1759
  mediaWs.sendAudio(audio.toString("base64"));
1760
+ session.recordFirstResponse();
1698
1761
  },
1699
1762
  () => {
1700
1763
  mediaWs.sendClear();
1764
+ session.recordBargeIn();
1701
1765
  },
1702
1766
  async () => {
1703
1767
  await mediaWs.flush();
@@ -1748,10 +1812,22 @@ var ClawOpsAgent = class {
1748
1812
  await mediaWs.connect(mediaWsUrl, this._apiKey);
1749
1813
  this._log.info("Media stream started: %s", session.callId);
1750
1814
  await sessionHandler.start(session, sessionTools);
1815
+ const telemetry = sessionHandler.getTelemetry?.() ?? null;
1816
+ if (telemetry) {
1817
+ telemetry.toolCount = sessionTools?.size ?? 0;
1818
+ telemetry.mcpServerCount = this._mcpServers?.length ?? 0;
1819
+ telemetry.builtinTools = this._builtinTools ? [...this._builtinTools].map((t) => t.toString()) : [];
1820
+ telemetry.recordingEnabled = this._recording;
1821
+ try {
1822
+ this._controlWs.send({ event: "call.telemetry", callId: session.callId, telemetry });
1823
+ } catch {
1824
+ }
1825
+ }
1751
1826
  await session.wait();
1752
1827
  await sessionHandler.stop();
1753
1828
  } catch (err) {
1754
1829
  this._log.error({ err }, "Call session error: %s", session.callId);
1830
+ session.recordEndReason("error");
1755
1831
  } finally {
1756
1832
  if (mcpClients.length > 0) {
1757
1833
  sessionTools.clearMcpTools();
@@ -1763,6 +1839,13 @@ var ClawOpsAgent = class {
1763
1839
  if (recorder) {
1764
1840
  recorder.stop();
1765
1841
  }
1842
+ if (!session.metrics.endReason) {
1843
+ session.recordEndReason(session.status === "ended" ? "user_hangup" : "agent_hangup");
1844
+ }
1845
+ try {
1846
+ this._controlWs?.send({ event: "call.metrics", callId: session.callId, metrics: session.metrics });
1847
+ } catch {
1848
+ }
1766
1849
  session._emit("call_end");
1767
1850
  session._markEnded();
1768
1851
  this._activeSessions.delete(session.callId);
@@ -1773,20 +1856,13 @@ var ClawOpsAgent = class {
1773
1856
  }
1774
1857
  };
1775
1858
 
1776
- // src/agent/pipeline/openai-realtime.ts
1777
- var OPENAI_REALTIME_URL = "wss://api.openai.com/v1/realtime?model=";
1778
- var HANG_UP_TOOL = {
1779
- type: "function",
1859
+ // src/agent/pipeline/builtin-tool-schemas.ts
1860
+ var HANG_UP = {
1780
1861
  name: "hang_up",
1781
1862
  description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
1782
- parameters: {
1783
- type: "object",
1784
- properties: {},
1785
- required: []
1786
- }
1863
+ parameters: { type: "object", properties: {} }
1787
1864
  };
1788
- var COLLECT_DTMF_TOOL = {
1789
- type: "function",
1865
+ var COLLECT_DTMF = {
1790
1866
  name: "collect_dtmf",
1791
1867
  description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4. \uBC18\uB4DC\uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB34\uC5C7\uC744 \uC785\uB825\uD574\uC57C \uD558\uB294\uC9C0 \uC548\uB0B4\uD55C \uD6C4 \uD638\uCD9C\uD558\uC138\uC694.",
1792
1868
  parameters: {
@@ -1799,8 +1875,7 @@ var COLLECT_DTMF_TOOL = {
1799
1875
  required: ["max_digits"]
1800
1876
  }
1801
1877
  };
1802
- var SEND_DTMF_TOOL = {
1803
- type: "function",
1878
+ var SEND_DTMF = {
1804
1879
  name: "send_dtmf",
1805
1880
  description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
1806
1881
  parameters: {
@@ -1808,492 +1883,419 @@ var SEND_DTMF_TOOL = {
1808
1883
  properties: {
1809
1884
  digits: {
1810
1885
  type: "string",
1811
- description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
1886
+ description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30. \uC608: '1', '1234#', '1w2'"
1812
1887
  }
1813
1888
  },
1814
1889
  required: ["digits"]
1815
1890
  }
1816
1891
  };
1817
- var OpenAIRealtime = class {
1818
- _apiKey;
1819
- _systemPrompt;
1820
- _model;
1821
- _voice;
1822
- _language;
1823
- _eagerness;
1824
- _greeting;
1825
- _log = NOOP_LOGGER;
1826
- _builtinTools = null;
1827
- setLogger(logger) {
1828
- this._log = logger;
1892
+ var TOOL_MAP = /* @__PURE__ */ new Map([
1893
+ ["hang_up" /* HANG_UP */, HANG_UP],
1894
+ ["collect_dtmf" /* COLLECT_DTMF */, COLLECT_DTMF],
1895
+ ["send_dtmf" /* SEND_DTMF */, SEND_DTMF]
1896
+ ]);
1897
+ var BUILTIN_TOOL_NAMES = new Set(
1898
+ Array.from(TOOL_MAP.values()).map((s) => s.name)
1899
+ );
1900
+ function toChatCompletions(schema) {
1901
+ return {
1902
+ type: "function",
1903
+ function: {
1904
+ name: schema.name,
1905
+ description: schema.description,
1906
+ parameters: schema.parameters
1907
+ }
1908
+ };
1909
+ }
1910
+ function toRealtime(schema) {
1911
+ return {
1912
+ type: "function",
1913
+ name: schema.name,
1914
+ description: schema.description,
1915
+ parameters: schema.parameters
1916
+ };
1917
+ }
1918
+ function toGemini(schema) {
1919
+ return {
1920
+ name: schema.name,
1921
+ description: schema.description,
1922
+ parameters: schema.parameters
1923
+ };
1924
+ }
1925
+ var CONVERTERS = {
1926
+ chat: toChatCompletions,
1927
+ realtime: toRealtime,
1928
+ gemini: toGemini
1929
+ };
1930
+ function getBuiltinToolSchemas(builtinTools, fmt) {
1931
+ const converter = CONVERTERS[fmt];
1932
+ const result = [];
1933
+ for (const [toolEnum, schema] of TOOL_MAP) {
1934
+ if (builtinTools === null || builtinTools.has(toolEnum)) {
1935
+ result.push(converter(schema));
1936
+ }
1829
1937
  }
1830
- setBuiltinTools(tools) {
1831
- this._builtinTools = tools;
1938
+ return result;
1939
+ }
1940
+ function isBuiltinTool(name) {
1941
+ return BUILTIN_TOOL_NAMES.has(name);
1942
+ }
1943
+ async function executeBuiltinTool(funcName, args, call) {
1944
+ if (funcName === "hang_up") {
1945
+ await call.hangup();
1946
+ return "";
1832
1947
  }
1833
- _ws = null;
1834
- _call = null;
1948
+ if (funcName === "collect_dtmf") {
1949
+ try {
1950
+ const result = await call.collectDtmf({
1951
+ maxDigits: args["max_digits"] ?? 4,
1952
+ finishOnKey: args["finish_on_key"] ?? "#",
1953
+ timeout: args["timeout"] ?? 5
1954
+ });
1955
+ return result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)";
1956
+ } catch (e) {
1957
+ return `Error: ${e}`;
1958
+ }
1959
+ }
1960
+ if (funcName === "send_dtmf") {
1961
+ try {
1962
+ await call.sendDtmfSequence(args["digits"] ?? "");
1963
+ return "sent";
1964
+ } catch (e) {
1965
+ return `Error: ${e}`;
1966
+ }
1967
+ }
1968
+ return null;
1969
+ }
1970
+
1971
+ // src/agent/pipeline/pipeline-session.ts
1972
+ var PipelineSession = class {
1973
+ _stt;
1974
+ _llm;
1975
+ _tts;
1976
+ _systemPrompt;
1977
+ _greeting;
1978
+ _language;
1979
+ _temperature;
1980
+ _maxTokens;
1981
+ _sampleRate;
1982
+ _interruptOnSpeech;
1983
+ _callSession = null;
1835
1984
  _tools = null;
1836
1985
  _recorder = null;
1837
- _closed = false;
1838
- // Truncation / barge-in tracking (matching Python SDK)
1839
- _lastAssistantItem = null;
1840
- _responseStartTs = null;
1841
- _sentAudioChunks = 0;
1842
- _audioRemainder = Buffer.alloc(0);
1843
- // Response state tracking — prevent sending response.create while one is active
1844
- _responseInProgress = false;
1845
- _onResponseDone = null;
1846
- constructor(options = {}) {
1847
- this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
1848
- this._systemPrompt = options.systemPrompt ?? "";
1849
- this._model = options.model ?? "gpt-realtime-1.5";
1850
- this._voice = options.voice ?? "marin";
1851
- this._language = options.language ?? "ko";
1852
- this._eagerness = options.eagerness ?? "high";
1986
+ _conversation = [];
1987
+ _audioBuffer = [];
1988
+ _running = false;
1989
+ _speaking = false;
1990
+ _builtinTools = null;
1991
+ _log = NOOP_LOGGER;
1992
+ constructor(options) {
1993
+ this._stt = options.stt;
1994
+ this._llm = options.llm;
1995
+ this._tts = options.tts;
1996
+ this._systemPrompt = options.systemPrompt;
1853
1997
  this._greeting = options.greeting ?? true;
1998
+ this._language = options.language ?? "ko";
1999
+ this._temperature = options.temperature;
2000
+ this._maxTokens = options.maxTokens;
2001
+ this._sampleRate = options.sampleRate ?? 8e3;
2002
+ this._interruptOnSpeech = options.interruptOnSpeech ?? true;
2003
+ if (options.toolRegistry) this._tools = options.toolRegistry;
2004
+ if (options.recorder) this._recorder = options.recorder;
1854
2005
  }
1855
- /** Inject per-call ToolRegistry. */
1856
2006
  setToolRegistry(registry) {
1857
2007
  this._tools = registry;
1858
2008
  }
1859
- /** Inject per-call AudioRecorder. */
1860
2009
  setRecorder(recorder) {
1861
2010
  this._recorder = recorder;
1862
2011
  }
1863
- async start(callSession, tools) {
1864
- this._call = callSession;
1865
- if (tools) this._tools = tools;
1866
- this._closed = false;
1867
- this._lastAssistantItem = null;
1868
- this._responseStartTs = null;
1869
- this._sentAudioChunks = 0;
1870
- this._audioRemainder = Buffer.alloc(0);
1871
- if (!this._apiKey) {
1872
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey option.");
2012
+ setBuiltinTools(tools) {
2013
+ this._builtinTools = tools;
2014
+ }
2015
+ getTelemetry() {
2016
+ const llm = this._llm;
2017
+ const stt = this._stt;
2018
+ const tts = this._tts;
2019
+ return {
2020
+ sessionType: "pipeline",
2021
+ llm: llm.provider && llm.model ? { provider: llm.provider, model: llm.model } : null,
2022
+ stt: stt.provider && stt.model ? { provider: stt.provider, model: stt.model } : null,
2023
+ tts: tts.provider && tts.model ? { provider: tts.provider, model: tts.model } : null,
2024
+ voice: tts.voiceId ?? null,
2025
+ language: this._language,
2026
+ greetingEnabled: this._greeting,
2027
+ recordingEnabled: !!this._recorder,
2028
+ toolCount: this._tools?.size ?? 0,
2029
+ mcpServerCount: 0,
2030
+ builtinTools: []
2031
+ };
2032
+ }
2033
+ setLogger(logger) {
2034
+ this._log = logger;
2035
+ if ("setLogger" in this._stt && typeof this._stt.setLogger === "function") {
2036
+ this._stt.setLogger(logger);
1873
2037
  }
1874
- const { WebSocket } = await import('ws');
1875
- const url = `${OPENAI_REALTIME_URL}${this._model}`;
1876
- this._ws = new WebSocket(url, {
1877
- headers: {
1878
- Authorization: `Bearer ${this._apiKey}`,
1879
- "OpenAI-Beta": "realtime=v1"
1880
- }
1881
- });
1882
- return new Promise((resolve, reject) => {
1883
- const ws = this._ws;
1884
- ws.on("open", () => {
1885
- this._sendSessionUpdate();
1886
- this._log.info("OpenAI Realtime connected");
1887
- if (this._greeting) {
1888
- this._send({ type: "response.create" });
1889
- }
1890
- resolve();
1891
- });
1892
- ws.on("message", (data) => {
1893
- try {
1894
- const msg = JSON.parse(data.toString());
1895
- this._handleMessage(msg);
1896
- } catch {
1897
- }
1898
- });
1899
- ws.on("close", () => {
1900
- this._closed = true;
2038
+ if ("setLogger" in this._tts && typeof this._tts.setLogger === "function") {
2039
+ this._tts.setLogger(logger);
2040
+ }
2041
+ }
2042
+ async start(callSession, tools) {
2043
+ this._callSession = callSession;
2044
+ this._tools = tools ?? null;
2045
+ this._running = true;
2046
+ this._log.info("PipelineSession started");
2047
+ this._conversation = [];
2048
+ if (this._systemPrompt) {
2049
+ this._conversation.push({
2050
+ role: "system",
2051
+ content: this._systemPrompt
1901
2052
  });
1902
- ws.on("error", (err) => {
1903
- if (!this._ws) {
1904
- reject(err);
1905
- }
1906
- this._log.error({ err }, "OpenAI Realtime WS error");
2053
+ }
2054
+ if (this._greeting) {
2055
+ this._generateGreeting().catch((err) => {
2056
+ this._log.error({ err }, "Greeting error");
1907
2057
  });
2058
+ }
2059
+ this._runSttLoop().catch((err) => {
2060
+ this._log.error({ err }, "STT loop error");
1908
2061
  });
1909
2062
  }
1910
- async feedDtmf(digits) {
1911
- await this._waitForResponseDone();
1912
- this._send({
1913
- type: "conversation.item.create",
1914
- item: {
1915
- type: "message",
1916
- role: "user",
1917
- content: [{ type: "input_text", text: `[DTMF \uC785\uB825: ${digits}]` }]
1918
- }
1919
- });
1920
- this._send({ type: "response.create" });
1921
- }
1922
2063
  feedAudio(audio) {
1923
- if (this._ws && this._ws.readyState === 1 && !this._closed) {
1924
- this._send({
1925
- type: "input_audio_buffer.append",
1926
- audio: audio.toString("base64")
1927
- });
2064
+ if (this._running) {
2065
+ this._audioBuffer.push(audio);
1928
2066
  }
1929
2067
  }
2068
+ async feedDtmf(digits) {
2069
+ this._conversation.push({
2070
+ role: "user",
2071
+ content: `[DTMF \uC785\uB825: ${digits}]`
2072
+ });
2073
+ await this._respond();
2074
+ }
1930
2075
  async stop() {
1931
- this._closed = true;
1932
- if (this._ws) {
1933
- this._ws.close();
1934
- this._ws = null;
1935
- }
2076
+ this._running = false;
2077
+ this._log.info("PipelineSession stopped");
2078
+ this._audioBuffer = [];
1936
2079
  }
1937
- _sendSessionUpdate() {
1938
- if (!this._ws || this._ws.readyState !== 1) return;
1939
- const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
1940
- if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */))
1941
- toolSchemas.push(HANG_UP_TOOL);
1942
- if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */))
1943
- toolSchemas.push(COLLECT_DTMF_TOOL);
1944
- if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */))
1945
- toolSchemas.push(SEND_DTMF_TOOL);
1946
- this._send({
1947
- type: "session.update",
1948
- session: {
1949
- modalities: ["text", "audio"],
1950
- voice: this._voice,
1951
- instructions: this._systemPrompt,
1952
- input_audio_format: "g711_ulaw",
1953
- output_audio_format: "g711_ulaw",
1954
- input_audio_transcription: {
1955
- model: "whisper-1",
1956
- language: this._language
1957
- },
1958
- input_audio_noise_reduction: { type: "far_field" },
1959
- turn_detection: {
1960
- type: "semantic_vad",
1961
- interrupt_response: true,
1962
- eagerness: this._eagerness
1963
- },
1964
- tools: toolSchemas
2080
+ async _runSttLoop() {
2081
+ const audioStream = this._createAudioStream();
2082
+ for await (const event of this._stt.transcribe(audioStream, {
2083
+ sampleRate: this._sampleRate
2084
+ })) {
2085
+ if (!this._running) break;
2086
+ if (event.type === "interim" && this._speaking && this._interruptOnSpeech) {
2087
+ this._speaking = false;
2088
+ if (this._callSession) {
2089
+ this._callSession.clearAudio();
2090
+ }
2091
+ this._log.info('Barge-in: "%s"', event.transcript.substring(0, 30));
1965
2092
  }
1966
- });
2093
+ if (event.type === "final" && event.transcript.trim()) {
2094
+ this._log.info("STT: %s", event.transcript);
2095
+ await this._handleUserSpeech(event.transcript);
2096
+ }
2097
+ }
1967
2098
  }
1968
- _handleMessage(msg) {
1969
- const type = msg["type"];
1970
- switch (type) {
1971
- case "response.audio.delta": {
1972
- this._handleAudioDelta(msg);
1973
- break;
1974
- }
1975
- case "response.audio.done": {
1976
- if (this._audioRemainder.length > 0) {
1977
- const padded = Buffer.concat([
1978
- this._audioRemainder,
1979
- Buffer.alloc(160 - this._audioRemainder.length, 255)
1980
- ]);
1981
- if (this._call) {
1982
- this._call.sendAudio(padded);
1983
- }
1984
- this._sentAudioChunks++;
1985
- this._audioRemainder = Buffer.alloc(0);
1986
- }
1987
- break;
1988
- }
1989
- case "input_audio_buffer.speech_started": {
1990
- this._handleTruncation();
1991
- break;
1992
- }
1993
- case "response.output_item.done": {
1994
- const item = msg["item"];
1995
- if (item && item["type"] === "function_call") {
1996
- this._handleToolCall(item);
1997
- }
1998
- break;
1999
- }
2000
- case "conversation.item.input_audio_transcription.completed": {
2001
- if (this._call) {
2002
- this._call._emit("transcript", "user", msg["transcript"] ?? "");
2003
- }
2004
- break;
2005
- }
2006
- case "response.audio_transcript.done": {
2007
- if (this._call) {
2008
- this._call._emit("transcript", "assistant", msg["transcript"] ?? "");
2009
- }
2010
- break;
2011
- }
2012
- case "response.created": {
2013
- this._responseInProgress = true;
2014
- break;
2015
- }
2016
- case "response.done": {
2017
- this._responseInProgress = false;
2018
- if (this._onResponseDone) {
2019
- const cb = this._onResponseDone;
2020
- this._onResponseDone = null;
2021
- cb();
2022
- }
2023
- break;
2024
- }
2025
- case "error": {
2026
- this._log.error({ apiError: msg["error"] }, "OpenAI error");
2027
- break;
2099
+ async *_createAudioStream() {
2100
+ while (this._running) {
2101
+ if (this._audioBuffer.length > 0) {
2102
+ const ulaw = this._audioBuffer.shift();
2103
+ const pcm8k = ulawToPcm16(ulaw);
2104
+ const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
2105
+ yield pcm16k;
2106
+ } else {
2107
+ await new Promise((resolve) => setTimeout(resolve, 20));
2028
2108
  }
2029
2109
  }
2030
2110
  }
2031
- _handleAudioDelta(msg) {
2032
- if (this._responseStartTs === null) {
2033
- this._responseStartTs = Date.now();
2034
- this._sentAudioChunks = 0;
2035
- }
2036
- if (msg["item_id"]) {
2037
- this._lastAssistantItem = msg["item_id"];
2038
- }
2039
- const ulaw = Buffer.from(msg["delta"], "base64");
2040
- if (this._recorder) {
2041
- this._recorder.writeOutbound(ulawToPcm16(ulaw));
2042
- }
2043
- const combined = Buffer.concat([this._audioRemainder, ulaw]);
2044
- const chunkSize = 160;
2045
- const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
2046
- for (let off = 0; off < fullEnd; off += chunkSize) {
2047
- if (this._call) {
2048
- this._call.sendAudio(combined.subarray(off, off + chunkSize));
2049
- }
2050
- this._sentAudioChunks++;
2051
- }
2052
- this._audioRemainder = combined.subarray(fullEnd);
2111
+ async _generateGreeting() {
2112
+ await new Promise((resolve) => setTimeout(resolve, 500));
2113
+ await this._respond();
2053
2114
  }
2054
- _handleTruncation() {
2055
- if (!this._lastAssistantItem || this._responseStartTs === null) {
2056
- return;
2057
- }
2058
- const audioEndMs = Math.max(0, this._sentAudioChunks * 20);
2059
- this._send({
2060
- type: "conversation.item.truncate",
2061
- item_id: this._lastAssistantItem,
2062
- content_index: 0,
2063
- audio_end_ms: audioEndMs
2115
+ async _handleUserSpeech(transcript) {
2116
+ this._conversation.push({ role: "user", content: transcript });
2117
+ await this._respond();
2118
+ }
2119
+ _buildEffectiveTools() {
2120
+ const builtinSchemas = getBuiltinToolSchemas(this._builtinTools, "chat");
2121
+ const dtmfSchemas = builtinSchemas.filter((s) => {
2122
+ const name = s["function"]?.["name"];
2123
+ return name === "collect_dtmf" || name === "send_dtmf";
2064
2124
  });
2065
- if (this._call) {
2066
- this._call.clearAudio();
2125
+ if (dtmfSchemas.length === 0) return this._tools ?? void 0;
2126
+ const base = this._tools ? this._tools.fork() : new ToolRegistry();
2127
+ for (const schema of dtmfSchemas) {
2128
+ const fn = schema["function"];
2129
+ const params = fn["parameters"];
2130
+ base.register({
2131
+ name: fn["name"],
2132
+ description: fn["description"],
2133
+ parameters: params["properties"] ?? {},
2134
+ required: params["required"] ?? [],
2135
+ handler: async () => ""
2136
+ });
2067
2137
  }
2068
- this._lastAssistantItem = null;
2069
- this._responseStartTs = null;
2070
- this._sentAudioChunks = 0;
2071
- this._audioRemainder = Buffer.alloc(0);
2138
+ return base;
2072
2139
  }
2073
- async _handleToolCall(item) {
2074
- const funcName = item["name"];
2075
- const callId = item["call_id"];
2076
- this._log.info("Tool call: %s", funcName);
2077
- if (funcName === "hang_up") {
2078
- if (this._call) {
2079
- await this._call.hangup();
2140
+ async _respond() {
2141
+ let fullResponse = "";
2142
+ const textChunks = [];
2143
+ const effectiveTools = this._buildEffectiveTools();
2144
+ const llmStream = this._llm.generate(this._conversation, {
2145
+ tools: effectiveTools,
2146
+ temperature: this._temperature,
2147
+ maxTokens: this._maxTokens
2148
+ });
2149
+ for await (const chunk of llmStream) {
2150
+ if (!this._running) break;
2151
+ if (chunk.type === "text" && chunk.text) {
2152
+ textChunks.push(chunk.text);
2153
+ fullResponse += chunk.text;
2154
+ } else if (chunk.type === "tool_call" && chunk.toolCall) {
2155
+ await this._handleToolCall(chunk);
2080
2156
  }
2081
- return;
2082
2157
  }
2083
- if (funcName === "collect_dtmf") {
2084
- if (this._call) {
2085
- let result2;
2086
- try {
2087
- const args = JSON.parse(item["arguments"] ?? "{}");
2088
- result2 = await this._call.collectDtmf({
2089
- maxDigits: args["max_digits"] ?? 4,
2090
- finishOnKey: args["finish_on_key"] ?? "#",
2091
- timeout: args["timeout"] ?? 5
2092
- });
2093
- } catch (err) {
2094
- result2 = `Error: ${err}`;
2158
+ if (fullResponse.trim()) {
2159
+ this._log.info("Assistant: %s", fullResponse.substring(0, 100));
2160
+ this._conversation.push({ role: "assistant", content: fullResponse });
2161
+ await this._synthesizeAndSend(fullResponse);
2162
+ }
2163
+ }
2164
+ async _handleToolCall(chunk) {
2165
+ if (!chunk.toolCall) return;
2166
+ const { id, name, arguments: argsStr } = chunk.toolCall;
2167
+ try {
2168
+ const args = JSON.parse(argsStr);
2169
+ if (BUILTIN_TOOL_NAMES.has(name) && this._callSession) {
2170
+ const result2 = await executeBuiltinTool(name, args, this._callSession);
2171
+ if (result2 !== null) {
2172
+ if (name === "hang_up") return;
2173
+ this._conversation.push({ role: "tool", content: result2, tool_call_id: id, name });
2174
+ await this._respond();
2175
+ return;
2095
2176
  }
2096
- await this._waitForResponseDone();
2097
- this._send({
2098
- type: "conversation.item.create",
2099
- item: {
2100
- type: "function_call_output",
2101
- call_id: callId,
2102
- output: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)"
2103
- }
2104
- });
2105
- this._send({ type: "response.create" });
2106
2177
  }
2107
- return;
2108
- }
2109
- if (funcName === "send_dtmf") {
2110
- if (this._call) {
2111
- let result2;
2112
- try {
2113
- const args = JSON.parse(item["arguments"] ?? "{}");
2114
- await this._call.sendDtmfSequence(args["digits"] ?? "");
2115
- result2 = "sent";
2116
- } catch (err) {
2117
- result2 = `Error: ${err}`;
2178
+ if (!this._tools) return;
2179
+ this._callSession?.recordToolCall();
2180
+ const result = await this._tools.call(name, args);
2181
+ this._conversation.push({
2182
+ role: "assistant",
2183
+ content: ""
2184
+ // Tool call info stored in the message flow
2185
+ });
2186
+ this._conversation.push({
2187
+ role: "tool",
2188
+ content: typeof result === "string" ? result : JSON.stringify(result),
2189
+ tool_call_id: id,
2190
+ name
2191
+ });
2192
+ const effectiveTools = this._buildEffectiveTools();
2193
+ let followUpText = "";
2194
+ const followUpStream = this._llm.generate(this._conversation, {
2195
+ tools: effectiveTools,
2196
+ temperature: this._temperature,
2197
+ maxTokens: this._maxTokens
2198
+ });
2199
+ for await (const followChunk of followUpStream) {
2200
+ if (!this._running) break;
2201
+ if (followChunk.type === "text" && followChunk.text) {
2202
+ followUpText += followChunk.text;
2118
2203
  }
2119
- await this._waitForResponseDone();
2120
- this._send({
2121
- type: "conversation.item.create",
2122
- item: {
2123
- type: "function_call_output",
2124
- call_id: callId,
2125
- output: result2
2126
- }
2127
- });
2128
- this._send({ type: "response.create" });
2129
2204
  }
2130
- return;
2131
- }
2132
- if (!this._tools || !this._tools.has(funcName)) {
2133
- this._log.error("Unknown tool: %s", funcName);
2134
- return;
2135
- }
2136
- let result;
2137
- try {
2138
- const args = JSON.parse(item["arguments"] ?? "{}");
2139
- result = await this._tools.call(funcName, args);
2140
- } catch (err) {
2141
- this._log.error({ err }, "Tool call failed: %s", funcName);
2142
- result = `Error: ${err}`;
2143
- }
2144
- await this._waitForResponseDone();
2145
- this._send({
2146
- type: "conversation.item.create",
2147
- item: {
2148
- type: "function_call_output",
2149
- call_id: callId,
2150
- output: typeof result === "string" ? result : JSON.stringify(result)
2205
+ if (followUpText.trim()) {
2206
+ this._conversation.push({ role: "assistant", content: followUpText });
2207
+ await this._synthesizeAndSend(followUpText);
2151
2208
  }
2152
- });
2153
- this._send({ type: "response.create" });
2154
- }
2155
- _waitForResponseDone() {
2156
- if (!this._responseInProgress) return Promise.resolve();
2157
- return new Promise((resolve) => {
2158
- this._onResponseDone = resolve;
2159
- });
2160
- }
2161
- _send(data) {
2162
- if (this._ws && this._ws.readyState === 1 && !this._closed) {
2163
- this._ws.send(JSON.stringify(data));
2164
- }
2165
- }
2166
- };
2167
-
2168
- // src/agent/pipeline/gemini-realtime.ts
2169
- var HANG_UP_TOOL2 = {
2170
- name: "hang_up",
2171
- description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
2172
- parameters: { type: "object", properties: {} }
2173
- };
2174
- var COLLECT_DTMF_TOOL2 = {
2175
- name: "collect_dtmf",
2176
- description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4. \uBC18\uB4DC\uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB34\uC5C7\uC744 \uC785\uB825\uD574\uC57C \uD558\uB294\uC9C0 \uC548\uB0B4\uD55C \uD6C4 \uD638\uCD9C\uD558\uC138\uC694.",
2177
- parameters: {
2178
- type: "object",
2179
- properties: {
2180
- max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
2181
- finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
2182
- timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
2183
- },
2184
- required: ["max_digits"]
2185
- }
2186
- };
2187
- var SEND_DTMF_TOOL2 = {
2188
- name: "send_dtmf",
2189
- description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
2190
- parameters: {
2191
- type: "object",
2192
- properties: {
2193
- digits: {
2194
- type: "string",
2195
- description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
2209
+ } catch (err) {
2210
+ this._log.error({ err }, "Tool call failed: %s", name);
2211
+ if (err instanceof Error) {
2212
+ this._callSession?.recordToolError(err);
2196
2213
  }
2197
- },
2198
- required: ["digits"]
2199
- }
2200
- };
2201
- function resolveRef(ref, defs) {
2202
- const parts = ref.replace(/^#\//, "").split("/");
2203
- let result = defs;
2204
- for (const part of parts) {
2205
- if (result && typeof result === "object" && !Array.isArray(result)) {
2206
- result = result[part];
2207
- } else {
2208
- return {};
2209
2214
  }
2210
2215
  }
2211
- return typeof result === "object" && result !== null && !Array.isArray(result) ? result : {};
2212
- }
2213
- function sanitizeSchemaForGemini(schema, defs, depth = 0) {
2214
- if (depth > 15) return { type: "object", properties: {} };
2215
- if (!schema || typeof schema !== "object") return { type: "object", properties: {} };
2216
- if (defs === void 0) {
2217
- defs = schema["$defs"] ?? schema["definitions"] ?? {};
2218
- }
2219
- if (typeof schema["$ref"] === "string") {
2220
- const resolved = resolveRef(schema["$ref"], { $defs: defs, definitions: defs });
2221
- if (resolved && Object.keys(resolved).length > 0) {
2222
- return sanitizeSchemaForGemini(resolved, defs, depth + 1);
2223
- }
2224
- return { type: "object", properties: {} };
2225
- }
2226
- for (const comboKey of ["oneOf", "anyOf", "allOf"]) {
2227
- const variants = schema[comboKey];
2228
- if (Array.isArray(variants) && variants.length > 0) {
2229
- for (const v of variants) {
2230
- if (v && typeof v === "object") {
2231
- const resolved = sanitizeSchemaForGemini(v, defs, depth + 1);
2232
- if (resolved["type"] === "object" && resolved["properties"]) {
2233
- return resolved;
2216
+ async _synthesizeAndSend(text) {
2217
+ if (!this._callSession || !this._running) return;
2218
+ this._speaking = true;
2219
+ try {
2220
+ for await (const audioChunk of this._tts.synthesize(text, {
2221
+ sampleRate: this._sampleRate
2222
+ })) {
2223
+ if (!this._running || !this._speaking) break;
2224
+ if (this._recorder) {
2225
+ const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2226
+ this._recorder.writeOutbound(pcm8k2);
2227
+ }
2228
+ const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2229
+ const ulaw = pcm16ToUlaw(pcm8k);
2230
+ for (let off = 0; off < ulaw.length; off += 160) {
2231
+ let chunk = ulaw.subarray(off, off + 160);
2232
+ if (chunk.length < 160) {
2233
+ chunk = Buffer.concat([chunk, Buffer.alloc(160 - chunk.length, 255)]);
2234
2234
  }
2235
+ this._callSession.sendAudio(chunk);
2235
2236
  }
2236
2237
  }
2237
- const first = variants[0];
2238
- if (first && typeof first === "object") {
2239
- return sanitizeSchemaForGemini(first, defs, depth + 1);
2240
- }
2241
- }
2242
- }
2243
- const result = {};
2244
- let schemaType = schema["type"];
2245
- if (Array.isArray(schemaType)) {
2246
- const nonNull = schemaType.filter((t) => t !== "null");
2247
- schemaType = nonNull[0] ?? "string";
2248
- }
2249
- if (schemaType) result["type"] = schemaType;
2250
- if (schema["description"]) result["description"] = schema["description"];
2251
- if (schema["enum"]) result["enum"] = schema["enum"];
2252
- if (schema["required"]) result["required"] = schema["required"];
2253
- if (schema["properties"] && typeof schema["properties"] === "object") {
2254
- const props = {};
2255
- for (const [key, val] of Object.entries(schema["properties"])) {
2256
- if (val && typeof val === "object") {
2257
- props[key] = sanitizeSchemaForGemini(val, defs, depth + 1);
2258
- }
2238
+ } catch (err) {
2239
+ this._log.error({ err }, "TTS error");
2240
+ } finally {
2241
+ this._speaking = false;
2259
2242
  }
2260
- result["properties"] = props;
2261
- }
2262
- if (schema["items"] && typeof schema["items"] === "object" && !Array.isArray(schema["items"])) {
2263
- result["items"] = sanitizeSchemaForGemini(
2264
- schema["items"],
2265
- defs,
2266
- depth + 1
2267
- );
2268
2243
  }
2269
- if (!result["type"] && result["properties"]) result["type"] = "object";
2270
- if (result["type"] === "object" && !result["properties"]) result["properties"] = {};
2271
- return result;
2272
- }
2273
- var GeminiRealtime = class {
2244
+ };
2245
+
2246
+ // src/agent/pipeline/realtime/openai-realtime.ts
2247
+ var OPENAI_REALTIME_URL = "wss://api.openai.com/v1/realtime?model=";
2248
+ var OpenAIRealtime = class {
2274
2249
  _apiKey;
2275
2250
  _systemPrompt;
2276
2251
  _model;
2277
2252
  _voice;
2278
2253
  _language;
2254
+ _turnDetection;
2279
2255
  _greeting;
2280
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2281
- _session = null;
2256
+ _log = NOOP_LOGGER;
2257
+ _builtinTools = null;
2258
+ setLogger(logger) {
2259
+ this._log = logger;
2260
+ }
2261
+ setBuiltinTools(tools) {
2262
+ this._builtinTools = tools;
2263
+ }
2264
+ getTelemetry() {
2265
+ return {
2266
+ sessionType: "openai_realtime",
2267
+ llm: { provider: "openai", model: this._model },
2268
+ stt: null,
2269
+ tts: null,
2270
+ voice: this._voice,
2271
+ language: this._language,
2272
+ greetingEnabled: this._greeting,
2273
+ recordingEnabled: !!this._recorder,
2274
+ toolCount: this._tools?.size ?? 0,
2275
+ mcpServerCount: 0,
2276
+ builtinTools: []
2277
+ };
2278
+ }
2279
+ _ws = null;
2282
2280
  _call = null;
2283
2281
  _tools = null;
2284
2282
  _recorder = null;
2285
2283
  _closed = false;
2286
- _sentAudioChunks = 0;
2287
- _audioRemainder = Buffer.alloc(0);
2288
- _builtinTools = null;
2289
- _toolCallInProgress = false;
2290
- _log = NOOP_LOGGER;
2284
+ // PlaybackState — 현재 재생 중인 응답 상태
2285
+ _playback = null;
2286
+ _latestMediaTs = 0;
2287
+ // Pending tool call tracking — 인터럽트 시 취소용
2288
+ _pendingToolCalls = /* @__PURE__ */ new Map();
2289
+ // Response state tracking — prevent sending response.create while one is active
2290
+ _responseInProgress = false;
2291
+ _onResponseDone = null;
2291
2292
  constructor(options = {}) {
2292
- this._apiKey = options.apiKey ?? process.env["GOOGLE_API_KEY"] ?? "";
2293
+ this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
2293
2294
  this._systemPrompt = options.systemPrompt ?? "";
2294
- this._model = options.model ?? "gemini-2.5-flash-native-audio-preview-12-2025";
2295
- this._voice = options.voice ?? "Kore";
2295
+ this._model = options.model ?? "gpt-realtime-1.5";
2296
+ this._voice = options.voice ?? "marin";
2296
2297
  this._language = options.language ?? "ko";
2298
+ this._turnDetection = options.turnDetection !== void 0 ? options.turnDetection : { type: "semantic_vad", eagerness: "medium", interrupt_response: true };
2297
2299
  this._greeting = options.greeting ?? true;
2298
2300
  }
2299
2301
  /** Inject per-call ToolRegistry. */
@@ -2304,340 +2306,409 @@ var GeminiRealtime = class {
2304
2306
  setRecorder(recorder) {
2305
2307
  this._recorder = recorder;
2306
2308
  }
2307
- setBuiltinTools(tools) {
2308
- this._builtinTools = tools;
2309
- }
2310
- setLogger(logger) {
2311
- this._log = logger;
2312
- }
2313
2309
  async start(callSession, tools) {
2314
2310
  this._call = callSession;
2315
2311
  if (tools) this._tools = tools;
2316
2312
  this._closed = false;
2317
- this._sentAudioChunks = 0;
2318
- this._audioRemainder = Buffer.alloc(0);
2313
+ this._playback = null;
2314
+ this._latestMediaTs = 0;
2319
2315
  if (!this._apiKey) {
2320
- throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey option.");
2321
- }
2322
- const { GoogleGenAI } = await import('@google/genai/node');
2323
- const client = new GoogleGenAI({ apiKey: this._apiKey });
2324
- const config = {
2325
- responseModalities: ["AUDIO"],
2326
- speechConfig: {
2327
- voiceConfig: {
2328
- prebuiltVoiceConfig: {
2329
- voiceName: this._voice
2330
- }
2331
- }
2332
- },
2333
- inputAudioTranscription: {},
2334
- outputAudioTranscription: {}
2335
- };
2336
- if (this._systemPrompt) {
2337
- config["systemInstruction"] = this._systemPrompt;
2338
- }
2339
- const toolSchemas = this._buildToolSchemas();
2340
- if (toolSchemas.length > 0) {
2341
- config["tools"] = [{ functionDeclarations: toolSchemas }];
2316
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey option.");
2342
2317
  }
2343
- this._session = await client.live.connect({
2344
- model: this._model,
2345
- config,
2346
- callbacks: {
2347
- onmessage: (msg) => this._handleMessage(msg),
2348
- onerror: (err) => {
2349
- this._log.error({ err }, "Gemini SDK error");
2350
- },
2351
- onclose: (ev) => {
2352
- this._log.info({ code: ev?.code ?? "unknown" }, "Gemini connection closed");
2353
- this._closed = true;
2354
- }
2318
+ const { WebSocket } = await import('ws');
2319
+ const url = `${OPENAI_REALTIME_URL}${this._model}`;
2320
+ this._ws = new WebSocket(url, {
2321
+ headers: {
2322
+ Authorization: `Bearer ${this._apiKey}`,
2323
+ "OpenAI-Beta": "realtime=v1"
2355
2324
  }
2356
2325
  });
2357
- if (this._greeting) {
2358
- this._session.sendClientContent({
2359
- turns: [
2360
- {
2361
- role: "user",
2362
- parts: [{ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." }]
2363
- }
2364
- ],
2365
- turnComplete: true
2326
+ return new Promise((resolve, reject) => {
2327
+ const ws = this._ws;
2328
+ ws.on("open", () => {
2329
+ this._sendSessionUpdate();
2330
+ this._log.info("OpenAI Realtime connected");
2331
+ if (this._greeting) {
2332
+ this._send({ type: "response.create" });
2333
+ }
2334
+ resolve();
2366
2335
  });
2367
- }
2368
- }
2369
- feedAudio(audio) {
2370
- if (this._session && !this._closed && !this._toolCallInProgress) {
2371
- const pcm8k = ulawToPcm16(audio);
2372
- if (this._recorder) {
2373
- this._recorder.writeInbound(pcm8k);
2374
- }
2375
- const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
2376
- this._session.sendRealtimeInput({
2377
- audio: {
2378
- data: Buffer.from(pcm16k).toString("base64"),
2379
- mimeType: "audio/pcm;rate=16000"
2336
+ ws.on("message", (data) => {
2337
+ try {
2338
+ const msg = JSON.parse(data.toString());
2339
+ this._handleMessage(msg);
2340
+ } catch {
2341
+ }
2342
+ });
2343
+ ws.on("close", () => {
2344
+ this._closed = true;
2345
+ });
2346
+ ws.on("error", (err) => {
2347
+ if (!this._ws) {
2348
+ reject(err);
2380
2349
  }
2350
+ this._log.error({ err }, "OpenAI Realtime WS error");
2381
2351
  });
2382
- }
2352
+ });
2383
2353
  }
2384
2354
  async feedDtmf(digits) {
2385
- if (this._session) {
2386
- this._session.sendClientContent({
2387
- turns: [{ role: "user", parts: [{ text: `[DTMF \uC785\uB825: ${digits}]` }] }],
2388
- turnComplete: true
2355
+ await this._waitForResponseDone();
2356
+ this._send({
2357
+ type: "conversation.item.create",
2358
+ item: {
2359
+ type: "message",
2360
+ role: "user",
2361
+ content: [{ type: "input_text", text: `[DTMF \uC785\uB825: ${digits}]` }]
2362
+ }
2363
+ });
2364
+ this._send({ type: "response.create" });
2365
+ }
2366
+ feedAudio(audio) {
2367
+ this._latestMediaTs = Date.now();
2368
+ if (this._ws && this._ws.readyState === 1 && !this._closed) {
2369
+ this._send({
2370
+ type: "input_audio_buffer.append",
2371
+ audio: audio.toString("base64")
2389
2372
  });
2390
2373
  }
2391
2374
  }
2392
2375
  async stop() {
2393
2376
  this._closed = true;
2394
- if (this._session) {
2395
- try {
2396
- this._session.close();
2397
- } catch {
2398
- }
2399
- this._session = null;
2377
+ for (const [, controller] of this._pendingToolCalls) {
2378
+ controller.abort();
2379
+ }
2380
+ this._pendingToolCalls.clear();
2381
+ if (this._ws) {
2382
+ this._ws.close();
2383
+ this._ws = null;
2400
2384
  }
2401
2385
  }
2402
- _buildToolSchemas() {
2403
- const toolDefs = this._tools ? this._tools.toOpenAITools().map((t) => ({
2404
- name: t.function.name,
2405
- description: t.function.description,
2406
- parameters: sanitizeSchemaForGemini(
2407
- t.function.parameters ?? { type: "object", properties: {} }
2408
- )
2409
- })) : [];
2410
- if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */)) toolDefs.push(HANG_UP_TOOL2);
2411
- if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */)) toolDefs.push(COLLECT_DTMF_TOOL2);
2412
- if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */)) toolDefs.push(SEND_DTMF_TOOL2);
2413
- return toolDefs;
2386
+ _sendSessionUpdate() {
2387
+ if (!this._ws || this._ws.readyState !== 1) return;
2388
+ const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
2389
+ toolSchemas.push(...getBuiltinToolSchemas(this._builtinTools, "realtime"));
2390
+ this._send({
2391
+ type: "session.update",
2392
+ session: {
2393
+ modalities: ["text", "audio"],
2394
+ voice: this._voice,
2395
+ instructions: this._systemPrompt,
2396
+ input_audio_format: "g711_ulaw",
2397
+ output_audio_format: "g711_ulaw",
2398
+ input_audio_transcription: {
2399
+ model: "whisper-1",
2400
+ language: this._language
2401
+ },
2402
+ input_audio_noise_reduction: { type: "far_field" },
2403
+ turn_detection: this._turnDetection,
2404
+ tools: toolSchemas
2405
+ }
2406
+ });
2414
2407
  }
2415
2408
  _handleMessage(msg) {
2416
- if (!this._call) return;
2417
- const serverContent = msg.serverContent;
2418
- if (serverContent) {
2419
- const modelTurn = serverContent.modelTurn;
2420
- if (modelTurn) {
2421
- for (const part of modelTurn.parts ?? []) {
2422
- const inlineData = part.inlineData;
2423
- if (inlineData?.data) {
2424
- const mimeType = inlineData.mimeType ?? "";
2425
- if (mimeType.includes("audio")) {
2426
- this._handleAudioData(inlineData.data);
2409
+ const type = msg["type"];
2410
+ switch (type) {
2411
+ case "response.audio.delta": {
2412
+ this._handleAudioDelta(msg);
2413
+ break;
2414
+ }
2415
+ case "response.audio.done": {
2416
+ if (this._playback) {
2417
+ this._playback.generating = false;
2418
+ if (this._playback.audioRemainder.length > 0) {
2419
+ const padded = Buffer.concat([
2420
+ this._playback.audioRemainder,
2421
+ Buffer.alloc(160 - this._playback.audioRemainder.length, 255)
2422
+ ]);
2423
+ if (this._call) {
2424
+ this._call.sendAudio(padded);
2427
2425
  }
2426
+ this._playback.sentChunks++;
2427
+ this._playback.audioRemainder = Buffer.alloc(0);
2428
2428
  }
2429
2429
  }
2430
+ break;
2430
2431
  }
2431
- if (serverContent.turnComplete) {
2432
- this._log.debug("Turn complete");
2433
- this._flushAudioRemainder();
2432
+ case "input_audio_buffer.speech_started": {
2433
+ this._handleTruncation();
2434
+ break;
2434
2435
  }
2435
- if (serverContent.interrupted) {
2436
- this._log.info("Barge-in detected");
2436
+ case "response.output_item.done": {
2437
+ const item = msg["item"];
2438
+ if (item && item["type"] === "function_call") {
2439
+ this._handleToolCall(item);
2440
+ }
2441
+ break;
2442
+ }
2443
+ case "conversation.item.input_audio_transcription.completed": {
2437
2444
  if (this._call) {
2438
- this._call.clearAudio();
2445
+ this._call._emit("transcript", "user", msg["transcript"] ?? "");
2439
2446
  }
2440
- this._sentAudioChunks = 0;
2441
- this._audioRemainder = Buffer.alloc(0);
2447
+ break;
2442
2448
  }
2443
- const inputText = serverContent.inputTranscription?.text;
2444
- if (inputText && this._call) {
2445
- this._log.info("User: %s", inputText);
2446
- this._call._emit("transcript", "user", inputText);
2449
+ case "response.audio_transcript.done": {
2450
+ if (this._call) {
2451
+ this._call._emit("transcript", "assistant", msg["transcript"] ?? "");
2452
+ }
2453
+ break;
2447
2454
  }
2448
- const outputText = serverContent.outputTranscription?.text;
2449
- if (outputText && this._call) {
2450
- this._log.info("Assistant: %s", outputText);
2451
- this._call._emit("transcript", "assistant", outputText);
2455
+ case "response.created": {
2456
+ this._responseInProgress = true;
2457
+ break;
2458
+ }
2459
+ case "response.done": {
2460
+ this._responseInProgress = false;
2461
+ if (this._onResponseDone) {
2462
+ const cb = this._onResponseDone;
2463
+ this._onResponseDone = null;
2464
+ cb();
2465
+ }
2466
+ break;
2467
+ }
2468
+ case "error": {
2469
+ this._log.error({ apiError: msg["error"] }, "OpenAI error");
2470
+ break;
2452
2471
  }
2453
- }
2454
- if (msg.toolCall) {
2455
- this._handleToolCall(msg.toolCall);
2456
- }
2457
- const toolCancellation = msg["toolCallCancellation"];
2458
- if (toolCancellation) {
2459
- this._log.info({ ids: toolCancellation.ids }, "Tool call cancelled");
2460
2472
  }
2461
2473
  }
2462
- _handleAudioData(b64Data) {
2463
- if (!this._call) return;
2464
- const pcm24k = Buffer.from(b64Data, "base64");
2474
+ _handleAudioDelta(msg) {
2475
+ if (this._playback === null) {
2476
+ this._playback = {
2477
+ itemId: msg["item_id"] || "",
2478
+ startTs: this._latestMediaTs || Date.now(),
2479
+ sentChunks: 0,
2480
+ generating: true,
2481
+ audioRemainder: Buffer.alloc(0)
2482
+ };
2483
+ } else if (msg["item_id"]) {
2484
+ this._playback.itemId = msg["item_id"];
2485
+ }
2486
+ const pb = this._playback;
2487
+ const ulaw = Buffer.from(msg["delta"], "base64");
2465
2488
  if (this._recorder) {
2466
- this._recorder.writeOutbound(resamplePcm16(pcm24k, 24e3, 8e3));
2489
+ this._recorder.writeOutbound(ulawToPcm16(ulaw));
2467
2490
  }
2468
- const pcm8k = resamplePcm16(pcm24k, 24e3, 8e3);
2469
- const ulaw = pcm16ToUlaw(pcm8k);
2470
- const combined = Buffer.concat([this._audioRemainder, ulaw]);
2491
+ const combined = Buffer.concat([pb.audioRemainder, ulaw]);
2471
2492
  const chunkSize = 160;
2472
2493
  const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
2473
- if (fullEnd > 0) {
2474
- this._call.sendAudio(combined.subarray(0, fullEnd));
2475
- this._sentAudioChunks += fullEnd / chunkSize;
2494
+ for (let off = 0; off < fullEnd; off += chunkSize) {
2495
+ if (this._call) {
2496
+ this._call.sendAudio(combined.subarray(off, off + chunkSize));
2497
+ }
2498
+ pb.sentChunks++;
2476
2499
  }
2477
- this._audioRemainder = combined.subarray(fullEnd);
2500
+ pb.audioRemainder = combined.subarray(fullEnd);
2478
2501
  }
2479
- _flushAudioRemainder() {
2480
- if (this._audioRemainder.length > 0 && this._call) {
2481
- const padded = Buffer.concat([
2482
- this._audioRemainder,
2483
- Buffer.alloc(160 - this._audioRemainder.length, 255)
2484
- ]);
2485
- this._call.sendAudio(padded);
2486
- this._sentAudioChunks++;
2487
- this._audioRemainder = Buffer.alloc(0);
2502
+ _handleTruncation() {
2503
+ for (const [, controller] of this._pendingToolCalls) {
2504
+ controller.abort();
2505
+ }
2506
+ this._pendingToolCalls.clear();
2507
+ if (this._call) {
2508
+ this._call.clearAudio();
2509
+ }
2510
+ const pb = this._playback;
2511
+ if (pb === null) {
2512
+ return;
2488
2513
  }
2514
+ const playedMs = Math.max(0, (this._latestMediaTs || Date.now()) - pb.startTs);
2515
+ this._log.info(
2516
+ "[Interrupt] item=%s played=%dms total=%dms",
2517
+ pb.itemId,
2518
+ playedMs,
2519
+ pb.sentChunks * 20
2520
+ );
2521
+ this._playback = null;
2489
2522
  }
2490
- async _handleToolCall(toolCall) {
2491
- const functionCalls = toolCall.functionCalls;
2492
- if (!functionCalls) return;
2493
- this._toolCallInProgress = true;
2494
- const responses = [];
2495
- for (const fc of functionCalls) {
2496
- const name = fc.name ?? "";
2497
- const fcId = fc.id ?? "";
2498
- const args = fc.args ?? {};
2499
- this._log.info({ tool: name, args }, "Tool call: %s", name);
2500
- if (name === "hang_up") {
2501
- this._log.info("hang_up: ending call");
2502
- if (this._call) {
2503
- await this._call.hangup();
2523
+ async _handleToolCall(item) {
2524
+ const funcName = item["name"];
2525
+ const callId = item["call_id"];
2526
+ this._log.info("Tool call: %s", funcName);
2527
+ const controller = new AbortController();
2528
+ this._pendingToolCalls.set(callId, controller);
2529
+ try {
2530
+ if (BUILTIN_TOOL_NAMES.has(funcName) && this._call) {
2531
+ const args = JSON.parse(item["arguments"] ?? "{}");
2532
+ const result2 = await executeBuiltinTool(funcName, args, this._call);
2533
+ if (result2 !== null) {
2534
+ if (funcName === "hang_up") return;
2535
+ if (controller.signal.aborted) return;
2536
+ await this._waitForResponseDone();
2537
+ this._send({
2538
+ type: "conversation.item.create",
2539
+ item: {
2540
+ type: "function_call_output",
2541
+ call_id: callId,
2542
+ output: result2
2543
+ }
2544
+ });
2545
+ this._send({ type: "response.create" });
2546
+ return;
2504
2547
  }
2505
- return;
2506
2548
  }
2507
- if (name === "collect_dtmf") {
2508
- if (this._call) {
2509
- let result;
2510
- try {
2511
- this._log.info({ maxDigits: args["max_digits"] ?? 4, timeout: args["timeout"] ?? 5 }, "collect_dtmf: waiting for digits");
2512
- result = await this._call.collectDtmf({
2513
- maxDigits: args["max_digits"] ?? 4,
2514
- finishOnKey: args["finish_on_key"] ?? "#",
2515
- timeout: args["timeout"] ?? 5
2516
- });
2517
- this._log.info("DTMF collected: %s", result || "(empty)");
2518
- } catch (err) {
2519
- this._log.error({ err }, "collect_dtmf error");
2520
- result = `Error: ${err}`;
2549
+ if (!this._tools || !this._tools.has(funcName)) {
2550
+ this._log.error("Unknown tool: %s", funcName);
2551
+ await this._waitForResponseDone();
2552
+ this._send({
2553
+ type: "conversation.item.create",
2554
+ item: {
2555
+ type: "function_call_output",
2556
+ call_id: callId,
2557
+ output: JSON.stringify({ error: `Unknown tool: ${funcName}` })
2521
2558
  }
2522
- responses.push({
2523
- id: fcId,
2524
- name,
2525
- response: { result: result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)" }
2526
- });
2559
+ });
2560
+ this._send({ type: "response.create" });
2561
+ return;
2562
+ }
2563
+ let result;
2564
+ try {
2565
+ const args = JSON.parse(item["arguments"] ?? "{}");
2566
+ this._call?.recordToolCall();
2567
+ result = await this._tools.call(funcName, args);
2568
+ } catch (err) {
2569
+ this._log.error({ err }, "Tool call failed: %s", funcName);
2570
+ if (err instanceof Error) {
2571
+ this._call?.recordToolError(err);
2527
2572
  }
2528
- continue;
2573
+ result = `Error: ${err}`;
2529
2574
  }
2530
- if (name === "send_dtmf") {
2531
- if (this._call) {
2532
- let result;
2533
- try {
2534
- this._log.info('send_dtmf: digits="%s"', args["digits"] ?? "");
2535
- await this._call.sendDtmfSequence(args["digits"] ?? "");
2536
- result = "sent";
2537
- this._log.info("send_dtmf: sent");
2538
- } catch (err) {
2539
- this._log.error({ err }, "send_dtmf error");
2540
- result = `Error: ${err}`;
2575
+ if (controller.signal.aborted) {
2576
+ this._log.info("Tool call cancelled (user interrupted): %s", funcName);
2577
+ return;
2578
+ }
2579
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
2580
+ await this._waitForResponseDone();
2581
+ this._send({
2582
+ type: "conversation.item.create",
2583
+ item: {
2584
+ type: "function_call_output",
2585
+ call_id: callId,
2586
+ output: resultStr
2587
+ }
2588
+ });
2589
+ this._log.info("[ToolResult] %s call_id=%s len=%d", funcName, callId, resultStr.length);
2590
+ this._send({ type: "response.create" });
2591
+ } finally {
2592
+ this._pendingToolCalls.delete(callId);
2593
+ }
2594
+ }
2595
+ _waitForResponseDone() {
2596
+ if (!this._responseInProgress) return Promise.resolve();
2597
+ return new Promise((resolve) => {
2598
+ this._onResponseDone = resolve;
2599
+ });
2600
+ }
2601
+ _send(data) {
2602
+ if (this._ws && this._ws.readyState === 1 && !this._closed) {
2603
+ this._ws.send(JSON.stringify(data));
2604
+ }
2605
+ }
2606
+ };
2607
+
2608
+ // src/agent/pipeline/realtime/gemini-realtime.ts
2609
+ function resolveRef(ref, defs) {
2610
+ const parts = ref.replace(/^#\//, "").split("/");
2611
+ let result = defs;
2612
+ for (const part of parts) {
2613
+ if (result && typeof result === "object" && !Array.isArray(result)) {
2614
+ result = result[part];
2615
+ } else {
2616
+ return {};
2617
+ }
2618
+ }
2619
+ return typeof result === "object" && result !== null && !Array.isArray(result) ? result : {};
2620
+ }
2621
+ function sanitizeSchemaForGemini(schema, defs, depth = 0) {
2622
+ if (depth > 15) return { type: "object", properties: {} };
2623
+ if (!schema || typeof schema !== "object") return { type: "object", properties: {} };
2624
+ if (defs === void 0) {
2625
+ defs = schema["$defs"] ?? schema["definitions"] ?? {};
2626
+ }
2627
+ if (typeof schema["$ref"] === "string") {
2628
+ const resolved = resolveRef(schema["$ref"], { $defs: defs, definitions: defs });
2629
+ if (resolved && Object.keys(resolved).length > 0) {
2630
+ return sanitizeSchemaForGemini(resolved, defs, depth + 1);
2631
+ }
2632
+ return { type: "object", properties: {} };
2633
+ }
2634
+ for (const comboKey of ["oneOf", "anyOf", "allOf"]) {
2635
+ const variants = schema[comboKey];
2636
+ if (Array.isArray(variants) && variants.length > 0) {
2637
+ for (const v of variants) {
2638
+ if (v && typeof v === "object") {
2639
+ const resolved = sanitizeSchemaForGemini(v, defs, depth + 1);
2640
+ if (resolved["type"] === "object" && resolved["properties"]) {
2641
+ return resolved;
2541
2642
  }
2542
- responses.push({ id: fcId, name, response: { result } });
2543
2643
  }
2544
- continue;
2545
- }
2546
- if (!this._tools || !this._tools.has(name)) {
2547
- this._log.error("Unknown tool: %s", name);
2548
- responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
2549
- continue;
2550
2644
  }
2551
- try {
2552
- const result = await this._tools.call(name, args);
2553
- const resultStr = typeof result === "string" ? result : JSON.stringify(result);
2554
- this._log.info("Tool result: %s -> %s", name, resultStr.substring(0, 200));
2555
- responses.push({
2556
- id: fcId,
2557
- name,
2558
- response: { result: resultStr }
2559
- });
2560
- } catch (err) {
2561
- this._log.error({ err }, "Tool call failed: %s", name);
2562
- responses.push({
2563
- id: fcId,
2564
- name,
2565
- response: { error: String(err) }
2566
- });
2645
+ const first = variants[0];
2646
+ if (first && typeof first === "object") {
2647
+ return sanitizeSchemaForGemini(first, defs, depth + 1);
2567
2648
  }
2568
2649
  }
2569
- if (responses.length > 0 && this._session) {
2570
- this._log.debug("Sending %d tool response(s)", responses.length);
2571
- this._session.sendToolResponse({
2572
- functionResponses: responses
2573
- });
2574
- }
2575
- this._toolCallInProgress = false;
2576
2650
  }
2577
- };
2578
-
2579
- // src/agent/pipeline/pipeline-session.ts
2580
- var COLLECT_DTMF_TOOL3 = {
2581
- function: {
2582
- description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4.",
2583
- parameters: {
2584
- properties: {
2585
- max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
2586
- finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
2587
- timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
2588
- },
2589
- required: ["max_digits"]
2590
- }
2651
+ const result = {};
2652
+ let schemaType = schema["type"];
2653
+ if (Array.isArray(schemaType)) {
2654
+ const nonNull = schemaType.filter((t) => t !== "null");
2655
+ schemaType = nonNull[0] ?? "string";
2591
2656
  }
2592
- };
2593
- var SEND_DTMF_TOOL3 = {
2594
- function: {
2595
- description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
2596
- parameters: {
2597
- properties: {
2598
- digits: { type: "string", description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30." }
2599
- },
2600
- required: ["digits"]
2657
+ if (schemaType) result["type"] = schemaType;
2658
+ if (schema["description"]) result["description"] = schema["description"];
2659
+ if (schema["enum"]) result["enum"] = schema["enum"];
2660
+ if (schema["required"]) result["required"] = schema["required"];
2661
+ if (schema["properties"] && typeof schema["properties"] === "object") {
2662
+ const props = {};
2663
+ for (const [key, val] of Object.entries(schema["properties"])) {
2664
+ if (val && typeof val === "object") {
2665
+ props[key] = sanitizeSchemaForGemini(val, defs, depth + 1);
2666
+ }
2601
2667
  }
2668
+ result["properties"] = props;
2602
2669
  }
2603
- };
2604
- var PipelineSession = class {
2605
- _stt;
2606
- _llm;
2607
- _tts;
2670
+ if (schema["items"] && typeof schema["items"] === "object" && !Array.isArray(schema["items"])) {
2671
+ result["items"] = sanitizeSchemaForGemini(
2672
+ schema["items"],
2673
+ defs,
2674
+ depth + 1
2675
+ );
2676
+ }
2677
+ if (!result["type"] && result["properties"]) result["type"] = "object";
2678
+ if (result["type"] === "object" && !result["properties"]) result["properties"] = {};
2679
+ return result;
2680
+ }
2681
+ var GeminiRealtime = class {
2682
+ _apiKey;
2608
2683
  _systemPrompt;
2609
- _greeting;
2684
+ _model;
2685
+ _voice;
2610
2686
  _language;
2611
- _temperature;
2612
- _maxTokens;
2613
- _sampleRate;
2614
- _interruptOnSpeech;
2615
- _callSession = null;
2687
+ _greeting;
2688
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2689
+ _session = null;
2690
+ _call = null;
2616
2691
  _tools = null;
2617
2692
  _recorder = null;
2618
- _conversation = [];
2619
- _audioBuffer = [];
2620
- _running = false;
2621
- _speaking = false;
2693
+ _closed = false;
2694
+ _sentAudioChunks = 0;
2695
+ _audioRemainder = Buffer.alloc(0);
2622
2696
  _builtinTools = null;
2697
+ _toolCallInProgress = false;
2623
2698
  _log = NOOP_LOGGER;
2624
- constructor(options) {
2625
- this._stt = options.stt;
2626
- this._llm = options.llm;
2627
- this._tts = options.tts;
2628
- this._systemPrompt = options.systemPrompt;
2629
- this._greeting = options.greeting ?? true;
2699
+ constructor(options = {}) {
2700
+ this._apiKey = options.apiKey ?? process.env["GOOGLE_API_KEY"] ?? "";
2701
+ this._systemPrompt = options.systemPrompt ?? "";
2702
+ this._model = options.model ?? "gemini-2.5-flash-native-audio-preview-12-2025";
2703
+ this._voice = options.voice ?? "Kore";
2630
2704
  this._language = options.language ?? "ko";
2631
- this._temperature = options.temperature;
2632
- this._maxTokens = options.maxTokens;
2633
- this._sampleRate = options.sampleRate ?? 8e3;
2634
- this._interruptOnSpeech = options.interruptOnSpeech ?? true;
2635
- if (options.toolRegistry) this._tools = options.toolRegistry;
2636
- if (options.recorder) this._recorder = options.recorder;
2705
+ this._greeting = options.greeting ?? true;
2637
2706
  }
2707
+ /** Inject per-call ToolRegistry. */
2638
2708
  setToolRegistry(registry) {
2639
2709
  this._tools = registry;
2640
2710
  }
2711
+ /** Inject per-call AudioRecorder. */
2641
2712
  setRecorder(recorder) {
2642
2713
  this._recorder = recorder;
2643
2714
  }
@@ -2646,239 +2717,266 @@ var PipelineSession = class {
2646
2717
  }
2647
2718
  setLogger(logger) {
2648
2719
  this._log = logger;
2649
- if ("setLogger" in this._stt && typeof this._stt.setLogger === "function") {
2650
- this._stt.setLogger(logger);
2651
- }
2652
- if ("setLogger" in this._tts && typeof this._tts.setLogger === "function") {
2653
- this._tts.setLogger(logger);
2654
- }
2720
+ }
2721
+ getTelemetry() {
2722
+ return {
2723
+ sessionType: "gemini_realtime",
2724
+ llm: { provider: "gemini", model: this._model },
2725
+ stt: null,
2726
+ tts: null,
2727
+ voice: this._voice,
2728
+ language: this._language,
2729
+ greetingEnabled: this._greeting,
2730
+ recordingEnabled: !!this._recorder,
2731
+ toolCount: this._tools?.size ?? 0,
2732
+ mcpServerCount: 0,
2733
+ builtinTools: []
2734
+ };
2655
2735
  }
2656
2736
  async start(callSession, tools) {
2657
- this._callSession = callSession;
2658
- this._tools = tools ?? null;
2659
- this._running = true;
2660
- this._log.info("PipelineSession started");
2661
- this._conversation = [];
2662
- if (this._systemPrompt) {
2663
- this._conversation.push({
2664
- role: "system",
2665
- content: this._systemPrompt
2666
- });
2737
+ this._call = callSession;
2738
+ if (tools) this._tools = tools;
2739
+ this._closed = false;
2740
+ this._sentAudioChunks = 0;
2741
+ this._audioRemainder = Buffer.alloc(0);
2742
+ if (!this._apiKey) {
2743
+ throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey option.");
2667
2744
  }
2668
- if (this._greeting) {
2669
- this._generateGreeting().catch((err) => {
2670
- this._log.error({ err }, "Greeting error");
2671
- });
2745
+ const { GoogleGenAI } = await import('@google/genai/node');
2746
+ const client = new GoogleGenAI({ apiKey: this._apiKey });
2747
+ const config = {
2748
+ responseModalities: ["AUDIO"],
2749
+ speechConfig: {
2750
+ voiceConfig: {
2751
+ prebuiltVoiceConfig: {
2752
+ voiceName: this._voice
2753
+ }
2754
+ }
2755
+ },
2756
+ inputAudioTranscription: {},
2757
+ outputAudioTranscription: {}
2758
+ };
2759
+ if (this._systemPrompt) {
2760
+ config["systemInstruction"] = this._systemPrompt;
2672
2761
  }
2673
- this._runSttLoop().catch((err) => {
2674
- this._log.error({ err }, "STT loop error");
2675
- });
2676
- }
2677
- feedAudio(audio) {
2678
- if (this._running) {
2679
- this._audioBuffer.push(audio);
2762
+ const toolSchemas = this._buildToolSchemas();
2763
+ if (toolSchemas.length > 0) {
2764
+ config["tools"] = [{ functionDeclarations: toolSchemas }];
2680
2765
  }
2681
- }
2682
- async feedDtmf(digits) {
2683
- this._conversation.push({
2684
- role: "user",
2685
- content: `[DTMF \uC785\uB825: ${digits}]`
2686
- });
2687
- await this._respond();
2688
- }
2689
- async stop() {
2690
- this._running = false;
2691
- this._log.info("PipelineSession stopped");
2692
- this._audioBuffer = [];
2693
- }
2694
- async _runSttLoop() {
2695
- const audioStream = this._createAudioStream();
2696
- for await (const event of this._stt.transcribe(audioStream, {
2697
- sampleRate: this._sampleRate
2698
- })) {
2699
- if (!this._running) break;
2700
- if (event.type === "interim" && this._speaking && this._interruptOnSpeech) {
2701
- this._speaking = false;
2702
- if (this._callSession) {
2703
- this._callSession.clearAudio();
2766
+ this._session = await client.live.connect({
2767
+ model: this._model,
2768
+ config,
2769
+ callbacks: {
2770
+ onmessage: (msg) => this._handleMessage(msg),
2771
+ onerror: (err) => {
2772
+ this._log.error({ err }, "Gemini SDK error");
2773
+ },
2774
+ onclose: (ev) => {
2775
+ this._log.info({ code: ev?.code ?? "unknown" }, "Gemini connection closed");
2776
+ this._closed = true;
2704
2777
  }
2705
- this._log.info('Barge-in: "%s"', event.transcript.substring(0, 30));
2706
- }
2707
- if (event.type === "final" && event.transcript.trim()) {
2708
- this._log.info("STT: %s", event.transcript);
2709
- await this._handleUserSpeech(event.transcript);
2710
2778
  }
2779
+ });
2780
+ if (this._greeting) {
2781
+ this._session.sendClientContent({
2782
+ turns: [
2783
+ {
2784
+ role: "user",
2785
+ parts: [{ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." }]
2786
+ }
2787
+ ],
2788
+ turnComplete: true
2789
+ });
2711
2790
  }
2712
2791
  }
2713
- async *_createAudioStream() {
2714
- while (this._running) {
2715
- if (this._audioBuffer.length > 0) {
2716
- const ulaw = this._audioBuffer.shift();
2717
- const pcm8k = ulawToPcm16(ulaw);
2718
- const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
2719
- yield pcm16k;
2720
- } else {
2721
- await new Promise((resolve) => setTimeout(resolve, 20));
2792
+ feedAudio(audio) {
2793
+ if (this._session && !this._closed && !this._toolCallInProgress) {
2794
+ const pcm8k = ulawToPcm16(audio);
2795
+ if (this._recorder) {
2796
+ this._recorder.writeInbound(pcm8k);
2722
2797
  }
2723
- }
2724
- }
2725
- async _generateGreeting() {
2726
- await new Promise((resolve) => setTimeout(resolve, 500));
2727
- await this._respond();
2728
- }
2729
- async _handleUserSpeech(transcript) {
2730
- this._conversation.push({ role: "user", content: transcript });
2731
- await this._respond();
2732
- }
2733
- _buildEffectiveTools() {
2734
- const includeCollectDtmf = !this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */);
2735
- const includeSendDtmf = !this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */);
2736
- if (!includeCollectDtmf && !includeSendDtmf) return this._tools ?? void 0;
2737
- const base = this._tools ? this._tools.fork() : new ToolRegistry();
2738
- if (includeCollectDtmf) {
2739
- base.register({
2740
- name: "collect_dtmf",
2741
- description: COLLECT_DTMF_TOOL3.function.description,
2742
- parameters: COLLECT_DTMF_TOOL3.function.parameters.properties,
2743
- required: COLLECT_DTMF_TOOL3.function.parameters.required,
2744
- handler: async () => ""
2798
+ const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
2799
+ this._session.sendRealtimeInput({
2800
+ audio: {
2801
+ data: Buffer.from(pcm16k).toString("base64"),
2802
+ mimeType: "audio/pcm;rate=16000"
2803
+ }
2745
2804
  });
2746
2805
  }
2747
- if (includeSendDtmf) {
2748
- base.register({
2749
- name: "send_dtmf",
2750
- description: SEND_DTMF_TOOL3.function.description,
2751
- parameters: SEND_DTMF_TOOL3.function.parameters.properties,
2752
- required: SEND_DTMF_TOOL3.function.parameters.required,
2753
- handler: async () => ""
2806
+ }
2807
+ async feedDtmf(digits) {
2808
+ if (this._session) {
2809
+ this._session.sendClientContent({
2810
+ turns: [{ role: "user", parts: [{ text: `[DTMF \uC785\uB825: ${digits}]` }] }],
2811
+ turnComplete: true
2754
2812
  });
2755
2813
  }
2756
- return base;
2757
2814
  }
2758
- async _respond() {
2759
- let fullResponse = "";
2760
- const textChunks = [];
2761
- const effectiveTools = this._buildEffectiveTools();
2762
- const llmStream = this._llm.generate(this._conversation, {
2763
- tools: effectiveTools,
2764
- temperature: this._temperature,
2765
- maxTokens: this._maxTokens
2766
- });
2767
- for await (const chunk of llmStream) {
2768
- if (!this._running) break;
2769
- if (chunk.type === "text" && chunk.text) {
2770
- textChunks.push(chunk.text);
2771
- fullResponse += chunk.text;
2772
- } else if (chunk.type === "tool_call" && chunk.toolCall) {
2773
- await this._handleToolCall(chunk);
2815
+ async stop() {
2816
+ this._closed = true;
2817
+ if (this._session) {
2818
+ try {
2819
+ this._session.close();
2820
+ } catch {
2774
2821
  }
2775
- }
2776
- if (fullResponse.trim()) {
2777
- this._log.info("Assistant: %s", fullResponse.substring(0, 100));
2778
- this._conversation.push({ role: "assistant", content: fullResponse });
2779
- await this._synthesizeAndSend(fullResponse);
2822
+ this._session = null;
2780
2823
  }
2781
2824
  }
2782
- async _handleToolCall(chunk) {
2783
- if (!chunk.toolCall) return;
2784
- const { id, name, arguments: argsStr } = chunk.toolCall;
2785
- try {
2786
- const args = JSON.parse(argsStr);
2787
- if (name === "collect_dtmf" && this._callSession) {
2788
- let result2;
2789
- try {
2790
- result2 = await this._callSession.collectDtmf({
2791
- maxDigits: args["max_digits"] ?? 4,
2792
- finishOnKey: args["finish_on_key"] ?? "#",
2793
- timeout: args["timeout"] ?? 5
2794
- });
2795
- } catch (err) {
2796
- result2 = `Error: ${err}`;
2825
+ _buildToolSchemas() {
2826
+ const toolDefs = this._tools ? this._tools.toOpenAITools().map((t) => ({
2827
+ name: t.function.name,
2828
+ description: t.function.description,
2829
+ parameters: sanitizeSchemaForGemini(
2830
+ t.function.parameters ?? { type: "object", properties: {} }
2831
+ )
2832
+ })) : [];
2833
+ toolDefs.push(...getBuiltinToolSchemas(this._builtinTools, "gemini"));
2834
+ return toolDefs;
2835
+ }
2836
+ _handleMessage(msg) {
2837
+ if (!this._call) return;
2838
+ const serverContent = msg.serverContent;
2839
+ if (serverContent) {
2840
+ const modelTurn = serverContent.modelTurn;
2841
+ if (modelTurn) {
2842
+ for (const part of modelTurn.parts ?? []) {
2843
+ const inlineData = part.inlineData;
2844
+ if (inlineData?.data) {
2845
+ const mimeType = inlineData.mimeType ?? "";
2846
+ if (mimeType.includes("audio")) {
2847
+ this._handleAudioData(inlineData.data);
2848
+ }
2849
+ }
2797
2850
  }
2798
- this._conversation.push({ role: "tool", content: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)", tool_call_id: id, name });
2799
- await this._respond();
2800
- return;
2801
2851
  }
2802
- if (name === "send_dtmf" && this._callSession) {
2803
- let result2;
2804
- try {
2805
- await this._callSession.sendDtmfSequence(args["digits"] ?? "");
2806
- result2 = "sent";
2807
- } catch (err) {
2808
- result2 = `Error: ${err}`;
2809
- }
2810
- this._conversation.push({ role: "tool", content: result2, tool_call_id: id, name });
2811
- await this._respond();
2812
- return;
2852
+ if (serverContent.turnComplete) {
2853
+ this._log.debug("Turn complete");
2854
+ this._flushAudioRemainder();
2813
2855
  }
2814
- if (!this._tools) return;
2815
- const result = await this._tools.call(name, args);
2816
- this._conversation.push({
2817
- role: "assistant",
2818
- content: ""
2819
- // Tool call info stored in the message flow
2820
- });
2821
- this._conversation.push({
2822
- role: "tool",
2823
- content: typeof result === "string" ? result : JSON.stringify(result),
2824
- tool_call_id: id,
2825
- name
2826
- });
2827
- const effectiveTools = this._buildEffectiveTools();
2828
- let followUpText = "";
2829
- const followUpStream = this._llm.generate(this._conversation, {
2830
- tools: effectiveTools,
2831
- temperature: this._temperature,
2832
- maxTokens: this._maxTokens
2833
- });
2834
- for await (const followChunk of followUpStream) {
2835
- if (!this._running) break;
2836
- if (followChunk.type === "text" && followChunk.text) {
2837
- followUpText += followChunk.text;
2856
+ if (serverContent.interrupted) {
2857
+ this._log.info("Barge-in detected");
2858
+ if (this._call) {
2859
+ this._call.clearAudio();
2838
2860
  }
2861
+ this._sentAudioChunks = 0;
2862
+ this._audioRemainder = Buffer.alloc(0);
2839
2863
  }
2840
- if (followUpText.trim()) {
2841
- this._conversation.push({ role: "assistant", content: followUpText });
2842
- await this._synthesizeAndSend(followUpText);
2864
+ const inputText = serverContent.inputTranscription?.text;
2865
+ if (inputText && this._call) {
2866
+ this._log.info("User: %s", inputText);
2867
+ this._call._emit("transcript", "user", inputText);
2843
2868
  }
2844
- } catch (err) {
2845
- this._log.error({ err }, "Tool call failed: %s", name);
2869
+ const outputText = serverContent.outputTranscription?.text;
2870
+ if (outputText && this._call) {
2871
+ this._log.info("Assistant: %s", outputText);
2872
+ this._call._emit("transcript", "assistant", outputText);
2873
+ }
2874
+ }
2875
+ if (msg.toolCall) {
2876
+ this._handleToolCall(msg.toolCall);
2877
+ }
2878
+ const toolCancellation = msg["toolCallCancellation"];
2879
+ if (toolCancellation) {
2880
+ this._log.info({ ids: toolCancellation.ids }, "Tool call cancelled");
2846
2881
  }
2847
2882
  }
2848
- async _synthesizeAndSend(text) {
2849
- if (!this._callSession || !this._running) return;
2850
- this._speaking = true;
2851
- try {
2852
- for await (const audioChunk of this._tts.synthesize(text, {
2853
- sampleRate: this._sampleRate
2854
- })) {
2855
- if (!this._running || !this._speaking) break;
2856
- if (this._recorder) {
2857
- const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2858
- this._recorder.writeOutbound(pcm8k2);
2859
- }
2860
- const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2861
- const ulaw = pcm16ToUlaw(pcm8k);
2862
- for (let off = 0; off < ulaw.length; off += 160) {
2863
- let chunk = ulaw.subarray(off, off + 160);
2864
- if (chunk.length < 160) {
2865
- chunk = Buffer.concat([chunk, Buffer.alloc(160 - chunk.length, 255)]);
2883
+ _handleAudioData(b64Data) {
2884
+ if (!this._call) return;
2885
+ const pcm24k = Buffer.from(b64Data, "base64");
2886
+ if (this._recorder) {
2887
+ this._recorder.writeOutbound(resamplePcm16(pcm24k, 24e3, 8e3));
2888
+ }
2889
+ const pcm8k = resamplePcm16(pcm24k, 24e3, 8e3);
2890
+ const ulaw = pcm16ToUlaw(pcm8k);
2891
+ const combined = Buffer.concat([this._audioRemainder, ulaw]);
2892
+ const chunkSize = 160;
2893
+ const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
2894
+ if (fullEnd > 0) {
2895
+ this._call.sendAudio(combined.subarray(0, fullEnd));
2896
+ this._sentAudioChunks += fullEnd / chunkSize;
2897
+ }
2898
+ this._audioRemainder = combined.subarray(fullEnd);
2899
+ }
2900
+ _flushAudioRemainder() {
2901
+ if (this._audioRemainder.length > 0 && this._call) {
2902
+ const padded = Buffer.concat([
2903
+ this._audioRemainder,
2904
+ Buffer.alloc(160 - this._audioRemainder.length, 255)
2905
+ ]);
2906
+ this._call.sendAudio(padded);
2907
+ this._sentAudioChunks++;
2908
+ this._audioRemainder = Buffer.alloc(0);
2909
+ }
2910
+ }
2911
+ async _handleToolCall(toolCall) {
2912
+ const functionCalls = toolCall.functionCalls;
2913
+ if (!functionCalls) return;
2914
+ this._toolCallInProgress = true;
2915
+ const responses = [];
2916
+ for (const fc of functionCalls) {
2917
+ const name = fc.name ?? "";
2918
+ const fcId = fc.id ?? "";
2919
+ const args = fc.args ?? {};
2920
+ this._log.info({ tool: name, args }, "Tool call: %s", name);
2921
+ if (BUILTIN_TOOL_NAMES.has(name) && this._call) {
2922
+ const result = await executeBuiltinTool(name, args, this._call);
2923
+ if (result !== null) {
2924
+ if (name === "hang_up") {
2925
+ this._log.info("hang_up: ending call");
2926
+ return;
2866
2927
  }
2867
- this._callSession.sendAudio(chunk);
2928
+ this._log.info("Builtin tool result: %s -> %s", name, result);
2929
+ responses.push({ id: fcId, name, response: { result } });
2930
+ continue;
2868
2931
  }
2869
2932
  }
2870
- } catch (err) {
2871
- this._log.error({ err }, "TTS error");
2872
- } finally {
2873
- this._speaking = false;
2933
+ if (!this._tools || !this._tools.has(name)) {
2934
+ this._log.error("Unknown tool: %s", name);
2935
+ responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
2936
+ continue;
2937
+ }
2938
+ try {
2939
+ this._call?.recordToolCall();
2940
+ const result = await this._tools.call(name, args);
2941
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
2942
+ this._log.info("Tool result: %s -> %s", name, resultStr.substring(0, 200));
2943
+ responses.push({
2944
+ id: fcId,
2945
+ name,
2946
+ response: { result: resultStr }
2947
+ });
2948
+ } catch (err) {
2949
+ this._log.error({ err }, "Tool call failed: %s", name);
2950
+ if (err instanceof Error) {
2951
+ this._call?.recordToolError(err);
2952
+ }
2953
+ responses.push({
2954
+ id: fcId,
2955
+ name,
2956
+ response: { error: String(err) }
2957
+ });
2958
+ }
2959
+ }
2960
+ if (responses.length > 0 && this._session) {
2961
+ this._log.debug("Sending %d tool response(s)", responses.length);
2962
+ this._session.sendToolResponse({
2963
+ functionResponses: responses
2964
+ });
2874
2965
  }
2966
+ this._toolCallInProgress = false;
2875
2967
  }
2876
2968
  };
2877
2969
 
2878
- // src/agent/pipeline/deepgram-stt.ts
2970
+ // src/agent/pipeline/stt/deepgram-stt.ts
2879
2971
  var DeepgramSTT = class {
2880
2972
  _options;
2881
2973
  _log = NOOP_LOGGER;
2974
+ get provider() {
2975
+ return "deepgram";
2976
+ }
2977
+ get model() {
2978
+ return this._options.model ?? "nova-3";
2979
+ }
2882
2980
  setLogger(logger) {
2883
2981
  this._log = logger;
2884
2982
  }
@@ -2996,10 +3094,19 @@ var DeepgramSTT = class {
2996
3094
  }
2997
3095
  };
2998
3096
 
2999
- // src/agent/pipeline/elevenlabs-tts.ts
3097
+ // src/agent/pipeline/tts/elevenlabs-tts.ts
3000
3098
  var ElevenLabsTTS = class {
3001
3099
  _options;
3002
3100
  _log = NOOP_LOGGER;
3101
+ get provider() {
3102
+ return "elevenlabs";
3103
+ }
3104
+ get model() {
3105
+ return this._options.model ?? "eleven_flash_v2_5";
3106
+ }
3107
+ get voiceId() {
3108
+ return this._options.voiceId ?? "EXAVITQu4vr4xnSDxMaL";
3109
+ }
3003
3110
  setLogger(logger) {
3004
3111
  this._log = logger;
3005
3112
  }
@@ -3149,9 +3256,15 @@ var ElevenLabsTTS = class {
3149
3256
  }
3150
3257
  };
3151
3258
 
3152
- // src/agent/pipeline/openai-llm.ts
3259
+ // src/agent/pipeline/llm/openai-llm.ts
3153
3260
  var OpenAILLM = class {
3154
3261
  _options;
3262
+ get provider() {
3263
+ return "openai";
3264
+ }
3265
+ get model() {
3266
+ return this._options.model;
3267
+ }
3155
3268
  constructor(options = {}) {
3156
3269
  this._options = {
3157
3270
  model: "gpt-4o-mini",
@@ -3235,9 +3348,15 @@ var OpenAILLM = class {
3235
3348
  }
3236
3349
  };
3237
3350
 
3238
- // src/agent/pipeline/anthropic-llm.ts
3351
+ // src/agent/pipeline/llm/anthropic-llm.ts
3239
3352
  var AnthropicLLM = class {
3240
3353
  _options;
3354
+ get provider() {
3355
+ return "anthropic";
3356
+ }
3357
+ get model() {
3358
+ return this._options.model;
3359
+ }
3241
3360
  constructor(options = {}) {
3242
3361
  this._options = {
3243
3362
  model: "claude-sonnet-4-6",
@@ -3340,9 +3459,15 @@ var AnthropicLLM = class {
3340
3459
  }
3341
3460
  };
3342
3461
 
3343
- // src/agent/pipeline/gemini-llm.ts
3462
+ // src/agent/pipeline/llm/gemini-llm.ts
3344
3463
  var GeminiLLM = class {
3345
3464
  _options;
3465
+ get provider() {
3466
+ return "gemini";
3467
+ }
3468
+ get model() {
3469
+ return this._options.model;
3470
+ }
3346
3471
  constructor(options = {}) {
3347
3472
  this._options = {
3348
3473
  model: "gemini-2.5-flash",
@@ -3424,9 +3549,15 @@ var GeminiLLM = class {
3424
3549
  }
3425
3550
  };
3426
3551
 
3427
- // src/agent/pipeline/openai-compat-llm.ts
3552
+ // src/agent/pipeline/llm/openai-compat-llm.ts
3428
3553
  var OpenAICompatLLM = class {
3429
3554
  _options;
3555
+ get provider() {
3556
+ return "openai_compatible";
3557
+ }
3558
+ get model() {
3559
+ return this._options.model;
3560
+ }
3430
3561
  constructor(options) {
3431
3562
  this._options = options;
3432
3563
  }
@@ -3507,9 +3638,15 @@ var OpenAICompatLLM = class {
3507
3638
  }
3508
3639
  };
3509
3640
 
3510
- // src/agent/pipeline/ollama-llm.ts
3641
+ // src/agent/pipeline/llm/ollama-llm.ts
3511
3642
  var OllamaLLM = class {
3512
3643
  _inner;
3644
+ get provider() {
3645
+ return "ollama";
3646
+ }
3647
+ get model() {
3648
+ return this._inner.model;
3649
+ }
3513
3650
  constructor(options = {}) {
3514
3651
  const baseUrl = options.baseUrl ?? process.env["OLLAMA_BASE_URL"] ?? "http://localhost:11434/v1";
3515
3652
  this._inner = new OpenAICompatLLM({
@@ -3525,9 +3662,15 @@ var OllamaLLM = class {
3525
3662
  }
3526
3663
  };
3527
3664
 
3528
- // src/agent/pipeline/mistral-llm.ts
3665
+ // src/agent/pipeline/llm/mistral-llm.ts
3529
3666
  var MistralLLM = class {
3530
3667
  _inner;
3668
+ get provider() {
3669
+ return "mistral";
3670
+ }
3671
+ get model() {
3672
+ return this._inner.model;
3673
+ }
3531
3674
  constructor(options = {}) {
3532
3675
  this._inner = new OpenAICompatLLM({
3533
3676
  apiKey: options.apiKey ?? process.env["MISTRAL_API_KEY"],
@@ -3542,9 +3685,15 @@ var MistralLLM = class {
3542
3685
  }
3543
3686
  };
3544
3687
 
3545
- // src/agent/pipeline/groq-llm.ts
3688
+ // src/agent/pipeline/llm/groq-llm.ts
3546
3689
  var GroqLLM = class {
3547
3690
  _inner;
3691
+ get provider() {
3692
+ return "groq";
3693
+ }
3694
+ get model() {
3695
+ return this._inner.model;
3696
+ }
3548
3697
  constructor(options = {}) {
3549
3698
  this._inner = new OpenAICompatLLM({
3550
3699
  apiKey: options.apiKey ?? process.env["GROQ_API_KEY"],
@@ -3559,9 +3708,15 @@ var GroqLLM = class {
3559
3708
  }
3560
3709
  };
3561
3710
 
3562
- // src/agent/pipeline/perplexity-llm.ts
3711
+ // src/agent/pipeline/llm/perplexity-llm.ts
3563
3712
  var PerplexityLLM = class {
3564
3713
  _inner;
3714
+ get provider() {
3715
+ return "perplexity";
3716
+ }
3717
+ get model() {
3718
+ return this._inner.model;
3719
+ }
3565
3720
  constructor(options = {}) {
3566
3721
  this._inner = new OpenAICompatLLM({
3567
3722
  apiKey: options.apiKey ?? process.env["PERPLEXITY_API_KEY"],
@@ -3576,9 +3731,15 @@ var PerplexityLLM = class {
3576
3731
  }
3577
3732
  };
3578
3733
 
3579
- // src/agent/pipeline/together-llm.ts
3734
+ // src/agent/pipeline/llm/together-llm.ts
3580
3735
  var TogetherLLM = class {
3581
3736
  _inner;
3737
+ get provider() {
3738
+ return "together";
3739
+ }
3740
+ get model() {
3741
+ return this._inner.model;
3742
+ }
3582
3743
  constructor(options = {}) {
3583
3744
  this._inner = new OpenAICompatLLM({
3584
3745
  apiKey: options.apiKey ?? process.env["TOGETHER_API_KEY"],
@@ -3593,9 +3754,15 @@ var TogetherLLM = class {
3593
3754
  }
3594
3755
  };
3595
3756
 
3596
- // src/agent/pipeline/fireworks-llm.ts
3757
+ // src/agent/pipeline/llm/fireworks-llm.ts
3597
3758
  var FireworksLLM = class {
3598
3759
  _inner;
3760
+ get provider() {
3761
+ return "fireworks";
3762
+ }
3763
+ get model() {
3764
+ return this._inner.model;
3765
+ }
3599
3766
  constructor(options = {}) {
3600
3767
  this._inner = new OpenAICompatLLM({
3601
3768
  apiKey: options.apiKey ?? process.env["FIREWORKS_API_KEY"],
@@ -3610,9 +3777,15 @@ var FireworksLLM = class {
3610
3777
  }
3611
3778
  };
3612
3779
 
3613
- // src/agent/pipeline/deepseek-llm.ts
3780
+ // src/agent/pipeline/llm/deepseek-llm.ts
3614
3781
  var DeepSeekLLM = class {
3615
3782
  _inner;
3783
+ get provider() {
3784
+ return "deepseek";
3785
+ }
3786
+ get model() {
3787
+ return this._inner.model;
3788
+ }
3616
3789
  constructor(options = {}) {
3617
3790
  this._inner = new OpenAICompatLLM({
3618
3791
  apiKey: options.apiKey ?? process.env["DEEPSEEK_API_KEY"],
@@ -3627,9 +3800,15 @@ var DeepSeekLLM = class {
3627
3800
  }
3628
3801
  };
3629
3802
 
3630
- // src/agent/pipeline/xai-llm.ts
3803
+ // src/agent/pipeline/llm/xai-llm.ts
3631
3804
  var XaiLLM = class {
3632
3805
  _inner;
3806
+ get provider() {
3807
+ return "xai";
3808
+ }
3809
+ get model() {
3810
+ return this._inner.model;
3811
+ }
3633
3812
  constructor(options = {}) {
3634
3813
  this._inner = new OpenAICompatLLM({
3635
3814
  apiKey: options.apiKey ?? process.env["XAI_API_KEY"],
@@ -3663,6 +3842,6 @@ function mcpServerHTTP(options) {
3663
3842
  };
3664
3843
  }
3665
3844
 
3666
- export { AnthropicLLM, AudioRecorder, BuiltinTool, CallSession, ClawOpsAgent, DECODE_TABLE, DeepSeekLLM, DeepgramSTT, ElevenLabsTTS, FireworksLLM, GeminiLLM, GeminiRealtime, GroqLLM, MCPClient, MistralLLM, OllamaLLM, OpenAICompatLLM, OpenAILLM, OpenAIRealtime, PerplexityLLM, PipelineSession, TogetherLLM, ToolRegistry, XaiLLM, createAgentLogger, createPipelineLogger, functionTool, getTracingConfig, mcpServerHTTP, mcpServerStdio, pcm16ToUlaw, resamplePcm16, resetTracingConfig, setTracingConfig, ulawToPcm16, zodToToolParams };
3845
+ export { AnthropicLLM, AudioRecorder, BUILTIN_TOOL_NAMES, BuiltinTool, CallSession, ClawOpsAgent, DECODE_TABLE, DeepSeekLLM, DeepgramSTT, ElevenLabsTTS, FireworksLLM, GeminiLLM, GeminiRealtime, GroqLLM, MCPClient, MistralLLM, OllamaLLM, OpenAICompatLLM, OpenAILLM, OpenAIRealtime, PerplexityLLM, PipelineSession, TogetherLLM, ToolRegistry, XaiLLM, createAgentLogger, createPipelineLogger, executeBuiltinTool, functionTool, getBuiltinToolSchemas, getTracingConfig, isBuiltinTool, mcpServerHTTP, mcpServerStdio, pcm16ToUlaw, resamplePcm16, resetTracingConfig, setTracingConfig, ulawToPcm16, zodToToolParams };
3667
3846
  //# sourceMappingURL=index.js.map
3668
3847
  //# sourceMappingURL=index.js.map