assistant-stream 0.2.39 → 0.2.40
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/dist/core/tool/toolResultStream.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.js +50 -32
- package/dist/core/tool/toolResultStream.js.map +1 -1
- package/dist/core/tool/toolResultStream.test.d.ts +2 -0
- package/dist/core/tool/toolResultStream.test.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/core/tool/toolResultStream.test.ts +400 -0
- package/src/core/tool/toolResultStream.ts +47 -23
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolResultStream.d.ts","sourceRoot":"","sources":["../../../src/core/tool/toolResultStream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAuC,MAAM,cAAc,CAAC;AAGzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA2ElD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,SAAS,EACvC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"toolResultStream.d.ts","sourceRoot":"","sources":["../../../src/core/tool/toolResultStream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAuC,MAAM,cAAc,CAAC;AAGzE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AA2ElD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,gBAAgB,EACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,SAAS,EACvC,WAAW,EAAE,WAAW,EACxB,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,6BAiElE;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EACD,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GACpB,CAAC,MAAM,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,SAAS,CAAC,GACxC,SAAS,EACb,WAAW,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,EAC9C,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,uBAWlE"}
|
|
@@ -37,41 +37,59 @@ function getToolStreamResponse(tools, abortSignal, reader, context, human) {
|
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
async function unstable_runPendingTools(message, tools, abortSignal, human) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
40
|
+
const toolCallPromises = message.parts.filter((part) => part.type === "tool-call").map(async (part) => {
|
|
41
|
+
const promiseOrUndefined = getToolResponse(
|
|
42
|
+
tools,
|
|
43
|
+
abortSignal,
|
|
44
|
+
part,
|
|
45
|
+
human ?? (async () => {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Tool human input is not supported in this context"
|
|
48
|
+
);
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
if (promiseOrUndefined) {
|
|
52
|
+
const result = await promiseOrUndefined;
|
|
53
|
+
return {
|
|
54
|
+
toolCallId: part.toolCallId,
|
|
55
|
+
result
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
});
|
|
60
|
+
const toolCallResults = (await Promise.all(toolCallPromises)).filter(
|
|
61
|
+
(result) => result !== null
|
|
62
|
+
);
|
|
63
|
+
if (toolCallResults.length === 0) {
|
|
64
|
+
return message;
|
|
65
|
+
}
|
|
66
|
+
const toolCallResultsById = toolCallResults.reduce(
|
|
67
|
+
(acc, { toolCallId, result }) => {
|
|
68
|
+
acc[toolCallId] = result;
|
|
69
|
+
return acc;
|
|
70
|
+
},
|
|
71
|
+
{}
|
|
72
|
+
);
|
|
73
|
+
const updatedParts = message.parts.map((p) => {
|
|
74
|
+
if (p.type === "tool-call") {
|
|
75
|
+
const toolResponse = toolCallResultsById[p.toolCallId];
|
|
76
|
+
if (toolResponse) {
|
|
77
|
+
return {
|
|
78
|
+
...p,
|
|
79
|
+
state: "result",
|
|
80
|
+
...toolResponse.artifact !== void 0 ? { artifact: toolResponse.artifact } : {},
|
|
81
|
+
result: toolResponse.result,
|
|
82
|
+
isError: toolResponse.isError
|
|
70
83
|
};
|
|
71
84
|
}
|
|
72
85
|
}
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
return p;
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
...message,
|
|
90
|
+
parts: updatedParts,
|
|
91
|
+
content: updatedParts
|
|
92
|
+
};
|
|
75
93
|
}
|
|
76
94
|
function toolResultStream(tools, abortSignal, human) {
|
|
77
95
|
const toolsFn = typeof tools === "function" ? tools : () => tools;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/tool/toolResultStream.ts"],"sourcesContent":["import { Tool, ToolCallReader, ToolExecuteFunction } from \"./tool-types\";\nimport { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { ToolResponse } from \"./ToolResponse\";\nimport { ToolExecutionStream } from \"./ToolExecutionStream\";\nimport { AssistantMessage } from \"../utils/types\";\nimport { ReadonlyJSONObject, ReadonlyJSONValue } from \"../../utils\";\n\nconst isStandardSchemaV1 = (\n schema: unknown,\n): schema is StandardSchemaV1<unknown> => {\n return (\n typeof schema === \"object\" &&\n schema !== null &&\n \"~standard\" in schema &&\n (schema as StandardSchemaV1<unknown>)[\"~standard\"].version === 1\n );\n};\n\nfunction getToolResponse(\n tools: Record<string, Tool> | undefined,\n abortSignal: AbortSignal,\n toolCall: {\n toolCallId: string;\n toolName: string;\n args: ReadonlyJSONObject;\n },\n human: (toolCallId: string, payload: unknown) => Promise<unknown>,\n) {\n const tool = tools?.[toolCall.toolName];\n if (!tool || !tool.execute) return undefined;\n\n const getResult = async (\n toolExecute: ToolExecuteFunction<ReadonlyJSONObject, unknown>,\n ): Promise<ToolResponse<ReadonlyJSONValue>> => {\n let executeFn = toolExecute;\n\n if (isStandardSchemaV1(tool.parameters)) {\n let result = tool.parameters[\"~standard\"].validate(toolCall.args);\n if (result instanceof Promise) result = await result;\n\n if (result.issues) {\n executeFn =\n tool.experimental_onSchemaValidationError ??\n (() => {\n throw new Error(\n `Function parameter validation failed. ${JSON.stringify(result.issues)}`,\n );\n });\n }\n }\n\n const result = (await executeFn(toolCall.args, {\n toolCallId: toolCall.toolCallId,\n abortSignal,\n human: (payload: unknown) => human(toolCall.toolCallId, payload),\n })) as unknown as ReadonlyJSONValue;\n return ToolResponse.toResponse(result);\n };\n\n return getResult(tool.execute);\n}\n\nfunction getToolStreamResponse(\n tools: Record<string, Tool> | undefined,\n abortSignal: AbortSignal,\n reader: ToolCallReader<any, ReadonlyJSONValue>,\n context: {\n toolCallId: string;\n toolName: string;\n },\n human: (toolCallId: string, payload: unknown) => Promise<unknown>,\n) {\n tools?.[context.toolName]?.streamCall?.(reader, {\n toolCallId: context.toolCallId,\n abortSignal,\n human: (payload: unknown) => human(context.toolCallId, payload),\n });\n}\n\nexport async function unstable_runPendingTools(\n message: AssistantMessage,\n tools: Record<string, Tool> | undefined,\n abortSignal: AbortSignal,\n human: (toolCallId: string, payload: unknown) => Promise<unknown>,\n) {\n
|
|
1
|
+
{"version":3,"sources":["../../../src/core/tool/toolResultStream.ts"],"sourcesContent":["import { Tool, ToolCallReader, ToolExecuteFunction } from \"./tool-types\";\nimport { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { ToolResponse } from \"./ToolResponse\";\nimport { ToolExecutionStream } from \"./ToolExecutionStream\";\nimport { AssistantMessage } from \"../utils/types\";\nimport { ReadonlyJSONObject, ReadonlyJSONValue } from \"../../utils\";\n\nconst isStandardSchemaV1 = (\n schema: unknown,\n): schema is StandardSchemaV1<unknown> => {\n return (\n typeof schema === \"object\" &&\n schema !== null &&\n \"~standard\" in schema &&\n (schema as StandardSchemaV1<unknown>)[\"~standard\"].version === 1\n );\n};\n\nfunction getToolResponse(\n tools: Record<string, Tool> | undefined,\n abortSignal: AbortSignal,\n toolCall: {\n toolCallId: string;\n toolName: string;\n args: ReadonlyJSONObject;\n },\n human: (toolCallId: string, payload: unknown) => Promise<unknown>,\n) {\n const tool = tools?.[toolCall.toolName];\n if (!tool || !tool.execute) return undefined;\n\n const getResult = async (\n toolExecute: ToolExecuteFunction<ReadonlyJSONObject, unknown>,\n ): Promise<ToolResponse<ReadonlyJSONValue>> => {\n let executeFn = toolExecute;\n\n if (isStandardSchemaV1(tool.parameters)) {\n let result = tool.parameters[\"~standard\"].validate(toolCall.args);\n if (result instanceof Promise) result = await result;\n\n if (result.issues) {\n executeFn =\n tool.experimental_onSchemaValidationError ??\n (() => {\n throw new Error(\n `Function parameter validation failed. ${JSON.stringify(result.issues)}`,\n );\n });\n }\n }\n\n const result = (await executeFn(toolCall.args, {\n toolCallId: toolCall.toolCallId,\n abortSignal,\n human: (payload: unknown) => human(toolCall.toolCallId, payload),\n })) as unknown as ReadonlyJSONValue;\n return ToolResponse.toResponse(result);\n };\n\n return getResult(tool.execute);\n}\n\nfunction getToolStreamResponse(\n tools: Record<string, Tool> | undefined,\n abortSignal: AbortSignal,\n reader: ToolCallReader<any, ReadonlyJSONValue>,\n context: {\n toolCallId: string;\n toolName: string;\n },\n human: (toolCallId: string, payload: unknown) => Promise<unknown>,\n) {\n tools?.[context.toolName]?.streamCall?.(reader, {\n toolCallId: context.toolCallId,\n abortSignal,\n human: (payload: unknown) => human(context.toolCallId, payload),\n });\n}\n\nexport async function unstable_runPendingTools(\n message: AssistantMessage,\n tools: Record<string, Tool> | undefined,\n abortSignal: AbortSignal,\n human: (toolCallId: string, payload: unknown) => Promise<unknown>,\n) {\n const toolCallPromises = message.parts\n .filter((part) => part.type === \"tool-call\")\n .map(async (part) => {\n const promiseOrUndefined = getToolResponse(\n tools,\n abortSignal,\n part,\n human ??\n (async () => {\n throw new Error(\n \"Tool human input is not supported in this context\",\n );\n }),\n );\n if (promiseOrUndefined) {\n const result = await promiseOrUndefined;\n return {\n toolCallId: part.toolCallId,\n result,\n };\n }\n return null;\n });\n\n const toolCallResults = (await Promise.all(toolCallPromises)).filter(\n (result) => result !== null,\n ) as { toolCallId: string; result: ToolResponse<ReadonlyJSONValue> }[];\n\n if (toolCallResults.length === 0) {\n return message;\n }\n\n const toolCallResultsById = toolCallResults.reduce(\n (acc, { toolCallId, result }) => {\n acc[toolCallId] = result;\n return acc;\n },\n {} as Record<string, ToolResponse<ReadonlyJSONValue>>,\n );\n\n const updatedParts = message.parts.map((p) => {\n if (p.type === \"tool-call\") {\n const toolResponse = toolCallResultsById[p.toolCallId];\n if (toolResponse) {\n return {\n ...p,\n state: \"result\" as const,\n ...(toolResponse.artifact !== undefined\n ? { artifact: toolResponse.artifact }\n : {}),\n result: toolResponse.result as ReadonlyJSONValue,\n isError: toolResponse.isError,\n };\n }\n }\n return p;\n });\n\n return {\n ...message,\n parts: updatedParts,\n content: updatedParts,\n };\n}\n\nexport function toolResultStream(\n tools:\n | Record<string, Tool>\n | (() => Record<string, Tool> | undefined)\n | undefined,\n abortSignal: AbortSignal | (() => AbortSignal),\n human: (toolCallId: string, payload: unknown) => Promise<unknown>,\n) {\n const toolsFn = typeof tools === \"function\" ? tools : () => tools;\n const abortSignalFn =\n typeof abortSignal === \"function\" ? abortSignal : () => abortSignal;\n return new ToolExecutionStream({\n execute: (toolCall) =>\n getToolResponse(toolsFn(), abortSignalFn(), toolCall, human),\n streamCall: ({ reader, ...context }) =>\n getToolStreamResponse(toolsFn(), abortSignalFn(), reader, context, human),\n });\n}\n"],"mappings":"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","names":["result"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolResultStream.test.d.ts","sourceRoot":"","sources":["../../../src/core/tool/toolResultStream.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistant-stream",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.40",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"sideEffects": false,
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@standard-schema/spec": "^1.0.0",
|
|
28
|
-
"@types/node": "^24.10.
|
|
28
|
+
"@types/node": "^24.10.1",
|
|
29
29
|
"tsx": "^4.20.6",
|
|
30
|
-
"vitest": "^4.0.
|
|
30
|
+
"vitest": "^4.0.8",
|
|
31
31
|
"@assistant-ui/x-buildutils": "0.0.1"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { unstable_runPendingTools } from "./toolResultStream";
|
|
3
|
+
import { AssistantMessage, ToolCallPart } from "../utils/types";
|
|
4
|
+
import { Tool } from "./tool-types";
|
|
5
|
+
|
|
6
|
+
const createDelayedTool = (delay: number, result?: string): Tool => ({
|
|
7
|
+
parameters: { type: "object", properties: {} },
|
|
8
|
+
execute: async () => {
|
|
9
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
10
|
+
return result ?? `Tool with ${delay}ms delay executed`;
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("unstable_runPendingTools", () => {
|
|
15
|
+
describe("parallel execution", () => {
|
|
16
|
+
it("should run tool calls in parallel", async () => {
|
|
17
|
+
const tool1 = createDelayedTool(100, "Tool 1");
|
|
18
|
+
const tool2 = createDelayedTool(100, "Tool 2");
|
|
19
|
+
const tool3 = createDelayedTool(100, "Tool 3");
|
|
20
|
+
|
|
21
|
+
const tools: Record<string, Tool> = {
|
|
22
|
+
tool1,
|
|
23
|
+
tool2,
|
|
24
|
+
tool3,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const message: AssistantMessage = {
|
|
28
|
+
role: "assistant",
|
|
29
|
+
status: {
|
|
30
|
+
type: "requires-action",
|
|
31
|
+
reason: "tool-calls",
|
|
32
|
+
},
|
|
33
|
+
parts: [
|
|
34
|
+
{
|
|
35
|
+
type: "tool-call",
|
|
36
|
+
toolCallId: "1",
|
|
37
|
+
toolName: "tool1",
|
|
38
|
+
args: {},
|
|
39
|
+
} as ToolCallPart,
|
|
40
|
+
{
|
|
41
|
+
type: "tool-call",
|
|
42
|
+
toolCallId: "2",
|
|
43
|
+
toolName: "tool2",
|
|
44
|
+
args: {},
|
|
45
|
+
} as ToolCallPart,
|
|
46
|
+
{
|
|
47
|
+
type: "tool-call",
|
|
48
|
+
toolCallId: "3",
|
|
49
|
+
toolName: "tool3",
|
|
50
|
+
args: {},
|
|
51
|
+
} as ToolCallPart,
|
|
52
|
+
],
|
|
53
|
+
content: [],
|
|
54
|
+
metadata: {
|
|
55
|
+
unstable_state: {},
|
|
56
|
+
unstable_data: [],
|
|
57
|
+
unstable_annotations: [],
|
|
58
|
+
steps: [],
|
|
59
|
+
custom: {},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
const updatedMessage = await unstable_runPendingTools(
|
|
65
|
+
message,
|
|
66
|
+
tools,
|
|
67
|
+
new AbortController().signal,
|
|
68
|
+
async () => {},
|
|
69
|
+
);
|
|
70
|
+
const endTime = Date.now();
|
|
71
|
+
|
|
72
|
+
const executionTime = endTime - startTime;
|
|
73
|
+
|
|
74
|
+
expect(executionTime).toBeGreaterThanOrEqual(100);
|
|
75
|
+
// The execution time should be less than the sum of the delays of both tools.
|
|
76
|
+
expect(executionTime).toBeLessThan(300);
|
|
77
|
+
|
|
78
|
+
expect(updatedMessage.parts).toHaveLength(3);
|
|
79
|
+
expect(updatedMessage.parts[0]).toMatchObject({
|
|
80
|
+
type: "tool-call",
|
|
81
|
+
toolCallId: "1",
|
|
82
|
+
state: "result",
|
|
83
|
+
result: "Tool 1",
|
|
84
|
+
isError: false,
|
|
85
|
+
});
|
|
86
|
+
expect(updatedMessage.parts[1]).toMatchObject({
|
|
87
|
+
type: "tool-call",
|
|
88
|
+
toolCallId: "2",
|
|
89
|
+
state: "result",
|
|
90
|
+
result: "Tool 2",
|
|
91
|
+
isError: false,
|
|
92
|
+
});
|
|
93
|
+
expect(updatedMessage.parts[2]).toMatchObject({
|
|
94
|
+
type: "tool-call",
|
|
95
|
+
toolCallId: "3",
|
|
96
|
+
state: "result",
|
|
97
|
+
result: "Tool 3",
|
|
98
|
+
isError: false,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should verify parallel execution via execution order", async () => {
|
|
103
|
+
let tool1Started = false;
|
|
104
|
+
let tool2Started = false;
|
|
105
|
+
let tool1Finished = false;
|
|
106
|
+
|
|
107
|
+
const tool1: Tool = {
|
|
108
|
+
parameters: {
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {},
|
|
111
|
+
},
|
|
112
|
+
execute: async () => {
|
|
113
|
+
tool1Started = true;
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
115
|
+
tool1Finished = true;
|
|
116
|
+
return "Tool 1 executed";
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const tool2: Tool = {
|
|
121
|
+
parameters: { type: "object", properties: {} },
|
|
122
|
+
execute: async () => {
|
|
123
|
+
tool2Started = true;
|
|
124
|
+
// In parallel execution, tool2 should start before tool1 finishes
|
|
125
|
+
expect(tool1Finished).toBe(false);
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
127
|
+
return "Tool 2 executed";
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const tools = { tool1, tool2 };
|
|
132
|
+
|
|
133
|
+
const message: AssistantMessage = {
|
|
134
|
+
role: "assistant",
|
|
135
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
136
|
+
parts: [
|
|
137
|
+
{
|
|
138
|
+
type: "tool-call",
|
|
139
|
+
toolCallId: "1",
|
|
140
|
+
toolName: "tool1",
|
|
141
|
+
args: {},
|
|
142
|
+
} as ToolCallPart,
|
|
143
|
+
{
|
|
144
|
+
type: "tool-call",
|
|
145
|
+
toolCallId: "2",
|
|
146
|
+
toolName: "tool2",
|
|
147
|
+
args: {},
|
|
148
|
+
} as ToolCallPart,
|
|
149
|
+
],
|
|
150
|
+
content: [],
|
|
151
|
+
metadata: {
|
|
152
|
+
unstable_state: {},
|
|
153
|
+
unstable_data: [],
|
|
154
|
+
unstable_annotations: [],
|
|
155
|
+
steps: [],
|
|
156
|
+
custom: {},
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
await unstable_runPendingTools(
|
|
161
|
+
message,
|
|
162
|
+
tools,
|
|
163
|
+
new AbortController().signal,
|
|
164
|
+
async () => {},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Verifying that both tools started (proving parallel execution)
|
|
168
|
+
expect(tool1Started).toBe(true);
|
|
169
|
+
expect(tool2Started).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("edge cases", () => {
|
|
174
|
+
it("should return original message when no tool calls exist", async () => {
|
|
175
|
+
const message: AssistantMessage = {
|
|
176
|
+
role: "assistant",
|
|
177
|
+
status: {
|
|
178
|
+
reason: "stop",
|
|
179
|
+
type: "complete",
|
|
180
|
+
},
|
|
181
|
+
parts: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: "Hello",
|
|
185
|
+
status: {
|
|
186
|
+
type: "complete",
|
|
187
|
+
reason: "stop",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
content: [],
|
|
192
|
+
metadata: {
|
|
193
|
+
unstable_state: {},
|
|
194
|
+
unstable_data: [],
|
|
195
|
+
unstable_annotations: [],
|
|
196
|
+
steps: [],
|
|
197
|
+
custom: {},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = await unstable_runPendingTools(
|
|
202
|
+
message,
|
|
203
|
+
{},
|
|
204
|
+
new AbortController().signal,
|
|
205
|
+
async () => {},
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(result).toEqual(message);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should handle missing tool gracefully", async () => {
|
|
212
|
+
const message: AssistantMessage = {
|
|
213
|
+
role: "assistant",
|
|
214
|
+
status: {
|
|
215
|
+
type: "requires-action",
|
|
216
|
+
reason: "tool-calls",
|
|
217
|
+
},
|
|
218
|
+
parts: [
|
|
219
|
+
{
|
|
220
|
+
type: "tool-call",
|
|
221
|
+
toolCallId: "1",
|
|
222
|
+
toolName: "nonexistentTool",
|
|
223
|
+
args: {},
|
|
224
|
+
status: { type: "requires-action", reason: "tool-call-result" },
|
|
225
|
+
} as ToolCallPart,
|
|
226
|
+
],
|
|
227
|
+
content: [],
|
|
228
|
+
metadata: {
|
|
229
|
+
unstable_state: {},
|
|
230
|
+
unstable_data: [],
|
|
231
|
+
unstable_annotations: [],
|
|
232
|
+
steps: [],
|
|
233
|
+
custom: {},
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const result = await unstable_runPendingTools(
|
|
238
|
+
message,
|
|
239
|
+
{},
|
|
240
|
+
new AbortController().signal,
|
|
241
|
+
async () => {},
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Tool call should remain unchanged (no result added)
|
|
245
|
+
expect(result.parts[0]).toMatchObject({
|
|
246
|
+
type: "tool-call",
|
|
247
|
+
toolCallId: "1",
|
|
248
|
+
toolName: "nonexistentTool",
|
|
249
|
+
});
|
|
250
|
+
expect(result.parts[0]).not.toHaveProperty("state");
|
|
251
|
+
expect(result.parts[0]).not.toHaveProperty("result");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should handle mixed text and tool-call parts", async () => {
|
|
255
|
+
const tool: Tool = {
|
|
256
|
+
parameters: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {},
|
|
259
|
+
},
|
|
260
|
+
execute: async () => "executed",
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const message: AssistantMessage = {
|
|
264
|
+
role: "assistant",
|
|
265
|
+
status: {
|
|
266
|
+
type: "requires-action",
|
|
267
|
+
reason: "tool-calls",
|
|
268
|
+
},
|
|
269
|
+
parts: [
|
|
270
|
+
{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: "Let me call a tool",
|
|
273
|
+
status: {
|
|
274
|
+
type: "complete",
|
|
275
|
+
reason: "stop",
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: "tool-call",
|
|
280
|
+
toolCallId: "1",
|
|
281
|
+
toolName: "tool",
|
|
282
|
+
args: {},
|
|
283
|
+
status: {
|
|
284
|
+
type: "requires-action",
|
|
285
|
+
reason: "tool-call-result",
|
|
286
|
+
},
|
|
287
|
+
} as ToolCallPart,
|
|
288
|
+
{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: "Done",
|
|
291
|
+
status: {
|
|
292
|
+
type: "complete",
|
|
293
|
+
reason: "stop",
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
content: [],
|
|
298
|
+
metadata: {
|
|
299
|
+
unstable_state: {},
|
|
300
|
+
unstable_data: [],
|
|
301
|
+
unstable_annotations: [],
|
|
302
|
+
steps: [],
|
|
303
|
+
custom: {},
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const result = await unstable_runPendingTools(
|
|
308
|
+
message,
|
|
309
|
+
{ tool },
|
|
310
|
+
new AbortController().signal,
|
|
311
|
+
async () => {},
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(result.parts).toHaveLength(3);
|
|
315
|
+
expect(result.parts[0]).toEqual({
|
|
316
|
+
type: "text",
|
|
317
|
+
text: "Let me call a tool",
|
|
318
|
+
status: { type: "complete", reason: "stop" },
|
|
319
|
+
});
|
|
320
|
+
expect(result.parts[1]).toMatchObject({
|
|
321
|
+
type: "tool-call",
|
|
322
|
+
state: "result",
|
|
323
|
+
result: "executed",
|
|
324
|
+
});
|
|
325
|
+
expect(result.parts[2]).toEqual({
|
|
326
|
+
type: "text",
|
|
327
|
+
text: "Done",
|
|
328
|
+
status: { type: "complete", reason: "stop" },
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("should handle tools with different execution times", async () => {
|
|
333
|
+
const fastTool = createDelayedTool(10, "fast");
|
|
334
|
+
const slowTool = createDelayedTool(100, "slow");
|
|
335
|
+
|
|
336
|
+
const tools = { fastTool, slowTool };
|
|
337
|
+
|
|
338
|
+
const message: AssistantMessage = {
|
|
339
|
+
role: "assistant",
|
|
340
|
+
status: {
|
|
341
|
+
type: "requires-action",
|
|
342
|
+
reason: "tool-calls",
|
|
343
|
+
},
|
|
344
|
+
parts: [
|
|
345
|
+
{
|
|
346
|
+
type: "tool-call",
|
|
347
|
+
toolCallId: "1",
|
|
348
|
+
toolName: "slowTool",
|
|
349
|
+
args: {},
|
|
350
|
+
status: {
|
|
351
|
+
type: "requires-action",
|
|
352
|
+
reason: "tool-call-result",
|
|
353
|
+
},
|
|
354
|
+
} as ToolCallPart,
|
|
355
|
+
{
|
|
356
|
+
type: "tool-call",
|
|
357
|
+
toolCallId: "2",
|
|
358
|
+
toolName: "fastTool",
|
|
359
|
+
args: {},
|
|
360
|
+
status: {
|
|
361
|
+
type: "requires-action",
|
|
362
|
+
reason: "tool-call-result",
|
|
363
|
+
},
|
|
364
|
+
} as ToolCallPart,
|
|
365
|
+
],
|
|
366
|
+
content: [],
|
|
367
|
+
metadata: {
|
|
368
|
+
unstable_state: {},
|
|
369
|
+
unstable_data: [],
|
|
370
|
+
unstable_annotations: [],
|
|
371
|
+
steps: [],
|
|
372
|
+
custom: {},
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const updatedMessage = await unstable_runPendingTools(
|
|
377
|
+
message,
|
|
378
|
+
tools,
|
|
379
|
+
new AbortController().signal,
|
|
380
|
+
async () => {},
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Both should complete successfully
|
|
384
|
+
expect(updatedMessage.parts[0]).toMatchObject({
|
|
385
|
+
type: "tool-call",
|
|
386
|
+
toolCallId: "1",
|
|
387
|
+
state: "result",
|
|
388
|
+
result: "slow",
|
|
389
|
+
isError: false,
|
|
390
|
+
});
|
|
391
|
+
expect(updatedMessage.parts[1]).toMatchObject({
|
|
392
|
+
type: "tool-call",
|
|
393
|
+
toolCallId: "2",
|
|
394
|
+
state: "result",
|
|
395
|
+
result: "fast",
|
|
396
|
+
isError: false,
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
@@ -83,9 +83,9 @@ export async function unstable_runPendingTools(
|
|
|
83
83
|
abortSignal: AbortSignal,
|
|
84
84
|
human: (toolCallId: string, payload: unknown) => Promise<unknown>,
|
|
85
85
|
) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
const toolCallPromises = message.parts
|
|
87
|
+
.filter((part) => part.type === "tool-call")
|
|
88
|
+
.map(async (part) => {
|
|
89
89
|
const promiseOrUndefined = getToolResponse(
|
|
90
90
|
tools,
|
|
91
91
|
abortSignal,
|
|
@@ -99,29 +99,53 @@ export async function unstable_runPendingTools(
|
|
|
99
99
|
);
|
|
100
100
|
if (promiseOrUndefined) {
|
|
101
101
|
const result = await promiseOrUndefined;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
...p,
|
|
106
|
-
state: "result" as const,
|
|
107
|
-
...(result.artifact !== undefined
|
|
108
|
-
? { artifact: result.artifact }
|
|
109
|
-
: {}),
|
|
110
|
-
result: result.result as ReadonlyJSONValue,
|
|
111
|
-
isError: result.isError,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return p;
|
|
115
|
-
});
|
|
116
|
-
message = {
|
|
117
|
-
...message,
|
|
118
|
-
parts: updatedParts,
|
|
119
|
-
content: updatedParts,
|
|
102
|
+
return {
|
|
103
|
+
toolCallId: part.toolCallId,
|
|
104
|
+
result,
|
|
120
105
|
};
|
|
121
106
|
}
|
|
122
|
-
|
|
107
|
+
return null;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const toolCallResults = (await Promise.all(toolCallPromises)).filter(
|
|
111
|
+
(result) => result !== null,
|
|
112
|
+
) as { toolCallId: string; result: ToolResponse<ReadonlyJSONValue> }[];
|
|
113
|
+
|
|
114
|
+
if (toolCallResults.length === 0) {
|
|
115
|
+
return message;
|
|
123
116
|
}
|
|
124
|
-
|
|
117
|
+
|
|
118
|
+
const toolCallResultsById = toolCallResults.reduce(
|
|
119
|
+
(acc, { toolCallId, result }) => {
|
|
120
|
+
acc[toolCallId] = result;
|
|
121
|
+
return acc;
|
|
122
|
+
},
|
|
123
|
+
{} as Record<string, ToolResponse<ReadonlyJSONValue>>,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const updatedParts = message.parts.map((p) => {
|
|
127
|
+
if (p.type === "tool-call") {
|
|
128
|
+
const toolResponse = toolCallResultsById[p.toolCallId];
|
|
129
|
+
if (toolResponse) {
|
|
130
|
+
return {
|
|
131
|
+
...p,
|
|
132
|
+
state: "result" as const,
|
|
133
|
+
...(toolResponse.artifact !== undefined
|
|
134
|
+
? { artifact: toolResponse.artifact }
|
|
135
|
+
: {}),
|
|
136
|
+
result: toolResponse.result as ReadonlyJSONValue,
|
|
137
|
+
isError: toolResponse.isError,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return p;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...message,
|
|
146
|
+
parts: updatedParts,
|
|
147
|
+
content: updatedParts,
|
|
148
|
+
};
|
|
125
149
|
}
|
|
126
150
|
|
|
127
151
|
export function toolResultStream(
|