@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.
@@ -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
- throw new AppError(503, "Assistant provider is not configured.");
505
- }
506
-
507
- const source = normalizeStreamInput(payload);
508
- const streamWriter = options.streamWriter;
509
- if (!hasStreamWriter(streamWriter)) {
510
- throw new Error("assistant.chat.stream requires streamWriter methods.");
511
- }
512
-
513
- const actor = context.actor;
514
-
515
- const conversationResult = await transcriptService.createConversationForTurn(
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
- context: assistantContext
528
- }
529
- );
530
-
531
- const conversation = conversationResult.conversation;
532
- const conversationId = conversation?.id;
533
- if (!conversationId) {
534
- throw new AppError(500, "Assistant failed to create conversation.");
535
- }
536
-
537
- async function resolveFiles(attachmentIds) {
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
- await transcriptService.appendMessage(
559
- assistantSurface,
560
- conversationId,
561
- {
562
- role: "user",
563
- kind: "chat",
564
- clientMessageSid: source.messageId,
565
- contentText: source.input,
566
- metadata: {
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
- ...(files.attachments.length ? { attachments: files.attachments } : {})
577
+ messageId: source.messageId
578
+ },
579
+ {
580
+ context: assistantContext
569
581
  }
570
- },
571
- {
572
- context: assistantContext,
573
- workspace
574
- }
575
- );
582
+ );
576
583
 
