@estuary-ai/sdk 0.4.1 → 0.5.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/index.mjs CHANGED
@@ -31464,6 +31464,231 @@ function parseActions(text) {
31464
31464
  return parser2.parse(text);
31465
31465
  }
31466
31466
 
31467
+ // src/scripting/script-player.ts
31468
+ var DEFAULT_LINE_TIMEOUT_MS = 3e4;
31469
+ var ScriptPlayer = class {
31470
+ host;
31471
+ lines;
31472
+ lineGapMs;
31473
+ loop;
31474
+ lineTimeoutMs;
31475
+ _index = -1;
31476
+ _state = "idle";
31477
+ currentMessageId = null;
31478
+ lineAcked = false;
31479
+ retiredIds = /* @__PURE__ */ new Set();
31480
+ selfInterruptPending = false;
31481
+ pauseRequested = false;
31482
+ // Number of stray bot_responses still expected from text-only lines that were superseded
31483
+ // (via next()/stop()) BEFORE they were acked. Their server messageId isn't known yet, so we
31484
+ // can't retire them by id — instead we swallow that many fresh responses by arrival order.
31485
+ pendingStaleResponses = 0;
31486
+ lineTimer = null;
31487
+ gapTimer = null;
31488
+ resolveDone;
31489
+ done;
31490
+ onBotResponse = (r2) => this.handleBotResponse(r2);
31491
+ onAudioComplete = (messageId) => this.handleAudioComplete(messageId);
31492
+ onInterrupt = (data) => this.handleExternalInterrupt(data);
31493
+ onDisconnected = () => this.finish("disconnected");
31494
+ constructor(host, lines, opts = {}) {
31495
+ this.host = host;
31496
+ const defaultTextOnly = opts.textOnly ?? false;
31497
+ this.lines = lines.map(
31498
+ (line) => typeof line === "string" ? { text: line, textOnly: defaultTextOnly } : { text: line.text, textOnly: line.textOnly ?? defaultTextOnly }
31499
+ );
31500
+ this.lineGapMs = Math.max(0, opts.lineGapMs ?? 0);
31501
+ this.loop = opts.loop ?? false;
31502
+ this.lineTimeoutMs = opts.lineTimeoutMs ?? DEFAULT_LINE_TIMEOUT_MS;
31503
+ this.done = new Promise((resolve) => {
31504
+ this.resolveDone = resolve;
31505
+ });
31506
+ this.host.on("botResponse", this.onBotResponse);
31507
+ this.host.on("audioPlaybackComplete", this.onAudioComplete);
31508
+ this.host.on("interrupt", this.onInterrupt);
31509
+ this.host.on("disconnected", this.onDisconnected);
31510
+ if (opts.autoStart ?? true) {
31511
+ queueMicrotask(() => this.play());
31512
+ }
31513
+ }
31514
+ get length() {
31515
+ return this.lines.length;
31516
+ }
31517
+ get index() {
31518
+ return this._index;
31519
+ }
31520
+ get state() {
31521
+ return this._state;
31522
+ }
31523
+ play() {
31524
+ if (this._state === "done" || this._state === "playing") return;
31525
+ this.pauseRequested = false;
31526
+ this._state = "playing";
31527
+ this.startNextLine();
31528
+ }
31529
+ resume() {
31530
+ this.play();
31531
+ }
31532
+ pause() {
31533
+ if (this._state === "done") return;
31534
+ this.pauseRequested = true;
31535
+ }
31536
+ next() {
31537
+ if (this._state === "done") return;
31538
+ if (this._state === "idle") {
31539
+ this.play();
31540
+ return;
31541
+ }
31542
+ const expectInterrupt = this._state === "playing";
31543
+ this.pauseRequested = false;
31544
+ this._state = "playing";
31545
+ this.clearTimers();
31546
+ this.supersedeCurrentLine(expectInterrupt);
31547
+ this.startNextLine();
31548
+ }
31549
+ stop() {
31550
+ this.finish("stopped", true);
31551
+ }
31552
+ // ─── internals ────────────────────────────────────────────────
31553
+ hasSpeakableLine() {
31554
+ return this.lines.some((l) => l.text.trim().length > 0);
31555
+ }
31556
+ startNextLine() {
31557
+ if (this._state !== "playing") return;
31558
+ if (!this.hasSpeakableLine()) {
31559
+ this.finish("finished");
31560
+ return;
31561
+ }
31562
+ let next = this._index + 1;
31563
+ for (; ; ) {
31564
+ if (next >= this.lines.length) {
31565
+ if (this.loop) {
31566
+ next = 0;
31567
+ } else {
31568
+ this.finish("finished");
31569
+ return;
31570
+ }
31571
+ }
31572
+ if (this.lines[next].text.trim()) break;
31573
+ next++;
31574
+ }
31575
+ this._index = next;
31576
+ this.currentMessageId = null;
31577
+ this.lineAcked = false;
31578
+ if (!this.host.isConnected) {
31579
+ this.finish("disconnected");
31580
+ return;
31581
+ }
31582
+ const line = this.lines[next];
31583
+ this.host.sayLine(line.text, line.textOnly);
31584
+ this.lineTimer = setTimeout(() => this.handleLineTimeout(), this.lineTimeoutMs);
31585
+ }
31586
+ handleBotResponse(r2) {
31587
+ if (this._state === "done" || this._index < 0) return;
31588
+ if (r2.messageId && this.retiredIds.has(r2.messageId)) return;
31589
+ if (r2.isInterjection) return;
31590
+ if (!this.lineAcked) {
31591
+ if (this.pendingStaleResponses > 0) {
31592
+ this.pendingStaleResponses--;
31593
+ if (r2.messageId) this.retiredIds.add(r2.messageId);
31594
+ return;
31595
+ }
31596
+ this.lineAcked = true;
31597
+ this.currentMessageId = r2.messageId ?? null;
31598
+ this.selfInterruptPending = false;
31599
+ this.host.emitScriptLineStarted({
31600
+ index: this._index,
31601
+ text: this.lines[this._index].text,
31602
+ messageId: r2.messageId
31603
+ });
31604
+ }
31605
+ if (this.currentMessageId && r2.messageId && r2.messageId !== this.currentMessageId) return;
31606
+ const line = this.lines[this._index];
31607
+ const completesOnResponse = line.textOnly || !this.host.willPlayScriptedAudio;
31608
+ if (completesOnResponse && r2.isFinal) {
31609
+ this.onLineComplete();
31610
+ }
31611
+ }
31612
+ handleAudioComplete(messageId) {
31613
+ if (this._state === "done" || this._index < 0) return;
31614
+ if (messageId && this.retiredIds.has(messageId)) return;
31615
+ const line = this.lines[this._index];
31616
+ if (line.textOnly || !this.host.willPlayScriptedAudio) return;
31617
+ if (this.currentMessageId && messageId && messageId !== this.currentMessageId) return;
31618
+ this.onLineComplete();
31619
+ }
31620
+ handleLineTimeout() {
31621
+ if (this._state === "done") return;
31622
+ this.host.log(`script line ${this._index} timed out after ${this.lineTimeoutMs}ms; advancing`);
31623
+ this.onLineComplete();
31624
+ }
31625
+ onLineComplete() {
31626
+ this.clearTimers();
31627
+ if (this.lineGapMs > 0) {
31628
+ this.gapTimer = setTimeout(() => this.afterGap(), this.lineGapMs);
31629
+ } else {
31630
+ this.afterGap();
31631
+ }
31632
+ }
31633
+ afterGap() {
31634
+ this.gapTimer = null;
31635
+ if (this._state === "done") return;
31636
+ if (this.pauseRequested) {
31637
+ this._state = "paused";
31638
+ return;
31639
+ }
31640
+ this.startNextLine();
31641
+ }
31642
+ handleExternalInterrupt(data) {
31643
+ if (this._state === "done") return;
31644
+ if (data?.messageId && this.retiredIds.has(data.messageId)) return;
31645
+ if (this.selfInterruptPending) {
31646
+ this.selfInterruptPending = false;
31647
+ return;
31648
+ }
31649
+ this.finish("interrupted");
31650
+ }
31651
+ /**
31652
+ * Retire the current line because we're moving off it.
31653
+ * @param expectInterrupt true when the line is still in-flight (next()/stop() mid-line), so the
31654
+ * server will emit a self-induced `interrupt` we must not treat as external. false when the
31655
+ * line already completed (e.g. next() while paused), where no interrupt is coming.
31656
+ */
31657
+ supersedeCurrentLine(expectInterrupt) {
31658
+ if (this.lineAcked && this.currentMessageId) {
31659
+ this.retiredIds.add(this.currentMessageId);
31660
+ } else if (expectInterrupt && this.lines[this._index]?.textOnly) {
31661
+ this.pendingStaleResponses++;
31662
+ }
31663
+ if (expectInterrupt) this.selfInterruptPending = true;
31664
+ }
31665
+ clearTimers() {
31666
+ if (this.lineTimer) {
31667
+ clearTimeout(this.lineTimer);
31668
+ this.lineTimer = null;
31669
+ }
31670
+ if (this.gapTimer) {
31671
+ clearTimeout(this.gapTimer);
31672
+ this.gapTimer = null;
31673
+ }
31674
+ }
31675
+ finish(reason, interrupt = false) {
31676
+ if (this._state === "done") return;
31677
+ this.clearTimers();
31678
+ if (interrupt) {
31679
+ this.selfInterruptPending = true;
31680
+ if (this.host.isConnected) this.host.interrupt();
31681
+ }
31682
+ this._state = "done";
31683
+ this.host.off("botResponse", this.onBotResponse);
31684
+ this.host.off("audioPlaybackComplete", this.onAudioComplete);
31685
+ this.host.off("interrupt", this.onInterrupt);
31686
+ this.host.off("disconnected", this.onDisconnected);
31687
+ this.host.emitScriptComplete({ reason });
31688
+ this.resolveDone({ reason });
31689
+ }
31690
+ };
31691
+
31467
31692
  // src/client.ts
