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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/LICENSE +21 -0
- data/README.md +321 -0
- data/Rakefile +32 -0
- data/docs/agent-md-spec.md +803 -0
- data/docs/parser-design.md +1369 -0
- data/docs/registry-api.md +882 -0
- data/examples/README.md +60 -0
- data/examples/manifests/changelog_writer.agent.md +81 -0
- data/examples/manifests/usage.rb +96 -0
- data/examples/memory_handoff/app/agents/researcher_agent.rb +36 -0
- data/examples/memory_handoff/app/agents/writer_agent.rb +41 -0
- data/examples/memory_handoff/usage.rb +45 -0
- data/examples/persistent_conversation/app/agents/support_agent.rb +59 -0
- data/examples/persistent_conversation/app/controllers/support_conversations_controller.rb +24 -0
- data/examples/persistent_conversation/app/views/agents/support/instructions.md.erb +8 -0
- data/examples/persistent_conversation/usage.rb +51 -0
- data/examples/reasoning/app/agents/analysis_agent.rb +52 -0
- data/examples/reasoning/usage.rb +52 -0
- data/examples/run_tracking/app/agents/report_agent.rb +30 -0
- data/examples/run_tracking/app/controllers/agent_runs_controller.rb +43 -0
- data/examples/run_tracking/app/jobs/document_analysis_job.rb +17 -0
- data/examples/run_tracking/app/services/document_analysis_run.rb +68 -0
- data/examples/run_tracking/usage.rb +85 -0
- data/examples/tool_streaming/app/agents/browser_agent.rb +65 -0
- data/examples/tool_streaming/app/channels/tool_status_channel.rb +24 -0
- data/examples/tool_streaming/app/views/browser_agent/tools/fetch_url.json.erb +15 -0
- data/examples/tool_streaming/usage.rb +47 -0
- data/lib/generators/solid_agent/agent/agent_generator.rb +95 -0
- data/lib/generators/solid_agent/agent/templates/action.text.erb +10 -0
- data/lib/generators/solid_agent/agent/templates/agent.rb.erb +93 -0
- data/lib/generators/solid_agent/context/context_generator.rb +124 -0
- data/lib/generators/solid_agent/context/templates/context_model.rb.erb +134 -0
- data/lib/generators/solid_agent/context/templates/create_context.rb.erb +32 -0
- data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +46 -0
- data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +37 -0
- data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +51 -0
- data/lib/generators/solid_agent/context/templates/message_model.rb.erb +47 -0
- data/lib/generators/solid_agent/install/install_generator.rb +92 -0
- data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +171 -0
- data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +76 -0
- data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
- data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
- data/lib/generators/solid_agent/install/templates/agent_message.rb.erb +76 -0
- data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
- data/lib/generators/solid_agent/install/templates/create_agent_contexts.rb.erb +32 -0
- data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +51 -0
- data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
- data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +38 -0
- data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
- data/lib/generators/solid_agent/install/templates/initializer.rb.erb +51 -0
- data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
- data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
- data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
- data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
- data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
- data/lib/generators/solid_agent/tool/templates/tool.json.erb +19 -0
- data/lib/generators/solid_agent/tool/tool_generator.rb +117 -0
- data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
- data/lib/solid_agent/agent_manifest/errors.rb +26 -0
- data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
- data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
- data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
- data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
- data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
- data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
- data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
- data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
- data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
- data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
- data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
- data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
- data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
- data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
- data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
- data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
- data/lib/solid_agent/agent_manifest/resource.rb +103 -0
- data/lib/solid_agent/agent_manifest/tool.rb +160 -0
- data/lib/solid_agent/agent_manifest/validator.rb +368 -0
- data/lib/solid_agent/agent_manifest.rb +381 -0
- data/lib/solid_agent/engine.rb +16 -0
- data/lib/solid_agent/has_context.rb +670 -0
- data/lib/solid_agent/has_memory.rb +136 -0
- data/lib/solid_agent/has_reasons.rb +230 -0
- data/lib/solid_agent/has_tools.rb +257 -0
- data/lib/solid_agent/model_naming.rb +42 -0
- data/lib/solid_agent/model_pricing.rb +93 -0
- data/lib/solid_agent/reasonable/reason.rb +205 -0
- data/lib/solid_agent/reasonable.rb +181 -0
- data/lib/solid_agent/records/agent.rb +520 -0
- data/lib/solid_agent/records/agent_run.rb +520 -0
- data/lib/solid_agent/records/agent_template.rb +142 -0
- data/lib/solid_agent/records/agent_version.rb +141 -0
- data/lib/solid_agent/records/ownable.rb +130 -0
- data/lib/solid_agent/records.rb +152 -0
- data/lib/solid_agent/run_fingerprint.rb +51 -0
- data/lib/solid_agent/streams_tool_updates.rb +178 -0
- data/lib/solid_agent/tool_cache.rb +91 -0
- data/lib/solid_agent/version.rb +5 -0
- data/lib/solid_agent.rb +95 -0
- data/sig/solid_agent.rbs +4 -0
- 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
|