assistant-stream 0.3.13 → 0.3.15

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.
Files changed (103) hide show
  1. package/README.md +39 -0
  2. package/dist/core/AssistantStream.d.ts +37 -0
  3. package/dist/core/AssistantStream.d.ts.map +1 -1
  4. package/dist/core/AssistantStream.js +22 -0
  5. package/dist/core/AssistantStream.js.map +1 -1
  6. package/dist/core/AssistantStreamChunk.d.ts +32 -0
  7. package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
  8. package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
  9. package/dist/core/accumulators/assistant-message-accumulator.js +3 -0
  10. package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
  11. package/dist/core/modules/assistant-stream.d.ts +68 -0
  12. package/dist/core/modules/assistant-stream.d.ts.map +1 -1
  13. package/dist/core/modules/assistant-stream.js +23 -0
  14. package/dist/core/modules/assistant-stream.js.map +1 -1
  15. package/dist/core/modules/tool-call.d.ts.map +1 -1
  16. package/dist/core/modules/tool-call.js +3 -0
  17. package/dist/core/modules/tool-call.js.map +1 -1
  18. package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
  19. package/dist/core/tool/ToolExecutionStream.js +3 -0
  20. package/dist/core/tool/ToolExecutionStream.js.map +1 -1
  21. package/dist/core/tool/ToolResponse.d.ts +44 -0
  22. package/dist/core/tool/ToolResponse.d.ts.map +1 -1
  23. package/dist/core/tool/ToolResponse.js +27 -0
  24. package/dist/core/tool/ToolResponse.js.map +1 -1
  25. package/dist/core/tool/tool-types.d.ts +119 -2
  26. package/dist/core/tool/tool-types.d.ts.map +1 -1
  27. package/dist/core/tool/toolResultStream.d.ts +15 -0
  28. package/dist/core/tool/toolResultStream.d.ts.map +1 -1
  29. package/dist/core/tool/toolResultStream.js +39 -1
  30. package/dist/core/tool/toolResultStream.js.map +1 -1
  31. package/dist/core/utils/types.d.ts +4 -0
  32. package/dist/core/utils/types.d.ts.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/resumable/ResumableStreamContext.d.ts +27 -0
  37. package/dist/resumable/ResumableStreamContext.d.ts.map +1 -0
  38. package/dist/resumable/ResumableStreamContext.js +121 -0
  39. package/dist/resumable/ResumableStreamContext.js.map +1 -0
  40. package/dist/resumable/constants.d.ts +2 -0
  41. package/dist/resumable/constants.d.ts.map +1 -0
  42. package/dist/resumable/constants.js +2 -0
  43. package/dist/resumable/constants.js.map +1 -0
  44. package/dist/resumable/createResumableAssistantStreamResponse.d.ts +24 -0
  45. package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -0
  46. package/dist/resumable/createResumableAssistantStreamResponse.js +40 -0
  47. package/dist/resumable/createResumableAssistantStreamResponse.js.map +1 -0
  48. package/dist/resumable/errors.d.ts +7 -0
  49. package/dist/resumable/errors.d.ts.map +1 -0
  50. package/dist/resumable/errors.js +15 -0
  51. package/dist/resumable/errors.js.map +1 -0
  52. package/dist/resumable/index.d.ts +7 -0
  53. package/dist/resumable/index.d.ts.map +1 -0
  54. package/dist/resumable/index.js +5 -0
  55. package/dist/resumable/index.js.map +1 -0
  56. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +13 -0
  57. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -0
  58. package/dist/resumable/stores/InMemoryResumableStreamStore.js +199 -0
  59. package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -0
  60. package/dist/resumable/stores/ioredis.d.ts +10 -0
  61. package/dist/resumable/stores/ioredis.d.ts.map +1 -0
  62. package/dist/resumable/stores/ioredis.js +95 -0
  63. package/dist/resumable/stores/ioredis.js.map +1 -0
  64. package/dist/resumable/stores/redis-impl.d.ts +60 -0
  65. package/dist/resumable/stores/redis-impl.d.ts.map +1 -0
  66. package/dist/resumable/stores/redis-impl.js +198 -0
  67. package/dist/resumable/stores/redis-impl.js.map +1 -0
  68. package/dist/resumable/stores/redis.d.ts +39 -0
  69. package/dist/resumable/stores/redis.d.ts.map +1 -0
  70. package/dist/resumable/stores/redis.js +113 -0
  71. package/dist/resumable/stores/redis.js.map +1 -0
  72. package/dist/resumable/types.d.ts +30 -0
  73. package/dist/resumable/types.d.ts.map +1 -0
  74. package/dist/resumable/types.js +2 -0
  75. package/dist/resumable/types.js.map +1 -0
  76. package/package.json +28 -2
  77. package/src/core/AssistantStream.ts +37 -0
  78. package/src/core/AssistantStreamChunk.ts +32 -0
  79. package/src/core/accumulators/assistant-message-accumulator.ts +3 -0
  80. package/src/core/modules/assistant-stream.ts +68 -0
  81. package/src/core/modules/tool-call.ts +3 -0
  82. package/src/core/tool/ToolExecutionStream.ts +3 -0
  83. package/src/core/tool/ToolResponse.ts +50 -0
  84. package/src/core/tool/tool-types.ts +125 -2
  85. package/src/core/tool/toolResultStream.test.ts +360 -2
  86. package/src/core/tool/toolResultStream.ts +45 -1
  87. package/src/core/utils/types.ts +4 -0
  88. package/src/index.ts +5 -1
  89. package/src/resumable/ResumableStreamContext.test.ts +274 -0
  90. package/src/resumable/ResumableStreamContext.ts +187 -0
  91. package/src/resumable/__tests__/integration.test.ts +159 -0
  92. package/src/resumable/constants.ts +1 -0
  93. package/src/resumable/createResumableAssistantStreamResponse.test.ts +243 -0
  94. package/src/resumable/createResumableAssistantStreamResponse.ts +80 -0
  95. package/src/resumable/errors.ts +26 -0
  96. package/src/resumable/index.ts +36 -0
  97. package/src/resumable/stores/InMemoryResumableStreamStore.test.ts +285 -0
  98. package/src/resumable/stores/InMemoryResumableStreamStore.ts +237 -0
  99. package/src/resumable/stores/ioredis.ts +123 -0
  100. package/src/resumable/stores/redis-impl.ts +304 -0
  101. package/src/resumable/stores/redis.test.ts +265 -0
  102. package/src/resumable/stores/redis.ts +171 -0
  103. package/src/resumable/types.ts +49 -0
