@@ -94,8 +94,12 @@ function repairBareStringFieldObject(input: string, toolName: string): unknown {
9494 return { [ field ] : value }
9595}
9696
97- function parseStringifiedToolInput ( input : unknown , toolName : string ) : unknown {
97+ function parseStringifiedToolInput (
98+ input : unknown ,
99+ toolName : string ,
100+ ) : { input : unknown ; parseError ?: string } {
98101 let parsed = input
102+ let parseError : string | undefined
99103
100104 // Some providers/models double-encode tool arguments, for example an input
101105 // value like "\"{\\\"path\\\":\\\"file.ts\\\"}\"". Repeated JSON.parse
@@ -104,27 +108,76 @@ function parseStringifiedToolInput(input: unknown, toolName: string): unknown {
104108 const stringInput = parsed
105109 try {
106110 parsed = JSON . parse ( stringInput )
107- } catch {
111+ parseError = undefined
112+ } catch ( error ) {
108113 const repaired = repairBareStringFieldObject ( stringInput , toolName )
109114 if ( repaired !== undefined ) {
110115 parsed = repaired
116+ parseError = undefined
117+ } else {
118+ parseError = error instanceof Error ? error . message : String ( error )
111119 }
112120 break
113121 }
114122 }
115123
116- return parsed
124+ return { input : parsed , parseError }
117125}
118126
119- function stringInputError ( toolName : string , toolCallId : string ) : ToolCallError {
127+ function stringInputError (
128+ toolName : string ,
129+ toolCallId : string ,
130+ parseError ?: string ,
131+ ) : ToolCallError {
132+ const parseDetails = parseError
133+ ? ` The JSON parser reported: ${ parseError } . If the arguments are incomplete, re-issue the full object.`
134+ : ''
120135 return {
121136 toolName,
122137 toolCallId,
123138 input : { } ,
124- error : `Invalid parameters for ${ toolName } : tool arguments were a string, not a JSON object. The runtime tried to parse stringified JSON before validation, but the value was still not a JSON object. Re-issue the tool call as a JSON object with properly escaped string values.` ,
139+ error : `Invalid parameters for ${ toolName } : tool arguments were a string, not a JSON object. The runtime tried to parse stringified JSON before validation, but the value was still not a JSON object.${ parseDetails } Re-issue the tool call as a JSON object with properly escaped string values.` ,
125140 }
126141}
127142
143+ function summarizeMissingReplacementFields (
144+ toolName : string ,
145+ issues : Array < {
146+ expected ?: unknown
147+ code ?: string
148+ path ?: PropertyKey [ ]
149+ message ?: string
150+ } > ,
151+ ) : string | undefined {
152+ if ( toolName !== 'str_replace' && toolName !== 'propose_str_replace' ) {
153+ return undefined
154+ }
155+
156+ const missingFields = issues . flatMap ( ( issue ) => {
157+ const [ root , index , field ] = issue . path ?? [ ]
158+ const isMissingReplacementString =
159+ issue . code === 'invalid_type' &&
160+ issue . expected === 'string' &&
161+ issue . message ?. includes ( 'received undefined' ) &&
162+ root === 'replacements' &&
163+ typeof index === 'number' &&
164+ ( field === 'old' || field === 'new' )
165+
166+ return isMissingReplacementString ? [ `replacements[${ index } ].${ field } ` ] : [ ]
167+ } )
168+
169+ if ( missingFields . length !== issues . length || missingFields . length === 0 ) {
170+ return undefined
171+ }
172+
173+ return [
174+ 'Missing required replacement fields:' ,
175+ ...missingFields . map ( ( field ) => `- ${ field } ` ) ,
176+ '' ,
177+ 'If the intent is deletion, set "new": "" explicitly.' ,
178+ ] . join ( '\n' )
179+ }
180+
128181function getToolValidationHint ( toolName : string ) : string | undefined {
129182 if ( toolName === 'str_replace' || toolName === 'propose_str_replace' ) {
130183 return 'Expected shape: { "path": string, "replacements": [{ "old": string, "new": string, "allowMultiple"?: boolean }] }.'
@@ -151,23 +204,32 @@ export function parseRawToolCall<T extends ToolName = ToolName>(params: {
151204 )
152205 const paramsSchema = toolParams [ toolName ] . inputSchema
153206
154- if ( typeof processedParameters === 'string' ) {
155- return stringInputError ( toolName , rawToolCall . toolCallId )
207+ if ( typeof processedParameters . input === 'string' ) {
208+ return stringInputError (
209+ toolName ,
210+ rawToolCall . toolCallId ,
211+ processedParameters . parseError ,
212+ )
156213 }
157214
158- const result = paramsSchema . safeParse ( processedParameters )
215+ const result = paramsSchema . safeParse ( processedParameters . input )
159216
160217 if ( ! result . success ) {
161218 const hint = getToolValidationHint ( toolName )
219+ const summary = summarizeMissingReplacementFields (
220+ toolName ,
221+ result . error . issues ,
222+ )
223+ const validationDetails = JSON . stringify ( result . error . issues , null , 2 )
162224 return {
163225 toolName,
164226 toolCallId : rawToolCall . toolCallId ,
165227 input : rawToolCall . input ,
166- error : `Invalid parameters for ${ toolName } : ${ JSON . stringify (
167- result . error . issues ,
168- null ,
169- 2 ,
170- ) } ${ hint ? `\n\n${ hint } ` : '' } `,
228+ error : `Invalid parameters for ${ toolName } : ${
229+ summary
230+ ? ` ${ summary } \n\nRaw validation issues:\n ${ validationDetails } `
231+ : validationDetails
232+ } ${ hint ? `\n\n${ hint } ` : '' } `,
171233 }
172234 }
173235
@@ -496,12 +558,16 @@ export function parseRawCustomToolCall(params: {
496558
497559 const parsedInput = parseStringifiedToolInput ( rawToolCall . input , toolName )
498560
499- if ( typeof parsedInput === 'string' ) {
500- return stringInputError ( toolName , rawToolCall . toolCallId )
561+ if ( typeof parsedInput . input === 'string' ) {
562+ return stringInputError (
563+ toolName ,
564+ rawToolCall . toolCallId ,
565+ parsedInput . parseError ,
566+ )
501567 }
502568
503569 const processedParameters : Record < string , any > = { }
504- for ( const [ param , val ] of Object . entries ( parsedInput ?? { } ) ) {
570+ for ( const [ param , val ] of Object . entries ( parsedInput . input ?? { } ) ) {
505571 processedParameters [ param ] = val
506572 }
507573
@@ -530,7 +596,7 @@ export function parseRawCustomToolCall(params: {
530596 }
531597 }
532598
533- const input = JSON . parse ( JSON . stringify ( parsedInput ) )
599+ const input = JSON . parse ( JSON . stringify ( parsedInput . input ) )
534600 if ( endsAgentStepParam in input ) {
535601 delete input [ endsAgentStepParam ]
536602 }
0 commit comments