@teamlearners/clawops 0.5.5 → 0.5.8

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.
@@ -1459,6 +1459,7 @@ var ClawOpsAgent = class {
1459
1459
  }
1460
1460
  /** Connect to the ClawOps platform and start listening for calls. */
1461
1461
  async connect() {
1462
+ if (this._controlWs) return;
1462
1463
  if (!this._apiKey) {
1463
1464
  throw new AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1464
1465
  }
@@ -1773,20 +1774,13 @@ var ClawOpsAgent = class {
1773
1774
  }
1774
1775
  };
1775
1776
 
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",
1777
+ // src/agent/pipeline/builtin-tool-schemas.ts
1778
+ var HANG_UP = {
1780
1779
  name: "hang_up",
1781
1780
  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
- }
1781
+ parameters: { type: "object", properties: {} }
1787
1782
  };
1788
- var COLLECT_DTMF_TOOL = {
1789
- type: "function",
1783
+ var COLLECT_DTMF = {
1790
1784
  name: "collect_dtmf",
1791
1785
  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
1786
  parameters: {
@@ -1799,8 +1793,7 @@ var COLLECT_DTMF_TOOL = {
1799
1793
  required: ["max_digits"]
1800
1794
  }
1801
1795
  };
1802
- var SEND_DTMF_TOOL = {
1803
- type: "function",
1796
+ var SEND_DTMF = {
1804
1797
  name: "send_dtmf",
1805
1798
  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
1799
  parameters: {
@@ -1808,181 +1801,512 @@ var SEND_DTMF_TOOL = {
1808
1801
  properties: {
1809
1802
  digits: {
1810
1803
  type: "string",
1811
- description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
1804
+ description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30. \uC608: '1', '1234#', '1w2'"
1812
1805
  }
1813
1806
  },
1814
1807
  required: ["digits"]
1815
1808
  }
1816
1809
  };
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;
1810
+ var TOOL_MAP = /* @__PURE__ */ new Map([
1811
+ ["hang_up" /* HANG_UP */, HANG_UP],
1812
+ ["collect_dtmf" /* COLLECT_DTMF */, COLLECT_DTMF],
1813
+ ["send_dtmf" /* SEND_DTMF */, SEND_DTMF]
1814
+ ]);
1815
+ var BUILTIN_TOOL_NAMES = new Set(
1816
+ Array.from(TOOL_MAP.values()).map((s) => s.name)
1817
+ );
1818
+ function toChatCompletions(schema) {
1819
+ return {
1820
+ type: "function",
1821
+ function: {
1822
+ name: schema.name,
1823
+ description: schema.description,
1824
+ parameters: schema.parameters
1825
+ }
1826
+ };
1827
+ }
1828
+ function toRealtime(schema) {
1829
+ return {
1830
+ type: "function",
1831
+ name: schema.name,
1832
+ description: schema.description,
1833
+ parameters: schema.parameters
1834
+ };
1835
+ }
1836
+ function toGemini(schema) {
1837
+ return {
1838
+ name: schema.name,
1839
+ description: schema.description,
1840
+ parameters: schema.parameters
1841
+ };
1842
+ }
1843
+ var CONVERTERS = {
1844
+ chat: toChatCompletions,
1845
+ realtime: toRealtime,
1846
+ gemini: toGemini
1847
+ };
1848
+ function getBuiltinToolSchemas(builtinTools, fmt) {
1849
+ const converter = CONVERTERS[fmt];
1850
+ const result = [];
1851
+ for (const [toolEnum, schema] of TOOL_MAP) {
1852
+ if (builtinTools === null || builtinTools.has(toolEnum)) {
1853
+ result.push(converter(schema));
1854
+ }
1829
1855
  }
1830
- setBuiltinTools(tools) {
1831
- this._builtinTools = tools;
1856
+ return result;
1857
+ }
1858
+ function isBuiltinTool(name) {
1859
+ return BUILTIN_TOOL_NAMES.has(name);
1860
+ }
1861
+ async function executeBuiltinTool(funcName, args, call) {
1862
+ if (funcName === "hang_up") {
1863
+ await call.hangup();
1864
+ return "";
1832
1865
  }
1833
- _ws = null;
1834
- _call = null;
1866
+ if (funcName === "collect_dtmf") {
1867
+ try {
1868
+ const result = await call.collectDtmf({
1869
+ maxDigits: args["max_digits"] ?? 4,
1870
+ finishOnKey: args["finish_on_key"] ?? "#",
1871
+ timeout: args["timeout"] ?? 5
1872
+ });
1873
+ return result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)";
1874
+ } catch (e) {
1875
+ return `Error: ${e}`;
1876
+ }
1877
+ }
1878
+ if (funcName === "send_dtmf") {
1879
+ try {
1880
+ await call.sendDtmfSequence(args["digits"] ?? "");
1881
+ return "sent";
1882
+ } catch (e) {
1883
+ return `Error: ${e}`;
1884
+ }
1885
+ }
1886
+ return null;
1887
+ }
1888
+
1889
+ // src/agent/pipeline/pipeline-session.ts
1890
+ var PipelineSession = class {
1891
+ _stt;
1892
+ _llm;
1893
+ _tts;
1894
+ _systemPrompt;
1895
+ _greeting;
1896
+ _language;
1897
+ _temperature;
1898
+ _maxTokens;
1899
+ _sampleRate;
1900
+ _interruptOnSpeech;
1901
+ _callSession = null;
1835
1902
  _tools = null;
1836
1903
  _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";
1904
+ _conversation = [];
1905
+ _audioBuffer = [];
1906
+ _running = false;
1907
+ _speaking = false;
1908
+ _builtinTools = null;
1909
+ _log = NOOP_LOGGER;
1910
+ constructor(options) {
1911
+ this._stt = options.stt;
1912
+ this._llm = options.llm;
1913
+ this._tts = options.tts;
1914
+ this._systemPrompt = options.systemPrompt;
1853
1915
  this._greeting = options.greeting ?? true;
1916
+ this._language = options.language ?? "ko";
1917
+ this._temperature = options.temperature;
1918
+ this._maxTokens = options.maxTokens;
1919
+ this._sampleRate = options.sampleRate ?? 8e3;
1920
+ this._interruptOnSpeech = options.interruptOnSpeech ?? true;
1921
+ if (options.toolRegistry) this._tools = options.toolRegistry;
1922
+ if (options.recorder) this._recorder = options.recorder;
1854
1923
  }
1855
- /** Inject per-call ToolRegistry. */
1856
1924
  setToolRegistry(registry) {
1857
1925
  this._tools = registry;
1858
1926
  }
1859
- /** Inject per-call AudioRecorder. */
1860
1927
  setRecorder(recorder) {
1861
1928
  this._recorder = recorder;
1862
1929
  }
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.");
1930
+ setBuiltinTools(tools) {
1931
+ this._builtinTools = tools;
1932
+ }
1933
+ setLogger(logger) {
1934
+ this._log = logger;
1935
+ if ("setLogger" in this._stt && typeof this._stt.setLogger === "function") {
1936
+ this._stt.setLogger(logger);
1873
1937
  }
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;
1938
+ if ("setLogger" in this._tts && typeof this._tts.setLogger === "function") {
1939
+ this._tts.setLogger(logger);
1940
+ }
1941
+ }
1942
+ async start(callSession, tools) {
1943
+ this._callSession = callSession;
1944
+ this._tools = tools ?? null;
1945
+ this._running = true;
1946
+ this._log.info("PipelineSession started");
1947
+ this._conversation = [];
1948
+ if (this._systemPrompt) {
1949
+ this._conversation.push({
1950
+ role: "system",
1951
+ content: this._systemPrompt
1901
1952
  });
1902
- ws.on("error", (err) => {
1903
- if (!this._ws) {
1904
- reject(err);
1905
- }
1906
- this._log.error({ err }, "OpenAI Realtime WS error");
1953
+ }
1954
+ if (this._greeting) {
1955
+ this._generateGreeting().catch((err) => {
1956
+ this._log.error({ err }, "Greeting error");
1907
1957
  });
1958
+ }
1959
+ this._runSttLoop().catch((err) => {
1960
+ this._log.error({ err }, "STT loop error");
1908
1961
  });
1909
1962
  }
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
1963
  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
- });
1964
+ if (this._running) {
1965
+ this._audioBuffer.push(audio);
1928
1966
  }
1929
1967
  }
1968
+ async feedDtmf(digits) {
1969
+ this._conversation.push({
1970
+ role: "user",
1971
+ content: `[DTMF \uC785\uB825: ${digits}]`
1972
+ });
1973
+ await this._respond();
1974
+ }
1930
1975
  async stop() {
1931
- this._closed = true;
1932
- if (this._ws) {
1933
- this._ws.close();
1934
- this._ws = null;
1976
+ this._running = false;
1977
+ this._log.info("PipelineSession stopped");
1978
+ this._audioBuffer = [];
1979
+ }
1980
+ async _runSttLoop() {
1981
+ const audioStream = this._createAudioStream();
1982
+ for await (const event of this._stt.transcribe(audioStream, {
1983
+ sampleRate: this._sampleRate
1984
+ })) {
1985
+ if (!this._running) break;
1986
+ if (event.type === "interim" && this._speaking && this._interruptOnSpeech) {
1987
+ this._speaking = false;
1988
+ if (this._callSession) {
1989
+ this._callSession.clearAudio();
1990
+ }
1991
+ this._log.info('Barge-in: "%s"', event.transcript.substring(0, 30));
1992
+ }
1993
+ if (event.type === "final" && event.transcript.trim()) {
1994
+ this._log.info("STT: %s", event.transcript);
1995
+ await this._handleUserSpeech(event.transcript);
1996
+ }
1935
1997
  }
1936
1998
  }
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
1999
+ async *_createAudioStream() {
2000
+ while (this._running) {
2001
+ if (this._audioBuffer.length > 0) {
2002
+ const ulaw = this._audioBuffer.shift();
2003
+ const pcm8k = ulawToPcm16(ulaw);
2004
+ const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
2005
+ yield pcm16k;
2006
+ } else {
2007
+ await new Promise((resolve) => setTimeout(resolve, 20));
1965
2008
  }
2009
+ }
2010
+ }
2011
+ async _generateGreeting() {
2012
+ await new Promise((resolve) => setTimeout(resolve, 500));
2013
+ await this._respond();
2014
+ }
2015
+ async _handleUserSpeech(transcript) {
2016
+ this._conversation.push({ role: "user", content: transcript });
2017
+ await this._respond();
2018
+ }
2019
+ _buildEffectiveTools() {
2020
+ const builtinSchemas = getBuiltinToolSchemas(this._builtinTools, "chat");
2021
+ const dtmfSchemas = builtinSchemas.filter((s) => {
2022
+ const name = s["function"]?.["name"];
2023
+ return name === "collect_dtmf" || name === "send_dtmf";
1966
2024
  });
2025
+ if (dtmfSchemas.length === 0) return this._tools ?? void 0;
2026
+ const base = this._tools ? this._tools.fork() : new ToolRegistry();
2027
+ for (const schema of dtmfSchemas) {
2028
+ const fn = schema["function"];
2029
+ const params = fn["parameters"];
2030
+ base.register({
2031
+ name: fn["name"],
2032
+ description: fn["description"],
2033
+ parameters: params["properties"] ?? {},
2034
+ required: params["required"] ?? [],
2035
+ handler: async () => ""
2036
+ });
2037
+ }
2038
+ return base;
1967
2039
  }
1968
- _handleMessage(msg) {
1969
- const type = msg["type"];
1970
- switch (type) {
2040
+ async _respond() {
2041
+ let fullResponse = "";
2042
+ const textChunks = [];
2043
+ const effectiveTools = this._buildEffectiveTools();
2044
+ const llmStream = this._llm.generate(this._conversation, {
2045
+ tools: effectiveTools,
2046
+ temperature: this._temperature,
2047
+ maxTokens: this._maxTokens
2048
+ });
2049
+ for await (const chunk of llmStream) {
2050
+ if (!this._running) break;
2051
+ if (chunk.type === "text" && chunk.text) {
2052
+ textChunks.push(chunk.text);
2053
+ fullResponse += chunk.text;
2054
+ } else if (chunk.type === "tool_call" && chunk.toolCall) {
2055
+ await this._handleToolCall(chunk);
2056
+ }
2057
+ }
2058
+ if (fullResponse.trim()) {
2059
+ this._log.info("Assistant: %s", fullResponse.substring(0, 100));
2060
+ this._conversation.push({ role: "assistant", content: fullResponse });
2061
+ await this._synthesizeAndSend(fullResponse);
2062
+ }
2063
+ }
2064
+ async _handleToolCall(chunk) {
2065
+ if (!chunk.toolCall) return;
2066
+ const { id, name, arguments: argsStr } = chunk.toolCall;
2067
+ try {
2068
+ const args = JSON.parse(argsStr);
2069
+ if (BUILTIN_TOOL_NAMES.has(name) && this._callSession) {
2070
+ const result2 = await executeBuiltinTool(name, args, this._callSession);
2071
+ if (result2 !== null) {
2072
+ if (name === "hang_up") return;
2073
+ this._conversation.push({ role: "tool", content: result2, tool_call_id: id, name });
2074
+ await this._respond();
2075
+ return;
2076
+ }
2077
+ }
2078
+ if (!this._tools) return;
2079
+ const result = await this._tools.call(name, args);
2080
+ this._conversation.push({
2081
+ role: "assistant",
2082
+ content: ""
2083
+ // Tool call info stored in the message flow
2084
+ });
2085
+ this._conversation.push({
2086
+ role: "tool",
2087
+ content: typeof result === "string" ? result : JSON.stringify(result),
2088
+ tool_call_id: id,
2089
+ name
2090
+ });
2091
+ const effectiveTools = this._buildEffectiveTools();
2092
+ let followUpText = "";
2093
+ const followUpStream = this._llm.generate(this._conversation, {
2094
+ tools: effectiveTools,
2095
+ temperature: this._temperature,
2096
+ maxTokens: this._maxTokens
2097
+ });
2098
+ for await (const followChunk of followUpStream) {
2099
+ if (!this._running) break;
2100
+ if (followChunk.type === "text" && followChunk.text) {
2101
+ followUpText += followChunk.text;
2102
+ }
2103
+ }
2104
+ if (followUpText.trim()) {
2105
+ this._conversation.push({ role: "assistant", content: followUpText });
2106
+ await this._synthesizeAndSend(followUpText);
2107
+ }
2108
+ } catch (err) {
2109
+ this._log.error({ err }, "Tool call failed: %s", name);
2110
+ }
2111
+ }
2112
+ async _synthesizeAndSend(text) {
2113
+ if (!this._callSession || !this._running) return;
2114
+ this._speaking = true;
2115
+ try {
2116
+ for await (const audioChunk of this._tts.synthesize(text, {
2117
+ sampleRate: this._sampleRate
2118
+ })) {
2119
+ if (!this._running || !this._speaking) break;
2120
+ if (this._recorder) {
2121
+ const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2122
+ this._recorder.writeOutbound(pcm8k2);
2123
+ }
2124
+ const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2125
+ const ulaw = pcm16ToUlaw(pcm8k);
2126
+ for (let off = 0; off < ulaw.length; off += 160) {
2127
+ let chunk = ulaw.subarray(off, off + 160);
2128
+ if (chunk.length < 160) {
2129
+ chunk = Buffer.concat([chunk, Buffer.alloc(160 - chunk.length, 255)]);
2130
+ }
2131
+ this._callSession.sendAudio(chunk);
2132
+ }
2133
+ }
2134
+ } catch (err) {
2135
+ this._log.error({ err }, "TTS error");
2136
+ } finally {
2137
+ this._speaking = false;
2138
+ }
2139
+ }
2140
+ };
2141
+
2142
+ // src/agent/pipeline/realtime/openai-realtime.ts
2143
+ var OPENAI_REALTIME_URL = "wss://api.openai.com/v1/realtime?model=";
2144
+ var OpenAIRealtime = class {
2145
+ _apiKey;
2146
+ _systemPrompt;
2147
+ _model;
2148
+ _voice;
2149
+ _language;
2150
+ _turnDetection;
2151
+ _greeting;
2152
+ _log = NOOP_LOGGER;
2153
+ _builtinTools = null;
2154
+ setLogger(logger) {
2155
+ this._log = logger;
2156
+ }
2157
+ setBuiltinTools(tools) {
2158
+ this._builtinTools = tools;
2159
+ }
2160
+ _ws = null;
2161
+ _call = null;
2162
+ _tools = null;
2163
+ _recorder = null;
2164
+ _closed = false;
2165
+ // PlaybackState — 현재 재생 중인 응답 상태
2166
+ _playback = null;
2167
+ _latestMediaTs = 0;
2168
+ // Pending tool call tracking — 인터럽트 시 취소용
2169
+ _pendingToolCalls = /* @__PURE__ */ new Map();
2170
+ // Response state tracking — prevent sending response.create while one is active
2171
+ _responseInProgress = false;
2172
+ _onResponseDone = null;
2173
+ constructor(options = {}) {
2174
+ this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
2175
+ this._systemPrompt = options.systemPrompt ?? "";
2176
+ this._model = options.model ?? "gpt-realtime-1.5";
2177
+ this._voice = options.voice ?? "marin";
2178
+ this._language = options.language ?? "ko";
2179
+ this._turnDetection = options.turnDetection !== void 0 ? options.turnDetection : { type: "semantic_vad", eagerness: "medium", interrupt_response: true };
2180
+ this._greeting = options.greeting ?? true;
2181
+ }
2182
+ /** Inject per-call ToolRegistry. */
2183
+ setToolRegistry(registry) {
2184
+ this._tools = registry;
2185
+ }
2186
+ /** Inject per-call AudioRecorder. */
2187
+ setRecorder(recorder) {
2188
+ this._recorder = recorder;
2189
+ }
2190
+ async start(callSession, tools) {
2191
+ this._call = callSession;
2192
+ if (tools) this._tools = tools;
2193
+ this._closed = false;
2194
+ this._playback = null;
2195
+ this._latestMediaTs = 0;
2196
+ if (!this._apiKey) {
2197
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey option.");
2198
+ }
2199
+ const { WebSocket } = await import('ws');
2200
+ const url = `${OPENAI_REALTIME_URL}${this._model}`;
2201
+ this._ws = new WebSocket(url, {
2202
+ headers: {
2203
+ Authorization: `Bearer ${this._apiKey}`,
2204
+ "OpenAI-Beta": "realtime=v1"
2205
+ }
2206
+ });
2207
+ return new Promise((resolve, reject) => {
2208
+ const ws = this._ws;
2209
+ ws.on("open", () => {
2210
+ this._sendSessionUpdate();
2211
+ this._log.info("OpenAI Realtime connected");
2212
+ if (this._greeting) {
2213
+ this._send({ type: "response.create" });
2214
+ }
2215
+ resolve();
2216
+ });
2217
+ ws.on("message", (data) => {
2218
+ try {
2219
+ const msg = JSON.parse(data.toString());
2220
+ this._handleMessage(msg);
2221
+ } catch {
2222
+ }
2223
+ });
2224
+ ws.on("close", () => {
2225
+ this._closed = true;
2226
+ });
2227
+ ws.on("error", (err) => {
2228
+ if (!this._ws) {
2229
+ reject(err);
2230
+ }
2231
+ this._log.error({ err }, "OpenAI Realtime WS error");
2232
+ });
2233
+ });
2234
+ }
2235
+ async feedDtmf(digits) {
2236
+ await this._waitForResponseDone();
2237
+ this._send({
2238
+ type: "conversation.item.create",
2239
+ item: {
2240
+ type: "message",
2241
+ role: "user",
2242
+ content: [{ type: "input_text", text: `[DTMF \uC785\uB825: ${digits}]` }]
2243
+ }
2244
+ });
2245
+ this._send({ type: "response.create" });
2246
+ }
2247
+ feedAudio(audio) {
2248
+ this._latestMediaTs = Date.now();
2249
+ if (this._ws && this._ws.readyState === 1 && !this._closed) {
2250
+ this._send({
2251
+ type: "input_audio_buffer.append",
2252
+ audio: audio.toString("base64")
2253
+ });
2254
+ }
2255
+ }
2256
+ async stop() {
2257
+ this._closed = true;
2258
+ for (const [, controller] of this._pendingToolCalls) {
2259
+ controller.abort();
2260
+ }
2261
+ this._pendingToolCalls.clear();
2262
+ if (this._ws) {
2263
+ this._ws.close();
2264
+ this._ws = null;
2265
+ }
2266
+ }
2267
+ _sendSessionUpdate() {
2268
+ if (!this._ws || this._ws.readyState !== 1) return;
2269
+ const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
2270
+ toolSchemas.push(...getBuiltinToolSchemas(this._builtinTools, "realtime"));
2271
+ this._send({
2272
+ type: "session.update",
2273
+ session: {
2274
+ modalities: ["text", "audio"],
2275
+ voice: this._voice,
2276
+ instructions: this._systemPrompt,
2277
+ input_audio_format: "g711_ulaw",
2278
+ output_audio_format: "g711_ulaw",
2279
+ input_audio_transcription: {
2280
+ model: "whisper-1",
2281
+ language: this._language
2282
+ },
2283
+ input_audio_noise_reduction: { type: "far_field" },
2284
+ turn_detection: this._turnDetection,
2285
+ tools: toolSchemas
2286
+ }
2287
+ });
2288
+ }
2289
+ _handleMessage(msg) {
2290
+ const type = msg["type"];
2291
+ switch (type) {
1971
2292
  case "response.audio.delta": {
1972
2293
  this._handleAudioDelta(msg);
1973
2294
  break;
1974
2295
  }
1975
2296
  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);
2297
+ if (this._playback) {
2298
+ this._playback.generating = false;
2299
+ if (this._playback.audioRemainder.length > 0) {
2300
+ const padded = Buffer.concat([
2301
+ this._playback.audioRemainder,
2302
+ Buffer.alloc(160 - this._playback.audioRemainder.length, 255)
2303
+ ]);
2304
+ if (this._call) {
2305
+ this._call.sendAudio(padded);
2306
+ }
2307
+ this._playback.sentChunks++;
2308
+ this._playback.audioRemainder = Buffer.alloc(0);
1983
2309
  }
1984
- this._sentAudioChunks++;
1985
- this._audioRemainder = Buffer.alloc(0);
1986
2310
  }
1987
2311
  break;
1988
2312
  }
@@ -2029,128 +2353,121 @@ var OpenAIRealtime = class {
2029
2353
  }
2030
2354
  }
2031
2355
  _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"];
2356
+ if (this._playback === null) {
2357
+ this._playback = {
2358
+ itemId: msg["item_id"] || "",
2359
+ startTs: this._latestMediaTs || Date.now(),
2360
+ sentChunks: 0,
2361
+ generating: true,
2362
+ audioRemainder: Buffer.alloc(0)
2363
+ };
2364
+ } else if (msg["item_id"]) {
2365
+ this._playback.itemId = msg["item_id"];
2038
2366
  }
2367
+ const pb = this._playback;
2039
2368
  const ulaw = Buffer.from(msg["delta"], "base64");
2040
2369
  if (this._recorder) {
2041
2370
  this._recorder.writeOutbound(ulawToPcm16(ulaw));
2042
2371
  }
2043
- const combined = Buffer.concat([this._audioRemainder, ulaw]);
2372
+ const combined = Buffer.concat([pb.audioRemainder, ulaw]);
2044
2373
  const chunkSize = 160;
2045
2374
  const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
2046
2375
  for (let off = 0; off < fullEnd; off += chunkSize) {
2047
2376
  if (this._call) {
2048
2377
  this._call.sendAudio(combined.subarray(off, off + chunkSize));
2049
2378
  }
2050
- this._sentAudioChunks++;
2379
+ pb.sentChunks++;
2051
2380
  }
2052
- this._audioRemainder = combined.subarray(fullEnd);
2381
+ pb.audioRemainder = combined.subarray(fullEnd);
2053
2382
  }
2054
2383
  _handleTruncation() {
2055
- if (!this._lastAssistantItem || this._responseStartTs === null) {
2056
- return;
2384
+ for (const [, controller] of this._pendingToolCalls) {
2385
+ controller.abort();
2057
2386
  }
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
2064
- });
2387
+ this._pendingToolCalls.clear();
2065
2388
  if (this._call) {
2066
2389
  this._call.clearAudio();
2067
2390
  }
2068
- this._lastAssistantItem = null;
2069
- this._responseStartTs = null;
2070
- this._sentAudioChunks = 0;
2071
- this._audioRemainder = Buffer.alloc(0);
2391
+ const pb = this._playback;
2392
+ if (pb === null) {
2393
+ return;
2394
+ }
2395
+ const playedMs = Math.max(0, (this._latestMediaTs || Date.now()) - pb.startTs);
2396
+ this._log.info(
2397
+ "[Interrupt] item=%s played=%dms total=%dms",
2398
+ pb.itemId,
2399
+ playedMs,
2400
+ pb.sentChunks * 20
2401
+ );
2402
+ this._playback = null;
2072
2403
  }
2073
2404
  async _handleToolCall(item) {
2074
2405
  const funcName = item["name"];
2075
2406
  const callId = item["call_id"];
2076
2407
  this._log.info("Tool call: %s", funcName);
2077
- if (funcName === "hang_up") {
2078
- if (this._call) {
2079
- await this._call.hangup();
2080
- }
2081
- return;
2082
- }
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
2408
+ const controller = new AbortController();
2409
+ this._pendingToolCalls.set(callId, controller);
2410
+ try {
2411
+ if (BUILTIN_TOOL_NAMES.has(funcName) && this._call) {
2412
+ const args = JSON.parse(item["arguments"] ?? "{}");
2413
+ const result2 = await executeBuiltinTool(funcName, args, this._call);
2414
+ if (result2 !== null) {
2415
+ if (funcName === "hang_up") return;
2416
+ if (controller.signal.aborted) return;
2417
+ await this._waitForResponseDone();
2418
+ this._send({
2419
+ type: "conversation.item.create",
2420
+ item: {
2421
+ type: "function_call_output",
2422
+ call_id: callId,
2423
+ output: result2
2424
+ }
2092
2425
  });
2093
- } catch (err) {
2094
- result2 = `Error: ${err}`;
2426
+ this._send({ type: "response.create" });
2427
+ return;
2095
2428
  }
2429
+ }
2430
+ if (!this._tools || !this._tools.has(funcName)) {
2431
+ this._log.error("Unknown tool: %s", funcName);
2096
2432
  await this._waitForResponseDone();
2097
2433
  this._send({
2098
2434
  type: "conversation.item.create",
2099
2435
  item: {
2100
2436
  type: "function_call_output",
2101
2437
  call_id: callId,
2102
- output: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)"
2438
+ output: JSON.stringify({ error: `Unknown tool: ${funcName}` })
2103
2439
  }
2104
2440
  });
2105
2441
  this._send({ type: "response.create" });
2442
+ return;
2106
2443
  }
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}`;
2118
- }
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" });
2444
+ let result;
2445
+ try {
2446
+ const args = JSON.parse(item["arguments"] ?? "{}");
2447
+ result = await this._tools.call(funcName, args);
2448
+ } catch (err) {
2449
+ this._log.error({ err }, "Tool call failed: %s", funcName);
2450
+ result = `Error: ${err}`;
2129
2451
  }
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)
2452
+ if (controller.signal.aborted) {
2453
+ this._log.info("Tool call cancelled (user interrupted): %s", funcName);
2454
+ return;
2151
2455
  }
2152
- });
2153
- this._send({ type: "response.create" });
2456
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
2457
+ await this._waitForResponseDone();
2458
+ this._send({
2459
+ type: "conversation.item.create",
2460
+ item: {
2461
+ type: "function_call_output",
2462
+ call_id: callId,
2463
+ output: resultStr
2464
+ }
2465
+ });
2466
+ this._log.info("[ToolResult] %s call_id=%s len=%d", funcName, callId, resultStr.length);
2467
+ this._send({ type: "response.create" });
2468
+ } finally {
2469
+ this._pendingToolCalls.delete(callId);
2470
+ }
2154
2471
  }
2155
2472
  _waitForResponseDone() {
2156
2473
  if (!this._responseInProgress) return Promise.resolve();
@@ -2165,39 +2482,7 @@ var OpenAIRealtime = class {
2165
2482
  }
2166
2483
  };
2167
2484
 
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."
2196
- }
2197
- },
2198
- required: ["digits"]
2199
- }
2200
- };
2485
+ // src/agent/pipeline/realtime/gemini-realtime.ts
2201
2486
  function resolveRef(ref, defs) {
2202
2487
  const parts = ref.replace(/^#\//, "").split("/");
2203
2488
  let result = defs;
@@ -2407,9 +2692,7 @@ var GeminiRealtime = class {
2407
2692
  t.function.parameters ?? { type: "object", properties: {} }
2408
2693
  )
2409
2694
  })) : [];
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);
2695
+ toolDefs.push(...getBuiltinToolSchemas(this._builtinTools, "gemini"));
2413
2696
  return toolDefs;
2414
2697
  }
2415
2698
  _handleMessage(msg) {
@@ -2497,51 +2780,17 @@ var GeminiRealtime = class {
2497
2780
  const fcId = fc.id ?? "";
2498
2781
  const args = fc.args ?? {};
2499
2782
  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();
2504
- }
2505
- return;
2506
- }
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}`;
2521
- }
2522
- responses.push({
2523
- id: fcId,
2524
- name,
2525
- response: { result: result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)" }
2526
- });
2527
- }
2528
- continue;
2529
- }
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}`;
2783
+ if (BUILTIN_TOOL_NAMES.has(name) && this._call) {
2784
+ const result = await executeBuiltinTool(name, args, this._call);
2785
+ if (result !== null) {
2786
+ if (name === "hang_up") {
2787
+ this._log.info("hang_up: ending call");
2788
+ return;
2541
2789
  }
2790
+ this._log.info("Builtin tool result: %s -> %s", name, result);
2542
2791
  responses.push({ id: fcId, name, response: { result } });
2792
+ continue;
2543
2793
  }
2544
- continue;
2545
2794
  }
2546
2795
  if (!this._tools || !this._tools.has(name)) {
2547
2796
  this._log.error("Unknown tool: %s", name);
@@ -2576,306 +2825,7 @@ var GeminiRealtime = class {
2576
2825
  }
2577
2826
  };
2578
2827
 
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
- }
2591
- }
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"]
2601
- }
2602
- }
2603
- };
2604
- var PipelineSession = class {
2605
- _stt;
2606
- _llm;
2607
- _tts;
2608
- _systemPrompt;
2609
- _greeting;
2610
- _language;
2611
- _temperature;
2612
- _maxTokens;
2613
- _sampleRate;
2614
- _interruptOnSpeech;
2615
- _callSession = null;
2616
- _tools = null;
2617
- _recorder = null;
2618
- _conversation = [];
2619
- _audioBuffer = [];
2620
- _running = false;
2621
- _speaking = false;
2622
- _builtinTools = null;
2623
- _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;
2630
- 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;
2637
- }
2638
- setToolRegistry(registry) {
2639
- this._tools = registry;
2640
- }
2641
- setRecorder(recorder) {
2642
- this._recorder = recorder;
2643
- }
2644
- setBuiltinTools(tools) {
2645
- this._builtinTools = tools;
2646
- }
2647
- setLogger(logger) {
2648
- 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
- }
2655
- }
2656
- 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
- });
2667
- }
2668
- if (this._greeting) {
2669
- this._generateGreeting().catch((err) => {
2670
- this._log.error({ err }, "Greeting error");
2671
- });
2672
- }
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);
2680
- }
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();
2704
- }
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
- }
2711
- }
2712
- }
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));
2722
- }
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 () => ""
2745
- });
2746
- }
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 () => ""
2754
- });
2755
- }
2756
- return base;
2757
- }
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);
2774
- }
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);
2780
- }
2781
- }
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}`;
2797
- }
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
- }
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;
2813
- }
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;
2838
- }
2839
- }
2840
- if (followUpText.trim()) {
2841
- this._conversation.push({ role: "assistant", content: followUpText });
2842
- await this._synthesizeAndSend(followUpText);
2843
- }
2844
- } catch (err) {
2845
- this._log.error({ err }, "Tool call failed: %s", name);
2846
- }
2847
- }
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)]);
2866
- }
2867
- this._callSession.sendAudio(chunk);
2868
- }
2869
- }
2870
- } catch (err) {
2871
- this._log.error({ err }, "TTS error");
2872
- } finally {
2873
- this._speaking = false;
2874
- }
2875
- }
2876
- };
2877
-
2878
- // src/agent/pipeline/deepgram-stt.ts
2828
+ // src/agent/pipeline/stt/deepgram-stt.ts
2879
2829
  var DeepgramSTT = class {
2880
2830
  _options;
2881
2831
  _log = NOOP_LOGGER;
@@ -2996,7 +2946,7 @@ var DeepgramSTT = class {
2996
2946
  }
2997
2947
  };
