ai 6.0.42 → 6.0.44

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.
@@ -0,0 +1,76 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`tool validation > should pass validation for provider-executed tools (deferred results) 1`] = `
4
+ [
5
+ {
6
+ "content": [
7
+ {
8
+ "input": {
9
+ "code": "print("hello")",
10
+ },
11
+ "providerExecuted": true,
12
+ "providerOptions": undefined,
13
+ "toolCallId": "call_1",
14
+ "toolName": "code_interpreter",
15
+ "type": "tool-call",
16
+ },
17
+ ],
18
+ "providerOptions": undefined,
19
+ "role": "assistant",
20
+ },
21
+ ]
22
+ `;
23
+
24
+ exports[`tool validation > should pass validation for tool-approval-response 1`] = `
25
+ [
26
+ {
27
+ "content": [
28
+ {
29
+ "input": {
30
+ "action": "delete_db",
31
+ },
32
+ "providerExecuted": undefined,
33
+ "providerOptions": undefined,
34
+ "toolCallId": "call_to_approve",
35
+ "toolName": "dangerous_action",
36
+ "type": "tool-call",
37
+ },
38
+ ],
39
+ "providerOptions": undefined,
40
+ "role": "assistant",
41
+ },
42
+ ]
43
+ `;
44
+
45
+ exports[`tool validation > should preserve provider-executed tool-approval-response 1`] = `
46
+ [
47
+ {
48
+ "content": [
49
+ {
50
+ "input": {
51
+ "action": "execute",
52
+ },
53
+ "providerExecuted": true,
54
+ "providerOptions": undefined,
55
+ "toolCallId": "call_provider_executed",
56
+ "toolName": "mcp_tool",
57
+ "type": "tool-call",
58
+ },
59
+ ],
60
+ "providerOptions": undefined,
61
+ "role": "assistant",
62
+ },
63
+ {
64
+ "content": [
65
+ {
66
+ "approvalId": "approval_provider",
67
+ "approved": true,
68
+ "reason": undefined,
69
+ "type": "tool-approval-response",
70
+ },
71
+ ],
72
+ "providerOptions": undefined,
73
+ "role": "tool",
74
+ },
75
+ ]
76
+ `;
@@ -29,6 +29,7 @@ import { convertToLanguageModelV3DataContent } from './data-content';
29
29
  import { InvalidMessageRoleError } from './invalid-message-role-error';
30
30
  import { StandardizedPrompt } from './standardize-prompt';
31
31
  import { asArray } from '../util/as-array';
32
+ import { MissingToolResultsError } from '../error/missing-tool-result-error';
32
33
 
