solid_agent 0.1.1 → 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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +21 -0
  4. data/README.md +209 -18
  5. data/Rakefile +22 -2
  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 +2 -2
  31. data/lib/generators/solid_agent/agent/templates/agent.rb.erb +3 -3
  32. data/lib/generators/solid_agent/context/templates/context_model.rb.erb +50 -16
  33. data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +8 -0
  34. data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +4 -0
  35. data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +11 -0
  36. data/lib/generators/solid_agent/install/install_generator.rb +9 -0
  37. data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +60 -17
  38. data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +23 -6
  39. data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
  40. data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
  41. data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
  42. data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +13 -0
  43. data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
  44. data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +5 -0
  45. data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
  46. data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
  47. data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
  48. data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
  49. data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
  50. data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
  51. data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
  52. data/lib/solid_agent/agent_manifest/errors.rb +26 -0
  53. data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
  54. data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
  55. data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
  56. data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
  57. data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
  58. data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
  59. data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
  60. data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
  61. data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
  62. data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
  63. data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
  64. data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
  65. data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
  66. data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
  67. data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
  68. data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
  69. data/lib/solid_agent/agent_manifest/resource.rb +103 -0
  70. data/lib/solid_agent/agent_manifest/tool.rb +160 -0
  71. data/lib/solid_agent/agent_manifest/validator.rb +368 -0
  72. data/lib/solid_agent/agent_manifest.rb +381 -0
  73. data/lib/solid_agent/has_context.rb +251 -30
  74. data/lib/solid_agent/has_memory.rb +136 -0
  75. data/lib/solid_agent/has_reasons.rb +230 -0
  76. data/lib/solid_agent/model_naming.rb +42 -0
  77. data/lib/solid_agent/model_pricing.rb +93 -0
  78. data/lib/solid_agent/reasonable/reason.rb +205 -0
  79. data/lib/solid_agent/reasonable.rb +181 -0
  80. data/lib/solid_agent/records/agent.rb +520 -0
  81. data/lib/solid_agent/records/agent_run.rb +520 -0
  82. data/lib/solid_agent/records/agent_template.rb +142 -0
  83. data/lib/solid_agent/records/agent_version.rb +141 -0
  84. data/lib/solid_agent/records/ownable.rb +130 -0
  85. data/lib/solid_agent/records.rb +152 -0
  86. data/lib/solid_agent/run_fingerprint.rb +51 -0
  87. data/lib/solid_agent/tool_cache.rb +91 -0
  88. data/lib/solid_agent/version.rb +1 -1
  89. data/lib/solid_agent.rb +70 -3
  90. metadata +87 -1
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: <%= file_name %>
3
+ version: 1.0.0
4
+ description: <%= @description %>
5
+ model: <%= @model %>
6
+ <% if @tools.any? -%>
7
+
8
+ tools:
9
+ <% @tools.each do |tool| -%>
10
+ - name: <%= tool %>
11
+ description: "TODO - Describe what <%= tool %> does"
12
+ inputSchema:
13
+ type: object
14
+ properties:
15
+ query:
16
+ type: string
17
+ description: "Input for <%= tool %>"
18
+ required:
19
+ - query
20
+ <% end -%>
21
+ <% end -%>
22
+
23
+ activeagent:
24
+ class_name: <%= agent_class_name %>
25
+ concerns:
26
+ <% if @contextual -%>
27
+ - has_context:
28
+ contextual: <%= @contextual %>
29
+ <% end -%>
30
+ <% if @tools.any? -%>
31
+ - has_tools: [<%= @tools.join(', ') %>]
32
+ <% end -%>
33
+ ---
34
+
35
+ # <%= agent_title %>
36
+
37
+ <%= @description %>
38
+
39
+ <%= default_instructions %>
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: <%= file_name %>
3
+ model: <%= @model %>
4
+ description: <%= @description %>
5
+ <% if @tools.any? -%>
6
+ tools:
7
+ <% @tools.each do |tool| -%>
8
+ - <%= tool %>
9
+ <% end -%>
10
+ <% end -%>
11
+ ---
12
+
13
+ <%= default_instructions %>
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module SolidAgent
7
+ module Generators
8
+ class ReasonsGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ desc "Adds reasoning columns to an existing generation model for extended thinking support"
13
+
14
+ argument :model_name, type: :string, default: "AgentGeneration",
15
+ desc: "The model to add reasoning columns to"
16
+
17
+ class_option :content_column, type: :string, default: "reasoning_content",
18
+ desc: "Column name for storing reasoning content"
19
+
20
+ class_option :tokens_column, type: :string, default: "reasoning_tokens",
21
+ desc: "Column name for storing reasoning token count"
22
+
23
+ class_option :metadata_column, type: :string, default: "reasoning_metadata",
24
+ desc: "Column name for storing reasoning metadata (JSON)"
25
+
26
+ def self.next_migration_number(dirname)
27
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
28
+ end
29
+
30
+ def create_migration
31
+ @table_name = model_name.underscore.pluralize
32
+ @content_column = options[:content_column]
33
+ @tokens_column = options[:tokens_column]
34
+ @metadata_column = options[:metadata_column]
35
+
36
+ migration_template(
37
+ "add_reasoning_columns.rb.erb",
38
+ "db/migrate/add_reasoning_to_#{@table_name}.rb"
39
+ )
40
+ end
41
+
42
+ def add_concern_to_model
43
+ model_file = "app/models/#{model_name.underscore}.rb"
44
+
45
+ return unless File.exist?(model_file)
46
+
47
+ inject_into_class model_file, model_name do
48
+ " include SolidAgent::Reasonable\n\n"
49
+ end
50
+
51
+ say "Added SolidAgent::Reasonable to #{model_name}", :green
52
+ end
53
+
54
+ def show_post_install_message
55
+ say ""
56
+ say "Reasoning support added to #{model_name}!", :green
57
+ say ""
58
+ say "Next steps:", :yellow
59
+ say " 1. Run migrations: rails db:migrate"
60
+ say " 2. Use reasoning in your agents:"
61
+ say ""
62
+ say " class MyAgent < ApplicationAgent"
63
+ say " include SolidAgent::HasReasons"
64
+ say " include SolidAgent::HasContext"
65
+ say ""
66
+ say " has_reasons auto_capture: true, persist: true"
67
+ say ""
68
+ say " def analyze"
69
+ say " result = prompt("
70
+ say " messages: messages,"
71
+ say " extended_thinking: true"
72
+ say " )"
73
+ say ""
74
+ say " # Access reasoning"
75
+ say " last_reasoning.content"
76
+ say " total_reasoning_tokens"
77
+ say " end"
78
+ say " end"
79
+ say ""
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddReasoningTo<%= @table_name.camelize %> < ActiveRecord::Migration[7.0]
4
+ def change
5
+ add_column :<%= @table_name %>, :<%= @content_column %>, :text
6
+ add_column :<%= @table_name %>, :<%= @tokens_column %>, :integer, default: 0
7
+ add_column :<%= @table_name %>, :<%= @metadata_column %>, :jsonb, default: {}
8
+
9
+ add_index :<%= @table_name %>, :<%= @tokens_column %>,
10
+ name: "index_<%= @table_name %>_on_<%= @tokens_column %>"
11
+ end
12
+ end
@@ -0,0 +1,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ # AgentBuilder generates ActiveAgent classes from Manifest definitions.
6
+ #
7
+ # Creates anonymous or named classes with the appropriate concerns included,
8
+ # tools defined, and system instructions configured.
9
+ #
10
+ # @example Build an agent class from a manifest
11
+ # manifest = AgentManifest.parse("research_assistant.agent.md")
12
+ # klass = AgentBuilder.build(manifest)
13
+ # agent = klass.new
14
+ # agent.research(query: "Ruby concurrency")
15
+ #
16
+ # @example Build with a custom base class
17
+ # klass = AgentBuilder.build(manifest, base_class: ApplicationAgent)
18
+ #
19
+ # @example Build with explicit naming
20
+ # klass = AgentBuilder.build(manifest, class_name: "CustomResearchAgent")
21
+ # CustomResearchAgent.new.call
22
+ #
23
+ class AgentBuilder
24
+ class << self
25
+ # Build an agent class from a manifest
26
+ #
27
+ # @param manifest [Manifest] The manifest to build from
28
+ # @param base_class [Class] Base class to inherit from
29
+ # @param class_name [String, nil] Optional class name for constant registration
30
+ # @param namespace [Module, nil] Optional namespace for constant registration
31
+ # @return [Class] The generated agent class
32
+ def build(manifest, base_class: nil, class_name: nil, namespace: nil)
33
+ # Determine base class
34
+ parent_class = determine_base_class(manifest, base_class)
35
+
36
+ # Create the class
37
+ klass = Class.new(parent_class)
38
+
39
+ # Configure the agent
40
+ configure_model(klass, manifest)
41
+ configure_concerns(klass, manifest)
42
+ configure_tools(klass, manifest)
43
+ configure_instructions(klass, manifest)
44
+ configure_metadata(klass, manifest)
45
+
46
+ # Register as constant if requested
47
+ if class_name || manifest_class_name(manifest)
48
+ register_constant(klass, class_name || manifest_class_name(manifest), namespace)
49
+ end
50
+
51
+ klass
52
+ end
53
+
54
+ # Build and instantiate an agent from a manifest
55
+ #
56
+ # @param manifest [Manifest] The manifest to build from
57
+ # @param params [Hash] Parameters to pass to the agent
58
+ # @return [Object] The instantiated agent
59
+ def build_instance(manifest, params: {}, **options)
60
+ klass = build(manifest, **options)
61
+ klass.new(params)
62
+ end
63
+
64
+ # Build from a file path
65
+ #
66
+ # @param path [String] Path to manifest file
67
+ # @param options [Hash] Options passed to build
68
+ # @return [Class] The generated agent class
69
+ def build_from_file(path, **options)
70
+ manifest = ParserRegistry.parse(path)
71
+ build(manifest, **options)
72
+ end
73
+
74
+ private
75
+
76
+ # Determine the base class for the agent
77
+ def determine_base_class(manifest, explicit_base)
78
+ return explicit_base if explicit_base
79
+
80
+ # Check activeagent extension for parent class
81
+ aa_config = manifest.extensions&.dig(:activeagent) || {}
82
+ if aa_config[:parent_class]
83
+ aa_config[:parent_class].to_s.constantize
84
+ elsif defined?(ApplicationAgent)
85
+ ApplicationAgent
86
+ elsif defined?(ActiveAgent::Base)
87
+ ActiveAgent::Base
88
+ else
89
+ # Fallback to a basic class
90
+ Object
91
+ end
92
+ end
93
+
94
+ # Configure model/provider settings
95
+ def configure_model(klass, manifest)
96
+ return unless manifest.model.present?
97
+
98
+ provider, model_name = parse_model_identifier(manifest.model)
99
+
100
+ # Set class-level configuration
101
+ klass.class_eval do
102
+ class_attribute :_manifest_model, default: nil
103
+ class_attribute :_manifest_provider, default: nil
104
+ class_attribute :_manifest_config, default: {}
105
+ end
106
+
107
+ klass._manifest_model = model_name
108
+ klass._manifest_provider = provider
109
+ klass._manifest_config = manifest.config || {}
110
+
111
+ # If the base class supports model configuration, use it
112
+ if klass.respond_to?(:model)
113
+ klass.model(model_name)
114
+ end
115
+
116
+ if klass.respond_to?(:provider) && provider
117
+ klass.provider(provider)
118
+ end
119
+ end
120
+
121
+ # Parse model identifier into provider and model name
122
+ def parse_model_identifier(model_string)
123
+ if model_string.include?("/")
124
+ parts = model_string.split("/", 2)
125
+ [parts[0], parts[1]]
126
+ else
127
+ [nil, model_string]
128
+ end
129
+ end
130
+
131
+ # Configure concerns based on manifest extensions
132
+ def configure_concerns(klass, manifest)
133
+ aa_config = manifest.extensions&.dig(:activeagent) || {}
134
+ concerns = aa_config[:concerns] || []
135
+
136
+ concerns.each do |concern_config|
137
+ apply_concern(klass, concern_config)
138
+ end
139
+
140
+ # Auto-include HasTools if manifest defines tools
141
+ if manifest.tools&.any? && !has_concern?(klass, SolidAgent::HasTools)
142
+ apply_concern(klass, "has_tools")
143
+ end
144
+ end
145
+
146
+ # Apply a single concern to the class
147
+ def apply_concern(klass, concern_config)
148
+ case concern_config
149
+ when String, Symbol
150
+ concern_name = concern_config.to_s
151
+ options = {}
152
+ when Hash
153
+ concern_name = concern_config.keys.first.to_s
154
+ options = concern_config.values.first || {}
155
+ else
156
+ return
157
+ end
158
+
159
+ concern_module = resolve_concern(concern_name)
160
+ return unless concern_module
161
+
162
+ klass.include(concern_module)
163
+
164
+ # Call the DSL method if it exists (e.g., has_context, has_tools)
165
+ if klass.respond_to?(concern_name)
166
+ if options.is_a?(Hash) && options.any?
167
+ klass.send(concern_name, **symbolize_keys(options))
168
+ elsif options.is_a?(Array)
169
+ klass.send(concern_name, *options)
170
+ else
171
+ klass.send(concern_name)
172
+ end
173
+ end
174
+ end
175
+
176
+ # Resolve a concern name to a module
177
+ def resolve_concern(name)
178
+ case name.to_s
179
+ when "has_context"
180
+ SolidAgent::HasContext
181
+ when "has_tools"
182
+ SolidAgent::HasTools
183
+ when "streams_tool_updates"
184
+ SolidAgent::StreamsToolUpdates
185
+ else
186
+ # Try to constantize
187
+ begin
188
+ name.to_s.camelize.constantize
189
+ rescue NameError
190
+ nil
191
+ end
192
+ end
193
+ end
194
+
195
+ # Check if a class already includes a concern
196
+ def has_concern?(klass, concern)
197
+ klass.included_modules.include?(concern)
198
+ end
199
+
200
+ # Configure tools from manifest
201
+ def configure_tools(klass, manifest)
202
+ return unless manifest.tools&.any?
203
+
204
+ manifest.tools.each do |tool|
205
+ define_tool(klass, tool) unless tool.reference?
206
+ end
207
+
208
+ # Store tool references for later resolution
209
+ tool_refs = manifest.tools.select(&:reference?).map(&:ref)
210
+ if tool_refs.any?
211
+ klass.class_attribute :_manifest_tool_refs, default: []
212
+ klass._manifest_tool_refs = tool_refs
213
+ end
214
+ end
215
+
216
+ # Define a tool on the class
217
+ def define_tool(klass, tool)
218
+ return unless klass.respond_to?(:tool)
219
+
220
+ tool_name = tool.name.to_sym
221
+ tool_desc = tool.description
222
+ tool_schema = tool.input_schema || {}
223
+
224
+ klass.tool(tool_name) do
225
+ description tool_desc if tool_desc
226
+
227
+ # Define parameters from schema
228
+ properties = tool_schema["properties"] || tool_schema[:properties] || {}
229
+ required = tool_schema["required"] || tool_schema[:required] || []
230
+
231
+ properties.each do |param_name, param_schema|
232
+ param_opts = {
233
+ type: param_schema["type"] || param_schema[:type] || "string",
234
+ required: required.include?(param_name.to_s),
235
+ description: param_schema["description"] || param_schema[:description]
236
+ }
237
+ param_opts[:enum] = param_schema["enum"] || param_schema[:enum] if param_schema["enum"] || param_schema[:enum]
238
+ param_opts[:default] = param_schema["default"] || param_schema[:default] if param_schema.key?("default") || param_schema.key?(:default)
239
+
240
+ parameter param_name.to_sym, **param_opts.compact
241
+ end
242
+ end
243
+
244
+ # Define a placeholder method if it doesn't exist
245
+ unless klass.instance_methods.include?(tool_name)
246
+ klass.define_method(tool_name) do |**args|
247
+ raise NotImplementedError, "Tool method '#{tool_name}' must be implemented"
248
+ end
249
+ end
250
+ end
251
+
252
+ # Configure system instructions
253
+ def configure_instructions(klass, manifest)
254
+ return unless manifest.instructions.present? || manifest.template.present?
255
+
256
+ instructions = manifest.instructions || manifest.template
257
+
258
+ klass.class_attribute :_manifest_instructions, default: nil
259
+ klass._manifest_instructions = instructions
260
+
261
+ # If the base class supports system instructions, set them
262
+ if klass.respond_to?(:system_instructions)
263
+ klass.system_instructions(instructions)
264
+ end
265
+
266
+ # Store template for rendering
267
+ if manifest.template.present?
268
+ klass.class_attribute :_manifest_template, default: nil
269
+ klass._manifest_template = manifest.template
270
+ end
271
+ end
272
+
273
+ # Configure metadata
274
+ def configure_metadata(klass, manifest)
275
+ klass.class_attribute :_manifest, default: nil
276
+ klass._manifest = manifest
277
+
278
+ klass.class_attribute :_manifest_name, default: nil
279
+ klass._manifest_name = manifest.name
280
+
281
+ klass.class_attribute :_manifest_version, default: nil
282
+ klass._manifest_version = manifest.version
283
+
284
+ klass.class_attribute :_manifest_description, default: nil
285
+ klass._manifest_description = manifest.description
286
+ end
287
+
288
+ # Get class name from manifest
289
+ def manifest_class_name(manifest)
290
+ aa_config = manifest.extensions&.dig(:activeagent) || {}
291
+ return aa_config[:class_name] if aa_config[:class_name]
292
+
293
+ # Generate from manifest name
294
+ if manifest.name.present?
295
+ name = manifest.name.to_s.tr("-", "_").camelize
296
+ name += "Agent" unless name.end_with?("Agent")
297
+ name
298
+ end
299
+ end
300
+
301
+ # Register the class as a constant
302
+ def register_constant(klass, name, namespace)
303
+ target = namespace || Object
304
+
305
+ if target.const_defined?(name, false)
306
+ target.send(:remove_const, name)
307
+ end
308
+
309
+ target.const_set(name, klass)
310
+ end
311
+
312
+ # Deep symbolize keys
313
+ def symbolize_keys(hash)
314
+ return hash unless hash.is_a?(Hash)
315
+
316
+ hash.transform_keys(&:to_sym).transform_values do |v|
317
+ v.is_a?(Hash) ? symbolize_keys(v) : v
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ # Base error class for all AgentManifest errors
6
+ class Error < SolidAgent::Error; end
7
+
8
+ # Raised when parsing a manifest file fails
9
+ class ParseError < Error; end
10
+
11
+ # Raised when manifest validation fails
12
+ class ValidationError < Error; end
13
+
14
+ # Raised when an unknown format is encountered
15
+ class UnknownFormatError < Error; end
16
+
17
+ # Raised when exporting a manifest fails
18
+ class ExportError < Error; end
19
+
20
+ # Raised when registry operations fail
21
+ class RegistryError < Error; end
22
+
23
+ # Raised when schema parsing or conversion fails
24
+ class SchemaError < Error; end
25
+ end
26
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ # ExporterRegistry manages format exporters for converting Manifests to various formats.
6
+ #
7
+ # @example Register an exporter
8
+ # ExporterRegistry.register(:agent_md, AgentMdExporter)
9
+ #
10
+ # @example Export a manifest
11
+ # content = ExporterRegistry.export(manifest, :dotprompt)
12
+ #
13
+ # @example Export to file
14
+ # ExporterRegistry.export_to_file(manifest, :agent_md, "agent.agent.md")
15
+ #
16
+ class ExporterRegistry
17
+ class << self
18
+ # Registered exporters by format name
19
+ #
20
+ # @return [Hash<Symbol, Class>]
21
+ def exporters
22
+ @exporters ||= {}
23
+ end
24
+
25
+ # Register an exporter for a format
26
+ #
27
+ # @param format [Symbol] Format name
28
+ # @param exporter_class [Class] Exporter class
29
+ def register(format, exporter_class)
30
+ exporters[format.to_sym] = exporter_class
31
+ end
32
+
33
+ # Get the exporter for a format
34
+ #
35
+ # @param format [Symbol, String] Format name
36
+ # @return [Class] Exporter class
37
+ # @raise [UnknownFormatError] if format not recognized
38
+ def exporter_for(format)
39
+ format_sym = format.to_sym
40
+ exporters[format_sym] || raise(UnknownFormatError, "Unknown export format: #{format}")
41
+ end
42
+
43
+ # Check if a format is supported
44
+ #
45
+ # @param format [Symbol, String] Format name
46
+ # @return [Boolean]
47
+ def supports?(format)
48
+ exporters.key?(format.to_sym)
49
+ end
50
+
51
+ # List all registered format names
52
+ #
53
+ # @return [Array<Symbol>]
54
+ def formats
55
+ exporters.keys
56
+ end
57
+
58
+ # Export a manifest to a format string
59
+ #
60
+ # @param manifest [Manifest] The manifest to export
61
+ # @param format [Symbol, String] Target format
62
+ # @param options [Hash] Format-specific options
63
+ # @return [String] Exported content
64
+ def export(manifest, format, **options)
65
+ exporter = exporter_for(format)
66
+ exporter.export(manifest, **options)
67
+ end
68
+
69
+ # Export a manifest to a file
70
+ #
71
+ # @param manifest [Manifest] The manifest to export
72
+ # @param format [Symbol, String] Target format
73
+ # @param path [String] Output file path
74
+ # @param options [Hash] Format-specific options
75
+ # @return [String] The path written to
76
+ def export_to_file(manifest, format, path, **options)
77
+ content = export(manifest, format, **options)
78
+ File.write(path, content, encoding: "UTF-8")
79
+ path
80
+ end
81
+
82
+ # Convert between formats
83
+ #
84
+ # @param input_path [String] Source file path
85
+ # @param output_format [Symbol, String] Target format
86
+ # @param output_path [String, nil] Output path (auto-generated if nil)
87
+ # @return [String] Output path
88
+ def convert(input_path, output_format, output_path = nil)
89
+ manifest = ParserRegistry.parse(input_path)
90
+
91
+ output_path ||= generate_output_path(input_path, output_format)
92
+ export_to_file(manifest, output_format, output_path)
93
+ end
94
+
95
+ private
96
+
97
+ # Generate output path based on format
98
+ def generate_output_path(input_path, format)
99
+ base = File.basename(input_path, ".*")
100
+ # Remove any existing format extensions
101
+ base = base.sub(/\.(agent|prompt)$/, "")
102
+ dir = File.dirname(input_path)
103
+
104
+ extension = case format.to_sym
105
+ when :agent_md then ".agent.md"
106
+ when :dotprompt then ".prompt"
107
+ when :crewai then ".yaml"
108
+ when :github_prompt then ".prompt.md"
109
+ else ".#{format}"
110
+ end
111
+
112
+ File.join(dir, "#{base}#{extension}")
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end