@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.
@@ -1485,6 +1485,7 @@ var ClawOpsAgent = class {
1485
1485
  }
1486
1486
  /** Connect to the ClawOps platform and start listening for calls. */
1487
1487
  async connect() {
1488
+ if (this._controlWs) return;
1488
1489
  if (!this._apiKey) {
1489
1490
  throw new chunk6IQN5RQD_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
1490
1491
  }
@@ -1799,20 +1800,13 @@ var ClawOpsAgent = class {
1799
1800
  }
1800
1801
  };
1801
1802
 
1802
- // src/agent/pipeline/openai-realtime.ts
1803
- var OPENAI_REALTIME_URL = "wss://api.openai.com/v1/realtime?model=";
1804
- var HANG_UP_TOOL = {
1805
- type: "function",
1803
+ // src/agent/pipeline/builtin-tool-schemas.ts
1804
+ var HANG_UP = {
1806
1805
  name: "hang_up",
1807
1806
  description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
1808
- parameters: {
1809
- type: "object",
1810
- properties: {},
1811
- required: []
1812
- }
1807
+ parameters: { type: "object", properties: {} }
1813
1808
  };
1814
- var COLLECT_DTMF_TOOL = {
1815
- type: "function",
1809
+ var COLLECT_DTMF = {
1816
1810
  name: "collect_dtmf",
1817
1811
  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.",
1818
1812
  parameters: {
@@ -1825,8 +1819,7 @@ var COLLECT_DTMF_TOOL = {
1825
1819
  required: ["max_digits"]
1826
1820
  }
1827
1821
  };
1828
- var SEND_DTMF_TOOL = {
1829
- type: "function",
1822
+ var SEND_DTMF = {
1830
1823
  name: "send_dtmf",
1831
1824
  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.",
1832
1825
  parameters: {
@@ -1834,181 +1827,512 @@ var SEND_DTMF_TOOL = {
1834
1827
  properties: {
1835
1828
  digits: {
1836
1829
  type: "string",
1837
- description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
1830
+ description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30. \uC608: '1', '1234#', '1w2'"
1838
1831
  }
1839
1832
  },
1840
1833
  required: ["digits"]
1841
1834
  }
1842
1835
  };
1843
- var OpenAIRealtime = class {
1844
- _apiKey;
1845
- _systemPrompt;
1846
- _model;
1847
- _voice;
1848
- _language;
1849
- _eagerness;
1850
- _greeting;
1851
- _log = NOOP_LOGGER;
1852
- _builtinTools = null;
1853
- setLogger(logger) {
1854
- this._log = logger;
1836
+ var TOOL_MAP = /* @__PURE__ */ new Map([
1837
+ ["hang_up" /* HANG_UP */, HANG_UP],
1838
+ ["collect_dtmf" /* COLLECT_DTMF */, COLLECT_DTMF],
1839
+ ["send_dtmf" /* SEND_DTMF */, SEND_DTMF]
1840
+ ]);
1841
+ var BUILTIN_TOOL_NAMES = new Set(
1842
+ Array.from(TOOL_MAP.values()).map((s) => s.name)
1843
+ );
1844
+ function toChatCompletions(schema) {
1845
+ return {
1846
+ type: "function",
1847
+ function: {
1848
+ name: schema.name,
1849
+ description: schema.description,
1850
+ parameters: schema.parameters
1851
+ }
1852
+ };
1853
+ }
1854
+ function toRealtime(schema) {
1855
+ return {
1856
+ type: "function",
1857
+ name: schema.name,
1858
+ description: schema.description,
1859
+ parameters: schema.parameters
1860
+ };
1861
+ }
1862
+ function toGemini(schema) {
1863
+ return {
1864
+ name: schema.name,
1865
+ description: schema.description,
1866
+ parameters: schema.parameters
1867
+ };
1868
+ }
1869
+ var CONVERTERS = {
1870
+ chat: toChatCompletions,
1871
+ realtime: toRealtime,
1872
+ gemini: toGemini
1873
+ };
1874
+ function getBuiltinToolSchemas(builtinTools, fmt) {
1875
+ const converter = CONVERTERS[fmt];
1876
+ const result = [];
1877
+ for (const [toolEnum, schema] of TOOL_MAP) {
1878
+ if (builtinTools === null || builtinTools.has(toolEnum)) {
1879
+ result.push(converter(schema));
1880
+ }
1855
1881
  }
1856
- setBuiltinTools(tools) {
1857
- this._builtinTools = tools;
1882
+ return result;
1883
+ }
1884
+ function isBuiltinTool(name) {
1885
+ return BUILTIN_TOOL_NAMES.has(name);
1886
+ }
1887
+ async function executeBuiltinTool(funcName, args, call) {
1888
+ if (funcName === "hang_up") {
1889
+ await call.hangup();
1890
+ return "";
1858
1891
  }
1859
- _ws = null;
1860
- _call = null;
1892
+ if (funcName === "collect_dtmf") {
1893
+ try {
1894
+ const result = await call.collectDtmf({
1895
+ maxDigits: args["max_digits"] ?? 4,
1896
+ finishOnKey: args["finish_on_key"] ?? "#",
1897
+ timeout: args["timeout"] ?? 5
1898
+ });
1899
+ return result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)";
1900
+ } catch (e) {
1901
+ return `Error: ${e}`;
1902
+ }
1903
+ }
1904
+ if (funcName === "send_dtmf") {
1905
+ try {
1906
+ await call.sendDtmfSequence(args["digits"] ?? "");
1907
+ return "sent";
1908
+ } catch (e) {
1909
+ return `Error: ${e}`;
1910
+ }
1911
+ }
1912
+ return null;
1913
+ }
1914
+
1915
+ // src/agent/pipeline/pipeline-session.ts
1916
+ var PipelineSession = class {
1917
+ _stt;
1918
+ _llm;
1919
+ _tts;
1920
+ _systemPrompt;
1921
+ _greeting;
1922
+ _language;
1923
+ _temperature;
1924
+ _maxTokens;
1925
+ _sampleRate;
1926
+ _interruptOnSpeech;
1927
+ _callSession = null;
1861
1928
  _tools = null;
1862
1929
  _recorder = null;
1863
- _closed = false;
1864
- // Truncation / barge-in tracking (matching Python SDK)
1865
- _lastAssistantItem = null;
1866
- _responseStartTs = null;
1867
- _sentAudioChunks = 0;
1868
- _audioRemainder = Buffer.alloc(0);
1869
- // Response state tracking — prevent sending response.create while one is active
1870
- _responseInProgress = false;
1871
- _onResponseDone = null;
1872
- constructor(options = {}) {
1873
- this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
1874
- this._systemPrompt = options.systemPrompt ?? "";
1875
- this._model = options.model ?? "gpt-realtime-1.5";
1876
- this._voice = options.voice ?? "marin";
1877
- this._language = options.language ?? "ko";
1878
- this._eagerness = options.eagerness ?? "high";
1930
+ _conversation = [];
1931
+ _audioBuffer = [];
1932
+ _running = false;
1933
+ _speaking = false;
1934
+ _builtinTools = null;
1935
+ _log = NOOP_LOGGER;
1936
+ constructor(options) {
1937
+ this._stt = options.stt;
1938
+ this._llm = options.llm;
1939
+ this._tts = options.tts;
1940
+ this._systemPrompt = options.systemPrompt;
1879
1941
  this._greeting = options.greeting ?? true;
1942
+ this._language = options.language ?? "ko";
1943
+ this._temperature = options.temperature;
1944
+ this._maxTokens = options.maxTokens;
1945
+ this._sampleRate = options.sampleRate ?? 8e3;
1946
+ this._interruptOnSpeech = options.interruptOnSpeech ?? true;
1947
+ if (options.toolRegistry) this._tools = options.toolRegistry;
1948
+ if (options.recorder) this._recorder = options.recorder;
1880
1949
  }
1881
- /** Inject per-call ToolRegistry. */
1882
1950
  setToolRegistry(registry) {
1883
1951
  this._tools = registry;
1884
1952
  }
1885
- /** Inject per-call AudioRecorder. */
1886
1953
  setRecorder(recorder) {
1887
1954
  this._recorder = recorder;
1888
1955
  }
1889
- async start(callSession, tools) {
1890
- this._call = callSession;
1891
- if (tools) this._tools = tools;
1892
- this._closed = false;
1893
- this._lastAssistantItem = null;
1894
- this._responseStartTs = null;
1895
- this._sentAudioChunks = 0;
1896
- this._audioRemainder = Buffer.alloc(0);
1897
- if (!this._apiKey) {
1898
- throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey option.");
1956
+ setBuiltinTools(tools) {
1957
+ this._builtinTools = tools;
1958
+ }
1959
+ setLogger(logger) {
1960
+ this._log = logger;
1961
+ if ("setLogger" in this._stt && typeof this._stt.setLogger === "function") {
1962
+ this._stt.setLogger(logger);
1899
1963
  }
1900
- const { WebSocket } = await import('ws');
1901
- const url = `${OPENAI_REALTIME_URL}${this._model}`;
1902
- this._ws = new WebSocket(url, {
1903
- headers: {
1904
- Authorization: `Bearer ${this._apiKey}`,
1905
- "OpenAI-Beta": "realtime=v1"
1906
- }
1907
- });
1908
- return new Promise((resolve, reject) => {
1909
- const ws = this._ws;
1910
- ws.on("open", () => {
1911
- this._sendSessionUpdate();
1912
- this._log.info("OpenAI Realtime connected");
1913
- if (this._greeting) {
1914
- this._send({ type: "response.create" });
1915
- }
1916
- resolve();
1917
- });
1918
- ws.on("message", (data) => {
1919
- try {
1920
- const msg = JSON.parse(data.toString());
1921
- this._handleMessage(msg);
1922
- } catch {
1923
- }
1924
- });
1925
- ws.on("close", () => {
1926
- this._closed = true;
1964
+ if ("setLogger" in this._tts && typeof this._tts.setLogger === "function") {
1965
+ this._tts.setLogger(logger);
1966
+ }
1967
+ }
1968
+ async start(callSession, tools) {
1969
+ this._callSession = callSession;
1970
+ this._tools = tools ?? null;
1971
+ this._running = true;
1972
+ this._log.info("PipelineSession started");
1973
+ this._conversation = [];
1974
+ if (this._systemPrompt) {
1975
+ this._conversation.push({
1976
+ role: "system",
1977
+ content: this._systemPrompt
1927
1978
  });
1928
- ws.on("error", (err) => {
1929
- if (!this._ws) {
1930
- reject(err);
1931
- }
1932
- this._log.error({ err }, "OpenAI Realtime WS error");
1979
+ }
1980
+ if (this._greeting) {
1981
+ this._generateGreeting().catch((err) => {
1982
+ this._log.error({ err }, "Greeting error");
1933
1983
  });
1984
+ }
1985
+ this._runSttLoop().catch((err) => {
1986
+ this._log.error({ err }, "STT loop error");
1934
1987
  });
1935
1988
  }
1936
- async feedDtmf(digits) {
1937
- await this._waitForResponseDone();
1938
- this._send({
1939
- type: "conversation.item.create",
1940
- item: {
1941
- type: "message",
1942
- role: "user",
1943
- content: [{ type: "input_text", text: `[DTMF \uC785\uB825: ${digits}]` }]
1944
- }
1945
- });
1946
- this._send({ type: "response.create" });
1947
- }
1948
1989
  feedAudio(audio) {
1949
- if (this._ws && this._ws.readyState === 1 && !this._closed) {
1950
- this._send({
1951
- type: "input_audio_buffer.append",
1952
- audio: audio.toString("base64")
1953
- });
1990
+ if (this._running) {
1991
+ this._audioBuffer.push(audio);
1954
1992
  }
1955
1993
  }
1994
+ async feedDtmf(digits) {
1995
+ this._conversation.push({
1996
+ role: "user",
1997
+ content: `[DTMF \uC785\uB825: ${digits}]`
1998
+ });
1999
+ await this._respond();
2000
+ }
1956
2001
  async stop() {
1957
- this._closed = true;
1958
- if (this._ws) {
1959
- this._ws.close();
1960
- this._ws = null;
2002
+ this._running = false;
2003
+ this._log.info("PipelineSession stopped");
2004
+ this._audioBuffer = [];
2005
+ }
2006
+ async _runSttLoop() {
2007
+ const audioStream = this._createAudioStream();
2008
+ for await (const event of this._stt.transcribe(audioStream, {
2009
+ sampleRate: this._sampleRate
2010
+ })) {
2011
+ if (!this._running) break;
2012
+ if (event.type === "interim" && this._speaking && this._interruptOnSpeech) {
2013
+ this._speaking = false;
2014
+ if (this._callSession) {
2015
+ this._callSession.clearAudio();
2016
+ }
2017
+ this._log.info('Barge-in: "%s"', event.transcript.substring(0, 30));
2018
+ }
2019
+ if (event.type === "final" && event.transcript.trim()) {
2020
+ this._log.info("STT: %s", event.transcript);
2021
+ await this._handleUserSpeech(event.transcript);
2022
+ }
1961
2023
  }
1962
2024
  }
1963
- _sendSessionUpdate() {
1964
- if (!this._ws || this._ws.readyState !== 1) return;
1965
- const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
1966
- if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */))
1967
- toolSchemas.push(HANG_UP_TOOL);
1968
- if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */))
1969
- toolSchemas.push(COLLECT_DTMF_TOOL);
1970
- if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */))
1971
- toolSchemas.push(SEND_DTMF_TOOL);
1972
- this._send({
1973
- type: "session.update",
1974
- session: {
1975
- modalities: ["text", "audio"],
1976
- voice: this._voice,
1977
- instructions: this._systemPrompt,
1978
- input_audio_format: "g711_ulaw",
1979
- output_audio_format: "g711_ulaw",
1980
- input_audio_transcription: {
1981
- model: "whisper-1",
1982
- language: this._language
1983
- },
1984
- input_audio_noise_reduction: { type: "far_field" },
1985
- turn_detection: {
1986
- type: "semantic_vad",
1987
- interrupt_response: true,
1988
- eagerness: this._eagerness
1989
- },
1990
- tools: toolSchemas
2025
+ async *_createAudioStream() {
2026
+ while (this._running) {
2027
+ if (this._audioBuffer.length > 0) {
2028
+ const ulaw = this._audioBuffer.shift();
2029
+ const pcm8k = ulawToPcm16(ulaw);
2030
+ const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
2031
+ yield pcm16k;
2032
+ } else {
2033
+ await new Promise((resolve) => setTimeout(resolve, 20));
1991
2034
  }
2035
+ }
2036
+ }
2037
+ async _generateGreeting() {
2038
+ await new Promise((resolve) => setTimeout(resolve, 500));
2039
+ await this._respond();
2040
+ }
2041
+ async _handleUserSpeech(transcript) {
2042
+ this._conversation.push({ role: "user", content: transcript });
2043
+ await this._respond();
2044
+ }
2045
+ _buildEffectiveTools() {
2046
+ const builtinSchemas = getBuiltinToolSchemas(this._builtinTools, "chat");
2047
+ const dtmfSchemas = builtinSchemas.filter((s) => {
2048
+ const name = s["function"]?.["name"];
2049
+ return name === "collect_dtmf" || name === "send_dtmf";
1992
2050
  });
2051
+ if (dtmfSchemas.length === 0) return this._tools ?? void 0;
2052
+ const base = this._tools ? this._tools.fork() : new ToolRegistry();
2053
+ for (const schema of dtmfSchemas) {
2054
+ const fn = schema["function"];
2055
+ const params = fn["parameters"];
2056
+ base.register({
2057
+ name: fn["name"],
2058
+ description: fn["description"],
2059
+ parameters: params["properties"] ?? {},
2060
+ required: params["required"] ?? [],
2061
+ handler: async () => ""
2062
+ });
2063
+ }
2064
+ return base;
1993
2065
  }
1994
- _handleMessage(msg) {
1995
- const type = msg["type"];
1996
- switch (type) {
2066
+ async _respond() {
2067
+ let fullResponse = "";
2068
+ const textChunks = [];
2069
+ const effectiveTools = this._buildEffectiveTools();
2070
+ const llmStream = this._llm.generate(this._conversation, {
2071
+ tools: effectiveTools,
2072
+ temperature: this._temperature,
2073
+ maxTokens: this._maxTokens
2074
+ });
2075
+ for await (const chunk of llmStream) {
2076
+ if (!this._running) break;
2077
+ if (chunk.type === "text" && chunk.text) {
2078
+ textChunks.push(chunk.text);
2079
+ fullResponse += chunk.text;
2080
+ } else if (chunk.type === "tool_call" && chunk.toolCall) {
2081
+ await this._handleToolCall(chunk);
2082
+ }
2083
+ }
2084
+ if (fullResponse.trim()) {
2085
+ this._log.info("Assistant: %s", fullResponse.substring(0, 100));
2086
+ this._conversation.push({ role: "assistant", content: fullResponse });
2087
+ await this._synthesizeAndSend(fullResponse);
2088
+ }
2089
+ }
2090
+ async _handleToolCall(chunk) {
2091
+ if (!chunk.toolCall) return;
2092
+ const { id, name, arguments: argsStr } = chunk.toolCall;
2093
+ try {
2094
+ const args = JSON.parse(argsStr);
2095
+ if (BUILTIN_TOOL_NAMES.has(name) && this._callSession) {
2096
+ const result2 = await executeBuiltinTool(name, args, this._callSession);
2097
+ if (result2 !== null) {
2098
+ if (name === "hang_up") return;
2099
+ this._conversation.push({ role: "tool", content: result2, tool_call_id: id, name });
2100
+ await this._respond();
2101
+ return;
2102
+ }
2103
+ }
2104
+ if (!this._tools) return;
2105
+ const result = await this._tools.call(name, args);
2106
+ this._conversation.push({
2107
+ role: "assistant",
2108
+ content: ""
2109
+ // Tool call info stored in the message flow
2110
+ });
2111
+ this._conversation.push({
2112
+ role: "tool",
2113
+ content: typeof result === "string" ? result : JSON.stringify(result),
2114
+ tool_call_id: id,
2115
+ name
2116
+ });
2117
+ const effectiveTools = this._buildEffectiveTools();
2118
+ let followUpText = "";
2119
+ const followUpStream = this._llm.generate(this._conversation, {
2120
+ tools: effectiveTools,
2121
+ temperature: this._temperature,
2122
+ maxTokens: this._maxTokens
2123
+ });
2124
+ for await (const followChunk of followUpStream) {
2125
+ if (!this._running) break;
2126
+ if (followChunk.type === "text" && followChunk.text) {
2127
+ followUpText += followChunk.text;
2128
+ }
2129
+ }
2130
+ if (followUpText.trim()) {
2131
+ this._conversation.push({ role: "assistant", content: followUpText });
2132
+ await this._synthesizeAndSend(followUpText);
2133
+ }
2134
+ } catch (err) {
2135
+ this._log.error({ err }, "Tool call failed: %s", name);
2136
+ }
2137
+ }
2138
+ async _synthesizeAndSend(text) {
2139
+ if (!this._callSession || !this._running) return;
2140
+ this._speaking = true;
2141
+ try {
2142
+ for await (const audioChunk of this._tts.synthesize(text, {
2143
+ sampleRate: this._sampleRate
2144
+ })) {
2145
+ if (!this._running || !this._speaking) break;
2146
+ if (this._recorder) {
2147
+ const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2148
+ this._recorder.writeOutbound(pcm8k2);
2149
+ }
2150
+ const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2151
+ const ulaw = pcm16ToUlaw(pcm8k);
2152
+ for (let off = 0; off < ulaw.length; off += 160) {
2153
+ let chunk = ulaw.subarray(off, off + 160);
2154
+ if (chunk.length < 160) {
2155
+ chunk = Buffer.concat([chunk, Buffer.alloc(160 - chunk.length, 255)]);
2156
+ }
2157
+ this._callSession.sendAudio(chunk);
2158
+ }
2159
+ }
2160
+ } catch (err) {
2161
+ this._log.error({ err }, "TTS error");
2162
+ } finally {
2163
+ this._speaking = false;
2164
+ }
2165
+ }
2166
+ };
2167
+
2168
+ // src/agent/pipeline/realtime/openai-realtime.ts
2169
+ var OPENAI_REALTIME_URL = "wss://api.openai.com/v1/realtime?model=";
2170
+ var OpenAIRealtime = class {
2171
+ _apiKey;
2172
+ _systemPrompt;
2173
+ _model;
2174
+ _voice;
2175
+ _language;
2176
+ _turnDetection;
2177
+ _greeting;
2178
+ _log = NOOP_LOGGER;
2179
+ _builtinTools = null;
2180
+ setLogger(logger) {
2181
+ this._log = logger;
2182
+ }
2183
+ setBuiltinTools(tools) {
2184
+ this._builtinTools = tools;
2185
+ }
2186
+ _ws = null;
2187
+ _call = null;
2188
+ _tools = null;
2189
+ _recorder = null;
2190
+ _closed = false;
2191
+ // PlaybackState — 현재 재생 중인 응답 상태
2192
+ _playback = null;
2193
+ _latestMediaTs = 0;
2194
+ // Pending tool call tracking — 인터럽트 시 취소용
2195
+ _pendingToolCalls = /* @__PURE__ */ new Map();
2196
+ // Response state tracking — prevent sending response.create while one is active
2197
+ _responseInProgress = false;
2198
+ _onResponseDone = null;
2199
+ constructor(options = {}) {
2200
+ this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
2201
+ this._systemPrompt = options.systemPrompt ?? "";
2202
+ this._model = options.model ?? "gpt-realtime-1.5";
2203
+ this._voice = options.voice ?? "marin";
2204
+ this._language = options.language ?? "ko";
2205
+ this._turnDetection = options.turnDetection !== void 0 ? options.turnDetection : { type: "semantic_vad", eagerness: "medium", interrupt_response: true };
2206
+ this._greeting = options.greeting ?? true;
2207
+ }
2208
+ /** Inject per-call ToolRegistry. */
2209
+ setToolRegistry(registry) {
2210
+ this._tools = registry;
2211
+ }
2212
+ /** Inject per-call AudioRecorder. */
2213
+ setRecorder(recorder) {
2214
+ this._recorder = recorder;
2215
+ }
2216
+ async start(callSession, tools) {
2217
+ this._call = callSession;
2218
+ if (tools) this._tools = tools;
2219
+ this._closed = false;
2220
+ this._playback = null;
2221
+ this._latestMediaTs = 0;
2222
+ if (!this._apiKey) {
2223
+ throw new Error("OpenAI API key is required. Set OPENAI_API_KEY or pass apiKey option.");
2224
+ }
2225
+ const { WebSocket } = await import('ws');
2226
+ const url = `${OPENAI_REALTIME_URL}${this._model}`;
2227
+ this._ws = new WebSocket(url, {
2228
+ headers: {
2229
+ Authorization: `Bearer ${this._apiKey}`,
2230
+ "OpenAI-Beta": "realtime=v1"
2231
+ }
2232
+ });
2233
+ return new Promise((resolve, reject) => {
2234
+ const ws = this._ws;
2235
+ ws.on("open", () => {
2236
+ this._sendSessionUpdate();
2237
+ this._log.info("OpenAI Realtime connected");
2238
+ if (this._greeting) {
2239
+ this._send({ type: "response.create" });
2240
+ }
2241
+ resolve();
2242
+ });
2243
+ ws.on("message", (data) => {
2244
+ try {
2245
+ const msg = JSON.parse(data.toString());
2246
+ this._handleMessage(msg);
2247
+ } catch {
2248
+ }
2249
+ });
2250
+ ws.on("close", () => {
2251
+ this._closed = true;
2252
+ });
2253
+ ws.on("error", (err) => {
2254
+ if (!this._ws) {
2255
+ reject(err);
2256
+ }
2257
+ this._log.error({ err }, "OpenAI Realtime WS error");
2258
+ });
2259
+ });
2260
+ }
2261
+ async feedDtmf(digits) {
2262
+ await this._waitForResponseDone();
2263
+ this._send({
2264
+ type: "conversation.item.create",
2265
+ item: {
2266
+ type: "message",
2267
+ role: "user",
2268
+ content: [{ type: "input_text", text: `[DTMF \uC785\uB825: ${digits}]` }]
2269
+ }
2270
+ });
2271
+ this._send({ type: "response.create" });
2272
+ }
2273
+ feedAudio(audio) {
2274
+ this._latestMediaTs = Date.now();
2275
+ if (this._ws && this._ws.readyState === 1 && !this._closed) {
2276
+ this._send({
2277
+ type: "input_audio_buffer.append",
2278
+ audio: audio.toString("base64")
2279
+ });
2280
+ }
2281
+ }
2282
+ async stop() {
2283
+ this._closed = true;
2284
+ for (const [, controller] of this._pendingToolCalls) {
2285
+ controller.abort();
2286
+ }
2287
+ this._pendingToolCalls.clear();
2288
+ if (this._ws) {
2289
+ this._ws.close();
2290
+ this._ws = null;
2291
+ }
2292
+ }
2293
+ _sendSessionUpdate() {
2294
+ if (!this._ws || this._ws.readyState !== 1) return;
2295
+ const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
2296
+ toolSchemas.push(...getBuiltinToolSchemas(this._builtinTools, "realtime"));
2297
+ this._send({
2298
+ type: "session.update",
2299
+ session: {
2300
+ modalities: ["text", "audio"],
2301
+ voice: this._voice,
2302
+ instructions: this._systemPrompt,
2303
+ input_audio_format: "g711_ulaw",
2304
+ output_audio_format: "g711_ulaw",
2305
+ input_audio_transcription: {
2306
+ model: "whisper-1",
2307
+ language: this._language
2308
+ },
2309
+ input_audio_noise_reduction: { type: "far_field" },
2310
+ turn_detection: this._turnDetection,
2311
+ tools: toolSchemas
2312
+ }
2313
+ });
2314
+ }
2315
+ _handleMessage(msg) {
2316
+ const type = msg["type"];
2317
+ switch (type) {
1997
2318
  case "response.audio.delta": {
1998
2319
  this._handleAudioDelta(msg);
1999
2320
  break;
2000
2321
  }
2001
2322
  case "response.audio.done": {
2002
- if (this._audioRemainder.length > 0) {
2003
- const padded = Buffer.concat([
2004
- this._audioRemainder,
2005
- Buffer.alloc(160 - this._audioRemainder.length, 255)
2006
- ]);
2007
- if (this._call) {
2008
- this._call.sendAudio(padded);
2323
+ if (this._playback) {
2324
+ this._playback.generating = false;
2325
+ if (this._playback.audioRemainder.length > 0) {
2326
+ const padded = Buffer.concat([
2327
+ this._playback.audioRemainder,
2328
+ Buffer.alloc(160 - this._playback.audioRemainder.length, 255)
2329
+ ]);
2330
+ if (this._call) {
2331
+ this._call.sendAudio(padded);
2332
+ }
2333
+ this._playback.sentChunks++;
2334
+ this._playback.audioRemainder = Buffer.alloc(0);
2009
2335
  }
2010
- this._sentAudioChunks++;
2011
- this._audioRemainder = Buffer.alloc(0);
2012
2336
  }
2013
2337
  break;
2014
2338
  }
@@ -2055,128 +2379,121 @@ var OpenAIRealtime = class {
2055
2379
  }
2056
2380
  }
2057
2381
  _handleAudioDelta(msg) {
2058
- if (this._responseStartTs === null) {
2059
- this._responseStartTs = Date.now();
2060
- this._sentAudioChunks = 0;
2061
- }
2062
- if (msg["item_id"]) {
2063
- this._lastAssistantItem = msg["item_id"];
2382
+ if (this._playback === null) {
2383
+ this._playback = {
2384
+ itemId: msg["item_id"] || "",
2385
+ startTs: this._latestMediaTs || Date.now(),
2386
+ sentChunks: 0,
2387
+ generating: true,
2388
+ audioRemainder: Buffer.alloc(0)
2389
+ };
2390
+ } else if (msg["item_id"]) {
2391
+ this._playback.itemId = msg["item_id"];
2064
2392
  }
2393
+ const pb = this._playback;
2065
2394
  const ulaw = Buffer.from(msg["delta"], "base64");
2066
2395
  if (this._recorder) {
2067
2396
  this._recorder.writeOutbound(ulawToPcm16(ulaw));
2068
2397
  }
2069
- const combined = Buffer.concat([this._audioRemainder, ulaw]);
2398
+ const combined = Buffer.concat([pb.audioRemainder, ulaw]);
2070
2399
  const chunkSize = 160;
2071
2400
  const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
2072
2401
  for (let off = 0; off < fullEnd; off += chunkSize) {
2073
2402
  if (this._call) {
2074
2403
  this._call.sendAudio(combined.subarray(off, off + chunkSize));
2075
2404
  }
2076
- this._sentAudioChunks++;
2405
+ pb.sentChunks++;
2077
2406
  }
2078
- this._audioRemainder = combined.subarray(fullEnd);
2407
+ pb.audioRemainder = combined.subarray(fullEnd);
2079
2408
  }
2080
2409
  _handleTruncation() {
2081
- if (!this._lastAssistantItem || this._responseStartTs === null) {
2082
- return;
2410
+ for (const [, controller] of this._pendingToolCalls) {
2411
+ controller.abort();
2083
2412
  }
2084
- const audioEndMs = Math.max(0, this._sentAudioChunks * 20);
2085
- this._send({
2086
- type: "conversation.item.truncate",
2087
- item_id: this._lastAssistantItem,
2088
- content_index: 0,
2089
- audio_end_ms: audioEndMs
2090
- });
2413
+ this._pendingToolCalls.clear();
2091
2414
  if (this._call) {
2092
2415
  this._call.clearAudio();
2093
2416
  }
2094
- this._lastAssistantItem = null;
2095
- this._responseStartTs = null;
2096
- this._sentAudioChunks = 0;
2097
- this._audioRemainder = Buffer.alloc(0);
2417
+ const pb = this._playback;
2418
+ if (pb === null) {
2419
+ return;
2420
+ }
2421
+ const playedMs = Math.max(0, (this._latestMediaTs || Date.now()) - pb.startTs);
2422
+ this._log.info(
2423
+ "[Interrupt] item=%s played=%dms total=%dms",
2424
+ pb.itemId,
2425
+ playedMs,
2426
+ pb.sentChunks * 20
2427
+ );
2428
+ this._playback = null;
2098
2429
  }
2099
2430
  async _handleToolCall(item) {
2100
2431
  const funcName = item["name"];
2101
2432
  const callId = item["call_id"];
2102
2433
  this._log.info("Tool call: %s", funcName);
2103
- if (funcName === "hang_up") {
2104
- if (this._call) {
2105
- await this._call.hangup();
2106
- }
2107
- return;
2108
- }
2109
- if (funcName === "collect_dtmf") {
2110
- if (this._call) {
2111
- let result2;
2112
- try {
2113
- const args = JSON.parse(item["arguments"] ?? "{}");
2114
- result2 = await this._call.collectDtmf({
2115
- maxDigits: args["max_digits"] ?? 4,
2116
- finishOnKey: args["finish_on_key"] ?? "#",
2117
- timeout: args["timeout"] ?? 5
2434
+ const controller = new AbortController();
2435
+ this._pendingToolCalls.set(callId, controller);
2436
+ try {
2437
+ if (BUILTIN_TOOL_NAMES.has(funcName) && this._call) {
2438
+ const args = JSON.parse(item["arguments"] ?? "{}");
2439
+ const result2 = await executeBuiltinTool(funcName, args, this._call);
2440
+ if (result2 !== null) {
2441
+ if (funcName === "hang_up") return;
2442
+ if (controller.signal.aborted) return;
2443
+ await this._waitForResponseDone();
2444
+ this._send({
2445
+ type: "conversation.item.create",
2446
+ item: {
2447
+ type: "function_call_output",
2448
+ call_id: callId,
2449
+ output: result2
2450
+ }
2118
2451
  });
2119
- } catch (err) {
2120
- result2 = `Error: ${err}`;
2452
+ this._send({ type: "response.create" });
2453
+ return;
2121
2454
  }
2455
+ }
2456
+ if (!this._tools || !this._tools.has(funcName)) {
2457
+ this._log.error("Unknown tool: %s", funcName);
2122
2458
  await this._waitForResponseDone();
2123
2459
  this._send({
2124
2460
  type: "conversation.item.create",
2125
2461
  item: {
2126
2462
  type: "function_call_output",
2127
2463
  call_id: callId,
2128
- output: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)"
2464
+ output: JSON.stringify({ error: `Unknown tool: ${funcName}` })
2129
2465
  }
2130
2466
  });
2131
2467
  this._send({ type: "response.create" });
2468
+ return;
2132
2469
  }
2133
- return;
2134
- }
2135
- if (funcName === "send_dtmf") {
2136
- if (this._call) {
2137
- let result2;
2138
- try {
2139
- const args = JSON.parse(item["arguments"] ?? "{}");
2140
- await this._call.sendDtmfSequence(args["digits"] ?? "");
2141
- result2 = "sent";
2142
- } catch (err) {
2143
- result2 = `Error: ${err}`;
2144
- }
2145
- await this._waitForResponseDone();
2146
- this._send({
2147
- type: "conversation.item.create",
2148
- item: {
2149
- type: "function_call_output",
2150
- call_id: callId,
2151
- output: result2
2152
- }
2153
- });
2154
- this._send({ type: "response.create" });
2470
+ let result;
2471
+ try {
2472
+ const args = JSON.parse(item["arguments"] ?? "{}");
2473
+ result = await this._tools.call(funcName, args);
2474
+ } catch (err) {
2475
+ this._log.error({ err }, "Tool call failed: %s", funcName);
2476
+ result = `Error: ${err}`;
2155
2477
  }
2156
- return;
2157
- }
2158
- if (!this._tools || !this._tools.has(funcName)) {
2159
- this._log.error("Unknown tool: %s", funcName);
2160
- return;
2161
- }
2162
- let result;
2163
- try {
2164
- const args = JSON.parse(item["arguments"] ?? "{}");
2165
- result = await this._tools.call(funcName, args);
2166
- } catch (err) {
2167
- this._log.error({ err }, "Tool call failed: %s", funcName);
2168
- result = `Error: ${err}`;
2169
- }
2170
- await this._waitForResponseDone();
2171
- this._send({
2172
- type: "conversation.item.create",
2173
- item: {
2174
- type: "function_call_output",
2175
- call_id: callId,
2176
- output: typeof result === "string" ? result : JSON.stringify(result)
2478
+ if (controller.signal.aborted) {
2479
+ this._log.info("Tool call cancelled (user interrupted): %s", funcName);
2480
+ return;
2177
2481
  }
2178
- });
2179
- this._send({ type: "response.create" });
2482
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
2483
+ await this._waitForResponseDone();
2484
+ this._send({
2485
+ type: "conversation.item.create",
2486
+ item: {
2487
+ type: "function_call_output",
2488
+ call_id: callId,
2489
+ output: resultStr
2490
+ }
2491
+ });
2492
+ this._log.info("[ToolResult] %s call_id=%s len=%d", funcName, callId, resultStr.length);
2493
+ this._send({ type: "response.create" });
2494
+ } finally {
2495
+ this._pendingToolCalls.delete(callId);
2496
+ }
2180
2497
  }
2181
2498
  _waitForResponseDone() {
2182
2499
  if (!this._responseInProgress) return Promise.resolve();
@@ -2191,39 +2508,7 @@ var OpenAIRealtime = class {
2191
2508
  }
2192
2509
  };
2193
2510
 
2194
- // src/agent/pipeline/gemini-realtime.ts
2195
- var HANG_UP_TOOL2 = {
2196
- name: "hang_up",
2197
- description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
2198
- parameters: { type: "object", properties: {} }
2199
- };
2200
- var COLLECT_DTMF_TOOL2 = {
2201
- name: "collect_dtmf",
2202
- 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.",
2203
- parameters: {
2204
- type: "object",
2205
- properties: {
2206
- max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
2207
- finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
2208
- timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
2209
- },
2210
- required: ["max_digits"]
2211
- }
2212
- };
2213
- var SEND_DTMF_TOOL2 = {
2214
- name: "send_dtmf",
2215
- 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.",
2216
- parameters: {
2217
- type: "object",
2218
- properties: {
2219
- digits: {
2220
- type: "string",
2221
- description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
2222
- }
2223
- },
2224
- required: ["digits"]
2225
- }
2226
- };
2511
+ // src/agent/pipeline/realtime/gemini-realtime.ts
2227
2512
  function resolveRef(ref, defs) {
2228
2513
  const parts = ref.replace(/^#\//, "").split("/");
2229
2514
  let result = defs;
@@ -2433,9 +2718,7 @@ var GeminiRealtime = class {
2433
2718
  t.function.parameters ?? { type: "object", properties: {} }
2434
2719
  )
2435
2720
  })) : [];
2436
- if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */)) toolDefs.push(HANG_UP_TOOL2);
2437
- if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */)) toolDefs.push(COLLECT_DTMF_TOOL2);
2438
- if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */)) toolDefs.push(SEND_DTMF_TOOL2);
2721
+ toolDefs.push(...getBuiltinToolSchemas(this._builtinTools, "gemini"));
2439
2722
  return toolDefs;
2440
2723
  }
2441
2724
  _handleMessage(msg) {
@@ -2523,51 +2806,17 @@ var GeminiRealtime = class {
2523
2806
  const fcId = fc.id ?? "";
2524
2807
  const args = fc.args ?? {};
2525
2808
  this._log.info({ tool: name, args }, "Tool call: %s", name);
2526
- if (name === "hang_up") {
2527
- this._log.info("hang_up: ending call");
2528
- if (this._call) {
2529
- await this._call.hangup();
2530
- }
2531
- return;
2532
- }
2533
- if (name === "collect_dtmf") {
2534
- if (this._call) {
2535
- let result;
2536
- try {
2537
- this._log.info({ maxDigits: args["max_digits"] ?? 4, timeout: args["timeout"] ?? 5 }, "collect_dtmf: waiting for digits");
2538
- result = await this._call.collectDtmf({
2539
- maxDigits: args["max_digits"] ?? 4,
2540
- finishOnKey: args["finish_on_key"] ?? "#",
2541
- timeout: args["timeout"] ?? 5
2542
- });
2543
- this._log.info("DTMF collected: %s", result || "(empty)");
2544
- } catch (err) {
2545
- this._log.error({ err }, "collect_dtmf error");
2546
- result = `Error: ${err}`;
2547
- }
2548
- responses.push({
2549
- id: fcId,
2550
- name,
2551
- response: { result: result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)" }
2552
- });
2553
- }
2554
- continue;
2555
- }
2556
- if (name === "send_dtmf") {
2557
- if (this._call) {
2558
- let result;
2559
- try {
2560
- this._log.info('send_dtmf: digits="%s"', args["digits"] ?? "");
2561
- await this._call.sendDtmfSequence(args["digits"] ?? "");
2562
- result = "sent";
2563
- this._log.info("send_dtmf: sent");
2564
- } catch (err) {
2565
- this._log.error({ err }, "send_dtmf error");
2566
- result = `Error: ${err}`;
2809
+ if (BUILTIN_TOOL_NAMES.has(name) && this._call) {
2810
+ const result = await executeBuiltinTool(name, args, this._call);
2811
+ if (result !== null) {
2812
+ if (name === "hang_up") {
2813
+ this._log.info("hang_up: ending call");
2814
+ return;
2567
2815
  }
2816
+ this._log.info("Builtin tool result: %s -> %s", name, result);
2568
2817
  responses.push({ id: fcId, name, response: { result } });
2818
+ continue;
2569
2819
  }
2570
- continue;
2571
2820
  }
2572
2821
  if (!this._tools || !this._tools.has(name)) {
2573
2822
  this._log.error("Unknown tool: %s", name);
@@ -2602,306 +2851,7 @@ var GeminiRealtime = class {
2602
2851
  }
2603
2852
  };
2604
2853
 
2605
- // src/agent/pipeline/pipeline-session.ts
2606
- var COLLECT_DTMF_TOOL3 = {
2607
- function: {
2608
- description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4.",
2609
- parameters: {
2610
- properties: {
2611
- max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
2612
- finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
2613
- timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
2614
- },
2615
- required: ["max_digits"]
2616
- }
2617
- }
2618
- };
2619
- var SEND_DTMF_TOOL3 = {
2620
- function: {
2621
- 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.",
2622
- parameters: {
2623
- properties: {
2624
- digits: { type: "string", description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30." }
2625
- },
2626
- required: ["digits"]
2627
- }
2628
- }
2629
- };
2630
- var PipelineSession = class {
2631
- _stt;
2632
- _llm;
2633
- _tts;
2634
- _systemPrompt;
2635
- _greeting;
2636
- _language;
2637
- _temperature;
2638
- _maxTokens;
2639
- _sampleRate;
2640
- _interruptOnSpeech;
2641
- _callSession = null;
2642
- _tools = null;
2643
- _recorder = null;
2644
- _conversation = [];
2645
- _audioBuffer = [];
2646
- _running = false;
2647
- _speaking = false;
2648
- _builtinTools = null;
2649
- _log = NOOP_LOGGER;
2650
- constructor(options) {
2651
- this._stt = options.stt;
2652
- this._llm = options.llm;
2653
- this._tts = options.tts;
2654
- this._systemPrompt = options.systemPrompt;
2655
- this._greeting = options.greeting ?? true;
2656
- this._language = options.language ?? "ko";
2657
- this._temperature = options.temperature;
2658
- this._maxTokens = options.maxTokens;
2659
- this._sampleRate = options.sampleRate ?? 8e3;
2660
- this._interruptOnSpeech = options.interruptOnSpeech ?? true;
2661
- if (options.toolRegistry) this._tools = options.toolRegistry;
2662
- if (options.recorder) this._recorder = options.recorder;
2663
- }
2664
- setToolRegistry(registry) {
2665
- this._tools = registry;
2666
- }
2667
- setRecorder(recorder) {
2668
- this._recorder = recorder;
2669
- }
2670
- setBuiltinTools(tools) {
2671
- this._builtinTools = tools;
2672
- }
2673
- setLogger(logger) {
2674
- this._log = logger;
2675
- if ("setLogger" in this._stt && typeof this._stt.setLogger === "function") {
2676
- this._stt.setLogger(logger);
2677
- }
2678
- if ("setLogger" in this._tts && typeof this._tts.setLogger === "function") {
2679
- this._tts.setLogger(logger);
2680
- }
2681
- }
2682
- async start(callSession, tools) {
2683
- this._callSession = callSession;
2684
- this._tools = tools ?? null;
2685
- this._running = true;
2686
- this._log.info("PipelineSession started");
2687
- this._conversation = [];
2688
- if (this._systemPrompt) {
2689
- this._conversation.push({
2690
- role: "system",
2691
- content: this._systemPrompt
2692
- });
2693
- }
2694
- if (this._greeting) {
2695
- this._generateGreeting().catch((err) => {
2696
- this._log.error({ err }, "Greeting error");
2697
- });
2698
- }
2699
- this._runSttLoop().catch((err) => {
2700
- this._log.error({ err }, "STT loop error");
2701
- });
2702
- }
2703
- feedAudio(audio) {
2704
- if (this._running) {
2705
- this._audioBuffer.push(audio);
2706
- }
2707
- }
2708
- async feedDtmf(digits) {
2709
- this._conversation.push({
2710
- role: "user",
2711
- content: `[DTMF \uC785\uB825: ${digits}]`
2712
- });
2713
- await this._respond();
2714
- }
2715
- async stop() {
2716
- this._running = false;
2717
- this._log.info("PipelineSession stopped");
2718
- this._audioBuffer = [];
2719
- }
2720
- async _runSttLoop() {
2721
- const audioStream = this._createAudioStream();
2722
- for await (const event of this._stt.transcribe(audioStream, {
2723
- sampleRate: this._sampleRate
2724
- })) {
2725
- if (!this._running) break;
2726
- if (event.type === "interim" && this._speaking && this._interruptOnSpeech) {
2727
- this._speaking = false;
2728
- if (this._callSession) {
2729
- this._callSession.clearAudio();
2730
- }
2731
- this._log.info('Barge-in: "%s"', event.transcript.substring(0, 30));
2732
- }
2733
- if (event.type === "final" && event.transcript.trim()) {
2734
- this._log.info("STT: %s", event.transcript);
2735
- await this._handleUserSpeech(event.transcript);
2736
- }
2737
- }
2738
- }
2739
- async *_createAudioStream() {
2740
- while (this._running) {
2741
- if (this._audioBuffer.length > 0) {
2742
- const ulaw = this._audioBuffer.shift();
2743
- const pcm8k = ulawToPcm16(ulaw);
2744
- const pcm16k = resamplePcm16(pcm8k, 8e3, 16e3);
2745
- yield pcm16k;
2746
- } else {
2747
- await new Promise((resolve) => setTimeout(resolve, 20));
2748
- }
2749
- }
2750
- }
2751
- async _generateGreeting() {
2752
- await new Promise((resolve) => setTimeout(resolve, 500));
2753
- await this._respond();
2754
- }
2755
- async _handleUserSpeech(transcript) {
2756
- this._conversation.push({ role: "user", content: transcript });
2757
- await this._respond();
2758
- }
2759
- _buildEffectiveTools() {
2760
- const includeCollectDtmf = !this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */);
2761
- const includeSendDtmf = !this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */);
2762
- if (!includeCollectDtmf && !includeSendDtmf) return this._tools ?? void 0;
2763
- const base = this._tools ? this._tools.fork() : new ToolRegistry();
2764
- if (includeCollectDtmf) {
2765
- base.register({
2766
- name: "collect_dtmf",
2767
- description: COLLECT_DTMF_TOOL3.function.description,
2768
- parameters: COLLECT_DTMF_TOOL3.function.parameters.properties,
2769
- required: COLLECT_DTMF_TOOL3.function.parameters.required,
2770
- handler: async () => ""
2771
- });
2772
- }
2773
- if (includeSendDtmf) {
2774
- base.register({
2775
- name: "send_dtmf",
2776
- description: SEND_DTMF_TOOL3.function.description,
2777
- parameters: SEND_DTMF_TOOL3.function.parameters.properties,
2778
- required: SEND_DTMF_TOOL3.function.parameters.required,
2779
- handler: async () => ""
2780
- });
2781
- }
2782
- return base;
2783
- }
2784
- async _respond() {
2785
- let fullResponse = "";
2786
- const textChunks = [];
2787
- const effectiveTools = this._buildEffectiveTools();
2788
- const llmStream = this._llm.generate(this._conversation, {
2789
- tools: effectiveTools,
2790
- temperature: this._temperature,
2791
- maxTokens: this._maxTokens
2792
- });
2793
- for await (const chunk of llmStream) {
2794
- if (!this._running) break;
2795
- if (chunk.type === "text" && chunk.text) {
2796
- textChunks.push(chunk.text);
2797
- fullResponse += chunk.text;
2798
- } else if (chunk.type === "tool_call" && chunk.toolCall) {
2799
- await this._handleToolCall(chunk);
2800
- }
2801
- }
2802
- if (fullResponse.trim()) {
2803
- this._log.info("Assistant: %s", fullResponse.substring(0, 100));
2804
- this._conversation.push({ role: "assistant", content: fullResponse });
2805
- await this._synthesizeAndSend(fullResponse);
2806
- }
2807
- }
2808
- async _handleToolCall(chunk) {
2809
- if (!chunk.toolCall) return;
2810
- const { id, name, arguments: argsStr } = chunk.toolCall;
2811
- try {
2812
- const args = JSON.parse(argsStr);
2813
- if (name === "collect_dtmf" && this._callSession) {
2814
- let result2;
2815
- try {
2816
- result2 = await this._callSession.collectDtmf({
2817
- maxDigits: args["max_digits"] ?? 4,
2818
- finishOnKey: args["finish_on_key"] ?? "#",
2819
- timeout: args["timeout"] ?? 5
2820
- });
2821
- } catch (err) {
2822
- result2 = `Error: ${err}`;
2823
- }
2824
- this._conversation.push({ role: "tool", content: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)", tool_call_id: id, name });
2825
- await this._respond();
2826
- return;
2827
- }
2828
- if (name === "send_dtmf" && this._callSession) {
2829
- let result2;
2830
- try {
2831
- await this._callSession.sendDtmfSequence(args["digits"] ?? "");
2832
- result2 = "sent";
2833
- } catch (err) {
2834
- result2 = `Error: ${err}`;
2835
- }
2836
- this._conversation.push({ role: "tool", content: result2, tool_call_id: id, name });
2837
- await this._respond();
2838
- return;
2839
- }
2840
- if (!this._tools) return;
2841
- const result = await this._tools.call(name, args);
2842
- this._conversation.push({
2843
- role: "assistant",
2844
- content: ""
2845
- // Tool call info stored in the message flow
2846
- });
2847
- this._conversation.push({
2848
- role: "tool",
2849
- content: typeof result === "string" ? result : JSON.stringify(result),
2850
- tool_call_id: id,
2851
- name
2852
- });
2853
- const effectiveTools = this._buildEffectiveTools();
2854
- let followUpText = "";
2855
- const followUpStream = this._llm.generate(this._conversation, {
2856
- tools: effectiveTools,
2857
- temperature: this._temperature,
2858
- maxTokens: this._maxTokens
2859
- });
2860
- for await (const followChunk of followUpStream) {
2861
- if (!this._running) break;
2862
- if (followChunk.type === "text" && followChunk.text) {
2863
- followUpText += followChunk.text;
2864
- }
2865
- }
2866
- if (followUpText.trim()) {
2867
- this._conversation.push({ role: "assistant", content: followUpText });
2868
- await this._synthesizeAndSend(followUpText);
2869
- }
2870
- } catch (err) {
2871
- this._log.error({ err }, "Tool call failed: %s", name);
2872
- }
2873
- }
2874
- async _synthesizeAndSend(text) {
2875
- if (!this._callSession || !this._running) return;
2876
- this._speaking = true;
2877
- try {
2878
- for await (const audioChunk of this._tts.synthesize(text, {
2879
- sampleRate: this._sampleRate
2880
- })) {
2881
- if (!this._running || !this._speaking) break;
2882
- if (this._recorder) {
2883
- const pcm8k2 = this._sampleRate !== 8e3 ? resamplePcm16(audioChunk, this._sampleRate, 8e3) : audioChunk;
2884
- this._recorder.writeOutbound(pcm8k2);
2885
- }
2886
- const pcm8k = resamplePcm16(audioChunk, this._sampleRate, 8e3);
2887
- const ulaw = pcm16ToUlaw(pcm8k);
2888
- for (let off = 0; off < ulaw.length; off += 160) {
2889
- let chunk = ulaw.subarray(off, off + 160);
2890
- if (chunk.length < 160) {
2891
- chunk = Buffer.concat([chunk, Buffer.alloc(160 - chunk.length, 255)]);
2892
- }
2893
- this._callSession.sendAudio(chunk);
2894
- }
2895
- }
2896
- } catch (err) {
2897
- this._log.error({ err }, "TTS error");
2898
- } finally {
2899
- this._speaking = false;
2900
- }
2901
- }
2902
- };
2903
-
2904
- // src/agent/pipeline/deepgram-stt.ts
2854
+ // src/agent/pipeline/stt/deepgram-stt.ts
2905
2855
  var DeepgramSTT = class {
2906
2856
  _options;
2907
2857
  _log = NOOP_LOGGER;
@@ -3022,7 +2972,7 @@ var DeepgramSTT = class {
3022
2972
  }
3023
2973
  };
3024
2974
 
3025
- // src/agent/pipeline/elevenlabs-tts.ts
2975
+ // src/agent/pipeline/tts/elevenlabs-tts.ts
3026
2976
  var ElevenLabsTTS = class {
3027
2977
  _options;
3028
2978
  _log = NOOP_LOGGER;
@@ -3175,7 +3125,7 @@ var ElevenLabsTTS = class {
3175
3125
  }
3176
3126
  };
3177
3127
 
3178
- // src/agent/pipeline/openai-llm.ts
3128
+ // src/agent/pipeline/llm/openai-llm.ts
3179
3129
  var OpenAILLM = class {
3180
3130
  _options;
3181
3131
  constructor(options = {}) {
@@ -3261,7 +3211,7 @@ var OpenAILLM = class {
3261
3211
  }
3262
3212
  };
3263
3213
 
3264
- // src/agent/pipeline/anthropic-llm.ts
3214
+ // src/agent/pipeline/llm/anthropic-llm.ts
3265
3215
  var AnthropicLLM = class {
3266
3216
  _options;
3267
3217
  constructor(options = {}) {
@@ -3366,7 +3316,7 @@ var AnthropicLLM = class {
3366
3316
  }
3367
3317
  };
3368
3318
 
3369
- // src/agent/pipeline/gemini-llm.ts
3319
+ // src/agent/pipeline/llm/gemini-llm.ts
3370
3320
  var GeminiLLM = class {
3371
3321
  _options;
3372
3322
  constructor(options = {}) {
@@ -3450,7 +3400,7 @@ var GeminiLLM = class {
3450
3400
  }
3451
3401
  };
3452
3402
 
3453
- // src/agent/pipeline/openai-compat-llm.ts
3403
+ // src/agent/pipeline/llm/openai-compat-llm.ts
3454
3404
  var OpenAICompatLLM = class {
3455
3405
  _options;
3456
3406
  constructor(options) {
@@ -3533,7 +3483,7 @@ var OpenAICompatLLM = class {
3533
3483
  }
3534
3484
  };
3535
3485
 
3536
- // src/agent/pipeline/ollama-llm.ts
3486
+ // src/agent/pipeline/llm/ollama-llm.ts
3537
3487
  var OllamaLLM = class {
3538
3488
  _inner;
3539
3489
  constructor(options = {}) {
@@ -3551,7 +3501,7 @@ var OllamaLLM = class {
3551
3501
  }
3552
3502
  };
3553
3503
 
3554
- // src/agent/pipeline/mistral-llm.ts
3504
+ // src/agent/pipeline/llm/mistral-llm.ts
3555
3505
  var MistralLLM = class {
3556
3506
  _inner;
3557
3507
  constructor(options = {}) {
@@ -3568,7 +3518,7 @@ var MistralLLM = class {
3568
3518
  }
3569
3519
  };
3570
3520
 
3571
- // src/agent/pipeline/groq-llm.ts
3521
+ // src/agent/pipeline/llm/groq-llm.ts
3572
3522
  var GroqLLM = class {
3573
3523
  _inner;
3574
3524
  constructor(options = {}) {
@@ -3585,7 +3535,7 @@ var GroqLLM = class {
3585
3535
  }
3586
3536
  };
3587
3537
 
3588
- // src/agent/pipeline/perplexity-llm.ts
3538
+ // src/agent/pipeline/llm/perplexity-llm.ts
3589
3539
  var PerplexityLLM = class {
3590
3540
  _inner;
3591
3541
  constructor(options = {}) {
@@ -3602,7 +3552,7 @@ var PerplexityLLM = class {
3602
3552
  }
3603
3553
  };
3604
3554
 
3605
- // src/agent/pipeline/together-llm.ts
3555
+ // src/agent/pipeline/llm/together-llm.ts
3606
3556
  var TogetherLLM = class {
3607
3557
  _inner;
3608
3558
  constructor(options = {}) {
@@ -3619,7 +3569,7 @@ var TogetherLLM = class {
3619
3569
  }
3620
3570
  };
3621
3571
 
3622
- // src/agent/pipeline/fireworks-llm.ts
3572
+ // src/agent/pipeline/llm/fireworks-llm.ts
3623
3573
  var FireworksLLM = class {
3624
3574
  _inner;
3625
3575
  constructor(options = {}) {
@@ -3636,7 +3586,7 @@ var FireworksLLM = class {
3636
3586
  }
3637
3587
  };
3638
3588
 
3639
- // src/agent/pipeline/deepseek-llm.ts
3589
+ // src/agent/pipeline/llm/deepseek-llm.ts
3640
3590
  var DeepSeekLLM = class {
3641
3591
  _inner;
3642
3592
  constructor(options = {}) {
@@ -3653,7 +3603,7 @@ var DeepSeekLLM = class {
3653
3603
  }
3654
3604
  };
3655
3605
 
3656
- // src/agent/pipeline/xai-llm.ts
3606
+ // src/agent/pipeline/llm/xai-llm.ts
3657
3607
  var XaiLLM = class {
3658
3608
  _inner;
3659
3609
  constructor(options = {}) {
@@ -3691,6 +3641,7 @@ function mcpServerHTTP(options) {
3691
3641
 
3692
3642
  exports.AnthropicLLM = AnthropicLLM;
3693
3643
  exports.AudioRecorder = AudioRecorder;
3644
+ exports.BUILTIN_TOOL_NAMES = BUILTIN_TOOL_NAMES;
3694
3645
  exports.BuiltinTool = BuiltinTool;
3695
3646
  exports.CallSession = CallSession;
3696
3647
  exports.ClawOpsAgent = ClawOpsAgent;
@@ -3715,8 +3666,11 @@ exports.ToolRegistry = ToolRegistry;
3715
3666
  exports.XaiLLM = XaiLLM;
3716
3667
  exports.createAgentLogger = createAgentLogger;
3717
3668
  exports.createPipelineLogger = createPipelineLogger;
3669
+ exports.executeBuiltinTool = executeBuiltinTool;
3718
3670
  exports.functionTool = functionTool;
3671
+ exports.getBuiltinToolSchemas = getBuiltinToolSchemas;
3719
3672
  exports.getTracingConfig = getTracingConfig;
3673
+ exports.isBuiltinTool = isBuiltinTool;
3720
3674
  exports.mcpServerHTTP = mcpServerHTTP;
3721
3675
  exports.mcpServerStdio = mcpServerStdio;
3722
3676
  exports.pcm16ToUlaw = pcm16ToUlaw;