|
5 | 5 | RunRawModelStreamEvent, |
6 | 6 | RunMessageOutputItem, |
7 | 7 | RunReasoningItem, |
| 8 | + RunToolApprovalItem, |
8 | 9 | RunToolCallItem, |
9 | 10 | RunToolCallOutputItem, |
10 | 11 | RunToolSearchCallItem, |
@@ -985,4 +986,161 @@ describe('createAiSdkUiMessageStreamResponse', () => { |
985 | 986 | expect(deltas).toHaveLength(1); |
986 | 987 | expect(deltas[0]).toMatchObject({ delta: 'Hello again' }); |
987 | 988 | }); |
| 989 | + |
| 990 | + test('falls back for invalid JSON arguments and maps non-function tool inputs', async () => { |
| 991 | + const agent = new Agent({ name: 'Test Agent' }); |
| 992 | + |
| 993 | + const invalidFunctionCall = new RunToolCallItem( |
| 994 | + { |
| 995 | + type: 'function_call', |
| 996 | + name: 'broken_json', |
| 997 | + arguments: '{not valid json', |
| 998 | + } as any, |
| 999 | + agent, |
| 1000 | + ); |
| 1001 | + const computerCall = new RunToolCallItem( |
| 1002 | + { |
| 1003 | + type: 'computer_call', |
| 1004 | + callId: 'computer-call-1', |
| 1005 | + action: { type: 'click', x: 1, y: 2, button: 'left' }, |
| 1006 | + } as any, |
| 1007 | + agent, |
| 1008 | + ); |
| 1009 | + const shellCall = new RunToolCallItem( |
| 1010 | + { |
| 1011 | + type: 'shell_call', |
| 1012 | + callId: 'shell-call-1', |
| 1013 | + action: { |
| 1014 | + command: 'pwd', |
| 1015 | + cwd: '/tmp', |
| 1016 | + }, |
| 1017 | + } as any, |
| 1018 | + agent, |
| 1019 | + ); |
| 1020 | + const applyPatchCall = new RunToolCallItem( |
| 1021 | + { |
| 1022 | + type: 'apply_patch_call', |
| 1023 | + callId: 'apply-patch-call-1', |
| 1024 | + operation: '*** Begin Patch\n*** End Patch\n', |
| 1025 | + } as any, |
| 1026 | + agent, |
| 1027 | + ); |
| 1028 | + |
| 1029 | + const events = (async function* () { |
| 1030 | + yield new RunItemStreamEvent('tool_called', invalidFunctionCall); |
| 1031 | + yield new RunItemStreamEvent('tool_called', computerCall); |
| 1032 | + yield new RunItemStreamEvent('tool_called', shellCall); |
| 1033 | + yield new RunItemStreamEvent('tool_called', applyPatchCall); |
| 1034 | + })(); |
| 1035 | + |
| 1036 | + const response = createAiSdkUiMessageStreamResponse(events); |
| 1037 | + const chunks = await readUiMessageChunks(response); |
| 1038 | + |
| 1039 | + expect( |
| 1040 | + chunks.filter((chunk) => chunk.type === 'tool-input-available'), |
| 1041 | + ).toMatchObject([ |
| 1042 | + { |
| 1043 | + toolCallId: expect.stringMatching(/^broken_json-call-/), |
| 1044 | + toolName: 'broken_json', |
| 1045 | + input: { raw: '{not valid json' }, |
| 1046 | + dynamic: true, |
| 1047 | + }, |
| 1048 | + { |
| 1049 | + toolCallId: 'computer-call-1', |
| 1050 | + toolName: 'computer_call', |
| 1051 | + input: { type: 'click', x: 1, y: 2, button: 'left' }, |
| 1052 | + dynamic: true, |
| 1053 | + }, |
| 1054 | + { |
| 1055 | + toolCallId: 'shell-call-1', |
| 1056 | + toolName: 'shell_call', |
| 1057 | + input: { |
| 1058 | + command: 'pwd', |
| 1059 | + cwd: '/tmp', |
| 1060 | + }, |
| 1061 | + dynamic: true, |
| 1062 | + }, |
| 1063 | + { |
| 1064 | + toolCallId: 'apply-patch-call-1', |
| 1065 | + toolName: 'apply_patch_call', |
| 1066 | + input: '*** Begin Patch\n*** End Patch\n', |
| 1067 | + dynamic: true, |
| 1068 | + }, |
| 1069 | + ]); |
| 1070 | + }); |
| 1071 | + |
| 1072 | + test('emits approval requests with generated fallback ids', async () => { |
| 1073 | + const agent = new Agent({ name: 'Test Agent' }); |
| 1074 | + |
| 1075 | + const approvalItem = new RunToolApprovalItem( |
| 1076 | + { |
| 1077 | + type: 'shell_call', |
| 1078 | + action: { |
| 1079 | + command: 'rm -rf /tmp/nope', |
| 1080 | + }, |
| 1081 | + } as any, |
| 1082 | + agent, |
| 1083 | + 'shell', |
| 1084 | + ); |
| 1085 | + |
| 1086 | + const events = (async function* () { |
| 1087 | + yield new RunItemStreamEvent('tool_approval_requested', approvalItem); |
| 1088 | + })(); |
| 1089 | + |
| 1090 | + const response = createAiSdkUiMessageStreamResponse(events); |
| 1091 | + const chunks = await readUiMessageChunks(response); |
| 1092 | + const approvalRequest = chunks.find( |
| 1093 | + (chunk) => chunk.type === 'tool-approval-request', |
| 1094 | + ); |
| 1095 | + |
| 1096 | + expect(approvalRequest).toMatchObject({ |
| 1097 | + type: 'tool-approval-request', |
| 1098 | + toolCallId: expect.stringMatching(/^shell-call-/), |
| 1099 | + approvalId: expect.stringMatching(/^shell-call-/), |
| 1100 | + }); |
| 1101 | + }); |
| 1102 | + |
| 1103 | + test('closes pending empty steps when response_done arrives before empty message output', async () => { |
| 1104 | + const agent = new Agent({ name: 'Test Agent' }); |
| 1105 | + |
| 1106 | + const emptyMessageOutput = new RunMessageOutputItem( |
| 1107 | + { |
| 1108 | + type: 'message', |
| 1109 | + role: 'assistant', |
| 1110 | + status: 'completed', |
| 1111 | + content: [], |
| 1112 | + }, |
| 1113 | + agent, |
| 1114 | + ); |
| 1115 | + |
| 1116 | + const events = (async function* () { |
| 1117 | + yield new RunRawModelStreamEvent({ type: 'response_started' }); |
| 1118 | + yield new RunRawModelStreamEvent({ |
| 1119 | + type: 'response_done', |
| 1120 | + response: { |
| 1121 | + id: 'resp-empty', |
| 1122 | + usage: { |
| 1123 | + inputTokens: 1, |
| 1124 | + outputTokens: 0, |
| 1125 | + totalTokens: 1, |
| 1126 | + }, |
| 1127 | + output: [], |
| 1128 | + }, |
| 1129 | + }); |
| 1130 | + yield new RunItemStreamEvent( |
| 1131 | + 'message_output_created', |
| 1132 | + emptyMessageOutput, |
| 1133 | + ); |
| 1134 | + })(); |
| 1135 | + |
| 1136 | + const response = createAiSdkUiMessageStreamResponse(events); |
| 1137 | + const chunks = await readUiMessageChunks(response); |
| 1138 | + |
| 1139 | + expect(chunks.map((chunk) => chunk.type)).toEqual([ |
| 1140 | + 'start', |
| 1141 | + 'start-step', |
| 1142 | + 'finish-step', |
| 1143 | + 'finish', |
| 1144 | + ]); |
| 1145 | + }); |
988 | 1146 | }); |
0 commit comments