poml 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/docs/tutorial/advanced/performance.md +695 -0
  3. data/docs/tutorial/advanced/tool-registration.md +776 -0
  4. data/docs/tutorial/basic-usage.md +351 -0
  5. data/docs/tutorial/components/chat-components.md +552 -0
  6. data/docs/tutorial/components/formatting.md +623 -0
  7. data/docs/tutorial/components/index.md +366 -0
  8. data/docs/tutorial/components/media-components.md +259 -0
  9. data/docs/tutorial/components/schema-components.md +668 -0
  10. data/docs/tutorial/index.md +184 -0
  11. data/docs/tutorial/output-formats.md +688 -0
  12. data/docs/tutorial/quickstart.md +30 -0
  13. data/docs/tutorial/template-engine.md +540 -0
  14. data/lib/poml/components/base.rb +146 -4
  15. data/lib/poml/components/content.rb +10 -3
  16. data/lib/poml/components/data.rb +539 -19
  17. data/lib/poml/components/examples.rb +235 -1
  18. data/lib/poml/components/formatting.rb +184 -18
  19. data/lib/poml/components/layout.rb +7 -2
  20. data/lib/poml/components/lists.rb +69 -35
  21. data/lib/poml/components/meta.rb +134 -5
  22. data/lib/poml/components/output_schema.rb +19 -1
  23. data/lib/poml/components/template.rb +72 -61
  24. data/lib/poml/components/text.rb +30 -1
  25. data/lib/poml/components/tool.rb +81 -0
  26. data/lib/poml/components/tool_definition.rb +339 -10
  27. data/lib/poml/components/tools.rb +14 -0
  28. data/lib/poml/components/utilities.rb +34 -18
  29. data/lib/poml/components.rb +19 -0
  30. data/lib/poml/context.rb +19 -4
  31. data/lib/poml/parser.rb +88 -63
  32. data/lib/poml/renderer.rb +191 -9
  33. data/lib/poml/template_engine.rb +138 -13
  34. data/lib/poml/version.rb +1 -1
  35. data/lib/poml.rb +16 -1
  36. data/readme.md +154 -27
  37. metadata +31 -4
  38. data/TUTORIAL.md +0 -987
@@ -6,11 +6,113 @@ module Poml
6
6
 
7
7
  name = get_attribute('name')
8
8
  description = get_attribute('description')
9
- # Support both old 'lang' and new 'parser' attributes for compatibility
10
- parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
11
9
 
12
10
  return '' unless name # Name is required for tools
13
11
 
12
+ # Check if this is declarative XML format (has description/parameter children)
13
+ has_xml_children = @element.children.any? { |child| ['description', 'parameter'].include?(child.tag_name.to_s) }
14
+
15
+ if has_xml_children
16
+ # Handle declarative XML format
17
+ handle_declarative_format(name, description)
18
+ else
19
+ # Handle JSON format (original behavior)
20
+ handle_json_format(name, description)
21
+ end
22
+
23
+ # Meta-like components don't produce output
24
+ ''
25
+ end
26
+
27
+ private
28
+
29
+ def handle_declarative_format(name, description)
30
+ tool_def = {
31
+ 'name' => name,
32
+ 'description' => extract_description(description)
33
+ }
34
+
35
+ # Extract tool-level metadata
36
+ extract_tool_metadata(tool_def)
37
+
38
+ # Extract parameters
39
+ tool_def['parameters'] = extract_parameters
40
+
41
+ # Extract tool-level examples
42
+ extract_tool_examples(tool_def)
43
+
44
+ # Register the tool
45
+ @context.tools ||= []
46
+ @context.tools << tool_def
47
+ end
48
+
49
+ def extract_tool_metadata(tool_def)
50
+ # Extract version, category, requires_auth, etc.
51
+ %w[version category requires_auth deprecated].each do |attr|
52
+ element = @element.children.find { |child| child.tag_name.to_s == attr }
53
+ if element
54
+ content = element.content&.strip
55
+ if attr == 'requires_auth' || attr == 'deprecated'
56
+ # Convert to boolean
57
+ tool_def[attr] = content == 'true'
58
+ else
59
+ tool_def[attr] = content
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def extract_tool_examples(tool_def)
66
+ example_elements = @element.children.select { |child| child.tag_name.to_s == 'example' }
67
+ return if example_elements.empty?
68
+
69
+ if example_elements.length == 1
70
+ # Single example
71
+ tool_def['example'] = parse_example_element(example_elements.first)
72
+ else
73
+ # Multiple examples
74
+ tool_def['examples'] = example_elements.map { |elem| parse_example_element(elem) }
75
+ end
76
+ end
77
+
78
+ def parse_example_element(example_element)
79
+ example = {}
80
+
81
+ # Check for title and description
82
+ title_elem = example_element.children.find { |child| child.tag_name.to_s == 'title' }
83
+ description_elem = example_element.children.find { |child| child.tag_name.to_s == 'description' }
84
+ parameters_elem = example_element.children.find { |child| child.tag_name.to_s == 'parameters' }
85
+
86
+ example['title'] = title_elem.content&.strip if title_elem
87
+ example['description'] = description_elem.content&.strip if description_elem
88
+
89
+ if parameters_elem
90
+ # Parse JSON parameters
91
+ content = parameters_elem.content.strip
92
+ begin
93
+ # Clean up the JSON content - remove extra whitespace and normalize
94
+ cleaned_content = content.gsub(/\n\s*/, ' ').gsub(/\s+/, ' ')
95
+ example['parameters'] = JSON.parse(cleaned_content)
96
+ rescue JSON::ParserError
97
+ # If JSON parsing fails, try to clean it up more aggressively
98
+ begin
99
+ # Replace problematic characters and try again
100
+ fixed_content = content.gsub(/\n/, '\\n').gsub(/\r/, '\\r').gsub(/\t/, '\\t')
101
+ example['parameters'] = JSON.parse(fixed_content)
102
+ rescue JSON::ParserError
103
+ # If still failing, store as string
104
+ example['parameters'] = content
105
+ end
106
+ end
107
+ end
108
+
109
+ example
110
+ end
111
+
112
+ def handle_json_format(name, description)
113
+ # Support both old 'lang' and new 'parser' attributes for compatibility
114
+ parser_attr = get_attribute('parser') || get_attribute('lang', 'auto')
115
+
14
116
  content = @element.content.strip
