@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.
- package/changelog.json +12 -0
- package/deploy/unraid/sanctuary-acceptance-contract.json +1 -1
- package/deploy/unraid/sanctuary-unit16-host-broker.mjs +8 -7
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/psyche/IDENTITY.md +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +2 -2
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/config.js +32 -2
- package/dist/heart/core.js +167 -115
- package/dist/heart/daemon/daemon.js +19 -9
- package/dist/heart/daemon/sanctuary-acceptance-adapter.js +88 -50
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +53 -15
- package/dist/heart/daemon/sanctuary-acceptance-scenarios.js +6 -26
- package/dist/heart/frontend-approval-runtime.js +87 -13
- package/dist/heart/identity.js +17 -5
- package/dist/heart/session-events.js +8 -1
- package/dist/heart/tool-approval.js +64 -31
- package/dist/mind/pending.js +4 -4
- package/dist/repertoire/mcp-manager.js +365 -381
- package/dist/repertoire/mcp-tools.js +66 -25
- package/dist/repertoire/plugin-mcp.js +3 -3
- package/dist/repertoire/shell-sessions.js +8 -7
- package/dist/repertoire/tool-arguments.js +30 -8
- package/dist/repertoire/tools-session.js +30 -5
- package/dist/repertoire/tools-shell.js +30 -14
- package/dist/repertoire/tools-voice.js +6 -6
- package/dist/repertoire/tools.js +231 -205
- package/dist/senses/bluebubbles/index.js +4 -2
- package/dist/senses/cli.js +6 -5
- package/dist/senses/private-runtime.js +9 -3
- package/dist/senses/shared-turn.js +12 -9
- package/dist/senses/teams.js +15 -12
- package/dist/senses/telegram-approval-runtime.js +129 -20
- package/dist/senses/telegram-client.js +19 -2
- package/dist/senses/telegram.js +54 -15
- package/dist/senses/voice/twilio-phone.js +123 -173
- package/npm-shrinkwrap.json +2 -2
- 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
|
|
1373
|
-
|
|
1374
|
-
.
|
|
1375
|
-
.
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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 =
|
|
1703
|
-
.
|
|
1704
|
-
|
|
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,
|
|
1764
|
+
const [instructions, toolContext] = ready ?? [
|
|
1751
1765
|
realtimeBootstrapInstructions(this.options.agentName, realtime.voiceStyle),
|
|
1752
|
-
|
|
1766
|
+
this.ensureVoiceToolContext(),
|
|
1753
1767
|
];
|
|
1754
|
-
|
|
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,
|
|
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:
|
|
1785
|
+
tools: realtimeToolsFromSelection(fullContext.toolSelection),
|
|
1769
1786
|
tool_choice: "auto",
|
|
1770
1787
|
},
|
|
1771
1788
|
});
|
|
1772
1789
|
})
|
|
1773
|
-
.catch(() =>
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1859
|
-
|
|
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
|
-
|
|
1866
|
-
return
|
|
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(
|
|
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 =
|
|
2665
|
-
this.autoResponsesSuppressedForAmd =
|
|
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
|
-
|
|
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,
|
|
2680
|
+
const [instructions, toolContext] = ready ?? [
|
|
2691
2681
|
realtimeBootstrapInstructions(this.options.agentName, realtime.voiceStyle),
|
|
2692
|
-
|
|
2682
|
+
this.ensureVoiceToolContext(),
|
|
2693
2683
|
];
|
|
2694
2684
|
if (this.closed || this.outboundAmdStopped())
|
|
2695
2685
|
return;
|
|
2696
|
-
await this.acceptOpenAISipCall(realtime, sip, instructions,
|
|
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
|
-
|
|
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,
|
|
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(() =>
|
|
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
|
-
|
|
2898
|
+
return selectRealtimeToolContext(this.toolContext);
|
|
2899
|
+
const context = selectRealtimeToolContext({
|
|
2895
2900
|
signin: async () => undefined,
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
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
|
-
|
|
2927
|
-
return
|
|
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(
|
|
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;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
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.
|
|
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",
|