@@ -247,6 +247,33 @@ export interface FormatChatHistoryOptions {
247247 includeTimestamps ?: boolean ;
248248}
249249
250+ export type ChatContextValidationSeverity = 'error' | 'warning' ;
251+
252+ export interface ChatContextValidationIssue {
253+ severity : ChatContextValidationSeverity ;
254+ code :
255+ | 'duplicate_id'
256+ | 'timestamp_order'
257+ | 'empty_message_content'
258+ | 'empty_text_term'
259+ | 'missing_image_term'
260+ | 'invalid_audio_term'
261+ | 'invalid_function_call'
262+ | 'invalid_function_call_args'
263+ | 'invalid_function_call_output'
264+ | 'orphan_function_call_output' ;
265+ index : number ;
266+ itemId : string ;
267+ message : string ;
268+ }
269+
270+ export interface ChatContextValidationResult {
271+ valid : boolean ;
272+ errors : number ;
273+ warnings : number ;
274+ issues : ChatContextValidationIssue [ ] ;
275+ }
276+
250277/**
251278 * Render a chat context into a readable multiline string for debugging and logging.
252279 */
@@ -273,6 +300,148 @@ export function formatChatHistory(
273300 ] . join ( '\n' ) ;
274301}
275302
303+ /**
304+ * Validate structural integrity of chat context items/terms for realtime usage.
305+ */
306+ export function validateChatContextStructure ( chatCtx : ChatContext ) : ChatContextValidationResult {
307+ const issues : ChatContextValidationIssue [ ] = [ ] ;
308+ const ids = new Set < string > ( ) ;
309+ const seenFunctionCallIds = new Set < string > ( ) ;
310+ let previousCreatedAt = - Infinity ;
311+
312+ const pushIssue = ( issue : ChatContextValidationIssue ) => {
313+ issues . push ( issue ) ;
314+ } ;
315+
316+ for ( let index = 0 ; index < chatCtx . items . length ; index += 1 ) {
317+ const item = chatCtx . items [ index ] ! ;
318+
319+ if ( ids . has ( item . id ) ) {
320+ pushIssue ( {
321+ severity : 'error' ,
322+ code : 'duplicate_id' ,
323+ index,
324+ itemId : item . id ,
325+ message : `Duplicate item id '${ item . id } '` ,
326+ } ) ;
327+ } else {
328+ ids . add ( item . id ) ;
329+ }
330+
331+ if ( item . createdAt < previousCreatedAt ) {
332+ pushIssue ( {
333+ severity : 'error' ,
334+ code : 'timestamp_order' ,
335+ index,
336+ itemId : item . id ,
337+ message : `Item createdAt (${ item . createdAt } ) is older than previous item (${ previousCreatedAt } )` ,
338+ } ) ;
339+ }
340+ previousCreatedAt = item . createdAt ;
341+
342+ if ( item . type === 'message' ) {
343+ if ( item . content . length === 0 ) {
344+ pushIssue ( {
345+ severity : 'warning' ,
346+ code : 'empty_message_content' ,
347+ index,
348+ itemId : item . id ,
349+ message : 'Message has empty content array' ,
350+ } ) ;
351+ }
352+
353+ item . content . forEach ( ( term , termIndex ) => {
354+ if ( typeof term === 'string' ) {
355+ if ( term . trim ( ) . length === 0 ) {
356+ pushIssue ( {
357+ severity : 'warning' ,
358+ code : 'empty_text_term' ,
359+ index,
360+ itemId : item . id ,
361+ message : `Message term[${ termIndex } ] is empty text` ,
362+ } ) ;
363+ }
364+ return ;
365+ }
366+
367+ if ( term . type === 'image_content' ) {
368+ if ( ! term . id || term . image === undefined || term . image === null ) {
369+ pushIssue ( {
370+ severity : 'error' ,
371+ code : 'missing_image_term' ,
372+ index,
373+ itemId : item . id ,
374+ message : `Message term[${ termIndex } ] has invalid image content` ,
375+ } ) ;
376+ }
377+ return ;
378+ }
379+
380+ if ( ! Array . isArray ( term . frame ) ) {
381+ pushIssue ( {
382+ severity : 'error' ,
383+ code : 'invalid_audio_term' ,
384+ index,
385+ itemId : item . id ,
386+ message : `Message term[${ termIndex } ] has invalid audio content` ,
387+ } ) ;
388+ }
389+ } ) ;
390+ } else if ( item . type === 'function_call' ) {
391+ if ( ! item . name || ! item . callId ) {
392+ pushIssue ( {
393+ severity : 'error' ,
394+ code : 'invalid_function_call' ,
395+ index,
396+ itemId : item . id ,
397+ message : 'Function call is missing name or callId' ,
398+ } ) ;
399+ } else {
400+ seenFunctionCallIds . add ( item . callId ) ;
401+ }
402+
403+ try {
404+ JSON . parse ( item . args ) ;
405+ } catch {
406+ pushIssue ( {
407+ severity : 'warning' ,
408+ code : 'invalid_function_call_args' ,
409+ index,
410+ itemId : item . id ,
411+ message : 'Function call args are not valid JSON' ,
412+ } ) ;
413+ }
414+ } else if ( item . type === 'function_call_output' ) {
415+ if ( ! item . callId ) {
416+ pushIssue ( {
417+ severity : 'error' ,
418+ code : 'invalid_function_call_output' ,
419+ index,
420+ itemId : item . id ,
421+ message : 'Function call output is missing callId' ,
422+ } ) ;
423+ } else if ( ! seenFunctionCallIds . has ( item . callId ) ) {
424+ pushIssue ( {
425+ severity : 'warning' ,
426+ code : 'orphan_function_call_output' ,
427+ index,
428+ itemId : item . id ,
429+ message : `Function call output references unknown callId '${ item . callId } '` ,
430+ } ) ;
431+ }
432+ }
433+ }
434+
435+ const errors = issues . filter ( ( issue ) => issue . severity === 'error' ) . length ;
436+ const warnings = issues . length - errors ;
437+ return {
438+ valid : errors === 0 ,
439+ errors,
440+ warnings,
441+ issues,
442+ } ;
443+ }
444+
276445function formatChatHistoryItem (
277446 item : ChatItem ,
278447 index : number ,
0 commit comments