15
117
 
16
118
  # Auto-detect format if parser_attr is auto
@@ -32,23 +134,156 @@ module Poml
32
134
 
33
135
  if schema
34
136
  @context.tools ||= []
137
+
138
+ # Extract description from JSON if not provided as attribute
139
+ json_description = nil
140
+ if schema.is_a?(Hash) && schema['description']
141
+ json_description = schema['description']
142
+ end
143
+
144
+ # Use attribute description first, then JSON description, then nil
145
+ final_description = description || json_description
146
+
35
147
  # Store tool with string keys for JSON compatibility
36
148
  tool_def = {
37
149
  'name' => name,
38
- 'description' => description,
150
+ 'description' => final_description,
39
151
  'schema' => schema.is_a?(String) ? schema : JSON.generate(schema)
40
152
  }
41
153
 
42
154
  # For compatibility with expected structure, also store as parameters
43
155
  if schema.is_a?(Hash)
44
- tool_def['parameters'] = schema
156
+ # Remove description from parameters if it was moved to top level
157
+ params = schema.dup
158
+ params.delete('description')
159
+
160
+ # If the schema has a 'parameters' key, extract it
161
+ if params['parameters']
162
+ tool_def['parameters'] = params['parameters']
163
+ else
164
+ tool_def['parameters'] = params
165
+ end
45
166
  end
46
167
 
47
168
  @context.tools << tool_def
48
169
  end
170
+ end
171
+
172
+ def extract_description(fallback_description = nil)
173
+ description_element = @element.children.find { |child| child.tag_name.to_s == 'description' }
174
+ return description_element&.content&.strip if description_element
49
175
 
