@xopcai/xopc 0.0.264 → 0.0.265
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/dist/browser-ext/dist/assets/auth-CknJ00ae.js +5844 -0
- package/dist/browser-ext/dist/assets/sidepanel-C69P3v9u.css +113 -0
- package/dist/browser-ext/dist/background.js +200 -68
- package/dist/browser-ext/dist/sidepanel.html +14 -0
- package/dist/browser-ext/dist/sidepanel.js +18832 -0
- package/dist/browser-ext/manifest.json +20 -20
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{activity-store-BYBCpt7_.js → activity-store-Bhyy6ENz.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-D20aBHPJ.js → agent-avatar-dicebear-DRTTU2H3.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-BZvMqNrU.js → agent-defaults-settings-panel-BsXF7LVi.js} +1 -1
- package/dist/gateway/static/root/assets/{agents-6foBELwt.js → agents-DpjWfxYx.js} +1 -1
- package/dist/gateway/static/root/assets/{api-YbtjDbuG.js → api-B2y7ZjZk.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-4ps1RrDU.js → app-management-settings-panel-33Mc81h2.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-C7nBHnkG.js → appearance-settings-C8JECSiJ.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-B_g-rL5b.js → apps-page-t64FcxIZ.js} +1 -1
- package/dist/gateway/static/root/assets/{archive-plugin-DfKK1iJO.js → archive-plugin-PzZ6SzfY.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-BOU1pdnQ.js → attachment-load-OhLL6K__.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-BePzexAH.js → automations-page-BpAYwuvx.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-workspace-7-m5pBEZ.js → automations-workspace-B6aTT5IC.js} +1 -1
- package/dist/gateway/static/root/assets/{binary-plugins-Cly8XDyt.js → binary-plugins-B5XpGsHg.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-DzSu62js.js → block-editor-BgEhoTnN.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-automation-inputs-8bHj7Fv2.js → browser-automation-inputs-s5I78Iko.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-automations-page-CzL7Aj2k.js → browser-automations-page-CBLunDRH.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-settings-page-Dum3mTiz.js → browser-settings-page-CSfpPCuD.js} +1 -1
- package/dist/gateway/static/root/assets/{capabilities-settings-panel-Ci6qVS-O.js → capabilities-settings-panel-DftNnICK.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-settings-BOGEZnSv.js → channels-settings-CpuWa5qu.js} +1 -1
- package/dist/gateway/static/root/assets/{channels-status-swr-BR8oQ_eP.js → channels-status-swr-_OsWr_vS.js} +1 -1
- package/dist/gateway/static/root/assets/{chat-terminal-dock-BF9ZXa5Z.js → chat-terminal-dock-mnPsS_GJ.js} +1 -1
- package/dist/gateway/static/root/assets/{connectors-page-DBvEvupa.js → connectors-page-D3nHGnNV.js} +1 -1
- package/dist/gateway/static/root/assets/{cron-expression-BszT_gjK.js → cron-expression-NcGBvIfe.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-D2MKBJ0G.js → date-picker-BrVcl7H5.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-DqigBx-G.js → dependency-picker--edd48Y9.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-DMPk4CaK.js → desktop-pet-settings-DWredJwO.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-CapspBGG.js → desktop-pet-vuyyb6I6.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-CXLsDtrW.js → extension-debug-page-CtBW4ebj.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-B2j87ROw.js → extension-page-CsUrmtW7.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-t1BApw-u.js → extension-settings-page-BUd31AuK.js} +1 -1
- package/dist/gateway/static/root/assets/{files-BArWTfr7.js → files-Be5viEFY.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-swr-BFSesvAC.js → gateway-config-swr-OObCK8jM.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-settings-BFAecoGy.js → gateway-settings-C6FQv7op.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-startup-retry-D1giYgI-.js → gateway-startup-retry-D_TxKN_A.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-settings-CyD9zDcU.js → heartbeat-settings-CDo1Gjp2.js} +1 -1
- package/dist/gateway/static/root/assets/{home-page-uItZMiBE.js → home-page-D3_uvVJY.js} +1 -1
- package/dist/gateway/static/root/assets/{image-generation-api-BeB-OPqO.js → image-generation-api-fPU9yGsw.js} +1 -1
- package/dist/gateway/static/root/assets/index-Dpu7EKTm.js +74 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-Q_CM-ZCs.js → keyboard-shortcuts-settings-CNVmrCrV.js} +1 -1
- package/dist/gateway/static/root/assets/{local-app-workbench-page-DaiBWfOt.js → local-app-workbench-page-B0IVDvs-.js} +1 -1
- package/dist/gateway/static/root/assets/{local-apps-page-BALMNCSp.js → local-apps-page-DvkarvLt.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-RJHYFoq9.js → locale-store-DFQX9kTl.js} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-C-TS7oFg.js → logs-page-BknY0vZf.js} +1 -1
- package/dist/gateway/static/root/assets/{management-settings-BajkJeYl.js → management-settings-DW6hJINQ.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-view-BxoQJqHx.js → markdown-view-D-5jHRWg.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-C_8Hl6KG.js → media-plugins-DYautOWY.js} +1 -1
- package/dist/gateway/static/root/assets/{messages-Cf1h5WXB.js → messages-Dj8QuS6e.js} +2 -2
- package/dist/gateway/static/root/assets/{mobile-pairing-wizard-CsWIZrZW.js → mobile-pairing-wizard-Dgu03cv-.js} +1 -1
- package/dist/gateway/static/root/assets/{note-detail-panel-CCbPg_GP.js → note-detail-panel-Cv9uH6Jq.js} +3 -3
- package/dist/gateway/static/root/assets/{notes-page-BdxxcUKa.js → notes-page-DcIAiYJn.js} +1 -1
- package/dist/gateway/static/root/assets/{page-tabs-BMYQef5D.js → page-tabs-DF6Q5XvA.js} +1 -1
- package/dist/gateway/static/root/assets/{preview-open-alternatives-CKZ-H5hJ.js → preview-open-alternatives-CQJH_jAN.js} +1 -1
- package/dist/gateway/static/root/assets/{product-delivery-CV4pDOtG.js → product-delivery-CCC4FJ3Y.js} +1 -1
- package/dist/gateway/static/root/assets/{product-open-page-zXJIptXs.js → product-open-page-CIGLPFm7.js} +1 -1
- package/dist/gateway/static/root/assets/{project-detail-page-DRhhxhZB.js → project-detail-page-D65JfNSw.js} +1 -1
- package/dist/gateway/static/root/assets/{projects-page-Wt7gF3Sx.js → projects-page-BfWF7Sz-.js} +1 -1
- package/dist/gateway/static/root/assets/{registry-api-BSfFaxVN.js → registry-api-CsZLatKo.js} +1 -1
- package/dist/gateway/static/root/assets/{remote-access-hub-B6de5Zrb.js → remote-access-hub-DA1g4CUD.js} +1 -1
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-DcUB5h1f.js → runtime-tools-settings-panel-0-bFSBB8.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-CquWhe7r.js → schema-form-csGbhTUr.js} +1 -1
- package/dist/gateway/static/root/assets/{schemas-BHS7bo06.js → schemas-BaskVM5g.js} +2 -2
- package/dist/gateway/static/root/assets/{sessions-page-BNWO2EVF.js → sessions-page-DXPBNhrZ.js} +1 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-DxeZm67O.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-DwswAefc.js → settings-form-section-YdrdY8B3.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-CoAVmjjs.js → settings-page-Cv9waRn3.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-C9EIcWtH.js → setup-status-panel-DPf-_05Z.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-DWwy_4zh.js → share-preview-page-DBoEHTPn.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-settings-CIUvq27-.js → shares-settings-BQP5yNcO.js} +1 -1
- package/dist/gateway/static/root/assets/skill-reload-api-B5T8cgjq.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-CqsgVtVh.js → skills-page-D1G8h2Lt.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-B-N73248.js → skills-page.utils-BNZYTop7.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-BndjXuBZ.js → system-settings-panel-ByuppbHi.js} +1 -1
- package/dist/gateway/static/root/assets/{task-detail-page-CG6Ha9t7.js → task-detail-page-DNmkrF6y.js} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-C5ssn64t.js → theme-store-zRT6vaZc.js} +1 -1
- package/dist/gateway/static/root/assets/{tunnel-api-BxZdcp3h.js → tunnel-api-CxWGpbwr.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-mj9CUuz1.js → use-autosave-B0l4qubU.js} +1 -1
- package/dist/gateway/static/root/assets/{user-model-page-DKeHMXI4.js → user-model-page-DMsI6YI7.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-CzxVMYlr.js → voice-api-key-field-Bcz7boJg.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-config-api-COA0V1cK.js → voice-config-api-DpJEu2RQ.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-DMiPK8dp.js → work-discovery-overlay-Dujq1j_u.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-C82OXCJK.js → workflow-run-setup-panel-CxNYbLNB.js} +1 -1
- package/dist/gateway/static/root/assets/{workflows-page-HLCUV3rQ.js → workflows-page-Dk1jK2nv.js} +1 -1
- package/dist/gateway/static/root/index.html +12 -12
- package/dist/package.js +1 -1
- package/dist/packages/browser-control-contract/src/index.js +14 -0
- package/dist/packages/endpoint-tools-protocol/src/index.js +2 -1
- package/dist/packages/gateway-contract/src/browser-page-context.js +43 -0
- package/dist/packages/gateway-contract/src/browser-tab-binding.js +29 -0
- package/dist/packages/gateway-contract/src/device-pairing.js +18 -2
- package/dist/packages/realtime-protocol/src/index.js +1 -0
- package/dist/packages/realtime-protocol/src/voice-audio.js +70 -9
- package/dist/packages/realtime-protocol/src/voice.js +65 -17
- package/dist/src/agent/source-context/browser-page.d.ts +3 -0
- package/dist/src/agent/source-context/browser-page.js +39 -0
- package/dist/src/agent/source-context/injector.js +9 -2
- package/dist/src/agent/source-context/types.d.ts +7 -1
- package/dist/src/agent/source-context/types.js +4 -1
- package/dist/src/agent/tools/factory.js +9 -1
- package/dist/src/browser/automations/schema.d.ts +1 -1
- package/dist/src/browser/automations/service.d.ts +1 -1
- package/dist/src/browser/native-messaging-host.d.ts +15 -0
- package/dist/src/browser/native-messaging-host.js +105 -0
- package/dist/src/browser/providers/browser-ext-install.d.ts +17 -1
- package/dist/src/browser/providers/browser-ext-install.js +82 -16
- package/dist/src/browser/providers/extension.d.ts +4 -0
- package/dist/src/browser/providers/extension.js +104 -38
- package/dist/src/browser/runtime/browser-runtime.d.ts +2 -1
- package/dist/src/browser/runtime/browser-runtime.js +6 -2
- package/dist/src/cli/commands/browser.js +13 -3
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/endpoint-tools/runtime.js +2 -0
- package/dist/src/gateway/chat-stream/mapper.js +3 -2
- package/dist/src/gateway/hono/app.js +19 -3
- package/dist/src/gateway/hono/middleware/auth.d.ts +4 -1
- package/dist/src/gateway/hono/middleware/auth.js +12 -1
- package/dist/src/gateway/hono/routes/browser-install.js +1 -1
- package/dist/src/gateway/hono/routes/browser.js +106 -7
- package/dist/src/gateway/hono/routes/devices.js +36 -3
- package/dist/src/gateway/hono/routes/endpoint-tools.js +15 -0
- package/dist/src/gateway/hono/routes/session-input-handler.d.ts +20 -2
- package/dist/src/gateway/hono/routes/session-input-handler.js +18 -0
- package/dist/src/gateway/hono/routes/sessions.js +12 -6
- package/dist/src/gateway/hono/routes/voice.js +5 -4
- package/dist/src/gateway/security/gateway-scopes.d.ts +1 -0
- package/dist/src/gateway/security/gateway-scopes.js +10 -1
- package/dist/src/gateway/security/origin-check.d.ts +1 -0
- package/dist/src/gateway/security/origin-check.js +5 -1
- package/dist/src/gateway/service/agent-runner.js +2 -1
- package/dist/src/gateway/service/session-input-coordinator.d.ts +1 -0
- package/dist/src/gateway/service/session-input-coordinator.js +5 -4
- package/dist/src/gateway/service.js +12 -11
- package/dist/src/realtime/runtime.js +2 -1
- package/dist/src/session/client-history.d.ts +4 -1
- package/dist/src/session/client-history.js +6 -3
- package/dist/src/storage/sqlite/browser-tab-binding-repository.d.ts +7 -0
- package/dist/src/storage/sqlite/browser-tab-binding-repository.js +60 -0
- package/dist/src/storage/sqlite/device-access-repository.d.ts +3 -1
- package/dist/src/storage/sqlite/device-access-repository.js +4 -3
- package/dist/src/storage/sqlite/device-pairing-approval.js +6 -4
- package/dist/src/storage/sqlite/device-pairing-repository.d.ts +1 -1
- package/dist/src/storage/sqlite/index.d.ts +1 -0
- package/dist/src/storage/sqlite/index.js +2 -1
- package/dist/src/storage/sqlite/migrations/155_browser_extension.sql +120 -0
- package/dist/src/storage/sqlite/migrations/156_browser_tab_bindings.sql +14 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/voice/realtime/agentBroker.d.ts +54 -0
- package/dist/src/voice/realtime/agentBroker.js +116 -0
- package/dist/src/voice/realtime/agentEngine.js +62 -15
- package/dist/src/voice/realtime/engine.d.ts +2 -1
- package/dist/src/voice/realtime/omniEngine.js +36 -7
- package/dist/src/voice/realtime/pcmFrameBuffer.d.ts +6 -0
- package/dist/src/voice/realtime/pcmFrameBuffer.js +29 -0
- package/dist/src/voice/realtime/runtime.js +97 -21
- package/dist/src/voice/realtime/runtime.types.d.ts +5 -21
- package/dist/src/voice/realtime/turnPolicy.d.ts +36 -0
- package/dist/src/voice/realtime/turnPolicy.js +134 -0
- package/package.json +2 -2
- package/dist/browser-ext/dist/content.js +0 -191
- package/dist/browser-ext/dist/popup.js +0 -32
- package/dist/browser-ext/popup.html +0 -37
- package/dist/gateway/static/root/assets/index-DQtjs2x3.js +0 -74
- package/dist/gateway/static/root/assets/settings-advanced-gate-DK1yTYE4.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-DfE3zFVw.js +0 -1
- package/dist/src/voice/realtime/conversationTurn.d.ts +0 -20
- package/dist/src/voice/realtime/conversationTurn.js +0 -83
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/voice/realtime/agentBroker.ts
|
|
2
|
+
const RUN_ASSIGNMENT_TIMEOUT_MS = 5e3;
|
|
3
|
+
const RUN_ASSIGNMENT_POLL_MS = 20;
|
|
4
|
+
const MAX_TRACKED_TASKS = 256;
|
|
5
|
+
function sleep(ms, signal) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const finish = () => {
|
|
8
|
+
signal.removeEventListener("abort", abort);
|
|
9
|
+
resolve();
|
|
10
|
+
};
|
|
11
|
+
const timer = setTimeout(finish, ms);
|
|
12
|
+
const abort = () => {
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
reject(signal.reason ?? /* @__PURE__ */ new Error("Voice task subscription cancelled"));
|
|
15
|
+
};
|
|
16
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
17
|
+
if (signal.aborted) abort();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function eventData(delivery) {
|
|
21
|
+
if (delivery.kind !== "realtime.event") return void 0;
|
|
22
|
+
const value = delivery.payload.data;
|
|
23
|
+
return value && typeof value === "object" && typeof value.type === "string" ? value : void 0;
|
|
24
|
+
}
|
|
25
|
+
/** Durable voice-to-Agent boundary. Playback and socket cancellation only detach event delivery. */
|
|
26
|
+
var DurableVoiceAgentBroker = class {
|
|
27
|
+
runByTask = /* @__PURE__ */ new Map();
|
|
28
|
+
constructor(deps) {
|
|
29
|
+
this.deps = deps;
|
|
30
|
+
}
|
|
31
|
+
async delegate(input) {
|
|
32
|
+
const clientMessageId = `voice:${input.expectedSessionId}:${input.turnId}`;
|
|
33
|
+
const activeRunId = this.deps.snapshot(input.sessionKey).activeRunId;
|
|
34
|
+
const activeRunCursor = activeRunId ? this.deps.currentSequence(`run:${activeRunId}`) : 0;
|
|
35
|
+
const result = await this.deps.submit({
|
|
36
|
+
sessionKey: input.sessionKey,
|
|
37
|
+
expectedSessionId: input.expectedSessionId,
|
|
38
|
+
clientMessageId,
|
|
39
|
+
delivery: activeRunId ? "steer" : "next",
|
|
40
|
+
content: input.text,
|
|
41
|
+
origin: {
|
|
42
|
+
type: "channel",
|
|
43
|
+
channel: "voice"
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (result.ok === false) throw new Error(`Voice task submission failed: ${result.code}`);
|
|
47
|
+
const deadline = Date.now() + RUN_ASSIGNMENT_TIMEOUT_MS;
|
|
48
|
+
let row = this.deps.find(input.sessionKey, clientMessageId);
|
|
49
|
+
while (!row?.runId && !row?.targetRunId) {
|
|
50
|
+
if (row && [
|
|
51
|
+
"completed",
|
|
52
|
+
"cancelled",
|
|
53
|
+
"failed",
|
|
54
|
+
"interrupted"
|
|
55
|
+
].includes(row.status)) throw new Error(row.error ?? `Voice task ended before assignment: ${row.status}`);
|
|
56
|
+
if (Date.now() >= deadline) throw new Error("Voice task run assignment timed out");
|
|
57
|
+
await sleep(RUN_ASSIGNMENT_POLL_MS, input.signal);
|
|
58
|
+
row = this.deps.find(input.sessionKey, clientMessageId);
|
|
59
|
+
}
|
|
60
|
+
const runId = row.runId ?? row.targetRunId;
|
|
61
|
+
this.runByTask.set(row.id, runId);
|
|
62
|
+
if (this.runByTask.size > MAX_TRACKED_TASKS) this.runByTask.delete(this.runByTask.keys().next().value);
|
|
63
|
+
const afterSeq = result.effectiveDelivery === "steer" && runId === activeRunId ? activeRunCursor : 0;
|
|
64
|
+
return {
|
|
65
|
+
taskId: row.id,
|
|
66
|
+
runId,
|
|
67
|
+
events: this.events(row.id, runId, afterSeq, input.signal)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async cancel(taskId) {
|
|
71
|
+
const runId = this.runByTask.get(taskId);
|
|
72
|
+
if (!runId) return false;
|
|
73
|
+
await this.deps.cancelRun(runId);
|
|
74
|
+
this.runByTask.delete(taskId);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
async *events(taskId, runId, afterSeq, signal) {
|
|
78
|
+
const queued = [];
|
|
79
|
+
let wake;
|
|
80
|
+
const push = (event) => {
|
|
81
|
+
queued.push(event);
|
|
82
|
+
wake?.();
|
|
83
|
+
wake = void 0;
|
|
84
|
+
};
|
|
85
|
+
const subscription = this.deps.subscribe(`run:${runId}`, afterSeq, push);
|
|
86
|
+
subscription.initial.forEach(push);
|
|
87
|
+
const abort = () => {
|
|
88
|
+
wake?.();
|
|
89
|
+
wake = void 0;
|
|
90
|
+
};
|
|
91
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
92
|
+
try {
|
|
93
|
+
while (!signal.aborted) {
|
|
94
|
+
if (!queued.length) await new Promise((resolve) => {
|
|
95
|
+
wake = resolve;
|
|
96
|
+
});
|
|
97
|
+
if (signal.aborted) return;
|
|
98
|
+
const delivery = queued.shift();
|
|
99
|
+
if (!delivery) continue;
|
|
100
|
+
if (delivery.kind === "realtime.gap") throw new Error("Voice task event history is unavailable");
|
|
101
|
+
const event = eventData(delivery);
|
|
102
|
+
if (!event) continue;
|
|
103
|
+
yield event;
|
|
104
|
+
if (event.type === "stream_end") {
|
|
105
|
+
this.runByTask.delete(taskId);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
signal.removeEventListener("abort", abort);
|
|
111
|
+
subscription.unsubscribe();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
//#endregion
|
|
116
|
+
export { DurableVoiceAgentBroker };
|
|
@@ -2,8 +2,9 @@ import { createLogger } from "../../utils/logger/index.js";
|
|
|
2
2
|
import { init_logger } from "../../utils/logger.js";
|
|
3
3
|
import { speakStream } from "../tts/speak-core.js";
|
|
4
4
|
import { AudioPlaybackWindow } from "./audio-playback-window.js";
|
|
5
|
-
import {
|
|
5
|
+
import { TurnCoordinator } from "./turnPolicy.js";
|
|
6
6
|
import { isLikelyPlaybackEcho } from "./playback-echo.js";
|
|
7
|
+
import { PcmFrameBuffer } from "./pcmFrameBuffer.js";
|
|
7
8
|
import { SpeakableSegmenter } from "./speakable-segmenter.js";
|
|
8
9
|
import crypto from "node:crypto";
|
|
9
10
|
//#region src/voice/realtime/agentEngine.ts
|
|
@@ -34,8 +35,24 @@ function createAgentVoiceEngine(options) {
|
|
|
34
35
|
let committing = false;
|
|
35
36
|
const finalizedUtterances = /* @__PURE__ */ new Set();
|
|
36
37
|
let pendingTurn;
|
|
37
|
-
const turn = new
|
|
38
|
+
const turn = new TurnCoordinator(claim.silenceDurationMs, (text, decision, turnId) => {
|
|
38
39
|
if (closed || muted) return;
|
|
40
|
+
log.info({
|
|
41
|
+
sessionId: claim.sessionId,
|
|
42
|
+
turnId,
|
|
43
|
+
disposition: decision.disposition,
|
|
44
|
+
confidence: decision.confidence,
|
|
45
|
+
source: decision.source,
|
|
46
|
+
transcriptCharacters: text.length
|
|
47
|
+
}, "Realtime voice turn committed");
|
|
48
|
+
send("turn.decision", {
|
|
49
|
+
turnId,
|
|
50
|
+
disposition: decision.disposition,
|
|
51
|
+
confidence: decision.confidence,
|
|
52
|
+
source: decision.source,
|
|
53
|
+
committed: true
|
|
54
|
+
});
|
|
55
|
+
send("turn.committed", { turnId });
|
|
39
56
|
if (queuedTurns >= 8) {
|
|
40
57
|
send("session.error", {
|
|
41
58
|
code: "INPUT_BACKPRESSURE",
|
|
@@ -48,13 +65,14 @@ function createAgentVoiceEngine(options) {
|
|
|
48
65
|
queuedTurns += 1;
|
|
49
66
|
const generation = inputGeneration;
|
|
50
67
|
const pending = {
|
|
68
|
+
turnId,
|
|
51
69
|
text,
|
|
52
70
|
cancelled: false
|
|
53
71
|
};
|
|
54
72
|
pendingTurn = pending;
|
|
55
73
|
conversationTail = conversationTail.then(() => {
|
|
56
74
|
if (pendingTurn === pending) pendingTurn = void 0;
|
|
57
|
-
if (generation === inputGeneration && !pending.cancelled) return
|
|
75
|
+
if (generation === inputGeneration && !pending.cancelled) return runAssistantTurn(text, turnId);
|
|
58
76
|
}).finally(() => {
|
|
59
77
|
if (generation === inputGeneration) queuedTurns -= 1;
|
|
60
78
|
});
|
|
@@ -129,11 +147,10 @@ function createAgentVoiceEngine(options) {
|
|
|
129
147
|
}
|
|
130
148
|
});
|
|
131
149
|
}
|
|
132
|
-
for (
|
|
133
|
-
|
|
134
|
-
await response.playback.reserve(chunk.byteLength, response.abortController.signal);
|
|
150
|
+
for (const frame of response.pcm.push(item.value)) {
|
|
151
|
+
await response.playback.reserve(frame.byteLength, response.abortController.signal);
|
|
135
152
|
if (activeResponse !== response || closed) return;
|
|
136
|
-
options.sendAudio(response.id,
|
|
153
|
+
options.sendAudio(response.id, frame);
|
|
137
154
|
}
|
|
138
155
|
}
|
|
139
156
|
} finally {
|
|
@@ -204,7 +221,7 @@ function createAgentVoiceEngine(options) {
|
|
|
204
221
|
if (response.speechWorker) prefetchNextSpeech(response);
|
|
205
222
|
else startSpeechWorker(response);
|
|
206
223
|
}
|
|
207
|
-
async function
|
|
224
|
+
async function runAssistantTurn(text, turnId) {
|
|
208
225
|
if (!claim.tts || !claim.request.sessionKey || !text.trim() || closed) return;
|
|
209
226
|
cancelActiveResponse("barge_in");
|
|
210
227
|
const response = {
|
|
@@ -219,14 +236,28 @@ function createAgentVoiceEngine(options) {
|
|
|
219
236
|
queuedSpeechCharacters: 0,
|
|
220
237
|
startedAt: Date.now(),
|
|
221
238
|
firstTextSeen: false,
|
|
222
|
-
awaitingClarification: false
|
|
239
|
+
awaitingClarification: false,
|
|
240
|
+
pcm: new PcmFrameBuffer()
|
|
223
241
|
};
|
|
224
242
|
activeResponse = response;
|
|
225
243
|
send("response.created", { responseId: response.id });
|
|
226
244
|
try {
|
|
227
245
|
await interruptionWrites;
|
|
228
246
|
if (response.abortController.signal.aborted || closed) return;
|
|
229
|
-
|
|
247
|
+
if (!claim.conversationSessionId) throw new Error("Conversation identity is unavailable");
|
|
248
|
+
const task = await options.runtime.agentBroker.delegate({
|
|
249
|
+
text,
|
|
250
|
+
turnId,
|
|
251
|
+
sessionKey: claim.request.sessionKey,
|
|
252
|
+
expectedSessionId: claim.conversationSessionId,
|
|
253
|
+
signal: response.abortController.signal
|
|
254
|
+
});
|
|
255
|
+
response.taskId = task.taskId;
|
|
256
|
+
send("task.created", {
|
|
257
|
+
responseId: response.id,
|
|
258
|
+
taskId: task.taskId
|
|
259
|
+
});
|
|
260
|
+
for await (const event of task.events) {
|
|
230
261
|
if (activeResponse !== response || response.abortController.signal.aborted) return;
|
|
231
262
|
if (event.type === "assistant_delta" || event.type === "tool_end" && event.payload?.toolName !== "clarify") response.awaitingClarification = false;
|
|
232
263
|
if (event.type === "assistant_delta" && typeof event.payload?.delta === "string") {
|
|
@@ -247,12 +278,19 @@ function createAgentVoiceEngine(options) {
|
|
|
247
278
|
queuePhrases(response, response.segmenter.push(event.payload.delta));
|
|
248
279
|
}
|
|
249
280
|
if (event.type === "tool_start") queuePhrases(response, response.segmenter.flush());
|
|
250
|
-
if ((event.type === "tool_start" || event.type === "tool_end") && typeof event.payload?.toolCallId === "string" && typeof event.payload?.toolName === "string") send("
|
|
251
|
-
|
|
281
|
+
if ((event.type === "tool_start" || event.type === "tool_end") && typeof event.payload?.toolCallId === "string" && typeof event.payload?.toolName === "string") send("task.activity", {
|
|
282
|
+
taskId: task.taskId,
|
|
252
283
|
toolCallId: event.payload.toolCallId.slice(0, 160),
|
|
253
284
|
toolName: event.payload.toolName.slice(0, 256),
|
|
254
285
|
status: event.type === "tool_start" ? "running" : event.payload.status === "error" ? "failed" : "completed"
|
|
255
286
|
});
|
|
287
|
+
if (event.type === "stream_end") {
|
|
288
|
+
const status = event.payload?.status;
|
|
289
|
+
send("task.done", {
|
|
290
|
+
taskId: task.taskId,
|
|
291
|
+
status: status === "cancelled" ? "cancelled" : status === "suspended" ? "suspended" : status === "error" ? "failed" : "completed"
|
|
292
|
+
});
|
|
293
|
+
}
|
|
256
294
|
if (event.type === "clarify_request" && typeof event.payload?.requestId === "string" && typeof event.payload?.question === "string") {
|
|
257
295
|
response.awaitingClarification = true;
|
|
258
296
|
send("response.clarification", {
|
|
@@ -274,6 +312,12 @@ function createAgentVoiceEngine(options) {
|
|
|
274
312
|
send("response.text.done", { responseId: response.id });
|
|
275
313
|
await waitForSpeech(response);
|
|
276
314
|
if (response.speechError) throw response.speechError;
|
|
315
|
+
const finalFrame = response.pcm.finish();
|
|
316
|
+
if (finalFrame) {
|
|
317
|
+
await response.playback.reserve(finalFrame.byteLength, response.abortController.signal);
|
|
318
|
+
if (activeResponse !== response || closed) return;
|
|
319
|
+
options.sendAudio(response.id, finalFrame);
|
|
320
|
+
}
|
|
277
321
|
await response.playback.drain(response.abortController.signal);
|
|
278
322
|
if (activeResponse !== response || response.abortController.signal.aborted) return;
|
|
279
323
|
if (response.audioStarted) send("response.audio.done", { responseId: response.id });
|
|
@@ -367,7 +411,7 @@ function createAgentVoiceEngine(options) {
|
|
|
367
411
|
if (claim.request.purpose === "conversation") {
|
|
368
412
|
if (pendingTurn) {
|
|
369
413
|
pendingTurn.cancelled = true;
|
|
370
|
-
turn.restore(pendingTurn.text);
|
|
414
|
+
turn.restore(pendingTurn.turnId, pendingTurn.text);
|
|
371
415
|
pendingTurn = void 0;
|
|
372
416
|
}
|
|
373
417
|
turn.start(event.utteranceId);
|
|
@@ -500,8 +544,11 @@ function createAgentVoiceEngine(options) {
|
|
|
500
544
|
});
|
|
501
545
|
return cancelActiveResponse(reason);
|
|
502
546
|
},
|
|
503
|
-
|
|
504
|
-
|
|
547
|
+
cancelTask(taskId) {
|
|
548
|
+
return options.runtime.agentBroker.cancel(taskId);
|
|
549
|
+
},
|
|
550
|
+
acknowledge(responseId, playedDurationMs) {
|
|
551
|
+
if (activeResponse?.id === responseId) activeResponse.playback.acknowledge(playedDurationMs * 48);
|
|
505
552
|
},
|
|
506
553
|
close() {
|
|
507
554
|
if (closing) return closing;
|
|
@@ -8,6 +8,7 @@ export interface VoiceEngine {
|
|
|
8
8
|
setInputMuted(muted: boolean): void | Promise<void>;
|
|
9
9
|
commit(): Promise<void>;
|
|
10
10
|
cancel(responseId: string, reason: 'client_cancelled' | 'barge_in'): boolean;
|
|
11
|
-
|
|
11
|
+
cancelTask(taskId: string): Promise<boolean>;
|
|
12
|
+
acknowledge(responseId: string, playedDurationMs: number): void;
|
|
12
13
|
close(): void | Promise<void>;
|
|
13
14
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createLogger } from "../../utils/logger/index.js";
|
|
2
2
|
import { init_logger } from "../../utils/logger.js";
|
|
3
3
|
import { AudioPlaybackWindow } from "./audio-playback-window.js";
|
|
4
|
-
import {
|
|
4
|
+
import { TurnCoordinator } from "./turnPolicy.js";
|
|
5
|
+
import { PcmFrameBuffer } from "./pcmFrameBuffer.js";
|
|
5
6
|
import crypto from "node:crypto";
|
|
6
7
|
import WebSocket from "ws";
|
|
7
8
|
//#region src/voice/realtime/omniEngine.ts
|
|
@@ -50,8 +51,16 @@ function createOmniVoiceEngine(options) {
|
|
|
50
51
|
let uploadTimer;
|
|
51
52
|
let turnSettled = false;
|
|
52
53
|
const speaking = /* @__PURE__ */ new Set();
|
|
53
|
-
const turn = new
|
|
54
|
+
const turn = new TurnCoordinator(options.silenceDurationMs, (_text, decision, turnId) => {
|
|
54
55
|
if (closed || failed || muted || inputBlocked) return;
|
|
56
|
+
options.send("turn.decision", {
|
|
57
|
+
turnId,
|
|
58
|
+
disposition: decision.disposition,
|
|
59
|
+
confidence: decision.confidence,
|
|
60
|
+
source: decision.source,
|
|
61
|
+
committed: true
|
|
62
|
+
});
|
|
63
|
+
options.send("turn.committed", { turnId });
|
|
55
64
|
turnSettled = true;
|
|
56
65
|
if (active) publish(active);
|
|
57
66
|
});
|
|
@@ -146,6 +155,23 @@ function createOmniVoiceEngine(options) {
|
|
|
146
155
|
options.send("response.text.done", { responseId: response.id });
|
|
147
156
|
await response.tail;
|
|
148
157
|
if (active !== response || closed) return;
|
|
158
|
+
const finalFrame = response.pcm.finish();
|
|
159
|
+
if (finalFrame) {
|
|
160
|
+
await response.playback.reserve(finalFrame.byteLength, response.abort.signal);
|
|
161
|
+
if (active !== response || closed) return;
|
|
162
|
+
if (!response.audio) {
|
|
163
|
+
response.audio = true;
|
|
164
|
+
options.send("response.audio.started", {
|
|
165
|
+
responseId: response.id,
|
|
166
|
+
format: {
|
|
167
|
+
encoding: "pcm_s16le",
|
|
168
|
+
channels: 1,
|
|
169
|
+
sampleRate: 24e3
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
options.sendAudio(response.id, finalFrame);
|
|
174
|
+
}
|
|
149
175
|
await response.playback.drain(response.abort.signal);
|
|
150
176
|
if (active !== response || closed) return;
|
|
151
177
|
save({
|
|
@@ -361,7 +387,8 @@ function createOmniVoiceEngine(options) {
|
|
|
361
387
|
tail: Promise.resolve(),
|
|
362
388
|
published: false,
|
|
363
389
|
settled,
|
|
364
|
-
release
|
|
390
|
+
release,
|
|
391
|
+
pcm: new PcmFrameBuffer()
|
|
365
392
|
};
|
|
366
393
|
if (turnSettled) publish(active);
|
|
367
394
|
} else if (active && event.response_id === active.id && event.type === "response.audio_transcript.delta") {
|
|
@@ -392,9 +419,8 @@ function createOmniVoiceEngine(options) {
|
|
|
392
419
|
response.queuedBytes += audio.length;
|
|
393
420
|
response.tail = response.tail.then(async () => {
|
|
394
421
|
await response.settled;
|
|
395
|
-
for (
|
|
422
|
+
for (const chunk of response.pcm.push(audio)) {
|
|
396
423
|
if (closed || active !== response) return;
|
|
397
|
-
const chunk = audio.subarray(offset, offset + 24e3);
|
|
398
424
|
await response.playback.reserve(chunk.length, response.abort.signal);
|
|
399
425
|
if (closed || active !== response) return;
|
|
400
426
|
if (!response.audio) {
|
|
@@ -471,8 +497,11 @@ function createOmniVoiceEngine(options) {
|
|
|
471
497
|
if (reason === "client_cancelled") discardInput();
|
|
472
498
|
return cancel(reason);
|
|
473
499
|
},
|
|
474
|
-
|
|
475
|
-
|
|
500
|
+
async cancelTask() {
|
|
501
|
+
return false;
|
|
502
|
+
},
|
|
503
|
+
acknowledge(responseId, playedDurationMs) {
|
|
504
|
+
if (active?.id === responseId) active.playback.acknowledge(playedDurationMs * 48);
|
|
476
505
|
},
|
|
477
506
|
close() {
|
|
478
507
|
if (closed) return writes;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/voice/realtime/pcmFrameBuffer.ts
|
|
2
|
+
const PCM_24KHZ_20MS_BYTES = 960;
|
|
3
|
+
/** Converts arbitrary PCM16 chunks into exact 20 ms wire frames. */
|
|
4
|
+
var PcmFrameBuffer = class {
|
|
5
|
+
remainder = new Uint8Array();
|
|
6
|
+
push(bytes) {
|
|
7
|
+
if (!bytes.byteLength || bytes.byteLength % 2) throw new Error("PCM16 audio must contain complete samples");
|
|
8
|
+
const pending = new Uint8Array(this.remainder.byteLength + bytes.byteLength);
|
|
9
|
+
pending.set(this.remainder);
|
|
10
|
+
pending.set(bytes, this.remainder.byteLength);
|
|
11
|
+
const frames = [];
|
|
12
|
+
let offset = 0;
|
|
13
|
+
while (offset + PCM_24KHZ_20MS_BYTES <= pending.byteLength) {
|
|
14
|
+
frames.push(pending.slice(offset, offset + PCM_24KHZ_20MS_BYTES));
|
|
15
|
+
offset += PCM_24KHZ_20MS_BYTES;
|
|
16
|
+
}
|
|
17
|
+
this.remainder = pending.slice(offset);
|
|
18
|
+
return frames;
|
|
19
|
+
}
|
|
20
|
+
finish() {
|
|
21
|
+
if (!this.remainder.byteLength) return void 0;
|
|
22
|
+
const frame = new Uint8Array(PCM_24KHZ_20MS_BYTES);
|
|
23
|
+
frame.set(this.remainder);
|
|
24
|
+
this.remainder = new Uint8Array();
|
|
25
|
+
return frame;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { PcmFrameBuffer };
|
|
@@ -10,7 +10,7 @@ import { resolveSTTProviderChain } from "../stt/factory.js";
|
|
|
10
10
|
import { mergeSttConfigFromAppConfig } from "../stt/config.js";
|
|
11
11
|
import { voiceConversationInstructions } from "./conversation-context.js";
|
|
12
12
|
import { createPreauthConnectionBudget } from "../../gateway/security/preauth-connection-budget.js";
|
|
13
|
-
import { encodeVoiceAudioFrame } from "../../../packages/realtime-protocol/src/voice-audio.js";
|
|
13
|
+
import { decodeVoiceUplinkAudioFrame, encodeVoiceAudioFrame } from "../../../packages/realtime-protocol/src/voice-audio.js";
|
|
14
14
|
import { VOICE_REALTIME_HEARTBEAT_INTERVAL_MS, VOICE_REALTIME_MAX_BINARY_FRAME_BYTES, VOICE_REALTIME_START_TIMEOUT_MS, VOICE_REALTIME_WS_PATH, parseVoiceClientJsonFrame, parseVoiceClientMessage } from "../../../packages/realtime-protocol/src/voice.js";
|
|
15
15
|
import { createAgentVoiceEngine } from "./agentEngine.js";
|
|
16
16
|
import { resolveOmniRoute } from "./omniRoute.js";
|
|
@@ -157,27 +157,28 @@ var VoiceRealtimeRuntime = class {
|
|
|
157
157
|
const config = structuredClone(this.options.getConfig());
|
|
158
158
|
this.pruneTickets(now);
|
|
159
159
|
if (!config.voice?.realtime?.enabled) throw new VoiceSessionCreationError("VOICE_DISABLED", "Realtime voice is disabled", 503);
|
|
160
|
+
const defaultMode = config.voice.realtime.defaultEngine === "omni" ? "natural" : "assistant";
|
|
160
161
|
request = request.purpose === "conversation" ? {
|
|
161
162
|
...request,
|
|
162
|
-
|
|
163
|
+
mode: request.mode ?? defaultMode
|
|
163
164
|
} : request;
|
|
164
165
|
let omni;
|
|
165
|
-
if (request.
|
|
166
|
+
if (request.mode === "natural") try {
|
|
166
167
|
omni = await resolveOmniRoute(config);
|
|
167
168
|
} catch {
|
|
168
169
|
throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", "Natural conversation is unavailable. Check its model, endpoint and credentials.", 503);
|
|
169
170
|
}
|
|
170
171
|
if (omni && (!this.options.recordOmniTranscript || !this.options.getConversationContext)) throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", "Natural conversation storage is unavailable", 503);
|
|
171
172
|
const conversationSessionId = request.purpose === "conversation" && request.sessionKey ? await this.options.getSessionIdentity?.(request.sessionKey) : void 0;
|
|
172
|
-
if (
|
|
173
|
+
if (request.purpose === "conversation" && !conversationSessionId) throw new VoiceSessionCreationError("SESSION_NOT_FOUND", "Conversation session was not found", 404);
|
|
173
174
|
const stt = omni ? void 0 : resolveStreamingStt(config, request.language);
|
|
174
175
|
if (!omni && !stt) throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", "No configured streaming speech-to-text provider is available", 503);
|
|
175
176
|
const inputMode = "server_vad";
|
|
176
177
|
if (stt && !stt.plugin.streamingAudio.turnDetection.includes(inputMode)) throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", `Streaming speech-to-text provider does not support ${inputMode}`, 503);
|
|
177
|
-
const tts = request.
|
|
178
|
+
const tts = request.mode === "assistant" ? resolveStreamingTts(config) : void 0;
|
|
178
179
|
if (request.purpose === "conversation") {
|
|
179
180
|
if (!request.sessionKey || !await this.options.sessionExists(request.sessionKey)) throw new VoiceSessionCreationError("SESSION_NOT_FOUND", "Conversation session was not found", 404);
|
|
180
|
-
if (this.options.sessionBusy(request.sessionKey)) throw new VoiceSessionCreationError("SESSION_CONFLICT", "Conversation session already has an active response", 409);
|
|
181
|
+
if (request.mode === "natural" && this.options.sessionBusy(request.sessionKey)) throw new VoiceSessionCreationError("SESSION_CONFLICT", "Conversation session already has an active response", 409);
|
|
181
182
|
if (this.conversationReservations.has(request.sessionKey)) throw new VoiceSessionCreationError("SESSION_CONFLICT", "Conversation session already has a voice connection", 409);
|
|
182
183
|
if (!omni && !tts) throw new VoiceSessionCreationError("PROVIDER_UNAVAILABLE", "No configured PCM streaming text-to-speech provider is available", 503);
|
|
183
184
|
}
|
|
@@ -200,11 +201,14 @@ var VoiceRealtimeRuntime = class {
|
|
|
200
201
|
request = resolvedRequest;
|
|
201
202
|
const sessionId = crypto.randomUUID();
|
|
202
203
|
const ticket = crypto.randomBytes(32).toString("base64url");
|
|
204
|
+
const connectionEpoch = crypto.randomInt(1, 4294967296);
|
|
203
205
|
const maxSessionMs = request.purpose === "conversation" ? Math.min(config.voice.realtime.maxConversationMs, omni?.route.managed ? 30 * 6e4 : Number.MAX_SAFE_INTEGER) : config.voice.realtime.maxDictationMs;
|
|
204
206
|
const claim = {
|
|
205
207
|
conversationSessionId,
|
|
206
208
|
sessionId,
|
|
209
|
+
connectionEpoch,
|
|
207
210
|
principalId,
|
|
211
|
+
...request.purpose === "conversation" && request.mode ? { mode: request.mode } : {},
|
|
208
212
|
request,
|
|
209
213
|
inputMode,
|
|
210
214
|
idleTimeoutMs: config.voice.realtime.idleTimeoutMs,
|
|
@@ -224,8 +228,10 @@ var VoiceRealtimeRuntime = class {
|
|
|
224
228
|
ticket,
|
|
225
229
|
ticketExpiresAt: new Date(claim.expiresAt).toISOString(),
|
|
226
230
|
websocketPath: VOICE_REALTIME_WS_PATH,
|
|
227
|
-
protocolVersion:
|
|
231
|
+
protocolVersion: 3,
|
|
232
|
+
connectionEpoch,
|
|
228
233
|
purpose: request.purpose,
|
|
234
|
+
...request.purpose === "conversation" && request.mode ? { mode: request.mode } : {},
|
|
229
235
|
inputMode,
|
|
230
236
|
bargeIn: config.voice.realtime.bargeIn,
|
|
231
237
|
inputFormat: {
|
|
@@ -233,6 +239,11 @@ var VoiceRealtimeRuntime = class {
|
|
|
233
239
|
sampleRate: 16e3,
|
|
234
240
|
channels: 1
|
|
235
241
|
},
|
|
242
|
+
media: {
|
|
243
|
+
transport: "websocket-pcm",
|
|
244
|
+
codec: "pcm_s16le",
|
|
245
|
+
frameDurationMs: 20
|
|
246
|
+
},
|
|
236
247
|
limits: {
|
|
237
248
|
maxBinaryFrameBytes: VOICE_REALTIME_MAX_BINARY_FRAME_BYTES,
|
|
238
249
|
maxSessionMs,
|
|
@@ -252,7 +263,7 @@ var VoiceRealtimeRuntime = class {
|
|
|
252
263
|
};
|
|
253
264
|
}
|
|
254
265
|
handleUpgrade(req, socket, head) {
|
|
255
|
-
if (new URL(req.url ?? "/", "http://gateway.local").pathname !== "/api/voice/realtime/
|
|
266
|
+
if (new URL(req.url ?? "/", "http://gateway.local").pathname !== "/api/voice/realtime/v3/ws") return false;
|
|
256
267
|
if (this.closed || this.wss.clients.size >= MAX_CONNECTIONS) {
|
|
257
268
|
socket.destroy();
|
|
258
269
|
return true;
|
|
@@ -319,6 +330,8 @@ var VoiceRealtimeRuntime = class {
|
|
|
319
330
|
let ready = false;
|
|
320
331
|
let seq = 0;
|
|
321
332
|
let audioSeq = 0;
|
|
333
|
+
let outputMediaTimestampMs = 0;
|
|
334
|
+
const inputSequences = /* @__PURE__ */ new Map();
|
|
322
335
|
let closed = false;
|
|
323
336
|
let lastActivityAt = Date.now();
|
|
324
337
|
const abortController = new AbortController();
|
|
@@ -326,10 +339,11 @@ var VoiceRealtimeRuntime = class {
|
|
|
326
339
|
let context;
|
|
327
340
|
let unsubscribeMemory;
|
|
328
341
|
let unsubscribeSession;
|
|
342
|
+
let inputMutationTail = Promise.resolve();
|
|
329
343
|
const send = (type, payload) => {
|
|
330
344
|
if (!claim || socket.readyState !== WebSocketState.OPEN) return;
|
|
331
345
|
const event = {
|
|
332
|
-
protocolVersion:
|
|
346
|
+
protocolVersion: 3,
|
|
333
347
|
eventId: crypto.randomUUID(),
|
|
334
348
|
seq: ++seq,
|
|
335
349
|
type,
|
|
@@ -434,11 +448,20 @@ var VoiceRealtimeRuntime = class {
|
|
|
434
448
|
setupStage = "engine";
|
|
435
449
|
const sendAudio = (responseId, bytes) => {
|
|
436
450
|
if (socket.bufferedAmount > 1024 * 1024) throw new Error("Voice client audio backpressure limit exceeded");
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
audio
|
|
441
|
-
|
|
451
|
+
if (closed || socket.readyState !== WebSocketState.OPEN) return;
|
|
452
|
+
for (let offset = 0; offset < bytes.byteLength; offset += 960) {
|
|
453
|
+
const audio = bytes.subarray(offset, offset + 960);
|
|
454
|
+
if (audio.byteLength !== 960) throw new Error("Realtime output must align to 20 ms PCM frames");
|
|
455
|
+
socket.send(encodeVoiceAudioFrame({
|
|
456
|
+
connectionEpoch: consumed.connectionEpoch,
|
|
457
|
+
responseId,
|
|
458
|
+
seq: ++audioSeq,
|
|
459
|
+
mediaTimestampMs: outputMediaTimestampMs,
|
|
460
|
+
durationMs: 20,
|
|
461
|
+
audio
|
|
462
|
+
}), { binary: true });
|
|
463
|
+
outputMediaTimestampMs += 20;
|
|
464
|
+
}
|
|
442
465
|
};
|
|
443
466
|
engine = consumed.omni ? createOmniVoiceEngine({
|
|
444
467
|
callId: consumed.sessionId,
|
|
@@ -469,12 +492,19 @@ var VoiceRealtimeRuntime = class {
|
|
|
469
492
|
}, "Realtime voice session ready");
|
|
470
493
|
send("session.ready", {
|
|
471
494
|
purpose: consumed.request.purpose,
|
|
495
|
+
...consumed.mode ? { mode: consumed.mode } : {},
|
|
496
|
+
connectionEpoch: consumed.connectionEpoch,
|
|
472
497
|
inputMode: consumed.inputMode,
|
|
473
498
|
inputFormat: {
|
|
474
499
|
encoding: "pcm_s16le",
|
|
475
500
|
sampleRate: 16e3,
|
|
476
501
|
channels: 1
|
|
477
502
|
},
|
|
503
|
+
media: {
|
|
504
|
+
transport: "websocket-pcm",
|
|
505
|
+
codec: "pcm_s16le",
|
|
506
|
+
frameDurationMs: 20
|
|
507
|
+
},
|
|
478
508
|
route: consumed.omni ? {
|
|
479
509
|
engine: "omni",
|
|
480
510
|
omni: consumed.omni.route
|
|
@@ -512,7 +542,19 @@ var VoiceRealtimeRuntime = class {
|
|
|
512
542
|
return;
|
|
513
543
|
}
|
|
514
544
|
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : Array.isArray(data) ? Buffer.concat(data) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
515
|
-
|
|
545
|
+
let frame;
|
|
546
|
+
try {
|
|
547
|
+
frame = decodeVoiceUplinkAudioFrame(bytes);
|
|
548
|
+
} catch {
|
|
549
|
+
send("session.error", {
|
|
550
|
+
code: "INVALID_AUDIO",
|
|
551
|
+
message: "Invalid PCM audio frame",
|
|
552
|
+
recoverable: false
|
|
553
|
+
});
|
|
554
|
+
shutdown("invalid_audio", true);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (frame.connectionEpoch !== claim.connectionEpoch || frame.audio.byteLength !== 640) {
|
|
516
558
|
send("session.error", {
|
|
517
559
|
code: "INVALID_AUDIO",
|
|
518
560
|
message: "Invalid PCM audio frame",
|
|
@@ -521,12 +563,24 @@ var VoiceRealtimeRuntime = class {
|
|
|
521
563
|
shutdown("invalid_audio", true);
|
|
522
564
|
return;
|
|
523
565
|
}
|
|
566
|
+
const previous = inputSequences.get(frame.utteranceId) ?? 0;
|
|
567
|
+
if (frame.audioSeq <= previous) return;
|
|
568
|
+
if (frame.audioSeq !== previous + 1 || inputSequences.size >= 256) {
|
|
569
|
+
send("session.error", {
|
|
570
|
+
code: "INVALID_AUDIO",
|
|
571
|
+
message: "Invalid audio sequence",
|
|
572
|
+
recoverable: false
|
|
573
|
+
});
|
|
574
|
+
shutdown("invalid_audio", true);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
inputSequences.set(frame.utteranceId, frame.audioSeq);
|
|
524
578
|
if (!ready || !engine) {
|
|
525
579
|
socket.close(4401, "Voice session is not ready");
|
|
526
580
|
return;
|
|
527
581
|
}
|
|
528
582
|
try {
|
|
529
|
-
engine.appendAudio(
|
|
583
|
+
engine.appendAudio(frame.audio);
|
|
530
584
|
} catch (error) {
|
|
531
585
|
log.warn({
|
|
532
586
|
err: error,
|
|
@@ -574,23 +628,45 @@ var VoiceRealtimeRuntime = class {
|
|
|
574
628
|
metric,
|
|
575
629
|
durationMs,
|
|
576
630
|
source: "client",
|
|
577
|
-
|
|
631
|
+
mode: claim.mode
|
|
578
632
|
}, "Voice client timing sample");
|
|
579
633
|
}
|
|
580
|
-
} else if (message.type === "response.
|
|
634
|
+
} else if (message.type === "response.stop_playback") {
|
|
581
635
|
if (!engine?.cancel(message.payload.responseId, "client_cancelled")) send("session.error", {
|
|
582
636
|
code: "NO_ACTIVE_RESPONSE",
|
|
583
637
|
message: "No matching response is active",
|
|
584
638
|
recoverable: true
|
|
585
639
|
});
|
|
586
|
-
} else if (message.type === "
|
|
587
|
-
|
|
640
|
+
} else if (message.type === "task.cancel") engine?.cancelTask(message.payload.taskId).then((cancelled) => {
|
|
641
|
+
if (!cancelled) send("session.error", {
|
|
642
|
+
code: "NO_ACTIVE_TASK",
|
|
643
|
+
message: "No matching task is active",
|
|
644
|
+
recoverable: true
|
|
645
|
+
});
|
|
646
|
+
}).catch((error) => {
|
|
647
|
+
log.warn({
|
|
648
|
+
err: error,
|
|
649
|
+
sessionId: claim?.sessionId,
|
|
650
|
+
taskId: message.payload.taskId
|
|
651
|
+
}, "Voice task cancellation failed");
|
|
652
|
+
send("session.error", {
|
|
653
|
+
code: "TASK_CANCEL_FAILED",
|
|
654
|
+
message: "Could not cancel the task",
|
|
655
|
+
recoverable: true
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
else if (message.type === "response.audio.played") try {
|
|
659
|
+
engine?.acknowledge(message.payload.responseId, message.payload.playedDurationMs);
|
|
588
660
|
} catch {
|
|
589
661
|
socket.close(4400, "Invalid playback acknowledgement");
|
|
590
662
|
}
|
|
591
663
|
else if (message.type === "input.mute") {
|
|
592
664
|
if (!ready || !engine || claim.request.purpose !== "conversation") return;
|
|
593
|
-
|
|
665
|
+
const muted = message.payload.muted;
|
|
666
|
+
inputMutationTail = inputMutationTail.then(async () => {
|
|
667
|
+
if (closed || !ready || !engine) return;
|
|
668
|
+
await engine.setInputMuted(muted);
|
|
669
|
+
}).catch((error) => {
|
|
594
670
|
log.warn({
|
|
595
671
|
err: error,
|
|
596
672
|
sessionId: claim?.sessionId
|