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,306 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module SolidAgent
6
+ module AgentManifest
7
+ # Manifest represents a unified, portable AI agent definition.
8
+ #
9
+ # It serves as the canonical internal representation that all parsers
10
+ # produce and all exporters consume, enabling cross-framework compatibility.
11
+ #
12
+ # @example Creating a manifest
13
+ # manifest = Manifest.new(
14
+ # name: "research-assistant",
15
+ # model: "anthropic/claude-sonnet-4-20250514",
16
+ # description: "An agent that researches topics"
17
+ # )
18
+ #
19
+ # @example Validating a manifest
20
+ # manifest.valid? # => true/false
21
+ # manifest.errors.full_messages # => ["Name can't be blank"]
22
+ #
23
+ class Manifest
24
+ include ActiveModel::Model
25
+ include ActiveModel::Validations
26
+
27
+ # === Meta attributes ===
28
+ # @return [String] Unique identifier (lowercase, hyphens only)
29
+ attr_accessor :name
30
+
31
+ # @return [String] SemVer version string (default: "1.0.0")
32
+ attr_accessor :version
33
+
34
+ # @return [String, nil] Brief description (max 280 chars)
35
+ attr_accessor :description
36
+
37
+ # @return [String, nil] Author name or organization
38
+ attr_accessor :author
39
+
40
+ # @return [String, nil] SPDX license identifier
41
+ attr_accessor :license
42
+
43
+ # @return [String, nil] Source repository URL
44
+ attr_accessor :repository
45
+
46
+ # @return [Array<String>] Categorization tags
47
+ attr_accessor :tags
48
+
49
+ # @return [String, nil] Parent agent to inherit from
50
+ attr_accessor :extends
51
+
52
+ # === Model configuration ===
53
+ # @return [String, nil] Model identifier (provider/model format)
54
+ attr_accessor :model
55
+
56
+ # @return [Hash] Model-specific parameters (temperature, max_tokens, etc.)
57
+ attr_accessor :config
58
+
59
+ # === Schemas ===
60
+ # @return [InputSchema, nil] Expected input structure
61
+ attr_accessor :input_schema
62
+
63
+ # @return [Hash, nil] Expected output format and structure
64
+ attr_accessor :output_schema
65
+
66
+ # === Tools & Resources ===
67
+ # @return [Array<Tool>] Available tools
68
+ attr_accessor :tools
69
+
70
+ # @return [Array<Resource>] External data sources
71
+ attr_accessor :resources
72
+
73
+ # === Instructions ===
74
+ # @return [String, nil] Extracted instructions from template
75
+ attr_accessor :instructions
76
+
77
+ # @return [String, nil] Full template content (with Liquid syntax)
78
+ attr_accessor :template
79
+
80
+ # === Framework extensions ===
81
+ # @return [Hash] Framework-specific configuration (activeagent:, crewai:, etc.)
82
+ attr_accessor :extensions
83
+
84
+ # === Source tracking ===
85
+ # @return [Symbol, nil] Original format (:agent_md, :dotprompt, etc.)
86
+ attr_accessor :source_format
87
+
88
+ # @return [String, nil] Original file path
89
+ attr_accessor :source_path
90
+
91
+ # === Examples & Tests ===
92
+ # @return [Array<Hash>] Example inputs/outputs
93
+ attr_accessor :examples
94
+
95
+ # @return [Array<Hash>] Test cases
96
+ attr_accessor :tests
97
+
98
+ # === Validations ===
99
+ validates :name, presence: true,
100
+ format: {
101
+ with: /\A[a-z][a-z0-9\-]*\z/,
102
+ message: "must start with lowercase letter and contain only lowercase letters, numbers, and hyphens"
103
+ },
104
+ allow_blank: false
105
+
106
+ validates :version,
107
+ format: {
108
+ with: /\A\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?\z/,
109
+ message: "must be valid semver (e.g., 1.0.0, 1.0.0-beta.1)"
110
+ },
111
+ allow_blank: true
112
+
113
+ validates :model,
114
+ format: {
115
+ with: %r{\A[a-z0-9_-]+/[a-z0-9._-]+\z}i,
116
+ message: "must be in provider/model format (e.g., anthropic/claude-sonnet-4-20250514)"
117
+ },
118
+ allow_blank: true
119
+
120
+ validate :validate_tools
121
+ validate :validate_extensions
122
+
123
+ def initialize(attributes = {})
124
+ # Set defaults before calling super
125
+ @version = "1.0.0"
126
+ @tags = []
127
+ @tools = []
128
+ @resources = []
129
+ @extensions = {}
130
+ @config = {}
131
+ @examples = []
132
+ @tests = []
133
+
134
+ super(attributes)
135
+
136
+ # Ensure arrays and hashes are properly initialized even if nil was passed
137
+ @tags ||= []
138
+ @tools ||= []
139
+ @resources ||= []
140
+ @extensions ||= {}
141
+ @config ||= {}
142
+ @examples ||= []
143
+ @tests ||= []
144
+ end
145
+
146
+ # === Convenience accessors for framework extensions ===
147
+
148
+ # @return [Hash] ActiveAgent-specific configuration
149
+ def activeagent_config
150
+ extensions[:activeagent] || extensions["activeagent"] || {}
151
+ end
152
+
153
+ # @return [Hash] CrewAI-specific configuration
154
+ def crewai_config
155
+ extensions[:crewai] || extensions["crewai"] || {}
156
+ end
157
+
158
+ # @return [Hash] LangChain-specific configuration
159
+ def langchain_config
160
+ extensions[:langchain] || extensions["langchain"] || {}
161
+ end
162
+
163
+ # @return [Hash] Genkit/Dotprompt-specific configuration
164
+ def genkit_config
165
+ extensions[:genkit] || extensions["genkit"] || {}
166
+ end
167
+
168
+ # Convert manifest to a hash representation
169
+ #
170
+ # @return [Hash] Hash representation suitable for serialization
171
+ def to_h
172
+ {
173
+ name: name,
174
+ version: version,
175
+ description: description,
176
+ author: author,
177
+ license: license,
178
+ repository: repository,
179
+ tags: tags.presence,
180
+ extends: extends,
181
+ model: model,
182
+ config: config.presence,
183
+ input_schema: input_schema&.to_h,
184
+ output_schema: output_schema,
185
+ tools: tools.map(&:to_h).presence,
186
+ resources: resources.map(&:to_h).presence,
187
+ instructions: instructions,
188
+ template: template,
189
+ extensions: extensions.presence
190
+ }.compact
191
+ end
192
+
193
+ # Convert to JSON string
194
+ #
195
+ # @return [String] JSON representation
196
+ def to_json(*args)
197
+ to_h.to_json(*args)
198
+ end
199
+
200
+ # Check if this manifest has any tools defined
201
+ #
202
+ # @return [Boolean]
203
+ def has_tools?
204
+ tools.any?
205
+ end
206
+
207
+ # Check if this manifest has any resources defined
208
+ #
209
+ # @return [Boolean]
210
+ def has_resources?
211
+ resources.any?
212
+ end
213
+
214
+ # Check if this manifest extends another agent
215
+ #
216
+ # @return [Boolean]
217
+ def extends?
218
+ extends.present?
219
+ end
220
+
221
+ # Get tool by name
222
+ #
223
+ # @param tool_name [String, Symbol] Name of the tool
224
+ # @return [Tool, nil]
225
+ def tool(tool_name)
226
+ tools.find { |t| t.name.to_s == tool_name.to_s }
227
+ end
228
+
229
+ # ============================================
230
+ # Provenance & Checksums
231
+ # ============================================
232
+
233
+ # Generate MD5 checksum of manifest content
234
+ #
235
+ # @return [String] 32-character hex digest
236
+ def checksum
237
+ Digest::MD5.hexdigest(to_h.to_json)
238
+ end
239
+ alias_method :digest, :checksum
240
+
241
+ # Generate checksum of instructions only
242
+ #
243
+ # @return [String, nil] Hex digest or nil if no instructions
244
+ def instructions_checksum
245
+ return nil unless instructions.present?
246
+ Digest::MD5.hexdigest(instructions)
247
+ end
248
+
249
+ # Generate checksum of model configuration
250
+ #
251
+ # @return [String] Hex digest of model + config
252
+ def config_checksum
253
+ Digest::MD5.hexdigest({ model: model, config: config }.to_json)
254
+ end
255
+
256
+ # Generate full provenance record
257
+ #
258
+ # @return [Hash] Provenance data for tracing
259
+ def provenance
260
+ {
261
+ manifest_checksum: checksum,
262
+ instructions_checksum: instructions_checksum,
263
+ config_checksum: config_checksum,
264
+ name: name,
265
+ version: version,
266
+ model: model,
267
+ source_path: source_path,
268
+ source_format: source_format,
269
+ generated_at: Time.now.iso8601,
270
+ tools_checksums: tools.map { |t| { name: t.name, checksum: Digest::MD5.hexdigest(t.to_h.to_json) } }
271
+ }.compact
272
+ end
273
+
274
+ # Short identifier combining name, version, and checksum prefix
275
+ #
276
+ # @return [String] e.g., "research-agent@1.0.0#a1b2c3d4"
277
+ def fingerprint
278
+ "#{name}@#{version}##{checksum[0..7]}"
279
+ end
280
+
281
+ private
282
+
283
+ def validate_tools
284
+ tools.each_with_index do |tool, index|
285
+ next if tool.is_a?(Tool)
286
+
287
+ errors.add(:tools, "item at index #{index} must be a Tool instance")
288
+ end
289
+ end
290
+
291
+ def validate_extensions
292
+ return unless extensions.present?
293
+
294
+ unless extensions.is_a?(Hash)
295
+ errors.add(:extensions, "must be a hash")
296
+ return
297
+ end
298
+
299
+ # Validate known extension structures
300
+ if activeagent_config.present? && !activeagent_config.is_a?(Hash)
301
+ errors.add(:extensions, "activeagent config must be a hash")
302
+ end
303
+ end
304
+ end
305
+ end
306
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ # ParserRegistry manages format parsers and provides format detection.
6
+ #
7
+ # It enables automatic detection of file formats and routes parsing
8
+ # to the appropriate parser class.
9
+ #
10
+ # @example Parsing a file
11
+ # manifest = ParserRegistry.parse("agent.agent.md")
12
+ # manifest = ParserRegistry.parse("agent.prompt", format: :dotprompt)
13
+ #
14
+ # @example Registering a custom parser
15
+ # ParserRegistry.register(:custom, MyCustomParser)
16
+ #
17
+ class ParserRegistry
18
+ class << self
19
+ # Registered parsers by format
20
+ #
21
+ # @return [Hash{Symbol => Class}]
22
+ def parsers
23
+ @parsers ||= {}
24
+ end
25
+
26
+ # Register a parser for a format
27
+ #
28
+ # @param format [Symbol] Format identifier
29
+ # @param parser_class [Class] Parser class (must respond to .parse and .parse_string)
30
+ def register(format, parser_class)
31
+ parsers[format.to_sym] = parser_class
32
+ end
33
+
34
+ # Get parser for a format
35
+ #
36
+ # @param format [Symbol] Format identifier
37
+ # @return [Class] Parser class
38
+ # @raise [UnknownFormatError] if no parser is registered
39
+ def parser_for(format)
40
+ parsers[format.to_sym] || raise(UnknownFormatError, "No parser registered for format: #{format}")
41
+ end
42
+
43
+ # Detect format from file path
44
+ #
45
+ # @param path [String] File path
46
+ # @return [Symbol] Detected format
47
+ # @raise [UnknownFormatError] if format cannot be detected
48
+ def detect_format(path)
49
+ extension = File.extname(path).downcase
50
+
51
+ case extension
52
+ when ".md"
53
+ detect_markdown_format(path)
54
+ when ".prompt"
55
+ :dotprompt
56
+ when ".yaml", ".yml"
57
+ detect_yaml_format(path)
58
+ when ".json"
59
+ detect_json_format(path)
60
+ else
61
+ raise UnknownFormatError, "Cannot detect format for extension: #{extension}"
62
+ end
63
+ end
64
+
65
+ # Parse a file, auto-detecting format if not specified
66
+ #
67
+ # @param path [String] Path to the file
68
+ # @param format [Symbol, nil] Optional format override
69
+ # @return [Manifest] Parsed manifest
70
+ def parse(path, format: nil)
71
+ format ||= detect_format(path)
72
+ parser_for(format).parse(path)
73
+ end
74
+
75
+ # Parse content string with explicit format
76
+ #
77
+ # @param content [String] File content
78
+ # @param format [Symbol] Format identifier
79
+ # @return [Manifest] Parsed manifest
80
+ def parse_string(content, format:)
81
+ parser_for(format).parse_string(content)
82
+ end
83
+
84
+ # List all registered formats
85
+ #
86
+ # @return [Array<Symbol>]
87
+ def registered_formats
88
+ parsers.keys
89
+ end
90
+
91
+ # Alias for registered_formats
92
+ # @return [Array<Symbol>]
93
+ def formats
94
+ registered_formats
95
+ end
96
+
97
+ # Check if a format is supported
98
+ #
99
+ # @param format [Symbol] Format to check
100
+ # @return [Boolean]
101
+ def format_supported?(format)
102
+ parsers.key?(format.to_sym)
103
+ end
104
+
105
+ # Alias for format_supported?
106
+ # @return [Boolean]
107
+ def supports?(format)
108
+ format_supported?(format)
109
+ end
110
+
111
+ private
112
+
113
+ # Detect specific markdown format (agent.md vs prompt.md)
114
+ def detect_markdown_format(path)
115
+ basename = File.basename(path)
116
+
117
+ if basename.end_with?(".agent.md")
118
+ :agent_md
119
+ elsif basename.end_with?(".prompt.md")
120
+ :github_prompt
121
+ else
122
+ # Could be either - try to detect from content
123
+ detect_markdown_format_from_content(path)
124
+ end
125
+ end
126
+
127
+ # Detect markdown format from file content
128
+ def detect_markdown_format_from_content(path)
129
+ return :agent_md unless File.exist?(path)
130
+
131
+ content = File.read(path, encoding: "UTF-8")
132
+
133
+ # Look for distinctive markers in frontmatter
134
+ if content.match?(/^---\s*\n.*?^activeagent:/m)
135
+ :agent_md
136
+ elsif content.match?(/^---\s*\n.*?^agent:/m)
137
+ :github_prompt
138
+ else
139
+ # Default to agent_md for generic markdown with frontmatter
140
+ :agent_md
141
+ end
142
+ rescue
143
+ :agent_md
144
+ end
145
+
146
+ # Detect YAML format (CrewAI vs other)
147
+ def detect_yaml_format(path)
148
+ return :crewai unless File.exist?(path)
149
+
150
+ content = File.read(path, encoding: "UTF-8")
151
+
152
+ # CrewAI typically has role/goal/backstory pattern
153
+ if content.match?(/role:\s*[>\|]?\s*\n?\s*.+/i) &&
154
+ content.match?(/goal:\s*[>\|]?\s*\n?\s*.+/i)
155
+ :crewai
156
+ elsif content.match?(/has_context|activeagent/i)
157
+ :activeagent_yaml
158
+ else
159
+ :yaml
160
+ end
161
+ rescue
162
+ :yaml
163
+ end
164
+
165
+ # Detect JSON format
166
+ def detect_json_format(path)
167
+ return :json unless File.exist?(path)
168
+
169
+ content = File.read(path, encoding: "UTF-8")
170
+ data = JSON.parse(content)
171
+
172
+ if data["inputSchema"]
173
+ :mcp_tool
174
+ elsif data["main"] && data["files"]
175
+ :agent_package
176
+ else
177
+ :json
178
+ end
179
+ rescue
180
+ :json
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ module Parsers
6
+ # AgentMdParser handles the native .agent.md format.
7
+ #
8
+ # This is the primary format for ActiveAgent manifests, featuring
9
+ # YAML frontmatter with comprehensive metadata and a Markdown body
10
+ # containing instructions and Liquid templates.
11
+ #
12
+ # @example Basic .agent.md file
13
+ # ---
14
+ # name: research-assistant
15
+ # model: anthropic/claude-sonnet-4-20250514
16
+ # ---
17
+ #
18
+ # # Research Assistant
19
+ #
20
+ # You help users research topics thoroughly.
21
+ #
22
+ class AgentMdParser < BaseParser
23
+ class << self
24
+ def format_name
25
+ :agent_md
26
+ end
27
+
28
+ def parse_string(content)
29
+ frontmatter, body = extract_frontmatter(content)
30
+
31
+ Manifest.new(
32
+ # Meta
33
+ name: frontmatter["name"],
34
+ version: frontmatter["version"],
35
+ description: frontmatter["description"],
36
+ author: frontmatter["author"],
37
+ license: frontmatter["license"],
38
+ repository: frontmatter["repository"],
39
+ tags: parse_tags(frontmatter["tags"]),
40
+ extends: frontmatter["extends"],
41
+
42
+ # Model
43
+ model: normalize_model(frontmatter["model"]),
44
+ config: parse_config(frontmatter),
45
+
46
+ # Schemas
47
+ input_schema: parse_input_schema(frontmatter["input"]),
48
+ output_schema: frontmatter["output"],
49
+
50
+ # Tools & Resources
51
+ tools: parse_tools(frontmatter["tools"]),
52
+ resources: parse_resources(frontmatter["resources"]),
53
+
54
+ # Instructions
55
+ instructions: extract_instructions(body),
56
+ template: body.presence,
57
+
58
+ # Extensions
59
+ extensions: extract_extensions(frontmatter),
60
+
61
+ # Examples & Tests
62
+ examples: frontmatter["examples"] || [],
63
+ tests: frontmatter["tests"] || []
64
+ )
65
+ end
66
+
67
+ private
68
+
69
+ # Parse tags ensuring array format
70
+ def parse_tags(tags_data)
71
+ case tags_data
72
+ when Array
73
+ tags_data.map(&:to_s)
74
+ when String
75
+ tags_data.split(",").map(&:strip)
76
+ else
77
+ []
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ # Register the parser
84
+ ParserRegistry.register(:agent_md, AgentMdParser)
85
+ end
86
+ end
87
+ end