@teamlearners/clawops 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { F as FunctionTool, S as Session, B as BuiltinTool, C as CallSession, a as STT, L as LLM, T as TTS, b as ToolRegistry, c as SessionTelemetry, d as SpeechEvent, e as ConversationMessage, f as LLMChunk } from '../base-Z-uZdd27.cjs';
2
- export { g as CallDirection, h as CallStatus, O as OpenAIToolDefinition, i as ToolCallRequest, j as functionTool, z as zodToToolParams } from '../base-Z-uZdd27.cjs';
1
+ import { F as FunctionTool, S as Session, B as BuiltinTool, C as CallSession, a as STT, L as LLM, T as TTS, b as ToolRegistry, c as SessionTelemetry, d as SpeechEvent, e as ConversationMessage, f as LLMChunk } from '../base-Cje1TDJi.cjs';
2
+ export { g as CallDirection, h as CallStatus, O as OpenAIToolDefinition, i as ToolCallRequest, j as functionTool, z as zodToToolParams } from '../base-Cje1TDJi.cjs';
3
3
  import { Logger } from 'pino';
4
4
  export { Logger } from 'pino';
5
5
  import 'zod';
@@ -242,6 +242,14 @@ declare class ClawOpsAgent {
242
242
  * multiple times — only the first invocation starts the task. Failures are
243
243
  * recorded in _prewarmFailed so the call-session path can fall back to start().
244
244
  */
245
+ /**
246
+ * 세션에 콜별 의존성(도구·내장도구·hold audio·recorder·logger)을 주입한다.
247
+ *
248
+ * prewarm 과 _startCallSession 양쪽에서 부른다. prewarm 은 세션이 LLM 에 보낼
249
+ * tool 스키마를 그 시점에 확정하므로(OpenAI session.update / Gemini live connect
250
+ * config), prewarm **전에** 최소 한 번은 도구가 들어가 있어야 한다.
251
+ */
252
+ private _injectSessionDeps;
245
253
  private _startPrewarm;
246
254
  private _handleRinging;
247
255
  private _handleFailed;
@@ -533,6 +541,11 @@ declare class OpenAIRealtime implements Session {
533
541
  private _greeting;
534
542
  private _log;
535
543
  private _builtinTools;
544
+ /**
545
+ * 마지막 session.update 로 LLM 에 알린 tool 이름들. attach() 가 이 값과 현재
546
+ * registry 를 비교해 달라졌을 때만(=MCP 도구 등 뒤늦게 붙은 경우) 재전송한다.
547
+ */
548
+ private _sentToolNames;
536
549
  setLogger(logger: Logger): void;
537
550
  setBuiltinTools(tools: Set<BuiltinTool>): void;
538
551
  getTelemetry(): SessionTelemetry;
@@ -562,6 +575,15 @@ declare class OpenAIRealtime implements Session {
562
575
  feedDtmf(digits: string): Promise<void>;
563
576
  feedAudio(audio: Buffer, timestamp?: number): void;
564
577
  stop(): Promise<void>;
578
+ /** Current tool schemas: user tools + builtin tools (flat realtime format). */
579
+ private _currentToolSchemas;
580
+ /**
581
+ * prewarm 이후 도구가 바뀌었으면 session.update 로 재전송한다.
582
+ *
583
+ * MCP 도구는 통화 시작 시점에 registry 에 붙으므로 prewarm 시점의 스키마에는
584
+ * 없다. 여기서 맞춰주지 않으면 발신 통화에서 MCP 도구를 영영 못 쓴다.
585
+ */
586
+ private _resyncTools;
565
587
  private _sendSessionUpdate;
566
588
  private _handleMessage;
567
589
  private _handleAudioDelta;
@@ -595,6 +617,13 @@ interface GeminiRealtimeOptions {
595
617
  realtimeInputConfig?: Record<string, unknown>;
596
618
  }
597
619
  declare class GeminiRealtime implements Session {
620
+ /**
621
+ * Gemini Live 는 connect 시점의 config 로 도구가 고정되어 세션 도중 변경할 수 없다.
622
+ *
623
+ * 통화 시작 시점에야 붙는 MCP 도구는 prewarm 된 세션에 넣을 방법이 없으므로,
624
+ * `ClawOpsAgent` 가 MCP 서버 설정과 이 플래그를 보고 prewarm 을 건너뛴다.
625
+ */
626
+ readonly toolsFrozenAfterPrewarm = true;
598
627
  private _apiKey;
599
628
  private _systemPrompt;
600
629
  private _model;
@@ -1,5 +1,5 @@
1
- import { F as FunctionTool, S as Session, B as BuiltinTool, C as CallSession, a as STT, L as LLM, T as TTS, b as ToolRegistry, c as SessionTelemetry, d as SpeechEvent, e as ConversationMessage, f as LLMChunk } from '../base-Z-uZdd27.js';
2
- export { g as CallDirection, h as CallStatus, O as OpenAIToolDefinition, i as ToolCallRequest, j as functionTool, z as zodToToolParams } from '../base-Z-uZdd27.js';
1
+ import { F as FunctionTool, S as Session, B as BuiltinTool, C as CallSession, a as STT, L as LLM, T as TTS, b as ToolRegistry, c as SessionTelemetry, d as SpeechEvent, e as ConversationMessage, f as LLMChunk } from '../base-Cje1TDJi.js';
2
+ export { g as CallDirection, h as CallStatus, O as OpenAIToolDefinition, i as ToolCallRequest, j as functionTool, z as zodToToolParams } from '../base-Cje1TDJi.js';
3
3
  import { Logger } from 'pino';
4
4
  export { Logger } from 'pino';
5
5
  import 'zod';
@@ -242,6 +242,14 @@ declare class ClawOpsAgent {
242
242
  * multiple times — only the first invocation starts the task. Failures are
243
243
  * recorded in _prewarmFailed so the call-session path can fall back to start().
244
244
  */
245
+ /**
246
+ * 세션에 콜별 의존성(도구·내장도구·hold audio·recorder·logger)을 주입한다.
247
+ *
248
+ * prewarm 과 _startCallSession 양쪽에서 부른다. prewarm 은 세션이 LLM 에 보낼
249
+ * tool 스키마를 그 시점에 확정하므로(OpenAI session.update / Gemini live connect
250
+ * config), prewarm **전에** 최소 한 번은 도구가 들어가 있어야 한다.
251
+ */
252
+ private _injectSessionDeps;
245
253
  private _startPrewarm;
246
254
  private _handleRinging;
247
255
  private _handleFailed;
@@ -533,6 +541,11 @@ declare class OpenAIRealtime implements Session {
533
541
  private _greeting;
534
542
  private _log;
535
543
  private _builtinTools;
544
+ /**
545
+ * 마지막 session.update 로 LLM 에 알린 tool 이름들. attach() 가 이 값과 현재
546
+ * registry 를 비교해 달라졌을 때만(=MCP 도구 등 뒤늦게 붙은 경우) 재전송한다.
547
+ */
548
+ private _sentToolNames;
536
549
  setLogger(logger: Logger): void;
537
550
  setBuiltinTools(tools: Set<BuiltinTool>): void;
538
551
  getTelemetry(): SessionTelemetry;
@@ -562,6 +575,15 @@ declare class OpenAIRealtime implements Session {
562
575
  feedDtmf(digits: string): Promise<void>;
563
576
  feedAudio(audio: Buffer, timestamp?: number): void;
564
577
  stop(): Promise<void>;
578
+ /** Current tool schemas: user tools + builtin tools (flat realtime format). */
579
+ private _currentToolSchemas;
580
+ /**
581
+ * prewarm 이후 도구가 바뀌었으면 session.update 로 재전송한다.
582
+ *
583
+ * MCP 도구는 통화 시작 시점에 registry 에 붙으므로 prewarm 시점의 스키마에는
584
+ * 없다. 여기서 맞춰주지 않으면 발신 통화에서 MCP 도구를 영영 못 쓴다.
585
+ */
586
+ private _resyncTools;
565
587
  private _sendSessionUpdate;
566
588
  private _handleMessage;
567
589
  private _handleAudioDelta;
@@ -595,6 +617,13 @@ interface GeminiRealtimeOptions {
595
617
  realtimeInputConfig?: Record<string, unknown>;
596
618
  }
597
619
  declare class GeminiRealtime implements Session {
620
+ /**
621
+ * Gemini Live 는 connect 시점의 config 로 도구가 고정되어 세션 도중 변경할 수 없다.
622
+ *
623
+ * 통화 시작 시점에야 붙는 MCP 도구는 prewarm 된 세션에 넣을 방법이 없으므로,
624
+ * `ClawOpsAgent` 가 MCP 서버 설정과 이 플래그를 보고 prewarm 을 건너뛴다.
625
+ */
626
+ readonly toolsFrozenAfterPrewarm = true;
598
627
  private _apiKey;
599
628
  private _systemPrompt;
600
629
  private _model;
@@ -1,6 +1,6 @@
1
- import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-7P2HGIPS.js';
2
- import { NOOP_LOGGER, resolveBuiltinTools, createAgentLogger, createPipelineLogger, BufferingCall, attachBuffered, ulawToPcm16, resamplePcm16, getBuiltinToolSchemas, BUILTIN_TOOL_NAMES, executeBuiltinTool, pcm16ToUlaw, applyUlawGain } from '../chunk-2BAYUQ7O.js';
3
- export { BUILTIN_TOOL_NAMES, BuiltinTool, DECODE_TABLE, createAgentLogger, createPipelineLogger, executeBuiltinTool, getBuiltinToolSchemas, isBuiltinTool, pcm16ToUlaw, resamplePcm16, ulawToPcm16 } from '../chunk-2BAYUQ7O.js';
1
+ import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-2QE6GHWC.js';
2
+ import { NOOP_LOGGER, resolveBuiltinTools, createAgentLogger, createPipelineLogger, BufferingCall, attachBuffered, ulawToPcm16, resamplePcm16, getBuiltinToolSchemas, BUILTIN_TOOL_NAMES, executeBuiltinTool, CALL_NOT_READY_RESULT, pcm16ToUlaw, applyUlawGain } from '../chunk-WKGTY7QT.js';
3
+ export { BUILTIN_TOOL_NAMES, BuiltinTool, DECODE_TABLE, createAgentLogger, createPipelineLogger, executeBuiltinTool, getBuiltinToolSchemas, isBuiltinTool, pcm16ToUlaw, resamplePcm16, ulawToPcm16 } from '../chunk-WKGTY7QT.js';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import os from 'os';
@@ -739,6 +739,7 @@ var CallSession = class {
739
739
  startTime;
740
740
  metadata;
741
741
  _status;
742
+ _endedStatus = null;
742
743
  _sendAudioFn = null;
743
744
  _clearAudioFn = null;
744
745
  _hangupFn = null;
@@ -787,6 +788,17 @@ var CallSession = class {
787
788
  get status() {
788
789
  return this._status;
789
790
  }
791
+ /**
792
+ * 서버가 통보한 최종 종료 상태. 통화가 끝나기 전에는 null.
793
+ *
794
+ * `completed`(성사) / `no-answer`(벨은 울렸으나 무응답) / `busy`(통화중) /
795
+ * `rejected`(수신 거절) / `canceled`(응답 전 발신 측 취소) / `failed`(시스템·망 오류).
796
+ * `status` 는 SDK 내부 수명주기(ringing→active→ended)이므로 통화 성사 여부는
797
+ * 이 값이나 `call_failed` 이벤트로 판단한다.
798
+ */
799
+ get endedStatus() {
800
+ return this._endedStatus;
801
+ }
790
802
  get duration() {
791
803
  return (Date.now() - this.startTime.getTime()) / 1e3;
792
804
  }
@@ -947,8 +959,20 @@ var CallSession = class {
947
959
  async wait() {
948
960
  return this._endedPromise;
949
961
  }
950
- /** Mark the session as ended (called internally). */
951
- _markEnded() {
962
+ /**
963
+ * Mark the session as ended (called internally).
964
+ *
965
+ * @param status 서버가 통보한 최종 상태(completed/no-answer/busy/rejected/canceled/
966
+ * failed). 주어지면 `endedStatus` 로 확정한다 — 상대가 받지 않은 통화를 성사된 통화와
967
+ * 구분하기 위함이다. 생략하면 미디어 세션 정리 경로에서의 호출이므로, 아직 종료 전일
968
+ * 때만 'completed' 로 채우고 이미 확정된 서버 상태는 덮어쓰지 않는다.
969
+ */
970
+ _markEnded(status) {
971
+ if (status !== void 0) {
972
+ this._endedStatus = status;
973
+ } else if (this._status !== "ended") {
974
+ this._endedStatus = "completed";
975
+ }
952
976
  this._status = "ended";
953
977
  this._resolveEnded();
954
978
  }
@@ -1600,10 +1624,15 @@ var ClawOpsAgent = class _ClawOpsAgent {
1600
1624
  }
1601
1625
  _handleEnded(event) {
1602
1626
  const callId = event["callId"];
1627
+ const status = event["status"] || "completed";
1603
1628
  const session = this._activeSessions.get(callId);
1604
1629
  if (session) {
1605
- this._log.info("Call ended (server): %s", callId);
1606
- session._markEnded();
1630
+ this._log.info("Call ended (server): %s (status=%s)", callId, status);
1631
+ if (status !== "completed") {
1632
+ this._log.info("Outbound call not connected: %s (%s)", callId, status);
1633
+ session._emit("call_failed", status);
1634
+ }
1635
+ session._markEnded(status);
1607
1636
  this._activeSessions.delete(callId);
1608
1637
  }
1609
1638
  void this._cleanupPrewarm(callId);
@@ -1668,10 +1697,43 @@ var ClawOpsAgent = class _ClawOpsAgent {
1668
1697
  * multiple times — only the first invocation starts the task. Failures are
1669
1698
  * recorded in _prewarmFailed so the call-session path can fall back to start().
1670
1699
  */
1700
+ /**
1701
+ * 세션에 콜별 의존성(도구·내장도구·hold audio·recorder·logger)을 주입한다.
1702
+ *
1703
+ * prewarm 과 _startCallSession 양쪽에서 부른다. prewarm 은 세션이 LLM 에 보낼
1704
+ * tool 스키마를 그 시점에 확정하므로(OpenAI session.update / Gemini live connect
1705
+ * config), prewarm **전에** 최소 한 번은 도구가 들어가 있어야 한다.
1706
+ */
1707
+ _injectSessionDeps(tools, recorder) {
1708
+ const sessionHandler = this._session;
1709
+ if ("setToolRegistry" in sessionHandler && typeof sessionHandler.setToolRegistry === "function") {
1710
+ sessionHandler.setToolRegistry(tools);
1711
+ }
1712
+ if (recorder && "setRecorder" in sessionHandler && typeof sessionHandler.setRecorder === "function") {
1713
+ sessionHandler.setRecorder(recorder);
1714
+ }
1715
+ if ("setBuiltinTools" in sessionHandler && typeof sessionHandler.setBuiltinTools === "function") {
1716
+ sessionHandler.setBuiltinTools(this._builtinTools);
1717
+ }
1718
+ if ("setLogger" in sessionHandler && typeof sessionHandler.setLogger === "function") {
1719
+ sessionHandler.setLogger(this._isPipelineSession ? this._pipelineLog : this._log);
1720
+ }
1721
+ if (this._holdAudioChunks && "setHoldAudio" in sessionHandler && typeof sessionHandler.setHoldAudio === "function") {
1722
+ sessionHandler.setHoldAudio(this._holdAudioChunks);
1723
+ }
1724
+ }
1671
1725
  _startPrewarm(callId) {
1672
1726
  if (this._prewarmTasks.has(callId)) return;
1673
1727
  const sessionHandler = this._session;
1674
1728
  if (typeof sessionHandler.prewarm !== "function") return;
1729
+ if (this._mcpServers.length > 0 && sessionHandler.toolsFrozenAfterPrewarm) {
1730
+ this._log.info(
1731
+ { callId },
1732
+ "Skipping prewarm \u2014 \uC138\uC158\uC774 \uC5F0\uACB0 \uD6C4 \uB3C4\uAD6C \uBCC0\uACBD\uC744 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC544 MCP \uB3C4\uAD6C\uAC00 \uB204\uB77D\uB41C\uB2E4."
1733
+ );
1734
+ return;
1735
+ }
1736
+ this._injectSessionDeps(this._tools.fork());
1675
1737
  const PREWARM_TIMEOUT_MS = 1e4;
1676
1738
  const t0 = Date.now();
1677
1739
  this._log.info(`[PREWARM-T] start call_id=${callId} t=${(t0 / 1e3).toFixed(3)}`);
@@ -1715,9 +1777,10 @@ var ClawOpsAgent = class _ClawOpsAgent {
1715
1777
  const callId = event["callId"];
1716
1778
  const session = this._activeSessions.get(callId);
1717
1779
  if (session) {
1718
- this._log.info("Outbound call failed: %s (%s)", callId, event["reason"] ?? "failed");
1719
- session._emit("call_failed", event["reason"] ?? "failed");
1720
- session._markEnded();
1780
+ const reason = event["reason"] ?? "failed";
1781
+ this._log.info("Outbound call failed: %s (%s)", callId, reason);
1782
+ session._emit("call_failed", reason);
1783
+ session._markEnded(reason);
1721
1784
  this._activeSessions.delete(callId);
1722
1785
  }
1723
1786
  void this._cleanupPrewarm(callId);
@@ -1836,21 +1899,7 @@ var ClawOpsAgent = class _ClawOpsAgent {
1836
1899
  session._waitForMark = (name, timeoutMs) => mediaWs.waitForMark(name, timeoutMs);
1837
1900
  session._flushTransport = () => mediaWs.flush();
1838
1901
  const sessionHandler = this._session;
1839
- if ("setToolRegistry" in sessionHandler && typeof sessionHandler.setToolRegistry === "function") {
1840
- sessionHandler.setToolRegistry(sessionTools);
1841
- }
1842
- if (recorder && "setRecorder" in sessionHandler && typeof sessionHandler.setRecorder === "function") {
1843
- sessionHandler.setRecorder(recorder);
1844
- }
1845
- if ("setBuiltinTools" in sessionHandler && typeof sessionHandler.setBuiltinTools === "function") {
1846
- sessionHandler.setBuiltinTools(this._builtinTools);
1847
- }
1848
- if ("setLogger" in sessionHandler && typeof sessionHandler.setLogger === "function") {
1849
- sessionHandler.setLogger(this._isPipelineSession ? this._pipelineLog : this._log);
1850
- }
1851
- if (this._holdAudioChunks && "setHoldAudio" in sessionHandler && typeof sessionHandler.setHoldAudio === "function") {
1852
- sessionHandler.setHoldAudio(this._holdAudioChunks);
1853
- }
1902
+ this._injectSessionDeps(sessionTools, recorder);
1854
1903
  this._callSessions.set(session.callId, sessionHandler);
1855
1904
  mediaWs.onAudio((ulawAudio, timestamp) => {
1856
1905
  latestMediaTs = timestamp;
@@ -2178,6 +2227,17 @@ var PipelineSession = class {
2178
2227
  return;
2179
2228
  }
2180
2229
  }
2230
+ if (BUILTIN_TOOL_NAMES.has(name) && this._callSession instanceof BufferingCall) {
2231
+ this._log.warn("Builtin tool %s called before answer \u2014 deferring", name);
2232
+ this._conversation.push({
2233
+ role: "tool",
2234
+ content: CALL_NOT_READY_RESULT,
2235
+ tool_call_id: id,
2236
+ name
2237
+ });
2238
+ await this._respond();
2239
+ return;
2240
+ }
2181
2241
  if (!this._tools) return;
2182
2242
  this._callSession?.recordToolCall();
2183
2243
  const player = this._holdAudioChunks && this._callSession && !(this._callSession instanceof BufferingCall) ? new HoldAudioPlayer(this._callSession, this._holdAudioChunks) : null;
@@ -2261,6 +2321,11 @@ var OpenAIRealtime = class {
2261
2321
  _greeting;
2262
2322
  _log = NOOP_LOGGER;
2263
2323
  _builtinTools = null;
2324
+ /**
2325
+ * 마지막 session.update 로 LLM 에 알린 tool 이름들. attach() 가 이 값과 현재
2326
+ * registry 를 비교해 달라졌을 때만(=MCP 도구 등 뒤늦게 붙은 경우) 재전송한다.
2327
+ */
2328
+ _sentToolNames = null;
2264
2329
  setLogger(logger) {
2265
2330
  this._log = logger;
2266
2331
  }
@@ -2370,6 +2435,7 @@ var OpenAIRealtime = class {
2370
2435
  }
2371
2436
  /** Attach a real CallSession to the prewarmed session and flush buffered audio. */
2372
2437
  async attach(callSession) {
2438
+ this._resyncTools();
2373
2439
  const prev = this._call;
2374
2440
  this._call = callSession;
2375
2441
  attachBuffered(prev, callSession);
@@ -2430,10 +2496,36 @@ var OpenAIRealtime = class {
2430
2496
  });
2431
2497
  }
2432
2498
  }
2433
- _sendSessionUpdate() {
2434
- if (!this._ws || this._ws.readyState !== 1) return;
2499
+ /** Current tool schemas: user tools + builtin tools (flat realtime format). */
2500
+ _currentToolSchemas() {
2435
2501
  const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
2436
2502
  toolSchemas.push(...getBuiltinToolSchemas(this._builtinTools, "realtime"));
2503
+ return toolSchemas;
2504
+ }
2505
+ /**
2506
+ * prewarm 이후 도구가 바뀌었으면 session.update 로 재전송한다.
2507
+ *
2508
+ * MCP 도구는 통화 시작 시점에 registry 에 붙으므로 prewarm 시점의 스키마에는
2509
+ * 없다. 여기서 맞춰주지 않으면 발신 통화에서 MCP 도구를 영영 못 쓴다.
2510
+ */
2511
+ _resyncTools() {
2512
+ if (!this._ws || this._ws.readyState !== 1) return;
2513
+ const toolSchemas = this._currentToolSchemas();
2514
+ const names = toolSchemas.map((t) => String(t["name"] ?? ""));
2515
+ if (this._sentToolNames && names.length === this._sentToolNames.length && names.every((n, i) => n === this._sentToolNames[i])) {
2516
+ return;
2517
+ }
2518
+ this._send({
2519
+ type: "session.update",
2520
+ session: { type: "realtime", tools: toolSchemas }
2521
+ });
2522
+ this._sentToolNames = names;
2523
+ this._log.info("Tool schema resynced after prewarm: %s", names.join(", "));
2524
+ }
2525
+ _sendSessionUpdate() {
2526
+ if (!this._ws || this._ws.readyState !== 1) return;
2527
+ const toolSchemas = this._currentToolSchemas();
2528
+ this._sentToolNames = toolSchemas.map((t) => String(t["name"] ?? ""));
2437
2529
  this._send({
2438
2530
  type: "session.update",
2439
2531
  session: {
@@ -2597,6 +2689,20 @@ var OpenAIRealtime = class {
2597
2689
  return;
2598
2690
  }
2599
2691
  }
2692
+ if (BUILTIN_TOOL_NAMES.has(funcName) && this._call instanceof BufferingCall) {
2693
+ this._log.warn("Builtin tool %s called before answer \u2014 deferring", funcName);
2694
+ await this._waitForResponseDone();
2695
+ this._send({
2696
+ type: "conversation.item.create",
2697
+ item: {
2698
+ type: "function_call_output",
2699
+ call_id: callId,
2700
+ output: CALL_NOT_READY_RESULT
2701
+ }
2702
+ });
2703
+ this._send({ type: "response.create" });
2704
+ return;
2705
+ }
2600
2706
  if (!this._tools || !this._tools.has(funcName)) {
2601
2707
  this._log.error("Unknown tool: %s", funcName);
2602
2708
  await this._waitForResponseDone();
@@ -2734,6 +2840,13 @@ function sanitizeSchemaForGemini(schema, defs, depth = 0) {
2734
2840
  return result;
2735
2841
  }
2736
2842
  var GeminiRealtime = class _GeminiRealtime {
2843
+ /**
2844
+ * Gemini Live 는 connect 시점의 config 로 도구가 고정되어 세션 도중 변경할 수 없다.
2845
+ *
2846
+ * 통화 시작 시점에야 붙는 MCP 도구는 prewarm 된 세션에 넣을 방법이 없으므로,
2847
+ * `ClawOpsAgent` 가 MCP 서버 설정과 이 플래그를 보고 prewarm 을 건너뛴다.
2848
+ */
2849
+ toolsFrozenAfterPrewarm = true;
2737
2850
  _apiKey;
2738
2851
  _systemPrompt;
2739
2852
  _model;
@@ -3039,6 +3152,11 @@ var GeminiRealtime = class _GeminiRealtime {
3039
3152
  continue;
3040
3153
  }
3041
3154
  }
3155
+ if (BUILTIN_TOOL_NAMES.has(name) && this._call instanceof BufferingCall) {
3156
+ this._log.warn("Builtin tool %s called before answer \u2014 deferring", name);
3157
+ responses.push({ id: fcId, name, response: { result: CALL_NOT_READY_RESULT } });
3158
+ continue;
3159
+ }
3042
3160
  if (!this._tools || !this._tools.has(name)) {
3043
3161
  this._log.error("Unknown tool: %s", name);
3044
3162
  responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });