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,381 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "agent_manifest/errors"
4
+ require_relative "agent_manifest/manifest"
5
+ require_relative "agent_manifest/tool"
6
+ require_relative "agent_manifest/resource"
7
+ require_relative "agent_manifest/input_schema"
8
+ require_relative "agent_manifest/picoschema"
9
+ require_relative "agent_manifest/parser_registry"
10
+ require_relative "agent_manifest/parsers/base_parser"
11
+ require_relative "agent_manifest/parsers/agent_md_parser"
12
+ require_relative "agent_manifest/parsers/dotprompt_parser"
13
+ require_relative "agent_manifest/parsers/crewai_parser"
14
+ require_relative "agent_manifest/parsers/github_prompt_parser"
15
+ require_relative "agent_manifest/exporter_registry"
16
+ require_relative "agent_manifest/exporters/base_exporter"
17
+ require_relative "agent_manifest/exporters/agent_md_exporter"
18
+ require_relative "agent_manifest/exporters/dotprompt_exporter"
19
+ require_relative "agent_manifest/exporters/crewai_exporter"
20
+ require_relative "agent_manifest/validator"
21
+ require_relative "agent_manifest/agent_builder"
22
+ require_relative "agent_manifest/registry/auth"
23
+ require_relative "agent_manifest/registry/client"
24
+
25
+ require "digest"
26
+ require "json"
27
+
28
+ module SolidAgent
29
+ # AgentManifest provides a unified interface for working with agent definition files.
30
+ #
31
+ # Supports multiple formats:
32
+ # - `.agent.md` - Native format with full feature support
33
+ # - `.prompt` - Google Dotprompt format
34
+ # - `agents.yaml` - CrewAI format
35
+ # - `.prompt.md` - GitHub Copilot format
36
+ #
37
+ # @example Parse a manifest file
38
+ # manifest = SolidAgent::AgentManifest.parse("research_assistant.agent.md")
39
+ #
40
+ # @example Parse with auto-detection
41
+ # manifest = SolidAgent::AgentManifest.parse("agents.yaml")
42
+ #
43
+ # @example Export to different format
44
+ # content = SolidAgent::AgentManifest.export(manifest, :dotprompt)
45
+ #
46
+ # @example Convert between formats
47
+ # SolidAgent::AgentManifest.convert("agent.yaml", :agent_md, "agent.agent.md")
48
+ #
49
+ # @example Load as agent class
50
+ # klass = SolidAgent::AgentManifest.load_agent("research.agent.md")
51
+ # agent = klass.new
52
+ #
53
+ # @example Validate a manifest
54
+ # errors = SolidAgent::AgentManifest.validate("agent.agent.md")
55
+ #
56
+ # @example Load from any source
57
+ # manifest = SolidAgent::AgentManifest.load("https://example.com/agent.md")
58
+ # manifest = SolidAgent::AgentManifest.load({ name: "quick", model: "gpt-4o" })
59
+ #
60
+ module AgentManifest
61
+ class << self
62
+ # ============================================
63
+ # Unified Loading API
64
+ # ============================================
65
+
66
+ # Load manifest from any source with auto-detection
67
+ #
68
+ # @param source [String, Hash] File path, URL, JSON string, YAML string, or Hash
69
+ # @param format [Symbol] Force format (:agent_md, :json, :yaml, :dotprompt, :crewai)
70
+ # @return [Manifest] Parsed manifest
71
+ def load(source, format: nil)
72
+ case source
73
+ when Hash
74
+ from_hash(source)
75
+ when ->(s) { s.is_a?(String) && s.match?(%r{\Ahttps?://}) }
76
+ from_url(source, format: format)
77
+ when ->(s) { s.is_a?(String) && File.exist?(s) }
78
+ from_file(source, format: format)
79
+ when String
80
+ from_string(source, format: format || detect_string_format(source))
81
+ else
82
+ raise ArgumentError, "Unknown source type: #{source.class}"
83
+ end
84
+ end
85
+
86
+ # Load manifest from URL
87
+ #
88
+ # @param url [String] URL to fetch manifest from
89
+ # @param format [Symbol] Force format (auto-detected if nil)
90
+ # @return [Manifest] Parsed manifest
91
+ def from_url(url, format: nil)
92
+ require "net/http"
93
+ require "uri"
94
+
95
+ uri = URI.parse(url)
96
+ response = Net::HTTP.get_response(uri)
97
+
98
+ unless response.is_a?(Net::HTTPSuccess)
99
+ raise SolidAgent::LoadError, "Failed to fetch #{url}: #{response.code}"
100
+ end
101
+
102
+ format ||= detect_format_from_url(url) || detect_format_from_content_type(response)
103
+ manifest = from_string(response.body, format: format)
104
+ manifest.source_path = url
105
+ manifest
106
+ end
107
+
108
+ # Load manifest from file
109
+ #
110
+ # @param path [String] Path to manifest file
111
+ # @param format [Symbol] Force format (auto-detected if nil)
112
+ # @return [Manifest] Parsed manifest
113
+ def from_file(path, format: nil)
114
+ parse(path, format: format)
115
+ end
116
+
117
+ # Load manifest from string content
118
+ #
119
+ # @param content [String] Manifest content
120
+ # @param format [Symbol] Content format
121
+ # @return [Manifest] Parsed manifest
122
+ def from_string(content, format:)
123
+ parse_string(content, format: format)
124
+ end
125
+
126
+ # Load manifest from JSON string
127
+ #
128
+ # @param json_string [String] JSON content
129
+ # @return [Manifest] Parsed manifest
130
+ def from_json(json_string)
131
+ from_hash(JSON.parse(json_string, symbolize_names: true))
132
+ end
133
+
134
+ # Load manifest from YAML string
135
+ #
136
+ # @param yaml_string [String] YAML content
137
+ # @return [Manifest] Parsed manifest
138
+ def from_yaml(yaml_string)
139
+ require "yaml"
140
+ from_hash(YAML.safe_load(yaml_string, symbolize_names: true, permitted_classes: [Symbol]))
141
+ end
142
+
143
+ # Load manifest from Hash
144
+ #
145
+ # @param hash [Hash] Manifest attributes
146
+ # @return [Manifest] Parsed manifest
147
+ def from_hash(hash)
148
+ Manifest.new(**hash.transform_keys(&:to_sym))
149
+ end
150
+
151
+ # Build agent class from manifest
152
+ #
153
+ # @param manifest [Manifest] Manifest to build from
154
+ # @param base_class [Class] Parent class
155
+ # @param class_name [String] Name to register constant as
156
+ # @return [Class] Generated agent class
157
+ def build(manifest, base_class: nil, class_name: nil)
158
+ AgentBuilder.build(manifest, base_class: base_class, class_name: class_name)
159
+ end
160
+
161
+ # Build agent instance from manifest
162
+ #
163
+ # @param manifest [Manifest] Manifest to build from
164
+ # @param params [Hash] Parameters for the agent
165
+ # @return [Object] Agent instance
166
+ def build_instance(manifest, params: {}, **options)
167
+ AgentBuilder.build_instance(manifest, params: params, **options)
168
+ end
169
+
170
+ # ============================================
171
+ # Provenance & Checksums
172
+ # ============================================
173
+
174
+ # Generate checksum for any content
175
+ #
176
+ # @param content [String, Hash, Object] Content to checksum
177
+ # @return [String] MD5 hex digest
178
+ def checksum(content)
179
+ data = case content
180
+ when String then content
181
+ when Hash then content.to_json
182
+ else content.to_s
183
+ end
184
+ Digest::MD5.hexdigest(data)
185
+ end
186
+
187
+ # Generate provenance record for a manifest
188
+ #
189
+ # @param manifest [Manifest] Manifest to generate provenance for
190
+ # @return [Hash] Provenance record with checksums
191
+ def provenance(manifest)
192
+ {
193
+ manifest_checksum: checksum(manifest.to_h),
194
+ instructions_checksum: manifest.instructions ? checksum(manifest.instructions) : nil,
195
+ model: manifest.model,
196
+ version: manifest.version,
197
+ source_path: manifest.source_path,
198
+ source_format: manifest.source_format,
199
+ generated_at: Time.now.iso8601,
200
+ tools: manifest.tools&.map { |t| { name: t.name, checksum: checksum(t.to_h) } }
201
+ }.compact
202
+ end
203
+
204
+ # ============================================
205
+ # Original API (maintained for compatibility)
206
+ # ============================================
207
+
208
+ # Parse a manifest file
209
+ #
210
+ # @param path [String] Path to manifest file
211
+ # @param format [Symbol, nil] Force specific format (auto-detected if nil)
212
+ # @return [Manifest] Parsed manifest
213
+ # @raise [ParseError] if parsing fails
214
+ # @raise [UnknownFormatError] if format cannot be determined
215
+ def parse(path, format: nil)
216
+ ParserRegistry.parse(path, format: format)
217
+ end
218
+
219
+ # Parse manifest content from a string
220
+ #
221
+ # @param content [String] Manifest content
222
+ # @param format [Symbol] Format of the content
223
+ # @return [Manifest] Parsed manifest
224
+ def parse_string(content, format:)
225
+ ParserRegistry.parse_string(content, format: format)
226
+ end
227
+
228
+ # Export a manifest to a format string
229
+ #
230
+ # @param manifest [Manifest] Manifest to export
231
+ # @param format [Symbol] Target format (:agent_md, :dotprompt, :crewai)
232
+ # @param options [Hash] Format-specific options
233
+ # @return [String] Exported content
234
+ def export(manifest, format, **options)
235
+ ExporterRegistry.export(manifest, format, **options)
236
+ end
237
+
238
+ # Export a manifest to a file
239
+ #
240
+ # @param manifest [Manifest] Manifest to export
241
+ # @param format [Symbol] Target format
242
+ # @param path [String] Output file path
243
+ # @param options [Hash] Format-specific options
244
+ # @return [String] The path written to
245
+ def export_to_file(manifest, format, path, **options)
246
+ ExporterRegistry.export_to_file(manifest, format, path, **options)
247
+ end
248
+
249
+ # Convert a manifest file between formats
250
+ #
251
+ # @param input_path [String] Source file path
252
+ # @param output_format [Symbol] Target format
253
+ # @param output_path [String, nil] Output path (auto-generated if nil)
254
+ # @return [String] Output path
255
+ def convert(input_path, output_format, output_path = nil)
256
+ ExporterRegistry.convert(input_path, output_format, output_path)
257
+ end
258
+
259
+ # Load an agent class from a manifest file
260
+ #
261
+ # @param path [String] Path to manifest file
262
+ # @param base_class [Class, nil] Base class to inherit from
263
+ # @param class_name [String, nil] Optional class name for registration
264
+ # @return [Class] The generated agent class
265
+ def load_agent(path, base_class: nil, class_name: nil)
266
+ manifest = parse(path)
267
+ AgentBuilder.build(manifest, base_class: base_class, class_name: class_name)
268
+ end
269
+
270
+ # Load and instantiate an agent from a manifest file
271
+ #
272
+ # @param path [String] Path to manifest file
273
+ # @param params [Hash] Parameters to pass to the agent
274
+ # @param options [Hash] Options passed to build
275
+ # @return [Object] The instantiated agent
276
+ def load_agent_instance(path, params: {}, **options)
277
+ manifest = parse(path)
278
+ AgentBuilder.build_instance(manifest, params: params, **options)
279
+ end
280
+
281
+ # Validate a manifest file or object
282
+ #
283
+ # @param path_or_manifest [String, Manifest] Path to file or Manifest object
284
+ # @param strict [Boolean] Enable strict validation
285
+ # @return [Array<String>] Array of error messages (empty if valid)
286
+ def validate(path_or_manifest, strict: false)
287
+ manifest = path_or_manifest.is_a?(String) ? parse(path_or_manifest) : path_or_manifest
288
+ Validator.validate(manifest, strict: strict)
289
+ end
290
+
291
+ # Check if a manifest is valid
292
+ #
293
+ # @param path_or_manifest [String, Manifest] Path to file or Manifest object
294
+ # @param strict [Boolean] Enable strict validation
295
+ # @return [Boolean]
296
+ def valid?(path_or_manifest, strict: false)
297
+ validate(path_or_manifest, strict: strict).empty?
298
+ end
299
+
300
+ # Validate and raise if invalid
301
+ #
302
+ # @param path_or_manifest [String, Manifest] Path to file or Manifest object
303
+ # @param strict [Boolean] Enable strict validation
304
+ # @raise [ValidationError] if invalid
305
+ # @return [Manifest] The validated manifest
306
+ def validate!(path_or_manifest, strict: false)
307
+ manifest = path_or_manifest.is_a?(String) ? parse(path_or_manifest) : path_or_manifest
308
+ Validator.validate!(manifest, strict: strict)
309
+ end
310
+
311
+ # List supported parser formats
312
+ #
313
+ # @return [Array<Symbol>]
314
+ def parser_formats
315
+ ParserRegistry.formats
316
+ end
317
+
318
+ # List supported exporter formats
319
+ #
320
+ # @return [Array<Symbol>]
321
+ def exporter_formats
322
+ ExporterRegistry.formats
323
+ end
324
+
325
+ # Detect the format of a file
326
+ #
327
+ # @param path [String] Path to file
328
+ # @return [Symbol] Detected format
329
+ # @raise [UnknownFormatError] if format cannot be determined
330
+ def detect_format(path)
331
+ ParserRegistry.detect_format(path)
332
+ end
333
+
334
+ private
335
+
336
+ # Detect format from string content
337
+ def detect_string_format(content)
338
+ content = content.strip
339
+ if content.start_with?("{") || content.start_with?("[")
340
+ :json
341
+ elsif content.start_with?("---")
342
+ # Could be YAML frontmatter (agent.md) or pure YAML
343
+ if content.match?(/\n---\s*\n/)
344
+ :agent_md
345
+ else
346
+ :yaml
347
+ end
348
+ elsif content.match?(/^\w+:\s/)
349
+ :yaml
350
+ elsif content.match?(/^#\s/)
351
+ :agent_md
352
+ else
353
+ :yaml
354
+ end
355
+ end
356
+
357
+ # Detect format from URL path
358
+ def detect_format_from_url(url)
359
+ require "uri"
360
+ path = URI.parse(url).path
361
+ case File.extname(path)
362
+ when ".json" then :json
363
+ when ".yaml", ".yml" then :yaml
364
+ when ".md" then :agent_md
365
+ when ".prompt" then :dotprompt
366
+ end
367
+ end
368
+
369
+ # Detect format from HTTP content-type
370
+ def detect_format_from_content_type(response)
371
+ content_type = response["content-type"].to_s
372
+ case content_type
373
+ when /json/ then :json
374
+ when /yaml/ then :yaml
375
+ when /markdown/ then :agent_md
376
+ else nil
377
+ end
378
+ end
379
+ end
380
+ end
381
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace SolidAgent
6
+
7
+ # Add generators to the Rails generator lookup path
8
+ config.generators do |g|
9
+ g.test_framework :minitest
10
+ end
11
+
12
+ initializer "solid_agent.load_generators" do
13
+ # Generators are automatically loaded from lib/generators
14
+ end
15
+ end
16
+ end