@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/README.md +35 -0
- package/dist/index.d.mts +56 -1
- package/dist/index.d.ts +56 -1
- package/dist/index.js +283 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +283 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
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();
|