amalgm 0.1.74 → 0.1.75
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 +1 -1
- package/runtime/scripts/chat-core/engine.js +13 -17
- package/runtime/scripts/chat-core/stores.js +3 -19
- package/runtime/scripts/chat-core/streams.js +3 -7
- package/runtime/scripts/chat-core/tests/engine.test.js +11 -15
- package/runtime/scripts/chat-core/tests/server.test.js +4 -2
package/package.json
CHANGED
|
@@ -102,8 +102,8 @@ class ChatCore {
|
|
|
102
102
|
this.turns.mark(entry.turnId, 'cancelling');
|
|
103
103
|
await this.runtime.stop(sessionId);
|
|
104
104
|
}
|
|
105
|
-
if (entry.
|
|
106
|
-
await entry.
|
|
105
|
+
if (entry.streamCompleted && typeof entry.streamCompleted.then === 'function') {
|
|
106
|
+
await entry.streamCompleted;
|
|
107
107
|
}
|
|
108
108
|
return true;
|
|
109
109
|
}
|
|
@@ -196,6 +196,10 @@ class ChatCore {
|
|
|
196
196
|
};
|
|
197
197
|
entry.streamEnded = false;
|
|
198
198
|
let resolveFinalized = null;
|
|
199
|
+
let resolveStreamCompleted = null;
|
|
200
|
+
entry.streamCompleted = new Promise((resolve) => {
|
|
201
|
+
resolveStreamCompleted = resolve;
|
|
202
|
+
});
|
|
199
203
|
entry.finalized = new Promise((resolve) => {
|
|
200
204
|
resolveFinalized = resolve;
|
|
201
205
|
});
|
|
@@ -216,18 +220,15 @@ class ChatCore {
|
|
|
216
220
|
let providerSessionId = contract.providerSessionId || null;
|
|
217
221
|
let stopReason = 'end_turn';
|
|
218
222
|
let sawDone = false;
|
|
219
|
-
let finalDoneEvent = null;
|
|
220
|
-
let finalDoneChunk = null;
|
|
221
223
|
|
|
222
224
|
const append = (e, options = {}) => {
|
|
223
225
|
const emitFrame = options.emitFrame !== false;
|
|
224
|
-
const visible = options.visible !== false;
|
|
225
226
|
const nextProviderSessionId = e.providerSessionId || providerSessionId;
|
|
226
227
|
const nextUsage = e.type === 'usage.final' ? e.usage : usage;
|
|
227
228
|
let chunk = null;
|
|
228
229
|
if (emitFrame) {
|
|
229
230
|
const frame = frameFor(e, { sessionId: contract.sessionId, providerSessionId: nextProviderSessionId, usage: nextUsage });
|
|
230
|
-
chunk = this.turns.addChunk(turnId, (index) => withSseId(frame, index), {
|
|
231
|
+
chunk = this.turns.addChunk(turnId, (index) => withSseId(frame, index), { seal: e.type === 'done' });
|
|
231
232
|
if (!chunk) throw new Error('Raw stream write failed before event application');
|
|
232
233
|
}
|
|
233
234
|
if (e.providerSessionId) providerSessionId = e.providerSessionId;
|
|
@@ -238,8 +239,6 @@ class ChatCore {
|
|
|
238
239
|
if (e.type === 'done') {
|
|
239
240
|
sawDone = true;
|
|
240
241
|
stopReason = e.stopReason || stopReason;
|
|
241
|
-
finalDoneEvent = e;
|
|
242
|
-
finalDoneChunk = chunk;
|
|
243
242
|
}
|
|
244
243
|
parts.apply(e);
|
|
245
244
|
return chunk;
|
|
@@ -248,20 +247,22 @@ class ChatCore {
|
|
|
248
247
|
try {
|
|
249
248
|
try {
|
|
250
249
|
const stream = await this.runtime.prompt(contract, runtimeInput);
|
|
251
|
-
for await (const e of stream) append(e
|
|
250
|
+
for await (const e of stream) append(e);
|
|
252
251
|
const current = this.turns.get(turnId);
|
|
253
252
|
if (!sawDone) {
|
|
254
253
|
append(done({
|
|
255
254
|
providerSessionId,
|
|
256
255
|
stopReason: current?.status === 'cancelling' ? 'cancelled' : stopReason,
|
|
257
|
-
})
|
|
256
|
+
}));
|
|
258
257
|
}
|
|
259
258
|
} catch (err) {
|
|
260
259
|
append(errorEvent(err.message, { providerSessionId }));
|
|
261
|
-
append(done({ providerSessionId, stopReason: 'error' })
|
|
260
|
+
append(done({ providerSessionId, stopReason: 'error' }));
|
|
262
261
|
stopReason = 'error';
|
|
263
262
|
}
|
|
264
263
|
entry.streamEnded = true;
|
|
264
|
+
this.turns.mark(turnId, stopReason === 'error' ? 'error' : stopReason === 'cancelled' ? 'cancelled' : 'complete');
|
|
265
|
+
if (resolveStreamCompleted) resolveStreamCompleted();
|
|
265
266
|
|
|
266
267
|
const savedParts = parts.finalize();
|
|
267
268
|
this.turns.setParts(turnId, savedParts);
|
|
@@ -288,15 +289,10 @@ class ChatCore {
|
|
|
288
289
|
this.turns.mark(turnId, 'save_failed');
|
|
289
290
|
throw err;
|
|
290
291
|
}
|
|
291
|
-
if (finalDoneChunk) {
|
|
292
|
-
this.turns.releaseChunk(turnId, finalDoneChunk.index);
|
|
293
|
-
} else {
|
|
294
|
-
append(finalDoneEvent || done({ providerSessionId, stopReason }));
|
|
295
|
-
}
|
|
296
|
-
this.turns.mark(turnId, stopReason === 'error' ? 'error' : stopReason === 'cancelled' ? 'cancelled' : 'complete');
|
|
297
292
|
this.turns.clear(turnId);
|
|
298
293
|
return { providerSessionId, usage, parts: savedParts, stopReason };
|
|
299
294
|
} finally {
|
|
295
|
+
if (resolveStreamCompleted) resolveStreamCompleted();
|
|
300
296
|
if (resolveFinalized) resolveFinalized();
|
|
301
297
|
}
|
|
302
298
|
}
|
|
@@ -47,23 +47,10 @@ class TurnStore {
|
|
|
47
47
|
if (Number.isInteger(entry.sealedIndex)) return null;
|
|
48
48
|
const index = entry.chunks.length;
|
|
49
49
|
const data = typeof frame === 'function' ? frame(index) : frame;
|
|
50
|
-
const
|
|
51
|
-
const chunk = { index, data, timestamp: Date.now(), visible };
|
|
50
|
+
const chunk = { index, data, timestamp: Date.now() };
|
|
52
51
|
entry.chunks.push(chunk);
|
|
53
52
|
if (options.seal === true) entry.sealedIndex = index;
|
|
54
53
|
entry.updatedAt = Date.now();
|
|
55
|
-
if (visible) this.emit(turnId, chunk, entry);
|
|
56
|
-
return chunk;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
releaseChunk(turnId, index) {
|
|
60
|
-
const entry = this.turns.get(turnId);
|
|
61
|
-
if (!entry || !['streaming', 'cancelling'].includes(entry.status)) return null;
|
|
62
|
-
const chunk = entry.chunks.find((candidate) => candidate.index === index);
|
|
63
|
-
if (!chunk) return null;
|
|
64
|
-
if (chunk.visible !== false) return chunk;
|
|
65
|
-
chunk.visible = true;
|
|
66
|
-
entry.updatedAt = Date.now();
|
|
67
54
|
this.emit(turnId, chunk, entry);
|
|
68
55
|
return chunk;
|
|
69
56
|
}
|
|
@@ -90,12 +77,9 @@ class TurnStore {
|
|
|
90
77
|
this.listeners.delete(turnId);
|
|
91
78
|
}
|
|
92
79
|
|
|
93
|
-
chunksAfter(turnId, afterIndex = -1
|
|
80
|
+
chunksAfter(turnId, afterIndex = -1) {
|
|
94
81
|
const entry = this.turns.get(turnId);
|
|
95
|
-
|
|
96
|
-
return entry
|
|
97
|
-
? entry.chunks.filter((chunk) => chunk.index > afterIndex && (!visibleOnly || chunk.visible !== false))
|
|
98
|
-
: [];
|
|
82
|
+
return entry ? entry.chunks.filter((chunk) => chunk.index > afterIndex) : [];
|
|
99
83
|
}
|
|
100
84
|
|
|
101
85
|
subscribe(turnId, callback) {
|
|
@@ -59,10 +59,6 @@ function normalizeAfterIndex(afterIndex) {
|
|
|
59
59
|
return Number.isFinite(afterIndex) ? afterIndex : -1;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
function visibleChunks(entry) {
|
|
63
|
-
return entry.chunks.filter((chunk) => chunk.visible !== false);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
62
|
function writeTurnTail(core, entry, sse, options = {}) {
|
|
67
63
|
const format = options.format || 'frame';
|
|
68
64
|
let lastSent = normalizeAfterIndex(options.afterIndex);
|
|
@@ -73,7 +69,7 @@ function writeTurnTail(core, entry, sse, options = {}) {
|
|
|
73
69
|
sse.write(namedSse('chunk', {
|
|
74
70
|
codeSessionId: current.sessionId,
|
|
75
71
|
chunk,
|
|
76
|
-
total:
|
|
72
|
+
total: current.chunks.length,
|
|
77
73
|
}));
|
|
78
74
|
} else {
|
|
79
75
|
sse.write(chunk.data);
|
|
@@ -85,7 +81,7 @@ function writeTurnTail(core, entry, sse, options = {}) {
|
|
|
85
81
|
if (format === 'raw-temp') {
|
|
86
82
|
sse.write(namedSse('complete', {
|
|
87
83
|
codeSessionId: current.sessionId,
|
|
88
|
-
total:
|
|
84
|
+
total: current.chunks.length,
|
|
89
85
|
}));
|
|
90
86
|
sse.end();
|
|
91
87
|
return;
|
|
@@ -105,7 +101,7 @@ function writeTurnTail(core, entry, sse, options = {}) {
|
|
|
105
101
|
});
|
|
106
102
|
sse.addCleanup(off);
|
|
107
103
|
|
|
108
|
-
const existing =
|
|
104
|
+
const existing = entry.chunks.filter((chunk) => chunk.index > lastSent);
|
|
109
105
|
if (format === 'raw-temp') {
|
|
110
106
|
sse.write(namedSse('existing', {
|
|
111
107
|
codeSessionId: entry.sessionId,
|
|
@@ -88,11 +88,10 @@ test('title generation waits for user save and does not block a fast turn', asyn
|
|
|
88
88
|
await waitTick();
|
|
89
89
|
|
|
90
90
|
assert.equal(titleStarted, false, 'title generation should not race ahead of the user message save');
|
|
91
|
-
assert.equal(frames.some((frame) => frame.includes('"complete"')),
|
|
91
|
+
assert.equal(frames.some((frame) => frame.includes('"complete"')), true, 'complete follows raw stream, not final persistence');
|
|
92
92
|
|
|
93
93
|
resolveUserSave();
|
|
94
94
|
await run;
|
|
95
|
-
assert.equal(frames.some((frame) => frame.includes('"complete"')), true, 'turn completes after persistence settles');
|
|
96
95
|
assert.equal(frames.some((frame) => frame.includes('"title_generated"')), false, 'pending title should not hold the stream open');
|
|
97
96
|
await waitTick();
|
|
98
97
|
assert.equal(titleStarted, true, 'title generation should start after the user message save settles');
|
|
@@ -103,7 +102,7 @@ test('title generation waits for user save and does not block a fast turn', asyn
|
|
|
103
102
|
assert.equal(frames.some((frame) => frame.includes('"title_generated"')), false);
|
|
104
103
|
});
|
|
105
104
|
|
|
106
|
-
test('complete frame
|
|
105
|
+
test('complete frame is visible before assistant message save settles', async () => {
|
|
107
106
|
let resolveAssistantSave;
|
|
108
107
|
const db = {
|
|
109
108
|
saveUserMessage: async () => true,
|
|
@@ -127,18 +126,16 @@ test('complete frame waits for assistant message save', async () => {
|
|
|
127
126
|
await waitTick();
|
|
128
127
|
|
|
129
128
|
assert.equal(frames.some((frame) => frame.includes('agent_message_chunk')), true, 'content should still stream immediately');
|
|
130
|
-
assert.equal(frames.some((frame) => frame.includes('"complete"')),
|
|
131
|
-
assert.equal(core.active('session-test').active,
|
|
129
|
+
assert.equal(frames.some((frame) => frame.includes('"complete"')), true, 'complete follows raw-temp before assistant save');
|
|
130
|
+
assert.equal(core.active('session-test').active, false, 'turn is no longer active once raw stream completes');
|
|
132
131
|
const pendingEntry = core.turns.latest('session-test');
|
|
133
132
|
const pendingComplete = pendingEntry.chunks.find((chunk) => chunk.data.includes('"_type":"complete"'));
|
|
134
|
-
assert.ok(pendingComplete, 'raw-temp stores final complete
|
|
135
|
-
assert.equal(pendingComplete.visible, false, 'final complete is hidden from UI until assistant save succeeds');
|
|
133
|
+
assert.ok(pendingComplete, 'raw-temp stores final complete while assistant save is pending');
|
|
136
134
|
assert.equal(pendingEntry.sealedIndex, pendingComplete.index, 'final complete seals the raw-temp log');
|
|
137
135
|
|
|
138
136
|
resolveAssistantSave();
|
|
139
137
|
await run;
|
|
140
138
|
|
|
141
|
-
assert.equal(frames.some((frame) => frame.includes('"complete"')), true, 'complete is emitted after assistant save');
|
|
142
139
|
assert.equal(core.turns.active('session-test'), null, 'temp/raw state clears after save and complete');
|
|
143
140
|
});
|
|
144
141
|
|
|
@@ -203,7 +200,7 @@ test('provider streams while user save is pending but assistant save waits', asy
|
|
|
203
200
|
await run;
|
|
204
201
|
});
|
|
205
202
|
|
|
206
|
-
test('stop waits for cancelled
|
|
203
|
+
test('stop waits for cancelled raw stream but not persistence', async () => {
|
|
207
204
|
let releaseRuntime;
|
|
208
205
|
let resolveAssistantSave;
|
|
209
206
|
const fakeRuntime = {
|
|
@@ -248,8 +245,8 @@ test('stop waits for cancelled turn persistence', async () => {
|
|
|
248
245
|
});
|
|
249
246
|
await waitTick();
|
|
250
247
|
|
|
251
|
-
assert.equal(stopResolved,
|
|
252
|
-
assert.equal(frames.some((frame) => frame.includes('"stopReason":"cancelled"')),
|
|
248
|
+
assert.equal(stopResolved, true, 'stop resolves once cancelled raw stream completes');
|
|
249
|
+
assert.equal(frames.some((frame) => frame.includes('"stopReason":"cancelled"')), true, 'cancelled complete follows raw stream before save');
|
|
253
250
|
|
|
254
251
|
resolveAssistantSave();
|
|
255
252
|
assert.equal(await stop, true);
|
|
@@ -261,7 +258,7 @@ test('stop waits for cancelled turn persistence', async () => {
|
|
|
261
258
|
assert.equal(core.turns.latest('session-test'), null, 'cancelled raw-temp clears after save');
|
|
262
259
|
});
|
|
263
260
|
|
|
264
|
-
test('new turn
|
|
261
|
+
test('new turn can start after prior raw stream completes while save is pending', async () => {
|
|
265
262
|
let resolveFirstSave;
|
|
266
263
|
let stopCalls = 0;
|
|
267
264
|
const prompts = [];
|
|
@@ -315,8 +312,8 @@ test('new turn waits for active turn finalization before starting', async () =>
|
|
|
315
312
|
}));
|
|
316
313
|
await waitTick();
|
|
317
314
|
|
|
318
|
-
assert.deepEqual(prompts, ['first'], 'second provider turn
|
|
319
|
-
assert.equal(dbCalls.some((call) => call[0] === 'saveUserMessage' && call[1] === 'user-test-2'),
|
|
315
|
+
assert.deepEqual(prompts, ['first', 'second'], 'second provider turn starts after first raw stream completes');
|
|
316
|
+
assert.equal(dbCalls.some((call) => call[0] === 'saveUserMessage' && call[1] === 'user-test-2'), true, 'second turn persistence starts independently');
|
|
320
317
|
assert.equal(stopCalls, 0, 'finished stream should be awaited, not stopped during final save');
|
|
321
318
|
|
|
322
319
|
resolveFirstSave();
|
|
@@ -324,5 +321,4 @@ test('new turn waits for active turn finalization before starting', async () =>
|
|
|
324
321
|
await second;
|
|
325
322
|
|
|
326
323
|
assert.deepEqual(prompts, ['first', 'second']);
|
|
327
|
-
assert.equal(dbCalls.some((call) => call[0] === 'saveUserMessage' && call[1] === 'user-test-2'), true);
|
|
328
324
|
});
|
|
@@ -108,9 +108,11 @@ test('POST /chat streams from the same indexed raw chunks as /raw-temp', async (
|
|
|
108
108
|
assert.match(rawExisting, /"index":0/);
|
|
109
109
|
assert.match(rawExisting, /agent_message_chunk/);
|
|
110
110
|
|
|
111
|
+
const completeFrame = firstChatFrame.includes('"complete"')
|
|
112
|
+
? firstChatFrame
|
|
113
|
+
: await readUntil(chatReader, '"complete"');
|
|
114
|
+
assert.match(completeFrame, /"stopReason":"end_turn"/);
|
|
111
115
|
resolveAssistantSave();
|
|
112
|
-
const rest = await readUntil(chatReader, '"complete"');
|
|
113
|
-
assert.match(rest, /"stopReason":"end_turn"/);
|
|
114
116
|
} finally {
|
|
115
117
|
await new Promise((resolve) => server.close(resolve));
|
|
116
118
|
}
|