osborn 0.9.147 → 0.9.149
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 +81 -8
- package/dist/index.js +20 -0
- package/package.json +1 -1
package/dist/claude-llm.js
CHANGED
|
@@ -332,9 +332,13 @@ export const NAMED_AGENTS = {
|
|
|
332
332
|
'Flag any change that could break existing callers, even if no test currently covers it.',
|
|
333
333
|
'',
|
|
334
334
|
'## How to work',
|
|
335
|
+
'0. **Get the diff first (MANDATORY):** Run `git diff HEAD~1 HEAD --name-only` to get the list of',
|
|
336
|
+
' changed files, then `git diff HEAD~1 HEAD` for the full diff. Build your entire test plan around',
|
|
337
|
+
' the SPECIFIC files and functions that changed — not a generic sweep.',
|
|
335
338
|
'1. Identify the correct test / build command from package.json, Makefile, or the task brief.',
|
|
336
339
|
'2. Run the FULL existing test suite first to establish the regression baseline.',
|
|
337
|
-
'3. Generate and run tests targeted at the SPECIFIC diff/change
|
|
340
|
+
'3. Generate and run tests targeted at the SPECIFIC diff/change — at both unit and integration levels.',
|
|
341
|
+
' Focus on: the changed functions/components, their callers, and any behavior the diff modifies.',
|
|
338
342
|
'4. Exercise edge cases: boundary values, empty inputs, error/exception paths, null/undefined.',
|
|
339
343
|
'5. Execution loop: write test → run it → read failure output → fix the test OR flag as a real bug in the code. Do NOT silently paper over a real defect.',
|
|
340
344
|
'6. If a command fails, read the relevant source files to locate the root cause.',
|
|
@@ -440,7 +444,19 @@ export const NAMED_AGENTS = {
|
|
|
440
444
|
'You must NEVER write code, config, or source files (.ts, .js, .json, .env, etc.) — the write gate',
|
|
441
445
|
'enforces this at the system level and will deny any such attempt.',
|
|
442
446
|
'',
|
|
447
|
+
'## Backward-compatibility mandate',
|
|
448
|
+
'For every change, explicitly verify:',
|
|
449
|
+
'- No existing exported function signature changed (parameter count/order/type, return type)',
|
|
450
|
+
'- No existing data-channel message schema changed (field names, types, required fields)',
|
|
451
|
+
'- No existing API endpoint behavior changed silently',
|
|
452
|
+
'- No existing UI prop interface changed in a breaking way',
|
|
453
|
+
'- Existing callers and consumers of the changed code still work',
|
|
454
|
+
'Tag any violation BLOCKER — silent compat breaks are the hardest bugs to catch after the fact.',
|
|
455
|
+
'',
|
|
443
456
|
'## How to work',
|
|
457
|
+
'0. **Get the diff first (MANDATORY):** Run `git diff HEAD~1 HEAD --stat` then `git diff HEAD~1 HEAD`.',
|
|
458
|
+
' Build your entire review around what ACTUALLY changed — not the writer\'s narrative alone.',
|
|
459
|
+
' If the task provides a diff, still verify it matches git history.',
|
|
444
460
|
'1. Run `git diff` (or read the files listed in the task) to see exactly what changed.',
|
|
445
461
|
'2. Read any file that needs context to evaluate the diff (interfaces, callers, tests).',
|
|
446
462
|
'3. Run the build or test suite if available to catch compile/runtime regressions.',
|
|
@@ -552,6 +568,10 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
552
568
|
// No JSONL replay after the first cold start.
|
|
553
569
|
#persistentQuery = null;
|
|
554
570
|
#messageChannel = null;
|
|
571
|
+
// Read-along tracking — per-turn message ID and chunk counter for TTS highlighting
|
|
572
|
+
#currentTurnMessageId = null;
|
|
573
|
+
#currentTurnChunkIndex = 0;
|
|
574
|
+
#currentTurnChunks = [];
|
|
555
575
|
#backgroundConsumerRunning = false;
|
|
556
576
|
// Active queries — multiple can be running (SDK queues them internally).
|
|
557
577
|
// We keep ALL references so interrupt() can stop whatever is currently executing.
|
|
@@ -1106,22 +1126,41 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1106
1126
|
}
|
|
1107
1127
|
// Stream assistant text → tts_say events
|
|
1108
1128
|
if (msg.type === 'assistant' && msg.message?.content) {
|
|
1129
|
+
// Assign a stable messageId for this turn (first block sets it, rest reuse)
|
|
1130
|
+
if (!this.#currentTurnMessageId) {
|
|
1131
|
+
this.#currentTurnMessageId = crypto.randomUUID();
|
|
1132
|
+
this.#currentTurnChunkIndex = 0;
|
|
1133
|
+
this.#currentTurnChunks = [];
|
|
1134
|
+
}
|
|
1135
|
+
const turnMessageId = this.#currentTurnMessageId;
|
|
1109
1136
|
for (const block of msg.message.content) {
|
|
1110
1137
|
if (block.type === 'text' && block.text) {
|
|
1111
|
-
|
|
1138
|
+
const chunkIndex = this.#currentTurnChunkIndex;
|
|
1139
|
+
callbacks.eventEmitter.emit('assistant_text', { text: block.text, messageId: turnMessageId, chunkIndex });
|
|
1112
1140
|
const ttsChunk = stripMarkdownForTTS(block.text);
|
|
1113
1141
|
if (ttsChunk.trim()) {
|
|
1142
|
+
this.#currentTurnChunks.push(ttsChunk);
|
|
1143
|
+
this.#currentTurnChunkIndex++;
|
|
1114
1144
|
console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
|
|
1115
|
-
callbacks.eventEmitter.emit('tts_say', { text: ttsChunk });
|
|
1145
|
+
callbacks.eventEmitter.emit('tts_say', { text: ttsChunk, messageId: turnMessageId, chunkIndex });
|
|
1116
1146
|
}
|
|
1117
1147
|
}
|
|
1118
1148
|
}
|
|
1119
1149
|
}
|
|
1120
1150
|
// Result — marks end of a turn (but we keep consuming for next turn)
|
|
1121
1151
|
if (msg.type === 'result') {
|
|
1152
|
+
const turnMessageId = this.#currentTurnMessageId;
|
|
1153
|
+
const turnChunks = [...this.#currentTurnChunks];
|
|
1122
1154
|
if (msg.result) {
|
|
1123
|
-
callbacks.eventEmitter.emit('assistant_result', { text: msg.result });
|
|
1155
|
+
callbacks.eventEmitter.emit('assistant_result', { text: msg.result, messageId: turnMessageId });
|
|
1124
1156
|
}
|
|
1157
|
+
if (turnMessageId && turnChunks.length > 0) {
|
|
1158
|
+
callbacks.eventEmitter.emit('tts_chunks', { messageId: turnMessageId, chunks: turnChunks });
|
|
1159
|
+
}
|
|
1160
|
+
// Reset per-turn state for next turn
|
|
1161
|
+
this.#currentTurnMessageId = null;
|
|
1162
|
+
this.#currentTurnChunkIndex = 0;
|
|
1163
|
+
this.#currentTurnChunks = [];
|
|
1125
1164
|
console.log('✅ Claude turn complete (persistent session stays alive)');
|
|
1126
1165
|
}
|
|
1127
1166
|
}
|
|
@@ -1158,13 +1197,26 @@ export class ClaudeLLM extends llm.LLM {
|
|
|
1158
1197
|
const idxPathReviewer = (this.#sessionId && this.#opts.workingDirectory)
|
|
1159
1198
|
? getIndexPath(this.#sessionId, this.#opts.workingDirectory)
|
|
1160
1199
|
: null;
|
|
1200
|
+
// Get the actual diff to give reviewer concrete evidence instead of just the narrative
|
|
1201
|
+
let gitDiff = '';
|
|
1202
|
+
try {
|
|
1203
|
+
const { execSync } = await import('child_process');
|
|
1204
|
+
const diffStat = execSync('git diff HEAD~1 HEAD --stat 2>/dev/null', { cwd: this.#opts.workingDirectory, timeout: 5000 }).toString().trim();
|
|
1205
|
+
const diff = execSync('git diff HEAD~1 HEAD 2>/dev/null', { cwd: this.#opts.workingDirectory, timeout: 5000 }).toString().trim();
|
|
1206
|
+
gitDiff = diffStat ? `\n\n<git_diff_stat>\n${diffStat}\n</git_diff_stat>\n\n<git_diff>\n${diff.slice(0, 6000)}\n</git_diff>` : '';
|
|
1207
|
+
}
|
|
1208
|
+
catch {
|
|
1209
|
+
// non-fatal: if git fails, proceed without diff
|
|
1210
|
+
}
|
|
1161
1211
|
const prompt = [
|
|
1162
1212
|
'Use the reviewer sub-agent to review this writer output for correctness/spec-adherence/obvious bugs.',
|
|
1213
|
+
'The git diff is provided below — use it as the authoritative source of what changed.',
|
|
1163
1214
|
'End your reply with exactly `VERDICT: ACCEPT` or `VERDICT: REJECT`.',
|
|
1164
1215
|
...(idxPathReviewer ? [`The session index is at ${idxPathReviewer} — you MUST read it before reviewing.`] : []),
|
|
1216
|
+
gitDiff,
|
|
1165
1217
|
'',
|
|
1166
1218
|
'<writer_output>',
|
|
1167
|
-
writerOutput.slice(0,
|
|
1219
|
+
writerOutput.slice(0, 6000),
|
|
1168
1220
|
'</writer_output>',
|
|
1169
1221
|
].join('\n');
|
|
1170
1222
|
// Do NOT pass agents here — the reviewer must be review-only and must not
|
|
@@ -1854,6 +1906,10 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1854
1906
|
// Run Claude Agent SDK query() and stream results
|
|
1855
1907
|
let hasOutput = false;
|
|
1856
1908
|
let fullResponse = ''; // Collect full response for frontend
|
|
1909
|
+
// Per-turn read-along tracking (non-skipTTSQueue path)
|
|
1910
|
+
let streamTurnMessageId = null;
|
|
1911
|
+
let streamTurnChunkIndex = 0;
|
|
1912
|
+
let streamTurnChunks = [];
|
|
1857
1913
|
// DIRECT MODE OPTIMIZATION: When skipTTSQueue is true, we run the Claude query
|
|
1858
1914
|
// in the background and return from run() immediately. This is critical because:
|
|
1859
1915
|
//
|
|
@@ -1940,20 +1996,29 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1940
1996
|
if (sdkRequestId) {
|
|
1941
1997
|
this.#eventEmitter.emit('query_request_id', { requestId: sdkRequestId });
|
|
1942
1998
|
}
|
|
1999
|
+
// Assign a stable messageId for this turn (first block sets it, rest reuse)
|
|
2000
|
+
if (!streamTurnMessageId) {
|
|
2001
|
+
streamTurnMessageId = crypto.randomUUID();
|
|
2002
|
+
streamTurnChunkIndex = 0;
|
|
2003
|
+
streamTurnChunks = [];
|
|
2004
|
+
}
|
|
1943
2005
|
for (const block of message.message.content) {
|
|
1944
2006
|
if (block.type === 'text' && block.text) {
|
|
1945
2007
|
hasOutput = true;
|
|
1946
2008
|
const rawText = block.text;
|
|
2009
|
+
const chunkIndex = streamTurnChunkIndex;
|
|
1947
2010
|
// Emit RAW text to frontend (for chat bubbles with full formatting)
|
|
1948
|
-
this.#eventEmitter.emit('assistant_text', { text: rawText });
|
|
2011
|
+
this.#eventEmitter.emit('assistant_text', { text: rawText, messageId: streamTurnMessageId, chunkIndex });
|
|
1949
2012
|
// Strip markdown for clean speech
|
|
1950
2013
|
const ttsChunk = stripMarkdownForTTS(rawText);
|
|
1951
2014
|
if (ttsChunk.trim()) {
|
|
2015
|
+
streamTurnChunks.push(ttsChunk);
|
|
2016
|
+
streamTurnChunkIndex++;
|
|
1952
2017
|
if (this.#opts.skipTTSQueue) {
|
|
1953
2018
|
// Direct mode: emit event for session.say() — bypasses LiveKit's
|
|
1954
2019
|
// BufferedTokenStream which causes stuck/delayed/out-of-order audio
|
|
1955
2020
|
console.log(`🔊 TTS say (${ttsChunk.length} chars): "${ttsChunk}"`);
|
|
1956
|
-
this.#eventEmitter.emit('tts_say', { text: ttsChunk });
|
|
2021
|
+
this.#eventEmitter.emit('tts_say', { text: ttsChunk, messageId: streamTurnMessageId, chunkIndex });
|
|
1957
2022
|
}
|
|
1958
2023
|
else {
|
|
1959
2024
|
// Realtime mode: use LLM stream queue (framework handles TTS)
|
|
@@ -1971,7 +2036,15 @@ class ClaudeLLMStream extends llm.LLMStream {
|
|
|
1971
2036
|
if (message.type === 'result' && message.result) {
|
|
1972
2037
|
const rawResult = message.result;
|
|
1973
2038
|
// Emit RAW result to frontend
|
|
1974
|
-
this.#eventEmitter.emit('assistant_result', { text: rawResult });
|
|
2039
|
+
this.#eventEmitter.emit('assistant_result', { text: rawResult, messageId: streamTurnMessageId });
|
|
2040
|
+
// Emit ordered chunk list for frontend read-along
|
|
2041
|
+
if (streamTurnMessageId && streamTurnChunks.length > 0) {
|
|
2042
|
+
this.#eventEmitter.emit('tts_chunks', { messageId: streamTurnMessageId, chunks: streamTurnChunks });
|
|
2043
|
+
}
|
|
2044
|
+
// Reset per-turn state
|
|
2045
|
+
streamTurnMessageId = null;
|
|
2046
|
+
streamTurnChunkIndex = 0;
|
|
2047
|
+
streamTurnChunks = [];
|
|
1975
2048
|
if (!hasOutput) {
|
|
1976
2049
|
hasOutput = true;
|
|
1977
2050
|
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
|
}
|