assistant-stream 0.3.13 → 0.3.14

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 (92) hide show
  1. package/README.md +39 -0
  2. package/dist/core/AssistantStreamChunk.d.ts +2 -0
  3. package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
  4. package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
  5. package/dist/core/accumulators/assistant-message-accumulator.js +3 -0
  6. package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
  7. package/dist/core/modules/tool-call.d.ts.map +1 -1
  8. package/dist/core/modules/tool-call.js +3 -0
  9. package/dist/core/modules/tool-call.js.map +1 -1
  10. package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
  11. package/dist/core/tool/ToolExecutionStream.js +3 -0
  12. package/dist/core/tool/ToolExecutionStream.js.map +1 -1
  13. package/dist/core/tool/ToolResponse.d.ts +3 -0
  14. package/dist/core/tool/ToolResponse.d.ts.map +1 -1
  15. package/dist/core/tool/ToolResponse.js +4 -0
  16. package/dist/core/tool/ToolResponse.js.map +1 -1
  17. package/dist/core/tool/tool-types.d.ts +17 -0
  18. package/dist/core/tool/tool-types.d.ts.map +1 -1
  19. package/dist/core/tool/toolResultStream.d.ts.map +1 -1
  20. package/dist/core/tool/toolResultStream.js +26 -1
  21. package/dist/core/tool/toolResultStream.js.map +1 -1
  22. package/dist/core/utils/types.d.ts +4 -0
  23. package/dist/core/utils/types.d.ts.map +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/resumable/ResumableStreamContext.d.ts +27 -0
  28. package/dist/resumable/ResumableStreamContext.d.ts.map +1 -0
  29. package/dist/resumable/ResumableStreamContext.js +121 -0
  30. package/dist/resumable/ResumableStreamContext.js.map +1 -0
  31. package/dist/resumable/constants.d.ts +2 -0
  32. package/dist/resumable/constants.d.ts.map +1 -0
  33. package/dist/resumable/constants.js +2 -0
  34. package/dist/resumable/constants.js.map +1 -0
  35. package/dist/resumable/createResumableAssistantStreamResponse.d.ts +24 -0
  36. package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -0
  37. package/dist/resumable/createResumableAssistantStreamResponse.js +40 -0
  38. package/dist/resumable/createResumableAssistantStreamResponse.js.map +1 -0
  39. package/dist/resumable/errors.d.ts +7 -0
  40. package/dist/resumable/errors.d.ts.map +1 -0
  41. package/dist/resumable/errors.js +15 -0
  42. package/dist/resumable/errors.js.map +1 -0
  43. package/dist/resumable/index.d.ts +7 -0
  44. package/dist/resumable/index.d.ts.map +1 -0
  45. package/dist/resumable/index.js +5 -0
  46. package/dist/resumable/index.js.map +1 -0
  47. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +13 -0
  48. package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -0
  49. package/dist/resumable/stores/InMemoryResumableStreamStore.js +199 -0
  50. package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -0
  51. package/dist/resumable/stores/ioredis.d.ts +10 -0
  52. package/dist/resumable/stores/ioredis.d.ts.map +1 -0
  53. package/dist/resumable/stores/ioredis.js +95 -0
  54. package/dist/resumable/stores/ioredis.js.map +1 -0
  55. package/dist/resumable/stores/redis-impl.d.ts +60 -0
  56. package/dist/resumable/stores/redis-impl.d.ts.map +1 -0
  57. package/dist/resumable/stores/redis-impl.js +198 -0
  58. package/dist/resumable/stores/redis-impl.js.map +1 -0
  59. package/dist/resumable/stores/redis.d.ts +39 -0
  60. package/dist/resumable/stores/redis.d.ts.map +1 -0
  61. package/dist/resumable/stores/redis.js +113 -0
  62. package/dist/resumable/stores/redis.js.map +1 -0
  63. package/dist/resumable/types.d.ts +30 -0
  64. package/dist/resumable/types.d.ts.map +1 -0
  65. package/dist/resumable/types.js +2 -0
  66. package/dist/resumable/types.js.map +1 -0
  67. package/package.json +28 -3
  68. package/src/core/AssistantStreamChunk.ts +2 -0
  69. package/src/core/accumulators/assistant-message-accumulator.ts +3 -0
  70. package/src/core/modules/tool-call.ts +3 -0
  71. package/src/core/tool/ToolExecutionStream.ts +3 -0
  72. package/src/core/tool/ToolResponse.ts +6 -0
  73. package/src/core/tool/tool-types.ts +23 -0
  74. package/src/core/tool/toolResultStream.test.ts +360 -2
  75. package/src/core/tool/toolResultStream.ts +30 -1
  76. package/src/core/utils/types.ts +4 -0
  77. package/src/index.ts +5 -1
  78. package/src/resumable/ResumableStreamContext.test.ts +274 -0
  79. package/src/resumable/ResumableStreamContext.ts +187 -0
  80. package/src/resumable/__tests__/integration.test.ts +159 -0
  81. package/src/resumable/constants.ts +1 -0
  82. package/src/resumable/createResumableAssistantStreamResponse.test.ts +243 -0
  83. package/src/resumable/createResumableAssistantStreamResponse.ts +80 -0
  84. package/src/resumable/errors.ts +26 -0
  85. package/src/resumable/index.ts +36 -0
  86. package/src/resumable/stores/InMemoryResumableStreamStore.test.ts +285 -0
  87. package/src/resumable/stores/InMemoryResumableStreamStore.ts +237 -0
  88. package/src/resumable/stores/ioredis.ts +123 -0
  89. package/src/resumable/stores/redis-impl.ts +304 -0
  90. package/src/resumable/stores/redis.test.ts +265 -0
  91. package/src/resumable/stores/redis.ts +171 -0
  92. package/src/resumable/types.ts +49 -0