@@ -1,5 +1,10 @@
1
- import { describe, expect, it } from "vitest";
2
- import { unstable_runPendingTools } from "./toolResultStream";
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ toolResultStream as unstable_toolResultStream,
4
+ unstable_runPendingTools,
5
+ } from "./toolResultStream";
6
+ import { ToolResponse } from "./ToolResponse";
7
+ import type { AssistantStreamChunk } from "../AssistantStreamChunk";
3
8
  import type { AssistantMessage, ToolCallPart } from "../utils/types";
4
9
  import type { Tool } from "./tool-types";
5
10
 
@@ -397,4 +402,357 @@ describe("unstable_runPendingTools", () => {
397
402
  });
398
403
  });
399
404
  });
405
+
406
+ describe("toModelOutput", () => {
407
+ it("attaches modelContent from toModelOutput onto the resolved tool-call part", async () => {
408
+ const tool: Tool = {
409
+ parameters: { type: "object", properties: {} },
410
+ execute: async () => ({
411
+ mediaType: "application/pdf",
412
+ base64: "JVBERi0xLjQK",
413
+ }),
414
+ toModelOutput: ({ output }) => {
415
+ const o = output as { base64: string; mediaType: string };
416
+ return [
417
+ { type: "text", text: "PDF contents:" },
418
+ {
419
+ type: "file",
420
+ data: o.base64,
421
+ mediaType: o.mediaType,
422
+ },
423
+ ];
424
+ },
425
+ };
426
+
427
+ const message: AssistantMessage = {
428
+ role: "assistant",
429
+ status: { type: "requires-action", reason: "tool-calls" },
430
+ parts: [
431
+ {
432
+ type: "tool-call",
433
+ toolCallId: "tc-1",
434
+ toolName: "readPdf",
435
+ args: {},
436
+ } as ToolCallPart,
437
+ ],
438
+ content: [],
439
+ metadata: {
440
+ unstable_state: {},
441
+ unstable_data: [],
442
+ unstable_annotations: [],
443
+ steps: [],
444
+ custom: {},
445
+ },
446
+ };
447
+
448
+ const updated = await unstable_runPendingTools(
449
+ message,
450
+ { readPdf: tool },
451
+ new AbortController().signal,
452
+ async () => {},
453
+ );
454
+
455
+ expect(updated.parts[0]).toMatchObject({
456
+ type: "tool-call",
457
+ state: "result",
458
+ result: { mediaType: "application/pdf", base64: "JVBERi0xLjQK" },
459
+ modelContent: [
460
+ { type: "text", text: "PDF contents:" },
461
+ {
462
+ type: "file",
463
+ data: "JVBERi0xLjQK",
464
+ mediaType: "application/pdf",
465
+ },
466
+ ],
467
+ });
468
+ });
469
+
470
+ it("does not call toModelOutput when the ToolResponse already carries modelContent", async () => {
471
+ let called = false;
472
+ const tool: Tool = {
473
+ parameters: { type: "object", properties: {} },
474
+ execute: async () =>
475
+ new ToolResponse({
476
+ result: { ok: true },
477
+ modelContent: [{ type: "text", text: "preset" }],
478
+ }),
479
+ toModelOutput: () => {
480
+ called = true;
481
+ return [{ type: "text", text: "should not run" }];
482
+ },
483
+ };
484
+
485
+ const message: AssistantMessage = {
486
+ role: "assistant",
487
+ status: { type: "requires-action", reason: "tool-calls" },
488
+ parts: [
489
+ {
490
+ type: "tool-call",
491
+ toolCallId: "tc-1",
492
+ toolName: "preset",
493
+ args: {},
494
+ } as ToolCallPart,
495
+ ],
496
+ content: [],
497
+ metadata: {
498
+ unstable_state: {},
499
+ unstable_data: [],
500
+ unstable_annotations: [],
501
+ steps: [],
502
+ custom: {},
503
+ },
504
+ };
505
+
506
+ const updated = await unstable_runPendingTools(
507
+ message,
508
+ { preset: tool },
509
+ new AbortController().signal,
510
+ async () => {},
511
+ );
512
+
513
+ expect(called).toBe(false);
514
+ expect(updated.parts[0]).toMatchObject({
515
+ type: "tool-call",
516
+ state: "result",
517
+ modelContent: [{ type: "text", text: "preset" }],
518
+ });
519
+ });
520
+
521
+ it("falls back to the successful execute result when toModelOutput itself throws", async () => {
522
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
523
+ const tool: Tool = {
524
+ parameters: { type: "object", properties: {} },
525
+ execute: async () => ({ ok: true }),
526
+ toModelOutput: () => {
527
+ throw new Error("projection failed");
528
+ },
529
+ };
530
+
531
+ const message: AssistantMessage = {
532
+ role: "assistant",
533
+ status: { type: "requires-action", reason: "tool-calls" },
534
+ parts: [
535
+ {
536
+ type: "tool-call",
537
+ toolCallId: "tc-1",
538
+ toolName: "flaky",
539
+ args: {},
540
+ } as ToolCallPart,
541
+ ],
542
+ content: [],
543
+ metadata: {
544
+ unstable_state: {},
545
+ unstable_data: [],
546
+ unstable_annotations: [],
547
+ steps: [],
548
+ custom: {},
549
+ },
550
+ };
551
+
552
+ const updated = await unstable_runPendingTools(
553
+ message,
554
+ { flaky: tool },
555
+ new AbortController().signal,
556
+ async () => {},
557
+ );
558
+
559
+ expect(updated.parts[0]).toMatchObject({
560
+ type: "tool-call",
561
+ state: "result",
562
+ result: { ok: true },
563
+ isError: false,
564
+ });
565
+ expect(updated.parts[0]).not.toHaveProperty("modelContent");
566
+ expect(warn).toHaveBeenCalledWith(
567
+ expect.stringContaining(`tool "flaky" toModelOutput threw`),
568
+ expect.any(Error),
569
+ );
570
+ warn.mockRestore();
571
+ });
572
+
573
+ it("forwards modelContent through the streaming path (toolResultStream + ToolExecutionStream)", async () => {
574
+ const tool: Tool = {
575
+ parameters: { type: "object", properties: {} },
576
+ execute: async () => ({
577
+ mediaType: "application/pdf",
578
+ base64: "JVBERi0xLjQK",
579
+ }),
580
+ toModelOutput: ({ output }) => {
581
+ const o = output as { mediaType: string; base64: string };
582
+ return [
583
+ { type: "text", text: "PDF contents:" },
584
+ { type: "file", data: o.base64, mediaType: o.mediaType },
585
+ ];
586
+ },
587
+ };
588
+
589
+ const inputChunks: AssistantStreamChunk[] = [
590
+ {
591
+ type: "part-start",
592
+ path: [],
593
+ part: {
594
+ type: "tool-call",
595
+ toolCallId: "tc-stream-1",
596
+ toolName: "readPdf",
597
+ },
598
+ },
599
+ { type: "text-delta", path: [0], textDelta: "{}" },
600
+ { type: "tool-call-args-text-finish", path: [0] },
601
+ { type: "part-finish", path: [0] },
602
+ ];
603
+
604
+ const inputStream = new ReadableStream<AssistantStreamChunk>({
605
+ start(controller) {
606
+ for (const chunk of inputChunks) controller.enqueue(chunk);
607
+ controller.close();
608
+ },
609
+ });
610
+
611
+ const outputChunks: AssistantStreamChunk[] = [];
612
+ await inputStream
613
+ .pipeThrough(
614
+ unstable_toolResultStream(
615
+ { readPdf: tool },
616
+ new AbortController().signal,
617
+ async () => {},
618
+ ),
619
+ )
620
+ .pipeTo(
621
+ new WritableStream<AssistantStreamChunk>({
622
+ write(chunk) {
623
+ outputChunks.push(chunk);
624
+ },
625
+ }),
626
+ );
627
+
628
+ const resultChunk = outputChunks.find((c) => c.type === "result") as
629
+ | (AssistantStreamChunk & { type: "result" })
630
+ | undefined;
631
+ expect(resultChunk).toBeDefined();
632
+ expect(resultChunk?.result).toEqual({
633
+ mediaType: "application/pdf",
634
+ base64: "JVBERi0xLjQK",
635
+ });
636
+ expect(resultChunk?.isError).toBe(false);
637
+ expect(resultChunk?.modelContent).toEqual([
638
+ { type: "text", text: "PDF contents:" },
639
+ {
640
+ type: "file",
641
+ data: "JVBERi0xLjQK",
642
+ mediaType: "application/pdf",
643
+ },
644
+ ]);
645
+ });
646
+
647
+ it("falls back to the plain result when toModelOutput throws in the streaming path", async () => {
648
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
649
+ const tool: Tool = {
650
+ parameters: { type: "object", properties: {} },
651
+ execute: async () => ({ ok: true }),
652
+ toModelOutput: () => {
653
+ throw new Error("projection failed");
654
+ },
655
+ };
656
+
657
+ const inputChunks: AssistantStreamChunk[] = [
658
+ {
659
+ type: "part-start",
660
+ path: [],
661
+ part: {
662
+ type: "tool-call",
663
+ toolCallId: "tc-stream-err",
664
+ toolName: "flaky",
665
+ },
666
+ },
667
+ { type: "text-delta", path: [0], textDelta: "{}" },
668
+ { type: "tool-call-args-text-finish", path: [0] },
669
+ { type: "part-finish", path: [0] },
670
+ ];
671
+
672
+ const inputStream = new ReadableStream<AssistantStreamChunk>({
673
+ start(controller) {
674
+ for (const chunk of inputChunks) controller.enqueue(chunk);
675
+ controller.close();
676
+ },
677
+ });
678
+
679
+ const outputChunks: AssistantStreamChunk[] = [];
680
+ await inputStream
681
+ .pipeThrough(
682
+ unstable_toolResultStream(
683
+ { flaky: tool },
684
+ new AbortController().signal,
685
+ async () => {},
686
+ ),
687
+ )
688
+ .pipeTo(
689
+ new WritableStream<AssistantStreamChunk>({
690
+ write(chunk) {
691
+ outputChunks.push(chunk);
692
+ },
693
+ }),
694
+ );
695
+
696
+ const resultChunk = outputChunks.find((c) => c.type === "result") as
697
+ | (AssistantStreamChunk & { type: "result" })
698
+ | undefined;
699
+ expect(resultChunk).toBeDefined();
700
+ expect(resultChunk?.result).toEqual({ ok: true });
701
+ expect(resultChunk?.isError).toBe(false);
702
+ expect(resultChunk?.modelContent).toBeUndefined();
703
+ expect(warn).toHaveBeenCalledWith(
704
+ expect.stringContaining(`tool "flaky" toModelOutput threw`),
705
+ expect.any(Error),
706
+ );
707
+ warn.mockRestore();
708
+ });
709
+
710
+ it("does not call toModelOutput when the tool errors", async () => {
711
+ let called = false;
712
+ const tool: Tool = {
713
+ parameters: { type: "object", properties: {} },
714
+ execute: async () => {
715
+ throw new Error("boom");
716
+ },
717
+ toModelOutput: () => {
718
+ called = true;
719
+ return [{ type: "text", text: "should not run" }];
720
+ },
721
+ };
722
+
723
+ const message: AssistantMessage = {
724
+ role: "assistant",
725
+ status: { type: "requires-action", reason: "tool-calls" },
726
+ parts: [
727
+ {
728
+ type: "tool-call",
729
+ toolCallId: "tc-1",
730
+ toolName: "broken",
731
+ args: {},
732
+ } as ToolCallPart,
733
+ ],
734
+ content: [],
735
+ metadata: {
736
+ unstable_state: {},
737
+ unstable_data: [],
738
+ unstable_annotations: [],
739
+ steps: [],
740
+ custom: {},
741
+ },
742
+ };
743
+
744
+ try {
745
+ await unstable_runPendingTools(
746
+ message,
747
+ { broken: tool },
748
+ new AbortController().signal,
749
+ async () => {},
750
+ );
751
+ } catch {
752
+ // execute throws; toModelOutput must not have been consulted
753
+ }
754
+
755
+ expect(called).toBe(false);
756
+ });
757
+ });
400
758
  });