50
- # Meta-like components don't produce output
51
- ''
176
+ # Fallback to attribute description
177
+ fallback_description || ''
178
+ end
179
+
180
+ def extract_parameters
181
+ parameter_elements = @element.children.select { |child| child.tag_name.to_s == 'parameter' }
182
+
183
+ if parameter_elements.empty?
184
+ # If no parameter elements, return empty object
185
+ return {}
186
+ end
187
+
188
+ parameters = {}
189
+
190
+ parameter_elements.each do |param_element|
191
+ param_name = param_element.attributes['name']
192
+ next unless param_name
193
+
194
+ param_type = param_element.attributes['type'] || 'string'
195
+ param_required = param_element.attributes['required']
196
+ param_description = extract_parameter_description(param_element)
197
+
198
+ param_def = {
199
+ 'type' => param_type,
200
+ 'required' => param_required == 'true' || param_required == true
201
+ }
202
+
203
+ param_def['description'] = param_description if param_description && !param_description.empty?
204
+
205
+ # Handle enum values
206
+ enum_element = param_element.children.find { |child| child.tag_name.to_s == 'enum' }
207
+ if enum_element
208
+ values = enum_element.children.select { |child| child.tag_name.to_s == 'value' }
209
+ .map(&:content)
210
+ .compact
211
+ param_def['enum'] = values unless values.empty?
212
+ end
213
+
214
+ # Handle array items
215
+ items_element = param_element.children.find { |child| child.tag_name.to_s == 'items' }
216
+ if items_element && param_type == 'array'
217
+ items_type = items_element.attributes['type'] || 'string'
218
+ items_description = extract_parameter_description(items_element)
219
+
220
+ items_def = { 'type' => items_type }
221
+ items_def['description'] = items_description if items_description && !items_description.empty?
222
+
223
+ # Handle example in items
224
+ items_example_elem = items_element.children.find { |child| child.tag_name.to_s == 'example' }
225
+ if items_example_elem
226
+ items_def['example'] = items_example_elem.content&.strip
227
+ end
228
+
229
+ param_def['items'] = items_def
230
+ end
231
+
232
+ # Handle parameter examples
233
+ example_element = param_element.children.find { |child| child.tag_name.to_s == 'example' }
234
+ if example_element
235
+ param_def['example'] = example_element.content&.strip
236
+ end
237
+
238
+ # Handle object properties
239
+ properties_element = param_element.children.find { |child| child.tag_name.to_s == 'properties' }
240
+ if properties_element && param_type == 'object'
241
+ properties = {}
242
+
243
+ property_elements = properties_element.children.select { |child| child.tag_name.to_s == 'property' }
244
+ property_elements.each do |prop_element|
245
+ prop_name = prop_element.attributes['name']
246
+ next unless prop_name
247
+
248
+ prop_type = prop_element.attributes['type'] || 'string'
249
+ prop_description = extract_parameter_description(prop_element)
250
+
251
+ prop_def = { 'type' => prop_type }
252
+ prop_def['description'] = prop_description if prop_description && !prop_description.empty?
253
+
254
+ properties[prop_name] = prop_def
255
+ end
256
+
257
+ param_def['properties'] = properties unless properties.empty?
258
+ end
259
+
260
+ # Handle schema references (raw JSON schema)
261
+ schema_element = param_element.children.find { |child| child.tag_name.to_s == 'schema' }
262
+ if schema_element
263
+ schema_content = schema_element.content.strip
264
+ begin
265
+ # Try to parse as JSON to validate
266
+ parsed_schema = JSON.parse(schema_content)
267
+ param_def['schema'] = parsed_schema
268
+ rescue JSON::ParserError
269
+ # If invalid JSON, store as string
270
+ param_def['schema'] = schema_content
271
+ end
272
+ end
273
+
274
+ parameters[param_name] = param_def
275
+ end
276
+
277
+ parameters
278
+ end
279
+
280
+ def extract_parameter_description(element)
281
+ # Check for description child element first
282
+ description_child = element.children.find { |child| child.tag_name.to_s == 'description' }
283
+ return description_child.content&.strip if description_child
284
+
285
+ # Fallback to element content if no description child
286
+ element.content&.strip
52
287
  end
53
288
 
54
289
  private
@@ -59,8 +294,9 @@ module Poml
59
294
 
60
295
  begin
61
296
  parsed = JSON.parse(substituted_content)
62
- # Return the parsed schema directly - don't wrap it
63
- parsed
297
+ # Apply enhanced tool registration features
298
+ enhanced_schema = apply_tool_enhancements(parsed)
299
+ enhanced_schema
64
300
  rescue JSON::ParserError => e
65
301
  raise Poml::Error, "Invalid JSON schema for tool '#{get_attribute('name')}': #{e.message}"
66
302
  end
@@ -76,8 +312,9 @@ module Poml
76
312
  # otherwise treat it as a placeholder
77
313
  if substituted_content.strip.start_with?('{', '[')
78
314
  parsed = JSON.parse(substituted_content)
79
- # Return the parsed schema directly - don't wrap it
80
- parsed
315
+ # Apply enhanced tool registration features
316
+ enhanced_schema = apply_tool_enhancements(parsed)
317
+ enhanced_schema
81
318
  else
82
319
  # This would need a JavaScript engine in a real implementation
83
320
  # For now, return a placeholder that indicates expression evaluation