@@ -4,6 +4,26 @@ import type { AsyncIterableStream } from "../../utils";
4
4
  import type { StandardSchemaV1 } from "@standard-schema/spec";
5
5
  import type { ToolResponse } from "./ToolResponse";
6
6
 
7
+ export type ToolModelContentPart =
8
+ | {
9
+ readonly type: "text";
10
+ readonly text: string;
11
+ }
12
+ | {
13
+ readonly type: "file";
14
+ readonly data: string;
15
+ readonly mediaType: string;
16
+ readonly filename?: string;
17
+ };
18
+
19
+ export type ToolModelOutputFunction<TArgs, TResult> = (options: {
20
+ toolCallId: string;
21
+ input: TArgs;
22
+ output: TResult;
23
+ }) =>
24
+ | readonly ToolModelContentPart[]
25
+ | Promise<readonly ToolModelContentPart[]>;
26
+
7
27
  /**
8
28
  * Interface for reading tool call arguments from a stream, which are
9
29
  * generated by a language learning model (LLM). Provides methods to
@@ -121,6 +141,7 @@ type BackendTool<
121
141
  parameters?: undefined;
122
142
  disabled?: undefined;
123
143
  execute?: undefined;
144
+ toModelOutput?: undefined;
124
145
  experimental_onSchemaValidationError?: undefined;
125
146
  };
126
147
 
@@ -134,6 +155,7 @@ type FrontendTool<
134
155
  parameters: StandardSchemaV1<TArgs> | JSONSchema7;
135
156
  disabled?: boolean;
136
157
  execute: ToolExecuteFunction<TArgs, TResult>;
158
+ toModelOutput?: ToolModelOutputFunction<TArgs, TResult>;
137
159
  experimental_onSchemaValidationError?: OnSchemaValidationErrorFunction<TResult>;
138
160
  };
139
161
 
@@ -147,6 +169,7 @@ type HumanTool<
147
169
  parameters: StandardSchemaV1<TArgs> | JSONSchema7;
148
170
  disabled?: boolean;
149
171
  execute?: undefined;
172
+ toModelOutput?: undefined;
150
173
  experimental_onSchemaValidationError?: undefined;
151
174
  };
152
175
 
@@ -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
  };
@@ -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";