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,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ # Picoschema provides bidirectional conversion between Picoschema
6
+ # (a compact, YAML-optimized schema format from Dotprompt) and JSON Schema.
7
+ #
8
+ # Picoschema is designed for human readability while JSON Schema provides
9
+ # full validation capabilities.
10
+ #
11
+ # @example Picoschema syntax
12
+ # # Simple types
13
+ # query: string, the search query
14
+ # limit?: integer # optional field
15
+ #
16
+ # # Enums
17
+ # status: string(draft, published, archived)
18
+ #
19
+ # # Arrays
20
+ # tags: [string]
21
+ #
22
+ # # Nested objects
23
+ # author: object
24
+ # name: string
25
+ # email: string
26
+ #
27
+ # # Array of objects
28
+ # comments: [object]
29
+ # author: string
30
+ # text: string
31
+ #
32
+ class Picoschema
33
+ SCALAR_TYPES = %w[string integer number boolean any].freeze
34
+
35
+ class << self
36
+ # Convert Picoschema to JSON Schema
37
+ #
38
+ # @param picoschema [Hash] Picoschema definition
39
+ # @return [Hash] JSON Schema object
40
+ def to_json_schema(picoschema)
41
+ return { "type" => "object", "properties" => {} } if picoschema.nil?
42
+ return picoschema if json_schema?(picoschema)
43
+
44
+ parse_object(picoschema)
45
+ end
46
+
47
+ # Convert JSON Schema to Picoschema
48
+ #
49
+ # @param json_schema [Hash] JSON Schema object
50
+ # @return [Hash] Picoschema definition
51
+ def from_json_schema(json_schema)
52
+ return {} if json_schema.nil?
53
+ return json_schema unless json_schema?(json_schema)
54
+
55
+ properties = json_schema["properties"] || json_schema[:properties] || {}
56
+ required = json_schema["required"] || json_schema[:required] || []
57
+
58
+ convert_properties(properties, required)
59
+ end
60
+
61
+ # Parse a single type string (e.g., "string, description")
62
+ #
63
+ # @param type_str [String] Type definition string
64
+ # @return [Hash] JSON Schema for the field
65
+ def parse_type_string(type_str)
66
+ return { "type" => "any" } if type_str.nil? || type_str.strip == "any"
67
+
68
+ str = type_str.to_s.strip
69
+
70
+ # Handle enum types first: string(a, b, c), description
71
+ # Need to split after the closing paren for enum types
72
+ if str =~ /\A(\w+\([^)]+\))(,\s*(.+))?\z/
73
+ type_part = ::Regexp.last_match(1).strip
74
+ description = ::Regexp.last_match(3)&.strip
75
+ else
76
+ # Normal split for non-enum types
77
+ parts = str.split(",", 2)
78
+ type_part = parts[0].strip
79
+ description = parts[1]&.strip
80
+ end
81
+
82
+ result = parse_type_part(type_part)
83
+ result["description"] = description if description.present?
84
+ result
85
+ end
86
+
87
+ private
88
+
89
+ # Check if a hash looks like JSON Schema
90
+ def json_schema?(schema)
91
+ return false unless schema.is_a?(Hash)
92
+
93
+ schema.key?("type") || schema.key?(:type) ||
94
+ schema.key?("properties") || schema.key?(:properties) ||
95
+ schema.key?("$schema") || schema.key?(:"$schema")
96
+ end
97
+
98
+ # Parse an object schema (hash of field definitions)
99
+ def parse_object(schema_hash)
100
+ properties = {}
101
+ required = []
102
+
103
+ schema_hash.each do |key, value|
104
+ field_name, optional = parse_field_name(key.to_s)
105
+ field_schema = parse_field_value(value)
106
+
107
+ properties[field_name] = field_schema
108
+ required << field_name unless optional
109
+ end
110
+
111
+ result = { "type" => "object", "properties" => properties }
112
+ result["required"] = required if required.any?
113
+ result
114
+ end
115
+
116
+ # Parse field name, detecting optional marker
117
+ def parse_field_name(key)
118
+ if key.end_with?("?")
119
+ [key.chomp("?"), true] # optional
120
+ else
121
+ [key, false] # required
122
+ end
123
+ end
124
+
125
+ # Parse a field value (could be type string, hash, or array)
126
+ def parse_field_value(value)
127
+ case value
128
+ when String
129
+ parse_type_string(value)
130
+ when Hash
131
+ # Nested object or already parsed schema
132
+ if json_schema?(value)
133
+ value
134
+ else
135
+ parse_object(value)
136
+ end
137
+ when Array
138
+ parse_array_type(value)
139
+ else
140
+ { "type" => "any" }
141
+ end
142
+ end
143
+
144
+ # Parse array type definition
145
+ def parse_array_type(array_value)
146
+ if array_value.empty?
147
+ { "type" => "array", "items" => {} }
148
+ elsif array_value.first.is_a?(Hash)
149
+ # Array of objects with nested schema
150
+ { "type" => "array", "items" => parse_object(array_value.first) }
151
+ else
152
+ # Array of scalar type
153
+ { "type" => "array", "items" => parse_type_string(array_value.first.to_s) }
154
+ end
155
+ end
156
+
157
+ # Parse the type part (before the comma)
158
+ def parse_type_part(type_part)
159
+ # Handle array shorthand: [string] or [object]
160
+ if type_part.start_with?("[") && type_part.end_with?("]")
161
+ inner = type_part[1..-2].strip
162
+ if inner == "object"
163
+ return { "type" => "array", "items" => { "type" => "object" } }
164
+ else
165
+ return { "type" => "array", "items" => parse_type_part(inner) }
166
+ end
167
+ end
168
+
169
+ # Handle enum: string(option1, option2, option3)
170
+ if type_part =~ /\A(\w+)\((.+)\)\z/
171
+ base_type = ::Regexp.last_match(1)
172
+ enum_str = ::Regexp.last_match(2)
173
+ enum_values = parse_enum_values(enum_str)
174
+ return { "type" => base_type, "enum" => enum_values }
175
+ end
176
+
177
+ # Handle nested object reference
178
+ if type_part == "object"
179
+ return { "type" => "object" }
180
+ end
181
+
182
+ # Handle scalar types
183
+ if SCALAR_TYPES.include?(type_part.downcase)
184
+ return { "type" => type_part.downcase }
185
+ end
186
+
187
+ # Unknown type - preserve as-is
188
+ { "type" => type_part }
189
+ end
190
+
191
+ # Parse enum values from string like "option1, option2, option3"
192
+ def parse_enum_values(enum_str)
193
+ enum_str.split(",").map do |val|
194
+ val = val.strip
195
+ # Remove quotes if present
196
+ val = val.gsub(/\A["']|["']\z/, "")
197
+ val
198
+ end
199
+ end
200
+
201
+ # Convert JSON Schema properties to Picoschema format
202
+ def convert_properties(properties, required_fields)
203
+ result = {}
204
+
205
+ properties.each do |name, schema|
206
+ optional = !required_fields.include?(name.to_s)
207
+ key = optional ? "#{name}?" : name.to_s
208
+ result[key] = convert_schema_to_pico(schema)
209
+ end
210
+
211
+ result
212
+ end
213
+
214
+ # Convert a single JSON Schema field to Picoschema string
215
+ def convert_schema_to_pico(schema)
216
+ type = schema["type"] || schema[:type]
217
+ desc = schema["description"] || schema[:description]
218
+ enum_values = schema["enum"] || schema[:enum]
219
+
220
+ # Handle array types
221
+ if type == "array"
222
+ items = schema["items"] || schema[:items] || {}
223
+ items_type = items["type"] || items[:type] || "any"
224
+ if items_type == "object"
225
+ # Complex - return as hash
226
+ return [convert_properties(items["properties"] || {}, items["required"] || [])]
227
+ else
228
+ return "[#{items_type}]"
229
+ end
230
+ end
231
+
232
+ # Handle object types
233
+ if type == "object" && (schema["properties"] || schema[:properties])
234
+ return convert_properties(
235
+ schema["properties"] || schema[:properties],
236
+ schema["required"] || schema[:required] || []
237
+ )
238
+ end
239
+
240
+ # Build picoschema string
241
+ pico = type.to_s
242
+ if enum_values&.any?
243
+ pico = "#{type}(#{enum_values.join(", ")})"
244
+ end
245
+ if desc.present?
246
+ pico = "#{pico}, #{desc}"
247
+ end
248
+
249
+ pico
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidAgent
4
+ module AgentManifest
5
+ module Registry
6
+ # Auth handles token management for the ActiveAgents registry.
7
+ #
8
+ # Tokens are stored in ~/.activeagent/token by default.
9
+ # Can also be configured via environment variable.
10
+ #
11
+ # @example Check if logged in
12
+ # Registry::Auth.logged_in?
13
+ #
14
+ # @example Get current token
15
+ # token = Registry::Auth.token
16
+ #
17
+ # @example Save a token
18
+ # Registry::Auth.save_token("aa_live_xxx")
19
+ #
20
+ class Auth
21
+ TOKEN_PATH = File.expand_path("~/.activeagent/token")
22
+ TOKEN_ENV_VAR = "ACTIVEAGENT_TOKEN"
23
+
24
+ class << self
25
+ # Get the current authentication token
26
+ #
27
+ # Checks in order:
28
+ # 1. Environment variable (ACTIVEAGENT_TOKEN)
29
+ # 2. Token file (~/.activeagent/token)
30
+ #
31
+ # @return [String, nil] The token or nil if not authenticated
32
+ def token
33
+ # Check environment variable first
34
+ env_token = ENV[TOKEN_ENV_VAR]
35
+ return env_token if env_token.present?
36
+
37
+ # Check token file
38
+ return nil unless File.exist?(TOKEN_PATH)
39
+
40
+ File.read(TOKEN_PATH).strip.presence
41
+ end
42
+
43
+ # Save a token to the token file
44
+ #
45
+ # @param new_token [String] The token to save
46
+ # @return [Boolean] true if saved successfully
47
+ def save_token(new_token)
48
+ # Ensure directory exists
49
+ FileUtils.mkdir_p(File.dirname(TOKEN_PATH))
50
+
51
+ # Write token with secure permissions
52
+ File.write(TOKEN_PATH, new_token.strip)
53
+ File.chmod(0o600, TOKEN_PATH)
54
+
55
+ true
56
+ rescue StandardError => e
57
+ raise RegistryError, "Failed to save token: #{e.message}"
58
+ end
59
+
60
+ # Clear the saved token
61
+ #
62
+ # @return [Boolean] true if cleared successfully
63
+ def clear_token
64
+ return true unless File.exist?(TOKEN_PATH)
65
+
66
+ File.delete(TOKEN_PATH)
67
+ true
68
+ rescue StandardError => e
69
+ raise RegistryError, "Failed to clear token: #{e.message}"
70
+ end
71
+
72
+ # Check if user is logged in
73
+ #
74
+ # @return [Boolean]
75
+ def logged_in?
76
+ token.present?
77
+ end
78
+
79
+ # Require authentication, raising if not logged in
80
+ #
81
+ # @raise [RegistryError] if not authenticated
82
+ # @return [String] The token
83
+ def require_token!
84
+ t = token
85
+ raise RegistryError, "Not authenticated. Run 'activeagent login' or set #{TOKEN_ENV_VAR}" unless t
86
+
87
+ t
88
+ end
89
+
90
+ # Get authorization header for API requests
91
+ #
92
+ # @return [Hash] Authorization header
93
+ def auth_header
94
+ t = token
95
+ return {} unless t
96
+
97
+ { "Authorization" => "Bearer #{t}" }
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end