@ouro.bot/cli 0.1.0-alpha.810 → 0.1.0-alpha.811

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.
Files changed (38) hide show
  1. package/changelog.json +12 -0
  2. package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
  3. package/deploy/unraid/sanctuary-unit16-host-broker.mjs +8 -7
  4. package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
  5. package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +1 -1
  6. package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
  7. package/deploy/unraid/sanctuary.xml +1 -1
  8. package/dist/heart/config.js +32 -2
  9. package/dist/heart/core.js +167 -115
  10. package/dist/heart/daemon/daemon.js +19 -9
  11. package/dist/heart/daemon/sanctuary-acceptance-adapter.js +88 -50
  12. package/dist/heart/daemon/sanctuary-acceptance-harness.js +53 -15
  13. package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +6 -26
  14. package/dist/heart/frontend-approval-runtime.js +87 -13
  15. package/dist/heart/identity.js +17 -5
  16. package/dist/heart/session-events.js +8 -1
  17. package/dist/heart/tool-approval.js +64 -31
  18. package/dist/mind/pending.js +4 -4
  19. package/dist/repertoire/mcp-manager.js +365 -381
  20. package/dist/repertoire/mcp-tools.js +66 -25
  21. package/dist/repertoire/plugin-mcp.js +3 -3
  22. package/dist/repertoire/shell-sessions.js +8 -7
  23. package/dist/repertoire/tool-arguments.js +30 -8
  24. package/dist/repertoire/tools-session.js +30 -5
  25. package/dist/repertoire/tools-shell.js +30 -14
  26. package/dist/repertoire/tools-voice.js +6 -6
  27. package/dist/repertoire/tools.js +231 -205
  28. package/dist/senses/bluebubbles/index.js +4 -2
  29. package/dist/senses/cli.js +6 -5
  30. package/dist/senses/private-runtime.js +9 -3
  31. package/dist/senses/shared-turn.js +12 -9
  32. package/dist/senses/teams.js +15 -12
  33. package/dist/senses/telegram-approval-runtime.js +129 -20
  34. package/dist/senses/telegram-client.js +19 -2
  35. package/dist/senses/telegram.js +54 -15
  36. package/dist/senses/voice/twilio-phone.js +123 -173
  37. package/npm-shrinkwrap.json +2 -2
  38. package/package.json +1 -1
@@ -70,6 +70,7 @@ const friends_1 = require("@ouro.bot/friends");
70
70
  const identity_1 = require("../../heart/identity");
71
71
  const mcp_manager_1 = require("../../repertoire/mcp-manager");
72
72
  const tools_1 = require("../../repertoire/tools");
73
+ const tool_arguments_1 = require("../../repertoire/tool-arguments");
73
74
  const config_1 = require("../../heart/config");
74
75
  const runtime_1 = require("../../nerves/runtime");
75
76
  const playback_1 = require("./playback");
@@ -1151,7 +1152,6 @@ class TwilioMediaStreamSession {
1151
1152
  }
1152
1153
  }
1153
1154
  /* v8 ignore stop */
1154
- const REALTIME_TOOL_FLOW_NAMES = new Set(["speak", "settle", "rest", "observe", "ponder"]);
1155
1155
  const OPENAI_REALTIME_DEFAULT_MODEL = "gpt-realtime-2";
1156
1156
  const OPENAI_REALTIME_DEFAULT_VOICE = "cedar";
1157
1157
  const OPENAI_REALTIME_DEFAULT_TRANSCRIPTION_MODEL = "gpt-realtime-whisper";
@@ -1172,7 +1172,6 @@ const OPENAI_REALTIME_RESPONSE_CREATE_CONFLICT_BACKOFF_MS = 1_000;
1172
1172
  const OPENAI_REALTIME_TOOL_PRESENCE_DELAY_MS = 900;
1173
1173
  const OPENAI_REALTIME_USER_TURN_RESPONSE_DELAY_MS = 1_000;
1174
1174
  const OPENAI_SIP_OUTBOUND_AMD_GREETING_TIMEOUT_MS = 10_000;
1175
- const OPENAI_SIP_UNSUPPORTED_TOOL_NAMES = new Set();
1176
1175
  const OPENAI_SIP_DEFAULT_API_BASE_URL = "https://api.openai.com/v1";
1177
1176
  const OPENAI_SIP_DEFAULT_WEBSOCKET_BASE_URL = "wss://api.openai.com/v1/realtime";
