@@ -48,11 +48,12 @@ function parseCanonicalEventTypes(source: string): string[] {
4848 * Extract field definitions from a Zod `.extend({...})` block body.
4949 */
5050function extractExtendFields ( extendBody : string ) : FieldInfo [ ] {
51+ // Strip comment lines so they don't match as field definitions
52+ const cleanBody = extendBody . replace ( / ^ \s * \/ \/ .* $ / gm, "" ) ;
5153 const fields : FieldInfo [ ] = [ ] ;
52- for ( const fieldMatch of extendBody . matchAll ( / ( \w + ) \s * : \s * ( [ ^ \n , ] + ) / g) ) {
54+ for ( const fieldMatch of cleanBody . matchAll ( / ( \w + ) \s * : \s * ( . + ) / g) ) {
5355 const fieldName = fieldMatch [ 1 ] ;
54- const fieldDef = fieldMatch [ 2 ] . trim ( ) ;
55- if ( fieldDef . startsWith ( "//" ) ) continue ;
56+ const fieldDef = fieldMatch [ 2 ] . replace ( / , \s * $ / , "" ) . trim ( ) ;
5657 const optional = fieldDef . includes ( ".optional()" ) || fieldDef . includes ( ".default(" ) ;
5758 fields . push ( { name : fieldName , optional } ) ;
5859 }
@@ -70,15 +71,30 @@ function extractExtendFields(extendBody: string): FieldInfo[] {
7071 * TextMessageContentEventSchema.omit({...}).extend({...})
7172 * where ThinkingTextMessageContentEventSchema inherits delta from TextMessageContent.
7273 */
74+
75+ /**
76+ * Parse base fields from `BaseEventSchema = z.object({...})` in canonical source.
77+ * Falls back to hardcoded defaults if parsing fails.
78+ */
79+ function parseCanonicalBaseFields ( source : string ) : FieldInfo [ ] {
80+ const baseMatch = source . match (
81+ / e x p o r t c o n s t B a s e E v e n t S c h e m a \s * = \s * z \s * \. \s * o b j e c t \( \{ ( [ \s \S ] * ?) \} \) / ,
82+ ) ;
83+ if ( ! baseMatch ) {
84+ return [
85+ { name : "type" , optional : false } ,
86+ { name : "timestamp" , optional : true } ,
87+ { name : "rawEvent" , optional : true } ,
88+ ] ;
89+ }
90+ return extractExtendFields ( baseMatch [ 1 ] ) ;
91+ }
92+
7393function parseCanonicalSchemas ( source : string ) : Map < string , SchemaInfo > {
7494 const schemas = new Map < string , SchemaInfo > ( ) ;
7595
76- // Base event fields (always inherited)
77- const baseFields : FieldInfo [ ] = [
78- { name : "type" , optional : false } ,
79- { name : "timestamp" , optional : true } ,
80- { name : "rawEvent" , optional : true } ,
81- ] ;
96+ // Parse base event fields dynamically from BaseEventSchema
97+ const baseFields = parseCanonicalBaseFields ( source ) ;
8298
8399 // Pass 1: collect raw schema definitions keyed by schema name
84100 interface RawSchema {
@@ -123,6 +139,14 @@ function parseCanonicalSchemas(source: string): Map<string, SchemaInfo> {
123139 fieldsBySchemaName . set ( schemaName , ownFields ) ;
124140 }
125141
142+ // Recursive parent field resolver for multi-level inheritance chains
143+ function resolveParentFields ( schemaName : string ) : FieldInfo [ ] {
144+ const entry = rawSchemas . get ( schemaName ) ;
145+ if ( ! entry ) return [ ] ;
146+ const parentFields = entry . parentSchemaName ? resolveParentFields ( entry . parentSchemaName ) : [ ] ;
147+ return [ ...parentFields , ...( fieldsBySchemaName . get ( schemaName ) || [ ] ) ] ;
148+ }
149+
126150 // Pass 2: resolve full field sets with parent inheritance
127151 for ( const [ , raw ] of rawSchemas ) {
128152 const fields = new Map < string , FieldInfo > ( ) ;
@@ -132,13 +156,10 @@ function parseCanonicalSchemas(source: string): Map<string, SchemaInfo> {
132156 fields . set ( f . name , { ...f } ) ;
133157 }
134158
135- // If there's a parent schema (not BaseEventSchema), inherit its extend fields
159+ // Resolve full parent chain (handles multi-level inheritance)
136160 if ( raw . parentSchemaName ) {
137- const parentFields = fieldsBySchemaName . get ( raw . parentSchemaName ) ;
138- if ( parentFields ) {
139- for ( const f of parentFields ) {
140- fields . set ( f . name , { ...f } ) ;
141- }
161+ for ( const f of resolveParentFields ( raw . parentSchemaName ) ) {
162+ fields . set ( f . name , { ...f } ) ;
142163 }
143164 }
144165
@@ -179,9 +200,34 @@ function parseAimockEventTypes(source: string): string[] {
179200 return members ;
180201}
181202
203+ /**
204+ * Parse base fields from `AGUIBaseEvent` interface in aimock source.
205+ * Falls back to hardcoded defaults if parsing fails.
206+ */
207+ function parseAimockBaseFields ( source : string ) : FieldInfo [ ] {
208+ const baseMatch = source . match ( / e x p o r t i n t e r f a c e A G U I B a s e E v e n t \s * \{ ( [ \s \S ] * ?) \} / ) ;
209+ if ( ! baseMatch ) {
210+ return [
211+ { name : "type" , optional : false } ,
212+ { name : "timestamp" , optional : true } ,
213+ { name : "rawEvent" , optional : true } ,
214+ ] ;
215+ }
216+ const fields : FieldInfo [ ] = [ ] ;
217+ for ( const fieldMatch of baseMatch [ 1 ] . matchAll ( / ( \w + ) ( \? ? ) \s * : \s * ( [ ^ ; ] + ) ; / g) ) {
218+ const fieldName = fieldMatch [ 1 ] ;
219+ const optional = fieldMatch [ 2 ] === "?" ;
220+ fields . push ( { name : fieldName , optional } ) ;
221+ }
222+ return fields ;
223+ }
224+
182225function parseAimockInterfaces ( source : string ) : Map < string , SchemaInfo > {
183226 const interfaces = new Map < string , SchemaInfo > ( ) ;
184227
228+ // Parse base fields dynamically from AGUIBaseEvent interface
229+ const baseFields = parseAimockBaseFields ( source ) ;
230+
185231 // Match interface blocks
186232 const interfacePattern = / e x p o r t i n t e r f a c e A G U I ( \w + E v e n t ) \s + e x t e n d s \s + \w + \s * \{ ( [ \s \S ] * ?) \} / g;
187233
@@ -193,12 +239,8 @@ function parseAimockInterfaces(source: string): Map<string, SchemaInfo> {
193239 if ( ! typeMatch ) continue ;
194240 const eventType = typeMatch [ 1 ] ;
195241
196- // Start with base fields (all extend AGUIBaseEvent)
197- const fields : FieldInfo [ ] = [
198- { name : "type" , optional : false } ,
199- { name : "timestamp" , optional : true } ,
200- { name : "rawEvent" , optional : true } ,
201- ] ;
242+ // Start with dynamically-parsed base fields
243+ const fields : FieldInfo [ ] = baseFields . map ( ( f ) => ( { ...f } ) ) ;
202244
203245 // Parse fields from the interface body
204246 for ( const fieldMatch of body . matchAll ( / ( \w + ) ( \? ? ) \s * : \s * ( [ ^ ; ] + ) ; / g) ) {
@@ -235,7 +277,7 @@ interface DriftItem {
235277const canonicalExists = fs . existsSync ( CANONICAL_EVENTS_PATH ) ;
236278const aimockExists = fs . existsSync ( AIMOCK_TYPES_PATH ) ;
237279
238- describe . skipIf ( ! canonicalExists ) ( "AG-UI schema drift" , ( ) => {
280+ describe . skipIf ( ! canonicalExists || ! aimockExists ) ( "AG-UI schema drift" , ( ) => {
239281 let canonicalSource : string ;
240282 let aimockSource : string ;
241283 let canonicalTypes : string [ ] ;
0 commit comments