33
34
  export async function convertToLanguageModelPrompt({
34
35
  prompt,
@@ -45,6 +46,38 @@ export async function convertToLanguageModelPrompt({
45
46
  supportedUrls,
46
47
  );
47
48
 
49
+ const approvalIdToToolCallId = new Map<string, string>();
50
+ for (const message of prompt.messages) {
51
+ if (message.role === 'assistant' && Array.isArray(message.content)) {
52
+ for (const part of message.content) {
53
+ if (
54
+ part.type === 'tool-approval-request' &&
55
+ 'approvalId' in part &&
56
+ 'toolCallId' in part
57
+ ) {
58
+ approvalIdToToolCallId.set(
59
+ part.approvalId as string,
60
+ part.toolCallId as string,
61
+ );
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ const approvedToolCallIds = new Set<string>();
68
+ for (const message of prompt.messages) {
69
+ if (message.role === 'tool') {
70
+ for (const part of message.content) {
71
+ if (part.type === 'tool-approval-response') {
72
+ const toolCallId = approvalIdToToolCallId.get(part.approvalId);
73
+ if (toolCallId) {
74
+ approvedToolCallIds.add(toolCallId);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+
48
81
  const messages = [
49
82
  ...(prompt.system != null
50
83
  ? typeof prompt.system === 'string'
@@ -76,7 +109,58 @@ export async function convertToLanguageModelPrompt({
76
109
  }
77
110
  }
78
111
 
79
- return combinedMessages;
112
+ const toolCallIds = new Set<string>();
113
+
114
+ for (const message of combinedMessages) {
115
+ switch (message.role) {
116
+ case 'assistant': {
117
+ for (const content of message.content) {
118
+ if (content.type === 'tool-call' && !content.providerExecuted) {
119
+ toolCallIds.add(content.toolCallId);
120
+ }
121
+ }
122
+ break;
123
+ }
124
+ case 'tool': {
125
+ for (const content of message.content) {
126
+ if (content.type === 'tool-result') {
127
+ toolCallIds.delete(content.toolCallId);
128
+ }
129
+ }
130
+ break;
131
+ }
132
+ case 'user':
133
+ case 'system':
134
+ // remove approved tool calls from the set before checking:
135
+ for (const id of approvedToolCallIds) {
136
+ toolCallIds.delete(id);
137
+ }
138
+
139
+ if (toolCallIds.size > 0) {
140
+ throw new MissingToolResultsError({
141
+ toolCallIds: Array.from(toolCallIds),
142
+ });
143
+ }
144
+ break;
145
+ }
146
+ }
147
+
148
+ // remove approved tool calls from the set before checking:
149
+ for (const id of approvedToolCallIds) {
150
+ toolCallIds.delete(id);
151
+ }
152
+
153
+ if (toolCallIds.size > 0) {
154
+ throw new MissingToolResultsError({ toolCallIds: Array.from(toolCallIds) });
155
+ }
156
+
157
+ return combinedMessages.filter(
158
+ // Filter out empty tool messages (e.g. if they only contained
159
+ // tool-approval-response parts that were removed).
160
+ // This prevents sending invalid empty messages to the provider.
161
+ // Note: provider-executed tool-approval-response parts are preserved.
162
+ message => message.role !== 'tool' || message.content.length > 0,
163
+ );
80
164
  }
81
165
 
82
166
  /**
@@ -0,0 +1,138 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { convertToLanguageModelPrompt } from './convert-to-language-model-prompt';
3
+ import { MissingToolResultsError } from '../error/missing-tool-result-error';
4
+
5
+ describe('tool validation', () => {
6
+ it('should pass validation for provider-executed tools (deferred results)', async () => {
7
+ const result = await convertToLanguageModelPrompt({
8
+ prompt: {
9
+ messages: [
10
+ {
11
+ role: 'assistant',
12
+ content: [
13
+ {
14
+ type: 'tool-call',
15
+ toolCallId: 'call_1',
16
+ toolName: 'code_interpreter',
17
+ input: { code: 'print("hello")' },
18
+ providerExecuted: true,
19
+ },
20
+ ],
21
+ },
22
+ ],
23
+ },
24
+ supportedUrls: {},
25
+ download: undefined,
26
+ });
27
+
28
+ expect(result).toMatchSnapshot();
29
+ });
30
+
31
+ it('should pass validation for tool-approval-response', async () => {
32
+ const result = await convertToLanguageModelPrompt({
33
+ prompt: {
34
+ messages: [
35
+ {
36
+ role: 'assistant',
37
+ content: [
38
+ {
39
+ type: 'tool-call',
40
+ toolCallId: 'call_to_approve',
41
+ toolName: 'dangerous_action',
42
+ input: { action: 'delete_db' },
43
+ },
44
+ {
45
+ type: 'tool-approval-request',
46
+ toolCallId: 'call_to_approve',
47
+ approvalId: 'approval_123',
48
+ toolName: 'dangerous_action',
49
+ input: { action: 'delete_db' },
50
+ } as any,
51
+ ],
52
+ },
53
+ {
54
+ role: 'tool',
55
+ content: [
56
+ {
57
+ type: 'tool-approval-response',
58
+ approvalId: 'approval_123',
59
+ approved: true,
60
+ } as any,
61
+ ],
62
+ },
63
+ ],
64
+ },
65
+ supportedUrls: {},
66
+ download: undefined,
67
+ });
68
+
69
+ expect(result).toMatchSnapshot();
70
+ });
71
+
72
+ it('should preserve provider-executed tool-approval-response', async () => {
73
+ const result = await convertToLanguageModelPrompt({
74
+ prompt: {
75
+ messages: [
76
+ {
77
+ role: 'assistant',
78
+ content: [
79
+ {
80
+ type: 'tool-call',
81
+ toolCallId: 'call_provider_executed',
82
+ toolName: 'mcp_tool',
83
+ input: { action: 'execute' },
84
+ providerExecuted: true,
85
+ },
86
+ {
87
+ type: 'tool-approval-request',
88
+ toolCallId: 'call_provider_executed',
89
+ approvalId: 'approval_provider',
90
+ toolName: 'mcp_tool',
91
+ input: { action: 'execute' },
92
+ } as any,
93
+ ],
94
+ },
95
+ {
96
+ role: 'tool',
97
+ content: [
98
+ {
99
+ type: 'tool-approval-response',
100
+ approvalId: 'approval_provider',
101
+ approved: true,
102
+ providerExecuted: true,
103
+ },
104
+ ],
105
+ },
106
+ ],
107
+ },
108
+ supportedUrls: {},
109
+ download: undefined,
110
+ });
111
+
112
+ expect(result).toMatchSnapshot();
113
+ });
114
+
115
+ it('should throw error for actual missing results', async () => {
116
+ await expect(async () => {
117
+ await convertToLanguageModelPrompt({
118
+ prompt: {
119
+ messages: [
120
+ {
121
+ role: 'assistant',
122
+ content: [
123
+ {
124
+ type: 'tool-call',
125
+ toolCallId: 'call_missing_result',
126
+ toolName: 'regular_tool',
127
+ input: {},
128
+ },
129
+ ],
130
+ },
131
+ ],
132
+ },
133
+ supportedUrls: {},
134
+ download: undefined,
135
+ });
136
+ }).rejects.toThrow(MissingToolResultsError);
137
+ });
138
+ });