31468
31693
  var DEFAULT_SAMPLE_RATE = 24e3;
31469
31694
  var REST_UNAVAILABLE_MESSAGE = "REST API not available with session token auth. Use a server-side proxy for REST calls.";
@@ -31480,6 +31705,7 @@ var EstuaryClient = class extends TypedEventEmitter {
31480
31705
  _hasAutoInterrupted = false;
31481
31706
  _autoInterruptGraceTimer = null;
31482
31707
  _isLiveKitSpeaking = false;
31708
+ _activeScript = null;
31483
31709
  constructor(config) {
31484
31710
  super();
31485
31711
  if (!config.apiKey && !config.sessionToken) {
@@ -31554,6 +31780,10 @@ var EstuaryClient = class extends TypedEventEmitter {
31554
31780
  /** Disconnect from the server */
31555
31781
  async disconnect() {
31556
31782
  this.logger.info("Disconnecting...");
31783
+ if (this._activeScript && this._activeScript.state !== "done") {
31784
+ this._activeScript.stop();
31785
+ }
31786
+ this._activeScript = null;
31557
31787
  if (this._autoInterruptGraceTimer) {
31558
31788
  clearTimeout(this._autoInterruptGraceTimer);
31559
31789
  this._autoInterruptGraceTimer = null;
@@ -31575,6 +31805,59 @@ var EstuaryClient = class extends TypedEventEmitter {
31575
31805
  this.ensureConnected();
31576
31806
  this.socketManager.emitEvent("say_line", { text, text_only: textOnly });
31577
31807
  }
31808
+ /**
31809
+ * Script a sequence of prewritten lines. Lines are paced so each finishes before the next
31810
+ * is sent — required because say_line interrupts any in-progress response server-side, so
31811
+ * unpaced lines would stomp each other. Returns a controller (play/pause/resume/next/stop +
31812
+ * an awaitable `done`). Starting a new script stops any currently-active one.
31813
+ */
31814
+ playScript(lines, opts) {
31815
+ this.ensureConnected();
31816
+ if (this._activeScript && this._activeScript.state !== "done") {
31817
+ this._activeScript.stop();
31818
+ }
31819
+ const player = new ScriptPlayer(this.createScriptHost(), lines, opts);
31820
+ this._activeScript = player;
31821
+ return player;
31822
+ }
31823
+ /** Convenience alias of playScript() for fire-and-forget scripted sequences. */
31824
+ sayLines(lines, opts) {
31825
+ return this.playScript(lines, opts);
31826
+ }
31827
+ createScriptHost() {
31828
+ const self2 = this;
31829
+ return {
31830
+ sayLine(text, textOnly) {
31831
+ self2.sayLine(text, textOnly);
31832
+ },
31833
+ interrupt() {
31834
+ self2.interrupt();
31835
+ },
31836
+ on(event, listener) {
31837
+ self2.on(event, listener);
31838
+ return self2;
31839
+ },
31840
+ off(event, listener) {
31841
+ self2.off(event, listener);
31842
+ return self2;
31843
+ },
31844
+ get isConnected() {
31845
+ return self2.isConnected;
31846
+ },
31847
+ get willPlayScriptedAudio() {
31848
+ return self2.audioPlayer != null;
31849
+ },
31850
+ emitScriptLineStarted(info) {
31851
+ self2.emit("scriptLineStarted", info);
31852
+ },
31853
+ emitScriptComplete(info) {
31854
+ self2.emit("scriptComplete", info);
31855
+ },
31856
+ log(msg) {
31857
+ self2.logger.debug(msg);
31858
+ }
31859
+ };
31860
+ }
31578
31861
  /** Interrupt the current bot response */
31579
31862
  interrupt(messageId) {
31580
31863
  this.ensureConnected();