diff --git a/.changeset/fix-tool-approval-signature.md b/.changeset/fix-tool-approval-signature.md new file mode 100644 index 000000000000..77b7679e9afc --- /dev/null +++ b/.changeset/fix-tool-approval-signature.md @@ -0,0 +1,5 @@ +---\ +'ai': patch\ +---\ +\ +fix(ai): preserve tool approval signature when transitioning to approval-responded diff --git a/packages/ai/src/ui/chat.test.ts b/packages/ai/src/ui/chat.test.ts index 5eaebe10d904..a35f61cded7e 100644 --- a/packages/ai/src/ui/chat.test.ts +++ b/packages/ai/src/ui/chat.test.ts @@ -2597,6 +2597,56 @@ describe('Chat', () => { }); describe('addToolApprovalResponse', () => { + describe('approved with signature', () => { + let chat: TestChat; + + beforeEach(async () => { + chat = new TestChat({ + id: '123', + generateId: mockId({ prefix: 'newid' }), + transport: new DefaultChatTransport({ + api: 'http://localhost:3000/api/chat', + }), + messages: [ + { + id: 'id-0', + role: 'user', + parts: [{ text: 'What is the weather in Tokyo?', type: 'text' }], + }, + { + id: 'id-1', + role: 'assistant', + parts: [ + { type: 'step-start' }, + { + type: 'tool-weather', + toolCallId: 'call-1', + state: 'approval-requested', + input: { city: 'Tokyo' }, + approval: { id: 'approval-1', signature: 'test-signature' }, + }, + ], + }, + ], + }); + + await chat.addToolApprovalResponse({ + id: 'approval-1', + approved: true, + }); + }); + + it('should preserve the signature in the approval response', () => { + expect(chat.messages[1].parts[1]).toMatchObject({ + approval: { + id: 'approval-1', + approved: true, + signature: 'test-signature', + }, + }); + }); + }); + describe('approved', () => { let chat: TestChat; diff --git a/packages/ai/src/ui/chat.ts b/packages/ai/src/ui/chat.ts index 74e77229d5ac..e6f621822a2d 100644 --- a/packages/ai/src/ui/chat.ts +++ b/packages/ai/src/ui/chat.ts @@ -493,7 +493,14 @@ export abstract class AbstractChat { ? { ...part, state: 'approval-responded', - approval: { id, approved, reason }, + approval: { + id, + approved, + reason, + ...(part.approval.signature != null + ? { signature: part.approval.signature } + : {}), + }, } : part; diff --git a/packages/ai/src/ui/process-ui-message-stream.test.ts b/packages/ai/src/ui/process-ui-message-stream.test.ts index e9712ebdedcc..2eecc658ac29 100644 --- a/packages/ai/src/ui/process-ui-message-stream.test.ts +++ b/packages/ai/src/ui/process-ui-message-stream.test.ts @@ -7757,6 +7757,75 @@ describe('processUIMessageStream', () => { }); }); + describe('automatic tool approval signature preservation', () => { + beforeEach(async () => { + const stream = createUIMessageStream([ + { type: 'start' }, + { type: 'start-step' }, + { + input: { + value: 'value', + }, + toolCallId: 'call-1', + toolName: 'tool1', + type: 'tool-input-available', + }, + { + approvalId: 'id-1', + isAutomatic: true, + signature: 'test-signature', + toolCallId: 'call-1', + type: 'tool-approval-request', + }, + { + approvalId: 'id-1', + approved: true, + type: 'tool-approval-response', + }, + { type: 'finish-step' }, + { type: 'finish' }, + ]); + + state = createStreamingUIMessageState({ + messageId: 'msg-123', + lastMessage: undefined, + }); + + await consumeStream({ + stream: processUIMessageStream({ + stream, + runUpdateMessageJob, + onError: error => { + throw error; + }, + }), + }); + }); + + it('should keep signature through approval response', () => { + expect(writeCalls.map(call => call.message.parts[1])).toMatchObject([ + {}, // tool-input-available + { + approval: { + id: 'id-1', + isAutomatic: true, + signature: 'test-signature', + }, + state: 'approval-requested', + }, + { + approval: { + id: 'id-1', + approved: true, + isAutomatic: true, + signature: 'test-signature', + }, + state: 'approval-responded', + }, + ]); + }); + }); + describe('automatic tool approval denial (static tool)', () => { beforeEach(async () => { const stream = createUIMessageStream([ diff --git a/packages/ai/src/ui/process-ui-message-stream.ts b/packages/ai/src/ui/process-ui-message-stream.ts index 86d97964c07d..5a9d15fc2954 100644 --- a/packages/ai/src/ui/process-ui-message-stream.ts +++ b/packages/ai/src/ui/process-ui-message-stream.ts @@ -731,6 +731,9 @@ export function processUIMessageStream({ approved: chunk.approved, ...(chunk.reason != null ? { reason: chunk.reason } : {}), ...(approval.isAutomatic === true ? { isAutomatic: true } : {}), + ...(approval.signature != null + ? { signature: approval.signature } + : {}), }; if (chunk.providerExecuted != null) { toolInvocation.providerExecuted = chunk.providerExecuted;