solid_agent 0.0.0 → 0.2.0

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +21 -0
  4. data/README.md +321 -0
  5. data/Rakefile +32 -0
  6. data/docs/agent-md-spec.md +803 -0
  7. data/docs/parser-design.md +1369 -0
  8. data/docs/registry-api.md +882 -0
  9. data/examples/README.md +60 -0
  10. data/examples/manifests/changelog_writer.agent.md +81 -0
  11. data/examples/manifests/usage.rb +96 -0
  12. data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
  13. data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
  14. data/examples/memory_handoff/usage.rb +45 -0
  15. data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
  16. data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
  17. data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
  18. data/examples/persistent_conversation/usage.rb +51 -0
  19. data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
  20. data/examples/reasoning/usage.rb +52 -0
  21. data/examples/run_tracking/app/agents/report_agent.rb +30 -0
  22. data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
  23. data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
  24. data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
  25. data/examples/run_tracking/usage.rb +85 -0
  26. data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
  27. data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
  28. data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
  29. data/examples/tool_streaming/usage.rb +47 -0
  30. data/lib/generators/solid_agent/agent/agent_generator.rb +95 -0
  31. data/lib/generators/solid_agent/agent/templates/action.text.erb +10 -0
  32. data/lib/generators/solid_agent/agent/templates/agent.rb.erb +93 -0
  33. data/lib/generators/solid_agent/context/context_generator.rb +124 -0
  34. data/lib/generators/solid_agent/context/templates/context_model.rb.erb +134 -0
  35. data/lib/generators/solid_agent/context/templates/create_context.rb.erb +32 -0
  36. data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +46 -0
  37. data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +37 -0
  38. data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +51 -0
  39. data/lib/generators/solid_agent/context/templates/message_model.rb.erb +47 -0
  40. data/lib/generators/solid_agent/install/install_generator.rb +92 -0
  41. data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +171 -0
  42. data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +76 -0
  43. data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
  44. data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
  45. data/lib/generators/solid_agent/install/templates/agent_message.rb.erb +76 -0
  46. data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
  47. data/lib/generators/solid_agent/install/templates/create_agent_contexts.rb.erb +32 -0
  48. data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +51 -0
  49. data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
  50. data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +38 -0
  51. data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
  52. data/lib/generators/solid_agent/install/templates/initializer.rb.erb +51 -0
  53. data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
  54. data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
  55. data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
  56. data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
  57. data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
  58. data/lib/generators/solid_agent/tool/templates/tool.json.erb +19 -0
  59. data/lib/generators/solid_agent/tool/tool_generator.rb +117 -0
  60. data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
  61. data/lib/solid_agent/agent_manifest/errors.rb +26 -0
  62. data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
  63. data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
  64. data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
  65. data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
  66. data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
  67. data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
  68. data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
  69. data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
  70. data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
  71. data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
  72. data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
  73. data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
  74. data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
  75. data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
  76. data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
  77. data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
  78. data/lib/solid_agent/agent_manifest/resource.rb +103 -0
  79. data/lib/solid_agent/agent_manifest/tool.rb +160 -0
  80. data/lib/solid_agent/agent_manifest/validator.rb +368 -0
  81. data/lib/solid_agent/agent_manifest.rb +381 -0
  82. data/lib/solid_agent/engine.rb +16 -0
  83. data/lib/solid_agent/has_context.rb +670 -0
  84. data/lib/solid_agent/has_memory.rb +136 -0
  85. data/lib/solid_agent/has_reasons.rb +230 -0
  86. data/lib/solid_agent/has_tools.rb +257 -0
  87. data/lib/solid_agent/model_naming.rb +42 -0
  88. data/lib/solid_agent/model_pricing.rb +93 -0
  89. data/lib/solid_agent/reasonable/reason.rb +205 -0
  90. data/lib/solid_agent/reasonable.rb +181 -0
  91. data/lib/solid_agent/records/agent.rb +520 -0
  92. data/lib/solid_agent/records/agent_run.rb +520 -0
  93. data/lib/solid_agent/records/agent_template.rb +142 -0
  94. data/lib/solid_agent/records/agent_version.rb +141 -0
  95. data/lib/solid_agent/records/ownable.rb +130 -0
  96. data/lib/solid_agent/records.rb +152 -0
  97. data/lib/solid_agent/run_fingerprint.rb +51 -0
  98. data/lib/solid_agent/streams_tool_updates.rb +178 -0
  99. data/lib/solid_agent/tool_cache.rb +91 -0
  100. data/lib/solid_agent/version.rb +5 -0
  101. data/lib/solid_agent.rb +95 -0
  102. data/sig/solid_agent.rbs +4 -0
  103. metadata +174 -14
