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,368 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ # Validator provides comprehensive validation for Manifests.
6
+ #
7
+ # Goes beyond ActiveModel validations to check semantic correctness,
8
+ # tool definitions, schema validity, and cross-field consistency.
9
+ #
10
+ # @example Validate a manifest
11
+ # errors = Validator.validate(manifest)
12
+ # if errors.empty?
13
+ # puts "Manifest is valid!"
14
+ # else
15
+ # errors.each { |e| puts "Error: #{e}" }
16
+ # end
17
+ #
18
+ # @example Check validity
19
+ # Validator.valid?(manifest) # => true/false
20
+ #
21
+ # @example Validate and raise
22
+ # Validator.validate!(manifest) # raises ValidationError if invalid
23
+ #
24
+ class Validator
25
+ # Known model providers for validation
26
+ KNOWN_PROVIDERS = %w[anthropic openai google meta cohere mistral].freeze
27
+
28
+ # Reserved names that cannot be used
29
+ RESERVED_NAMES = %w[agent manifest config settings default system].freeze
30
+
31
+ class << self
32
+ # Validate a manifest and return array of error messages
33
+ #
34
+ # @param manifest [Manifest] Manifest to validate
35
+ # @param strict [Boolean] Enable strict validation
36
+ # @return [Array<String>] Error messages (empty if valid)
37
+ def validate(manifest, strict: false)
38
+ errors = []
39
+
40
+ errors.concat(validate_meta(manifest))
41
+ errors.concat(validate_model(manifest))
42
+ errors.concat(validate_tools(manifest))
43
+ errors.concat(validate_resources(manifest))
44
+ errors.concat(validate_schemas(manifest))
45
+ errors.concat(validate_extensions(manifest))
46
+ errors.concat(validate_content(manifest))
47
+
48
+ errors.concat(validate_strict(manifest)) if strict
49
+
50
+ errors
51
+ end
52
+
53
+ # Check if a manifest is valid
54
+ #
55
+ # @param manifest [Manifest] Manifest to validate
56
+ # @param strict [Boolean] Enable strict validation
57
+ # @return [Boolean]
58
+ def valid?(manifest, strict: false)
59
+ validate(manifest, strict: strict).empty?
60
+ end
61
+
62
+ # Validate and raise if invalid
63
+ #
64
+ # @param manifest [Manifest] Manifest to validate
65
+ # @param strict [Boolean] Enable strict validation
66
+ # @raise [ValidationError] if manifest is invalid
67
+ # @return [Manifest] The validated manifest
68
+ def validate!(manifest, strict: false)
69
+ errors = validate(manifest, strict: strict)
70
+ raise ValidationError, "Invalid manifest: #{errors.join('; ')}" if errors.any?
71
+
72
+ manifest
73
+ end
74
+
75
+ private
76
+
77
+ # Validate meta fields
78
+ def validate_meta(manifest)
79
+ errors = []
80
+
81
+ # Name is required
82
+ if manifest.name.blank?
83
+ errors << "name is required"
84
+ else
85
+ # Name format
86
+ unless manifest.name =~ /\A[a-z][a-z0-9\-]*\z/
87
+ errors << "name must start with lowercase letter and contain only lowercase letters, numbers, and hyphens"
88
+ end
89
+
90
+ # Name length
91
+ if manifest.name.length > 100
92
+ errors << "name must be 100 characters or less"
93
+ end
94
+
95
+ # Reserved names
96
+ if RESERVED_NAMES.include?(manifest.name)
97
+ errors << "name '#{manifest.name}' is reserved"
98
+ end
99
+ end
100
+
101
+ # Version format
102
+ if manifest.version.present?
103
+ unless manifest.version =~ /\A\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?\z/
104
+ errors << "version must follow semantic versioning (e.g., 1.0.0, 1.0.0-beta.1)"
105
+ end
106
+ end
107
+
108
+ # Tags
109
+ if manifest.tags.is_a?(Array)
110
+ manifest.tags.each do |tag|
111
+ unless tag =~ /\A[a-z][a-z0-9\-]*\z/
112
+ errors << "tag '#{tag}' must be lowercase with hyphens only"
113
+ end
114
+ end
115
+ end
116
+
117
+ # Repository URL
118
+ if manifest.repository.present?
119
+ unless manifest.repository =~ %r{\Ahttps?://}
120
+ errors << "repository must be a valid URL"
121
+ end
122
+ end
123
+
124
+ errors
125
+ end
126
+
127
+ # Validate model configuration
128
+ def validate_model(manifest)
129
+ errors = []
130
+
131
+ return errors if manifest.model.blank?
132
+
133
+ # Model format - should be provider/model
134
+ if manifest.model.include?("/")
135
+ provider, model_name = manifest.model.split("/", 2)
136
+
137
+ # Warn about unknown providers (not an error)
138
+ unless KNOWN_PROVIDERS.include?(provider)
139
+ # This is just informational, not an error
140
+ end
141
+
142
+ if model_name.blank?
143
+ errors << "model identifier must include model name after provider"
144
+ end
145
+ end
146
+
147
+ # Config validation
148
+ if manifest.config.present?
149
+ # Temperature range
150
+ temp = manifest.config[:temperature] || manifest.config["temperature"]
151
+ if temp && (temp.to_f < 0 || temp.to_f > 2)
152
+ errors << "temperature must be between 0 and 2"
153
+ end
154
+
155
+ # Max tokens
156
+ max_tokens = manifest.config[:max_tokens] || manifest.config["max_tokens"]
157
+ if max_tokens && max_tokens.to_i <= 0
158
+ errors << "max_tokens must be a positive integer"
159
+ end
160
+
161
+ # Top P
162
+ top_p = manifest.config[:top_p] || manifest.config["top_p"]
163
+ if top_p && (top_p.to_f < 0 || top_p.to_f > 1)
164
+ errors << "top_p must be between 0 and 1"
165
+ end
166
+ end
167
+
168
+ errors
169
+ end
170
+
171
+ # Validate tool definitions
172
+ def validate_tools(manifest)
173
+ errors = []
174
+
175
+ return errors unless manifest.tools.is_a?(Array)
176
+
177
+ tool_names = Set.new
178
+
179
+ manifest.tools.each_with_index do |tool, index|
180
+ prefix = "tools[#{index}]"
181
+
182
+ if tool.reference?
183
+ # Reference validation
184
+ if tool.ref.blank?
185
+ errors << "#{prefix}: $ref cannot be blank"
186
+ end
187
+ else
188
+ # Inline tool validation
189
+ if tool.name.blank?
190
+ errors << "#{prefix}: name is required for inline tools"
191
+ else
192
+ # Check for duplicates
193
+ if tool_names.include?(tool.name)
194
+ errors << "#{prefix}: duplicate tool name '#{tool.name}'"
195
+ end
196
+ tool_names.add(tool.name)
197
+
198
+ # Name format
199
+ unless tool.name =~ /\A[a-zA-Z][a-zA-Z0-9_]*\z/
200
+ errors << "#{prefix}: name '#{tool.name}' must start with letter and contain only alphanumeric and underscore"
201
+ end
202
+ end
203
+
204
+ # Input schema validation
205
+ if tool.input_schema.present?
206
+ unless tool.input_schema.is_a?(Hash)
207
+ errors << "#{prefix}: inputSchema must be an object"
208
+ else
209
+ unless tool.input_schema["type"] == "object"
210
+ errors << "#{prefix}: inputSchema type should be 'object'"
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ errors
218
+ end
219
+
220
+ # Validate resource definitions
221
+ def validate_resources(manifest)
222
+ errors = []
223
+
224
+ return errors unless manifest.resources.is_a?(Array)
225
+
226
+ manifest.resources.each_with_index do |resource, index|
227
+ prefix = "resources[#{index}]"
228
+
229
+ if resource.uri.blank?
230
+ errors << "#{prefix}: uri is required"
231
+ else
232
+ # Basic URI format check
233
+ unless resource.uri =~ %r{\A[a-z][a-z0-9+.-]*://}i
234
+ errors << "#{prefix}: uri must be a valid URI with scheme"
235
+ end
236
+ end
237
+
238
+ # MIME type format
239
+ if resource.mime_type.present?
240
+ unless resource.mime_type =~ %r{\A[a-z]+/[a-z0-9.+-]+\z}i
241
+ errors << "#{prefix}: mimeType format is invalid"
242
+ end
243
+ end
244
+ end
245
+
246
+ errors
247
+ end
248
+
249
+ # Validate schemas
250
+ def validate_schemas(manifest)
251
+ errors = []
252
+
253
+ # Input schema
254
+ if manifest.input_schema.present?
255
+ begin
256
+ manifest.input_schema.to_json_schema
257
+ rescue StandardError => e
258
+ errors << "input schema is invalid: #{e.message}"
259
+ end
260
+ end
261
+
262
+ # Output schema
263
+ if manifest.output_schema.present?
264
+ unless manifest.output_schema.is_a?(Hash)
265
+ errors << "output schema must be an object"
266
+ end
267
+ end
268
+
269
+ errors
270
+ end
271
+
272
+ # Validate framework extensions
273
+ def validate_extensions(manifest)
274
+ errors = []
275
+
276
+ return errors unless manifest.extensions.is_a?(Hash)
277
+
278
+ # ActiveAgent extensions
279
+ aa_ext = manifest.extensions[:activeagent]
280
+ if aa_ext.is_a?(Hash)
281
+ # Class name format
282
+ if aa_ext[:class_name].present?
283
+ unless aa_ext[:class_name] =~ /\A[A-Z][a-zA-Z0-9]*Agent\z/
284
+ errors << "activeagent.class_name should follow Rails naming convention (e.g., MyAgent)"
285
+ end
286
+ end
287
+
288
+ # Concerns validation
289
+ if aa_ext[:concerns].is_a?(Array)
290
+ aa_ext[:concerns].each do |concern|
291
+ concern_name = concern.is_a?(Hash) ? concern.keys.first : concern
292
+ unless concern_name.to_s =~ /\Ahas_[a-z_]+\z/
293
+ errors << "activeagent.concerns: '#{concern_name}' should follow has_* naming pattern"
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ errors
300
+ end
301
+
302
+ # Validate content (instructions, template)
303
+ def validate_content(manifest)
304
+ errors = []
305
+
306
+ # Must have some form of content
307
+ if manifest.instructions.blank? && manifest.template.blank?
308
+ errors << "manifest must have instructions or template content"
309
+ end
310
+
311
+ # Template syntax check (basic Liquid validation)
312
+ if manifest.template.present?
313
+ # Check for unclosed tags
314
+ open_tags = manifest.template.scan(/\{%\s*(\w+)/).flatten
315
+ close_tags = manifest.template.scan(/\{%\s*end(\w+)/).flatten
316
+
317
+ # Simple balance check for common tags
318
+ %w[if for unless case].each do |tag|
319
+ opens = open_tags.count(tag)
320
+ closes = close_tags.count(tag)
321
+ if opens != closes
322
+ errors << "template has unbalanced {% #{tag} %} tags (#{opens} opens, #{closes} closes)"
323
+ end
324
+ end
325
+
326
+ # Check for unclosed variable tags
327
+ if manifest.template.count("{{") != manifest.template.count("}}")
328
+ errors << "template has unclosed {{ }} variable tags"
329
+ end
330
+ end
331
+
332
+ errors
333
+ end
334
+
335
+ # Strict validation (additional checks)
336
+ def validate_strict(manifest)
337
+ errors = []
338
+
339
+ # Description required in strict mode
340
+ if manifest.description.blank?
341
+ errors << "description is required in strict mode"
342
+ end
343
+
344
+ # Model required in strict mode
345
+ if manifest.model.blank?
346
+ errors << "model is required in strict mode"
347
+ end
348
+
349
+ # All tools must have descriptions
350
+ manifest.tools&.each_with_index do |tool, index|
351
+ next if tool.reference?
352
+
353
+ if tool.description.blank?
354
+ errors << "tools[#{index}]: description is required in strict mode"
355
+ end
356
+ end
357
+
358
+ # Version required in strict mode
359
+ if manifest.version.blank?
360
+ errors << "version is required in strict mode"
361
+ end
362
+
363
+ errors
364
+ end
365
+ end
366
+ end
367
+ end
368
+ end