amalgm 0.1.76 → 0.1.77
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/package.json
CHANGED
|
@@ -129,6 +129,49 @@ function ensureClaudeToolState(state) {
|
|
|
129
129
|
return state;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function ensureClaudeTextState(state) {
|
|
133
|
+
if (!state) return null;
|
|
134
|
+
if (!state.claudeTextByIndex) state.claudeTextByIndex = new Map();
|
|
135
|
+
if (!state.claudeReasoningByIndex) state.claudeReasoningByIndex = new Map();
|
|
136
|
+
return state;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function blockIndex(value) {
|
|
140
|
+
const index = Number(value);
|
|
141
|
+
return Number.isInteger(index) && index >= 0 ? index : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function emitIndexedTextDelta(state, index, kind, delta, providerSessionId, raw) {
|
|
145
|
+
if (!delta) return [];
|
|
146
|
+
const textState = ensureClaudeTextState(state);
|
|
147
|
+
const key = blockIndex(index);
|
|
148
|
+
if (textState && key !== null) {
|
|
149
|
+
const map = kind === 'reasoning' ? textState.claudeReasoningByIndex : textState.claudeTextByIndex;
|
|
150
|
+
map.set(key, `${map.get(key) || ''}${delta}`);
|
|
151
|
+
}
|
|
152
|
+
return kind === 'reasoning'
|
|
153
|
+
? [reasoningDelta(delta, { providerSessionId, ...raw })]
|
|
154
|
+
: [textDelta(delta, { providerSessionId, ...raw })];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function emitIndexedTextSnapshot(block, field, kind, providerSessionId, state, raw) {
|
|
158
|
+
const text = block?.[field];
|
|
159
|
+
if (!text) return [];
|
|
160
|
+
let delta = text;
|
|
161
|
+
const textState = ensureClaudeTextState(state);
|
|
162
|
+
const key = blockIndex(block.index);
|
|
163
|
+
if (textState && key !== null) {
|
|
164
|
+
const map = kind === 'reasoning' ? textState.claudeReasoningByIndex : textState.claudeTextByIndex;
|
|
165
|
+
const streamed = map.get(key) || '';
|
|
166
|
+
delta = streamed ? (text.startsWith(streamed) ? text.slice(streamed.length) : '') : text;
|
|
167
|
+
map.set(key, text);
|
|
168
|
+
}
|
|
169
|
+
if (!delta) return [];
|
|
170
|
+
return kind === 'reasoning'
|
|
171
|
+
? [reasoningDelta(delta, { providerSessionId, ...raw })]
|
|
172
|
+
: [textDelta(delta, { providerSessionId, ...raw })];
|
|
173
|
+
}
|
|
174
|
+
|
|
132
175
|
function stableStringify(value) {
|
|
133
176
|
if (value == null || typeof value !== 'object') return JSON.stringify(value);
|
|
134
177
|
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
@@ -259,9 +302,11 @@ function startOrUpdateClaudeTool(block, providerSessionId, state, raw) {
|
|
|
259
302
|
|
|
260
303
|
function normalizeClaudeContentBlock(block, providerSessionId, state, raw = undefined) {
|
|
261
304
|
if (!block || typeof block !== 'object') return [];
|
|
262
|
-
if (block.type === 'text' && block.text)
|
|
305
|
+
if (block.type === 'text' && block.text) {
|
|
306
|
+
return emitIndexedTextSnapshot(block, 'text', 'text', providerSessionId, state, raw);
|
|
307
|
+
}
|
|
263
308
|
if ((block.type === 'thinking' || block.type === 'redacted_thinking') && block.thinking) {
|
|
264
|
-
return
|
|
309
|
+
return emitIndexedTextSnapshot(block, 'thinking', 'reasoning', providerSessionId, state, raw);
|
|
265
310
|
}
|
|
266
311
|
if (TOOL_USE_BLOCK_TYPES.has(block.type)) {
|
|
267
312
|
return startOrUpdateClaudeTool(block, providerSessionId, state, raw);
|
|
@@ -315,6 +360,14 @@ function normalizeClaudeStreamEvent(message, providerSessionId, state) {
|
|
|
315
360
|
return startOrUpdateClaudeTool({ ...block, index: event.index }, providerSessionId, state, raw);
|
|
316
361
|
}
|
|
317
362
|
|
|
363
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'text_delta') {
|
|
364
|
+
return emitIndexedTextDelta(state, event.index, 'text', event.delta.text, providerSessionId, raw);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (event.type === 'content_block_delta' && event.delta?.type === 'thinking_delta') {
|
|
368
|
+
return emitIndexedTextDelta(state, event.index, 'reasoning', event.delta.thinking, providerSessionId, raw);
|
|
369
|
+
}
|
|
370
|
+
|
|
318
371
|
if (event.type === 'content_block_delta' && event.delta?.type === 'input_json_delta') {
|
|
319
372
|
const toolState = ensureClaudeToolState(state);
|
|
320
373
|
const tool = toolState?.claudeToolsByIndex.get(Number(event.index));
|
|
@@ -376,9 +429,9 @@ function normalizeClaudeMessage(message, state) {
|
|
|
376
429
|
|
|
377
430
|
if (message.type === 'assistant') {
|
|
378
431
|
const events = [];
|
|
379
|
-
for (const block of message.message?.content || []) {
|
|
432
|
+
for (const [index, block] of (message.message?.content || []).entries()) {
|
|
380
433
|
events.push(...normalizeClaudeContentBlock(
|
|
381
|
-
block,
|
|
434
|
+
{ ...block, index },
|
|
382
435
|
providerSessionId,
|
|
383
436
|
state,
|
|
384
437
|
eventRaw('claude.sdk.message', 'claude/assistant/content_block', message),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { normalizeClaudeMessage } = require('../normalizers/claude');
|
|
6
|
+
|
|
7
|
+
test('claude text stream deltas emit before final assistant snapshot without duplication', () => {
|
|
8
|
+
const state = { providerSessionId: 'claude-session' };
|
|
9
|
+
|
|
10
|
+
const first = normalizeClaudeMessage({
|
|
11
|
+
type: 'content_block_delta',
|
|
12
|
+
index: 0,
|
|
13
|
+
delta: { type: 'text_delta', text: 'hel' },
|
|
14
|
+
}, state);
|
|
15
|
+
assert.equal(first.length, 1);
|
|
16
|
+
assert.equal(first[0].type, 'text.delta');
|
|
17
|
+
assert.equal(first[0].text, 'hel');
|
|
18
|
+
|
|
19
|
+
const second = normalizeClaudeMessage({
|
|
20
|
+
type: 'content_block_delta',
|
|
21
|
+
index: 0,
|
|
22
|
+
delta: { type: 'text_delta', text: 'lo' },
|
|
23
|
+
}, state);
|
|
24
|
+
assert.equal(second.length, 1);
|
|
25
|
+
assert.equal(second[0].type, 'text.delta');
|
|
26
|
+
assert.equal(second[0].text, 'lo');
|
|
27
|
+
|
|
28
|
+
const finalSnapshot = normalizeClaudeMessage({
|
|
29
|
+
type: 'assistant',
|
|
30
|
+
message: {
|
|
31
|
+
content: [{ type: 'text', text: 'hello' }],
|
|
32
|
+
},
|
|
33
|
+
}, state);
|
|
34
|
+
assert.deepEqual(finalSnapshot, []);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('claude thinking stream deltas emit reasoning before final snapshot without duplication', () => {
|
|
38
|
+
const state = { providerSessionId: 'claude-session' };
|
|
39
|
+
|
|
40
|
+
const streamed = normalizeClaudeMessage({
|
|
41
|
+
type: 'content_block_delta',
|
|
42
|
+
index: 0,
|
|
43
|
+
delta: { type: 'thinking_delta', thinking: 'inspect first' },
|
|
44
|
+
}, state);
|
|
45
|
+
assert.equal(streamed.length, 1);
|
|
46
|
+
assert.equal(streamed[0].type, 'reasoning.delta');
|
|
47
|
+
assert.equal(streamed[0].text, 'inspect first');
|
|
48
|
+
|
|
49
|
+
const finalSnapshot = normalizeClaudeMessage({
|
|
50
|
+
type: 'assistant',
|
|
51
|
+
message: {
|
|
52
|
+
content: [{ type: 'thinking', thinking: 'inspect first' }],
|
|
53
|
+
},
|
|
54
|
+
}, state);
|
|
55
|
+
assert.deepEqual(finalSnapshot, []);
|
|
56
|
+
});
|