@@ -0,0 +1,1369 @@
1
+ # Agent Parser Design
2
+
3
+ A modular parser architecture that supports multiple agent definition formats with a unified internal representation.
4
+
5
+ ## Architecture Overview
6
+
7
+ ```
8
+ ┌─────────────────────────────────────────────────────────────────────┐
9
+ │ AgentManifest │
10
+ │ (Unified Internal Model) │
11
+ └─────────────────────────────────────────────────────────────────────┘
12
+
13
+
14
+ ┌───────────────────────┼───────────────────────┐
15
+ │ │ │
16
+ │ │ │
17
+ ┌────────┴────────┐ ┌─────────┴─────────┐ ┌────────┴────────┐
18
+ │ AgentMdParser │ │ DotpromptParser │ │ CrewAIParser │
19
+ │ (.agent.md) │ │ (.prompt) │ │ (agents.yaml) │
20
+ └─────────────────┘ └───────────────────┘ └─────────────────┘
21
+ │ │ │
22
+ │ │ │
23
+ ┌────────┴────────┐ ┌─────────┴─────────┐ ┌────────┴────────┐
24
+ │ Format A │ │ Format B │ │ Format C │
25
+ │ .agent.md │ │ .prompt │ │ agents.yaml │
26
+ └─────────────────┘ └───────────────────┘ └─────────────────┘
27
+
28
+
29
+
30
+ ┌───────────────────────┼───────────────────────┐
31
+ │ │ │
32
+ ┌────────┴────────┐ ┌─────────┴─────────┐ ┌────────┴────────┐
33
+ │ ActiveAgent │ │ CrewAI │ │ LangChain │
34
+ │ Exporter │ │ Exporter │ │ Exporter │
35
+ └─────────────────┘ └───────────────────┘ └─────────────────┘
36
+ ```
37
+
38
+ ## Core Components
39
+
40
+ ### 1. AgentManifest (Unified Model)
41
+
42
+ The canonical internal representation that all parsers produce and exporters consume:
43
+
44
+ ```ruby
45
+ # lib/agent_manifest/manifest.rb
46
+ module AgentManifest
47
+ class Manifest
48
+ attr_accessor :name, :version, :description, :author, :license,
49
+ :repository, :tags, :extends
50
+
51
+ attr_accessor :model, :config
52
+
53
+ attr_accessor :input_schema, :output_schema
54
+
55
+ attr_accessor :tools, :resources
56
+
57
+ attr_accessor :instructions, :template
58
+
59
+ attr_accessor :extensions # Framework-specific: { activeagent: {}, crewai: {} }
60
+
61
+ attr_accessor :examples, :tests
62
+
63
+ attr_accessor :source_format, :source_path # For round-trip preservation
64
+
65
+ def initialize(attributes = {})
66
+ attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
67
+ @extensions ||= {}
68
+ @tools ||= []
69
+ @resources ||= []
70
+ @tags ||= []
71
+ @examples ||= []
72
+ @tests ||= []
73
+ end
74
+
75
+ def to_h
76
+ {
77
+ name: name,
78
+ version: version,
79
+ description: description,
80
+ author: author,
81
+ license: license,
82
+ repository: repository,
83
+ tags: tags,
84
+ extends: extends,
85
+ model: model,
86
+ config: config,
87
+ input_schema: input_schema,
88
+ output_schema: output_schema,
89
+ tools: tools.map(&:to_h),
90
+ resources: resources.map(&:to_h),
91
+ instructions: instructions,
92
+ template: template,
93
+ extensions: extensions
94
+ }.compact
95
+ end
96
+
97
+ # Convenience accessors for framework extensions
98
+ def activeagent_config
99
+ extensions[:activeagent] || {}
100
+ end
101
+
102
+ def crewai_config
103
+ extensions[:crewai] || {}
104
+ end
105
+
106
+ def langchain_config
107
+ extensions[:langchain] || {}
108
+ end
109
+ end
110
+
111
+ class Tool
112
+ attr_accessor :name, :description, :input_schema, :ref
113
+
114
+ def initialize(attributes = {})
115
+ attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
116
+ end
117
+
118
+ def reference?
119
+ ref.present?
120
+ end
121
+
122
+ def to_h
123
+ if reference?
124
+ { "$ref" => ref }
125
+ else
126
+ {
127
+ name: name,
128
+ description: description,
129
+ inputSchema: input_schema
130
+ }.compact
131
+ end
132
+ end
133
+
134
+ # MCP-compatible JSON output
135
+ def to_mcp_json
136
+ {
137
+ name: name,
138
+ description: description,
139
+ inputSchema: input_schema
140
+ }.to_json
141
+ end
142
+ end
143
+
144
+ class Resource
145
+ attr_accessor :name, :description, :uri, :mime_type
146
+
147
+ def initialize(attributes = {})
148
+ attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
149
+ end
150
+
151
+ def to_h
152
+ {
153
+ name: name,
154
+ description: description,
155
+ uri: uri,
156
+ mimeType: mime_type
157
+ }.compact
158
+ end
159
+ end
160
+
161
+ class InputSchema
162
+ attr_accessor :schema, :format # format: :picoschema or :json_schema
163
+
164
+ def initialize(schema:, format: :picoschema)
165
+ @schema = schema
166
+ @format = format
167
+ end
168
+
169
+ def to_json_schema
170
+ case format
171
+ when :picoschema
172
+ PicoschemaParser.to_json_schema(schema)
173
+ when :json_schema
174
+ schema
175
+ end
176
+ end
177
+
178
+ def to_picoschema
179
+ case format
180
+ when :picoschema
181
+ schema
182
+ when :json_schema
183
+ PicoschemaParser.from_json_schema(schema)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ ```
189
+
190
+ ### 2. Parser Registry
191
+
192
+ ```ruby
193
+ # lib/agent_manifest/parser_registry.rb
194
+ module AgentManifest
195
+ class ParserRegistry
196
+ class << self
197
+ def parsers
198
+ @parsers ||= {}
199
+ end
200
+
201
+ def register(format, parser_class)
202
+ parsers[format.to_sym] = parser_class
203
+ end
204
+
205
+ def parser_for(format)
206
+ parsers[format.to_sym] || raise(UnknownFormatError, "No parser for format: #{format}")
207
+ end
208
+
209
+ def detect_format(path)
210
+ extension = File.extname(path).downcase
211
+ case extension
212
+ when '.md'
213
+ # Could be .agent.md or regular markdown
214
+ path.end_with?('.agent.md') ? :agent_md : :markdown
215
+ when '.prompt'
216
+ :dotprompt
217
+ when '.yaml', '.yml'
218
+ detect_yaml_format(path)
219
+ when '.json'
220
+ detect_json_format(path)
221
+ else
222
+ raise UnknownFormatError, "Cannot detect format for: #{path}"
223
+ end
224
+ end
225
+
226
+ def parse(path, format: nil)
227
+ format ||= detect_format(path)
228
+ parser_for(format).parse(path)
229
+ end
230
+
231
+ def parse_string(content, format:)
232
+ parser_for(format).parse_string(content)
233
+ end
234
+
235
+ private
236
+
237
+ def detect_yaml_format(path)
238
+ content = File.read(path)
239
+ if content.include?('role:') && content.include?('goal:')
240
+ :crewai
241
+ elsif content.include?('has_context') || content.include?('activeagent')
242
+ :activeagent_yaml
243
+ else
244
+ :yaml
245
+ end
246
+ end
247
+
248
+ def detect_json_format(path)
249
+ content = JSON.parse(File.read(path))
250
+ if content['inputSchema']
251
+ :mcp_tool
252
+ elsif content['main'] && content['files']
253
+ :agent_package
254
+ else
255
+ :json
256
+ end
257
+ end
258
+ end
259
+ end
260
+
261
+ class UnknownFormatError < StandardError; end
262
+ end
263
+ ```
264
+
265
+ ### 3. Base Parser
266
+
267
+ ```ruby
268
+ # lib/agent_manifest/parsers/base_parser.rb
269
+ module AgentManifest
270
+ module Parsers
271
+ class BaseParser
272
+ class << self
273
+ def parse(path)
274
+ content = File.read(path)
275
+ manifest = parse_string(content)
276
+ manifest.source_path = path
277
+ manifest.source_format = format_name
278
+ manifest
279
+ end
280
+
281
+ def parse_string(content)
282
+ raise NotImplementedError, "Subclasses must implement parse_string"
283
+ end
284
+
285
+ def format_name
286
+ raise NotImplementedError, "Subclasses must implement format_name"
287
+ end
288
+
289
+ protected
290
+
291
+ def normalize_model(model_string)
292
+ # Normalize model identifiers to provider/model format
293
+ return nil unless model_string
294
+
295
+ if model_string.include?('/')
296
+ model_string
297
+ elsif model_string.start_with?('gpt-')
298
+ "openai/#{model_string}"
299
+ elsif model_string.start_with?('claude-')
300
+ "anthropic/#{model_string}"
301
+ elsif model_string.start_with?('gemini-')
302
+ "google/#{model_string}"
303
+ else
304
+ model_string
305
+ end
306
+ end
307
+
308
+ def parse_tools(tools_data)
309
+ return [] unless tools_data
310
+
311
+ tools_data.map do |tool_data|
312
+ if tool_data.is_a?(String)
313
+ Tool.new(ref: tool_data)
314
+ elsif tool_data['$ref']
315
+ Tool.new(ref: tool_data['$ref'])
316
+ else
317
+ Tool.new(
318
+ name: tool_data['name'],
319
+ description: tool_data['description'],
320
+ input_schema: tool_data['inputSchema'] || tool_data['input_schema']
321
+ )
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
329
+ ```
330
+
331
+ ## Format Parsers
332
+
333
+ ### 4. AgentMd Parser (.agent.md)
334
+
335
+ ```ruby
336
+ # lib/agent_manifest/parsers/agent_md_parser.rb
337
+ require 'yaml'
338
+
339
+ module AgentManifest
340
+ module Parsers
341
+ class AgentMdParser < BaseParser
342
+ FRONTMATTER_REGEX = /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
343
+
344
+ class << self
345
+ def format_name
346
+ :agent_md
347
+ end
348
+
349
+ def parse_string(content)
350
+ frontmatter, body = extract_frontmatter(content)
351
+
352
+ Manifest.new(
353
+ # Meta
354
+ name: frontmatter['name'],
355
+ version: frontmatter['version'] || '1.0.0',
356
+ description: frontmatter['description'],
357
+ author: frontmatter['author'],
358
+ license: frontmatter['license'],
359
+ repository: frontmatter['repository'],
360
+ tags: frontmatter['tags'] || [],
361
+ extends: frontmatter['extends'],
362
+
363
+ # Model
364
+ model: normalize_model(frontmatter['model']),
365
+ config: frontmatter['config'] || {},
366
+
367
+ # Schemas
368
+ input_schema: parse_schema(frontmatter['input']),
369
+ output_schema: parse_schema(frontmatter['output']),
370
+
371
+ # Tools & Resources
372
+ tools: parse_tools(frontmatter['tools']),
373
+ resources: parse_resources(frontmatter['resources']),
374
+
375
+ # Instructions
376
+ instructions: extract_instructions(body),
377
+ template: body,
378
+
379
+ # Extensions
380
+ extensions: extract_extensions(frontmatter)
381
+ )
382
+ end
383
+
384
+ private
385
+
386
+ def extract_frontmatter(content)
387
+ match = content.match(FRONTMATTER_REGEX)
388
+ raise ParseError, "Invalid .agent.md format: missing frontmatter" unless match
389
+
390
+ frontmatter = YAML.safe_load(match[1], permitted_classes: [Symbol])
391
+ body = match[2].strip
392
+
393
+ [frontmatter, body]
394
+ end
395
+
396
+ def parse_schema(schema_data)
397
+ return nil unless schema_data
398
+
399
+ schema = schema_data['schema']
400
+ return nil unless schema
401
+
402
+ # Detect if it's Picoschema or JSON Schema
403
+ if schema.is_a?(Hash) && schema['type']
404
+ InputSchema.new(schema: schema, format: :json_schema)
405
+ else
406
+ InputSchema.new(schema: schema, format: :picoschema)
407
+ end
408
+ end
409
+
410
+ def parse_resources(resources_data)
411
+ return [] unless resources_data
412
+
413
+ resources_data.map do |res|
414
+ Resource.new(
415
+ name: res['name'],
416
+ description: res['description'],
417
+ uri: res['uri'],
418
+ mime_type: res['mimeType']
419
+ )
420
+ end
421
+ end
422
+
423
+ def extract_instructions(body)
424
+ # Extract content from ## Instructions section
425
+ if body =~ /##\s*Instructions\s*\n(.*?)(?=\n##|\z)/mi
426
+ $1.strip
427
+ else
428
+ # Use entire body as instructions if no section found
429
+ body.gsub(/^#\s+.*\n/, '').strip
430
+ end
431
+ end
432
+
433
+ def extract_extensions(frontmatter)
434
+ extensions = {}
435
+ %w[activeagent crewai langchain genkit].each do |framework|
436
+ extensions[framework.to_sym] = frontmatter[framework] if frontmatter[framework]
437
+ end
438
+ extensions
439
+ end
440
+ end
441
+ end
442
+
443
+ # Register the parser
444
+ ParserRegistry.register(:agent_md, AgentMdParser)
445
+ end
446
+ end
447
+ ```
448
+
449
+ ### 5. Dotprompt Parser (.prompt)
450
+
451
+ ```ruby
452
+ # lib/agent_manifest/parsers/dotprompt_parser.rb
453
+ module AgentManifest
454
+ module Parsers
455
+ class DotpromptParser < BaseParser
456
+ FRONTMATTER_REGEX = /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
457
+
458
+ class << self
459
+ def format_name
460
+ :dotprompt
461
+ end
462
+
463
+ def parse_string(content)
464
+ frontmatter, body = extract_frontmatter(content)
465
+
466
+ # Extract name from filename if not in frontmatter
467
+ name = frontmatter['name'] || 'unnamed-prompt'
468
+
469
+ Manifest.new(
470
+ name: name,
471
+ version: frontmatter['version'] || '1.0.0',
472
+
473
+ # Model - Dotprompt format
474
+ model: normalize_model(frontmatter['model']),
475
+ config: extract_config(frontmatter),
476
+
477
+ # Schemas - Dotprompt uses input/output directly
478
+ input_schema: parse_dotprompt_schema(frontmatter['input']),
479
+ output_schema: parse_dotprompt_output(frontmatter['output']),
480
+
481
+ # Tools
482
+ tools: parse_tools(frontmatter['tools']),
483
+
484
+ # Template
485
+ instructions: body,
486
+ template: body,
487
+
488
+ # Store original format info
489
+ extensions: {
490
+ dotprompt: frontmatter.except('model', 'input', 'output', 'tools')
491
+ }
492
+ )
493
+ end
494
+
495
+ private
496
+
497
+ def extract_frontmatter(content)
498
+ match = content.match(FRONTMATTER_REGEX)
499
+ raise ParseError, "Invalid .prompt format: missing frontmatter" unless match
500
+
501
+ [YAML.safe_load(match[1]), match[2].strip]
502
+ end
503
+
504
+ def extract_config(frontmatter)
505
+ config = {}
506
+ config[:temperature] = frontmatter['temperature'] if frontmatter['temperature']
507
+ config[:max_tokens] = frontmatter['maxOutputTokens'] if frontmatter['maxOutputTokens']
508
+ config[:top_p] = frontmatter['topP'] if frontmatter['topP']
509
+ config[:top_k] = frontmatter['topK'] if frontmatter['topK']
510
+ config[:stop] = frontmatter['stopSequences'] if frontmatter['stopSequences']
511
+ config
512
+ end
513
+
514
+ def parse_dotprompt_schema(input_data)
515
+ return nil unless input_data
516
+
517
+ schema = input_data['schema']
518
+ return nil unless schema
519
+
520
+ # Dotprompt uses Picoschema by default
521
+ InputSchema.new(schema: schema, format: :picoschema)
522
+ end
523
+
524
+ def parse_dotprompt_output(output_data)
525
+ return nil unless output_data
526
+
527
+ {
528
+ format: output_data['format'],
529
+ schema: output_data['schema']
530
+ }
531
+ end
532
+ end
533
+ end
534
+
535
+ ParserRegistry.register(:dotprompt, DotpromptParser)
536
+ end
537
+ end
538
+ ```
539
+
540
+ ### 6. CrewAI Parser (agents.yaml)
541
+
542
+ ```ruby
543
+ # lib/agent_manifest/parsers/crewai_parser.rb
544
+ module AgentManifest
545
+ module Parsers
546
+ class CrewAIParser < BaseParser
547
+ class << self
548
+ def format_name
549
+ :crewai
550
+ end
551
+
552
+ def parse_string(content)
553
+ data = YAML.safe_load(content)
554
+
555
+ # CrewAI files contain multiple agents, return array
556
+ agents = data.map do |agent_name, agent_data|
557
+ parse_agent(agent_name, agent_data)
558
+ end
559
+
560
+ # If single agent, return it directly
561
+ agents.size == 1 ? agents.first : agents
562
+ end
563
+
564
+ def parse_file_pair(agents_path, tasks_path = nil)
565
+ agents_content = File.read(agents_path)
566
+ agents = parse_string(agents_content)
567
+
568
+ if tasks_path && File.exist?(tasks_path)
569
+ tasks = YAML.safe_load(File.read(tasks_path))
570
+ # Merge task info into agents
571
+ # (simplified - real implementation would be more complex)
572
+ end
573
+
574
+ agents
575
+ end
576
+
577
+ private
578
+
579
+ def parse_agent(name, data)
580
+ Manifest.new(
581
+ name: name.to_s.underscore.dasherize,
582
+ description: data['goal'],
583
+
584
+ model: normalize_model(data['llm']),
585
+
586
+ # CrewAI doesn't have formal schemas
587
+ input_schema: nil,
588
+ output_schema: nil,
589
+
590
+ # Map tools
591
+ tools: parse_crewai_tools(data['tools']),
592
+
593
+ # Instructions from role + backstory
594
+ instructions: build_instructions(data),
595
+ template: build_template(data),
596
+
597
+ # Preserve CrewAI-specific config
598
+ extensions: {
599
+ crewai: {
600
+ role: data['role'],
601
+ goal: data['goal'],
602
+ backstory: data['backstory'],
603
+ verbose: data['verbose'],
604
+ allow_delegation: data['allow_delegation'],
605
+ max_iter: data['max_iter'],
606
+ max_rpm: data['max_rpm']
607
+ }.compact
608
+ }
609
+ )
610
+ end
611
+
612
+ def parse_crewai_tools(tools)
613
+ return [] unless tools
614
+
615
+ tools.map do |tool|
616
+ if tool.is_a?(String)
617
+ # Reference to a tool class
618
+ Tool.new(ref: tool)
619
+ else
620
+ Tool.new(
621
+ name: tool['name'],
622
+ description: tool['description'],
623
+ input_schema: tool['args_schema']
624
+ )
625
+ end
626
+ end
627
+ end
628
+
629
+ def build_instructions(data)
630
+ parts = []
631
+ parts << "Role: #{data['role']}" if data['role']
632
+ parts << "Goal: #{data['goal']}" if data['goal']
633
+ parts << "\n#{data['backstory']}" if data['backstory']
634
+ parts.join("\n")
635
+ end
636
+
637
+ def build_template(data)
638
+ <<~TEMPLATE
639
+ # #{data['role']}
640
+
641
+ ## Goal
642
+ #{data['goal']}
643
+
644
+ ## Backstory
645
+ #{data['backstory']}
646
+ TEMPLATE
647
+ end
648
+ end
649
+ end
650
+
651
+ ParserRegistry.register(:crewai, CrewAIParser)
652
+ end
653
+ end
654
+ ```
655
+
656
+ ### 7. GitHub Prompt Parser (.prompt.md)
657
+
658
+ ```ruby
659
+ # lib/agent_manifest/parsers/github_prompt_parser.rb
660
+ module AgentManifest
661
+ module Parsers
662
+ class GitHubPromptParser < BaseParser
663
+ FRONTMATTER_REGEX = /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
664
+
665
+ class << self
666
+ def format_name
667
+ :github_prompt
668
+ end
669
+
670
+ def parse_string(content)
671
+ frontmatter, body = extract_frontmatter(content)
672
+
673
+ Manifest.new(
674
+ name: extract_name_from_frontmatter(frontmatter),
675
+ description: frontmatter['description'],
676
+
677
+ model: normalize_model(frontmatter['model']),
678
+
679
+ tools: parse_github_tools(frontmatter['tools']),
680
+
681
+ instructions: body,
682
+ template: body,
683
+
684
+ extensions: {
685
+ github_prompt: {
686
+ agent: frontmatter['agent'],
687
+ mode: frontmatter['mode']
688
+ }.compact
689
+ }
690
+ )
691
+ end
692
+
693
+ private
694
+
695
+ def extract_frontmatter(content)
696
+ match = content.match(FRONTMATTER_REGEX)
697
+ if match
698
+ [YAML.safe_load(match[1]), match[2].strip]
699
+ else
700
+ [{}, content.strip]
701
+ end
702
+ end
703
+
704
+ def extract_name_from_frontmatter(frontmatter)
705
+ frontmatter['name'] || 'github-prompt'
706
+ end
707
+
708
+ def parse_github_tools(tools)
709
+ return [] unless tools
710
+
711
+ # GitHub tools are string references like 'githubRepo', 'search/codebase'
712
+ tools.map do |tool|
713
+ Tool.new(ref: "github://#{tool}")
714
+ end
715
+ end
716
+ end
717
+ end
718
+
719
+ ParserRegistry.register(:github_prompt, GitHubPromptParser)
720
+ end
721
+ end
722
+ ```
723
+
724
+ ## Format Exporters
725
+
726
+ ### 8. Exporter Registry
727
+
728
+ ```ruby
729
+ # lib/agent_manifest/exporter_registry.rb
730
+ module AgentManifest
731
+ class ExporterRegistry
732
+ class << self
733
+ def exporters
734
+ @exporters ||= {}
735
+ end
736
+
737
+ def register(format, exporter_class)
738
+ exporters[format.to_sym] = exporter_class
739
+ end
740
+
741
+ def exporter_for(format)
742
+ exporters[format.to_sym] || raise(UnknownFormatError, "No exporter for format: #{format}")
743
+ end
744
+
745
+ def export(manifest, format:, path: nil)
746
+ content = exporter_for(format).export(manifest)
747
+ if path
748
+ File.write(path, content)
749
+ path
750
+ else
751
+ content
752
+ end
753
+ end
754
+ end
755
+ end
756
+ end
757
+ ```
758
+
759
+ ### 9. AgentMd Exporter
760
+
761
+ ```ruby
762
+ # lib/agent_manifest/exporters/agent_md_exporter.rb
763
+ module AgentManifest
764
+ module Exporters
765
+ class AgentMdExporter
766
+ class << self
767
+ def export(manifest)
768
+ frontmatter = build_frontmatter(manifest)
769
+ body = manifest.template || build_default_body(manifest)
770
+
771
+ <<~OUTPUT
772
+ ---
773
+ #{frontmatter.to_yaml.lines[1..].join}---
774
+
775
+ #{body}
776
+ OUTPUT
777
+ end
778
+
779
+ private
780
+
781
+ def build_frontmatter(manifest)
782
+ fm = {}
783
+
784
+ # Meta
785
+ fm['name'] = manifest.name
786
+ fm['version'] = manifest.version if manifest.version != '1.0.0'
787
+ fm['description'] = manifest.description if manifest.description
788
+ fm['author'] = manifest.author if manifest.author
789
+ fm['license'] = manifest.license if manifest.license
790
+ fm['repository'] = manifest.repository if manifest.repository
791
+ fm['tags'] = manifest.tags if manifest.tags.any?
792
+ fm['extends'] = manifest.extends if manifest.extends
793
+
794
+ # Model
795
+ fm['model'] = manifest.model if manifest.model
796
+ fm['config'] = manifest.config if manifest.config&.any?
797
+
798
+ # Schemas
799
+ if manifest.input_schema
800
+ fm['input'] = { 'schema' => manifest.input_schema.to_picoschema }
801
+ end
802
+ if manifest.output_schema
803
+ fm['output'] = manifest.output_schema
804
+ end
805
+
806
+ # Tools
807
+ if manifest.tools.any?
808
+ fm['tools'] = manifest.tools.map(&:to_h)
809
+ end
810
+
811
+ # Resources
812
+ if manifest.resources.any?
813
+ fm['resources'] = manifest.resources.map(&:to_h)
814
+ end
815
+
816
+ # Extensions
817
+ manifest.extensions.each do |framework, config|
818
+ fm[framework.to_s] = config if config.any?
819
+ end
820
+
821
+ fm
822
+ end
823
+
824
+ def build_default_body(manifest)
825
+ <<~BODY
826
+ # #{manifest.name.titleize}
827
+
828
+ #{manifest.description}
829
+
830
+ ## Instructions
831
+
832
+ #{manifest.instructions}
833
+ BODY
834
+ end
835
+ end
836
+ end
837
+
838
+ ExporterRegistry.register(:agent_md, AgentMdExporter)
839
+ end
840
+ end
841
+ ```
842
+
843
+ ### 10. Dotprompt Exporter
844
+
845
+ ```ruby
846
+ # lib/agent_manifest/exporters/dotprompt_exporter.rb
847
+ module AgentManifest
848
+ module Exporters
849
+ class DotpromptExporter
850
+ class << self
851
+ def export(manifest)
852
+ frontmatter = build_frontmatter(manifest)
853
+ body = convert_template(manifest.template)
854
+
855
+ <<~OUTPUT
856
+ ---
857
+ #{frontmatter.to_yaml.lines[1..].join}---
858
+
859
+ #{body}
860
+ OUTPUT
861
+ end
862
+
863
+ private
864
+
865
+ def build_frontmatter(manifest)
866
+ fm = {}
867
+
868
+ fm['model'] = manifest.model if manifest.model
869
+
870
+ # Config uses Dotprompt field names
871
+ if manifest.config
872
+ fm['temperature'] = manifest.config[:temperature] if manifest.config[:temperature]
873
+ fm['maxOutputTokens'] = manifest.config[:max_tokens] if manifest.config[:max_tokens]
874
+ fm['topP'] = manifest.config[:top_p] if manifest.config[:top_p]
875
+ fm['topK'] = manifest.config[:top_k] if manifest.config[:top_k]
876
+ fm['stopSequences'] = manifest.config[:stop] if manifest.config[:stop]
877
+ end
878
+
879
+ # Schemas
880
+ if manifest.input_schema
881
+ fm['input'] = { 'schema' => manifest.input_schema.to_picoschema }
882
+ end
883
+ if manifest.output_schema
884
+ fm['output'] = manifest.output_schema
885
+ end
886
+
887
+ # Tools
888
+ if manifest.tools.any?
889
+ fm['tools'] = manifest.tools.map { |t| t.name }
890
+ end
891
+
892
+ fm
893
+ end
894
+
895
+ def convert_template(template)
896
+ # Template syntax is compatible (both use Handlebars)
897
+ # But may need to strip markdown sections
898
+ return '' unless template
899
+
900
+ # Remove markdown headers that aren't part of the prompt
901
+ template
902
+ .gsub(/^#\s+.*\n/, '')
903
+ .gsub(/^##\s+(Instructions|Guidelines|Context)\s*\n/, '')
904
+ .strip
905
+ end
906
+ end
907
+ end
908
+
909
+ ExporterRegistry.register(:dotprompt, DotpromptExporter)
910
+ end
911
+ end
912
+ ```
913
+
914
+ ### 11. CrewAI Exporter
915
+
916
+ ```ruby
917
+ # lib/agent_manifest/exporters/crewai_exporter.rb
918
+ module AgentManifest
919
+ module Exporters
920
+ class CrewAIExporter
921
+ class << self
922
+ def export(manifest)
923
+ agent_key = manifest.name.underscore
924
+
925
+ agent_data = build_agent_data(manifest)
926
+
927
+ { agent_key => agent_data }.to_yaml
928
+ end
929
+
930
+ def export_multiple(manifests)
931
+ agents = {}
932
+ manifests.each do |manifest|
933
+ agent_key = manifest.name.underscore
934
+ agents[agent_key] = build_agent_data(manifest)
935
+ end
936
+ agents.to_yaml
937
+ end
938
+
939
+ private
940
+
941
+ def build_agent_data(manifest)
942
+ data = {}
943
+
944
+ # Use CrewAI extension if available
945
+ crewai = manifest.crewai_config
946
+
947
+ data['role'] = crewai[:role] || extract_role(manifest)
948
+ data['goal'] = crewai[:goal] || manifest.description
949
+ data['backstory'] = crewai[:backstory] || manifest.instructions
950
+
951
+ # Model
952
+ data['llm'] = convert_model(manifest.model) if manifest.model
953
+
954
+ # Tools
955
+ if manifest.tools.any?
956
+ data['tools'] = manifest.tools.map { |t| t.ref || t.name }
957
+ end
958
+
959
+ # Other CrewAI options
960
+ data['verbose'] = crewai[:verbose] if crewai[:verbose]
961
+ data['allow_delegation'] = crewai[:allow_delegation] if crewai.key?(:allow_delegation)
962
+ data['max_iter'] = crewai[:max_iter] if crewai[:max_iter]
963
+ data['max_rpm'] = crewai[:max_rpm] if crewai[:max_rpm]
964
+
965
+ data.compact
966
+ end
967
+
968
+ def extract_role(manifest)
969
+ # Try to extract role from instructions or name
970
+ manifest.name.titleize.gsub('-', ' ')
971
+ end
972
+
973
+ def convert_model(model)
974
+ # CrewAI uses different model format
975
+ # e.g., "anthropic/claude-sonnet-4-20250514" -> "claude-sonnet-4-20250514" or provider-specific
976
+ model.split('/').last
977
+ end
978
+ end
979
+ end
980
+
981
+ ExporterRegistry.register(:crewai, CrewAIExporter)
982
+ end
983
+ end
984
+ ```
985
+
986
+ ## Picoschema Parser
987
+
988
+ ```ruby
989
+ # lib/agent_manifest/picoschema_parser.rb
990
+ module AgentManifest
991
+ class PicoschemaParser
992
+ SCALAR_TYPES = %w[string integer number boolean any].freeze
993
+
994
+ class << self
995
+ # Convert Picoschema to JSON Schema
996
+ def to_json_schema(picoschema)
997
+ return nil unless picoschema
998
+
999
+ if picoschema.is_a?(Hash)
1000
+ parse_object(picoschema)
1001
+ else
1002
+ { "type" => "object" }
1003
+ end
1004
+ end
1005
+
1006
+ # Convert JSON Schema to Picoschema
1007
+ def from_json_schema(json_schema)
1008
+ return nil unless json_schema
1009
+
1010
+ if json_schema['type'] == 'object' && json_schema['properties']
1011
+ convert_properties(json_schema['properties'], json_schema['required'] || [])
1012
+ else
1013
+ json_schema
1014
+ end
1015
+ end
1016
+
1017
+ private
1018
+
1019
+ def parse_object(schema_hash)
1020
+ properties = {}
1021
+ required = []
1022
+
1023
+ schema_hash.each do |key, value|
1024
+ field_name, optional = parse_field_name(key)
1025
+ field_schema = parse_field(value)
1026
+
1027
+ properties[field_name] = field_schema
1028
+ required << field_name unless optional
1029
+ end
1030
+
1031
+ result = { "type" => "object", "properties" => properties }
1032
+ result["required"] = required if required.any?
1033
+ result
1034
+ end
1035
+
1036
+ def parse_field_name(key)
1037
+ if key.end_with?('?')
1038
+ [key.chomp('?'), true] # optional
1039
+ else
1040
+ [key, false] # required
1041
+ end
1042
+ end
1043
+
1044
+ def parse_field(value)
1045
+ case value
1046
+ when String
1047
+ parse_type_string(value)
1048
+ when Hash
1049
+ # Nested object
1050
+ parse_object(value)
1051
+ when Array
1052
+ # Array type
1053
+ if value.first.is_a?(Hash)
1054
+ { "type" => "array", "items" => parse_object(value.first) }
1055
+ else
1056
+ { "type" => "array", "items" => parse_type_string(value.first.to_s) }
1057
+ end
1058
+ else
1059
+ { "type" => "string" }
1060
+ end
1061
+ end
1062
+
1063
+ def parse_type_string(type_str)
1064
+ # Parse: "type, description" or "type(enum1, enum2), description"
1065
+ parts = type_str.split(',', 2)
1066
+ type_part = parts[0].strip
1067
+ description = parts[1]&.strip
1068
+
1069
+ result = {}
1070
+
1071
+ # Check for enum: string(a, b, c)
1072
+ if type_part =~ /^(\w+)\(([^)]+)\)$/
1073
+ base_type = $1
1074
+ enum_values = $2.split(',').map(&:strip)
1075
+ result["type"] = base_type
1076
+ result["enum"] = enum_values
1077
+ elsif SCALAR_TYPES.include?(type_part)
1078
+ result["type"] = type_part
1079
+ elsif type_part.start_with?('[') && type_part.end_with?(']')
1080
+ inner = type_part[1..-2]
1081
+ result["type"] = "array"
1082
+ result["items"] = { "type" => inner }
1083
+ else
1084
+ result["type"] = type_part
1085
+ end
1086
+
1087
+ result["description"] = description if description
1088
+
1089
+ result
1090
+ end
1091
+
1092
+ def convert_properties(properties, required_fields)
1093
+ result = {}
1094
+
1095
+ properties.each do |name, schema|
1096
+ optional = !required_fields.include?(name)
1097
+ key = optional ? "#{name}?" : name
1098
+ result[key] = convert_schema_to_pico(schema)
1099
+ end
1100
+
1101
+ result
1102
+ end
1103
+
1104
+ def convert_schema_to_pico(schema)
1105
+ type = schema['type']
1106
+ desc = schema['description']
1107
+
1108
+ pico = type.to_s
1109
+ if schema['enum']
1110
+ pico = "#{type}(#{schema['enum'].join(', ')})"
1111
+ end
1112
+ if desc
1113
+ pico = "#{pico}, #{desc}"
1114
+ end
1115
+
1116
+ pico
1117
+ end
1118
+ end
1119
+ end
1120
+ end
1121
+ ```
1122
+
1123
+ ## Main Entry Point
1124
+
1125
+ ```ruby
1126
+ # lib/agent_manifest.rb
1127
+ require_relative 'agent_manifest/manifest'
1128
+ require_relative 'agent_manifest/parser_registry'
1129
+ require_relative 'agent_manifest/exporter_registry'
1130
+ require_relative 'agent_manifest/picoschema_parser'
1131
+
1132
+ # Load all parsers
1133
+ require_relative 'agent_manifest/parsers/base_parser'
1134
+ require_relative 'agent_manifest/parsers/agent_md_parser'
1135
+ require_relative 'agent_manifest/parsers/dotprompt_parser'
1136
+ require_relative 'agent_manifest/parsers/crewai_parser'
1137
+ require_relative 'agent_manifest/parsers/github_prompt_parser'
1138
+
1139
+ # Load all exporters
1140
+ require_relative 'agent_manifest/exporters/agent_md_exporter'
1141
+ require_relative 'agent_manifest/exporters/dotprompt_exporter'
1142
+ require_relative 'agent_manifest/exporters/crewai_exporter'
1143
+
1144
+ module AgentManifest
1145
+ class << self
1146
+ # Parse any supported format
1147
+ def parse(path, format: nil)
1148
+ ParserRegistry.parse(path, format: format)
1149
+ end
1150
+
1151
+ # Parse from string
1152
+ def parse_string(content, format:)
1153
+ ParserRegistry.parse_string(content, format: format)
1154
+ end
1155
+
1156
+ # Export to any supported format
1157
+ def export(manifest, format:, path: nil)
1158
+ ExporterRegistry.export(manifest, format: format, path: path)
1159
+ end
1160
+
1161
+ # Convert between formats
1162
+ def convert(input_path, output_format:, output_path: nil)
1163
+ manifest = parse(input_path)
1164
+ export(manifest, format: output_format, path: output_path)
1165
+ end
1166
+
1167
+ # Load and instantiate as ActiveAgent
1168
+ def load_agent(path, format: nil)
1169
+ manifest = parse(path, format: format)
1170
+ AgentBuilder.build(manifest)
1171
+ end
1172
+ end
1173
+ end
1174
+ ```
1175
+
1176
+ ## ActiveAgent Integration
1177
+
1178
+ ```ruby
1179
+ # lib/agent_manifest/agent_builder.rb
1180
+ module AgentManifest
1181
+ class AgentBuilder
1182
+ class << self
1183
+ def build(manifest)
1184
+ # Create a dynamic agent class from manifest
1185
+ agent_class = Class.new(base_class(manifest)) do
1186
+ # Include concerns based on manifest
1187
+ end
1188
+
1189
+ # Configure from manifest
1190
+ configure_agent(agent_class, manifest)
1191
+
1192
+ agent_class
1193
+ end
1194
+
1195
+ private
1196
+
1197
+ def base_class(manifest)
1198
+ config = manifest.activeagent_config
1199
+ parent = config[:parent_class] || 'ApplicationAgent'
1200
+ parent.constantize
1201
+ rescue NameError
1202
+ ActiveAgent::Base
1203
+ end
1204
+
1205
+ def configure_agent(klass, manifest)
1206
+ config = manifest.activeagent_config
1207
+
1208
+ # Set model
1209
+ if manifest.model
1210
+ provider, model = manifest.model.split('/')
1211
+ klass.generate_with provider.to_sym, model: model
1212
+ end
1213
+
1214
+ # Include concerns
1215
+ concerns = config[:concerns] || []
1216
+ concerns.each do |concern|
1217
+ apply_concern(klass, concern)
1218
+ end
1219
+
1220
+ # Define tools
1221
+ manifest.tools.each do |tool|
1222
+ define_tool(klass, tool) unless tool.reference?
1223
+ end
1224
+
1225
+ # Set instructions
1226
+ klass.define_method(:system_instructions) do
1227
+ manifest.instructions
1228
+ end
1229
+ end
1230
+
1231
+ def apply_concern(klass, concern)
1232
+ case concern
1233
+ when Hash
1234
+ concern.each do |name, options|
1235
+ case name.to_sym
1236
+ when :has_context
1237
+ klass.include(SolidAgent::HasContext)
1238
+ klass.has_context(**options.symbolize_keys)
1239
+ when :has_tools
1240
+ klass.include(SolidAgent::HasTools)
1241
+ klass.has_tools(*options) if options.is_a?(Array)
1242
+ when :streams_tool_updates
1243
+ klass.include(SolidAgent::StreamsToolUpdates)
1244
+ end
1245
+ end
1246
+ when String, Symbol
1247
+ klass.include("SolidAgent::#{concern.to_s.camelize}".constantize)
1248
+ end
1249
+ end
1250
+
1251
+ def define_tool(klass, tool)
1252
+ klass.tool tool.name.to_sym do
1253
+ description tool.description
1254
+ if tool.input_schema && tool.input_schema['properties']
1255
+ tool.input_schema['properties'].each do |param_name, param_schema|
1256
+ required = tool.input_schema['required']&.include?(param_name)
1257
+ parameter param_name.to_sym,
1258
+ type: param_schema['type']&.to_sym,
1259
+ required: required,
1260
+ description: param_schema['description']
1261
+ end
1262
+ end
1263
+ end
1264
+ end
1265
+ end
1266
+ end
1267
+ end
1268
+ ```
1269
+
1270
+ ## CLI Commands
1271
+
1272
+ ```ruby
1273
+ # lib/agent_manifest/cli.rb
1274
+ module AgentManifest
1275
+ class CLI < Thor
1276
+ desc "parse PATH", "Parse an agent file and display info"
1277
+ option :format, type: :string, desc: "Force input format"
1278
+ option :json, type: :boolean, desc: "Output as JSON"
1279
+ def parse(path)
1280
+ manifest = AgentManifest.parse(path, format: options[:format]&.to_sym)
1281
+
1282
+ if options[:json]
1283
+ puts JSON.pretty_generate(manifest.to_h)
1284
+ else
1285
+ display_manifest(manifest)
1286
+ end
1287
+ end
1288
+
1289
+ desc "convert INPUT OUTPUT", "Convert between agent formats"
1290
+ option :from, type: :string, desc: "Input format"
1291
+ option :to, type: :string, required: true, desc: "Output format"
1292
+ def convert(input, output)
1293
+ manifest = AgentManifest.parse(input, format: options[:from]&.to_sym)
1294
+ AgentManifest.export(manifest, format: options[:to].to_sym, path: output)
1295
+ say "Converted #{input} -> #{output}", :green
1296
+ end
1297
+
1298
+ desc "validate PATH", "Validate an agent file"
1299
+ def validate(path)
1300
+ manifest = AgentManifest.parse(path)
1301
+ errors = Validator.validate(manifest)
1302
+
1303
+ if errors.empty?
1304
+ say "✓ Valid agent definition", :green
1305
+ else
1306
+ say "✗ Validation errors:", :red
1307
+ errors.each { |e| say " - #{e}", :red }
1308
+ exit 1
1309
+ end
1310
+ end
1311
+
1312
+ desc "init NAME", "Create a new agent file"
1313
+ option :format, type: :string, default: "agent_md", desc: "Output format"
1314
+ option :template, type: :string, desc: "Template to use"
1315
+ def init(name)
1316
+ # Generate from template
1317
+ end
1318
+
1319
+ private
1320
+
1321
+ def display_manifest(manifest)
1322
+ say "Agent: #{manifest.name}", :green
1323
+ say "Version: #{manifest.version}"
1324
+ say "Model: #{manifest.model}" if manifest.model
1325
+ say "Tools: #{manifest.tools.map(&:name).join(', ')}" if manifest.tools.any?
1326
+ say "Description: #{manifest.description}" if manifest.description
1327
+ end
1328
+ end
1329
+ end
1330
+ ```
1331
+
1332
+ ## Usage Examples
1333
+
1334
+ ```ruby
1335
+ # Parse any format
1336
+ manifest = AgentManifest.parse("research-assistant.agent.md")
1337
+ manifest = AgentManifest.parse("agent.prompt") # Dotprompt
1338
+ manifest = AgentManifest.parse("agents.yaml") # CrewAI
1339
+
1340
+ # Convert formats
1341
+ AgentManifest.convert("agent.prompt", output_format: :agent_md, output_path: "agent.agent.md")
1342
+ AgentManifest.convert("agents.yaml", output_format: :agent_md, output_path: "agent.agent.md")
1343
+
1344
+ # Export to different format
1345
+ content = AgentManifest.export(manifest, format: :crewai)
1346
+ AgentManifest.export(manifest, format: :dotprompt, path: "output.prompt")
1347
+
1348
+ # Load and use as ActiveAgent
1349
+ agent_class = AgentManifest.load_agent("research-assistant.agent.md")
1350
+ agent = agent_class.new(params: { query: "What is AI?" })
1351
+ result = agent.research.generate_now
1352
+
1353
+ # Parse from string
1354
+ content = File.read("agent.agent.md")
1355
+ manifest = AgentManifest.parse_string(content, format: :agent_md)
1356
+ ```
1357
+
1358
+ ## Supported Formats Summary
1359
+
1360
+ | Format | Extension | Parse | Export | Notes |
1361
+ |--------|-----------|-------|--------|-------|
1362
+ | AgentMd | `.agent.md` | ✅ | ✅ | Native format |
1363
+ | Dotprompt | `.prompt` | ✅ | ✅ | Google/Firebase |
1364
+ | CrewAI | `agents.yaml` | ✅ | ✅ | Python framework |
1365
+ | GitHub Prompt | `.prompt.md` | ✅ | ⚠️ | Copilot prompts |
1366
+ | LangChain | `*.py` | 🚧 | 🚧 | Code-based |
1367
+ | MCP Tool | `.tool.json` | ✅ | ✅ | Tool definitions only |
1368
+
1369
+ ✅ Full support | ⚠️ Partial | 🚧 Planned