osborn 0.9.147 → 0.9.148
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/claude-llm.js +50 -6
- package/dist/index.js +20 -0
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -552,6 +552,10 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
552
552
|
// No JSONL replay after the first cold start.
|
|
553
553
|
#persistentQuery = null;
|
|
554
554
|
#messageChannel = null;
|
|
555
|
+
// Read-along tracking — per-turn message ID and chunk counter for TTS highlighting
|
|
556
|
+
#currentTurnMessageId = null;
|
|
557
|
+
#currentTurnChunkIndex = 0;
|
|
558
|
+
#currentTurnChunks = [];
|
|
555
559
|
#backgroundConsumerRunning = false;
|
|
556
560
|
// Active queries — multiple can be running (SDK queues them internally).
|
|
557
561
|
// We keep ALL references so interrupt() can stop whatever is currently executing.
|
|
@@ -1106,22 +1110,41 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1106
1110
|
}
|
|
1107
1111
|
// Stream assistant text → tts_say events
|
|
1108
1112
|
if (msg.type === 'assistant' && msg.message?.content) {
|
|
1113
|
+
// Assign a stable messageId for this turn (first block sets it, rest reuse)
|
|
1114
|
+
if (!this.#currentTurnMessageId) {
|
|
1115
|
+
this.#currentTurnMessageId = crypto.randomUUID();
|
|
1116
|
+
this.#currentTurnChunkIndex = 0;
|
|
1117
|
+
this.#currentTurnChunks = [];
|
|
1118
|
+
}
|
|
1119
|
+
const turnMessageId = this.#currentTurnMessageId;
|
|
1109
1120
|
for (const block of msg.message.content) {
|
|
1110
1121
|
if (block.type === 'text' && block.text) {
|
|
1111
|
-
|
|
1122
|
+
const chunkIndex = this.#currentTurnChunkIndex;
|
|
1123
|
+
callbacks.eventEmitter.emit('assistant_text', { text: block.text, messageId: turnMessageId, chunkIndex });
|
|
1112
1124
|
const ttsChunk = stripMarkdownForTTS(block.text);
|
|
1113
1125
|
if (ttsChunk.trim()) {
|
|
1126
|
+
this.#currentTurnChunks.push(ttsChunk);
|
|
1127
|
+
this.#currentTurnChunkIndex++;
|
|
1114
1128
|
console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
|
|
1115
|
-
callbacks.eventEmitter.emit('tts_say', { text: ttsChunk });
|
|
1129
|
+
callbacks.eventEmitter.emit('tts_say', { text: ttsChunk, messageId: turnMessageId, chunkIndex });
|
|
1116
1130
|
}
|
|
1117
1131
|
}
|
|
1118
1132
|
}
|
|
1119
1133
|
}
|
|
1120
1134
|
// Result — marks end of a turn (but we keep consuming for next turn)
|
|
1121
1135
|
if (msg.type === 'result') {
|
|
1136
|
+
const turnMessageId = this.#currentTurnMessageId;
|
|
1137
|
+
const turnChunks = [...this.#currentTurnChunks];
|
|
1122
1138
|
if (msg.result) {
|
|
1123
|
-
callbacks.eventEmitter.emit('assistant_result', { text: msg.result });
|
|
1139
|
+
callbacks.eventEmitter.emit('assistant_result', { text: msg.result, messageId: turnMessageId });
|
|
1140
|
+
}
|
|
1141
|
+
if (turnMessageId && turnChunks.length > 0) {
|
|
1142
|
+
callbacks.eventEmitter.emit('tts_chunks', { messageId: turnMessageId, chunks: turnChunks });
|
|
1124
1143
|
}
|
|
1144
|
+
// Reset per-turn state for next turn
|
|
1145
|
+
this.#currentTurnMessageId = null;
|
|
1146
|
+
this.#currentTurnChunkIndex = 0;
|
|
1147
|
+
this.#currentTurnChunks = [];
|
|
1125
1148
|
console.log('✅ Claude turn complete (persistent session stays alive)');
|
|
1126
1149
|
}
|
|
1127
1150
|
}
|
|
@@ -1854,6 +1877,10 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1854
1877
|
// Run Claude Agent SDK query() and stream results
|
|
1855
1878
|
let hasOutput = false;
|
|
1856
1879
|
let fullResponse = ''; // Collect full response for frontend
|
|
1880
|
+
// Per-turn read-along tracking (non-skipTTSQueue path)
|
|
1881
|
+
let streamTurnMessageId = null;
|
|
1882
|
+
let streamTurnChunkIndex = 0;
|
|
1883
|
+
let streamTurnChunks = [];
|
|
1857
1884
|
// DIRECT MODE OPTIMIZATION: When skipTTSQueue is true, we run the Claude query
|
|
1858
1885
|
// in the background and return from run() immediately. This is critical because:
|
|
1859
1886
|
//
|
|
@@ -1940,20 +1967,29 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1940
1967
|
if (sdkRequestId) {
|
|
1941
1968
|
this.#eventEmitter.emit('query_request_id', { requestId: sdkRequestId });
|
|
1942
1969
|
}
|
|
1970
|
+
// Assign a stable messageId for this turn (first block sets it, rest reuse)
|
|
1971
|
+
if (!streamTurnMessageId) {
|
|
1972
|
+
streamTurnMessageId = crypto.randomUUID();
|
|
1973
|
+
streamTurnChunkIndex = 0;
|
|
1974
|
+
streamTurnChunks = [];
|
|
1975
|
+
}
|
|
1943
1976
|
for (const block of message.message.content) {
|
|
1944
1977
|
if (block.type === 'text' && block.text) {
|
|
1945
1978
|
hasOutput = true;
|
|
1946
1979
|
const rawText = block.text;
|
|
1980
|
+
const chunkIndex = streamTurnChunkIndex;
|
|
1947
1981
|
// Emit RAW text to frontend (for chat bubbles with full formatting)
|
|
1948
|
-
this.#eventEmitter.emit('assistant_text', { text: rawText });
|
|
1982
|
+
this.#eventEmitter.emit('assistant_text', { text: rawText, messageId: streamTurnMessageId, chunkIndex });
|
|
1949
1983
|
// Strip markdown for clean speech
|
|
1950
1984
|
const ttsChunk = stripMarkdownForTTS(rawText);
|
|
1951
1985
|
if (ttsChunk.trim()) {
|
|
1986
|
+
streamTurnChunks.push(ttsChunk);
|
|
1987
|
+
streamTurnChunkIndex++;
|
|
1952
1988
|
if (this.#opts.skipTTSQueue) {
|
|
1953
1989
|
// Direct mode: emit event for session.say() — bypasses LiveKit's
|
|
1954
1990
|
// BufferedTokenStream which causes stuck/delayed/out-of-order audio
|
|
1955
1991
|
console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
|
|
1956
|
-
this.#eventEmitter.emit('tts_say', { text: ttsChunk });
|
|
1992
|
+
this.#eventEmitter.emit('tts_say', { text: ttsChunk, messageId: streamTurnMessageId, chunkIndex });
|
|
1957
1993
|
}
|
|
1958
1994
|
else {
|
|
1959
1995
|
// Realtime mode: use LLM stream queue (framework handles TTS)
|
|
@@ -1971,7 +2007,15 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1971
2007
|
if (message.type === 'result' && message.result) {
|
|
1972
2008
|
const rawResult = message.result;
|
|
1973
2009
|
// Emit RAW result to frontend
|
|
1974
|
-
this.#eventEmitter.emit('assistant_result', { text: rawResult });
|
|
2010
|
+
this.#eventEmitter.emit('assistant_result', { text: rawResult, messageId: streamTurnMessageId });
|
|
2011
|
+
// Emit ordered chunk list for frontend read-along
|
|
2012
|
+
if (streamTurnMessageId && streamTurnChunks.length > 0) {
|
|
2013
|
+
this.#eventEmitter.emit('tts_chunks', { messageId: streamTurnMessageId, chunks: streamTurnChunks });
|
|
2014
|
+
}
|
|
2015
|
+
// Reset per-turn state
|
|
2016
|
+
streamTurnMessageId = null;
|
|
2017
|
+
streamTurnChunkIndex = 0;
|
|
2018
|
+
streamTurnChunks = [];
|
|
1975
2019
|
if (!hasOutput) {
|
|
1976
2020
|
hasOutput = true;
|
|
1977
2021
|
const ttsText = stripMarkdownForTTS(rawResult);
|
package/dist/index.js
CHANGED
|
@@ -2814,6 +2814,8 @@ async function main() {
|
|
|
2814
2814
|
text: data.text,
|
|
2815
2815
|
isStreaming: true,
|
|
2816
2816
|
agentRole: 'direct',
|
|
2817
|
+
messageId: data.messageId,
|
|
2818
|
+
chunkIndex: data.chunkIndex,
|
|
2817
2819
|
});
|
|
2818
2820
|
});
|
|
2819
2821
|
// Wire up Claude final result - RAW result goes to frontend
|
|
@@ -2825,6 +2827,15 @@ async function main() {
|
|
|
2825
2827
|
isStreaming: false,
|
|
2826
2828
|
isFinal: true,
|
|
2827
2829
|
agentRole: 'direct',
|
|
2830
|
+
messageId: data.messageId,
|
|
2831
|
+
});
|
|
2832
|
+
});
|
|
2833
|
+
// Wire up ordered TTS chunk list — emitted once per turn for read-along
|
|
2834
|
+
directLLM.events.on('tts_chunks', (data) => {
|
|
2835
|
+
sendToFrontend({
|
|
2836
|
+
type: 'tts_chunks',
|
|
2837
|
+
messageId: data.messageId,
|
|
2838
|
+
chunks: data.chunks,
|
|
2828
2839
|
});
|
|
2829
2840
|
});
|
|
2830
2841
|
// Wire up permission requests - sends to frontend for user approval
|
|
@@ -3001,6 +3012,15 @@ async function main() {
|
|
|
3001
3012
|
playbackStartedAt = Date.now();
|
|
3002
3013
|
console.log(`🔊 [${sayId}] audio first frame out (playbackStarted)`);
|
|
3003
3014
|
audioOutputRef.off('playbackStarted', onPlaybackStarted);
|
|
3015
|
+
// Notify frontend that this TTS chunk is now audibly playing
|
|
3016
|
+
if (data.messageId != null && data.chunkIndex != null) {
|
|
3017
|
+
sendToFrontend({
|
|
3018
|
+
type: 'tts_chunk_playing',
|
|
3019
|
+
messageId: data.messageId,
|
|
3020
|
+
chunkIndex: data.chunkIndex,
|
|
3021
|
+
text: data.text,
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3004
3024
|
};
|
|
3005
3025
|
audioOutputRef.on('playbackStarted', onPlaybackStarted);
|
|
3006
3026
|
}
|