@@ -94,5 +331,97 @@ module Poml
94
331
  }
95
332
  end
96
333
  end
334
+
335
+ # Apply enhanced tool registration features
336
+ def apply_tool_enhancements(schema)
337
+ return schema unless schema.is_a?(Hash)
338
+
339
+ # Apply parameter key conversion and runtime type conversion
340
+ enhanced_schema = convert_parameter_keys(schema)
341
+ enhanced_schema = apply_runtime_parameter_conversion(enhanced_schema)
342
+ enhanced_schema
343
+ end
344
+
345
+ # Convert kebab-case keys to camelCase recursively
346
+ def convert_parameter_keys(obj)
347
+ case obj
348
+ when Hash
349
+ converted = {}
350
+ obj.each do |key, value|
351
+ # Convert kebab-case to camelCase
352
+ new_key = kebab_to_camel_case(key.to_s)
353
+
354
+ # Special handling for 'required' array
355
+ if key == 'required' && value.is_a?(Array)
356
+ converted[new_key] = value.map { |req_key| kebab_to_camel_case(req_key.to_s) }
357
+ else
358
+ converted[new_key] = convert_parameter_keys(value)
359
+ end
360
+ end
361
+ converted
362
+ when Array
363
+ obj.map { |item| convert_parameter_keys(item) }
364
+ else
365
+ obj
366
+ end
367
+ end
368
+
369
+ # Convert kebab-case string to camelCase
370
+ def kebab_to_camel_case(str)
371
+ # Split on hyphens and convert to camelCase
372
+ parts = str.split('-')
373
+ return str if parts.length == 1 # No conversion needed
374
+
375
+ parts[0] + parts[1..-1].map(&:capitalize).join
376
+ end
377
+
378
+ # Apply runtime parameter type conversion
379
+ def apply_runtime_parameter_conversion(schema)
380
+ return schema unless schema.is_a?(Hash)
381
+
382
+ converted = schema.dup
383
+
384
+ # Process properties if they exist
385
+ if converted['properties'].is_a?(Hash)
386
+ converted['properties'] = converted['properties'].map do |key, prop|
387
+ [key, convert_property_value(prop)]
388
+ end.to_h
389
+ end
390
+
391
+ converted
392
+ end
393
+
394
+ # Convert individual property values based on 'convert' attribute
395
+ def convert_property_value(property)
396
+ return property unless property.is_a?(Hash) && property['convert']
397
+
398
+ converted_prop = property.dup
399
+ convert_value = property['convert']
400
+ prop_type = property['type']
401
+
402
+ begin
403
+ case prop_type
404
+ when 'boolean'
405
+ # Convert various truthy/falsy values to boolean
406
+ case convert_value.to_s.downcase
407
+ when 'true', '1', 'yes', 'on'
408
+ # Keep the convert attribute for now but could be removed
409
+ when 'false', '0', 'no', 'off'
410
+ # Keep the convert attribute for now but could be removed
411
+ end
412
+ when 'number'
413
+ # Validate that the value can be converted to a number
414
+ Float(convert_value) # This will raise an exception if invalid
415
+ when 'object', 'array'
416
+ # Validate that the value is valid JSON
417
+ JSON.parse(convert_value) # This will raise an exception if invalid
418
+ end
419
+ rescue StandardError
420
+ # If conversion fails, remove the convert attribute silently
421
+ converted_prop.delete('convert')
422
+ end
423
+
424
+ converted_prop
425
+ end
97
426
  end
98
427
  end
@@ -0,0 +1,14 @@
1
+ module Poml
2
+ # Tools wrapper component that contains tool definitions
3
+ class ToolsComponent < Component
4
+ def render
5
+ # Process child tool elements
6
+ @element.children.each do |child|
7
+ Components.render_element(child, @context)
8
+ end
9
+
10
+ # Tools components don't produce output by default
11
+ ''
12
+ end
13
+ end
14
+ end
@@ -4,7 +4,8 @@ module Poml
4
4
  def render
5
5
  apply_stylesheet
6
6
 
7
- content = @element.content.empty? ? render_children : @element.content
7
+ # Always use render_children if there are child elements, otherwise use content
8
+ content = @element.children.empty? ? @element.content : render_children
8
9
 
9
10
  # Add to structured chat messages if context supports it
10
11
  if @context.respond_to?(:chat_messages)
@@ -12,14 +13,18 @@ module Poml
12
13
  'role' => 'assistant',
13
14
  'content' => content
14
15
  }