@@ -87,7 +87,33 @@ function getToolResponse(
87
87
  abortSignal,
88
88
  human: (payload: unknown) => human(toolCall.toolCallId, payload),
89
89
  })) as unknown as ReadonlyJSONValue;
90
- return ToolResponse.toResponse(result);
90
+ const response = ToolResponse.toResponse(result);
91
+ if (
92
+ tool.toModelOutput &&
93
+ !response.isError &&
94
+ response.modelContent === undefined
95
+ ) {
96
+ try {
97
+ const modelContent = await tool.toModelOutput({
98
+ toolCallId: toolCall.toolCallId,
99
+ input: toolCall.args,
100
+ output: response.result,
101
+ });
102
+ return new ToolResponse({
103
+ result: response.result,
104
+ artifact: response.artifact,
105
+ isError: response.isError,
106
+ messages: response.messages,
107
+ modelContent,
108
+ });
109
+ } catch (e) {
110
+ console.warn(
111
+ `[assistant-stream] tool "${toolCall.toolName}" toModelOutput threw; falling back to default projection.`,
112
+ e,
113
+ );
114
+ }
115
+ }
116
+ return response;
91
117
  })();
92
118
 
93
119
  return Promise.race([executePromise, abortPromise]);
@@ -169,6 +195,9 @@ export async function unstable_runPendingTools(
169
195
  ...(toolResponse.artifact !== undefined
170
196
  ? { artifact: toolResponse.artifact }
171
197
  : {}),
198
+ ...(toolResponse.modelContent !== undefined
199
+ ? { modelContent: toolResponse.modelContent }
200
+ : {}),
172
201
  result: toolResponse.result as ReadonlyJSONValue,
173
202
  isError: toolResponse.isError,
174
203
  };
