@arcote.tech/arc-chat 0.7.10 → 0.7.11
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 +258 -0
- package/package.json +7 -7
- package/src/aggregates/message.ts +74 -58
- package/src/chat-builder.ts +7 -1
- package/src/index.ts +14 -2
- package/src/listeners/ai-generation-listener.ts +155 -178
- package/src/react/chat-component.tsx +241 -204
- package/src/routes/chat-stream-route.ts +21 -10
- package/src/streaming/stream-registry.ts +252 -118
|
@@ -3,13 +3,17 @@ import { listener, type ArcContextElement, type ArcFunction } from "@arcote.tech
|
|
|
3
3
|
import type {
|
|
4
4
|
ArcToolAny,
|
|
5
5
|
AssistantContentBlock,
|
|
6
|
-
ChatStreamEvent,
|
|
7
6
|
Conversation,
|
|
8
7
|
ConversationTurn,
|
|
9
8
|
LLMProvider,
|
|
10
9
|
ToolCall,
|
|
11
10
|
} from "@arcote.tech/arc-ai";
|
|
12
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
finalize,
|
|
13
|
+
publish,
|
|
14
|
+
startStream,
|
|
15
|
+
type PublishableEvent,
|
|
16
|
+
} from "../streaming/stream-registry";
|
|
13
17
|
|
|
14
18
|
// ─── Config ─────────────────────────────────────────────────────
|
|
15
19
|
|
|
@@ -125,7 +129,7 @@ async function buildInstructions(
|
|
|
125
129
|
scopeId,
|
|
126
130
|
};
|
|
127
131
|
const result = await (instruction.handler as Function)(instructionCtx);
|
|
128
|
-
if (typeof result === "string") return
|
|
132
|
+
if (typeof result === "string") return result as unknown as InstructionResult;
|
|
129
133
|
if (result && typeof result === "object" && "prompt" in result) return result as InstructionResult;
|
|
130
134
|
return { prompt: "" };
|
|
131
135
|
}
|
|
@@ -136,11 +140,6 @@ async function buildInstructions(
|
|
|
136
140
|
* Decide whether to ask the provider for a continuation (delta) or send the
|
|
137
141
|
* full conversation. Continuation is only used when the provider supports it
|
|
138
142
|
* AND we have a known `responseId` to anchor the request.
|
|
139
|
-
*
|
|
140
|
-
* @param history Full conversation history including any new turns appended
|
|
141
|
-
* for this call.
|
|
142
|
-
* @param newTurnsStartIdx Index in `history` where "new" turns begin
|
|
143
|
-
* (everything before is "already known" by the model).
|
|
144
143
|
*/
|
|
145
144
|
function makeConversation(
|
|
146
145
|
provider: LLMProvider,
|
|
@@ -190,10 +189,11 @@ interface RunLoopConfig {
|
|
|
190
189
|
instruction?: ArcFunction<any>;
|
|
191
190
|
/** ID pustego assistant row'a utworzonego synchronicznie w mutacji
|
|
192
191
|
* triggerującej generację (`sendMessage`/`systemMessage`/`startStage`/
|
|
193
|
-
* `respondToTool`). Listener używa go w PIERWSZEJ
|
|
194
|
-
* wołać `startAssistantTurn`. Dzięki temu klient widzi
|
|
195
|
-
* natychmiast po mutacji i otwiera SSE zanim chunki zaczną
|
|
196
|
-
* Następne iteracje (multi-turn po server tool exec) tworzą fresh
|
|
192
|
+
* `respondToTool`/`retryGeneration`). Listener używa go w PIERWSZEJ
|
|
193
|
+
* iteracji zamiast wołać `startAssistantTurn`. Dzięki temu klient widzi
|
|
194
|
+
* assistant row natychmiast po mutacji i otwiera SSE zanim chunki zaczną
|
|
195
|
+
* lecieć. Następne iteracje (multi-turn po server tool exec) tworzą fresh
|
|
196
|
+
* rows. */
|
|
197
197
|
preCreatedAssistantMessageId?: string;
|
|
198
198
|
}
|
|
199
199
|
|
|
@@ -213,36 +213,22 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
213
213
|
instruction,
|
|
214
214
|
} = config;
|
|
215
215
|
|
|
216
|
-
|
|
216
|
+
const history = config.history;
|
|
217
217
|
let newTurnsStartIdx = config.initialNewTurnsStartIdx;
|
|
218
218
|
let executionCount = 0;
|
|
219
219
|
/** The in-progress assistant row for the CURRENT iteration. Pre-set z
|
|
220
|
-
* `preCreatedAssistantMessageId` dla pierwszej iteracji
|
|
221
|
-
*
|
|
222
|
-
* `startAssistantTurn` jak wcześniej. Closed at the bottom via
|
|
223
|
-
* `completeAssistantTurn`. Error handler używa do mark open turn jako
|
|
224
|
-
* failed. */
|
|
220
|
+
* `preCreatedAssistantMessageId` dla pierwszej iteracji. Wartość
|
|
221
|
+
* `undefined` przy iteracjach 2+ → loop wywoła `startAssistantTurn`. */
|
|
225
222
|
let currentTurnId: string | undefined = config.preCreatedAssistantMessageId;
|
|
226
223
|
/** True gdy w bieżącej iteracji `currentTurnId` był pre-utworzony przez
|
|
227
224
|
* mutację. Wtedy skipujemy ponowne `startAssistantTurn`. */
|
|
228
225
|
let usingPreCreatedTurn = config.preCreatedAssistantMessageId != null;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
seq?: number;
|
|
236
|
-
sessionId?: string;
|
|
237
|
-
},
|
|
238
|
-
) => {
|
|
239
|
-
seqCounter += 1;
|
|
240
|
-
broadcast(sessionId, {
|
|
241
|
-
...evt,
|
|
242
|
-
sessionId,
|
|
243
|
-
seq: seqCounter,
|
|
244
|
-
messageId: evt.messageId ?? currentTurnId,
|
|
245
|
-
} as ChatStreamEvent);
|
|
226
|
+
|
|
227
|
+
/** Pushuje event do in-memory stream-registry per `currentTurnId`. Registry
|
|
228
|
+
* akumuluje `currentBlocks` i broadcast'uje do wszystkich subscriberów. */
|
|
229
|
+
const send = (event: PublishableEvent) => {
|
|
230
|
+
if (!currentTurnId) return;
|
|
231
|
+
publish(currentTurnId, event);
|
|
246
232
|
};
|
|
247
233
|
|
|
248
234
|
try {
|
|
@@ -266,7 +252,7 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
266
252
|
|
|
267
253
|
// Open a new in-progress assistant row before the stream starts. The
|
|
268
254
|
// frontend detects `isGenerating: true` on this row and subscribes to
|
|
269
|
-
// the SSE stream
|
|
255
|
+
// the per-messageId SSE stream.
|
|
270
256
|
//
|
|
271
257
|
// Pierwsza iteracja: row już utworzony w mutacji triggerującej (przez
|
|
272
258
|
// `preCreatedAssistantMessageId`) → skipujemy. Kolejne iteracje
|
|
@@ -280,37 +266,11 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
280
266
|
currentTurnId = turnStart.messageId;
|
|
281
267
|
}
|
|
282
268
|
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const SNAPSHOT_EVERY_MS = 2000;
|
|
289
|
-
/** Aktualnie budowane bloki — accumulator dla snapshotu. */
|
|
290
|
-
const liveBlocks: AssistantContentBlock[] = [];
|
|
291
|
-
const liveToolCalls = new Map<
|
|
292
|
-
string,
|
|
293
|
-
{ name: string; argumentsBuffer: string }
|
|
294
|
-
>();
|
|
295
|
-
const maybeSnapshot = async (force = false) => {
|
|
296
|
-
chunksSinceSnapshot += 1;
|
|
297
|
-
const due =
|
|
298
|
-
force ||
|
|
299
|
-
chunksSinceSnapshot >= SNAPSHOT_EVERY_N ||
|
|
300
|
-
Date.now() - lastSnapshotAt >= SNAPSHOT_EVERY_MS;
|
|
301
|
-
if (!due || !currentTurnId) return;
|
|
302
|
-
chunksSinceSnapshot = 0;
|
|
303
|
-
lastSnapshotAt = Date.now();
|
|
304
|
-
try {
|
|
305
|
-
await ctx.mutate(messageElement).saveProgressSnapshot({
|
|
306
|
-
messageId: currentTurnId,
|
|
307
|
-
partialBlocks: JSON.stringify(liveBlocks),
|
|
308
|
-
partialLastSeq: seqCounter,
|
|
309
|
-
});
|
|
310
|
-
} catch {
|
|
311
|
-
// snapshot best-effort — pojawi się przy kolejnym chunku
|
|
312
|
-
}
|
|
313
|
-
};
|
|
269
|
+
// Open the in-memory stream — od teraz `subscribe(currentTurnId)`
|
|
270
|
+
// zwraca live SSE z `init` + zachowanymi chunkami. Idempotent: jeśli
|
|
271
|
+
// klient zdąży się zasubskrybować wcześniej (race), startStream nie
|
|
272
|
+
// robi nic.
|
|
273
|
+
startStream(currentTurnId!);
|
|
314
274
|
|
|
315
275
|
const result = await provider.streamComplete(
|
|
316
276
|
{
|
|
@@ -325,39 +285,18 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
325
285
|
},
|
|
326
286
|
(chunk) => {
|
|
327
287
|
if (chunk.type === "text_delta" && chunk.textDelta) {
|
|
328
|
-
// accumulate w liveBlocks (last text block lub nowy)
|
|
329
|
-
const last = liveBlocks[liveBlocks.length - 1];
|
|
330
|
-
if (last && last.type === "text") {
|
|
331
|
-
last.text += chunk.textDelta;
|
|
332
|
-
} else {
|
|
333
|
-
liveBlocks.push({ type: "text", text: chunk.textDelta });
|
|
334
|
-
}
|
|
335
288
|
send({ type: "text_delta", textDelta: chunk.textDelta });
|
|
336
|
-
void maybeSnapshot();
|
|
337
289
|
} else if (chunk.type === "tool_call_started" && chunk.toolCallId) {
|
|
338
|
-
liveToolCalls.set(chunk.toolCallId, {
|
|
339
|
-
name: chunk.toolCallName ?? "",
|
|
340
|
-
argumentsBuffer: "",
|
|
341
|
-
});
|
|
342
|
-
liveBlocks.push({
|
|
343
|
-
type: "tool_call",
|
|
344
|
-
id: chunk.toolCallId,
|
|
345
|
-
name: chunk.toolCallName ?? "",
|
|
346
|
-
arguments: {},
|
|
347
|
-
});
|
|
348
290
|
send({
|
|
349
291
|
type: "tool_call_pending",
|
|
350
292
|
toolCallId: chunk.toolCallId,
|
|
351
293
|
toolCallName: chunk.toolCallName,
|
|
352
294
|
});
|
|
353
|
-
void maybeSnapshot(true);
|
|
354
295
|
} else if (
|
|
355
296
|
chunk.type === "tool_call_arguments_delta" &&
|
|
356
297
|
chunk.toolCallId &&
|
|
357
298
|
chunk.argumentsDelta
|
|
358
299
|
) {
|
|
359
|
-
const tc = liveToolCalls.get(chunk.toolCallId);
|
|
360
|
-
if (tc) tc.argumentsBuffer += chunk.argumentsDelta;
|
|
361
300
|
send({
|
|
362
301
|
type: "tool_call_arguments_delta",
|
|
363
302
|
toolCallId: chunk.toolCallId,
|
|
@@ -367,27 +306,13 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
367
306
|
chunk.type === "tool_call_arguments_complete" &&
|
|
368
307
|
chunk.toolCallId
|
|
369
308
|
) {
|
|
370
|
-
// update accumulated block z complete args
|
|
371
|
-
const args = chunk.arguments ?? {};
|
|
372
|
-
const block = liveBlocks.find(
|
|
373
|
-
(b): b is Extract<AssistantContentBlock, { type: "tool_call" }> =>
|
|
374
|
-
b.type === "tool_call" && b.id === chunk.toolCallId,
|
|
375
|
-
);
|
|
376
|
-
if (block) block.arguments = args;
|
|
377
|
-
// toolCallName z liveBlocks (provider zna nazwę od tool_call_started)
|
|
378
|
-
// — bez tego klient pushuje tool z `toolName: ""` i nie znajduje
|
|
379
|
-
// viewComponent w toolsMap → fallback do generic ChatToolLog
|
|
380
|
-
// ("Wykonuję..."), AskQuestionsView nigdy nie mountuje się.
|
|
381
|
-
const toolCallName =
|
|
382
|
-
liveToolCalls.get(chunk.toolCallId)?.name ?? block?.name;
|
|
383
309
|
send({
|
|
384
310
|
type: "tool_call_arguments_complete",
|
|
385
311
|
toolCallId: chunk.toolCallId,
|
|
386
|
-
toolCallName,
|
|
387
|
-
arguments:
|
|
312
|
+
toolCallName: chunk.toolCallName,
|
|
313
|
+
arguments: chunk.arguments ?? {},
|
|
388
314
|
});
|
|
389
|
-
|
|
390
|
-
} else if (chunk.type === "usage_update") {
|
|
315
|
+
} else if (chunk.type === "usage_update" && chunk.usage) {
|
|
391
316
|
send({ type: "usage_update", usage: chunk.usage });
|
|
392
317
|
}
|
|
393
318
|
},
|
|
@@ -412,47 +337,47 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
412
337
|
const hasToolCalls =
|
|
413
338
|
result.finishReason === "tool_call" && toolCalls.length > 0;
|
|
414
339
|
|
|
415
|
-
// Close the turn row — same row that was opened above.
|
|
416
|
-
//
|
|
417
|
-
// blocks + responseId
|
|
340
|
+
// Close the turn row — same row that was opened above. Final blocks
|
|
341
|
+
// are the SINGLE persistent write of message content in this turn.
|
|
342
|
+
// Intermediate turns (gdy mamy tool calls) carry blocks + responseId;
|
|
343
|
+
// ostatni turn nosi też usage.
|
|
418
344
|
await ctx.mutate(messageElement).completeAssistantTurn({
|
|
419
345
|
messageId: currentTurnId!,
|
|
420
346
|
blocks: JSON.stringify(result.blocks),
|
|
421
347
|
previousResponseId: result.responseId,
|
|
422
348
|
usage: hasToolCalls ? undefined : JSON.stringify(result.usage),
|
|
423
349
|
});
|
|
350
|
+
|
|
351
|
+
// Tear down the in-memory stream: broadcast `done` do subscriberów,
|
|
352
|
+
// close controllery, drop registry entry po grace window. Klient z
|
|
353
|
+
// `done` flippuje isStreaming=false i renderuje final blocks z DB.
|
|
354
|
+
finalize(
|
|
355
|
+
currentTurnId!,
|
|
356
|
+
hasToolCalls
|
|
357
|
+
? undefined
|
|
358
|
+
: {
|
|
359
|
+
usage: result.usage,
|
|
360
|
+
finishReason: result.finishReason,
|
|
361
|
+
executionCount,
|
|
362
|
+
},
|
|
363
|
+
);
|
|
424
364
|
currentTurnId = undefined;
|
|
425
365
|
|
|
426
|
-
if (!hasToolCalls)
|
|
427
|
-
send({
|
|
428
|
-
type: "done",
|
|
429
|
-
usage: result.usage,
|
|
430
|
-
finishReason: result.finishReason,
|
|
431
|
-
executionCount,
|
|
432
|
-
lastSeq: seqCounter,
|
|
433
|
-
});
|
|
434
|
-
endStream(sessionId);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
366
|
+
if (!hasToolCalls) return;
|
|
437
367
|
|
|
438
368
|
const serverCalls = toolCalls.filter((tc) => serverToolsMap.has(tc.name));
|
|
439
369
|
const interactiveCalls = toolCalls.filter((tc) =>
|
|
440
370
|
interactiveToolNames.has(tc.name),
|
|
441
371
|
);
|
|
442
372
|
|
|
443
|
-
// Execute server tools — append each result to history as a separate turn
|
|
373
|
+
// Execute server tools — append each result to history as a separate turn.
|
|
374
|
+
// Note: ten turn (`finalize`d powyżej) już jest zamknięty, więc kolejne
|
|
375
|
+
// `send()` byłyby no-opem. Server-tool execution emit'uje eventy poprzez
|
|
376
|
+
// mutację `saveToolResult` (która tworzy tool_result row w DB) — klient
|
|
377
|
+
// dostaje je via aggregate query update. Nie korzystamy ze stream-registry
|
|
378
|
+
// dla tool execution.
|
|
444
379
|
const newToolResults: ConversationTurn[] = [];
|
|
445
380
|
for (const tc of serverCalls) {
|
|
446
|
-
// `tool_call_pending` poszło już ze streamingu (przy `started`).
|
|
447
|
-
// Teraz `executing` po stronie servera.
|
|
448
|
-
send({
|
|
449
|
-
type: "tool_call_arguments_complete",
|
|
450
|
-
toolCallId: tc.id,
|
|
451
|
-
toolCallName: tc.name,
|
|
452
|
-
arguments: tc.arguments,
|
|
453
|
-
executionCount,
|
|
454
|
-
});
|
|
455
|
-
|
|
456
381
|
const tool = serverToolsMap.get(tc.name);
|
|
457
382
|
let resultContent: string;
|
|
458
383
|
let isError = false;
|
|
@@ -482,19 +407,6 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
482
407
|
isError,
|
|
483
408
|
});
|
|
484
409
|
|
|
485
|
-
send({
|
|
486
|
-
type: "tool_call_executed",
|
|
487
|
-
toolCallId: tc.id,
|
|
488
|
-
toolCallName: tc.name,
|
|
489
|
-
toolResult: {
|
|
490
|
-
toolCallId: tc.id,
|
|
491
|
-
name: tc.name,
|
|
492
|
-
content: resultContent,
|
|
493
|
-
isError,
|
|
494
|
-
},
|
|
495
|
-
executionCount,
|
|
496
|
-
});
|
|
497
|
-
|
|
498
410
|
newToolResults.push({
|
|
499
411
|
role: "tool_result",
|
|
500
412
|
toolCallId: tc.id,
|
|
@@ -505,16 +417,9 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
505
417
|
}
|
|
506
418
|
|
|
507
419
|
// Interactive tools — stop the loop, wait for userResponded.
|
|
508
|
-
// The assistant turn (with the interactive tool_call)
|
|
509
|
-
//
|
|
510
|
-
if (interactiveCalls.length > 0)
|
|
511
|
-
send({
|
|
512
|
-
type: "interactive_tool_request",
|
|
513
|
-
toolCalls: interactiveCalls,
|
|
514
|
-
executionCount,
|
|
515
|
-
});
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
420
|
+
// The assistant turn (with the interactive tool_call) was already
|
|
421
|
+
// finalized above. Listener B (resume) will create a fresh turn.
|
|
422
|
+
if (interactiveCalls.length > 0) return;
|
|
518
423
|
|
|
519
424
|
// Append tool results to history; mark them as the "new turns" for the
|
|
520
425
|
// next iteration's continuation request.
|
|
@@ -526,12 +431,12 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
526
431
|
}
|
|
527
432
|
} catch (err) {
|
|
528
433
|
const errorMsg = `AI error: ${err instanceof Error ? err.message : String(err)}`;
|
|
529
|
-
send({
|
|
530
|
-
type: "error",
|
|
531
|
-
error: errorMsg,
|
|
532
|
-
executionCount,
|
|
533
|
-
});
|
|
534
434
|
if (currentTurnId) {
|
|
435
|
+
publish(currentTurnId, {
|
|
436
|
+
type: "error",
|
|
437
|
+
error: errorMsg,
|
|
438
|
+
executionCount,
|
|
439
|
+
});
|
|
535
440
|
try {
|
|
536
441
|
await ctx.mutate(messageElement).completeAssistantTurn({
|
|
537
442
|
messageId: currentTurnId,
|
|
@@ -539,8 +444,8 @@ async function runGenerationLoop(config: RunLoopConfig) {
|
|
|
539
444
|
error: errorMsg,
|
|
540
445
|
});
|
|
541
446
|
} catch {}
|
|
447
|
+
finalize(currentTurnId, { error: errorMsg, executionCount });
|
|
542
448
|
}
|
|
543
|
-
endStream(sessionId);
|
|
544
449
|
}
|
|
545
450
|
}
|
|
546
451
|
|
|
@@ -579,8 +484,6 @@ export function createAiGenerationListener(config: AiGenerationListenerConfig) {
|
|
|
579
484
|
scopeId,
|
|
580
485
|
content: userContent,
|
|
581
486
|
model: modelName,
|
|
582
|
-
role,
|
|
583
|
-
assistantMessageId,
|
|
584
487
|
} = event.payload as any;
|
|
585
488
|
|
|
586
489
|
const model = modelName ?? "gpt-5";
|
|
@@ -613,9 +516,6 @@ export function createAiGenerationListener(config: AiGenerationListenerConfig) {
|
|
|
613
516
|
maxExecutionCount,
|
|
614
517
|
toolChoice: config.toolChoice,
|
|
615
518
|
instruction,
|
|
616
|
-
// Pre-utworzony empty assistant row z mutacji `sendMessage`/
|
|
617
|
-
// `systemMessage`/`startStage` — pierwsza iteracja używa go zamiast
|
|
618
|
-
// wołać `startAssistantTurn`.
|
|
619
519
|
preCreatedAssistantMessageId: (
|
|
620
520
|
event.payload as { assistantMessageId?: string }
|
|
621
521
|
).assistantMessageId,
|
|
@@ -653,13 +553,7 @@ export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
|
653
553
|
.query([messageElement, ...allQueryElements])
|
|
654
554
|
.mutate([messageElement, ...allMutationElements])
|
|
655
555
|
.handle(async (ctx, event) => {
|
|
656
|
-
const {
|
|
657
|
-
sessionId,
|
|
658
|
-
scopeId,
|
|
659
|
-
toolCallId,
|
|
660
|
-
toolName,
|
|
661
|
-
content: toolResult,
|
|
662
|
-
} = event.payload;
|
|
556
|
+
const { sessionId, scopeId, toolCallId } = event.payload;
|
|
663
557
|
|
|
664
558
|
const dbMessages = await ctx
|
|
665
559
|
.query(messageElement)
|
|
@@ -670,8 +564,6 @@ export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
|
670
564
|
const history = buildHistory(dbMessages);
|
|
671
565
|
|
|
672
566
|
// Compute "new turns start" — index of the just-arrived tool_result.
|
|
673
|
-
// Anything before it is "already known" (assistant emitted the matching
|
|
674
|
-
// tool_call earlier and OpenAI has it server-side).
|
|
675
567
|
let newTurnsStartIdx = history.length;
|
|
676
568
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
677
569
|
const t = history[i];
|
|
@@ -681,7 +573,6 @@ export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
|
681
573
|
}
|
|
682
574
|
}
|
|
683
575
|
|
|
684
|
-
// Determine the model from the most recent assistant row in DB
|
|
685
576
|
const lastAssistantRow = [...dbMessages]
|
|
686
577
|
.reverse()
|
|
687
578
|
.find((m: any) => m.role === "assistant" && m.model);
|
|
@@ -690,9 +581,6 @@ export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
|
690
581
|
const provider = resolveProvider(model, scopeId);
|
|
691
582
|
if (!provider) return;
|
|
692
583
|
|
|
693
|
-
void toolName;
|
|
694
|
-
void toolResult;
|
|
695
|
-
|
|
696
584
|
await runGenerationLoop({
|
|
697
585
|
ctx,
|
|
698
586
|
messageElement,
|
|
@@ -708,10 +596,99 @@ export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
|
708
596
|
maxExecutionCount,
|
|
709
597
|
toolChoice: config.toolChoice,
|
|
710
598
|
instruction,
|
|
711
|
-
// Pre-utworzony empty assistant row z mutacji `respondToTool`.
|
|
712
599
|
preCreatedAssistantMessageId: (
|
|
713
600
|
event.payload as { assistantMessageId?: string }
|
|
714
601
|
).assistantMessageId,
|
|
715
602
|
});
|
|
716
603
|
});
|
|
717
604
|
}
|
|
605
|
+
|
|
606
|
+
// ─── Listener C: retryRequested → AI retry ──────────────────────
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Reaguje na `retryRequested` emit'owany przez mutację `retryGeneration`
|
|
610
|
+
* (klient klika Retry po SSE 410). Interrupted assistant row jest już
|
|
611
|
+
* usunięty z DB przez projection; w event payload mamy fresh
|
|
612
|
+
* `preCreatedAssistantMessageId`. Listener buduje historię z DB (kończy
|
|
613
|
+
* się na ostatniej user message, bo fresh assistant ma `isGenerating=true`
|
|
614
|
+
* bez `blocks` → skip'owany przez `buildHistory`) i odpala generation loop.
|
|
615
|
+
*/
|
|
616
|
+
export function createAiRetryListener(config: AiGenerationListenerConfig) {
|
|
617
|
+
const {
|
|
618
|
+
name,
|
|
619
|
+
messageElement,
|
|
620
|
+
resolveProvider,
|
|
621
|
+
instruction,
|
|
622
|
+
serverTools,
|
|
623
|
+
interactiveTools,
|
|
624
|
+
allQueryElements,
|
|
625
|
+
allMutationElements,
|
|
626
|
+
maxExecutionCount,
|
|
627
|
+
} = config;
|
|
628
|
+
|
|
629
|
+
const retryRequestedEvent = messageElement.getEvent("retryRequested");
|
|
630
|
+
const serverToolsMap = new Map(serverTools.map((t) => [t.name, t]));
|
|
631
|
+
const interactiveToolNames = new Set(interactiveTools.map((t) => t.name));
|
|
632
|
+
const allToolsForLLM = [...serverTools, ...interactiveTools];
|
|
633
|
+
const toolDefs =
|
|
634
|
+
allToolsForLLM.length > 0
|
|
635
|
+
? allToolsForLLM.map((t) => t.toJsonSchema())
|
|
636
|
+
: undefined;
|
|
637
|
+
|
|
638
|
+
return listener(`${name}AiRetry`)
|
|
639
|
+
.listenTo([retryRequestedEvent])
|
|
640
|
+
.async()
|
|
641
|
+
.query([messageElement, ...allQueryElements])
|
|
642
|
+
.mutate([messageElement, ...allMutationElements])
|
|
643
|
+
.handle(async (ctx, event) => {
|
|
644
|
+
const {
|
|
645
|
+
messageId: assistantMsgId,
|
|
646
|
+
sessionId,
|
|
647
|
+
scopeId,
|
|
648
|
+
model: modelName,
|
|
649
|
+
} = event.payload as any;
|
|
650
|
+
|
|
651
|
+
const dbMessages = await ctx
|
|
652
|
+
.query(messageElement)
|
|
653
|
+
.getByScope({ scopeId });
|
|
654
|
+
|
|
655
|
+
const history = buildHistory(dbMessages);
|
|
656
|
+
|
|
657
|
+
// Find the last user turn — that's the boundary for "new turns" so the
|
|
658
|
+
// continuation request only sends it as delta. Pre-existing assistant
|
|
659
|
+
// turns w historii zatrzymują się przed nią.
|
|
660
|
+
let newTurnsStartIdx = history.length;
|
|
661
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
662
|
+
if (history[i].role === "user") {
|
|
663
|
+
newTurnsStartIdx = i;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const lastAssistantRow = [...dbMessages]
|
|
669
|
+
.reverse()
|
|
670
|
+
.find((m: any) => m.role === "assistant" && m.model);
|
|
671
|
+
const model = modelName ?? lastAssistantRow?.model ?? "gpt-5";
|
|
672
|
+
|
|
673
|
+
const provider = resolveProvider(model, scopeId);
|
|
674
|
+
if (!provider) return;
|
|
675
|
+
|
|
676
|
+
await runGenerationLoop({
|
|
677
|
+
ctx,
|
|
678
|
+
messageElement,
|
|
679
|
+
provider,
|
|
680
|
+
model,
|
|
681
|
+
history,
|
|
682
|
+
initialNewTurnsStartIdx: newTurnsStartIdx,
|
|
683
|
+
toolDefs,
|
|
684
|
+
serverToolsMap,
|
|
685
|
+
interactiveToolNames,
|
|
686
|
+
scopeId,
|
|
687
|
+
sessionId,
|
|
688
|
+
maxExecutionCount,
|
|
689
|
+
toolChoice: config.toolChoice,
|
|
690
|
+
instruction,
|
|
691
|
+
preCreatedAssistantMessageId: assistantMsgId,
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
}
|