1178
1177
  /* v8 ignore start -- OpenAI Realtime/SIP provider adapter helpers are exercised through bridge-level SIP/Realtimes tests; branch permutations are provider-shape fallbacks @preserve */
@@ -1369,16 +1368,36 @@ function realtimeOutputAudioConfig(realtime, format) {
1369
1368
  ...(speed === undefined ? {} : { speed }),
1370
1369
  };
1371
1370
  }
1372
- function realtimeToolsFromChatTools(tools, excludedToolNames = new Set()) {
1373
- return tools
1374
- .filter((tool) => !REALTIME_TOOL_FLOW_NAMES.has(tool.function.name) && !excludedToolNames.has(tool.function.name))
1375
- .map((tool) => ({
1371
+ function selectRealtimeToolContext(context, mcpManager) {
1372
+ const selectCurrentTools = () => {
1373
+ const selected = (0, tools_1.selectToolsForChannel)((0, friends_1.getChannelCapabilities)("voice"), context.context?.friend.toolPreferences, context.context, undefined, mcpManager, undefined, context);
1374
+ return Object.freeze({ ordinary: selected.ordinary, engine: Object.freeze([]) });
1375
+ };
1376
+ return { ...context, toolSelection: selectCurrentTools(), selectCurrentTools };
1377
+ }
1378
+ async function prepareRealtimeToolContext(context) {
1379
+ let mcpManager;
1380
+ try {
1381
+ mcpManager = await (0, mcp_manager_1.getSharedMcpManager)({ agentName: context.agentName, agentRoot: context.agentRoot }) ?? undefined;
1382
+ }
1383
+ catch (error) {
1384
+ (0, runtime_1.emitNervesEvent)({
1385
+ level: "warn", component: "senses", event: "senses.voice_mcp_discovery_error",
1386
+ message: "MCP discovery failed; keeping native voice tools",
1387
+ meta: { agentName: context.agentName, error: errorMessage(error) },
1388
+ });
1389
+ }
1390
+ return selectRealtimeToolContext(context, mcpManager);
1391
+ }
1392
+ function realtimeToolsFromSelection(selection) {
1393
+ return selection.ordinary.map(({ tool }) => ({
1376
1394
  type: "function",
1377
1395
  name: tool.function.name,
1378
1396
  ...(tool.function.description ? { description: tool.function.description } : {}),
1379
1397
  parameters: tool.function.parameters ?? { type: "object", properties: {} },
1380
1398
  }));
1381
1399
  }
1400
+ /* v8 ignore start -- unchanged transport route parsing @preserve */
1382
1401
  function mediaStreamRequestedConversationEngine(url) {
1383
1402
  if (!url)
1384
1403
  return undefined;
@@ -1391,26 +1410,17 @@ function mediaStreamRequestedConversationEngine(url) {
1391
1410
  return undefined;
1392
1411
  }
1393
1412
  }
1394
- function parseToolArguments(raw) {
1395
- if (!raw.trim())
1396
- return {};
1397
- const parsed = JSON.parse(raw);
1398
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1399
- return {};
1400
- const args = {};
1401
- for (const [key, value] of Object.entries(parsed)) {
1402
- if (typeof value === "string") {
1403
- args[key] = value;
1404
- }
1405
- else if (value === undefined) {
1406
- args[key] = "";
1407
- }
1408
- else {
1409
- args[key] = JSON.stringify(value);
1410
- }
1411
- }
1412
- return args;
1413
- }
1413
+ /* v8 ignore stop */
1414
+ function parseToolArguments(raw, name, selection) {
1415
+ if (typeof raw !== "string")
1416
+ throw new Error("invalid tool arguments: expected a JSON object string");
1417
+ const schema = selection.ordinary.find((definition) => definition.tool.function.name === name)?.tool.function.parameters ?? {};
1418
+ const validation = (0, tool_arguments_1.validateAdvertisedToolArguments)(raw, schema);
1419
+ if (!validation.ok)
1420
+ throw new Error(`invalid tool arguments: ${validation.reason}`);
1421
+ return validation.value.arguments;
1422
+ }
1423
+ /* v8 ignore start -- unchanged transcript and provider instruction helpers @preserve */
1414
1424
  function transcriptMessageText(messages) {
1415
1425
  const recent = messages
1416
1426
  .filter((message) => message.role === "user" || message.role === "assistant")
@@ -1493,9 +1503,6 @@ function realtimeBootstrapInstructions(agentName, voiceStyle) {
1493
1503
  "If the caller is done or asks to end the call, say a brief goodbye and call voice_end_call.",
1494
1504
  ].filter(Boolean).join(" ");
1495
1505
  }
1496
- function realtimeBootstrapTools() {
1497
- return realtimeToolsFromChatTools((0, tools_1.getToolsForChannel)((0, friends_1.getChannelCapabilities)("voice")));
1498
- }
1499
1506
  function timeoutAfter(ms) {
1500
1507
  return new Promise((resolve) => {
1501
1508
  const timer = setTimeout(() => resolve(undefined), ms);
@@ -1691,17 +1698,24 @@ class TwilioOpenAIRealtimeMediaStreamSession {
1691
1698
  this.end();
1692
1699
  }
1693
1700
  }
1701
+ /* v8 ignore stop */
1694
1702
  async startOpenAIRealtimeSession() {
1695
1703
  const realtime = this.options.openaiRealtime;
1696
1704
  if (!realtime?.apiKey?.trim()) {
1697
1705
  throw new Error("OpenAI Realtime API key is not configured");
1698
1706
  }
1699
- this.ensureVoiceToolContext();
1707
+ const bootstrapContext = this.ensureVoiceToolContext();
1700
1708
  const instructionsPromise = this.buildInstructions()
1701
1709
  .catch(() => realtimeBootstrapInstructions(this.options.agentName));
1702
- const toolsPromise = this.buildRealtimeTools()
1703
- .then((tools) => realtimeToolsFromChatTools(tools))
1704
- .catch(() => realtimeBootstrapTools());
1710
+ const toolsPromise = prepareRealtimeToolContext(bootstrapContext).catch((error) => {
1711
+ (0, runtime_1.emitNervesEvent)({
1712
+ level: "error", component: "senses", event: "senses.voice_twilio_realtime_tool_config_error",
1713
+ message: "Realtime tool configuration failed",
1714
+ meta: { agentName: this.options.agentName, error: errorMessage(error) },
1715
+ });
1716
+ this.end();
1717
+ return null;
1718
+ });
1705
1719
  const ws = new ws_1.WebSocket(openAIRealtimeWebSocketUrl(realtime), {
1706
1720
  headers: {
1707
1721
  Authorization: `Bearer ${realtime.apiKey.trim()}`,
@@ -1747,32 +1761,43 @@ class TwilioOpenAIRealtimeMediaStreamSession {
1747
1761
  timeoutAfter(OPENAI_REALTIME_BOOTSTRAP_TIMEOUT_MS),
1748
1762
  ]);
1749
1763
  const usedBootstrap = ready === undefined;
1750
- const [instructions, tools] = ready ?? [
1764
+ const [instructions, toolContext] = ready ?? [
1751
1765
  realtimeBootstrapInstructions(this.options.agentName, realtime.voiceStyle),
1752
- realtimeBootstrapTools(),
1766
+ this.ensureVoiceToolContext(),
1753
1767
  ];
1754
- this.sendOpenAIRealtimeSessionUpdate(realtime, instructions, tools);
1768
+ if (!toolContext || this.closed)
1769
+ return;
1770
+ this.sendOpenAIRealtimeSessionUpdate(realtime, instructions, toolContext);
1755
1771
  this.flushPendingAudio();
1756
1772
  this.sendInitialGreeting();
1757
1773
  if (!usedBootstrap)
1758
1774
  return;
1759
1775
  Promise.all([instructionsPromise, toolsPromise])
1760
- .then(([fullInstructions, fullTools]) => {
1761
- if (this.closed)
1776
+ .then(([fullInstructions, fullContext]) => {
1777
+ if (!fullContext || this.closed)
1762
1778
  return;
1779
+ this.toolContext = fullContext;
1763
1780
  this.sendOpenAI({
1764
1781
  type: "session.update",
1765
1782
  session: {
1766
1783
  type: "realtime",
1767
1784
  instructions: fullInstructions,
1768
- tools: fullTools,
1785
+ tools: realtimeToolsFromSelection(fullContext.toolSelection),
1769
1786
  tool_choice: "auto",
1770
1787
  },
1771
1788
  });
1772
1789
  })
1773
- .catch(() => undefined);
1790
+ .catch((error) => {
1791
+ (0, runtime_1.emitNervesEvent)({
1792
+ level: "error", component: "senses", event: "senses.voice_twilio_realtime_tool_config_error",
1793
+ message: "Realtime tool configuration could not be published",
1794
+ meta: { agentName: this.options.agentName, error: errorMessage(error) },
1795
+ });
1796
+ this.end();
1797
+ });
1774
1798
  }
1775
- sendOpenAIRealtimeSessionUpdate(realtime, instructions, tools) {
1799
+ sendOpenAIRealtimeSessionUpdate(realtime, instructions, toolContext) {
1800
+ this.toolContext = toolContext;
1776
1801
  this.sendOpenAI({
1777
1802
  type: "session.update",
1778
1803
  session: {
@@ -1788,13 +1813,14 @@ class TwilioOpenAIRealtimeMediaStreamSession {
1788
1813
  },
1789
1814
  output: realtimeOutputAudioConfig(realtime, { type: "audio/pcmu" }),
1790
1815
  },
1791
- tools,
1816
+ tools: realtimeToolsFromSelection(toolContext.toolSelection),
1792
1817
  tool_choice: "auto",
1793
1818
  max_output_tokens: OPENAI_REALTIME_MAX_OUTPUT_TOKENS,
1794
1819
  ...(realtime.reasoningEffort ? { reasoning: { effort: realtime.reasoningEffort } } : {}),
1795
1820
  },
1796
1821
  });
1797
1822
  }
1823
+ /* v8 ignore start -- unchanged instruction and hangup transport owners @preserve */
1798
1824
  async buildInstructions() {
1799
1825
  (0, identity_1.setAgentName)(this.options.agentName);
1800
1826
  const agentRoot = resolveTwilioPhoneAgentRoot(this.options);
@@ -1827,63 +1853,25 @@ class TwilioOpenAIRealtimeMediaStreamSession {
1827
1853
  this.hangupRequested = true;
1828
1854
  setTimeout(() => this.completeHangupIfReady("tool_fallback"), 7_500).unref?.();
1829
1855
  }
1856
+ /* v8 ignore stop */
1830
1857
  ensureVoiceToolContext() {
1831
1858
  if (this.toolContext)
1832
- return;
1833
- this.toolContext = {
1834
- signin: async () => undefined,
1835
- ...(this.resolvedContext ? { context: this.resolvedContext } : {}),
1836
- ...(this.friendStore ? { friendStore: this.friendStore } : {}),
1837
- voiceCall: {
1838
- requestEnd: () => this.requestHangupFromTool(),
1839
- playAudio: (request) => this.playPreparedAudio(request),
1840
- },
1841
- };
1842
- }
1843
- async buildRealtimeTools() {
1844
- if (!this.resolvedContext || !this.friendStore) {
1845
- const voiceContext = await resolveVoiceFriendContext(this.options, {
1846
- friendId: this.friendId,
1847
- remotePhone: this.from || undefined,
1848
- callSid: this.callSid,
1849
- });
1850
- this.friendId = voiceContext.friendId;
1851
- this.friendStore = voiceContext.friendStore;
1852
- this.resolvedContext = voiceContext.resolved;
1853
- }
1854
- const resolved = this.resolvedContext;
1855
- const friendStore = this.friendStore;
1856
- this.toolContext = {
1859
+ return selectRealtimeToolContext(this.toolContext);
1860
+ const context = selectRealtimeToolContext({
1857
1861
  signin: async () => undefined,
1858
- context: resolved,
1859
- friendStore,
1862
+ agentName: this.options.agentName,
1863
+ agentRoot: resolveTwilioPhoneAgentRoot(this.options),
1864
+ context: this.resolvedContext,
1865
+ friendStore: this.friendStore,
1860
1866
  voiceCall: {
1861
1867
  requestEnd: () => this.requestHangupFromTool(),
1862
1868
  playAudio: (request) => this.playPreparedAudio(request),
1863
1869
  },
1864
- };
1865
- void this.refreshRealtimeToolsWithMcp(resolved);
1866
- return (0, tools_1.getToolsForChannel)((0, friends_1.getChannelCapabilities)("voice"), resolved.friend.toolPreferences, resolved, undefined, undefined);
1867
- }
1868
- async refreshRealtimeToolsWithMcp(resolved) {
1869
- try {
1870
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
1871
- if (!mcpManager || this.closed)
1872
- return;
1873
- const tools = realtimeToolsFromChatTools((0, tools_1.getToolsForChannel)((0, friends_1.getChannelCapabilities)("voice"), resolved.friend.toolPreferences, resolved, undefined, mcpManager));
1874
- this.sendOpenAI({
1875
- type: "session.update",
1876
- session: {
1877
- type: "realtime",
1878
- tools,
1879
- tool_choice: "auto",
1880
- },
1881
- });
1882
- }
1883
- catch {
1884
- // Keep realtime calls conversational even if optional MCP tool discovery is slow or unavailable.
1885
- }
1870
+ });
1871
+ this.toolContext = { ...context, toolSelection: Object.freeze({ ordinary: Object.freeze([]), engine: Object.freeze([]) }) };
1872
+ return context;
1886
1873
  }
1874
+ /* v8 ignore start -- unchanged transport code retains its existing exclusion; owned tool setup above is measured @preserve */
1887
1875
  sendInitialGreeting() {
1888
1876
  if (this.greetingSent)
1889
1877
  return;
@@ -2212,6 +2200,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
2212
2200
  for (const state of this.toolResponses.values())
2213
2201
  this.clearRealtimeToolPresenceTimer(state);
2214
2202
  }
2203
+ /* v8 ignore stop */
2215
2204
  async runRealtimeTool(event) {
2216
2205
  const name = typeof event.name === "string" ? event.name : "";
2217
2206
  const callId = typeof event.call_id === "string" ? event.call_id : "";
@@ -2251,7 +2240,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
2251
2240
  });
2252
2241
  let output;
2253
2242
  try {
2254
- const args = parseToolArguments(typeof event.arguments === "string" ? event.arguments : "");
2243
+ const args = parseToolArguments(event.arguments, name, this.toolContext.toolSelection);
2255
2244
  (0, runtime_1.emitNervesEvent)({
2256
2245
  component: "senses",
2257
2246
  event: "senses.voice_twilio_realtime_tool_start",
@@ -2294,6 +2283,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
2294
2283
  this.requestRealtimeResponse();
2295
2284
  }
2296
2285
  }
2286
+ /* v8 ignore start -- unchanged response coordination transport @preserve */
2297
2287
  noteRealtimeResponseCreated(event) {
2298
2288
  this.realtimeResponseCreateInFlight = null;
2299
2289
  this.untrackedActiveRealtimeResponse = false;
@@ -2627,6 +2617,7 @@ class OpenAISipPhoneSession {
2627
2617
  get outboundId() {
2628
2618
  return this.metadata.outboundId;
2629
2619
  }
2620
+ /* v8 ignore stop */
2630
2621
  async start() {
2631
2622
  try {
2632
2623
  const realtime = this.options.openaiRealtime;
@@ -2661,10 +2652,10 @@ class OpenAISipPhoneSession {
2661
2652
  await this.rejectOpenAISipCall(realtime, sip, "amd_preclassified_nonhuman");
2662
2653
  return;
2663
2654
  }
2664
- this.outboundAmdState = initialGreetingMode === "hold" ? "pending" : "not_needed";
2665
- this.autoResponsesSuppressedForAmd = initialGreetingMode === "hold";
2655
+ this.outboundAmdState = "not_needed";
2656
+ this.autoResponsesSuppressedForAmd = false;
2666
2657
  await this.updateOutboundJobIfNeeded();
2667
- this.ensureVoiceToolContext();
2658
+ const bootstrapContext = this.ensureVoiceToolContext();
2668
2659
  (0, runtime_1.emitNervesEvent)({
2669
2660
  component: "senses",
2670
2661
  event: "senses.voice_openai_sip_call_start",
@@ -2679,21 +2670,20 @@ class OpenAISipPhoneSession {
2679
2670
  this.registry?.register(this);
2680
2671
  const fullConfigPromise = Promise.all([
2681
2672
  this.buildInstructions(),
2682
- this.buildRealtimeTools()
2683
- .then((tools) => realtimeToolsFromChatTools(tools, OPENAI_SIP_UNSUPPORTED_TOOL_NAMES)),
2673
+ prepareRealtimeToolContext(bootstrapContext),
2684
2674
  ]);
2685
2675
  const ready = await Promise.race([
2686
2676
  fullConfigPromise,
2687
2677
  timeoutAfter(OPENAI_REALTIME_BOOTSTRAP_TIMEOUT_MS),
2688
2678
  ]);
2689
2679
  const usedBootstrap = ready === undefined;
2690
- const [instructions, tools] = ready ?? [
2680
+ const [instructions, toolContext] = ready ?? [
2691
2681
  realtimeBootstrapInstructions(this.options.agentName, realtime.voiceStyle),
2692
- realtimeBootstrapTools(),
2682
+ this.ensureVoiceToolContext(),
2693
2683
  ];
2694
2684
  if (this.closed || this.outboundAmdStopped())
2695
2685
  return;
2696
- await this.acceptOpenAISipCall(realtime, sip, instructions, tools);
2686
+ await this.acceptOpenAISipCall(realtime, sip, instructions, toolContext);
2697
2687
  if (this.closed || this.outboundAmdStopped())
2698
2688
  return;
2699
2689
  this.openControlWebSocket(realtime, sip, fullConfigPromise, usedBootstrap);
@@ -2710,6 +2700,7 @@ class OpenAISipPhoneSession {
2710
2700
  throw error;
2711
2701
  }
2712
2702
  }
2703
+ /* v8 ignore start -- unchanged outbound AMD transport @preserve */
2713
2704
  async updateOutboundJobIfNeeded() {
2714
2705
  if (this.metadata.direction !== "outbound" || !this.metadata.outboundId)
2715
2706
  return;
@@ -2741,7 +2732,8 @@ class OpenAISipPhoneSession {
2741
2732
  outboundAmdStopped() {
2742
2733
  return this.outboundAmdState === "nonhuman" || this.outboundAmdState === "timeout";
2743
2734
  }
2744
- async acceptOpenAISipCall(realtime, sip, instructions, tools) {
2735
+ /* v8 ignore stop */
2736
+ async acceptOpenAISipCall(realtime, sip, instructions, toolContext) {
2745
2737
  const fetchImpl = sip.fetch ?? fetch;
2746
2738
  const response = await fetchImpl(openAISipCallActionUrl(sip, this.metadata.callId, "accept"), {
2747
2739
  method: "POST",
@@ -2761,7 +2753,7 @@ class OpenAISipPhoneSession {
2761
2753
  },
2762
2754
  output: realtimeOutputAudioConfig(realtime),
2763
2755
  },
2764
- tools,
2756
+ tools: realtimeToolsFromSelection(toolContext.toolSelection),
2765
2757
  tool_choice: "auto",
2766
2758
  max_output_tokens: OPENAI_REALTIME_MAX_OUTPUT_TOKENS,
2767
2759
  }),
@@ -2770,6 +2762,7 @@ class OpenAISipPhoneSession {
2770
2762
  const responseText = await response.text().catch(() => "");
2771
2763
  throw new Error(`OpenAI SIP call accept failed: ${response.status} ${responseText}`.trim());
2772
2764
  }
2765
+ this.toolContext = toolContext;
2773
2766
  (0, runtime_1.emitNervesEvent)({
2774
2767
  component: "senses",
2775
2768
  event: "senses.voice_openai_sip_call_accepted",
@@ -2782,6 +2775,7 @@ class OpenAISipPhoneSession {
2782
2775
  },
2783
2776
  });
2784
2777
  }
2778
+ /* v8 ignore start -- unchanged call-rejection transport @preserve */
2785
2779
  async rejectOpenAISipCall(realtime, sip, trigger) {
2786
2780
  try {
2787
2781
  const response = await (sip.fetch ?? fetch)(openAISipCallActionUrl(sip, this.metadata.callId, "reject"), {
@@ -2812,6 +2806,7 @@ class OpenAISipPhoneSession {
2812
2806
  this.close(trigger);
2813
2807
  }
2814
2808
  }
2809
+ /* v8 ignore stop */
2815
2810
  openControlWebSocket(realtime, sip, fullConfigPromise, usedBootstrap) {
2816
2811
  const ws = new ws_1.WebSocket(openAISipControlWebSocketUrl(sip, this.metadata.callId), {
2817
2812
  headers: {
@@ -2831,21 +2826,29 @@ class OpenAISipPhoneSession {
2831
2826
  if (!usedBootstrap)
2832
2827
  return;
2833
2828
  fullConfigPromise
2834
- .then(([instructions, tools]) => {
2829
+ .then(([instructions, toolContext]) => {
2835
2830
  if (this.closed)
2836
2831
  return;
2832
+ this.toolContext = toolContext;
2837
2833
  this.sendOpenAI({
2838
2834
  type: "session.update",
2839
2835
  session: {
2840
2836
  type: "realtime",
2841
2837
  instructions,
2842
- tools,
2838
+ tools: realtimeToolsFromSelection(toolContext.toolSelection),
2843
2839
  tool_choice: "auto",
2844
2840
  ...(realtime.reasoningEffort ? { reasoning: { effort: realtime.reasoningEffort } } : {}),
2845
2841
  },
2846
2842
  });
2847
2843
  })
2848
- .catch(() => undefined);
2844
+ .catch((error) => {
2845
+ (0, runtime_1.emitNervesEvent)({
2846
+ level: "error", component: "senses", event: "senses.voice_openai_sip_tool_config_error",
2847
+ message: "SIP tool configuration could not be published",
2848
+ meta: { agentName: this.options.agentName, error: errorMessage(error) },
2849
+ });
2850
+ this.close("configuration_error");
2851
+ });
2849
2852
  });
2850
2853
  ws.on("message", (raw) => this.handleOpenAIMessage(raw));
2851
2854
  ws.on("close", () => {
@@ -2862,6 +2865,7 @@ class OpenAISipPhoneSession {
2862
2865
  });
2863
2866
  });
2864
2867
  }
2868
+ /* v8 ignore start -- unchanged SIP instruction owner @preserve */
2865
2869
  async buildInstructions() {
2866
2870
  (0, identity_1.setAgentName)(this.options.agentName);
2867
2871
  const agentRoot = resolveTwilioPhoneAgentRoot(this.options);
@@ -2888,44 +2892,25 @@ class OpenAISipPhoneSession {
2888
2892
  (0, context_1.saveSession)(this.sessionPath, this.sessionMessages);
2889
2893
  return realtimeSystem;
2890
2894
  }
2895
+ /* v8 ignore stop */
2891
2896
  ensureVoiceToolContext() {
2892
2897
  if (this.toolContext)
2893
- return;
2894
- this.toolContext = {
2898
+ return selectRealtimeToolContext(this.toolContext);
2899
+ const context = selectRealtimeToolContext({
2895
2900
  signin: async () => undefined,
2896
- ...(this.resolvedContext ? { context: this.resolvedContext } : {}),
2897
- ...(this.friendStore ? { friendStore: this.friendStore } : {}),
2898
- voiceCall: {
2899
- requestEnd: () => this.requestHangupFromTool(),
2900
- playAudio: (request) => this.playRealtimeAudioCue(request),
2901
- },
2902
- };
2903
- }
2904
- async buildRealtimeTools() {
2905
- if (!this.resolvedContext || !this.friendStore) {
2906
- const voiceContext = await resolveVoiceFriendContext(this.options, {
2907
- friendId: this.friendId,
2908
- remotePhone: this.metadata.from || undefined,
2909
- callSid: this.metadata.callId,
2910
- });
2911
- this.friendId = voiceContext.friendId;
2912
- this.friendStore = voiceContext.friendStore;
2913
- this.resolvedContext = voiceContext.resolved;
2914
- }
2915
- const resolved = this.resolvedContext;
2916
- const friendStore = this.friendStore;
2917
- this.toolContext = {
2918
- signin: async () => undefined,
2919
- context: resolved,
2920
- friendStore,
2901
+ agentName: this.options.agentName,
2902
+ agentRoot: resolveTwilioPhoneAgentRoot(this.options),
2903
+ context: this.resolvedContext,
2904
+ friendStore: this.friendStore,
2921
2905
  voiceCall: {
2922
2906
  requestEnd: () => this.requestHangupFromTool(),
2923
2907
  playAudio: (request) => this.playRealtimeAudioCue(request),
2924
2908
  },
2925
- };
2926
- void this.refreshRealtimeToolsWithMcp(resolved);
2927
- return (0, tools_1.getToolsForChannel)((0, friends_1.getChannelCapabilities)("voice"), resolved.friend.toolPreferences, resolved, undefined, undefined);
2909
+ });
2910
+ this.toolContext = { ...context, toolSelection: Object.freeze({ ordinary: Object.freeze([]), engine: Object.freeze([]) }) };
2911
+ return context;
2928
2912
  }
2913
+ /* v8 ignore start -- unchanged audio transport code retains its existing exclusion @preserve */
2929
2914
  playRealtimeAudioCue(request) {
2930
2915
  const source = request.source ?? "tone";
2931
2916
  const label = request.label?.trim() || (source === "tone" ? "tone cue" : "audio clip");
@@ -2968,25 +2953,6 @@ class OpenAISipPhoneSession {
2968
2953
  ].join(" "),
2969
2954
  };
2970
2955
  }
2971
- async refreshRealtimeToolsWithMcp(resolved) {
2972
- try {
2973
- const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
2974
- if (!mcpManager || this.closed)
2975
- return;
2976
- const tools = realtimeToolsFromChatTools((0, tools_1.getToolsForChannel)((0, friends_1.getChannelCapabilities)("voice"), resolved.friend.toolPreferences, resolved, undefined, mcpManager), OPENAI_SIP_UNSUPPORTED_TOOL_NAMES);
2977
- this.sendOpenAI({
2978
- type: "session.update",
2979
- session: {
2980
- type: "realtime",
2981
- tools,
2982
- tool_choice: "auto",
2983
- },
2984
- });
2985
- }
2986
- catch {
2987
- // Keep SIP calls conversational even if optional MCP tool discovery is slow or unavailable.
2988
- }
2989
- }
2990
2956
  recordOutboundAmdTranscriptCandidate(transcript) {
2991
2957
  if (this.outboundAmdState !== "pending")
2992
2958
  return;
@@ -3336,6 +3302,7 @@ class OpenAISipPhoneSession {
3336
3302
  for (const state of this.toolResponses.values())
3337
3303
  this.clearRealtimeToolPresenceTimer(state);
3338
3304
  }
3305
+ /* v8 ignore stop */
3339
3306
  async runRealtimeTool(event) {
3340
3307
  const name = typeof event.name === "string" ? event.name : "";
3341
3308
  const callId = typeof event.call_id === "string" ? event.call_id : "";
@@ -3348,24 +3315,6 @@ class OpenAISipPhoneSession {
3348
3315
  toolState.suppressFollowup = true;
3349
3316
  if (toolState && !toolState.suppressFollowup)
3350
3317
  this.scheduleRealtimeToolPresence(responseId, toolState);
3351
- // A coordinated tool call (one with a responseId from OpenAI's active
3352
- // response cycle) is proof that the realtime server has already parsed the
3353
- // caller's most recent turn into a tool intent. If we still hold a
3354
- // synthetic caller floor for that turn — because the matching
3355
- // input_audio_transcription.completed event has not arrived yet, which is
3356
- // common in unit fixtures and during fast-turn races — dismiss it so the
3357
- // floor gate is not stuck thinking the caller still owns the floor when
3358
- // the assistant is mid-response.
3359
- if (coordinated && this.activeCallerTurnId) {
3360
- const turnId = this.activeCallerTurnId;
3361
- this.activeCallerTurnId = undefined;
3362
- this.floor.apply({
3363
- type: "caller.turn.dismissed",
3364
- atMs: Date.now(),
3365
- turnId,
3366
- reason: "coordinated_tool_call",
3367
- });
3368
- }
3369
3318
  this.floor.apply({
3370
3319
  type: "tool.call.started",
3371
3320
  atMs: Date.now(),
@@ -3375,7 +3324,7 @@ class OpenAISipPhoneSession {
3375
3324
  });
3376
3325
  let output;
3377
3326
  try {
3378
- const args = parseToolArguments(typeof event.arguments === "string" ? event.arguments : "");
3327
+ const args = parseToolArguments(event.arguments, name, this.toolContext.toolSelection);
3379
3328
  (0, runtime_1.emitNervesEvent)({
3380
3329
  component: "senses",
3381
3330
  event: "senses.voice_openai_sip_tool_start",
@@ -3418,6 +3367,7 @@ class OpenAISipPhoneSession {
3418
3367
  this.requestRealtimeResponse();
3419
3368
  }
3420
3369
  }
3370
+ /* v8 ignore start -- unchanged SIP response coordination @preserve */
3421
3371
  noteRealtimeResponseCreated(event) {
3422
3372
  this.realtimeResponseCreateInFlight = null;
3423
3373
  this.untrackedActiveRealtimeResponse = false;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.810",
3
+ "version": "0.1.0-alpha.811",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@ouro.bot/cli",
9
- "version": "0.1.0-alpha.810",
9
+ "version": "0.1.0-alpha.811",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.810",
3
+ "version": "0.1.0-alpha.811",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },