@pellux/goodvibes-sdk 1.1.0 → 1.3.0
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/contracts/artifacts/operator-contract.json +1416 -88
- package/dist/platform/companion/companion-chat-manager.d.ts +6 -12
- package/dist/platform/companion/companion-chat-manager.d.ts.map +1 -1
- package/dist/platform/companion/companion-chat-manager.js +8 -2
- package/dist/platform/config/index.d.ts +2 -1
- package/dist/platform/config/index.d.ts.map +1 -1
- package/dist/platform/config/index.js +1 -0
- package/dist/platform/config/manager.d.ts +35 -0
- package/dist/platform/config/manager.d.ts.map +1 -1
- package/dist/platform/config/manager.js +98 -2
- package/dist/platform/config/shared-config-tier.d.ts +51 -0
- package/dist/platform/config/shared-config-tier.d.ts.map +1 -0
- package/dist/platform/config/shared-config-tier.js +127 -0
- package/dist/platform/control-plane/method-catalog-runtime.d.ts.map +1 -1
- package/dist/platform/control-plane/method-catalog-runtime.js +95 -1
- package/dist/platform/control-plane/operator-contract-schemas-knowledge.d.ts.map +1 -1
- package/dist/platform/control-plane/operator-contract-schemas-knowledge.js +6 -1
- package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts +7 -0
- package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts.map +1 -1
- package/dist/platform/control-plane/operator-contract-schemas-runtime.js +40 -1
- package/dist/platform/daemon/facade-composition.d.ts.map +1 -1
- package/dist/platform/daemon/facade-composition.js +1 -0
- package/dist/platform/daemon/homeassistant-chat.d.ts +4 -1
- package/dist/platform/daemon/homeassistant-chat.d.ts.map +1 -1
- package/dist/platform/daemon/homeassistant-chat.js +1 -1
- package/dist/platform/daemon/http/homeassistant-routes.d.ts.map +1 -1
- package/dist/platform/daemon/http/homeassistant-routes.js +28 -2
- package/dist/platform/integrations/webhooks.d.ts +15 -0
- package/dist/platform/integrations/webhooks.d.ts.map +1 -1
- package/dist/platform/integrations/webhooks.js +18 -0
- package/dist/platform/knowledge/extraction-policy.d.ts +12 -1
- package/dist/platform/knowledge/extraction-policy.d.ts.map +1 -1
- package/dist/platform/knowledge/extraction-policy.js +19 -1
- package/dist/platform/knowledge/graphql-schema.d.ts +1 -1
- package/dist/platform/knowledge/graphql-schema.d.ts.map +1 -1
- package/dist/platform/knowledge/graphql-schema.js +5 -0
- package/dist/platform/knowledge/home-graph/index.d.ts.map +1 -1
- package/dist/platform/knowledge/home-graph/index.js +4 -0
- package/dist/platform/knowledge/home-graph/link.d.ts +2 -2
- package/dist/platform/knowledge/home-graph/link.d.ts.map +1 -1
- package/dist/platform/knowledge/home-graph/link.js +60 -9
- package/dist/platform/knowledge/home-graph/refinement.d.ts +14 -9
- package/dist/platform/knowledge/home-graph/refinement.d.ts.map +1 -1
- package/dist/platform/knowledge/home-graph/refinement.js +18 -3
- package/dist/platform/knowledge/home-graph/reset.js +21 -4
- package/dist/platform/knowledge/home-graph/service.d.ts +10 -11
- package/dist/platform/knowledge/home-graph/service.d.ts.map +1 -1
- package/dist/platform/knowledge/home-graph/triage.d.ts +103 -0
- package/dist/platform/knowledge/home-graph/triage.d.ts.map +1 -0
- package/dist/platform/knowledge/home-graph/triage.js +362 -0
- package/dist/platform/knowledge/home-graph/types.d.ts +15 -0
- package/dist/platform/knowledge/home-graph/types.d.ts.map +1 -1
- package/dist/platform/knowledge/knowledge-api.d.ts +6 -0
- package/dist/platform/knowledge/knowledge-api.d.ts.map +1 -1
- package/dist/platform/knowledge/knowledge-api.js +5 -0
- package/dist/platform/knowledge/knowledge-history-types.d.ts +56 -0
- package/dist/platform/knowledge/knowledge-history-types.d.ts.map +1 -0
- package/dist/platform/knowledge/knowledge-history-types.js +1 -0
- package/dist/platform/knowledge/packet.d.ts.map +1 -1
- package/dist/platform/knowledge/packet.js +27 -3
- package/dist/platform/knowledge/scope-records.js +4 -2
- package/dist/platform/knowledge/semantic/answer-gaps.d.ts +8 -0
- package/dist/platform/knowledge/semantic/answer-gaps.d.ts.map +1 -1
- package/dist/platform/knowledge/semantic/answer-gaps.js +26 -6
- package/dist/platform/knowledge/semantic/enrichment.d.ts.map +1 -1
- package/dist/platform/knowledge/semantic/enrichment.js +15 -13
- package/dist/platform/knowledge/semantic/graph-index.d.ts.map +1 -1
- package/dist/platform/knowledge/semantic/graph-index.js +3 -1
- package/dist/platform/knowledge/semantic/service.d.ts +7 -0
- package/dist/platform/knowledge/semantic/service.d.ts.map +1 -1
- package/dist/platform/knowledge/semantic/service.js +10 -1
- package/dist/platform/knowledge/service-node-admin.d.ts +46 -0
- package/dist/platform/knowledge/service-node-admin.d.ts.map +1 -0
- package/dist/platform/knowledge/service-node-admin.js +72 -0
- package/dist/platform/knowledge/service.d.ts +24 -20
- package/dist/platform/knowledge/service.d.ts.map +1 -1
- package/dist/platform/knowledge/service.js +23 -47
- package/dist/platform/knowledge/store-config.d.ts +34 -0
- package/dist/platform/knowledge/store-config.d.ts.map +1 -1
- package/dist/platform/knowledge/store-config.js +23 -0
- package/dist/platform/knowledge/store-load.d.ts +3 -1
- package/dist/platform/knowledge/store-load.d.ts.map +1 -1
- package/dist/platform/knowledge/store-load.js +3 -1
- package/dist/platform/knowledge/store-node-history.d.ts +49 -0
- package/dist/platform/knowledge/store-node-history.d.ts.map +1 -0
- package/dist/platform/knowledge/store-node-history.js +331 -0
- package/dist/platform/knowledge/store-record-delete.d.ts +25 -0
- package/dist/platform/knowledge/store-record-delete.d.ts.map +1 -0
- package/dist/platform/knowledge/store-record-delete.js +84 -0
- package/dist/platform/knowledge/store-schema.d.ts +3 -1
- package/dist/platform/knowledge/store-schema.d.ts.map +1 -1
- package/dist/platform/knowledge/store-schema.js +66 -0
- package/dist/platform/knowledge/store.d.ts +19 -1
- package/dist/platform/knowledge/store.d.ts.map +1 -1
- package/dist/platform/knowledge/store.js +69 -108
- package/dist/platform/knowledge/types.d.ts +18 -0
- package/dist/platform/knowledge/types.d.ts.map +1 -1
- package/dist/platform/providers/registry.d.ts +18 -0
- package/dist/platform/providers/registry.d.ts.map +1 -1
- package/dist/platform/providers/registry.js +49 -3
- package/dist/platform/providers/runtime-snapshot.d.ts.map +1 -1
- package/dist/platform/providers/runtime-snapshot.js +14 -2
- package/dist/platform/runtime/memory-spine/client.d.ts +137 -12
- package/dist/platform/runtime/memory-spine/client.d.ts.map +1 -1
- package/dist/platform/runtime/memory-spine/client.js +119 -1
- package/dist/platform/runtime/memory-spine/index.d.ts +8 -1
- package/dist/platform/runtime/memory-spine/index.d.ts.map +1 -1
- package/dist/platform/runtime/memory-spine/index.js +7 -0
- package/dist/platform/runtime/memory-spine/recall-snapshot.d.ts +74 -0
- package/dist/platform/runtime/memory-spine/recall-snapshot.d.ts.map +1 -0
- package/dist/platform/runtime/memory-spine/recall-snapshot.js +87 -0
- package/dist/platform/runtime/memory-spine/wire-verb-availability.d.ts +59 -0
- package/dist/platform/runtime/memory-spine/wire-verb-availability.d.ts.map +1 -0
- package/dist/platform/runtime/memory-spine/wire-verb-availability.js +114 -0
- package/dist/platform/runtime/services.d.ts.map +1 -1
- package/dist/platform/runtime/services.js +3 -12
- package/dist/platform/state/memory-recall-contract.d.ts +7 -0
- package/dist/platform/state/memory-recall-contract.d.ts.map +1 -1
- package/dist/platform/state/memory-recall-contract.js +2 -0
- package/dist/platform/utils/notify.d.ts +25 -1
- package/dist/platform/utils/notify.d.ts.map +1 -1
- package/dist/platform/utils/notify.js +29 -1
- package/dist/platform/version.js +1 -1
- package/dist/platform/voice/index.d.ts +2 -0
- package/dist/platform/voice/index.d.ts.map +1 -1
- package/dist/platform/voice/index.js +3 -0
- package/dist/platform/voice/spoken-turn/audio-sink.d.ts +111 -0
- package/dist/platform/voice/spoken-turn/audio-sink.d.ts.map +1 -0
- package/dist/platform/voice/spoken-turn/audio-sink.js +1 -0
- package/dist/platform/voice/spoken-turn/controller.d.ts +117 -0
- package/dist/platform/voice/spoken-turn/controller.d.ts.map +1 -0
- package/dist/platform/voice/spoken-turn/controller.js +448 -0
- package/dist/platform/voice/spoken-turn/index.d.ts +16 -0
- package/dist/platform/voice/spoken-turn/index.d.ts.map +1 -0
- package/dist/platform/voice/spoken-turn/index.js +12 -0
- package/dist/platform/voice/spoken-turn/text-chunker.d.ts +33 -0
- package/dist/platform/voice/spoken-turn/text-chunker.d.ts.map +1 -0
- package/dist/platform/voice/spoken-turn/text-chunker.js +97 -0
- package/package.json +9 -9
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { summarizeError } from '../../utils/index.js';
|
|
2
|
+
import { TtsTextChunker } from './text-chunker.js';
|
|
3
|
+
/**
|
|
4
|
+
* How many synthesis requests may sit in the pipeline at once (synthesizing,
|
|
5
|
+
* waiting to play, or playing). 2 = the chunk being played plus ONE prefetch,
|
|
6
|
+
* so the next audio is ready the moment the current sink drains. Bounding
|
|
7
|
+
* this is what keeps a streaming answer from bursting N concurrent requests
|
|
8
|
+
* at the voice provider — ElevenLabs plans allow as few as 3 concurrent, and
|
|
9
|
+
* an unbounded burst 429s the whole turn. The SDK config schema has no tts.*
|
|
10
|
+
* key for pipeline tuning, so this is a constant by design.
|
|
11
|
+
*/
|
|
12
|
+
const SYNTHESIS_PIPELINE_WINDOW = 2;
|
|
13
|
+
/**
|
|
14
|
+
* Upper bound for one merged synthesis request's text. The ElevenLabs
|
|
15
|
+
* provider passes request text through verbatim (no cap of its own); the API
|
|
16
|
+
* caps text per request by plan — 2,500 chars on the lowest tiers, 5,000 on
|
|
17
|
+
* most others. 1,500 stays safely under every plan while still folding a
|
|
18
|
+
* multi-paragraph answer into one or two requests.
|
|
19
|
+
*/
|
|
20
|
+
const SYNTHESIS_MERGE_MAX_CHARS = 1500;
|
|
21
|
+
/**
|
|
22
|
+
* Backoff schedule for transient synthesis failures (429 rate/concurrency
|
|
23
|
+
* limits, transient 5xx, network drops): first retry after 1s, second after
|
|
24
|
+
* 2.5s, then the chunk is skipped honestly and the turn continues. The SDK's
|
|
25
|
+
* provider errors are plain Error strings with the HTTP status embedded — no
|
|
26
|
+
* Retry-After header is exposed — so the schedule is fixed, not server-driven.
|
|
27
|
+
*/
|
|
28
|
+
const SYNTHESIS_RETRY_DELAYS_MS = [1000, 2500];
|
|
29
|
+
/**
|
|
30
|
+
* Source label attached to every synthesis request's metadata. Consumers may
|
|
31
|
+
* override it so provider-side telemetry attributes requests to the right
|
|
32
|
+
* surface (TUI, agent, webui).
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_SYNTHESIS_SOURCE = 'goodvibes-sdk';
|
|
35
|
+
/**
|
|
36
|
+
* SpokenTurnController — the shared spoken-output policy engine. It watches a
|
|
37
|
+
* turn's lifecycle events, chunks the streamed answer into speech-sized pieces,
|
|
38
|
+
* merges and dispatches synthesis requests through a bounded 2-slot window with
|
|
39
|
+
* retry/backoff, and drives an injected {@link AudioSink} with honest
|
|
40
|
+
* drain-vs-interrupt semantics. All I/O is the sink's; this class is pure
|
|
41
|
+
* policy and runs unchanged in the terminal, the agent, and the browser.
|
|
42
|
+
*/
|
|
43
|
+
export class SpokenTurnController {
|
|
44
|
+
pendingPrompt = null;
|
|
45
|
+
activeTurnId = null;
|
|
46
|
+
chunker = null;
|
|
47
|
+
chunkSequence = 0;
|
|
48
|
+
playbackChain = Promise.resolve();
|
|
49
|
+
abortControllers = new Set();
|
|
50
|
+
timer = null;
|
|
51
|
+
errorReportedForTurn = false;
|
|
52
|
+
noSinkNoticed = false;
|
|
53
|
+
/** Chunker output waiting to be merged into a synthesis request. */
|
|
54
|
+
pendingTexts = [];
|
|
55
|
+
/** Requests currently in the pipeline (synthesizing / waiting / playing). */
|
|
56
|
+
pipelineDepth = 0;
|
|
57
|
+
/** Bumped on every teardown so stale pipeline releases are ignored. */
|
|
58
|
+
pipelineGeneration = 0;
|
|
59
|
+
pumpScheduled = false;
|
|
60
|
+
/** Set when TURN_COMPLETED arrives; the turn releases once the pipeline drains. */
|
|
61
|
+
completedTurnId = null;
|
|
62
|
+
voiceService;
|
|
63
|
+
configManager;
|
|
64
|
+
sink;
|
|
65
|
+
notify;
|
|
66
|
+
source;
|
|
67
|
+
now;
|
|
68
|
+
setIntervalImpl;
|
|
69
|
+
clearIntervalImpl;
|
|
70
|
+
setTimeoutImpl;
|
|
71
|
+
clearTimeoutImpl;
|
|
72
|
+
constructor(options) {
|
|
73
|
+
this.voiceService = options.voiceService;
|
|
74
|
+
this.configManager = options.configManager;
|
|
75
|
+
this.sink = options.sink;
|
|
76
|
+
this.notify = options.notify;
|
|
77
|
+
this.source = options.source ?? DEFAULT_SYNTHESIS_SOURCE;
|
|
78
|
+
this.now = options.now ?? (() => Date.now());
|
|
79
|
+
this.setIntervalImpl = options.setInterval ?? setInterval;
|
|
80
|
+
this.clearIntervalImpl = options.clearInterval ?? clearInterval;
|
|
81
|
+
this.setTimeoutImpl = options.setTimeout ?? setTimeout;
|
|
82
|
+
this.clearTimeoutImpl = options.clearTimeout ?? clearTimeout;
|
|
83
|
+
}
|
|
84
|
+
submitNextTurn(prompt) {
|
|
85
|
+
const normalized = prompt.trim();
|
|
86
|
+
if (!normalized)
|
|
87
|
+
return false;
|
|
88
|
+
this.stop();
|
|
89
|
+
if (!this.sink.available) {
|
|
90
|
+
if (!this.noSinkNoticed) {
|
|
91
|
+
this.noSinkNoticed = true;
|
|
92
|
+
this.notify?.('[TTS] Text response will continue, but live audio is unavailable. Install mpv or ffplay.');
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
// Reset the no-sink notice if the sink becomes available again.
|
|
97
|
+
this.noSinkNoticed = false;
|
|
98
|
+
this.pendingPrompt = normalized;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Returns whether speech was actually ACTIVE when stopped. The notice only
|
|
103
|
+
* prints in that case — stop() on an idle controller used to notify anyway,
|
|
104
|
+
* spamming "[TTS] Spoken output stopped." on every Ctrl+C (an earlier replay
|
|
105
|
+
* fix); callers use the return to decide whether the press "did a
|
|
106
|
+
* job" (see handleCtrlC's consume-on-speech-stop).
|
|
107
|
+
*/
|
|
108
|
+
stop(message) {
|
|
109
|
+
const wasActive = this.pendingPrompt !== null || this.activeTurnId !== null
|
|
110
|
+
|| this.chunker !== null || this.abortControllers.size > 0;
|
|
111
|
+
this.pendingPrompt = null;
|
|
112
|
+
this.activeTurnId = null;
|
|
113
|
+
this.chunker?.reset();
|
|
114
|
+
this.chunker = null;
|
|
115
|
+
this.stopTimer();
|
|
116
|
+
this.resetPipeline();
|
|
117
|
+
for (const controller of this.abortControllers)
|
|
118
|
+
controller.abort();
|
|
119
|
+
this.abortControllers.clear();
|
|
120
|
+
this.sink.stop();
|
|
121
|
+
this.playbackChain = Promise.resolve();
|
|
122
|
+
this.errorReportedForTurn = false;
|
|
123
|
+
if (message && wasActive)
|
|
124
|
+
this.notify?.(`[TTS] ${message}`);
|
|
125
|
+
return wasActive;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Exit-path teardown: drops everything not yet audible (pending arm,
|
|
129
|
+
* buffered text, queued chunks) but lets the audio the user is already
|
|
130
|
+
* hearing finish naturally, capped at `drainTimeoutMs`, before the hard
|
|
131
|
+
* stop. Deliberate interrupts (Ctrl+C, /tts stop, turn cancel) keep their
|
|
132
|
+
* instant path through stop(); this is only for exiting the app while the
|
|
133
|
+
* final audio of a completed response is still draining.
|
|
134
|
+
*/
|
|
135
|
+
async stopForExit(drainTimeoutMs = 2000) {
|
|
136
|
+
this.pendingPrompt = null;
|
|
137
|
+
this.activeTurnId = null;
|
|
138
|
+
this.chunker?.reset();
|
|
139
|
+
this.chunker = null;
|
|
140
|
+
this.stopTimer();
|
|
141
|
+
this.resetPipeline();
|
|
142
|
+
// Cancel chunks that have not started playing; the chunk currently in the
|
|
143
|
+
// sink is not in this set (its controller is released before playback).
|
|
144
|
+
for (const controller of this.abortControllers)
|
|
145
|
+
controller.abort();
|
|
146
|
+
this.abortControllers.clear();
|
|
147
|
+
await this.sink.waitForDrain(drainTimeoutMs);
|
|
148
|
+
// Backstop: anything still alive after the window is torn down hard.
|
|
149
|
+
this.stop();
|
|
150
|
+
}
|
|
151
|
+
handleTurnEvent(event) {
|
|
152
|
+
if (event.type === 'TURN_SUBMITTED') {
|
|
153
|
+
this.maybeStartTurn(event.turnId, event.prompt);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!this.activeTurnId || event.turnId !== this.activeTurnId)
|
|
157
|
+
return;
|
|
158
|
+
if (event.type === 'STREAM_DELTA') {
|
|
159
|
+
this.queueTexts(this.chunker?.push(event.content) ?? []);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (event.type === 'STREAM_END') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (event.type === 'TURN_COMPLETED') {
|
|
166
|
+
this.finishTurn(event.turnId);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (event.type === 'TURN_CANCEL' || event.type === 'TURN_ERROR' || event.type === 'PREFLIGHT_FAIL') {
|
|
170
|
+
this.stop(event.type === 'TURN_CANCEL' ? 'Spoken output stopped.' : 'Spoken output stopped because the turn did not complete.');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
maybeStartTurn(turnId, prompt) {
|
|
174
|
+
if (!this.pendingPrompt)
|
|
175
|
+
return;
|
|
176
|
+
if (prompt.trim() !== this.pendingPrompt)
|
|
177
|
+
return;
|
|
178
|
+
this.pendingPrompt = null;
|
|
179
|
+
this.activeTurnId = turnId;
|
|
180
|
+
this.chunkSequence = 0;
|
|
181
|
+
this.errorReportedForTurn = false;
|
|
182
|
+
this.chunker = new TtsTextChunker({ now: this.now });
|
|
183
|
+
this.playbackChain = Promise.resolve();
|
|
184
|
+
this.resetPipeline();
|
|
185
|
+
this.startTimer();
|
|
186
|
+
this.notify?.(`[TTS] Live playback queued through ${this.sink.label}.`);
|
|
187
|
+
}
|
|
188
|
+
finishTurn(turnId) {
|
|
189
|
+
if (turnId !== this.activeTurnId)
|
|
190
|
+
return;
|
|
191
|
+
this.queueTexts(this.chunker?.flushAll() ?? []);
|
|
192
|
+
this.stopTimer();
|
|
193
|
+
this.completedTurnId = turnId;
|
|
194
|
+
// Nothing pending and nothing in flight releases immediately; otherwise
|
|
195
|
+
// the last pipeline slot to free performs the release.
|
|
196
|
+
this.maybeReleaseTurn();
|
|
197
|
+
}
|
|
198
|
+
resetPipeline() {
|
|
199
|
+
this.pendingTexts = [];
|
|
200
|
+
this.pipelineDepth = 0;
|
|
201
|
+
this.pipelineGeneration++;
|
|
202
|
+
this.completedTurnId = null;
|
|
203
|
+
}
|
|
204
|
+
startTimer() {
|
|
205
|
+
this.stopTimer();
|
|
206
|
+
this.timer = this.setIntervalImpl(() => {
|
|
207
|
+
if (!this.activeTurnId || !this.chunker)
|
|
208
|
+
return;
|
|
209
|
+
this.queueTexts(this.chunker.flushDue());
|
|
210
|
+
}, 250);
|
|
211
|
+
}
|
|
212
|
+
stopTimer() {
|
|
213
|
+
if (!this.timer)
|
|
214
|
+
return;
|
|
215
|
+
this.clearIntervalImpl(this.timer);
|
|
216
|
+
this.timer = null;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Chunker output does NOT map 1:1 to synthesis requests. Text queues here
|
|
220
|
+
* and the pump merges everything pending into one request whenever a
|
|
221
|
+
* pipeline slot is free — so the request count tracks how often the model
|
|
222
|
+
* out-paces the audio, not how many sentences it wrote. A short answer that
|
|
223
|
+
* arrives before the first pump tick is exactly one request.
|
|
224
|
+
*/
|
|
225
|
+
queueTexts(chunks) {
|
|
226
|
+
for (const chunk of chunks) {
|
|
227
|
+
if (chunk.trim())
|
|
228
|
+
this.pendingTexts.push(chunk);
|
|
229
|
+
}
|
|
230
|
+
if (this.pendingTexts.length > 0)
|
|
231
|
+
this.schedulePump();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Deferred one tick so text delivered in the same synchronous burst (fast
|
|
235
|
+
* deltas, or a turn that completes instantly) coalesces into a single
|
|
236
|
+
* request instead of firing per sentence boundary.
|
|
237
|
+
*/
|
|
238
|
+
schedulePump() {
|
|
239
|
+
if (this.pumpScheduled)
|
|
240
|
+
return;
|
|
241
|
+
this.pumpScheduled = true;
|
|
242
|
+
queueMicrotask(() => {
|
|
243
|
+
this.pumpScheduled = false;
|
|
244
|
+
this.pump();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
pump() {
|
|
248
|
+
while (this.activeTurnId && this.pendingTexts.length > 0 && this.pipelineDepth < SYNTHESIS_PIPELINE_WINDOW) {
|
|
249
|
+
this.dispatchChunk(this.takeMergedText());
|
|
250
|
+
}
|
|
251
|
+
this.maybeReleaseTurn();
|
|
252
|
+
}
|
|
253
|
+
/** Merge everything pending into one request, capped at the per-request text limit. */
|
|
254
|
+
takeMergedText() {
|
|
255
|
+
let merged = '';
|
|
256
|
+
while (this.pendingTexts.length > 0) {
|
|
257
|
+
const next = this.pendingTexts[0];
|
|
258
|
+
if (!merged && next.length > SYNTHESIS_MERGE_MAX_CHARS) {
|
|
259
|
+
// A single oversized entry (e.g. a large end-of-turn flush): split at
|
|
260
|
+
// a word boundary under the per-request cap; the rest stays queued.
|
|
261
|
+
const cut = findSplitIndex(next, SYNTHESIS_MERGE_MAX_CHARS);
|
|
262
|
+
this.pendingTexts[0] = next.slice(cut).trim();
|
|
263
|
+
return next.slice(0, cut).trim();
|
|
264
|
+
}
|
|
265
|
+
if (merged && merged.length + 1 + next.length > SYNTHESIS_MERGE_MAX_CHARS)
|
|
266
|
+
break;
|
|
267
|
+
merged = merged ? `${merged} ${next}` : next;
|
|
268
|
+
this.pendingTexts.shift();
|
|
269
|
+
}
|
|
270
|
+
return merged;
|
|
271
|
+
}
|
|
272
|
+
dispatchChunk(text) {
|
|
273
|
+
const turnId = this.activeTurnId;
|
|
274
|
+
if (!turnId || !text.trim())
|
|
275
|
+
return;
|
|
276
|
+
const sequence = ++this.chunkSequence;
|
|
277
|
+
const generation = this.pipelineGeneration;
|
|
278
|
+
this.pipelineDepth++;
|
|
279
|
+
const abortController = new AbortController();
|
|
280
|
+
this.abortControllers.add(abortController);
|
|
281
|
+
const resultPromise = this.synthesizeWithRetry(text, turnId, sequence, abortController.signal)
|
|
282
|
+
.then((result) => ({ ok: true, result }))
|
|
283
|
+
.catch((error) => ({ ok: false, error }));
|
|
284
|
+
this.playbackChain = this.playbackChain.then(async () => {
|
|
285
|
+
try {
|
|
286
|
+
if (abortController.signal.aborted) {
|
|
287
|
+
this.abortControllers.delete(abortController);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const result = await resultPromise;
|
|
291
|
+
this.abortControllers.delete(abortController);
|
|
292
|
+
// Re-check after the await: an abort that landed while synthesis was
|
|
293
|
+
// in flight (deliberate stop or exit) makes the rejection expected —
|
|
294
|
+
// it must not be reported, and it must not hard-stop a sink that may
|
|
295
|
+
// still be draining the previous chunk.
|
|
296
|
+
if (abortController.signal.aborted)
|
|
297
|
+
return;
|
|
298
|
+
if (!result.ok) {
|
|
299
|
+
// Retries are exhausted (or the failure was not transient). Skip
|
|
300
|
+
// just this chunk and keep speaking the rest of the turn — a gap in
|
|
301
|
+
// speech beats losing the whole response.
|
|
302
|
+
this.reportSkippedChunk(result.error);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
await this.sink.play(result.result.chunks, {
|
|
306
|
+
format: String(result.result.format ?? 'mp3'),
|
|
307
|
+
signal: abortController.signal,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
this.releasePipelineSlot(generation);
|
|
312
|
+
}
|
|
313
|
+
}).catch((error) => {
|
|
314
|
+
this.abortControllers.delete(abortController);
|
|
315
|
+
this.reportError(error);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
releasePipelineSlot(generation) {
|
|
319
|
+
if (generation !== this.pipelineGeneration)
|
|
320
|
+
return;
|
|
321
|
+
this.pipelineDepth = Math.max(0, this.pipelineDepth - 1);
|
|
322
|
+
if (this.pendingTexts.length > 0) {
|
|
323
|
+
this.schedulePump();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
this.maybeReleaseTurn();
|
|
327
|
+
}
|
|
328
|
+
maybeReleaseTurn() {
|
|
329
|
+
if (!this.completedTurnId || this.completedTurnId !== this.activeTurnId)
|
|
330
|
+
return;
|
|
331
|
+
if (this.pendingTexts.length > 0 || this.pipelineDepth > 0 || this.pumpScheduled)
|
|
332
|
+
return;
|
|
333
|
+
this.activeTurnId = null;
|
|
334
|
+
this.completedTurnId = null;
|
|
335
|
+
this.chunker = null;
|
|
336
|
+
this.abortControllers.clear();
|
|
337
|
+
}
|
|
338
|
+
async synthesizeWithRetry(text, turnId, sequence, signal) {
|
|
339
|
+
for (let attempt = 0;; attempt++) {
|
|
340
|
+
try {
|
|
341
|
+
return await this.synthesize(text, turnId, sequence, signal);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
const retryable = attempt < SYNTHESIS_RETRY_DELAYS_MS.length
|
|
345
|
+
&& !signal.aborted
|
|
346
|
+
&& isTransientSynthesisError(error);
|
|
347
|
+
if (!retryable)
|
|
348
|
+
throw error;
|
|
349
|
+
await this.delay(SYNTHESIS_RETRY_DELAYS_MS[attempt], signal);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** Abortable backoff sleep — an abort clears the timer and rejects, so a stop mid-backoff leaves nothing running. */
|
|
354
|
+
delay(ms, signal) {
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
if (signal.aborted) {
|
|
357
|
+
reject(new Error('Synthesis retry cancelled'));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const timer = this.setTimeoutImpl(() => {
|
|
361
|
+
signal.removeEventListener('abort', onAbort);
|
|
362
|
+
resolve();
|
|
363
|
+
}, ms);
|
|
364
|
+
const onAbort = () => {
|
|
365
|
+
this.clearTimeoutImpl(timer);
|
|
366
|
+
reject(new Error('Synthesis retry cancelled'));
|
|
367
|
+
};
|
|
368
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
synthesize(text, turnId, sequence, signal) {
|
|
372
|
+
return this.voiceService.synthesizeStream(readOptionalConfigString(this.configManager, 'tts.provider'), {
|
|
373
|
+
text,
|
|
374
|
+
voiceId: readOptionalConfigString(this.configManager, 'tts.voice'),
|
|
375
|
+
format: 'mp3',
|
|
376
|
+
speed: readOptionalConfigNumber(this.configManager, 'tts.speed'),
|
|
377
|
+
signal,
|
|
378
|
+
metadata: {
|
|
379
|
+
source: this.source,
|
|
380
|
+
feature: 'live-tts',
|
|
381
|
+
turnId,
|
|
382
|
+
sequence,
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* One synthesis request failed after its retries. Report once per turn and
|
|
388
|
+
* keep going — the rest of the response still plays.
|
|
389
|
+
*/
|
|
390
|
+
reportSkippedChunk(error) {
|
|
391
|
+
if (this.errorReportedForTurn)
|
|
392
|
+
return;
|
|
393
|
+
this.errorReportedForTurn = true;
|
|
394
|
+
this.notify?.(`[TTS] Skipping part of the spoken response — synthesis kept failing (${summarizeError(error)}). Playback continues with the rest.`);
|
|
395
|
+
}
|
|
396
|
+
reportError(error) {
|
|
397
|
+
if (this.errorReportedForTurn)
|
|
398
|
+
return;
|
|
399
|
+
this.errorReportedForTurn = true;
|
|
400
|
+
this.activeTurnId = null;
|
|
401
|
+
this.chunker = null;
|
|
402
|
+
this.stopTimer();
|
|
403
|
+
this.resetPipeline();
|
|
404
|
+
for (const controller of this.abortControllers)
|
|
405
|
+
controller.abort();
|
|
406
|
+
this.abortControllers.clear();
|
|
407
|
+
this.sink.stop();
|
|
408
|
+
this.playbackChain = Promise.resolve();
|
|
409
|
+
this.notify?.(`[TTS] Live playback stopped: ${summarizeError(error)}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Transient = worth a bounded retry: rate/concurrency limits (HTTP 429),
|
|
414
|
+
* transient server errors (5xx), and network-level drops. The SDK's voice
|
|
415
|
+
* providers throw plain Error strings with the HTTP status embedded in the
|
|
416
|
+
* message, so classification is by message content.
|
|
417
|
+
*/
|
|
418
|
+
function isTransientSynthesisError(error) {
|
|
419
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
420
|
+
if (message.includes('429') || message.includes('rate limit') || message.includes('rate_limit')
|
|
421
|
+
|| message.includes('too many requests') || message.includes('concurrent'))
|
|
422
|
+
return true;
|
|
423
|
+
if (/http 5\d\d/.test(message))
|
|
424
|
+
return true;
|
|
425
|
+
return message.includes('fetch failed') || message.includes('network')
|
|
426
|
+
|| message.includes('timed out') || message.includes('timeout')
|
|
427
|
+
|| message.includes('econnreset') || message.includes('socket');
|
|
428
|
+
}
|
|
429
|
+
/** Split point at or under `limit`, preferring the last word boundary. */
|
|
430
|
+
function findSplitIndex(text, limit) {
|
|
431
|
+
const space = text.lastIndexOf(' ', limit);
|
|
432
|
+
return space > 0 ? space : limit;
|
|
433
|
+
}
|
|
434
|
+
function readOptionalConfigString(configManager, key) {
|
|
435
|
+
const value = String(configManager.get(key) ?? '').trim();
|
|
436
|
+
return value || undefined;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* readOptionalConfigNumber — reads a numeric config value by key.
|
|
440
|
+
*
|
|
441
|
+
* Accepts a string key and casts it, returning undefined when the value is
|
|
442
|
+
* absent, zero, or not a finite positive number.
|
|
443
|
+
*/
|
|
444
|
+
function readOptionalConfigNumber(configManager, key) {
|
|
445
|
+
const raw = configManager.get(key);
|
|
446
|
+
const value = typeof raw === 'number' ? raw : parseFloat(String(raw ?? ''));
|
|
447
|
+
return isFinite(value) && value > 0 ? value : undefined;
|
|
448
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spoken-turn — the shared spoken-output (live TTS) policy engine.
|
|
3
|
+
*
|
|
4
|
+
* SDK owns POLICY: the sentence chunker, the bounded 2-slot synthesis window
|
|
5
|
+
* with merge coalescing, the transient-failure retry/backoff with honest
|
|
6
|
+
* skip-and-continue, and the turn-lifecycle state machine with drain-vs-
|
|
7
|
+
* interrupt semantics. Consumers own I/O by supplying an {@link AudioSink}
|
|
8
|
+
* (a terminal subprocess player, a browser Web Audio sink, or a test fake) and,
|
|
9
|
+
* where they run their own scheduler, injectable clocks/timers.
|
|
10
|
+
*/
|
|
11
|
+
export { SpokenTurnController } from './controller.js';
|
|
12
|
+
export type { SpokenTurnControllerOptions } from './controller.js';
|
|
13
|
+
export { TtsTextChunker, normalizeSpeechText } from './text-chunker.js';
|
|
14
|
+
export type { TtsTextChunkerOptions } from './text-chunker.js';
|
|
15
|
+
export type { AudioSink, AudioSinkPlaybackOptions } from './audio-sink.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/platform/voice/spoken-turn/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spoken-turn — the shared spoken-output (live TTS) policy engine.
|
|
3
|
+
*
|
|
4
|
+
* SDK owns POLICY: the sentence chunker, the bounded 2-slot synthesis window
|
|
5
|
+
* with merge coalescing, the transient-failure retry/backoff with honest
|
|
6
|
+
* skip-and-continue, and the turn-lifecycle state machine with drain-vs-
|
|
7
|
+
* interrupt semantics. Consumers own I/O by supplying an {@link AudioSink}
|
|
8
|
+
* (a terminal subprocess player, a browser Web Audio sink, or a test fake) and,
|
|
9
|
+
* where they run their own scheduler, injectable clocks/timers.
|
|
10
|
+
*/
|
|
11
|
+
export { SpokenTurnController } from './controller.js';
|
|
12
|
+
export { TtsTextChunker, normalizeSpeechText } from './text-chunker.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TtsTextChunker — turns a stream of provider content deltas into speech-sized
|
|
3
|
+
* chunks at sentence boundaries, with a max-length cut and a latency flush so a
|
|
4
|
+
* long unpunctuated run still starts speaking. This is pure policy (no I/O, no
|
|
5
|
+
* timers of its own — the caller drives `flushDue()` on its own clock), so it
|
|
6
|
+
* lives in the SDK and is shared verbatim by every voice consumer.
|
|
7
|
+
*/
|
|
8
|
+
export interface TtsTextChunkerOptions {
|
|
9
|
+
readonly minBoundaryChars?: number | undefined;
|
|
10
|
+
readonly maxChunkChars?: number | undefined;
|
|
11
|
+
readonly maxLatencyMs?: number | undefined;
|
|
12
|
+
readonly now?: (() => number) | undefined;
|
|
13
|
+
}
|
|
14
|
+
export declare class TtsTextChunker {
|
|
15
|
+
private buffer;
|
|
16
|
+
private firstBufferedAt;
|
|
17
|
+
private readonly minBoundaryChars;
|
|
18
|
+
private readonly maxChunkChars;
|
|
19
|
+
private readonly maxLatencyMs;
|
|
20
|
+
private readonly now;
|
|
21
|
+
constructor(options?: TtsTextChunkerOptions);
|
|
22
|
+
push(delta: string): string[];
|
|
23
|
+
flushDue(): string[];
|
|
24
|
+
flushAll(): string[];
|
|
25
|
+
reset(): void;
|
|
26
|
+
private drainReady;
|
|
27
|
+
private findBoundary;
|
|
28
|
+
private findLatestSentenceBoundary;
|
|
29
|
+
private findWordBoundaryBefore;
|
|
30
|
+
private takeChunk;
|
|
31
|
+
}
|
|
32
|
+
export declare function normalizeSpeechText(text: string): string;
|
|
33
|
+
//# sourceMappingURL=text-chunker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-chunker.d.ts","sourceRoot":"","sources":["../../../../src/platform/voice/spoken-turn/text-chunker.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,CAAC;CAC3C;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,GAAE,qBAA0B;IAO/C,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAO7B,QAAQ,IAAI,MAAM,EAAE;IAMpB,QAAQ,IAAI,MAAM,EAAE;IASpB,KAAK,IAAI,IAAI;IAKb,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,0BAA0B;IAelC,OAAO,CAAC,sBAAsB;IAQ9B,OAAO,CAAC,SAAS;CAMlB;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export class TtsTextChunker {
|
|
2
|
+
buffer = '';
|
|
3
|
+
firstBufferedAt = null;
|
|
4
|
+
minBoundaryChars;
|
|
5
|
+
maxChunkChars;
|
|
6
|
+
maxLatencyMs;
|
|
7
|
+
now;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.minBoundaryChars = options.minBoundaryChars ?? 24;
|
|
10
|
+
this.maxChunkChars = options.maxChunkChars ?? 320;
|
|
11
|
+
this.maxLatencyMs = options.maxLatencyMs ?? 1_000;
|
|
12
|
+
this.now = options.now ?? (() => Date.now());
|
|
13
|
+
}
|
|
14
|
+
push(delta) {
|
|
15
|
+
if (!delta)
|
|
16
|
+
return [];
|
|
17
|
+
if (this.firstBufferedAt === null)
|
|
18
|
+
this.firstBufferedAt = this.now();
|
|
19
|
+
this.buffer += delta;
|
|
20
|
+
return this.drainReady(false);
|
|
21
|
+
}
|
|
22
|
+
flushDue() {
|
|
23
|
+
if (!this.buffer.trim() || this.firstBufferedAt === null)
|
|
24
|
+
return [];
|
|
25
|
+
if (this.now() - this.firstBufferedAt < this.maxLatencyMs)
|
|
26
|
+
return [];
|
|
27
|
+
return this.drainReady(true);
|
|
28
|
+
}
|
|
29
|
+
flushAll() {
|
|
30
|
+
if (!this.buffer.trim()) {
|
|
31
|
+
this.buffer = '';
|
|
32
|
+
this.firstBufferedAt = null;
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return [this.takeChunk(this.buffer.length)].filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
reset() {
|
|
38
|
+
this.buffer = '';
|
|
39
|
+
this.firstBufferedAt = null;
|
|
40
|
+
}
|
|
41
|
+
drainReady(forceLatencyFlush) {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
while (this.buffer.trim()) {
|
|
44
|
+
const boundary = this.findBoundary(forceLatencyFlush);
|
|
45
|
+
if (boundary <= 0)
|
|
46
|
+
break;
|
|
47
|
+
const chunk = this.takeChunk(boundary);
|
|
48
|
+
if (chunk)
|
|
49
|
+
chunks.push(chunk);
|
|
50
|
+
forceLatencyFlush = false;
|
|
51
|
+
}
|
|
52
|
+
return chunks;
|
|
53
|
+
}
|
|
54
|
+
findBoundary(forceLatencyFlush) {
|
|
55
|
+
const latestSentence = this.findLatestSentenceBoundary();
|
|
56
|
+
if (latestSentence >= this.minBoundaryChars)
|
|
57
|
+
return latestSentence;
|
|
58
|
+
if (this.buffer.length >= this.maxChunkChars) {
|
|
59
|
+
return this.findWordBoundaryBefore(this.maxChunkChars) || this.maxChunkChars;
|
|
60
|
+
}
|
|
61
|
+
if (forceLatencyFlush) {
|
|
62
|
+
return this.buffer.length;
|
|
63
|
+
}
|
|
64
|
+
return -1;
|
|
65
|
+
}
|
|
66
|
+
findLatestSentenceBoundary() {
|
|
67
|
+
let best = -1;
|
|
68
|
+
for (let i = 0; i < this.buffer.length; i++) {
|
|
69
|
+
const char = this.buffer[i];
|
|
70
|
+
if (char !== '.' && char !== '!' && char !== '?' && char !== ';' && char !== ':' && char !== '\n') {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const next = this.buffer[i + 1];
|
|
74
|
+
if (i === this.buffer.length - 1 || next === undefined || /\s/.test(next)) {
|
|
75
|
+
best = i + 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return best;
|
|
79
|
+
}
|
|
80
|
+
findWordBoundaryBefore(index) {
|
|
81
|
+
const max = Math.min(index, this.buffer.length);
|
|
82
|
+
for (let i = max; i > 0; i--) {
|
|
83
|
+
if (/\s/.test(this.buffer[i - 1] ?? ''))
|
|
84
|
+
return i;
|
|
85
|
+
}
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
88
|
+
takeChunk(end) {
|
|
89
|
+
const raw = this.buffer.slice(0, end);
|
|
90
|
+
this.buffer = this.buffer.slice(end);
|
|
91
|
+
this.firstBufferedAt = this.buffer.trim() ? this.now() : null;
|
|
92
|
+
return normalizeSpeechText(raw);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function normalizeSpeechText(text) {
|
|
96
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
97
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "TypeScript SDK for building GoodVibes operator, peer, web, mobile, and daemon-connected apps with typed contracts, auth, realtime events, and transport layers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goodvibes",
|
|
@@ -477,14 +477,14 @@
|
|
|
477
477
|
"sideEffects": false,
|
|
478
478
|
"type": "module",
|
|
479
479
|
"dependencies": {
|
|
480
|
-
"@pellux/goodvibes-contracts": "1.
|
|
481
|
-
"@pellux/goodvibes-daemon-sdk": "1.
|
|
482
|
-
"@pellux/goodvibes-errors": "1.
|
|
483
|
-
"@pellux/goodvibes-operator-sdk": "1.
|
|
484
|
-
"@pellux/goodvibes-peer-sdk": "1.
|
|
485
|
-
"@pellux/goodvibes-transport-core": "1.
|
|
486
|
-
"@pellux/goodvibes-transport-http": "1.
|
|
487
|
-
"@pellux/goodvibes-transport-realtime": "1.
|
|
480
|
+
"@pellux/goodvibes-contracts": "1.3.0",
|
|
481
|
+
"@pellux/goodvibes-daemon-sdk": "1.3.0",
|
|
482
|
+
"@pellux/goodvibes-errors": "1.3.0",
|
|
483
|
+
"@pellux/goodvibes-operator-sdk": "1.3.0",
|
|
484
|
+
"@pellux/goodvibes-peer-sdk": "1.3.0",
|
|
485
|
+
"@pellux/goodvibes-transport-core": "1.3.0",
|
|
486
|
+
"@pellux/goodvibes-transport-http": "1.3.0",
|
|
487
|
+
"@pellux/goodvibes-transport-realtime": "1.3.0"
|
|
488
488
|
},
|
|
489
489
|
"optionalDependencies": {
|
|
490
490
|
"@agentclientprotocol/sdk": "^0.21.0",
|