15
- # Return empty for raw format to avoid duplication
16
- return ''
17
16
  end
18
17
 
19
18
  if xml_mode?
20
19
  render_as_xml('ai-msg', content, { speaker: 'ai' })
21
20
  else
22
- content
21
+ case @context.output_format
22
+ when 'raw'
23
+ # In raw format, return content only if chat mode is disabled
24
+ @context.chat ? "" : content
25
+ else
26
+ content
27
+ end
23
28
  end
24
29
  end
25
30
  end
@@ -29,7 +34,8 @@ module Poml
29
34
  def render
30
35
  apply_stylesheet
31
36
 
32
- content = @element.content.empty? ? render_children : @element.content
37
+ # Always use render_children if there are child elements, otherwise use content
38
+ content = @element.children.empty? ? @element.content : render_children
33
39
 
34
40
  # Add to structured chat messages if context supports it
35
41
  if @context.respond_to?(:chat_messages)
@@ -37,14 +43,18 @@ module Poml
37
43
  'role' => 'user',
38
44
  'content' => content
39
45
  }
40
- # Return empty for raw format to avoid duplication
41
- return ''
42
46
  end
43
47
 
44
48
  if xml_mode?
45
49
  render_as_xml('user-msg', content, { speaker: 'human' })
46
50
  else
47
- content
51
+ case @context.output_format
52
+ when 'raw'
53
+ # In raw format, return content only if chat mode is disabled
54
+ @context.chat ? "" : content
55
+ else
56
+ content
57
+ end
48
58
  end
49
59
  end
50
60
  end
@@ -54,7 +64,8 @@ module Poml
54
64
  def render
55
65
  apply_stylesheet
56
66
 
57
- content = @element.content.empty? ? render_children : @element.content
67
+ # Always use render_children if there are child elements, otherwise use content
68
+ content = @element.children.empty? ? @element.content : render_children
58
69
 
59
70
  # Add to structured chat messages if context supports it
60
71
  if @context.respond_to?(:chat_messages)
@@ -62,14 +73,18 @@ module Poml
62
73
  'role' => 'system',
63
74
  'content' => content
64
75
  }
65
- # Return empty for raw format to avoid duplication
66
- return ''
67
76
  end
68
77
 
69
78
  if xml_mode?
70
79
  render_as_xml('system-msg', content, { speaker: 'system' })
71
80
  else
72
- content
81
+ case @context.output_format
82
+ when 'raw'
83
+ # In raw format, return content only if chat mode is disabled
84
+ @context.chat ? "" : content
85
+ else
86
+ content
87
+ end
73
88
  end
74
89
  end
75
90
  end
@@ -94,7 +109,7 @@ module Poml
94
109
  end
95
110
  end
96
111
 
97
- # Conversation component for displaying chat conversations
112
+ # Conversation component for formatting chat messages (matches original POML API)
98
113
  class ConversationComponent < Component
99
114
  def render
100
115
  apply_stylesheet
@@ -124,7 +139,7 @@ module Poml
124
139
  render_conversation_markdown(messages)
125
140
  end
126
141
  end
127
-
142
+
128
143
  private
129
144
 
130
145
  def apply_message_selection(messages, selection)
@@ -187,7 +202,8 @@ module Poml
187
202
  end
188
203
  result << ""
189
204
  end
