@pellux/goodvibes-sdk 1.0.0 → 1.2.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.
Files changed (241) hide show
  1. package/dist/contracts/artifacts/operator-contract.json +2878 -264
  2. package/dist/events/communication.d.ts +1 -1
  3. package/dist/platform/agents/orchestrator-runner.d.ts +5 -5
  4. package/dist/platform/agents/orchestrator-runner.js +6 -6
  5. package/dist/platform/agents/orchestrator.d.ts +4 -4
  6. package/dist/platform/agents/orchestrator.js +2 -2
  7. package/dist/platform/agents/wrfc-controller.d.ts +1 -1
  8. package/dist/platform/agents/wrfc-controller.js +3 -3
  9. package/dist/platform/calendar/index.d.ts +2 -2
  10. package/dist/platform/calendar/index.js +3 -3
  11. package/dist/platform/calendar/oauth-providers.d.ts +1 -1
  12. package/dist/platform/calendar/oauth-providers.js +1 -1
  13. package/dist/platform/calendar/oauth-types.d.ts +1 -1
  14. package/dist/platform/calendar/oauth-types.js +1 -1
  15. package/dist/platform/companion/companion-chat-branching.d.ts +66 -0
  16. package/dist/platform/companion/companion-chat-branching.d.ts.map +1 -0
  17. package/dist/platform/companion/companion-chat-branching.js +142 -0
  18. package/dist/platform/companion/companion-chat-broker-bridge.d.ts +1 -1
  19. package/dist/platform/companion/companion-chat-broker-bridge.d.ts.map +1 -1
  20. package/dist/platform/companion/companion-chat-broker-sync.d.ts +2 -2
  21. package/dist/platform/companion/companion-chat-broker-sync.d.ts.map +1 -1
  22. package/dist/platform/companion/companion-chat-broker-sync.js +1 -1
  23. package/dist/platform/companion/companion-chat-manager.d.ts +26 -5
  24. package/dist/platform/companion/companion-chat-manager.d.ts.map +1 -1
  25. package/dist/platform/companion/companion-chat-manager.js +67 -81
  26. package/dist/platform/companion/companion-chat-routes.d.ts.map +1 -1
  27. package/dist/platform/companion/companion-chat-routes.js +77 -1
  28. package/dist/platform/companion/companion-chat-turn-execution.d.ts +61 -0
  29. package/dist/platform/companion/companion-chat-turn-execution.d.ts.map +1 -0
  30. package/dist/platform/companion/companion-chat-turn-execution.js +107 -0
  31. package/dist/platform/companion/companion-chat-types.d.ts +67 -0
  32. package/dist/platform/companion/companion-chat-types.d.ts.map +1 -1
  33. package/dist/platform/config/credential-status.d.ts +1 -1
  34. package/dist/platform/config/credential-status.js +1 -1
  35. package/dist/platform/config/index.d.ts +1 -1
  36. package/dist/platform/config/index.js +1 -1
  37. package/dist/platform/config/manager.d.ts +1 -1
  38. package/dist/platform/config/manager.js +1 -1
  39. package/dist/platform/config/migrations.d.ts +2 -2
  40. package/dist/platform/config/migrations.js +2 -2
  41. package/dist/platform/config/schema-domain-runtime.d.ts +8 -0
  42. package/dist/platform/config/schema-domain-runtime.d.ts.map +1 -1
  43. package/dist/platform/config/schema-domain-runtime.js +32 -0
  44. package/dist/platform/config/schema-types.d.ts +10 -2
  45. package/dist/platform/config/schema-types.d.ts.map +1 -1
  46. package/dist/platform/control-plane/gateway-scope-enforcement.js +2 -2
  47. package/dist/platform/control-plane/index.d.ts +2 -0
  48. package/dist/platform/control-plane/index.d.ts.map +1 -1
  49. package/dist/platform/control-plane/index.js +5 -1
  50. package/dist/platform/control-plane/invoke-input-validation.d.ts +1 -1
  51. package/dist/platform/control-plane/invoke-input-validation.js +1 -1
  52. package/dist/platform/control-plane/method-catalog-calendar.d.ts +2 -2
  53. package/dist/platform/control-plane/method-catalog-calendar.js +2 -2
  54. package/dist/platform/control-plane/method-catalog-channels.js +3 -3
  55. package/dist/platform/control-plane/method-catalog-control-automation.js +4 -4
  56. package/dist/platform/control-plane/method-catalog-control-companion.d.ts +1 -1
  57. package/dist/platform/control-plane/method-catalog-control-companion.d.ts.map +1 -1
  58. package/dist/platform/control-plane/method-catalog-control-companion.js +43 -0
  59. package/dist/platform/control-plane/method-catalog-control-core.js +3 -3
  60. package/dist/platform/control-plane/method-catalog-email.d.ts +1 -1
  61. package/dist/platform/control-plane/method-catalog-email.js +1 -1
  62. package/dist/platform/control-plane/method-catalog-fleet.d.ts +1 -1
  63. package/dist/platform/control-plane/method-catalog-push.d.ts +30 -0
  64. package/dist/platform/control-plane/method-catalog-push.d.ts.map +1 -0
  65. package/dist/platform/control-plane/method-catalog-push.js +80 -0
  66. package/dist/platform/control-plane/method-catalog-route-reconcile.d.ts +1 -1
  67. package/dist/platform/control-plane/method-catalog-runtime.d.ts.map +1 -1
  68. package/dist/platform/control-plane/method-catalog-runtime.js +177 -1
  69. package/dist/platform/control-plane/method-catalog.d.ts.map +1 -1
  70. package/dist/platform/control-plane/method-catalog.js +2 -0
  71. package/dist/platform/control-plane/operator-contract-schemas-fleet.js +1 -1
  72. package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts +15 -0
  73. package/dist/platform/control-plane/operator-contract-schemas-runtime.d.ts.map +1 -1
  74. package/dist/platform/control-plane/operator-contract-schemas-runtime.js +81 -0
  75. package/dist/platform/control-plane/routes/checkpoints.d.ts +1 -1
  76. package/dist/platform/control-plane/routes/fleet.d.ts +2 -2
  77. package/dist/platform/control-plane/routes/fleet.js +1 -1
  78. package/dist/platform/control-plane/routes/gateway-verb-error.d.ts +1 -1
  79. package/dist/platform/control-plane/routes/gateway-verb-error.js +1 -1
  80. package/dist/platform/control-plane/routes/index.js +1 -1
  81. package/dist/platform/control-plane/routes/invocation-params.d.ts +1 -1
  82. package/dist/platform/control-plane/routes/push.d.ts +26 -0
  83. package/dist/platform/control-plane/routes/push.d.ts.map +1 -0
  84. package/dist/platform/control-plane/routes/push.js +104 -0
  85. package/dist/platform/control-plane/routes/register-gateway-verb-groups.d.ts +31 -0
  86. package/dist/platform/control-plane/routes/register-gateway-verb-groups.d.ts.map +1 -0
  87. package/dist/platform/control-plane/routes/register-gateway-verb-groups.js +16 -0
  88. package/dist/platform/control-plane/routes/register-w3-s2.d.ts +2 -2
  89. package/dist/platform/control-plane/routes/session-search.d.ts +4 -4
  90. package/dist/platform/control-plane/session-broker.d.ts +1 -1
  91. package/dist/platform/control-plane/session-broker.js +1 -1
  92. package/dist/platform/control-plane/session-store-importer.d.ts +1 -1
  93. package/dist/platform/control-plane/session-store-importer.js +1 -1
  94. package/dist/platform/control-plane/session-types.d.ts +1 -1
  95. package/dist/platform/core/orchestrator-runtime.d.ts +4 -4
  96. package/dist/platform/core/orchestrator-tool-runtime.d.ts +1 -1
  97. package/dist/platform/core/orchestrator-turn-loop.d.ts +6 -6
  98. package/dist/platform/core/orchestrator-turn-loop.d.ts.map +1 -1
  99. package/dist/platform/core/orchestrator-turn-loop.js +23 -22
  100. package/dist/platform/core/orchestrator.d.ts +10 -10
  101. package/dist/platform/core/orchestrator.d.ts.map +1 -1
  102. package/dist/platform/core/orchestrator.js +8 -8
  103. package/dist/platform/daemon/boot.d.ts +6 -0
  104. package/dist/platform/daemon/boot.d.ts.map +1 -1
  105. package/dist/platform/daemon/boot.js +1 -0
  106. package/dist/platform/daemon/control-plane.js +2 -2
  107. package/dist/platform/daemon/facade.d.ts +4 -7
  108. package/dist/platform/daemon/facade.d.ts.map +1 -1
  109. package/dist/platform/daemon/facade.js +5 -7
  110. package/dist/platform/daemon/http/router-session-broker-adapter.d.ts +1 -1
  111. package/dist/platform/daemon/http/router.d.ts +1 -0
  112. package/dist/platform/daemon/http/router.d.ts.map +1 -1
  113. package/dist/platform/daemon/http/router.js +36 -19
  114. package/dist/platform/daemon/http/runtime-route-types.d.ts +1 -1
  115. package/dist/platform/daemon/http/runtime-route-types.d.ts.map +1 -1
  116. package/dist/platform/daemon/http/webui-serving.d.ts +68 -0
  117. package/dist/platform/daemon/http/webui-serving.d.ts.map +1 -0
  118. package/dist/platform/daemon/http/webui-serving.js +230 -0
  119. package/dist/platform/knowledge/knowledge-api.d.ts +1 -1
  120. package/dist/platform/orchestration/budget.d.ts +2 -2
  121. package/dist/platform/orchestration/cancellation.d.ts +2 -2
  122. package/dist/platform/orchestration/controller-compat.d.ts +2 -3
  123. package/dist/platform/orchestration/controller-compat.d.ts.map +1 -1
  124. package/dist/platform/orchestration/dirty-guard.js +1 -1
  125. package/dist/platform/orchestration/engine.d.ts.map +1 -1
  126. package/dist/platform/orchestration/engine.js +3 -4
  127. package/dist/platform/orchestration/persistence.js +2 -2
  128. package/dist/platform/orchestration/phase-runner.d.ts +4 -4
  129. package/dist/platform/orchestration/scheduler.d.ts +1 -1
  130. package/dist/platform/orchestration/types.d.ts +3 -3
  131. package/dist/platform/presentation/glyphs.d.ts +1 -1
  132. package/dist/platform/presentation/glyphs.js +1 -1
  133. package/dist/platform/presentation/index.d.ts +3 -3
  134. package/dist/platform/presentation/index.js +3 -3
  135. package/dist/platform/presentation/thinking-phrases.d.ts +1 -1
  136. package/dist/platform/presentation/thinking-phrases.js +1 -1
  137. package/dist/platform/presentation/waiting-wording.d.ts +1 -1
  138. package/dist/platform/presentation/waiting-wording.js +1 -1
  139. package/dist/platform/push/delivery.d.ts +48 -0
  140. package/dist/platform/push/delivery.d.ts.map +1 -0
  141. package/dist/platform/push/delivery.js +117 -0
  142. package/dist/platform/push/encryption.d.ts +41 -0
  143. package/dist/platform/push/encryption.d.ts.map +1 -0
  144. package/dist/platform/push/encryption.js +82 -0
  145. package/dist/platform/push/index.d.ts +17 -0
  146. package/dist/platform/push/index.d.ts.map +1 -0
  147. package/dist/platform/push/index.js +10 -0
  148. package/dist/platform/push/service.d.ts +75 -0
  149. package/dist/platform/push/service.d.ts.map +1 -0
  150. package/dist/platform/push/service.js +95 -0
  151. package/dist/platform/push/subscription-store.d.ts +50 -0
  152. package/dist/platform/push/subscription-store.d.ts.map +1 -0
  153. package/dist/platform/push/subscription-store.js +123 -0
  154. package/dist/platform/push/types.d.ts +68 -0
  155. package/dist/platform/push/types.d.ts.map +1 -0
  156. package/dist/platform/push/types.js +13 -0
  157. package/dist/platform/push/vapid.d.ts +54 -0
  158. package/dist/platform/push/vapid.d.ts.map +1 -0
  159. package/dist/platform/push/vapid.js +113 -0
  160. package/dist/platform/runtime/feature-flags/flags.js +3 -3
  161. package/dist/platform/runtime/fleet/adapters/agent.d.ts +2 -2
  162. package/dist/platform/runtime/fleet/adapters/agent.d.ts.map +1 -1
  163. package/dist/platform/runtime/fleet/adapters/agent.js +4 -4
  164. package/dist/platform/runtime/fleet/adapters/automation.d.ts +1 -1
  165. package/dist/platform/runtime/fleet/adapters/automation.js +1 -1
  166. package/dist/platform/runtime/fleet/adapters/code-index.d.ts +1 -1
  167. package/dist/platform/runtime/fleet/adapters/code-index.js +1 -1
  168. package/dist/platform/runtime/fleet/adapters/orchestration.js +1 -1
  169. package/dist/platform/runtime/fleet/adapters/schedule.d.ts +4 -4
  170. package/dist/platform/runtime/fleet/adapters/schedule.js +4 -4
  171. package/dist/platform/runtime/fleet/adapters/trigger.d.ts +3 -4
  172. package/dist/platform/runtime/fleet/adapters/trigger.d.ts.map +1 -1
  173. package/dist/platform/runtime/fleet/adapters/trigger.js +3 -4
  174. package/dist/platform/runtime/fleet/adapters/wrfc.js +1 -1
  175. package/dist/platform/runtime/fleet/registry.d.ts +7 -7
  176. package/dist/platform/runtime/fleet/registry.d.ts.map +1 -1
  177. package/dist/platform/runtime/fleet/registry.js +3 -3
  178. package/dist/platform/runtime/fleet/types.d.ts +9 -9
  179. package/dist/platform/runtime/fleet/types.d.ts.map +1 -1
  180. package/dist/platform/runtime/memory-spine/client.d.ts +240 -0
  181. package/dist/platform/runtime/memory-spine/client.d.ts.map +1 -0
  182. package/dist/platform/runtime/memory-spine/client.js +221 -0
  183. package/dist/platform/runtime/memory-spine/index.d.ts +17 -0
  184. package/dist/platform/runtime/memory-spine/index.d.ts.map +1 -0
  185. package/dist/platform/runtime/memory-spine/index.js +16 -0
  186. package/dist/platform/runtime/memory-spine/recall-snapshot.d.ts +74 -0
  187. package/dist/platform/runtime/memory-spine/recall-snapshot.d.ts.map +1 -0
  188. package/dist/platform/runtime/memory-spine/recall-snapshot.js +74 -0
  189. package/dist/platform/runtime/operator-client.d.ts +1 -1
  190. package/dist/platform/runtime/services.d.ts +6 -6
  191. package/dist/platform/runtime/services.d.ts.map +1 -1
  192. package/dist/platform/runtime/services.js +9 -9
  193. package/dist/platform/runtime/session-spine/union-cache.d.ts +1 -1
  194. package/dist/platform/runtime/session-spine/union-cache.js +1 -1
  195. package/dist/platform/state/canonical-memory.d.ts +7 -7
  196. package/dist/platform/state/canonical-memory.js +8 -8
  197. package/dist/platform/state/code-index-chunking.js +1 -1
  198. package/dist/platform/state/code-index-db.d.ts +1 -1
  199. package/dist/platform/state/code-index-store.js +1 -1
  200. package/dist/platform/state/index.d.ts +2 -2
  201. package/dist/platform/state/index.d.ts.map +1 -1
  202. package/dist/platform/state/index.js +1 -1
  203. package/dist/platform/state/memory-recall-contract.d.ts +59 -4
  204. package/dist/platform/state/memory-recall-contract.d.ts.map +1 -1
  205. package/dist/platform/state/memory-recall-contract.js +79 -3
  206. package/dist/platform/state/memory-registry.d.ts +8 -0
  207. package/dist/platform/state/memory-registry.d.ts.map +1 -1
  208. package/dist/platform/state/memory-registry.js +10 -0
  209. package/dist/platform/state/memory-vector-store.js +1 -1
  210. package/dist/platform/state/sqlite-vec-loader.d.ts +1 -1
  211. package/dist/platform/state/sqlite-vec-loader.js +1 -1
  212. package/dist/platform/state/vibe-projection.d.ts +3 -3
  213. package/dist/platform/state/vibe-projection.js +3 -3
  214. package/dist/platform/tools/agent/manager.d.ts +10 -10
  215. package/dist/platform/tools/agent/manager.js +7 -7
  216. package/dist/platform/tools/exec/runtime.js +5 -5
  217. package/dist/platform/tools/exec/schema.d.ts +1 -1
  218. package/dist/platform/tools/exec/schema.d.ts.map +1 -1
  219. package/dist/platform/tools/fetch/runtime.d.ts +1 -1
  220. package/dist/platform/tools/registry.d.ts +1 -1
  221. package/dist/platform/tools/registry.js +1 -1
  222. package/dist/platform/types/tools.d.ts +1 -1
  223. package/dist/platform/utils/request-body.d.ts.map +1 -1
  224. package/dist/platform/utils/request-body.js +50 -5
  225. package/dist/platform/version.js +1 -1
  226. package/dist/platform/voice/index.d.ts +2 -0
  227. package/dist/platform/voice/index.d.ts.map +1 -1
  228. package/dist/platform/voice/index.js +3 -0
  229. package/dist/platform/voice/spoken-turn/audio-sink.d.ts +111 -0
  230. package/dist/platform/voice/spoken-turn/audio-sink.d.ts.map +1 -0
  231. package/dist/platform/voice/spoken-turn/audio-sink.js +1 -0
  232. package/dist/platform/voice/spoken-turn/controller.d.ts +117 -0
  233. package/dist/platform/voice/spoken-turn/controller.d.ts.map +1 -0
  234. package/dist/platform/voice/spoken-turn/controller.js +448 -0
  235. package/dist/platform/voice/spoken-turn/index.d.ts +16 -0
  236. package/dist/platform/voice/spoken-turn/index.d.ts.map +1 -0
  237. package/dist/platform/voice/spoken-turn/index.js +12 -0
  238. package/dist/platform/voice/spoken-turn/text-chunker.d.ts +33 -0
  239. package/dist/platform/voice/spoken-turn/text-chunker.d.ts.map +1 -0
  240. package/dist/platform/voice/spoken-turn/text-chunker.js +97 -0
  241. package/package.json +13 -9
