WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
13006db
docs: Motivation notepad
Arron-Stothart Nov 28, 2025
ed91c96
ws: minimal solution
Arron-Stothart Nov 28, 2025
efc25d7
ws: add input overides to stream text
Arron-Stothart Nov 28, 2025
07aa886
Update notepad
Arron-Stothart Nov 28, 2025
584334d
Test cases
Arron-Stothart Nov 28, 2025
3097384
Update test snapshots
Arron-Stothart Nov 28, 2025
5d97731
Add allowInputModification to approval contract
Arron-Stothart Nov 29, 2025
eb16765
Test updates and linting
Arron-Stothart Nov 29, 2025
ed5aad1
Delete Notepad
Arron-Stothart Nov 29, 2025
0a12329
Generate changeset
Arron-Stothart Nov 29, 2025
848c5da
Rename tool.inputEditable and add to conversions
Arron-Stothart Nov 29, 2025
a895f74
oai example
Arron-Stothart Nov 29, 2025
8c68551
rename: modified to edited
Arron-Stothart Nov 29, 2025
1a29e30
Naming improvements
Arron-Stothart Dec 3, 2025
6a78b85
Use nested override object for tool input modifications
Arron-Stothart Dec 3, 2025
79aa21a
Naming improvements
Arron-Stothart Dec 3, 2025
7b8b43d
Merge branch 'main' into feat/tool-approval-input-editing
Arron-Stothart Dec 3, 2025
d1ebd28
Update next-openai
Arron-Stothart Dec 3, 2025
b09f5ac
Update test snapshots
Arron-Stothart Dec 3, 2025
2362acc
inherit dynamic flag from toolCall in override
Arron-Stothart Dec 5, 2025
8abe037
Simplify error message for input modification not allowed
Arron-Stothart Dec 5, 2025
13ed6ee
Merge branch 'main' into feat/tool-approval-input-editing
Arron-Stothart Dec 5, 2025
037d544
Remove unnecessary override from output-denied and providerExecuted f…
Arron-Stothart Dec 5, 2025
1ba41bf
Reintroduce override to output-denied
Arron-Stothart Dec 5, 2025
5880802
Merge branch 'main' into feat/tool-approval-input-editing
Arron-Stothart Dec 5, 2025
377fc54
Prettier fix on next-openai
Arron-Stothart Dec 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/rare-students-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@ai-sdk/provider-utils': patch
'ai': patch
---

Support modifying tool inputs during approval
21 changes: 18 additions & 3 deletions examples/ai-core/src/generate-text/openai-tool-approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const weatherTool = tool({
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
needsApproval: true,
allowsInputEditing: true,
});