190
- result.join("\n")
205
+ result.join("
206
+ ")
191
207
  end
192
208
 
193
209
  def escape_xml(text)
@@ -271,10 +287,10 @@ module Poml
271
287
 
272
288
  if show_content
273
289
  begin
274
- content = File.read(full_path, encoding: 'utf-8')
290
+ content = read_file_with_encoding(full_path)
275
291
  item[:content] = content
276
- rescue
277
- item[:content] = '[Binary file or read error]'
292
+ rescue => e
293
+ item[:content] = "[Error reading file: #{e.message}]"
278
294
  end
279
295
  end
280
296
 
@@ -13,6 +13,7 @@ require_relative 'components/formatting'
13
13
  require_relative 'components/media'
14
14
  require_relative 'components/utilities'
15
15
  require_relative 'components/meta'
16
+ require_relative 'components/tools'
16
17
  require_relative 'components/template'
17
18
  require_relative 'components/output_schema'
18
19
  require_relative 'components/tool_definition'
@@ -38,10 +39,21 @@ module Poml
38
39
  span: InlineComponent,
39
40
  inline: InlineComponent,
40
41
  h: HeaderComponent,
42
+ h1: HeaderComponent,
43
+ h2: HeaderComponent,
44
+ h3: HeaderComponent,
45
+ h4: HeaderComponent,
46
+ h5: HeaderComponent,
47
+ h6: HeaderComponent,
41
48
  header: HeaderComponent,
42
49
  br: NewlineComponent,
43
50
  newline: NewlineComponent,
44
51
  code: CodeComponent,
52
+ 'code-block': CodeBlockComponent,
53
+ 'codeblock': CodeBlockComponent,
54
+ callout: CalloutComponent,
55
+ blockquote: BlockquoteComponent,
56
+ quote: BlockquoteComponent,
45
57
  section: SubContentComponent,
46
58
  subcontent: SubContentComponent,
47
59
 
@@ -79,6 +91,7 @@ module Poml
79
91
 
80
92
  # List components
81
93
  list: ListComponent,
94
+ 'numbered-list': ListComponent, # alias for numbered lists
82
95
  item: ItemComponent,
83
96
 
84
97
  # Layout components
@@ -128,9 +141,15 @@ module Poml
128
141
  # Meta components
129
142
  meta: MetaComponent,
130
143
  Meta: MetaComponent,
144
+
145
+ # Tools components
146
+ tools: ToolsComponent,
147
+ Tools: ToolsComponent,
131
148
  'output-schema': OutputSchemaComponent,
132
149
  'outputschema': OutputSchemaComponent,
133
150
  OutputSchema: OutputSchemaComponent,
151
+ schema: OutputSchemaComponent, # 'schema' is an alias for 'output-schema'
152
+ Schema: OutputSchemaComponent,
134
153
  'tool-definition': ToolDefinitionComponent,
135
154
  'tooldefinition': ToolDefinitionComponent,
136
155
  ToolDefinition: ToolDefinitionComponent,
data/lib/poml/context.rb CHANGED
@@ -5,15 +5,15 @@ module Poml
5
5
  # Context object that holds variables, stylesheets, and processing state
6
6
  class Context
7
7
  attr_accessor :variables, :stylesheet, :chat, :texts, :source_path, :syntax, :header_level
8
- attr_accessor :response_schema, :tools, :runtime_parameters, :disabled_components
9
- attr_accessor :template_engine, :chat_messages, :custom_metadata
8
+ attr_accessor :response_schema, :response_schema_with_metadata, :tools, :runtime_parameters, :disabled_components
9
+ attr_accessor :template_engine, :chat_messages, :custom_metadata, :output_format, :output_content
10
10
 
11
- def initialize(variables: {}, stylesheet: nil, chat: true, syntax: nil)
11
+ def initialize(variables: {}, stylesheet: nil, chat: true, syntax: nil, source_path: nil, output_format: nil)
12
12
  @variables = variables || {}
13
13
  @stylesheet = parse_stylesheet(stylesheet)
14
14
  @chat = chat
15
15
  @texts = {}
16
- @source_path = nil
16
+ @source_path = source_path
17
17
  @syntax = syntax
18
18
  @header_level = 1 # Track current header nesting level
19
19
  @response_schema = nil
@@ -23,6 +23,8 @@ module Poml
23
23
  @template_engine = TemplateEngine.new(self)
24
24
  @chat_messages = [] # Track structured chat messages
25
25
  @custom_metadata = {} # Track general metadata like title, description etc.
26
+ @output_format = output_format # Track target output format for component behavior
27
+ @output_content = nil # Track content from <output> components
26
28
  end
27
29
 
28
30
  def xml_mode?
@@ -65,6 +67,19 @@ module Poml
65
67
  child.disabled_components = @disabled_components.dup
66
68
  child.chat_messages = @chat_messages # Share the same array reference
67
69
  child.custom_metadata = @custom_metadata # Share the same hash reference
70
+ child.output_format = @output_format # Copy output format
71
+ child.output_content = @output_content # Copy output content
72
+ child.template_engine = @template_engine # Copy template engine
73
+
74
+ # Copy instance variables that components may set (like list context)
75
+ instance_variables.each do |var|
76
+ unless [:@variables, :@stylesheet, :@chat, :@syntax, :@header_level,
77
+ :@response_schema, :@tools, :@runtime_parameters, :@disabled_components,
78
+ :@chat_messages, :@custom_metadata, :@output_format, :@output_content, :@template_engine].include?(var)
79
+ child.instance_variable_set(var, instance_variable_get(var))
80
+ end
81
+ end
82
+
68
83
  child
69
84
  end
70
85