@@ -185,10 +214,25 @@ export async function unstable_runPendingTools(
185
214
  }
186
215
 
187
216
  export type ToolResultStreamOptions = {
217
+ /** Called immediately before a frontend tool's `execute` function runs. */
188
218
  onExecutionStart?: (toolCallId: string, toolName: string) => void;
219
+ /** Called after frontend tool execution finishes or fails. */
189
220
  onExecutionEnd?: (toolCallId: string, toolName: string) => void;
190
221
  };
191
222
 
223
+ /**
224
+ * Transform stream that executes frontend tools and appends tool results.
225
+ *
226
+ * The transform watches streamed tool-call arguments, runs the matching
227
+ * frontend tool once its arguments are complete, and emits a result chunk for
228
+ * the tool call. Backend and human tools pass through according to their tool
229
+ * definition.
230
+ *
231
+ * @param tools Tool registry or function returning the current registry.
232
+ * @param abortSignal Signal, or signal getter, used for the current run.
233
+ * @param human Callback used to resolve human-tool requests from UI input.
234
+ * @param options Optional execution lifecycle callbacks.
235
+ */
192
236
  export function toolResultStream(
193
237
  tools:
194
238
  | Record<string, Tool>
@@ -2,6 +2,7 @@ import type {
2
2
  ReadonlyJSONObject,
3
3
  ReadonlyJSONValue,
4
4
  } from "../../utils/json/json-value";
5
+ import type { ToolModelContentPart } from "../tool/tool-types";
5
6
 
6
7
  type TextStatus =
7
8
  | {
@@ -61,6 +62,7 @@ type ToolCallPartBase = {
61
62
  args: ReadonlyJSONObject;
62
63
  artifact?: ReadonlyJSONValue;
63
64
  result?: ReadonlyJSONValue;
65
+ modelContent?: readonly ToolModelContentPart[];
64
66
  isError?: boolean;
65
67
  parentId?: string;
66
68
  };
@@ -68,12 +70,14 @@ type ToolCallPartBase = {
68
70
  type ToolCallPartWithoutResult = ToolCallPartBase & {
69
71
  state: "partial-call" | "call";
70
72
  result?: undefined;
73
+ modelContent?: undefined;
71
74
  };
72
75
 
73
76
  type ToolCallPartWithResult = ToolCallPartBase & {
74
77
  state: "result";
75
78
  result: ReadonlyJSONValue;
76
79
  artifact?: ReadonlyJSONValue;
80
+ modelContent?: readonly ToolModelContentPart[];
77
81
  isError?: boolean;
78
82
  };
79
83
 
package/src/index.ts CHANGED
@@ -35,7 +35,11 @@ export type {
35
35
  DataPart,
36
36
  } from "./core/utils/types";
37
37
 
38
- export type { Tool } from "./core/tool/tool-types";
38
+ export type {
39
+ Tool,
40
+ ToolModelContentPart,
41
+ ToolModelOutputFunction,
42
+ } from "./core/tool/tool-types";
39
43
  export { ToolResponse, type ToolResponseLike } from "./core/tool/ToolResponse";
40
44
  export { ToolExecutionStream } from "./core/tool/ToolExecutionStream";
41
45
  export type { ToolCallReader } from "./core/tool/tool-types";