neoagent 3.2.1-beta.2 → 3.2.1-beta.3
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/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +30803 -30805
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +5 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +347 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +8 -41
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +15 -28
- package/server/services/ai/providers/githubCopilot.js +36 -16
- package/server/services/ai/providers/google.js +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +13 -22
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +102 -82
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +158 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +87 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +84 -9
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -15,6 +15,7 @@ const scheduleAdapter = require('./adapters/schedule');
|
|
|
15
15
|
const { normalizeJsonObject } = require('./utils');
|
|
16
16
|
const { normalizeOutgoingMessageForPlatform } = require('../messaging/formatting_guides');
|
|
17
17
|
const { isTransientError } = require('../ai/providerRetry');
|
|
18
|
+
const { isAbortError, throwIfAborted } = require('../../utils/abort');
|
|
18
19
|
|
|
19
20
|
const MAX_AUTONOMOUS_RETRIES = 1;
|
|
20
21
|
const MAX_RECURRING_TASK_START_DELAY_MS = 90 * 1000;
|
|
@@ -112,6 +113,7 @@ class TaskRuntime {
|
|
|
112
113
|
this.runningTaskExecutions = new Set();
|
|
113
114
|
this.activeExecutionPromises = new Set();
|
|
114
115
|
this.activePolls = new Map();
|
|
116
|
+
this.abortController = new AbortController();
|
|
115
117
|
this.integrationEventCleanups = [];
|
|
116
118
|
this.triggerRegistry = new TriggerRegistry(taskAdapters);
|
|
117
119
|
this.started = false;
|
|
@@ -149,6 +151,9 @@ class TaskRuntime {
|
|
|
149
151
|
throw new Error('Task runtime cannot start while shutdown is in progress.');
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
if (this.abortController.signal.aborted) {
|
|
155
|
+
this.abortController = new AbortController();
|
|
156
|
+
}
|
|
152
157
|
this.started = true;
|
|
153
158
|
this.stopping = false;
|
|
154
159
|
this.state = 'starting';
|
|
@@ -208,6 +213,7 @@ class TaskRuntime {
|
|
|
208
213
|
this.started = false;
|
|
209
214
|
this.stopping = true;
|
|
210
215
|
this.state = 'stopping';
|
|
216
|
+
this.abortController.abort('Task runtime is stopping.');
|
|
211
217
|
this._stopScheduling();
|
|
212
218
|
|
|
213
219
|
this.stopPromise = (async () => {
|
|
@@ -301,6 +307,9 @@ class TaskRuntime {
|
|
|
301
307
|
runTaskNow(taskId, userId) {
|
|
302
308
|
const task = this.taskRepository.getTaskById(taskId, userId);
|
|
303
309
|
if (!task) throw new Error('Task not found');
|
|
310
|
+
if (this.stopping || this.abortController.signal.aborted) {
|
|
311
|
+
return { running: false, skipped: true, reason: 'runtime_stopping' };
|
|
312
|
+
}
|
|
304
313
|
void this._executeTask(taskId, userId, {
|
|
305
314
|
scheduledAt: new Date().toISOString(),
|
|
306
315
|
manual: true,
|
|
@@ -349,6 +358,7 @@ class TaskRuntime {
|
|
|
349
358
|
const due = this.taskRepository.listDueOneTimeTasks();
|
|
350
359
|
|
|
351
360
|
for (const task of due) {
|
|
361
|
+
if (this.abortController.signal.aborted) break;
|
|
352
362
|
this.scheduleJobs.delete(task.id);
|
|
353
363
|
try {
|
|
354
364
|
const result = await this._executeTask(task.id, task.user_id, {
|
|
@@ -373,9 +383,13 @@ class TaskRuntime {
|
|
|
373
383
|
return this._runPoll('integration', async () => {
|
|
374
384
|
const tasks = this.taskRepository.listEnabledByTriggerTypes(POLLED_TRIGGER_TYPES);
|
|
375
385
|
for (const task of tasks) {
|
|
386
|
+
if (this.abortController.signal.aborted) break;
|
|
376
387
|
try {
|
|
377
|
-
await pollIntegrationTask(this, task
|
|
388
|
+
await pollIntegrationTask(this, task, {
|
|
389
|
+
signal: this.abortController.signal,
|
|
390
|
+
});
|
|
378
391
|
} catch (error) {
|
|
392
|
+
if (isAbortError(error, this.abortController.signal) && this.stopping) break;
|
|
379
393
|
console.error(`[Tasks] Trigger poll failed for task ${task.id}:`, error.message);
|
|
380
394
|
}
|
|
381
395
|
}
|
|
@@ -390,13 +404,16 @@ class TaskRuntime {
|
|
|
390
404
|
if (active) {
|
|
391
405
|
return active;
|
|
392
406
|
}
|
|
393
|
-
if (this.stopping) {
|
|
407
|
+
if (this.stopping || this.abortController.signal.aborted) {
|
|
394
408
|
return Promise.resolve({ skipped: true, reason: 'runtime_stopping' });
|
|
395
409
|
}
|
|
396
410
|
|
|
397
411
|
const promise = Promise.resolve()
|
|
398
|
-
.then(callback)
|
|
412
|
+
.then(() => callback(this.abortController.signal))
|
|
399
413
|
.catch((error) => {
|
|
414
|
+
if (isAbortError(error, this.abortController.signal) && this.stopping) {
|
|
415
|
+
return { skipped: true, reason: 'runtime_stopping' };
|
|
416
|
+
}
|
|
400
417
|
this.lastError = error.message;
|
|
401
418
|
onError(error);
|
|
402
419
|
return { error: error.message };
|
|
@@ -453,7 +470,7 @@ class TaskRuntime {
|
|
|
453
470
|
}
|
|
454
471
|
|
|
455
472
|
async _executeTask(taskId, userId, executionMeta = {}) {
|
|
456
|
-
if (this.stopping) {
|
|
473
|
+
if (this.stopping || this.abortController.signal.aborted) {
|
|
457
474
|
return { skipped: true, reason: 'runtime_stopping' };
|
|
458
475
|
}
|
|
459
476
|
const executionKey = `${userId}:${taskId}`;
|
|
@@ -488,6 +505,8 @@ class TaskRuntime {
|
|
|
488
505
|
}
|
|
489
506
|
|
|
490
507
|
async _executeTaskSerial(taskId, userId, executionMeta = {}) {
|
|
508
|
+
const signal = this.abortController.signal;
|
|
509
|
+
if (signal.aborted) return { skipped: true, reason: 'runtime_stopping' };
|
|
491
510
|
const task = this.taskRepository.getTaskById(taskId, userId);
|
|
492
511
|
if (!task || !task.enabled) return { skipped: true, reason: 'missing_or_disabled' };
|
|
493
512
|
|
|
@@ -582,7 +601,11 @@ class TaskRuntime {
|
|
|
582
601
|
taskId,
|
|
583
602
|
manual: executionMeta.manual === true,
|
|
584
603
|
scheduledAt: executionMeta.scheduledAt || null,
|
|
604
|
+
signal,
|
|
585
605
|
});
|
|
606
|
+
if (this.stopping || signal.aborted) {
|
|
607
|
+
return { skipped: true, reason: 'runtime_stopping' };
|
|
608
|
+
}
|
|
586
609
|
this.io.to(`user:${userId}`).emit('tasks:task_complete', { taskId, result });
|
|
587
610
|
this._recordTaskLifecycle({
|
|
588
611
|
userId,
|
|
@@ -647,12 +670,16 @@ class TaskRuntime {
|
|
|
647
670
|
skipVerifier: false,
|
|
648
671
|
stream: false,
|
|
649
672
|
context: executionMeta.triggerPayload || {},
|
|
673
|
+
signal,
|
|
650
674
|
};
|
|
651
675
|
try {
|
|
652
676
|
const result = typeof this.agentEngine.runWithModel === 'function'
|
|
653
677
|
? await this.agentEngine.runWithModel(userId, finalPrompt, runOptions, normalizedConfig.model || null)
|
|
654
678
|
: await this.agentEngine.run(userId, finalPrompt, runOptions);
|
|
655
679
|
completedRunId = result?.runId || null;
|
|
680
|
+
if (this.stopping || signal.aborted) {
|
|
681
|
+
return { skipped: true, reason: 'runtime_stopping', runId: completedRunId };
|
|
682
|
+
}
|
|
656
683
|
const fallbackDelivery = await this._deliverTaskResultIfNeeded({
|
|
657
684
|
userId,
|
|
658
685
|
agentId,
|
|
@@ -692,6 +719,9 @@ class TaskRuntime {
|
|
|
692
719
|
});
|
|
693
720
|
return result;
|
|
694
721
|
} catch (err) {
|
|
722
|
+
if (isAbortError(err, signal) && this.stopping) {
|
|
723
|
+
return { skipped: true, reason: 'runtime_stopping', runId: completedRunId };
|
|
724
|
+
}
|
|
695
725
|
const transientExecutionError = isTransientError(err);
|
|
696
726
|
if (completedRunId && !transientExecutionError) {
|
|
697
727
|
this.taskRepository.markAgentRunFailed(completedRunId, userId, err.message);
|
|
@@ -733,6 +763,9 @@ class TaskRuntime {
|
|
|
733
763
|
}
|
|
734
764
|
}
|
|
735
765
|
} catch (err) {
|
|
766
|
+
if (isAbortError(err, signal) && this.stopping) {
|
|
767
|
+
return { skipped: true, reason: 'runtime_stopping', runId: completedRunId };
|
|
768
|
+
}
|
|
736
769
|
console.error(`[Tasks] Task ${taskId} error:`, err.message);
|
|
737
770
|
if (err?.code !== 'TASK_DELIVERY_FAILED') {
|
|
738
771
|
const failureMessage = this._buildTaskFailureMessage(taskName, err);
|
|
@@ -1045,6 +1078,7 @@ class TaskRuntime {
|
|
|
1045
1078
|
deliveryState,
|
|
1046
1079
|
allowPlainResultFallback = true,
|
|
1047
1080
|
}) {
|
|
1081
|
+
throwIfAborted(this.abortController.signal, 'Task runtime is stopping.');
|
|
1048
1082
|
if (deliveryState?.messagingSent || deliveryState?.noResponse || taskConfig.callTo) return null;
|
|
1049
1083
|
const targets = this._buildNotifyTargets(userId, agentId, taskConfig);
|
|
1050
1084
|
if (!targets.length) return null;
|
|
@@ -1118,6 +1152,7 @@ class TaskRuntime {
|
|
|
1118
1152
|
mediaPath: target.mediaPath || null,
|
|
1119
1153
|
runId: result?.runId || null,
|
|
1120
1154
|
persistConversation: true,
|
|
1155
|
+
signal: this.abortController.signal,
|
|
1121
1156
|
});
|
|
1122
1157
|
deliveryState.messagingSent = true;
|
|
1123
1158
|
deliveryState.proactiveMessageStaged = false;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { randomUUID } = require('crypto');
|
|
4
4
|
|
|
5
5
|
const { runVoiceTranscriptTurn } = require('./turnRunner');
|
|
6
|
+
const { createAbortError, throwIfAborted } = require('../../utils/abort');
|
|
6
7
|
|
|
7
8
|
class VoiceAgentBridge {
|
|
8
9
|
constructor({ agentEngine, memoryManager, runtimeManager }) {
|
|
@@ -20,6 +21,8 @@ class VoiceAgentBridge {
|
|
|
20
21
|
await session.publishTranscriptFinal(transcriptText);
|
|
21
22
|
await session.setState('thinking');
|
|
22
23
|
const runId = randomUUID();
|
|
24
|
+
let ownedRunId = runId;
|
|
25
|
+
const turnSignal = session.signal;
|
|
23
26
|
session.currentRunId = runId;
|
|
24
27
|
|
|
25
28
|
try {
|
|
@@ -40,8 +43,11 @@ class VoiceAgentBridge {
|
|
|
40
43
|
allowInterimUpdates: true,
|
|
41
44
|
voiceSessionId: session.id,
|
|
42
45
|
runId,
|
|
46
|
+
signal: turnSignal,
|
|
43
47
|
});
|
|
44
|
-
|
|
48
|
+
throwIfAborted(turnSignal, 'Voice turn was interrupted.');
|
|
49
|
+
ownedRunId = result.runId || runId;
|
|
50
|
+
session.currentRunId = ownedRunId;
|
|
45
51
|
const replyText = String(result.replyText || '').trim();
|
|
46
52
|
if (replyText) {
|
|
47
53
|
if (deferredFollowUp) {
|
|
@@ -50,6 +56,7 @@ class VoiceAgentBridge {
|
|
|
50
56
|
deferredFollowUp,
|
|
51
57
|
replyText,
|
|
52
58
|
result.runId || runId,
|
|
59
|
+
{ signal: turnSignal },
|
|
53
60
|
);
|
|
54
61
|
if (!followUp?.sent) {
|
|
55
62
|
await this.runtimeManager.deliverAssistantMessage(session, replyText, {
|
|
@@ -62,12 +69,18 @@ class VoiceAgentBridge {
|
|
|
62
69
|
});
|
|
63
70
|
}
|
|
64
71
|
}
|
|
72
|
+
throwIfAborted(turnSignal, 'Voice turn was interrupted.');
|
|
65
73
|
await session.setState('idle');
|
|
66
|
-
session.currentRunId
|
|
74
|
+
if (session.currentRunId === ownedRunId) {
|
|
75
|
+
session.currentRunId = null;
|
|
76
|
+
}
|
|
67
77
|
return result;
|
|
68
78
|
} catch (error) {
|
|
69
|
-
session.currentRunId = null;
|
|
70
|
-
|
|
79
|
+
if (session.currentRunId === ownedRunId) session.currentRunId = null;
|
|
80
|
+
if (session.signal === turnSignal && !session.closed) {
|
|
81
|
+
await session.setState('idle');
|
|
82
|
+
}
|
|
83
|
+
if (turnSignal.aborted) throw createAbortError(turnSignal);
|
|
71
84
|
throw error;
|
|
72
85
|
}
|
|
73
86
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { getProviderRuntimeConfig } = require('../ai/models');
|
|
4
4
|
const { resolveSttModel, transcribeVoiceInput } = require('./providers');
|
|
5
5
|
const { writeTempAudioFile, removeTempFile } = require('./liveAudio');
|
|
6
|
+
const { createAbortError } = require('../../utils/abort');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_PARTIAL_DEBOUNCE_MS = 700;
|
|
8
9
|
const DEFAULT_MIN_PARTIAL_BYTES = 8000;
|
|
@@ -68,6 +69,7 @@ class BufferedLiveRelayAdapter {
|
|
|
68
69
|
userId: session.userId,
|
|
69
70
|
agentId: session.agentId,
|
|
70
71
|
timeoutMs: 20000,
|
|
72
|
+
signal: session.signal,
|
|
71
73
|
});
|
|
72
74
|
} finally {
|
|
73
75
|
// Release buffered audio immediately after commit so completed turns do
|
|
@@ -99,6 +101,7 @@ class BufferedLiveRelayAdapter {
|
|
|
99
101
|
userId: session.userId,
|
|
100
102
|
agentId: session.agentId,
|
|
101
103
|
timeoutMs: 6000,
|
|
104
|
+
signal: session.signal,
|
|
102
105
|
});
|
|
103
106
|
if (transcript) {
|
|
104
107
|
await session.publishTranscriptPartial(transcript);
|
|
@@ -136,9 +139,11 @@ class BufferedLiveRelayAdapter {
|
|
|
136
139
|
apiKey: attempt.apiKey,
|
|
137
140
|
baseUrl: attempt.baseUrl,
|
|
138
141
|
timeoutMs: options.timeoutMs,
|
|
142
|
+
signal: options.signal,
|
|
139
143
|
});
|
|
140
144
|
return String(transcript || '').trim();
|
|
141
145
|
} catch (error) {
|
|
146
|
+
if (options.signal?.aborted) throw createAbortError(options.signal);
|
|
142
147
|
lastError = error;
|
|
143
148
|
}
|
|
144
149
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
function voiceAbortError(message, code) {
|
|
4
|
+
const error = new Error(message);
|
|
5
|
+
error.name = 'AbortError';
|
|
6
|
+
error.code = code;
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
class VoiceLiveSession {
|
|
4
11
|
constructor({
|
|
5
12
|
id,
|
|
@@ -32,6 +39,11 @@ class VoiceLiveSession {
|
|
|
32
39
|
this.lastAssistantText = '';
|
|
33
40
|
this.assistantMessageCount = 0;
|
|
34
41
|
this.closed = false;
|
|
42
|
+
this.turnAbortController = new AbortController();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get signal() {
|
|
46
|
+
return this.turnAbortController.signal;
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
resetInput(mimeType = 'audio/pcm;rate=16000;channels=1') {
|
|
@@ -46,6 +58,13 @@ class VoiceLiveSession {
|
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
resetTurnState() {
|
|
61
|
+
if (!this.turnAbortController.signal.aborted) {
|
|
62
|
+
this.turnAbortController.abort(voiceAbortError(
|
|
63
|
+
'Voice turn was reset.',
|
|
64
|
+
'VOICE_TURN_RESET',
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
this.turnAbortController = new AbortController();
|
|
49
68
|
this.lastPartialTranscript = '';
|
|
50
69
|
this.lastFinalTranscript = '';
|
|
51
70
|
this.lastAssistantText = '';
|
|
@@ -204,6 +223,12 @@ class VoiceLiveSession {
|
|
|
204
223
|
|
|
205
224
|
async interruptOutput() {
|
|
206
225
|
this.interrupted = true;
|
|
226
|
+
if (!this.turnAbortController.signal.aborted) {
|
|
227
|
+
this.turnAbortController.abort(voiceAbortError(
|
|
228
|
+
'Voice output was interrupted.',
|
|
229
|
+
'VOICE_INTERRUPTED',
|
|
230
|
+
));
|
|
231
|
+
}
|
|
207
232
|
if (typeof this.sink?.interruptOutput === 'function') {
|
|
208
233
|
await this.sink.interruptOutput(this);
|
|
209
234
|
}
|
|
@@ -217,6 +242,12 @@ class VoiceLiveSession {
|
|
|
217
242
|
|
|
218
243
|
async close(reason = 'closed') {
|
|
219
244
|
this.closed = true;
|
|
245
|
+
if (!this.turnAbortController.signal.aborted) {
|
|
246
|
+
this.turnAbortController.abort(voiceAbortError(
|
|
247
|
+
`Voice session closed: ${reason}.`,
|
|
248
|
+
'VOICE_SESSION_CLOSED',
|
|
249
|
+
));
|
|
250
|
+
}
|
|
220
251
|
if (typeof this.sink?.close === 'function') {
|
|
221
252
|
await this.sink.close(this, reason);
|
|
222
253
|
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { readResponseBuffer } = require('../network/http');
|
|
4
|
+
const { runWithAbortTimeout } = require('../../utils/abort');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SPEECH_TIMEOUT_MS = 30000;
|
|
7
|
+
const DEFAULT_MAX_SPEECH_BYTES = 32 * 1024 * 1024;
|
|
8
|
+
|
|
3
9
|
async function synthesizeSpeechBuffer(
|
|
4
10
|
client,
|
|
5
11
|
text,
|
|
6
|
-
{
|
|
12
|
+
{
|
|
13
|
+
model = 'gpt-4o-mini-tts',
|
|
14
|
+
voice = 'alloy',
|
|
15
|
+
responseFormat = 'mp3',
|
|
16
|
+
signal = null,
|
|
17
|
+
timeoutMs = DEFAULT_SPEECH_TIMEOUT_MS,
|
|
18
|
+
maxResponseBytes = DEFAULT_MAX_SPEECH_BYTES,
|
|
19
|
+
} = {},
|
|
7
20
|
) {
|
|
8
21
|
if (!client) {
|
|
9
22
|
throw new Error('OpenAI client is not configured for speech synthesis.');
|
|
@@ -14,14 +27,26 @@ async function synthesizeSpeechBuffer(
|
|
|
14
27
|
throw new Error('Speech input is empty; cannot synthesize audio.');
|
|
15
28
|
}
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
return runWithAbortTimeout(async (operationSignal) => {
|
|
31
|
+
const response = await client.audio.speech.create({
|
|
32
|
+
model: String(model || 'gpt-4o-mini-tts').trim() || 'gpt-4o-mini-tts',
|
|
33
|
+
voice: String(voice || 'alloy').trim() || 'alloy',
|
|
34
|
+
input: content,
|
|
35
|
+
response_format: String(responseFormat || 'mp3').trim() || 'mp3',
|
|
36
|
+
}, { signal: operationSignal });
|
|
23
37
|
|
|
24
|
-
|
|
38
|
+
return readResponseBuffer(response, {
|
|
39
|
+
maxResponseBytes,
|
|
40
|
+
serviceName: 'OpenAI speech synthesis',
|
|
41
|
+
signal: operationSignal,
|
|
42
|
+
tooLargeCode: 'VOICE_PROVIDER_RESPONSE_TOO_LARGE',
|
|
43
|
+
});
|
|
44
|
+
}, {
|
|
45
|
+
signal,
|
|
46
|
+
timeoutMs,
|
|
47
|
+
timeoutCode: 'VOICE_PROVIDER_TIMEOUT',
|
|
48
|
+
label: 'OpenAI speech synthesis',
|
|
49
|
+
});
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
module.exports = {
|