@@ -0,0 +1,117 @@
1
+ import type { ConfigManager } from '../../config/index.js';
2
+ import type { TurnEvent } from '../../../events/turn.js';
3
+ import type { VoiceService } from '../service.js';
4
+ import type { AudioSink } from './audio-sink.js';
5
+ export interface SpokenTurnControllerOptions {
6
+ readonly voiceService: Pick<VoiceService, 'synthesizeStream'>;
7
+ readonly configManager: Pick<ConfigManager, 'get'>;
8
+ /** The injectable I/O boundary. See {@link AudioSink}. */
9
+ readonly sink: AudioSink;
10
+ readonly notify?: ((message: string) => void) | undefined;
11
+ /**
12
+ * Attribution label recorded in each synthesis request's metadata.source
13
+ * (e.g. 'goodvibes-tui', 'goodvibes-agent', 'goodvibes-webui'). Defaults to
14
+ * 'goodvibes-sdk'.
15
+ */
16
+ readonly source?: string | undefined;
17
+ readonly now?: (() => number) | undefined;
18
+ readonly setInterval?: typeof setInterval | undefined;
19
+ readonly clearInterval?: typeof clearInterval | undefined;
20
+ /** Injectable clock for retry backoff timers (tests use fakes). */
21
+ readonly setTimeout?: typeof setTimeout | undefined;
22
+ readonly clearTimeout?: typeof clearTimeout | undefined;
23
+ }
24
+ /**
25
+ * SpokenTurnController — the shared spoken-output policy engine. It watches a
26
+ * turn's lifecycle events, chunks the streamed answer into speech-sized pieces,
27
+ * merges and dispatches synthesis requests through a bounded 2-slot window with
28
+ * retry/backoff, and drives an injected {@link AudioSink} with honest
29
+ * drain-vs-interrupt semantics. All I/O is the sink's; this class is pure
30
+ * policy and runs unchanged in the terminal, the agent, and the browser.
31
+ */
32
+ export declare class SpokenTurnController {
33
+ private pendingPrompt;
34
+ private activeTurnId;
35
+ private chunker;
36
+ private chunkSequence;
37
+ private playbackChain;
38
+ private readonly abortControllers;
39
+ private timer;
40
+ private errorReportedForTurn;
41
+ private noSinkNoticed;
42
+ /** Chunker output waiting to be merged into a synthesis request. */
43
+ private pendingTexts;
44
+ /** Requests currently in the pipeline (synthesizing / waiting / playing). */
45
+ private pipelineDepth;
46
+ /** Bumped on every teardown so stale pipeline releases are ignored. */
47
+ private pipelineGeneration;
48
+ private pumpScheduled;
49
+ /** Set when TURN_COMPLETED arrives; the turn releases once the pipeline drains. */
50
+ private completedTurnId;
51
+ private readonly voiceService;
52
+ private readonly configManager;
53
+ private readonly sink;
54
+ private readonly notify?;
55
+ private readonly source;
56
+ private readonly now;
57
+ private readonly setIntervalImpl;
58
+ private readonly clearIntervalImpl;
59
+ private readonly setTimeoutImpl;
60
+ private readonly clearTimeoutImpl;
61
+ constructor(options: SpokenTurnControllerOptions);
62
+ submitNextTurn(prompt: string): boolean;
63
+ /**
64
+ * Returns whether speech was actually ACTIVE when stopped. The notice only
65
+ * prints in that case — stop() on an idle controller used to notify anyway,
66
+ * spamming "[TTS] Spoken output stopped." on every Ctrl+C (an earlier replay
67
+ * fix); callers use the return to decide whether the press "did a
68
+ * job" (see handleCtrlC's consume-on-speech-stop).
69
+ */
70
+ stop(message?: string): boolean;
71
+ /**
72
+ * Exit-path teardown: drops everything not yet audible (pending arm,
73
+ * buffered text, queued chunks) but lets the audio the user is already
74
+ * hearing finish naturally, capped at `drainTimeoutMs`, before the hard
75
+ * stop. Deliberate interrupts (Ctrl+C, /tts stop, turn cancel) keep their
76
+ * instant path through stop(); this is only for exiting the app while the
77
+ * final audio of a completed response is still draining.
78
+ */
79
+ stopForExit(drainTimeoutMs?: number): Promise<void>;
80
+ handleTurnEvent(event: TurnEvent): void;
81
+ private maybeStartTurn;
82
+ private finishTurn;
83
+ private resetPipeline;
84
+ private startTimer;
85
+ private stopTimer;
86
+ /**
87
+ * Chunker output does NOT map 1:1 to synthesis requests. Text queues here
88
+ * and the pump merges everything pending into one request whenever a
89
+ * pipeline slot is free — so the request count tracks how often the model
90
+ * out-paces the audio, not how many sentences it wrote. A short answer that
91
+ * arrives before the first pump tick is exactly one request.
92
+ */
93
+ private queueTexts;
94
+ /**
95
+ * Deferred one tick so text delivered in the same synchronous burst (fast
96
+ * deltas, or a turn that completes instantly) coalesces into a single
97
+ * request instead of firing per sentence boundary.
98
+ */
99
+ private schedulePump;
100
+ private pump;
101
+ /** Merge everything pending into one request, capped at the per-request text limit. */
102
+ private takeMergedText;
103
+ private dispatchChunk;
104
+ private releasePipelineSlot;
105
+ private maybeReleaseTurn;
106
+ private synthesizeWithRetry;
107
+ /** Abortable backoff sleep — an abort clears the timer and rejects, so a stop mid-backoff leaves nothing running. */
108
+ private delay;
109
+ private synthesize;
110
+ /**
111
+ * One synthesis request failed after its retries. Report once per turn and
112
+ * keep going — the rest of the response still plays.
113
+ */
114
+ private reportSkippedChunk;
115
+ private reportError;
116
+ }
117
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../../../src/platform/voice/spoken-turn/controller.ts"],"names":[],"mappings":"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"}
@@ -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"}