2998
2948
 
2999
- // src/agent/pipeline/elevenlabs-tts.ts
2949
+ // src/agent/pipeline/tts/elevenlabs-tts.ts
3000
2950
  var ElevenLabsTTS = class {
3001
2951
  _options;
3002
2952
  _log = NOOP_LOGGER;
@@ -3149,7 +3099,7 @@ var ElevenLabsTTS = class {
3149
3099
  }
3150
3100
  };
3151
3101
 
3152
- // src/agent/pipeline/openai-llm.ts
3102
+ // src/agent/pipeline/llm/openai-llm.ts
3153
3103
  var OpenAILLM = class {
3154
3104
  _options;
3155
3105
  constructor(options = {}) {
@@ -3235,7 +3185,7 @@ var OpenAILLM = class {
3235
3185
  }
3236
3186
  };
3237
3187
 
3238
- // src/agent/pipeline/anthropic-llm.ts
3188
+ // src/agent/pipeline/llm/anthropic-llm.ts
3239
3189
  var AnthropicLLM = class {
3240
3190
  _options;
3241
3191
  constructor(options = {}) {
@@ -3340,7 +3290,7 @@ var AnthropicLLM = class {
3340
3290
  }
3341
3291
  };
3342
3292
 
3343
- // src/agent/pipeline/gemini-llm.ts
3293
+ // src/agent/pipeline/llm/gemini-llm.ts
3344
3294
  var GeminiLLM = class {
3345
3295
  _options;
3346
3296
  constructor(options = {}) {
@@ -3424,7 +3374,7 @@ var GeminiLLM = class {
3424
3374
  }
3425
3375
  };
3426
3376
 
3427
- // src/agent/pipeline/openai-compat-llm.ts
3377
+ // src/agent/pipeline/llm/openai-compat-llm.ts
3428
3378
  var OpenAICompatLLM = class {
3429
3379
  _options;
3430
3380
  constructor(options) {
@@ -3507,7 +3457,7 @@ var OpenAICompatLLM = class {
3507
3457
  }
3508
3458
  };
3509
3459
 
3510
- // src/agent/pipeline/ollama-llm.ts
3460
+ // src/agent/pipeline/llm/ollama-llm.ts
3511
3461
  var OllamaLLM = class {
3512
3462
  _inner;
3513
3463
  constructor(options = {}) {
@@ -3525,7 +3475,7 @@ var OllamaLLM = class {
3525
3475
  }
3526
3476
  };
3527
3477
 
3528
- // src/agent/pipeline/mistral-llm.ts
3478
+ // src/agent/pipeline/llm/mistral-llm.ts
3529
3479
  var MistralLLM = class {
3530
3480
  _inner;
3531
3481
  constructor(options = {}) {
@@ -3542,7 +3492,7 @@ var MistralLLM = class {
3542
3492
  }
3543
3493
  };
3544
3494
 
3545
- // src/agent/pipeline/groq-llm.ts
3495
+ // src/agent/pipeline/llm/groq-llm.ts
3546
3496
  var GroqLLM = class {
3547
3497
  _inner;
3548
3498
  constructor(options = {}) {
@@ -3559,7 +3509,7 @@ var GroqLLM = class {
3559
3509
  }
3560
3510
  };
3561
3511
 
3562
- // src/agent/pipeline/perplexity-llm.ts
3512
+ // src/agent/pipeline/llm/perplexity-llm.ts
3563
3513
  var PerplexityLLM = class {
3564
3514
  _inner;
3565
3515
  constructor(options = {}) {
@@ -3576,7 +3526,7 @@ var PerplexityLLM = class {
3576
3526
  }
3577
3527
  };
3578
3528
 
3579
- // src/agent/pipeline/together-llm.ts
3529
+ // src/agent/pipeline/llm/together-llm.ts
3580
3530
  var TogetherLLM = class {
3581
3531
  _inner;
3582
3532
  constructor(options = {}) {
@@ -3593,7 +3543,7 @@ var TogetherLLM = class {
3593
3543
  }
3594
3544
  };
3595
3545
 
3596
- // src/agent/pipeline/fireworks-llm.ts
3546
+ // src/agent/pipeline/llm/fireworks-llm.ts
3597
3547
  var FireworksLLM = class {
3598
3548
  _inner;
3599
3549
  constructor(options = {}) {
@@ -3610,7 +3560,7 @@ var FireworksLLM = class {
3610
3560
  }
3611
3561
  };
3612
3562
 
3613
- // src/agent/pipeline/deepseek-llm.ts
3563
+ // src/agent/pipeline/llm/deepseek-llm.ts
3614
3564
  var DeepSeekLLM = class {
3615
3565
  _inner;
3616
3566
  constructor(options = {}) {
@@ -3627,7 +3577,7 @@ var DeepSeekLLM = class {
3627
3577
  }
3628
3578
  };
3629
3579
 
3630
- // src/agent/pipeline/xai-llm.ts
3580
+ // src/agent/pipeline/llm/xai-llm.ts
3631
3581
  var XaiLLM = class {
3632
3582
  _inner;
3633
3583
  constructor(options = {}) {
@@ -3663,6 +3613,6 @@ function mcpServerHTTP(options) {
3663
3613
  };
3664
3614
  }
3665
3615
 
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 };
3616
+ 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
3617
  //# sourceMappingURL=index.js.map
3668
3618
  //# sourceMappingURL=index.js.map