@@ -49,6 +49,10 @@ class _InlineTable(dict, InlineTableDict):
4949 pass
5050
5151
52+ def _is_runtime_field (description : str ) -> bool :
53+ return "at runtime" in description
54+
55+
5256def _wrap_comment (text : str , prefix : str = "" , width : int = 80 ) -> str :
5357 """Wrap text into multiline comment format."""
5458 lines = text .strip ().split ("\n " )
@@ -122,7 +126,7 @@ def _dump_toml_scalar(
122126 case "BinarySize" :
123127 value = f"{ BinarySize (value ):s} " .upper ()
124128 case "HostPortPair" :
125- value = {"host" : value . host , "port" : value . port }
129+ value = {"host" : value [ " host" ] , "port" : value [ " port" ] }
126130 case "EnumByValue" :
127131 assert ctx .annotation is not None
128132 value = ctx .annotation (value ).value
@@ -193,7 +197,7 @@ def _get_field_info(model_cls: Type[BaseModel], field_name: str, indent: int) ->
193197 try :
194198 factory_instance = field .default_factory () # type: ignore
195199 if isinstance (factory_instance , BaseModel ):
196- field_info ["default" ] = factory_instance .model_dump ()
200+ field_info ["default" ] = factory_instance .model_dump (mode = "python" )
197201 else :
198202 field_info ["default" ] = factory_instance
199203 except Exception :
@@ -224,7 +228,7 @@ def _process_property(
224228 # Add description as comment if available
225229 description = field_info .get ("description" ) or prop_schema .get ("description" )
226230 if description :
227- if "This field is injected at runtime" in description :
231+ if _is_runtime_field ( description ) :
228232 # Skip runtimme-generated fields.
229233 return []
230234 comment_lines = _wrap_comment (description )
@@ -332,8 +336,20 @@ def _process_schema(
332336 # Group properties by type
333337 simple_props = {}
334338 object_props = {}
339+ array_of_tables_props = {}
335340
336341 for prop_name , prop_schema in properties .items ():
342+ original_prop_schema = prop_schema
343+
344+ # Handle anyOf (optional types) - unwrap to check the inner type
345+ if "anyOf" in prop_schema :
346+ # Check if this is an optional type (Type | None)
347+ any_of_items = prop_schema ["anyOf" ]
348+ non_null_items = [item for item in any_of_items if item != {"type" : "null" }]
349+ if len (non_null_items ) == 1 and len (any_of_items ) == 2 :
350+ # This is an optional type - unwrap it to check if it's an object
351+ prop_schema = non_null_items [0 ]
352+
337353 if "$ref" in prop_schema :
338354 # Resolve reference
339355 ref_path = prop_schema ["$ref" ].split ("/" )
@@ -345,12 +361,36 @@ def _process_schema(
345361
346362 prop_type = prop_schema .get ("type" , "" )
347363
348- if (prop_type == "object" or "properties" in prop_schema ) and prop_schema [
364+ # Check if this is an array of objects (array of tables in TOML)
365+ if prop_type == "array" and "items" in prop_schema :
366+ items_schema = prop_schema ["items" ]
367+ # Resolve $ref in items if present
368+ if "$ref" in items_schema :
369+ ref_path = items_schema ["$ref" ].split ("/" )
370+ if ref_path [0 ] == "#" and len (ref_path ) > 1 :
371+ resolved = schema
372+ for part in ref_path [1 :]:
373+ resolved = resolved .get (part , {})
374+ items_schema = resolved
375+
376+ # Check if the items are objects (complex types)
377+ if items_schema .get ("type" ) == "object" or "properties" in items_schema :
378+ array_of_tables_props [prop_name ] = (original_prop_schema , items_schema )
379+ continue
380+
381+ # Check if this is a complex object that should be expanded
382+ if (prop_type == "object" or "properties" in prop_schema ) and prop_schema .get (
349383 "title"
350- ] != "HostPortPair" :
384+ ) != "HostPortPair" :
385+ # Preserve description from original schema if it was unwrapped
386+ if "description" in original_prop_schema and "description" not in prop_schema :
387+ prop_schema = {
388+ ** prop_schema ,
389+ "description" : original_prop_schema ["description" ],
390+ }
351391 object_props [prop_name ] = prop_schema
352392 else :
353- simple_props [prop_name ] = prop_schema
393+ simple_props [prop_name ] = original_prop_schema
354394
355395 # Add simple properties first
356396 processed_simple_props = []
@@ -360,6 +400,7 @@ def _process_schema(
360400 )
361401 if prop_lines :
362402 lines .extend (prop_lines )
403+ # Exclude runtime-injected fields from the warning
363404 processed_simple_props .append (prop_name )
364405
365406 if path == [] and processed_simple_props :
@@ -368,19 +409,25 @@ def _process_schema(
368409 "The configuration schema CANNOT have simple fields in the root "
369410 "without any section header according to the TOML specification. "
370411 "Also, optional sections should be defined non-optional with explicit default factory. "
371- f"Please move or fix these fields/sections: { ', ' .join (simple_props . keys () )} . "
412+ f"Please move or fix these fields/sections: { ', ' .join (processed_simple_props )} . "
372413 )
373414
374415 # Add object properties as sections
375416 for prop_name , prop_schema in object_props .items ():
376417 indent_str = " " * len (path )
377418
419+ # Skip if this is a runtime-injected field
420+ description = prop_schema .get ("description" , "" )
421+ if _is_runtime_field (description ):
422+ continue
423+
378424 if lines and lines [- 1 ].strip (): # Add blank line before section
379425 lines .append ("" )
380426
381427 # Add section comment
382- if "description" in prop_schema :
383- comment_lines = _wrap_comment (prop_schema ["description" ], prefix = indent_str )
428+ description = prop_schema .get ("description" , "" )
429+ if description :
430+ comment_lines = _wrap_comment (description , prefix = indent_str )
384431 lines .extend (comment_lines .split ("\n " ))
385432
386433 # Add section header
@@ -422,6 +469,67 @@ def _process_schema(
422469 )
423470 lines .extend (nested_lines )
424471
472+ # Add array of tables properties using [[array.name]] syntax
473+ for prop_name , (prop_schema , items_schema ) in array_of_tables_props .items ():
474+ indent_str = " " * len (path )
475+
476+ # Skip if this is a runtime-injected field
477+ description = prop_schema .get ("description" , "" )
478+ if _is_runtime_field (description ):
479+ continue
480+
481+ if lines and lines [- 1 ].strip (): # Add blank line before section
482+ lines .append ("" )
483+
484+ # Add array of tables comment
485+ if description :
486+ comment_lines = _wrap_comment (description , prefix = indent_str )
487+ lines .extend (comment_lines .split ("\n " ))
488+
489+ # Add array of tables header with double brackets [[array.name]]
490+ section_path = path + [prop_name ]
491+ array_header = f"{ indent_str } [[{ '.' .join (section_path )} ]]"
492+ lines .append (array_header )
493+ print (array_header )
494+
495+ # Add a comment about adding multiple entries
496+ lines .append (
497+ f"{ indent_str } # Add multiple [[{ '.' .join (section_path )} ]] sections as needed"
498+ )
499+
500+ # Process nested properties for the item schema
501+ nested_model_cls = None
502+ if model_cls and hasattr (model_cls , "model_fields" ):
503+ field_info = _get_field_info (model_cls , prop_name , indent = len (path ))
504+ if field_info :
505+ # Try to find the field and extract the item type from list annotation
506+ field = None
507+ if prop_name in model_cls .model_fields :
508+ field = model_cls .model_fields [prop_name ]
509+ else :
510+ # Search by alias
511+ for finfo in model_cls .model_fields .values ():
512+ if (
513+ hasattr (finfo , "serialization_alias" )
514+ and finfo .serialization_alias == prop_name
515+ ):
516+ field = finfo
517+ break
518+
519+ if field :
520+ if hasattr (field , "annotation" ) and hasattr (field .annotation , "__origin__" ):
521+ # Handle generic types like list[SubAgentConfig]
522+ args = getattr (field .annotation , "__args__" , ())
523+ if args and hasattr (args [0 ], "model_fields" ):
524+ nested_model_cls = args [0 ]
525+ elif hasattr (field .annotation , "model_fields" ):
526+ nested_model_cls = field .annotation
527+
528+ nested_lines = _process_schema (
529+ items_schema , path = section_path , parent_required = [], model_cls = nested_model_cls
530+ )
531+ lines .extend (nested_lines )
532+
425533 return lines
426534
427535 # Process the root schema
0 commit comments