@xopcai/xopc 0.0.264 → 0.0.266
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-Qrp5ALCF.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-BZvMqNrU.js → agent-defaults-settings-panel-DwinST48.js} +1 -1
- package/dist/gateway/static/root/assets/{agents-6foBELwt.js → agents-Dlr0YyNt.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-bLNvz93c.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-C7nBHnkG.js → appearance-settings-D5qgIepF.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-B_g-rL5b.js → apps-page-BQVlXhIb.js} +1 -1
- package/dist/gateway/static/root/assets/{archive-plugin-DfKK1iJO.js → archive-plugin-BEA7VAA4.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-BOU1pdnQ.js → attachment-load-B1RyF3uu.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-BePzexAH.js → automations-page-BfH0FdKa.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-workspace-7-m5pBEZ.js → automations-workspace-C37AlD3E.js} +1 -1
- package/dist/gateway/static/root/assets/{binary-plugins-Cly8XDyt.js → binary-plugins-EU884ZXi.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-DzSu62js.js → block-editor-Bjr3naHb.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-CDrYrNGy.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-settings-page-Dum3mTiz.js → browser-settings-page-DOmUkj2k.js} +1 -1
- package/dist/gateway/static/root/assets/{capabilities-settings-panel-Ci6qVS-O.js → capabilities-settings-panel-slt1o04n.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-settings-BOGEZnSv.js → channels-settings-oHTTX6Jy.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-DEYQdv6n.js} +1 -1
- package/dist/gateway/static/root/assets/{connectors-page-DBvEvupa.js → connectors-page-olrbxHIT.js} +1 -1
- package/dist/gateway/static/root/assets/{cron-expression-BszT_gjK.js → cron-expression-DFrbDWpP.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-D2MKBJ0G.js → date-picker-CYriYKpH.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-DqigBx-G.js → dependency-picker-XY3qUErs.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-CapspBGG.js → desktop-pet-Dsc__ynA.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-DMPk4CaK.js → desktop-pet-settings-EpuX3C08.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-CXLsDtrW.js → extension-debug-page-B_NeLV4K.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-B2j87ROw.js → extension-page-D3JC6mLB.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-t1BApw-u.js → extension-settings-page-CXTQt1lp.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-yeYm_UPE.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-coh1LFsg.js} +1 -1
- package/dist/gateway/static/root/assets/{home-page-uItZMiBE.js → home-page-CAgoBZqp.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-Cn__9Un2.js +74 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-Q_CM-ZCs.js → keyboard-shortcuts-settings-Cu1bI3Bg.js} +1 -1
- package/dist/gateway/static/root/assets/{local-app-workbench-page-DaiBWfOt.js → local-app-workbench-page-C9s2lFaI.js} +1 -1
- package/dist/gateway/static/root/assets/{local-apps-page-BALMNCSp.js → local-apps-page-CJ03Umjr.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-CY_DQKJ2.js} +1 -1
- package/dist/gateway/static/root/assets/{management-settings-BajkJeYl.js → management-settings-YPEemeMo.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-BjT6jlrA.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-A8eLtV6v.js} +1 -1
- package/dist/gateway/static/root/assets/{note-detail-panel-CCbPg_GP.js → note-detail-panel-DHT2QksJ.js} +3 -3
- package/dist/gateway/static/root/assets/{notes-page-BdxxcUKa.js → notes-page-BRFQiivM.js} +1 -1
- package/dist/gateway/static/root/assets/{page-tabs-BMYQef5D.js → page-tabs-DNUwulnl.js} +1 -1
- package/dist/gateway/static/root/assets/{preview-open-alternatives-CKZ-H5hJ.js → preview-open-alternatives-B33KsKkJ.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-CItEFVt9.js} +1 -1
- package/dist/gateway/static/root/assets/{projects-page-Wt7gF3Sx.js → projects-page-Bmy-c_lL.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-H6B1cA2V.js} +1 -1
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-DcUB5h1f.js → runtime-tools-settings-panel-BsCovC39.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-CquWhe7r.js → schema-form-NsPb_Vam.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-CyYLSXk_.js} +1 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-3T4rPk4A.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-DwswAefc.js → settings-form-section-BzPIPe1-.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-CoAVmjjs.js → settings-page-4idpMhsw.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-C9EIcWtH.js → setup-status-panel-D2ds_TTD.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-DWwy_4zh.js → share-preview-page-Djo3VTaC.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-settings-CIUvq27-.js → shares-settings-fhRzNfRp.js} +1 -1
- package/dist/gateway/static/root/assets/skill-reload-api-Ct8tUju_.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-CqsgVtVh.js → skills-page-riM7FytZ.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-B-N73248.js → skills-page.utils-B3G8LBb-.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-BndjXuBZ.js → system-settings-panel-CW91jjnB.js} +1 -1
- package/dist/gateway/static/root/assets/{task-detail-page-CG6Ha9t7.js → task-detail-page-BBXKvl0k.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-DILER0Yb.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-mj9CUuz1.js → use-autosave-GxnP8vLO.js} +1 -1
- package/dist/gateway/static/root/assets/{user-model-page-DKeHMXI4.js → user-model-page-I1H4XRfA.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-CzxVMYlr.js → voice-api-key-field-CI3sXyl4.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-DZC9Exn3.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-C82OXCJK.js → workflow-run-setup-panel-CaBaQER2.js} +1 -1
- package/dist/gateway/static/root/assets/{workflows-page-HLCUV3rQ.js → workflows-page-C-_Dxihw.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/agent-manager.js +4 -4
- package/dist/src/agent/background-review/run-background-review.js +1 -1
- package/dist/src/agent/embedded/model-runtime.js +1 -1
- package/dist/src/agent/embedded/run-for-session.js +1 -1
- package/dist/src/agent/image/tool-model-config.js +1 -1
- package/dist/src/agent/mcp/oauth/mcp-oauth-store.js +1 -1
- package/dist/src/agent/service.js +2 -2
- package/dist/src/agent/session/session-inspector.js +1 -1
- package/dist/src/agent/skills/hub-lock.js +1 -1
- 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/tasks/task-judge-service.js +3 -3
- package/dist/src/agent/tools/factory.js +9 -1
- package/dist/src/ai-assist/text-assist.js +1 -1
- package/dist/src/auth/credentials.js +2 -2
- package/dist/src/auth/oauth/xopc-cloud.js +1 -1
- package/dist/src/auth/profiles/store.js +1 -1
- package/dist/src/auth/sync-provider-auth.js +2 -2
- 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/cli/commands/doctor/checks/provider-auth.js +1 -1
- package/dist/src/cli/commands/models.js +1 -1
- package/dist/src/config/index.js +4 -4
- package/dist/src/config/loader.js +2 -2
- package/dist/src/config/models-json.js +1 -1
- package/dist/src/endpoint-tools/runtime.js +2 -0
- package/dist/src/extensions/lockfile.js +1 -1
- package/dist/src/gateway/agents-admin.js +1 -1
- package/dist/src/gateway/chat-stream/mapper.js +3 -2
- package/dist/src/gateway/file-path-classifier.js +1 -1
- package/dist/src/gateway/hono/app.js +19 -3
- package/dist/src/gateway/hono/lib/config-payload.js +1 -1
- 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/models.js +1 -1
- 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/config-coordinator.js +1 -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 +22 -21
- package/dist/src/providers/auth-runtime/resolve-auth.js +1 -1
- package/dist/src/providers/index.js +2 -2
- package/dist/src/providers/model-registry.js +1 -1
- package/dist/src/providers/register-runtime-providers.js +1 -1
- 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/tui/tui-startup-resources.js +1 -1
- package/dist/src/tui/tui.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/dist/src/work-discovery/service.js +1 -1
- package/dist/src/workflows/service/workflow-run-service.js +1 -1
- 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
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateVoiceSessionRequest, VoiceProviderRoute } from '@xopcai/realtime-protocol/voice';
|
|
1
|
+
import type { CreateVoiceSessionRequest, VoiceMode, VoiceProviderRoute } from '@xopcai/realtime-protocol/voice';
|
|
2
2
|
import type { Config } from '../../config/schema.js';
|
|
3
3
|
import type { MediaUnderstandingProvider } from '../../media-understanding/types.js';
|
|
4
4
|
import type { ResolvedSpeechProvider } from '../tts/factory.js';
|
|
@@ -6,6 +6,7 @@ import type { TTSConfig } from '../tts/types.js';
|
|
|
6
6
|
import type { VoiceConversationContext } from './conversation-context.js';
|
|
7
7
|
import type { OmniTranscript } from './omniEngine.js';
|
|
8
8
|
import type { OmniRoute } from './omniRoute.js';
|
|
9
|
+
import type { VoiceAgentBroker } from './agentBroker.js';
|
|
9
10
|
export interface ResolvedStreamingStt {
|
|
10
11
|
plugin: MediaUnderstandingProvider & Required<Pick<MediaUnderstandingProvider, 'openAudioStream' | 'streamingAudio'>>;
|
|
11
12
|
model: string;
|
|
@@ -18,7 +19,9 @@ export interface ResolvedStreamingStt {
|
|
|
18
19
|
}
|
|
19
20
|
export interface VoiceTicketClaim {
|
|
20
21
|
sessionId: string;
|
|
22
|
+
connectionEpoch: number;
|
|
21
23
|
principalId: string;
|
|
24
|
+
mode?: VoiceMode;
|
|
22
25
|
request: CreateVoiceSessionRequest;
|
|
23
26
|
inputMode: 'server_vad';
|
|
24
27
|
idleTimeoutMs: number;
|
|
@@ -37,24 +40,6 @@ export interface ResolvedStreamingTts {
|
|
|
37
40
|
config: TTSConfig;
|
|
38
41
|
route: VoiceProviderRoute;
|
|
39
42
|
}
|
|
40
|
-
interface VoiceAgentEvent {
|
|
41
|
-
type: string;
|
|
42
|
-
payload?: {
|
|
43
|
-
delta?: unknown;
|
|
44
|
-
message?: unknown;
|
|
45
|
-
status?: unknown;
|
|
46
|
-
toolCallId?: unknown;
|
|
47
|
-
toolName?: unknown;
|
|
48
|
-
requestId?: unknown;
|
|
49
|
-
kind?: unknown;
|
|
50
|
-
question?: unknown;
|
|
51
|
-
choices?: unknown;
|
|
52
|
-
suggestedAnswer?: unknown;
|
|
53
|
-
version?: unknown;
|
|
54
|
-
createdAt?: unknown;
|
|
55
|
-
expiresAt?: unknown;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
43
|
export interface VoiceRealtimeRuntimeOptions {
|
|
59
44
|
recordOmniTranscript?: (sessionKey: string, callId: string, entry: OmniTranscript, expectedSessionId: string) => Promise<void>;
|
|
60
45
|
getConversationContext?: (sessionKey: string, expectedSessionId: string) => Promise<VoiceConversationContext>;
|
|
@@ -62,7 +47,7 @@ export interface VoiceRealtimeRuntimeOptions {
|
|
|
62
47
|
getConfig: () => Config;
|
|
63
48
|
sessionExists: (sessionKey: string) => Promise<boolean>;
|
|
64
49
|
sessionBusy: (sessionKey: string) => boolean;
|
|
65
|
-
|
|
50
|
+
agentBroker: VoiceAgentBroker;
|
|
66
51
|
recordInterruption: (entry: {
|
|
67
52
|
sessionKey: string;
|
|
68
53
|
responseId: string;
|
|
@@ -71,4 +56,3 @@ export interface VoiceRealtimeRuntimeOptions {
|
|
|
71
56
|
interruptedDuring: 'thinking' | 'speaking';
|
|
72
57
|
}) => Promise<void>;
|
|
73
58
|
}
|
|
74
|
-
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type TurnDisposition = 'complete' | 'incomplete' | 'backchannel' | 'wait';
|
|
2
|
+
export interface TurnSnapshot {
|
|
3
|
+
text: string;
|
|
4
|
+
silenceDurationMs: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TurnDecision {
|
|
7
|
+
disposition: TurnDisposition;
|
|
8
|
+
confidence: number;
|
|
9
|
+
holdMs: number;
|
|
10
|
+
source: 'provider' | 'semantic' | 'heuristic';
|
|
11
|
+
}
|
|
12
|
+
export interface TurnPolicy {
|
|
13
|
+
decide(snapshot: TurnSnapshot): TurnDecision | Promise<TurnDecision>;
|
|
14
|
+
}
|
|
15
|
+
export declare function hasContinuationHint(text: string): boolean;
|
|
16
|
+
export declare const heuristicTurnPolicy: TurnPolicy;
|
|
17
|
+
/** Accumulates provider utterances and commits only after the selected policy settles. */
|
|
18
|
+
export declare class TurnCoordinator {
|
|
19
|
+
private readonly silenceDurationMs;
|
|
20
|
+
private readonly onCommit;
|
|
21
|
+
private readonly policy;
|
|
22
|
+
private readonly parts;
|
|
23
|
+
private timer;
|
|
24
|
+
private stoppedAt;
|
|
25
|
+
private decisionGeneration;
|
|
26
|
+
constructor(silenceDurationMs: number, onCommit: (text: string, decision: TurnDecision, turnId: string) => void, policy?: TurnPolicy);
|
|
27
|
+
start(id: string): void;
|
|
28
|
+
stop(id: string): void;
|
|
29
|
+
final(id: string, text: string): void;
|
|
30
|
+
reset(): void;
|
|
31
|
+
restore(id: string, text: string): void;
|
|
32
|
+
private text;
|
|
33
|
+
private invalidatePendingDecision;
|
|
34
|
+
private evaluate;
|
|
35
|
+
private schedule;
|
|
36
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//#region src/voice/realtime/turnPolicy.ts
|
|
2
|
+
const MIN_TURN_SILENCE_MS = 1200;
|
|
3
|
+
const FINAL_SETTLE_MS = 350;
|
|
4
|
+
const INCOMPLETE_HOLD_MS = 1800;
|
|
5
|
+
const DECISION_TIMEOUT_MS = 750;
|
|
6
|
+
const MAX_TRANSCRIPT_CHARACTERS = 32e3;
|
|
7
|
+
const MAX_UTTERANCES = 256;
|
|
8
|
+
function isPromise(value) {
|
|
9
|
+
return typeof value.then === "function";
|
|
10
|
+
}
|
|
11
|
+
function hasContinuationHint(text) {
|
|
12
|
+
const tail = text.trim().replace(/[\s,。!?、;:,.!?;:]+$/u, "");
|
|
13
|
+
return /(?:…|\.\.\.)[\s,。!?,.!?]*$/u.test(text) || /(?:因为|但是|然后|而且|如果|所以|比如|或者|还有|我想|我要|我觉得|帮我|帮我查一下|帮我查查|请帮我|就是说|那个|这个|呃|嗯)$/u.test(tail) || /\b(?:because|but|and|or|if|so|then|um|uh|i want to|i would like to|can you|could you|let me)\s*$/iu.test(tail);
|
|
14
|
+
}
|
|
15
|
+
const heuristicTurnPolicy = { decide({ text }) {
|
|
16
|
+
const incomplete = hasContinuationHint(text);
|
|
17
|
+
return {
|
|
18
|
+
disposition: incomplete ? "incomplete" : "complete",
|
|
19
|
+
confidence: incomplete ? .6 : .5,
|
|
20
|
+
holdMs: incomplete ? INCOMPLETE_HOLD_MS : 0,
|
|
21
|
+
source: "heuristic"
|
|
22
|
+
};
|
|
23
|
+
} };
|
|
24
|
+
/** Accumulates provider utterances and commits only after the selected policy settles. */
|
|
25
|
+
var TurnCoordinator = class {
|
|
26
|
+
parts = /* @__PURE__ */ new Map();
|
|
27
|
+
timer;
|
|
28
|
+
stoppedAt = 0;
|
|
29
|
+
decisionGeneration = 0;
|
|
30
|
+
constructor(silenceDurationMs, onCommit, policy = heuristicTurnPolicy) {
|
|
31
|
+
this.silenceDurationMs = silenceDurationMs;
|
|
32
|
+
this.onCommit = onCommit;
|
|
33
|
+
this.policy = policy;
|
|
34
|
+
}
|
|
35
|
+
start(id) {
|
|
36
|
+
if (this.parts.has(id)) return;
|
|
37
|
+
this.invalidatePendingDecision();
|
|
38
|
+
this.parts.set(id, { stopped: false });
|
|
39
|
+
}
|
|
40
|
+
stop(id) {
|
|
41
|
+
const part = this.parts.get(id);
|
|
42
|
+
if (part?.stopped) return;
|
|
43
|
+
this.parts.set(id, {
|
|
44
|
+
...part,
|
|
45
|
+
stopped: true
|
|
46
|
+
});
|
|
47
|
+
this.stoppedAt = Date.now();
|
|
48
|
+
this.evaluate();
|
|
49
|
+
}
|
|
50
|
+
final(id, text) {
|
|
51
|
+
const part = this.parts.get(id);
|
|
52
|
+
if (part?.text !== void 0) return;
|
|
53
|
+
if (!part?.stopped) this.stoppedAt = Date.now();
|
|
54
|
+
this.parts.set(id, {
|
|
55
|
+
text: text.trim(),
|
|
56
|
+
stopped: true
|
|
57
|
+
});
|
|
58
|
+
if (this.parts.size > MAX_UTTERANCES || this.text().length > MAX_TRANSCRIPT_CHARACTERS) {
|
|
59
|
+
this.reset();
|
|
60
|
+
throw new Error("Voice turn input limit reached");
|
|
61
|
+
}
|
|
62
|
+
this.evaluate();
|
|
63
|
+
}
|
|
64
|
+
reset() {
|
|
65
|
+
this.invalidatePendingDecision();
|
|
66
|
+
this.parts.clear();
|
|
67
|
+
}
|
|
68
|
+
restore(id, text) {
|
|
69
|
+
this.invalidatePendingDecision();
|
|
70
|
+
this.parts.set(id, {
|
|
71
|
+
text,
|
|
72
|
+
stopped: true
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
text() {
|
|
76
|
+
return [...this.parts.values()].map((part) => part.text ?? "").filter(Boolean).join(" ");
|
|
77
|
+
}
|
|
78
|
+
invalidatePendingDecision() {
|
|
79
|
+
this.decisionGeneration += 1;
|
|
80
|
+
clearTimeout(this.timer);
|
|
81
|
+
this.timer = void 0;
|
|
82
|
+
}
|
|
83
|
+
evaluate() {
|
|
84
|
+
this.invalidatePendingDecision();
|
|
85
|
+
if ([...this.parts.values()].some((part) => !part.stopped || part.text === void 0)) return;
|
|
86
|
+
const text = this.text();
|
|
87
|
+
if (!text) {
|
|
88
|
+
this.reset();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const generation = this.decisionGeneration;
|
|
92
|
+
const snapshot = {
|
|
93
|
+
text,
|
|
94
|
+
silenceDurationMs: Math.max(0, Date.now() - this.stoppedAt)
|
|
95
|
+
};
|
|
96
|
+
let result;
|
|
97
|
+
try {
|
|
98
|
+
result = this.policy.decide(snapshot);
|
|
99
|
+
} catch {
|
|
100
|
+
this.schedule(text, heuristicTurnPolicy.decide(snapshot), generation);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (isPromise(result)) {
|
|
104
|
+
let settled = false;
|
|
105
|
+
const finish = (decision) => {
|
|
106
|
+
if (settled || generation !== this.decisionGeneration) return;
|
|
107
|
+
settled = true;
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
this.schedule(text, decision, generation);
|
|
110
|
+
};
|
|
111
|
+
const fallback = () => finish(heuristicTurnPolicy.decide(snapshot));
|
|
112
|
+
const timeout = setTimeout(fallback, DECISION_TIMEOUT_MS);
|
|
113
|
+
result.then(finish).catch(fallback);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.schedule(text, result, generation);
|
|
117
|
+
}
|
|
118
|
+
schedule(text, decision, generation) {
|
|
119
|
+
if (generation !== this.decisionGeneration) return;
|
|
120
|
+
const holdMs = Math.max(FINAL_SETTLE_MS, MIN_TURN_SILENCE_MS - this.silenceDurationMs, Number.isFinite(decision.holdMs) ? Math.max(0, decision.holdMs) : 0);
|
|
121
|
+
this.timer = setTimeout(() => {
|
|
122
|
+
if (generation !== this.decisionGeneration) return;
|
|
123
|
+
const turnId = [...this.parts.keys()][0];
|
|
124
|
+
if (decision.disposition === "wait") {
|
|
125
|
+
this.evaluate();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this.reset();
|
|
129
|
+
if (decision.disposition !== "backchannel") this.onCommit(text, decision, turnId);
|
|
130
|
+
}, Math.max(FINAL_SETTLE_MS, this.stoppedAt + holdMs - Date.now()));
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
export { TurnCoordinator, hasContinuationHint, heuristicTurnPolicy };
|
|
@@ -6,12 +6,12 @@ import { getAgentDefaultModelRef, init_schema } from "../config/schema.js";
|
|
|
6
6
|
import { buildSessionKey, init_session_key } from "../routing/session-key.js";
|
|
7
7
|
import { getDefaultAgentId, init_resolve_route } from "../routing/resolve-route.js";
|
|
8
8
|
import { init_providers, resolveModel } from "../providers/index.js";
|
|
9
|
+
import { isLocalModelBaseUrl } from "../providers/model-call.js";
|
|
9
10
|
import { writeKnowledgeItem } from "../knowledge-memory/repository.js";
|
|
10
11
|
import "../knowledge-memory/index.js";
|
|
11
12
|
import { createPriorityWindow, createUserGoal } from "../user-model/goals.js";
|
|
12
13
|
import { getAssertionSlot, getUserAssertion, linkAssertionEvidence, reconcileAssertion, setAssertionStatus } from "../user-model/repository.js";
|
|
13
14
|
import "../user-model/index.js";
|
|
14
|
-
import { isLocalModelBaseUrl } from "../providers/model-call.js";
|
|
15
15
|
import { allowsRemoteSourceProcessing } from "../user-context/sources/processing-policy.js";
|
|
16
16
|
import { analyzeUnderstandingSources, analyzeWorkContext, workDiscoveryResultMarkdown } from "./analyzer.js";
|
|
17
17
|
import { createUnderstandingSourceRun, getUnderstandingSourceRun, updateUnderstandingSourceGrantCheckpoint, updateUnderstandingSourceGrantPolicies, updateUnderstandingSourceRun, upsertUnderstandingSourceGrant } from "../user-context/sources/repository.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TaskRunRepository } from "../../tasks/task-run-repository.js";
|
|
2
2
|
import { getDefaultAgentId, init_resolve_route } from "../../routing/resolve-route.js";
|
|
3
|
-
import { extractProfileAgentId, resolveEffectiveAgentProfileForSession } from "../../config/agent-profile.js";
|
|
4
3
|
import { init_providers, resolveModel } from "../../providers/index.js";
|
|
4
|
+
import { extractProfileAgentId, resolveEffectiveAgentProfileForSession } from "../../config/agent-profile.js";
|
|
5
5
|
import { TaskRepository } from "../../tasks/task-repository.js";
|
|
6
6
|
import { TaskConversationRepository } from "../../tasks/task-conversation-repository.js";
|
|
7
7
|
import { getProjectWorkspacePathForSession } from "../../projects/workspace.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xopcai/xopc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.266",
|
|
4
4
|
"description": "Local-first personal AI assistant with reviewable user understanding, durable tasks, workflows, automations, and local or cloud models.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -163,9 +163,9 @@
|
|
|
163
163
|
"vitepress": "^1.6.4",
|
|
164
164
|
"vitest": "4.1.5",
|
|
165
165
|
"vue": "^3.5.35",
|
|
166
|
+
"@xopcai/browser-control-contract": "0.1.0",
|
|
166
167
|
"@xopcai/endpoint-tools-protocol": "0.0.0",
|
|
167
168
|
"@xopcai/realtime-client": "0.0.0",
|
|
168
|
-
"@xopcai/browser-control-contract": "0.1.0",
|
|
169
169
|
"@xopcai/realtime-protocol": "0.0.0",
|
|
170
170
|
"@xopcai/gateway-contract": "0.0.0"
|
|
171
171
|
},
|