@jskit-ai/assistant-runtime 0.1.168 → 0.1.170
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/fixtures/conversation/vite.config.mjs +3 -0
- package/migrations/assistant_turn_requests.cjs +20 -0
- package/package.json +10 -7
- package/src/client/components/AssistantSurfaceClientElement.vue +14 -4
- package/src/client/composables/useAssistantRuntime.js +62 -37
- package/src/server/AssistantProvider.js +5 -1
- package/src/server/repositories/turnRequestsRepository.js +35 -0
- package/src/server/services/chatService.js +415 -359
- package/src/shared/assistantRuntimeConfig.js +2 -1
- package/test/chatServiceLifecycle.test.js +4 -2
- package/test/conversation.browser.test.js +50 -0
- package/test/lazyAppConfig.test.js +3 -0
- package/test/packageMetadata.test.js +2 -1
- package/test/support/memoryTurnRequests.js +19 -0
- package/test/turnRequests.test.js +122 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
2
3
|
import { normalizeObject, normalizeRecordId, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
3
4
|
import { resolveWorkspaceSlug } from "@jskit-ai/assistant-core/server";
|
|
@@ -49,11 +50,11 @@ function normalizeStreamInput(payload = {}) {
|
|
|
49
50
|
const source = normalizeObject(payload);
|
|
50
51
|
const messageId = normalizeText(source.messageId);
|
|
51
52
|
const input = normalizeText(source.input).slice(0, MAX_INPUT_CHARS);
|
|
52
|
-
if (!messageId) {
|
|
53
|
+
if (!messageId || messageId.length > 128) {
|
|
53
54
|
throw new AppError(400, "Validation failed.", {
|
|
54
55
|
details: {
|
|
55
56
|
fieldErrors: {
|
|
56
|
-
messageId: "messageId is required."
|
|
57
|
+
messageId: "messageId is required and must be at most 128 characters."
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
});
|
|
@@ -463,6 +464,7 @@ function buildAssistantActionContext(context = {}, assistantSurface = {}) {
|
|
|
463
464
|
|
|
464
465
|
function createChatService({
|
|
465
466
|
aiClientFactory,
|
|
467
|
+
turnRequests,
|
|
466
468
|
attachments,
|
|
467
469
|
transcriptService,
|
|
468
470
|
serviceToolCatalog,
|
|
@@ -471,9 +473,9 @@ function createChatService({
|
|
|
471
473
|
resolveAppConfig = null,
|
|
472
474
|
workspaceScopeSupport = null
|
|
473
475
|
} = {}) {
|
|
474
|
-
if (!aiClientFactory || typeof aiClientFactory.resolveClient !== "function" || !transcriptService || !serviceToolCatalog || !assistantConfigService) {
|
|
476
|
+
if (!aiClientFactory || typeof aiClientFactory.resolveClient !== "function" || !turnRequests || !transcriptService || !serviceToolCatalog || !assistantConfigService) {
|
|
475
477
|
throw new Error(
|
|
476
|
-
"createChatService requires aiClientFactory.resolveClient(), transcriptService, serviceToolCatalog, and assistantConfigService."
|
|
478
|
+
"createChatService requires aiClientFactory.resolveClient(), turnRequests, transcriptService, serviceToolCatalog, and assistantConfigService."
|
|
477
479
|
);
|
|
478
480
|
}
|
|
479
481
|
|
|
@@ -492,132 +494,132 @@ function createChatService({
|
|
|
492
494
|
return workspaceScopeSupport.resolveWorkspace(context, input);
|
|
493
495
|
}
|
|
494
496
|
|
|
497
|
+
function replayRequest(record, request, writer) {
|
|
498
|
+
if (!isDeepStrictEqual(record.request, JSON.parse(JSON.stringify(request)))) {
|
|
499
|
+
throw new AppError(409, "This message ID belongs to a different request.");
|
|
500
|
+
}
|
|
501
|
+
const response = record.response || {};
|
|
502
|
+
if (record.status === "running") {
|
|
503
|
+
if (response.meta) writer.sendMeta(response.meta);
|
|
504
|
+
writer.sendError({ type: ASSISTANT_STREAM_EVENT_TYPES.ERROR, code: "assistant_request_unconfirmed",
|
|
505
|
+
message: "This message was already submitted. Its completion is not confirmed; check conversation history before sending new work." });
|
|
506
|
+
writer.sendDone({ type: ASSISTANT_STREAM_EVENT_TYPES.DONE, messageId: request.messageId, status: "unconfirmed" });
|
|
507
|
+
return { conversationId: response.meta?.conversationId, messageId: request.messageId, status: "unconfirmed" };
|
|
508
|
+
}
|
|
509
|
+
if (response.failure && !response.meta) throw new AppError(response.failure.status, response.failure.message);
|
|
510
|
+
if (response.meta) writer.sendMeta(response.meta);
|
|
511
|
+
if (response.answer) writer.sendAssistantMessage(response.answer);
|
|
512
|
+
if (response.error) writer.sendError(response.error);
|
|
513
|
+
if (response.failure) writer.sendError({ type: ASSISTANT_STREAM_EVENT_TYPES.ERROR, ...response.failure });
|
|
514
|
+
writer.sendDone(response.done || { type: ASSISTANT_STREAM_EVENT_TYPES.DONE, messageId: request.messageId, status: record.status });
|
|
515
|
+
return { conversationId: response.meta?.conversationId, messageId: request.messageId, status: record.status };
|
|
516
|
+
}
|
|
517
|
+
|
|
495
518
|
async function streamChat(payload = {}, options = {}) {
|
|
496
519
|
const assistantSurface = requireAssistantSurface(resolveCurrentAppConfig(), payload?.targetSurfaceId);
|
|
497
520
|
const context = normalizeObject(options.context);
|
|
498
521
|
const assistantContext = buildAssistantActionContext(context, assistantSurface);
|
|
499
522
|
const workspace = resolveRuntimeWorkspace(assistantSurface, assistantContext, payload);
|
|
523
|
+
const source = normalizeStreamInput(payload);
|
|
524
|
+
const actor = context.actor;
|
|
525
|
+
const actorUserId = normalizeRecordId(actor?.id, { fallback: null });
|
|
526
|
+
if (!actorUserId) throw new AppError(401, "Authentication required.");
|
|
527
|
+
if (!hasStreamWriter(options.streamWriter)) throw new Error("assistant.chat.stream requires streamWriter methods.");
|
|
528
|
+
const scope = { actorUserId, surfaceId: assistantSurface.targetSurfaceId,
|
|
529
|
+
workspaceId: normalizeRecordId(workspace?.id || workspace, { fallback: null }) };
|
|
530
|
+
const request = { ...source, integrationId: normalizeText(payload.integrationId) };
|
|
531
|
+
const existing = await turnRequests.find(scope, source.messageId);
|
|
532
|
+
if (existing) return replayRequest(existing, request, options.streamWriter);
|
|
500
533
|
const aiClient = await aiClientFactory.resolveClient(assistantSurface.targetSurfaceId, {
|
|
501
534
|
context: assistantContext, integrationId: payload.integrationId
|
|
502
535
|
});
|
|
503
|
-
if (!aiClient.enabled)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
508
|
-
const streamWriter =
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
assistantSurface,
|
|
517
|
-
workspace,
|
|
518
|
-
actor,
|
|
519
|
-
{
|
|
520
|
-
conversationId: source.conversationId,
|
|
521
|
-
provider: aiClient.provider,
|
|
522
|
-
model: aiClient.defaultModel,
|
|
523
|
-
surfaceId: assistantSurface.targetSurfaceId,
|
|
524
|
-
messageId: source.messageId
|
|
536
|
+
if (!aiClient.enabled) throw new AppError(503, "Assistant provider is not configured.");
|
|
537
|
+
const claim = await turnRequests.claim(scope, request);
|
|
538
|
+
if (!claim.acquired) return replayRequest(claim, request, options.streamWriter);
|
|
539
|
+
const response = {};
|
|
540
|
+
const output = options.streamWriter;
|
|
541
|
+
const streamWriter = {
|
|
542
|
+
sendToolCall: event => output.sendToolCall(event),
|
|
543
|
+
sendToolResult: event => output.sendToolResult(event),
|
|
544
|
+
sendMeta(event) { response.meta = event; output.sendMeta(event); },
|
|
545
|
+
sendAssistantDelta(event) {
|
|
546
|
+
response.answer = { type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE,
|
|
547
|
+
text: `${response.answer?.text || ""}${event.delta || ""}`, status: "streaming" };
|
|
548
|
+
output.sendAssistantDelta(event);
|
|
525
549
|
},
|
|
526
|
-
{
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (attachmentIds === undefined || (Array.isArray(attachmentIds) && !attachmentIds.length)) return { attachments: [], content: [] };
|
|
539
|
-
if (!Array.isArray(attachmentIds) || attachmentIds.length > 10 ||
|
|
540
|
-
attachmentIds.some(id => typeof id !== "string" || !id || id.length > 256)) {
|
|
541
|
-
throw new AppError(400, "Invalid attachment identifiers.");
|
|
542
|
-
}
|
|
543
|
-
if (!attachments?.resolve || !aiClient.supportsAttachments) {
|
|
544
|
-
throw new AppError(400, "File attachments are not configured for this assistant.");
|
|
545
|
-
}
|
|
546
|
-
// Only the application can authorize uploads and turn stored bytes into
|
|
547
|
-
// provider input. Browser-supplied paths, URLs and file contents are never used.
|
|
548
|
-
return attachments.resolve({ attachmentIds, context: assistantContext, workspace, conversation });
|
|
549
|
-
}
|
|
550
|
-
const files = await resolveFiles(source.attachmentIds);
|
|
551
|
-
const history = [];
|
|
552
|
-
for (const message of source.history) {
|
|
553
|
-
const previousFiles = await resolveFiles(message.attachmentIds);
|
|
554
|
-
history.push({ role: message.role, content: previousFiles.content.length
|
|
555
|
-
? [{ type: "text", text: message.content }, ...previousFiles.content] : message.content });
|
|
550
|
+
sendAssistantMessage(event) { response.answer = event; output.sendAssistantMessage(event); },
|
|
551
|
+
sendError(event) { response.error = event; output.sendError(event); },
|
|
552
|
+
sendDone(event) { response.done = event; }
|
|
553
|
+
};
|
|
554
|
+
let result;
|
|
555
|
+
try {
|
|
556
|
+
result = await executeTurn();
|
|
557
|
+
} catch (error) {
|
|
558
|
+
response.failure = { message: String(error?.message || "Assistant request failed."),
|
|
559
|
+
status: Number(error?.status || error?.statusCode || 500) };
|
|
560
|
+
await turnRequests.update(claim, response, "failed");
|
|
561
|
+
throw error;
|
|
556
562
|
}
|
|
563
|
+
await turnRequests.update(claim, response, result.status);
|
|
564
|
+
if (response.done) output.sendDone(response.done);
|
|
565
|
+
return result;
|
|
557
566
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
+
async function executeTurn() {
|
|
568
|
+
const conversationResult = await transcriptService.createConversationForTurn(
|
|
569
|
+
assistantSurface,
|
|
570
|
+
workspace,
|
|
571
|
+
actor,
|
|
572
|
+
{
|
|
573
|
+
conversationId: source.conversationId,
|
|
574
|
+
provider: aiClient.provider,
|
|
575
|
+
model: aiClient.defaultModel,
|
|
567
576
|
surfaceId: assistantSurface.targetSurfaceId,
|
|
568
|
-
|
|
577
|
+
messageId: source.messageId
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
context: assistantContext
|
|
569
581
|
}
|
|
570
|
-
|
|
571
|
-
{
|
|
572
|
-
context: assistantContext,
|
|
573
|
-
workspace
|
|
574
|
-
}
|
|
575
|
-
);
|
|
582
|
+
);
|
|
576
583
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
{
|
|
582
|
-
surface: assistantSurface.targetSurfaceId
|
|
583
|
-
},
|
|
584
|
-
{
|
|
585
|
-
context: assistantContext,
|
|
586
|
-
input: payload
|
|
584
|
+
const conversation = conversationResult.conversation;
|
|
585
|
+
const conversationId = conversation?.id;
|
|
586
|
+
if (!conversationId) {
|
|
587
|
+
throw new AppError(500, "Assistant failed to create conversation.");
|
|
587
588
|
}
|
|
588
|
-
);
|
|
589
|
-
const systemPrompt = buildSystemPrompt({
|
|
590
|
-
targetSurfaceId: assistantSurface.targetSurfaceId,
|
|
591
|
-
toolDescriptors: toolSet.tools,
|
|
592
|
-
workspaceSlug: resolveWorkspaceSlug(assistantContext, payload),
|
|
593
|
-
customSystemPrompt
|
|
594
|
-
});
|
|
595
589
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
590
|
+
async function resolveFiles(attachmentIds) {
|
|
591
|
+
if (attachmentIds === undefined || (Array.isArray(attachmentIds) && !attachmentIds.length)) return { attachments: [], content: [] };
|
|
592
|
+
if (!Array.isArray(attachmentIds) || attachmentIds.length > 10 ||
|
|
593
|
+
attachmentIds.some(id => typeof id !== "string" || !id || id.length > 256)) {
|
|
594
|
+
throw new AppError(400, "Invalid attachment identifiers.");
|
|
595
|
+
}
|
|
596
|
+
if (!attachments?.resolve || !aiClient.supportsAttachments) {
|
|
597
|
+
throw new AppError(400, "File attachments are not configured for this assistant.");
|
|
598
|
+
}
|
|
599
|
+
// Only the application can authorize uploads and turn stored bytes into
|
|
600
|
+
// provider input. Browser-supplied paths, URLs and file contents are never used.
|
|
601
|
+
return attachments.resolve({ attachmentIds, context: assistantContext, workspace, conversation });
|
|
605
602
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
603
|
+
const files = await resolveFiles(source.attachmentIds);
|
|
604
|
+
const history = [];
|
|
605
|
+
for (const message of source.history) {
|
|
606
|
+
const previousFiles = await resolveFiles(message.attachmentIds);
|
|
607
|
+
history.push({ role: message.role, content: previousFiles.content.length
|
|
608
|
+
? [{ type: "text", text: message.content }, ...previousFiles.content] : message.content });
|
|
612
609
|
}
|
|
613
610
|
|
|
614
611
|
await transcriptService.appendMessage(
|
|
615
612
|
assistantSurface,
|
|
616
613
|
conversationId,
|
|
617
614
|
{
|
|
618
|
-
role: "
|
|
615
|
+
role: "user",
|
|
619
616
|
kind: "chat",
|
|
620
|
-
|
|
617
|
+
clientMessageSid: source.messageId,
|
|
618
|
+
contentText: source.input,
|
|
619
|
+
metadata: {
|
|
620
|
+
surfaceId: assistantSurface.targetSurfaceId,
|
|
621
|
+
...(files.attachments.length ? { attachments: files.attachments } : {})
|
|
622
|
+
}
|
|
621
623
|
},
|
|
622
624
|
{
|
|
623
625
|
context: assistantContext,
|
|
@@ -625,75 +627,50 @@ function createChatService({
|
|
|
625
627
|
}
|
|
626
628
|
);
|
|
627
629
|
|
|
628
|
-
|
|
630
|
+
const toolSet = serviceToolCatalog.resolveToolSet(assistantContext);
|
|
631
|
+
const customSystemPrompt = await assistantConfigService.resolveSystemPrompt(
|
|
629
632
|
assistantSurface,
|
|
630
|
-
|
|
633
|
+
workspace,
|
|
631
634
|
{
|
|
632
|
-
|
|
633
|
-
metadata
|
|
635
|
+
surface: assistantSurface.targetSurfaceId
|
|
634
636
|
},
|
|
635
637
|
{
|
|
636
638
|
context: assistantContext,
|
|
637
|
-
|
|
639
|
+
input: payload
|
|
638
640
|
}
|
|
639
641
|
);
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
streamWriter.sendDone({
|
|
646
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
|
|
647
|
-
messageId: source.messageId,
|
|
648
|
-
status: "completed"
|
|
642
|
+
const systemPrompt = buildSystemPrompt({
|
|
643
|
+
targetSurfaceId: assistantSurface.targetSurfaceId,
|
|
644
|
+
toolDescriptors: toolSet.tools,
|
|
645
|
+
workspaceSlug: resolveWorkspaceSlug(assistantContext, payload),
|
|
646
|
+
customSystemPrompt
|
|
649
647
|
});
|
|
650
648
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (displayedText) {
|
|
661
|
-
streamWriter.sendAssistantMessage({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE, text: "", status: "streaming" });
|
|
662
|
-
displayedText = "";
|
|
663
|
-
}
|
|
664
|
-
return consumeCompletionStream(stream, text => {
|
|
665
|
-
if (text === displayedText) return;
|
|
666
|
-
if (text.startsWith(displayedText)) {
|
|
667
|
-
streamWriter.sendAssistantDelta({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA, delta: text.slice(displayedText.length) });
|
|
668
|
-
} else {
|
|
669
|
-
streamWriter.sendAssistantMessage({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE, text, status: "streaming" });
|
|
649
|
+
const messages = [
|
|
650
|
+
{
|
|
651
|
+
role: "system",
|
|
652
|
+
content: systemPrompt
|
|
653
|
+
},
|
|
654
|
+
...history,
|
|
655
|
+
{
|
|
656
|
+
role: "user",
|
|
657
|
+
content: files.content.length ? [{ type: "text", text: source.input }, ...files.content] : source.input
|
|
670
658
|
}
|
|
671
|
-
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
async function executeToolCalls(toolCalls = [], { toolFailures = [], toolSuccesses = [] } = {}) {
|
|
676
|
-
const roundFailures = [];
|
|
659
|
+
];
|
|
677
660
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
arguments: toolCall.arguments
|
|
684
|
-
});
|
|
661
|
+
async function completeWithAssistantMessage(assistantMessageText, { metadata = {} } = {}) {
|
|
662
|
+
const normalizedAssistantMessageText = normalizeText(sanitizeAssistantMessageText(assistantMessageText));
|
|
663
|
+
if (!normalizedAssistantMessageText) {
|
|
664
|
+
throw new AppError(502, "Assistant returned no output.");
|
|
665
|
+
}
|
|
685
666
|
|
|
686
667
|
await transcriptService.appendMessage(
|
|
687
668
|
assistantSurface,
|
|
688
669
|
conversationId,
|
|
689
670
|
{
|
|
690
671
|
role: "assistant",
|
|
691
|
-
kind: "
|
|
692
|
-
contentText:
|
|
693
|
-
metadata: {
|
|
694
|
-
toolCallId: toolCall.id,
|
|
695
|
-
tool: toolCall.name
|
|
696
|
-
}
|
|
672
|
+
kind: "chat",
|
|
673
|
+
contentText: normalizedAssistantMessageText
|
|
697
674
|
},
|
|
698
675
|
{
|
|
699
676
|
context: assistantContext,
|
|
@@ -701,25 +678,12 @@ function createChatService({
|
|
|
701
678
|
}
|
|
702
679
|
);
|
|
703
680
|
|
|
704
|
-
|
|
705
|
-
toolName: toolCall.name,
|
|
706
|
-
argumentsText: toolCall.arguments,
|
|
707
|
-
context: assistantContext,
|
|
708
|
-
toolSet
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
await transcriptService.appendMessage(
|
|
681
|
+
await transcriptService.completeConversation(
|
|
712
682
|
assistantSurface,
|
|
713
683
|
conversationId,
|
|
714
684
|
{
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
contentText: JSON.stringify(toolResult),
|
|
718
|
-
metadata: {
|
|
719
|
-
toolCallId: toolCall.id,
|
|
720
|
-
tool: toolCall.name,
|
|
721
|
-
ok: toolResult.ok === true
|
|
722
|
-
}
|
|
685
|
+
status: "completed",
|
|
686
|
+
metadata
|
|
723
687
|
},
|
|
724
688
|
{
|
|
725
689
|
context: assistantContext,
|
|
@@ -727,236 +691,328 @@ function createChatService({
|
|
|
727
691
|
}
|
|
728
692
|
);
|
|
729
693
|
|
|
730
|
-
|
|
731
|
-
|
|
694
|
+
streamWriter.sendAssistantMessage({
|
|
695
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE,
|
|
696
|
+
text: normalizedAssistantMessageText
|
|
697
|
+
});
|
|
698
|
+
streamWriter.sendDone({
|
|
699
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
|
|
700
|
+
messageId: source.messageId,
|
|
701
|
+
status: "completed"
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
conversationId,
|
|
706
|
+
messageId: source.messageId,
|
|
707
|
+
status: "completed"
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
let displayedText = "";
|
|
712
|
+
async function consumeResponse(stream) {
|
|
713
|
+
if (displayedText) {
|
|
714
|
+
streamWriter.sendAssistantMessage({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE, text: "", status: "streaming" });
|
|
715
|
+
displayedText = "";
|
|
716
|
+
}
|
|
717
|
+
return consumeCompletionStream(stream, text => {
|
|
718
|
+
if (text === displayedText) return;
|
|
719
|
+
if (text.startsWith(displayedText)) {
|
|
720
|
+
streamWriter.sendAssistantDelta({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_DELTA, delta: text.slice(displayedText.length) });
|
|
721
|
+
} else {
|
|
722
|
+
streamWriter.sendAssistantMessage({ type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE, text, status: "streaming" });
|
|
723
|
+
}
|
|
724
|
+
displayedText = text;
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function executeToolCalls(toolCalls = [], { toolFailures = [], toolSuccesses = [] } = {}) {
|
|
729
|
+
const roundFailures = [];
|
|
730
|
+
|
|
731
|
+
for (const toolCall of toolCalls) {
|
|
732
|
+
streamWriter.sendToolCall({
|
|
733
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_CALL,
|
|
734
|
+
toolCallId: toolCall.id,
|
|
732
735
|
name: toolCall.name,
|
|
733
|
-
|
|
736
|
+
arguments: toolCall.arguments
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
await transcriptService.appendMessage(
|
|
740
|
+
assistantSurface,
|
|
741
|
+
conversationId,
|
|
742
|
+
{
|
|
743
|
+
role: "assistant",
|
|
744
|
+
kind: "tool_call",
|
|
745
|
+
contentText: toolCall.arguments,
|
|
746
|
+
metadata: {
|
|
747
|
+
toolCallId: toolCall.id,
|
|
748
|
+
tool: toolCall.name
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
context: assistantContext,
|
|
753
|
+
workspace
|
|
754
|
+
}
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
const toolResult = await serviceToolCatalog.executeToolCall({
|
|
758
|
+
toolName: toolCall.name,
|
|
759
|
+
argumentsText: toolCall.arguments,
|
|
760
|
+
context: assistantContext,
|
|
761
|
+
toolSet
|
|
734
762
|
});
|
|
763
|
+
|
|
764
|
+
await transcriptService.appendMessage(
|
|
765
|
+
assistantSurface,
|
|
766
|
+
conversationId,
|
|
767
|
+
{
|
|
768
|
+
role: "assistant",
|
|
769
|
+
kind: "tool_result",
|
|
770
|
+
contentText: JSON.stringify(toolResult),
|
|
771
|
+
metadata: {
|
|
772
|
+
toolCallId: toolCall.id,
|
|
773
|
+
tool: toolCall.name,
|
|
774
|
+
ok: toolResult.ok === true
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
context: assistantContext,
|
|
779
|
+
workspace
|
|
780
|
+
}
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
if (toolResult.ok) {
|
|
784
|
+
toolSuccesses.push({
|
|
785
|
+
name: toolCall.name,
|
|
786
|
+
result: toolResult.result
|
|
787
|
+
});
|
|
788
|
+
streamWriter.sendToolResult({
|
|
789
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
|
|
790
|
+
toolCallId: toolCall.id,
|
|
791
|
+
name: toolCall.name,
|
|
792
|
+
ok: true,
|
|
793
|
+
result: toolResult.result
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
messages.push({
|
|
797
|
+
role: "tool",
|
|
798
|
+
tool_call_id: toolCall.id,
|
|
799
|
+
content: JSON.stringify(toolResult.result ?? null)
|
|
800
|
+
});
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const failure = {
|
|
805
|
+
name: toolCall.name,
|
|
806
|
+
error: toolResult.error
|
|
807
|
+
};
|
|
808
|
+
roundFailures.push(failure);
|
|
809
|
+
toolFailures.push(failure);
|
|
810
|
+
|
|
735
811
|
streamWriter.sendToolResult({
|
|
736
812
|
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
|
|
737
813
|
toolCallId: toolCall.id,
|
|
738
814
|
name: toolCall.name,
|
|
739
|
-
ok:
|
|
740
|
-
|
|
815
|
+
ok: false,
|
|
816
|
+
error: toolResult.error
|
|
741
817
|
});
|
|
742
818
|
|
|
743
819
|
messages.push({
|
|
744
820
|
role: "tool",
|
|
745
821
|
tool_call_id: toolCall.id,
|
|
746
|
-
content: JSON.stringify(
|
|
822
|
+
content: JSON.stringify({
|
|
823
|
+
error: toolResult.error
|
|
824
|
+
})
|
|
747
825
|
});
|
|
748
|
-
continue;
|
|
749
826
|
}
|
|
750
827
|
|
|
751
|
-
|
|
752
|
-
name: toolCall.name,
|
|
753
|
-
error: toolResult.error
|
|
754
|
-
};
|
|
755
|
-
roundFailures.push(failure);
|
|
756
|
-
toolFailures.push(failure);
|
|
757
|
-
|
|
758
|
-
streamWriter.sendToolResult({
|
|
759
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_RESULT,
|
|
760
|
-
toolCallId: toolCall.id,
|
|
761
|
-
name: toolCall.name,
|
|
762
|
-
ok: false,
|
|
763
|
-
error: toolResult.error
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
messages.push({
|
|
767
|
-
role: "tool",
|
|
768
|
-
tool_call_id: toolCall.id,
|
|
769
|
-
content: JSON.stringify({
|
|
770
|
-
error: toolResult.error
|
|
771
|
-
})
|
|
772
|
-
});
|
|
828
|
+
return roundFailures;
|
|
773
829
|
}
|
|
774
830
|
|
|
775
|
-
|
|
776
|
-
|
|
831
|
+
async function recoverWithoutTools({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
|
|
832
|
+
for (let pass = 0; pass < MAX_RECOVERY_PASSES; pass += 1) {
|
|
833
|
+
const recoveryMessages = [
|
|
834
|
+
...messages,
|
|
835
|
+
{
|
|
836
|
+
role: "system",
|
|
837
|
+
content: buildRecoveryPrompt({
|
|
838
|
+
reason,
|
|
839
|
+
toolFailures,
|
|
840
|
+
toolSuccesses
|
|
841
|
+
})
|
|
842
|
+
}
|
|
843
|
+
];
|
|
777
844
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
content: buildRecoveryPrompt({
|
|
785
|
-
reason,
|
|
786
|
-
toolFailures,
|
|
787
|
-
toolSuccesses
|
|
788
|
-
})
|
|
789
|
-
}
|
|
790
|
-
];
|
|
845
|
+
const completionStream = await aiClient.createChatCompletionStream({
|
|
846
|
+
messages: recoveryMessages,
|
|
847
|
+
tools: [],
|
|
848
|
+
signal: options.abortSignal
|
|
849
|
+
});
|
|
850
|
+
const completion = await consumeResponse(completionStream);
|
|
791
851
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
});
|
|
797
|
-
const completion = await consumeResponse(completionStream);
|
|
852
|
+
const recoveryToolCalls = completion.toolCalls.filter((entry) => entry.name);
|
|
853
|
+
if (recoveryToolCalls.length > 0) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
798
856
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
857
|
+
const assistantMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
858
|
+
if (assistantMessageText && !isAssistantProgressOnlyText(assistantMessageText)) {
|
|
859
|
+
return completeWithAssistantMessage(assistantMessageText, {
|
|
860
|
+
metadata: {
|
|
861
|
+
recoveryReason: reason || "unknown",
|
|
862
|
+
toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
802
866
|
}
|
|
803
867
|
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
868
|
+
const fallbackText = buildRecoveryFallbackAnswer({
|
|
869
|
+
reason,
|
|
870
|
+
toolFailures,
|
|
871
|
+
toolSuccesses
|
|
872
|
+
});
|
|
873
|
+
return completeWithAssistantMessage(fallbackText, {
|
|
874
|
+
metadata: {
|
|
875
|
+
recoveryReason: reason || "unknown",
|
|
876
|
+
toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
|
|
877
|
+
}
|
|
878
|
+
});
|
|
813
879
|
}
|
|
814
880
|
|
|
815
|
-
|
|
816
|
-
reason,
|
|
817
|
-
toolFailures,
|
|
818
|
-
toolSuccesses
|
|
819
|
-
});
|
|
820
|
-
return completeWithAssistantMessage(fallbackText, {
|
|
821
|
-
metadata: {
|
|
822
|
-
recoveryReason: reason || "unknown",
|
|
823
|
-
toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
let streamed = false;
|
|
881
|
+
let streamed = false;
|
|
829
882
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
});
|
|
838
|
-
streamed = true;
|
|
839
|
-
|
|
840
|
-
const excludedToolNames = new Set();
|
|
841
|
-
const toolFailures = [];
|
|
842
|
-
const toolSuccesses = [];
|
|
843
|
-
|
|
844
|
-
const preflightTools = resolvePreflightTools(toolSet.tools, source.input);
|
|
845
|
-
for (const [index, tool] of preflightTools.entries()) {
|
|
846
|
-
const toolCall = {
|
|
847
|
-
id: `assistant_preflight_${index + 1}`,
|
|
848
|
-
name: tool.name,
|
|
849
|
-
arguments: "{}"
|
|
883
|
+
try {
|
|
884
|
+
const receipt = {
|
|
885
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.META,
|
|
886
|
+
messageId: source.messageId,
|
|
887
|
+
conversationId,
|
|
888
|
+
provider: aiClient.provider,
|
|
889
|
+
model: aiClient.defaultModel
|
|
850
890
|
};
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
891
|
+
await turnRequests.update(claim, { meta: receipt });
|
|
892
|
+
streamWriter.sendMeta(receipt);
|
|
893
|
+
streamed = true;
|
|
894
|
+
|
|
895
|
+
const excludedToolNames = new Set();
|
|
896
|
+
const toolFailures = [];
|
|
897
|
+
const toolSuccesses = [];
|
|
898
|
+
|
|
899
|
+
const preflightTools = resolvePreflightTools(toolSet.tools, source.input);
|
|
900
|
+
for (const [index, tool] of preflightTools.entries()) {
|
|
901
|
+
const toolCall = {
|
|
902
|
+
id: `assistant_preflight_${index + 1}`,
|
|
903
|
+
name: tool.name,
|
|
904
|
+
arguments: "{}"
|
|
905
|
+
};
|
|
906
|
+
messages.push(buildAssistantToolCallMessage([toolCall]));
|
|
907
|
+
const preflightFailures = await executeToolCalls([toolCall], {
|
|
908
|
+
toolFailures,
|
|
909
|
+
toolSuccesses
|
|
910
|
+
});
|
|
911
|
+
for (const failure of preflightFailures) {
|
|
912
|
+
const toolName = normalizeText(failure?.name);
|
|
913
|
+
if (toolName) {
|
|
914
|
+
excludedToolNames.add(toolName);
|
|
915
|
+
}
|
|
860
916
|
}
|
|
861
917
|
}
|
|
862
|
-
}
|
|
863
918
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
919
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
|
|
920
|
+
const roundToolDescriptors = toolSet.tools.filter(
|
|
921
|
+
(tool) => !excludedToolNames.has(normalizeText(tool.name))
|
|
922
|
+
);
|
|
923
|
+
const roundToolSchemas = roundToolDescriptors.map((tool) => serviceToolCatalog.toOpenAiToolSchema(tool));
|
|
869
924
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
925
|
+
const completionStream = await aiClient.createChatCompletionStream({
|
|
926
|
+
messages,
|
|
927
|
+
tools: roundToolSchemas,
|
|
928
|
+
signal: options.abortSignal
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
const completion = await consumeResponse(completionStream);
|
|
932
|
+
|
|
933
|
+
const toolCalls = completion.toolCalls.filter((entry) => entry.name);
|
|
934
|
+
if (toolCalls.length < 1) {
|
|
935
|
+
const finalMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
936
|
+
if (finalMessageText && !isAssistantProgressOnlyText(finalMessageText)) {
|
|
937
|
+
return completeWithAssistantMessage(finalMessageText, {
|
|
938
|
+
metadata: toolFailures.length > 0
|
|
939
|
+
? {
|
|
940
|
+
recoveryReason: "tool_failure",
|
|
941
|
+
toolFailureCount: toolFailures.length
|
|
942
|
+
}
|
|
943
|
+
: {}
|
|
944
|
+
});
|
|
945
|
+
}
|
|
875
946
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
if (toolCalls.length < 1) {
|
|
880
|
-
const finalMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
|
|
881
|
-
if (finalMessageText && !isAssistantProgressOnlyText(finalMessageText)) {
|
|
882
|
-
return completeWithAssistantMessage(finalMessageText, {
|
|
883
|
-
metadata: toolFailures.length > 0
|
|
884
|
-
? {
|
|
885
|
-
recoveryReason: "tool_failure",
|
|
886
|
-
toolFailureCount: toolFailures.length
|
|
887
|
-
}
|
|
888
|
-
: {}
|
|
947
|
+
messages.push({
|
|
948
|
+
role: "system",
|
|
949
|
+
content: COMPLETION_INSTRUCTION
|
|
889
950
|
});
|
|
951
|
+
continue;
|
|
890
952
|
}
|
|
891
953
|
|
|
892
|
-
messages.push(
|
|
893
|
-
|
|
894
|
-
|
|
954
|
+
messages.push(buildAssistantToolCallMessage(toolCalls));
|
|
955
|
+
|
|
956
|
+
const roundFailures = await executeToolCalls(toolCalls, {
|
|
957
|
+
toolFailures,
|
|
958
|
+
toolSuccesses
|
|
895
959
|
});
|
|
896
|
-
continue;
|
|
897
|
-
}
|
|
898
960
|
|
|
899
|
-
|
|
961
|
+
if (roundFailures.length > 0) {
|
|
962
|
+
for (const failure of roundFailures) {
|
|
963
|
+
const toolName = normalizeText(failure?.name);
|
|
964
|
+
if (toolName) {
|
|
965
|
+
excludedToolNames.add(toolName);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
900
970
|
|
|
901
|
-
|
|
971
|
+
return recoverWithoutTools({
|
|
972
|
+
reason: toolFailures.length > 0 ? "tool_failure" : "max_tool_rounds",
|
|
902
973
|
toolFailures,
|
|
903
974
|
toolSuccesses
|
|
904
975
|
});
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
976
|
+
} catch (error) {
|
|
977
|
+
const aborted = isAbortError(error);
|
|
978
|
+
const status = aborted ? "aborted" : "failed";
|
|
979
|
+
|
|
980
|
+
if (streamed) {
|
|
981
|
+
await transcriptService.completeConversation(
|
|
982
|
+
assistantSurface,
|
|
983
|
+
conversationId,
|
|
984
|
+
{
|
|
985
|
+
status
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
context: assistantContext,
|
|
989
|
+
workspace
|
|
911
990
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
} catch (error) {
|
|
922
|
-
const aborted = isAbortError(error);
|
|
923
|
-
const status = aborted ? "aborted" : "failed";
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
streamWriter.sendError({
|
|
994
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.ERROR,
|
|
995
|
+
messageId: source.messageId,
|
|
996
|
+
code: aborted ? "assistant_stream_aborted" : String(error?.code || "assistant_stream_failed"),
|
|
997
|
+
message: aborted ? "Assistant request was cancelled." : String(error?.message || "Assistant request failed."),
|
|
998
|
+
status: aborted ? 499 : Number(error?.status || error?.statusCode || 500)
|
|
999
|
+
});
|
|
924
1000
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
conversationId,
|
|
929
|
-
{
|
|
1001
|
+
streamWriter.sendDone({
|
|
1002
|
+
type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
|
|
1003
|
+
messageId: source.messageId,
|
|
930
1004
|
status
|
|
931
|
-
}
|
|
932
|
-
{
|
|
933
|
-
context: assistantContext,
|
|
934
|
-
workspace
|
|
935
|
-
}
|
|
936
|
-
);
|
|
937
|
-
|
|
938
|
-
streamWriter.sendError({
|
|
939
|
-
type: ASSISTANT_STREAM_EVENT_TYPES.ERROR,
|
|
940
|
-
messageId: source.messageId,
|
|
941
|
-
code: aborted ? "assistant_stream_aborted" : String(error?.code || "assistant_stream_failed"),
|
|
942
|
-
message: aborted ? "Assistant request was cancelled." : String(error?.message || "Assistant request failed."),
|
|
943
|
-
status: aborted ? 499 : Number(error?.status || error?.statusCode || 500)
|
|
944
|
-
});
|
|
1005
|
+
});
|
|
945
1006
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1007
|
+
return {
|
|
1008
|
+
conversationId,
|
|
1009
|
+
messageId: source.messageId,
|
|
1010
|
+
status
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
951
1013
|
|
|
952
|
-
|
|
953
|
-
conversationId,
|
|
954
|
-
messageId: source.messageId,
|
|
955
|
-
status
|
|
956
|
-
};
|
|
1014
|
+
throw error;
|
|
957
1015
|
}
|
|
958
|
-
|
|
959
|
-
throw error;
|
|
960
1016
|
}
|
|
961
1017
|
}
|
|
962
1018
|
|