577
- const toolSet = serviceToolCatalog.resolveToolSet(assistantContext);
578
- const customSystemPrompt = await assistantConfigService.resolveSystemPrompt(
579
- assistantSurface,
580
- workspace,
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
- const messages = [
597
- {
598
- role: "system",
599
- content: systemPrompt
600
- },
601
- ...history,
602
- {
603
- role: "user",
604
- content: files.content.length ? [{ type: "text", text: source.input }, ...files.content] : source.input
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
- async function completeWithAssistantMessage(assistantMessageText, { metadata = {} } = {}) {
609
- const normalizedAssistantMessageText = normalizeText(sanitizeAssistantMessageText(assistantMessageText));
610
- if (!normalizedAssistantMessageText) {
611
- throw new AppError(502, "Assistant returned no output.");
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: "assistant",
615
+ role: "user",
619
616
  kind: "chat",
620
- contentText: normalizedAssistantMessageText
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
- await transcriptService.completeConversation(
630
+ const toolSet = serviceToolCatalog.resolveToolSet(assistantContext);
631
+ const customSystemPrompt = await assistantConfigService.resolveSystemPrompt(
629
632
  assistantSurface,
630
- conversationId,
633
+ workspace,
631
634
  {
632
- status: "completed",
633
- metadata
635
+ surface: assistantSurface.targetSurfaceId
634
636
  },
635
637
  {
636
638
  context: assistantContext,
637
- workspace
639
+ input: payload
638
640
  }
639
641
  );
640
-
641
- streamWriter.sendAssistantMessage({
642
- type: ASSISTANT_STREAM_EVENT_TYPES.ASSISTANT_MESSAGE,
643
- text: normalizedAssistantMessageText
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
- return {
652
- conversationId,
653
- messageId: source.messageId,
654
- status: "completed"
655
- };
656
- }
657
-
658
- let displayedText = "";
659
- async function consumeResponse(stream) {
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
- displayedText = text;
672
- });
673
- }
674
-
675
- async function executeToolCalls(toolCalls = [], { toolFailures = [], toolSuccesses = [] } = {}) {
676
- const roundFailures = [];
659
+ ];
677
660
 
678
- for (const toolCall of toolCalls) {
679
- streamWriter.sendToolCall({
680
- type: ASSISTANT_STREAM_EVENT_TYPES.TOOL_CALL,
681
- toolCallId: toolCall.id,
682
- name: toolCall.name,
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: "tool_call",
692
- contentText: toolCall.arguments,
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
- const toolResult = await serviceToolCatalog.executeToolCall({
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
- role: "assistant",
716
- kind: "tool_result",
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
- if (toolResult.ok) {
731
- toolSuccesses.push({
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
- result: toolResult.result
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: true,
740
- result: toolResult.result
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(toolResult.result ?? null)
822
+ content: JSON.stringify({
823
+ error: toolResult.error
824
+ })
747
825
  });
748
- continue;
749
826
  }
750
827
 
751
- const failure = {
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
- return roundFailures;
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
- async function recoverWithoutTools({ reason = "", toolFailures = [], toolSuccesses = [] } = {}) {
779
- for (let pass = 0; pass < MAX_RECOVERY_PASSES; pass += 1) {
780
- const recoveryMessages = [
781
- ...messages,
782
- {
783
- role: "system",
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
- const completionStream = await aiClient.createChatCompletionStream({
793
- messages: recoveryMessages,
794
- tools: [],
795
- signal: options.abortSignal
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
- const recoveryToolCalls = completion.toolCalls.filter((entry) => entry.name);
800
- if (recoveryToolCalls.length > 0) {
801
- continue;
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 assistantMessageText = normalizeText(sanitizeAssistantMessageText(completion.assistantText));
805
- if (assistantMessageText && !isAssistantProgressOnlyText(assistantMessageText)) {
806
- return completeWithAssistantMessage(assistantMessageText, {
807
- metadata: {
808
- recoveryReason: reason || "unknown",
809
- toolFailureCount: Array.isArray(toolFailures) ? toolFailures.length : 0
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
- const fallbackText = buildRecoveryFallbackAnswer({
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
- try {
831
- streamWriter.sendMeta({
832
- type: ASSISTANT_STREAM_EVENT_TYPES.META,
833
- messageId: source.messageId,
834
- conversationId,
835
- provider: aiClient.provider,
836
- model: aiClient.defaultModel
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
- messages.push(buildAssistantToolCallMessage([toolCall]));
852
- const preflightFailures = await executeToolCalls([toolCall], {
853
- toolFailures,
854
- toolSuccesses
855
- });
856
- for (const failure of preflightFailures) {
857
- const toolName = normalizeText(failure?.name);
858
- if (toolName) {
859
- excludedToolNames.add(toolName);
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
- for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
865
- const roundToolDescriptors = toolSet.tools.filter(
866
- (tool) => !excludedToolNames.has(normalizeText(tool.name))
867
- );
868
- const roundToolSchemas = roundToolDescriptors.map((tool) => serviceToolCatalog.toOpenAiToolSchema(tool));
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
- const completionStream = await aiClient.createChatCompletionStream({
871
- messages,
872
- tools: roundToolSchemas,
873
- signal: options.abortSignal
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
- const completion = await consumeResponse(completionStream);
877
-
878
- const toolCalls = completion.toolCalls.filter((entry) => entry.name);
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
- role: "system",
894
- content: COMPLETION_INSTRUCTION
954
+ messages.push(buildAssistantToolCallMessage(toolCalls));
955
+
956
+ const roundFailures = await executeToolCalls(toolCalls, {
957
+ toolFailures,
958
+ toolSuccesses
895
959
  });
896
- continue;
897
- }
898
960
 
899
- messages.push(buildAssistantToolCallMessage(toolCalls));
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
- const roundFailures = await executeToolCalls(toolCalls, {
971
+ return recoverWithoutTools({
972
+ reason: toolFailures.length > 0 ? "tool_failure" : "max_tool_rounds",
902
973
  toolFailures,
903
974
  toolSuccesses
904
975
  });
905
-
906
- if (roundFailures.length > 0) {
907
- for (const failure of roundFailures) {
908
- const toolName = normalizeText(failure?.name);
909
- if (toolName) {
910
- excludedToolNames.add(toolName);
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
- return recoverWithoutTools({
917
- reason: toolFailures.length > 0 ? "tool_failure" : "max_tool_rounds",
918
- toolFailures,
919
- toolSuccesses
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
- if (streamed) {
926
- await transcriptService.completeConversation(
927
- assistantSurface,
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
- streamWriter.sendDone({
947
- type: ASSISTANT_STREAM_EVENT_TYPES.DONE,
948
- messageId: source.messageId,
949
- status
950
- });
1007
+ return {
1008
+ conversationId,
1009
+ messageId: source.messageId,
1010
+ status
1011
+ };
1012
+ }
951
1013
 
952
- return {
953
- conversationId,
954
- messageId: source.messageId,
955
- status
956
- };
1014
+ throw error;
957
1015
  }
958
-
959
- throw error;
960
1016
  }
961
1017
  }
962
1018