run(async () => {
Expand Down Expand Up @@ -61,14 +62,28 @@ run(async () => {
if (part.type === 'tool-approval-request') {
if (part.toolCall.toolName === 'weather' && !part.toolCall.dynamic) {
const answer = await terminal.question(
`\nCan I retrieve the weather for ${part.toolCall.input.location} (y/n)?`,
`\nCan I retrieve the weather for ${part.toolCall.input.location} (y/n/e)?`,
);

const approved = ['y', 'yes', 'e', 'edit'].includes(
answer.toLowerCase(),
);
const edit = ['e', 'edit'].includes(answer.toLowerCase());

let override = undefined;

if (edit) {
const newLocation = await terminal.question(
`Enter new location (current: ${part.toolCall.input.location}): `,
);
override = { input: { location: newLocation } };
}

approvals.push({
type: 'tool-approval-response',
approvalId: part.approvalId,
approved:
answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes',
approved,
override,
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from 'react';
import type { WeatherUIToolWithApprovalInvocation } from '@/tool/weather-tool-with-approval';
import type { ChatAddToolApproveResponseFunction } from 'ai';

Expand All @@ -8,20 +9,34 @@ export default function WeatherWithApprovalView({
invocation: WeatherUIToolWithApprovalInvocation;
addToolApprovalResponse: ChatAddToolApproveResponseFunction;
}) {
const [city, setCity] = useState(invocation.input?.city ?? '');

switch (invocation.state) {
case 'approval-requested':
return (
<div className="text-gray-500">
Can I retrieve the weather for {invocation.input.city}?
<div>
<div>Can I retrieve the weather for {invocation.input.city}?</div>
{invocation.approval.allowsInputEditing && (
<input
value={city}
onChange={e => setCity(e.target.value)}
className="mt-2 px-2 py-1 border rounded"
/>
)}
<div className="mt-2">
<button
className="px-4 py-2 mr-2 text-white bg-blue-500 rounded transition-colors hover:bg-blue-600"
onClick={() =>
onClick={() => {
const trimmed = city?.trim();
addToolApprovalResponse({
id: invocation.approval.id,
approved: true,
})
}
override:
trimmed && trimmed !== invocation.input.city
? { input: { city: trimmed } }
: undefined,
});
}}
>
Approve
</button>
Expand All @@ -42,7 +57,8 @@ export default function WeatherWithApprovalView({
case 'approval-responded':
return (
<div className="text-gray-500">
Can I retrieve the weather for {invocation.input.city}?
Can I retrieve the weather for{' '}
{invocation.approval.override?.input.city ?? invocation.input.city}?
<div>{invocation.approval.approved ? 'Approved' : 'Denied'}</div>
</div>
);
Expand All @@ -52,7 +68,7 @@ export default function WeatherWithApprovalView({
<div className="text-gray-500">
{invocation.output.state === 'loading'
? 'Fetching weather information...'
: `Weather in ${invocation.input.city}: ${invocation.output.weather}`}
: `Weather in ${invocation.approval?.override?.input.city ?? invocation.input.city}: ${invocation.output.weather}`}
</div>
);
case 'output-denied':
Expand Down
1 change: 1 addition & 0 deletions examples/next-openai/tool/weather-tool-with-approval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const weatherToolWithApproval = tool({
description: 'Get the weather in a location',
inputSchema: z.object({ city: z.string() }),
needsApproval: true,
allowsInputEditing: true,
async *execute() {
yield { state: 'loading' as const };

Expand Down
4 changes: 4 additions & 0 deletions packages/ai/src/generate-text/generate-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3928,6 +3928,7 @@ describe('generateText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCall": {
"input": {
Expand Down Expand Up @@ -3962,6 +3963,7 @@ describe('generateText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
Expand Down Expand Up @@ -4066,6 +4068,7 @@ describe('generateText', () => {
"type": "tool-result",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCall": {
"input": {
Expand Down Expand Up @@ -4110,6 +4113,7 @@ describe('generateText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
Expand Down
15 changes: 11 additions & 4 deletions packages/ai/src/generate-text/generate-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { DownloadFunction } from '../util/download/download-function';
import { prepareRetries } from '../util/prepare-retries';
import { VERSION } from '../version';
import { collectToolApprovals } from './collect-tool-approvals';
import { validateAndApplyToolInputOverrides } from './validate-and-apply-tool-input-overrides';
import { ContentPart } from './content-part';
import { executeToolCall } from './execute-tool-call';
import { extractTextContent } from './extract-text-content';
Expand Down Expand Up @@ -347,10 +348,13 @@ A function that attempts to repair a tool call that failed to parse.
deniedToolApprovals.length > 0 ||
approvedToolApprovals.length > 0
) {
const { validToolCalls, invalidToolErrors } =
validateAndApplyToolInputOverrides({
approvals: approvedToolApprovals,
});

const toolOutputs = await executeTools({
toolCalls: approvedToolApprovals.map(
toolApproval => toolApproval.toolCall,
),
toolCalls: validToolCalls,
tools: tools as TOOLS,
tracer,
telemetry,
Expand All @@ -359,11 +363,13 @@ A function that attempts to repair a tool call that failed to parse.
experimental_context,
});

const allToolOutputs = [...toolOutputs, ...invalidToolErrors];

responseMessages.push({
role: 'tool',
content: [
// add regular tool results for approved tool calls:
...toolOutputs.map(output => ({
...allToolOutputs.map(output => ({
type: 'tool-result' as const,
toolCallId: output.toolCallId,
toolName: output.toolName,
Expand Down Expand Up @@ -595,6 +601,7 @@ A function that attempts to repair a tool call that failed to parse.
type: 'tool-approval-request',
approvalId: generateId(),
toolCall,
allowsInputEditing: tool.allowsInputEditing,
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ describe('runToolsTransformation', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-0",
"toolCall": {
"input": {
Expand Down
1 change: 1 addition & 0 deletions packages/ai/src/generate-text/run-tools-transformation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ export function runToolsTransformation<TOOLS extends ToolSet>({
type: 'tool-approval-request',
approvalId: generateId(),
toolCall,
allowsInputEditing: tool.allowsInputEditing,
});
break;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/ai/src/generate-text/stream-text.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14595,6 +14595,7 @@ describe('streamText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCall": {
"input": {
Expand Down Expand Up @@ -14661,6 +14662,7 @@ describe('streamText', () => {
"type": "tool-input-available",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
Expand Down Expand Up @@ -14691,6 +14693,7 @@ describe('streamText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCall": {
"input": {
Expand Down Expand Up @@ -14725,6 +14728,7 @@ describe('streamText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
Expand Down Expand Up @@ -14811,6 +14815,7 @@ describe('streamText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCall": {
"input": {
Expand Down Expand Up @@ -14898,6 +14903,7 @@ describe('streamText', () => {
"type": "tool-input-available",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
Expand Down Expand Up @@ -14949,6 +14955,7 @@ describe('streamText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCall": {
"input": {
Expand Down Expand Up @@ -15004,6 +15011,7 @@ describe('streamText', () => {
"type": "tool-call",
},
{
"allowsInputEditing": undefined,
"approvalId": "id-1",
"toolCallId": "call-1",
"type": "tool-approval-request",
Expand Down
16 changes: 13 additions & 3 deletions packages/ai/src/generate-text/stream-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { DownloadFunction } from '../util/download/download-function';
import { now as originalNow } from '../util/now';
import { prepareRetries } from '../util/prepare-retries';
import { collectToolApprovals } from './collect-tool-approvals';
import { validateAndApplyToolInputOverrides } from './validate-and-apply-tool-input-overrides';
import { ContentPart } from './content-part';
import { executeToolCall } from './execute-tool-call';
import { Output, text } from './output';
Expand All @@ -88,6 +89,7 @@ import {
import { toResponseMessages } from './to-response-messages';
import { TypedToolCall } from './tool-call';
import { ToolCallRepairFunction } from './tool-call-repair-function';
import { TypedToolError } from './tool-error';
import { ToolOutput } from './tool-output';
import { StaticToolOutputDenied } from './tool-output-denied';
import { ToolSet } from './tool-set';
Expand Down Expand Up @@ -1106,12 +1108,17 @@ class DefaultStreamTextResult<TOOLS extends ToolSet, OUTPUT extends Output>
} as StaticToolOutputDenied<TOOLS>);
}

const { validToolCalls, invalidToolErrors } =
validateAndApplyToolInputOverrides({
approvals: approvedToolApprovals,
});

const toolOutputs: Array<ToolOutput<TOOLS>> = [];

await Promise.all(
approvedToolApprovals.map(async toolApproval => {
validToolCalls.map(async toolCall => {
const result = await executeToolCall({
toolCall: toolApproval.toolCall,
toolCall,
tools,
tracer,
telemetry,
Expand All @@ -1130,11 +1137,13 @@ class DefaultStreamTextResult<TOOLS extends ToolSet, OUTPUT extends Output>
}),
);

const allToolOutputs = [...toolOutputs, ...invalidToolErrors];

initialResponseMessages.push({
role: 'tool',
content: [
// add regular tool results for approved tool calls:
...toolOutputs.map(output => ({
...allToolOutputs.map(output => ({
type: 'tool-result' as const,
toolCallId: output.toolCallId,
toolName: output.toolName,
Expand Down Expand Up @@ -2071,6 +2080,7 @@ However, the LLM results are expected to be small enough to not cause issues.
type: 'tool-approval-request',
approvalId: part.approvalId,
toolCallId: part.toolCall.toolCallId,
allowsInputEditing: part.allowsInputEditing,
});
break;
}
Expand Down
Loading