assistant-stream 0.3.12 → 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.
- package/README.md +39 -0
- package/dist/core/AssistantStreamChunk.d.ts +2 -0
- package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.js +3 -0
- package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
- package/dist/core/modules/tool-call.d.ts.map +1 -1
- package/dist/core/modules/tool-call.js +3 -0
- package/dist/core/modules/tool-call.js.map +1 -1
- package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
- package/dist/core/tool/ToolExecutionStream.js +3 -0
- package/dist/core/tool/ToolExecutionStream.js.map +1 -1
- package/dist/core/tool/ToolResponse.d.ts +3 -0
- package/dist/core/tool/ToolResponse.d.ts.map +1 -1
- package/dist/core/tool/ToolResponse.js +4 -0
- package/dist/core/tool/ToolResponse.js.map +1 -1
- package/dist/core/tool/tool-types.d.ts +17 -0
- package/dist/core/tool/tool-types.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.js +26 -1
- package/dist/core/tool/toolResultStream.js.map +1 -1
- package/dist/core/utils/types.d.ts +4 -0
- package/dist/core/utils/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resumable/ResumableStreamContext.d.ts +27 -0
- package/dist/resumable/ResumableStreamContext.d.ts.map +1 -0
- package/dist/resumable/ResumableStreamContext.js +121 -0
- package/dist/resumable/ResumableStreamContext.js.map +1 -0
- package/dist/resumable/constants.d.ts +2 -0
- package/dist/resumable/constants.d.ts.map +1 -0
- package/dist/resumable/constants.js +2 -0
- package/dist/resumable/constants.js.map +1 -0
- package/dist/resumable/createResumableAssistantStreamResponse.d.ts +24 -0
- package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -0
- package/dist/resumable/createResumableAssistantStreamResponse.js +40 -0
- package/dist/resumable/createResumableAssistantStreamResponse.js.map +1 -0
- package/dist/resumable/errors.d.ts +7 -0
- package/dist/resumable/errors.d.ts.map +1 -0
- package/dist/resumable/errors.js +15 -0
- package/dist/resumable/errors.js.map +1 -0
- package/dist/resumable/index.d.ts +7 -0
- package/dist/resumable/index.d.ts.map +1 -0
- package/dist/resumable/index.js +5 -0
- package/dist/resumable/index.js.map +1 -0
- package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +13 -0
- package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -0
- package/dist/resumable/stores/InMemoryResumableStreamStore.js +199 -0
- package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -0
- package/dist/resumable/stores/ioredis.d.ts +10 -0
- package/dist/resumable/stores/ioredis.d.ts.map +1 -0
- package/dist/resumable/stores/ioredis.js +95 -0
- package/dist/resumable/stores/ioredis.js.map +1 -0
- package/dist/resumable/stores/redis-impl.d.ts +60 -0
- package/dist/resumable/stores/redis-impl.d.ts.map +1 -0
- package/dist/resumable/stores/redis-impl.js +198 -0
- package/dist/resumable/stores/redis-impl.js.map +1 -0
- package/dist/resumable/stores/redis.d.ts +39 -0
- package/dist/resumable/stores/redis.d.ts.map +1 -0
- package/dist/resumable/stores/redis.js +113 -0
- package/dist/resumable/stores/redis.js.map +1 -0
- package/dist/resumable/types.d.ts +30 -0
- package/dist/resumable/types.d.ts.map +1 -0
- package/dist/resumable/types.js +2 -0
- package/dist/resumable/types.js.map +1 -0
- package/package.json +30 -5
- package/src/core/AssistantStreamChunk.ts +2 -0
- package/src/core/accumulators/assistant-message-accumulator.ts +3 -0
- package/src/core/modules/tool-call.ts +3 -0
- package/src/core/tool/ToolExecutionStream.ts +3 -0
- package/src/core/tool/ToolResponse.ts +6 -0
- package/src/core/tool/tool-types.ts +23 -0
- package/src/core/tool/toolResultStream.test.ts +360 -2
- package/src/core/tool/toolResultStream.ts +30 -1
- package/src/core/utils/types.ts +4 -0
- package/src/index.ts +5 -1
- package/src/resumable/ResumableStreamContext.test.ts +274 -0
- package/src/resumable/ResumableStreamContext.ts +187 -0
- package/src/resumable/__tests__/integration.test.ts +159 -0
- package/src/resumable/constants.ts +1 -0
- package/src/resumable/createResumableAssistantStreamResponse.test.ts +243 -0
- package/src/resumable/createResumableAssistantStreamResponse.ts +80 -0
- package/src/resumable/errors.ts +26 -0
- package/src/resumable/index.ts +36 -0
- package/src/resumable/stores/InMemoryResumableStreamStore.test.ts +285 -0
- package/src/resumable/stores/InMemoryResumableStreamStore.ts +237 -0
- package/src/resumable/stores/ioredis.ts +123 -0
- package/src/resumable/stores/redis-impl.ts +304 -0
- package/src/resumable/stores/redis.test.ts +265 -0
- package/src/resumable/stores/redis.ts +171 -0
- 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 {
|
|
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
|
-
|
|
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
|
};
|
package/src/core/utils/types.ts
CHANGED
|
@@ -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 {
|
|
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";
|