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,1369 @@
|
|
|
1
|
+
# Agent Parser Design
|
|
2
|
+
|
|
3
|
+
A modular parser architecture that supports multiple agent definition formats with a unified internal representation.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ AgentManifest │
|
|
10
|
+
│ (Unified Internal Model) │
|
|
11
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
12
|
+
▲
|
|
13
|
+
│
|
|
14
|
+
┌───────────────────────┼───────────────────────┐
|
|
15
|
+
│ │ │
|
|
16
|
+
│ │ │
|
|
17
|
+
┌────────┴────────┐ ┌─────────┴─────────┐ ┌────────┴────────┐
|
|
18
|
+
│ AgentMdParser │ │ DotpromptParser │ │ CrewAIParser │
|
|
19
|
+
│ (.agent.md) │ │ (.prompt) │ │ (agents.yaml) │
|
|
20
|
+
└─────────────────┘ └───────────────────┘ └─────────────────┘
|
|
21
|
+
│ │ │
|
|
22
|
+
│ │ │
|
|
23
|
+
┌────────┴────────┐ ┌─────────┴─────────┐ ┌────────┴────────┐
|
|
24
|
+
│ Format A │ │ Format B │ │ Format C │
|
|
25
|
+
│ .agent.md │ │ .prompt │ │ agents.yaml │
|
|
26
|
+
└─────────────────┘ └───────────────────┘ └─────────────────┘
|
|
27
|
+
|
|
28
|
+
│
|
|
29
|
+
▼
|
|
30
|
+
┌───────────────────────┼───────────────────────┐
|
|
31
|
+
│ │ │
|
|
32
|
+
┌────────┴────────┐ ┌─────────┴─────────┐ ┌────────┴────────┐
|
|
33
|
+
│ ActiveAgent │ │ CrewAI │ │ LangChain │
|
|
34
|
+
│ Exporter │ │ Exporter │ │ Exporter │
|
|
35
|
+
└─────────────────┘ └───────────────────┘ └─────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Core Components
|
|
39
|
+
|
|
40
|
+
### 1. AgentManifest (Unified Model)
|
|
41
|
+
|
|
42
|
+
The canonical internal representation that all parsers produce and exporters consume:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# lib/agent_manifest/manifest.rb
|
|
46
|
+
module AgentManifest
|
|
47
|
+
class Manifest
|
|
48
|
+
attr_accessor :name, :version, :description, :author, :license,
|
|
49
|
+
:repository, :tags, :extends
|
|
50
|
+
|
|
51
|
+
attr_accessor :model, :config
|
|
52
|
+
|
|
53
|
+
attr_accessor :input_schema, :output_schema
|
|
54
|
+
|
|
55
|
+
attr_accessor :tools, :resources
|
|
56
|
+
|
|
57
|
+
attr_accessor :instructions, :template
|
|
58
|
+
|
|
59
|
+
attr_accessor :extensions # Framework-specific: { activeagent: {}, crewai: {} }
|
|
60
|
+
|
|
61
|
+
attr_accessor :examples, :tests
|
|
62
|
+
|
|
63
|
+
attr_accessor :source_format, :source_path # For round-trip preservation
|
|
64
|
+
|
|
65
|
+
def initialize(attributes = {})
|
|
66
|
+
attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
|
|
67
|
+
@extensions ||= {}
|
|
68
|
+
@tools ||= []
|
|
69
|
+
@resources ||= []
|
|
70
|
+
@tags ||= []
|
|
71
|
+
@examples ||= []
|
|
72
|
+
@tests ||= []
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_h
|
|
76
|
+
{
|
|
77
|
+
name: name,
|
|
78
|
+
version: version,
|
|
79
|
+
description: description,
|
|
80
|
+
author: author,
|
|
81
|
+
license: license,
|
|
82
|
+
repository: repository,
|
|
83
|
+
tags: tags,
|
|
84
|
+
extends: extends,
|
|
85
|
+
model: model,
|
|
86
|
+
config: config,
|
|
87
|
+
input_schema: input_schema,
|
|
88
|
+
output_schema: output_schema,
|
|
89
|
+
tools: tools.map(&:to_h),
|
|
90
|
+
resources: resources.map(&:to_h),
|
|
91
|
+
instructions: instructions,
|
|
92
|
+
template: template,
|
|
93
|
+
extensions: extensions
|
|
94
|
+
}.compact
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Convenience accessors for framework extensions
|
|
98
|
+
def activeagent_config
|
|
99
|
+
extensions[:activeagent] || {}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def crewai_config
|
|
103
|
+
extensions[:crewai] || {}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def langchain_config
|
|
107
|
+
extensions[:langchain] || {}
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class Tool
|
|
112
|
+
attr_accessor :name, :description, :input_schema, :ref
|
|
113
|
+
|
|
114
|
+
def initialize(attributes = {})
|
|
115
|
+
attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def reference?
|
|
119
|
+
ref.present?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def to_h
|
|
123
|
+
if reference?
|
|
124
|
+
{ "$ref" => ref }
|
|
125
|
+
else
|
|
126
|
+
{
|
|
127
|
+
name: name,
|
|
128
|
+
description: description,
|
|
129
|
+
inputSchema: input_schema
|
|
130
|
+
}.compact
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# MCP-compatible JSON output
|
|
135
|
+
def to_mcp_json
|
|
136
|
+
{
|
|
137
|
+
name: name,
|
|
138
|
+
description: description,
|
|
139
|
+
inputSchema: input_schema
|
|
140
|
+
}.to_json
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class Resource
|
|
145
|
+
attr_accessor :name, :description, :uri, :mime_type
|
|
146
|
+
|
|
147
|
+
def initialize(attributes = {})
|
|
148
|
+
attributes.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def to_h
|
|
152
|
+
{
|
|
153
|
+
name: name,
|
|
154
|
+
description: description,
|
|
155
|
+
uri: uri,
|
|
156
|
+
mimeType: mime_type
|
|
157
|
+
}.compact
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class InputSchema
|
|
162
|
+
attr_accessor :schema, :format # format: :picoschema or :json_schema
|
|
163
|
+
|
|
164
|
+
def initialize(schema:, format: :picoschema)
|
|
165
|
+
@schema = schema
|
|
166
|
+
@format = format
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def to_json_schema
|
|
170
|
+
case format
|
|
171
|
+
when :picoschema
|
|
172
|
+
PicoschemaParser.to_json_schema(schema)
|
|
173
|
+
when :json_schema
|
|
174
|
+
schema
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def to_picoschema
|
|
179
|
+
case format
|
|
180
|
+
when :picoschema
|
|
181
|
+
schema
|
|
182
|
+
when :json_schema
|
|
183
|
+
PicoschemaParser.from_json_schema(schema)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### 2. Parser Registry
|
|
191
|
+
|
|
192
|
+
```ruby
|
|
193
|
+
# lib/agent_manifest/parser_registry.rb
|
|
194
|
+
module AgentManifest
|
|
195
|
+
class ParserRegistry
|
|
196
|
+
class << self
|
|
197
|
+
def parsers
|
|
198
|
+
@parsers ||= {}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def register(format, parser_class)
|
|
202
|
+
parsers[format.to_sym] = parser_class
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def parser_for(format)
|
|
206
|
+
parsers[format.to_sym] || raise(UnknownFormatError, "No parser for format: #{format}")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def detect_format(path)
|
|
210
|
+
extension = File.extname(path).downcase
|
|
211
|
+
case extension
|
|
212
|
+
when '.md'
|
|
213
|
+
# Could be .agent.md or regular markdown
|
|
214
|
+
path.end_with?('.agent.md') ? :agent_md : :markdown
|
|
215
|
+
when '.prompt'
|
|
216
|
+
:dotprompt
|
|
217
|
+
when '.yaml', '.yml'
|
|
218
|
+
detect_yaml_format(path)
|
|
219
|
+
when '.json'
|
|
220
|
+
detect_json_format(path)
|
|
221
|
+
else
|
|
222
|
+
raise UnknownFormatError, "Cannot detect format for: #{path}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def parse(path, format: nil)
|
|
227
|
+
format ||= detect_format(path)
|
|
228
|
+
parser_for(format).parse(path)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def parse_string(content, format:)
|
|
232
|
+
parser_for(format).parse_string(content)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
def detect_yaml_format(path)
|
|
238
|
+
content = File.read(path)
|
|
239
|
+
if content.include?('role:') && content.include?('goal:')
|
|
240
|
+
:crewai
|
|
241
|
+
elsif content.include?('has_context') || content.include?('activeagent')
|
|
242
|
+
:activeagent_yaml
|
|
243
|
+
else
|
|
244
|
+
:yaml
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def detect_json_format(path)
|
|
249
|
+
content = JSON.parse(File.read(path))
|
|
250
|
+
if content['inputSchema']
|
|
251
|
+
:mcp_tool
|
|
252
|
+
elsif content['main'] && content['files']
|
|
253
|
+
:agent_package
|
|
254
|
+
else
|
|
255
|
+
:json
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
class UnknownFormatError < StandardError; end
|
|
262
|
+
end
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 3. Base Parser
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
# lib/agent_manifest/parsers/base_parser.rb
|
|
269
|
+
module AgentManifest
|
|
270
|
+
module Parsers
|
|
271
|
+
class BaseParser
|
|
272
|
+
class << self
|
|
273
|
+
def parse(path)
|
|
274
|
+
content = File.read(path)
|
|
275
|
+
manifest = parse_string(content)
|
|
276
|
+
manifest.source_path = path
|
|
277
|
+
manifest.source_format = format_name
|
|
278
|
+
manifest
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def parse_string(content)
|
|
282
|
+
raise NotImplementedError, "Subclasses must implement parse_string"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def format_name
|
|
286
|
+
raise NotImplementedError, "Subclasses must implement format_name"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
protected
|
|
290
|
+
|
|
291
|
+
def normalize_model(model_string)
|
|
292
|
+
# Normalize model identifiers to provider/model format
|
|
293
|
+
return nil unless model_string
|
|
294
|
+
|
|
295
|
+
if model_string.include?('/')
|
|
296
|
+
model_string
|
|
297
|
+
elsif model_string.start_with?('gpt-')
|
|
298
|
+
"openai/#{model_string}"
|
|
299
|
+
elsif model_string.start_with?('claude-')
|
|
300
|
+
"anthropic/#{model_string}"
|
|
301
|
+
elsif model_string.start_with?('gemini-')
|
|
302
|
+
"google/#{model_string}"
|
|
303
|
+
else
|
|
304
|
+
model_string
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def parse_tools(tools_data)
|
|
309
|
+
return [] unless tools_data
|
|
310
|
+
|
|
311
|
+
tools_data.map do |tool_data|
|
|
312
|
+
if tool_data.is_a?(String)
|
|
313
|
+
Tool.new(ref: tool_data)
|
|
314
|
+
elsif tool_data['$ref']
|
|
315
|
+
Tool.new(ref: tool_data['$ref'])
|
|
316
|
+
else
|
|
317
|
+
Tool.new(
|
|
318
|
+
name: tool_data['name'],
|
|
319
|
+
description: tool_data['description'],
|
|
320
|
+
input_schema: tool_data['inputSchema'] || tool_data['input_schema']
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Format Parsers
|
|
332
|
+
|
|
333
|
+
### 4. AgentMd Parser (.agent.md)
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
# lib/agent_manifest/parsers/agent_md_parser.rb
|
|
337
|
+
require 'yaml'
|
|
338
|
+
|
|
339
|
+
module AgentManifest
|
|
340
|
+
module Parsers
|
|
341
|
+
class AgentMdParser < BaseParser
|
|
342
|
+
FRONTMATTER_REGEX = /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
|
|
343
|
+
|
|
344
|
+
class << self
|
|
345
|
+
def format_name
|
|
346
|
+
:agent_md
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def parse_string(content)
|
|
350
|
+
frontmatter, body = extract_frontmatter(content)
|
|
351
|
+
|
|
352
|
+
Manifest.new(
|
|
353
|
+
# Meta
|
|
354
|
+
name: frontmatter['name'],
|
|
355
|
+
version: frontmatter['version'] || '1.0.0',
|
|
356
|
+
description: frontmatter['description'],
|
|
357
|
+
author: frontmatter['author'],
|
|
358
|
+
license: frontmatter['license'],
|
|
359
|
+
repository: frontmatter['repository'],
|
|
360
|
+
tags: frontmatter['tags'] || [],
|
|
361
|
+
extends: frontmatter['extends'],
|
|
362
|
+
|
|
363
|
+
# Model
|
|
364
|
+
model: normalize_model(frontmatter['model']),
|
|
365
|
+
config: frontmatter['config'] || {},
|
|
366
|
+
|
|
367
|
+
# Schemas
|
|
368
|
+
input_schema: parse_schema(frontmatter['input']),
|
|
369
|
+
output_schema: parse_schema(frontmatter['output']),
|
|
370
|
+
|
|
371
|
+
# Tools & Resources
|
|
372
|
+
tools: parse_tools(frontmatter['tools']),
|
|
373
|
+
resources: parse_resources(frontmatter['resources']),
|
|
374
|
+
|
|
375
|
+
# Instructions
|
|
376
|
+
instructions: extract_instructions(body),
|
|
377
|
+
template: body,
|
|
378
|
+
|
|
379
|
+
# Extensions
|
|
380
|
+
extensions: extract_extensions(frontmatter)
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
private
|
|
385
|
+
|
|
386
|
+
def extract_frontmatter(content)
|
|
387
|
+
match = content.match(FRONTMATTER_REGEX)
|
|
388
|
+
raise ParseError, "Invalid .agent.md format: missing frontmatter" unless match
|
|
389
|
+
|
|
390
|
+
frontmatter = YAML.safe_load(match[1], permitted_classes: [Symbol])
|
|
391
|
+
body = match[2].strip
|
|
392
|
+
|
|
393
|
+
[frontmatter, body]
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def parse_schema(schema_data)
|
|
397
|
+
return nil unless schema_data
|
|
398
|
+
|
|
399
|
+
schema = schema_data['schema']
|
|
400
|
+
return nil unless schema
|
|
401
|
+
|
|
402
|
+
# Detect if it's Picoschema or JSON Schema
|
|
403
|
+
if schema.is_a?(Hash) && schema['type']
|
|
404
|
+
InputSchema.new(schema: schema, format: :json_schema)
|
|
405
|
+
else
|
|
406
|
+
InputSchema.new(schema: schema, format: :picoschema)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def parse_resources(resources_data)
|
|
411
|
+
return [] unless resources_data
|
|
412
|
+
|
|
413
|
+
resources_data.map do |res|
|
|
414
|
+
Resource.new(
|
|
415
|
+
name: res['name'],
|
|
416
|
+
description: res['description'],
|
|
417
|
+
uri: res['uri'],
|
|
418
|
+
mime_type: res['mimeType']
|
|
419
|
+
)
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def extract_instructions(body)
|
|
424
|
+
# Extract content from ## Instructions section
|
|
425
|
+
if body =~ /##\s*Instructions\s*\n(.*?)(?=\n##|\z)/mi
|
|
426
|
+
$1.strip
|
|
427
|
+
else
|
|
428
|
+
# Use entire body as instructions if no section found
|
|
429
|
+
body.gsub(/^#\s+.*\n/, '').strip
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def extract_extensions(frontmatter)
|
|
434
|
+
extensions = {}
|
|
435
|
+
%w[activeagent crewai langchain genkit].each do |framework|
|
|
436
|
+
extensions[framework.to_sym] = frontmatter[framework] if frontmatter[framework]
|
|
437
|
+
end
|
|
438
|
+
extensions
|
|
439
|
+
end
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# Register the parser
|
|
444
|
+
ParserRegistry.register(:agent_md, AgentMdParser)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### 5. Dotprompt Parser (.prompt)
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
# lib/agent_manifest/parsers/dotprompt_parser.rb
|
|
453
|
+
module AgentManifest
|
|
454
|
+
module Parsers
|
|
455
|
+
class DotpromptParser < BaseParser
|
|
456
|
+
FRONTMATTER_REGEX = /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
|
|
457
|
+
|
|
458
|
+
class << self
|
|
459
|
+
def format_name
|
|
460
|
+
:dotprompt
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def parse_string(content)
|
|
464
|
+
frontmatter, body = extract_frontmatter(content)
|
|
465
|
+
|
|
466
|
+
# Extract name from filename if not in frontmatter
|
|
467
|
+
name = frontmatter['name'] || 'unnamed-prompt'
|
|
468
|
+
|
|
469
|
+
Manifest.new(
|
|
470
|
+
name: name,
|
|
471
|
+
version: frontmatter['version'] || '1.0.0',
|
|
472
|
+
|
|
473
|
+
# Model - Dotprompt format
|
|
474
|
+
model: normalize_model(frontmatter['model']),
|
|
475
|
+
config: extract_config(frontmatter),
|
|
476
|
+
|
|
477
|
+
# Schemas - Dotprompt uses input/output directly
|
|
478
|
+
input_schema: parse_dotprompt_schema(frontmatter['input']),
|
|
479
|
+
output_schema: parse_dotprompt_output(frontmatter['output']),
|
|
480
|
+
|
|
481
|
+
# Tools
|
|
482
|
+
tools: parse_tools(frontmatter['tools']),
|
|
483
|
+
|
|
484
|
+
# Template
|
|
485
|
+
instructions: body,
|
|
486
|
+
template: body,
|
|
487
|
+
|
|
488
|
+
# Store original format info
|
|
489
|
+
extensions: {
|
|
490
|
+
dotprompt: frontmatter.except('model', 'input', 'output', 'tools')
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
private
|
|
496
|
+
|
|
497
|
+
def extract_frontmatter(content)
|
|
498
|
+
match = content.match(FRONTMATTER_REGEX)
|
|
499
|
+
raise ParseError, "Invalid .prompt format: missing frontmatter" unless match
|
|
500
|
+
|
|
501
|
+
[YAML.safe_load(match[1]), match[2].strip]
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def extract_config(frontmatter)
|
|
505
|
+
config = {}
|
|
506
|
+
config[:temperature] = frontmatter['temperature'] if frontmatter['temperature']
|
|
507
|
+
config[:max_tokens] = frontmatter['maxOutputTokens'] if frontmatter['maxOutputTokens']
|
|
508
|
+
config[:top_p] = frontmatter['topP'] if frontmatter['topP']
|
|
509
|
+
config[:top_k] = frontmatter['topK'] if frontmatter['topK']
|
|
510
|
+
config[:stop] = frontmatter['stopSequences'] if frontmatter['stopSequences']
|
|
511
|
+
config
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def parse_dotprompt_schema(input_data)
|
|
515
|
+
return nil unless input_data
|
|
516
|
+
|
|
517
|
+
schema = input_data['schema']
|
|
518
|
+
return nil unless schema
|
|
519
|
+
|
|
520
|
+
# Dotprompt uses Picoschema by default
|
|
521
|
+
InputSchema.new(schema: schema, format: :picoschema)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def parse_dotprompt_output(output_data)
|
|
525
|
+
return nil unless output_data
|
|
526
|
+
|
|
527
|
+
{
|
|
528
|
+
format: output_data['format'],
|
|
529
|
+
schema: output_data['schema']
|
|
530
|
+
}
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
ParserRegistry.register(:dotprompt, DotpromptParser)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 6. CrewAI Parser (agents.yaml)
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
# lib/agent_manifest/parsers/crewai_parser.rb
|
|
544
|
+
module AgentManifest
|
|
545
|
+
module Parsers
|
|
546
|
+
class CrewAIParser < BaseParser
|
|
547
|
+
class << self
|
|
548
|
+
def format_name
|
|
549
|
+
:crewai
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def parse_string(content)
|
|
553
|
+
data = YAML.safe_load(content)
|
|
554
|
+
|
|
555
|
+
# CrewAI files contain multiple agents, return array
|
|
556
|
+
agents = data.map do |agent_name, agent_data|
|
|
557
|
+
parse_agent(agent_name, agent_data)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# If single agent, return it directly
|
|
561
|
+
agents.size == 1 ? agents.first : agents
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def parse_file_pair(agents_path, tasks_path = nil)
|
|
565
|
+
agents_content = File.read(agents_path)
|
|
566
|
+
agents = parse_string(agents_content)
|
|
567
|
+
|
|
568
|
+
if tasks_path && File.exist?(tasks_path)
|
|
569
|
+
tasks = YAML.safe_load(File.read(tasks_path))
|
|
570
|
+
# Merge task info into agents
|
|
571
|
+
# (simplified - real implementation would be more complex)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
agents
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
private
|
|
578
|
+
|
|
579
|
+
def parse_agent(name, data)
|
|
580
|
+
Manifest.new(
|
|
581
|
+
name: name.to_s.underscore.dasherize,
|
|
582
|
+
description: data['goal'],
|
|
583
|
+
|
|
584
|
+
model: normalize_model(data['llm']),
|
|
585
|
+
|
|
586
|
+
# CrewAI doesn't have formal schemas
|
|
587
|
+
input_schema: nil,
|
|
588
|
+
output_schema: nil,
|
|
589
|
+
|
|
590
|
+
# Map tools
|
|
591
|
+
tools: parse_crewai_tools(data['tools']),
|
|
592
|
+
|
|
593
|
+
# Instructions from role + backstory
|
|
594
|
+
instructions: build_instructions(data),
|
|
595
|
+
template: build_template(data),
|
|
596
|
+
|
|
597
|
+
# Preserve CrewAI-specific config
|
|
598
|
+
extensions: {
|
|
599
|
+
crewai: {
|
|
600
|
+
role: data['role'],
|
|
601
|
+
goal: data['goal'],
|
|
602
|
+
backstory: data['backstory'],
|
|
603
|
+
verbose: data['verbose'],
|
|
604
|
+
allow_delegation: data['allow_delegation'],
|
|
605
|
+
max_iter: data['max_iter'],
|
|
606
|
+
max_rpm: data['max_rpm']
|
|
607
|
+
}.compact
|
|
608
|
+
}
|
|
609
|
+
)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def parse_crewai_tools(tools)
|
|
613
|
+
return [] unless tools
|
|
614
|
+
|
|
615
|
+
tools.map do |tool|
|
|
616
|
+
if tool.is_a?(String)
|
|
617
|
+
# Reference to a tool class
|
|
618
|
+
Tool.new(ref: tool)
|
|
619
|
+
else
|
|
620
|
+
Tool.new(
|
|
621
|
+
name: tool['name'],
|
|
622
|
+
description: tool['description'],
|
|
623
|
+
input_schema: tool['args_schema']
|
|
624
|
+
)
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def build_instructions(data)
|
|
630
|
+
parts = []
|
|
631
|
+
parts << "Role: #{data['role']}" if data['role']
|
|
632
|
+
parts << "Goal: #{data['goal']}" if data['goal']
|
|
633
|
+
parts << "\n#{data['backstory']}" if data['backstory']
|
|
634
|
+
parts.join("\n")
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def build_template(data)
|
|
638
|
+
<<~TEMPLATE
|
|
639
|
+
# #{data['role']}
|
|
640
|
+
|
|
641
|
+
## Goal
|
|
642
|
+
#{data['goal']}
|
|
643
|
+
|
|
644
|
+
## Backstory
|
|
645
|
+
#{data['backstory']}
|
|
646
|
+
TEMPLATE
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
ParserRegistry.register(:crewai, CrewAIParser)
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### 7. GitHub Prompt Parser (.prompt.md)
|
|
657
|
+
|
|
658
|
+
```ruby
|
|
659
|
+
# lib/agent_manifest/parsers/github_prompt_parser.rb
|
|
660
|
+
module AgentManifest
|
|
661
|
+
module Parsers
|
|
662
|
+
class GitHubPromptParser < BaseParser
|
|
663
|
+
FRONTMATTER_REGEX = /\A---\s*\n(.*?)\n---\s*\n(.*)\z/m
|
|
664
|
+
|
|
665
|
+
class << self
|
|
666
|
+
def format_name
|
|
667
|
+
:github_prompt
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def parse_string(content)
|
|
671
|
+
frontmatter, body = extract_frontmatter(content)
|
|
672
|
+
|
|
673
|
+
Manifest.new(
|
|
674
|
+
name: extract_name_from_frontmatter(frontmatter),
|
|
675
|
+
description: frontmatter['description'],
|
|
676
|
+
|
|
677
|
+
model: normalize_model(frontmatter['model']),
|
|
678
|
+
|
|
679
|
+
tools: parse_github_tools(frontmatter['tools']),
|
|
680
|
+
|
|
681
|
+
instructions: body,
|
|
682
|
+
template: body,
|
|
683
|
+
|
|
684
|
+
extensions: {
|
|
685
|
+
github_prompt: {
|
|
686
|
+
agent: frontmatter['agent'],
|
|
687
|
+
mode: frontmatter['mode']
|
|
688
|
+
}.compact
|
|
689
|
+
}
|
|
690
|
+
)
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
private
|
|
694
|
+
|
|
695
|
+
def extract_frontmatter(content)
|
|
696
|
+
match = content.match(FRONTMATTER_REGEX)
|
|
697
|
+
if match
|
|
698
|
+
[YAML.safe_load(match[1]), match[2].strip]
|
|
699
|
+
else
|
|
700
|
+
[{}, content.strip]
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
def extract_name_from_frontmatter(frontmatter)
|
|
705
|
+
frontmatter['name'] || 'github-prompt'
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def parse_github_tools(tools)
|
|
709
|
+
return [] unless tools
|
|
710
|
+
|
|
711
|
+
# GitHub tools are string references like 'githubRepo', 'search/codebase'
|
|
712
|
+
tools.map do |tool|
|
|
713
|
+
Tool.new(ref: "github://#{tool}")
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
ParserRegistry.register(:github_prompt, GitHubPromptParser)
|
|
720
|
+
end
|
|
721
|
+
end
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
## Format Exporters
|
|
725
|
+
|
|
726
|
+
### 8. Exporter Registry
|
|
727
|
+
|
|
728
|
+
```ruby
|
|
729
|
+
# lib/agent_manifest/exporter_registry.rb
|
|
730
|
+
module AgentManifest
|
|
731
|
+
class ExporterRegistry
|
|
732
|
+
class << self
|
|
733
|
+
def exporters
|
|
734
|
+
@exporters ||= {}
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
def register(format, exporter_class)
|
|
738
|
+
exporters[format.to_sym] = exporter_class
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def exporter_for(format)
|
|
742
|
+
exporters[format.to_sym] || raise(UnknownFormatError, "No exporter for format: #{format}")
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
def export(manifest, format:, path: nil)
|
|
746
|
+
content = exporter_for(format).export(manifest)
|
|
747
|
+
if path
|
|
748
|
+
File.write(path, content)
|
|
749
|
+
path
|
|
750
|
+
else
|
|
751
|
+
content
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### 9. AgentMd Exporter
|
|
760
|
+
|
|
761
|
+
```ruby
|
|
762
|
+
# lib/agent_manifest/exporters/agent_md_exporter.rb
|
|
763
|
+
module AgentManifest
|
|
764
|
+
module Exporters
|
|
765
|
+
class AgentMdExporter
|
|
766
|
+
class << self
|
|
767
|
+
def export(manifest)
|
|
768
|
+
frontmatter = build_frontmatter(manifest)
|
|
769
|
+
body = manifest.template || build_default_body(manifest)
|
|
770
|
+
|
|
771
|
+
<<~OUTPUT
|
|
772
|
+
---
|
|
773
|
+
#{frontmatter.to_yaml.lines[1..].join}---
|
|
774
|
+
|
|
775
|
+
#{body}
|
|
776
|
+
OUTPUT
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
private
|
|
780
|
+
|
|
781
|
+
def build_frontmatter(manifest)
|
|
782
|
+
fm = {}
|
|
783
|
+
|
|
784
|
+
# Meta
|
|
785
|
+
fm['name'] = manifest.name
|
|
786
|
+
fm['version'] = manifest.version if manifest.version != '1.0.0'
|
|
787
|
+
fm['description'] = manifest.description if manifest.description
|
|
788
|
+
fm['author'] = manifest.author if manifest.author
|
|
789
|
+
fm['license'] = manifest.license if manifest.license
|
|
790
|
+
fm['repository'] = manifest.repository if manifest.repository
|
|
791
|
+
fm['tags'] = manifest.tags if manifest.tags.any?
|
|
792
|
+
fm['extends'] = manifest.extends if manifest.extends
|
|
793
|
+
|
|
794
|
+
# Model
|
|
795
|
+
fm['model'] = manifest.model if manifest.model
|
|
796
|
+
fm['config'] = manifest.config if manifest.config&.any?
|
|
797
|
+
|
|
798
|
+
# Schemas
|
|
799
|
+
if manifest.input_schema
|
|
800
|
+
fm['input'] = { 'schema' => manifest.input_schema.to_picoschema }
|
|
801
|
+
end
|
|
802
|
+
if manifest.output_schema
|
|
803
|
+
fm['output'] = manifest.output_schema
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# Tools
|
|
807
|
+
if manifest.tools.any?
|
|
808
|
+
fm['tools'] = manifest.tools.map(&:to_h)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# Resources
|
|
812
|
+
if manifest.resources.any?
|
|
813
|
+
fm['resources'] = manifest.resources.map(&:to_h)
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
# Extensions
|
|
817
|
+
manifest.extensions.each do |framework, config|
|
|
818
|
+
fm[framework.to_s] = config if config.any?
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
fm
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def build_default_body(manifest)
|
|
825
|
+
<<~BODY
|
|
826
|
+
# #{manifest.name.titleize}
|
|
827
|
+
|
|
828
|
+
#{manifest.description}
|
|
829
|
+
|
|
830
|
+
## Instructions
|
|
831
|
+
|
|
832
|
+
#{manifest.instructions}
|
|
833
|
+
BODY
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
ExporterRegistry.register(:agent_md, AgentMdExporter)
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### 10. Dotprompt Exporter
|
|
844
|
+
|
|
845
|
+
```ruby
|
|
846
|
+
# lib/agent_manifest/exporters/dotprompt_exporter.rb
|
|
847
|
+
module AgentManifest
|
|
848
|
+
module Exporters
|
|
849
|
+
class DotpromptExporter
|
|
850
|
+
class << self
|
|
851
|
+
def export(manifest)
|
|
852
|
+
frontmatter = build_frontmatter(manifest)
|
|
853
|
+
body = convert_template(manifest.template)
|
|
854
|
+
|
|
855
|
+
<<~OUTPUT
|
|
856
|
+
---
|
|
857
|
+
#{frontmatter.to_yaml.lines[1..].join}---
|
|
858
|
+
|
|
859
|
+
#{body}
|
|
860
|
+
OUTPUT
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
private
|
|
864
|
+
|
|
865
|
+
def build_frontmatter(manifest)
|
|
866
|
+
fm = {}
|
|
867
|
+
|
|
868
|
+
fm['model'] = manifest.model if manifest.model
|
|
869
|
+
|
|
870
|
+
# Config uses Dotprompt field names
|
|
871
|
+
if manifest.config
|
|
872
|
+
fm['temperature'] = manifest.config[:temperature] if manifest.config[:temperature]
|
|
873
|
+
fm['maxOutputTokens'] = manifest.config[:max_tokens] if manifest.config[:max_tokens]
|
|
874
|
+
fm['topP'] = manifest.config[:top_p] if manifest.config[:top_p]
|
|
875
|
+
fm['topK'] = manifest.config[:top_k] if manifest.config[:top_k]
|
|
876
|
+
fm['stopSequences'] = manifest.config[:stop] if manifest.config[:stop]
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
# Schemas
|
|
880
|
+
if manifest.input_schema
|
|
881
|
+
fm['input'] = { 'schema' => manifest.input_schema.to_picoschema }
|
|
882
|
+
end
|
|
883
|
+
if manifest.output_schema
|
|
884
|
+
fm['output'] = manifest.output_schema
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
# Tools
|
|
888
|
+
if manifest.tools.any?
|
|
889
|
+
fm['tools'] = manifest.tools.map { |t| t.name }
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
fm
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
def convert_template(template)
|
|
896
|
+
# Template syntax is compatible (both use Handlebars)
|
|
897
|
+
# But may need to strip markdown sections
|
|
898
|
+
return '' unless template
|
|
899
|
+
|
|
900
|
+
# Remove markdown headers that aren't part of the prompt
|
|
901
|
+
template
|
|
902
|
+
.gsub(/^#\s+.*\n/, '')
|
|
903
|
+
.gsub(/^##\s+(Instructions|Guidelines|Context)\s*\n/, '')
|
|
904
|
+
.strip
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
ExporterRegistry.register(:dotprompt, DotpromptExporter)
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### 11. CrewAI Exporter
|
|
915
|
+
|
|
916
|
+
```ruby
|
|
917
|
+
# lib/agent_manifest/exporters/crewai_exporter.rb
|
|
918
|
+
module AgentManifest
|
|
919
|
+
module Exporters
|
|
920
|
+
class CrewAIExporter
|
|
921
|
+
class << self
|
|
922
|
+
def export(manifest)
|
|
923
|
+
agent_key = manifest.name.underscore
|
|
924
|
+
|
|
925
|
+
agent_data = build_agent_data(manifest)
|
|
926
|
+
|
|
927
|
+
{ agent_key => agent_data }.to_yaml
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
def export_multiple(manifests)
|
|
931
|
+
agents = {}
|
|
932
|
+
manifests.each do |manifest|
|
|
933
|
+
agent_key = manifest.name.underscore
|
|
934
|
+
agents[agent_key] = build_agent_data(manifest)
|
|
935
|
+
end
|
|
936
|
+
agents.to_yaml
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
private
|
|
940
|
+
|
|
941
|
+
def build_agent_data(manifest)
|
|
942
|
+
data = {}
|
|
943
|
+
|
|
944
|
+
# Use CrewAI extension if available
|
|
945
|
+
crewai = manifest.crewai_config
|
|
946
|
+
|
|
947
|
+
data['role'] = crewai[:role] || extract_role(manifest)
|
|
948
|
+
data['goal'] = crewai[:goal] || manifest.description
|
|
949
|
+
data['backstory'] = crewai[:backstory] || manifest.instructions
|
|
950
|
+
|
|
951
|
+
# Model
|
|
952
|
+
data['llm'] = convert_model(manifest.model) if manifest.model
|
|
953
|
+
|
|
954
|
+
# Tools
|
|
955
|
+
if manifest.tools.any?
|
|
956
|
+
data['tools'] = manifest.tools.map { |t| t.ref || t.name }
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# Other CrewAI options
|
|
960
|
+
data['verbose'] = crewai[:verbose] if crewai[:verbose]
|
|
961
|
+
data['allow_delegation'] = crewai[:allow_delegation] if crewai.key?(:allow_delegation)
|
|
962
|
+
data['max_iter'] = crewai[:max_iter] if crewai[:max_iter]
|
|
963
|
+
data['max_rpm'] = crewai[:max_rpm] if crewai[:max_rpm]
|
|
964
|
+
|
|
965
|
+
data.compact
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def extract_role(manifest)
|
|
969
|
+
# Try to extract role from instructions or name
|
|
970
|
+
manifest.name.titleize.gsub('-', ' ')
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
def convert_model(model)
|
|
974
|
+
# CrewAI uses different model format
|
|
975
|
+
# e.g., "anthropic/claude-sonnet-4-20250514" -> "claude-sonnet-4-20250514" or provider-specific
|
|
976
|
+
model.split('/').last
|
|
977
|
+
end
|
|
978
|
+
end
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
ExporterRegistry.register(:crewai, CrewAIExporter)
|
|
982
|
+
end
|
|
983
|
+
end
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
## Picoschema Parser
|
|
987
|
+
|
|
988
|
+
```ruby
|
|
989
|
+
# lib/agent_manifest/picoschema_parser.rb
|
|
990
|
+
module AgentManifest
|
|
991
|
+
class PicoschemaParser
|
|
992
|
+
SCALAR_TYPES = %w[string integer number boolean any].freeze
|
|
993
|
+
|
|
994
|
+
class << self
|
|
995
|
+
# Convert Picoschema to JSON Schema
|
|
996
|
+
def to_json_schema(picoschema)
|
|
997
|
+
return nil unless picoschema
|
|
998
|
+
|
|
999
|
+
if picoschema.is_a?(Hash)
|
|
1000
|
+
parse_object(picoschema)
|
|
1001
|
+
else
|
|
1002
|
+
{ "type" => "object" }
|
|
1003
|
+
end
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
# Convert JSON Schema to Picoschema
|
|
1007
|
+
def from_json_schema(json_schema)
|
|
1008
|
+
return nil unless json_schema
|
|
1009
|
+
|
|
1010
|
+
if json_schema['type'] == 'object' && json_schema['properties']
|
|
1011
|
+
convert_properties(json_schema['properties'], json_schema['required'] || [])
|
|
1012
|
+
else
|
|
1013
|
+
json_schema
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
private
|
|
1018
|
+
|
|
1019
|
+
def parse_object(schema_hash)
|
|
1020
|
+
properties = {}
|
|
1021
|
+
required = []
|
|
1022
|
+
|
|
1023
|
+
schema_hash.each do |key, value|
|
|
1024
|
+
field_name, optional = parse_field_name(key)
|
|
1025
|
+
field_schema = parse_field(value)
|
|
1026
|
+
|
|
1027
|
+
properties[field_name] = field_schema
|
|
1028
|
+
required << field_name unless optional
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
result = { "type" => "object", "properties" => properties }
|
|
1032
|
+
result["required"] = required if required.any?
|
|
1033
|
+
result
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
def parse_field_name(key)
|
|
1037
|
+
if key.end_with?('?')
|
|
1038
|
+
[key.chomp('?'), true] # optional
|
|
1039
|
+
else
|
|
1040
|
+
[key, false] # required
|
|
1041
|
+
end
|
|
1042
|
+
end
|
|
1043
|
+
|
|
1044
|
+
def parse_field(value)
|
|
1045
|
+
case value
|
|
1046
|
+
when String
|
|
1047
|
+
parse_type_string(value)
|
|
1048
|
+
when Hash
|
|
1049
|
+
# Nested object
|
|
1050
|
+
parse_object(value)
|
|
1051
|
+
when Array
|
|
1052
|
+
# Array type
|
|
1053
|
+
if value.first.is_a?(Hash)
|
|
1054
|
+
{ "type" => "array", "items" => parse_object(value.first) }
|
|
1055
|
+
else
|
|
1056
|
+
{ "type" => "array", "items" => parse_type_string(value.first.to_s) }
|
|
1057
|
+
end
|
|
1058
|
+
else
|
|
1059
|
+
{ "type" => "string" }
|
|
1060
|
+
end
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
def parse_type_string(type_str)
|
|
1064
|
+
# Parse: "type, description" or "type(enum1, enum2), description"
|
|
1065
|
+
parts = type_str.split(',', 2)
|
|
1066
|
+
type_part = parts[0].strip
|
|
1067
|
+
description = parts[1]&.strip
|
|
1068
|
+
|
|
1069
|
+
result = {}
|
|
1070
|
+
|
|
1071
|
+
# Check for enum: string(a, b, c)
|
|
1072
|
+
if type_part =~ /^(\w+)\(([^)]+)\)$/
|
|
1073
|
+
base_type = $1
|
|
1074
|
+
enum_values = $2.split(',').map(&:strip)
|
|
1075
|
+
result["type"] = base_type
|
|
1076
|
+
result["enum"] = enum_values
|
|
1077
|
+
elsif SCALAR_TYPES.include?(type_part)
|
|
1078
|
+
result["type"] = type_part
|
|
1079
|
+
elsif type_part.start_with?('[') && type_part.end_with?(']')
|
|
1080
|
+
inner = type_part[1..-2]
|
|
1081
|
+
result["type"] = "array"
|
|
1082
|
+
result["items"] = { "type" => inner }
|
|
1083
|
+
else
|
|
1084
|
+
result["type"] = type_part
|
|
1085
|
+
end
|
|
1086
|
+
|
|
1087
|
+
result["description"] = description if description
|
|
1088
|
+
|
|
1089
|
+
result
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
def convert_properties(properties, required_fields)
|
|
1093
|
+
result = {}
|
|
1094
|
+
|
|
1095
|
+
properties.each do |name, schema|
|
|
1096
|
+
optional = !required_fields.include?(name)
|
|
1097
|
+
key = optional ? "#{name}?" : name
|
|
1098
|
+
result[key] = convert_schema_to_pico(schema)
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
result
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
def convert_schema_to_pico(schema)
|
|
1105
|
+
type = schema['type']
|
|
1106
|
+
desc = schema['description']
|
|
1107
|
+
|
|
1108
|
+
pico = type.to_s
|
|
1109
|
+
if schema['enum']
|
|
1110
|
+
pico = "#{type}(#{schema['enum'].join(', ')})"
|
|
1111
|
+
end
|
|
1112
|
+
if desc
|
|
1113
|
+
pico = "#{pico}, #{desc}"
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
pico
|
|
1117
|
+
end
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
end
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
## Main Entry Point
|
|
1124
|
+
|
|
1125
|
+
```ruby
|
|
1126
|
+
# lib/agent_manifest.rb
|
|
1127
|
+
require_relative 'agent_manifest/manifest'
|
|
1128
|
+
require_relative 'agent_manifest/parser_registry'
|
|
1129
|
+
require_relative 'agent_manifest/exporter_registry'
|
|
1130
|
+
require_relative 'agent_manifest/picoschema_parser'
|
|
1131
|
+
|
|
1132
|
+
# Load all parsers
|
|
1133
|
+
require_relative 'agent_manifest/parsers/base_parser'
|
|
1134
|
+
require_relative 'agent_manifest/parsers/agent_md_parser'
|
|
1135
|
+
require_relative 'agent_manifest/parsers/dotprompt_parser'
|
|
1136
|
+
require_relative 'agent_manifest/parsers/crewai_parser'
|
|
1137
|
+
require_relative 'agent_manifest/parsers/github_prompt_parser'
|
|
1138
|
+
|
|
1139
|
+
# Load all exporters
|
|
1140
|
+
require_relative 'agent_manifest/exporters/agent_md_exporter'
|
|
1141
|
+
require_relative 'agent_manifest/exporters/dotprompt_exporter'
|
|
1142
|
+
require_relative 'agent_manifest/exporters/crewai_exporter'
|
|
1143
|
+
|
|
1144
|
+
module AgentManifest
|
|
1145
|
+
class << self
|
|
1146
|
+
# Parse any supported format
|
|
1147
|
+
def parse(path, format: nil)
|
|
1148
|
+
ParserRegistry.parse(path, format: format)
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
# Parse from string
|
|
1152
|
+
def parse_string(content, format:)
|
|
1153
|
+
ParserRegistry.parse_string(content, format: format)
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
# Export to any supported format
|
|
1157
|
+
def export(manifest, format:, path: nil)
|
|
1158
|
+
ExporterRegistry.export(manifest, format: format, path: path)
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
# Convert between formats
|
|
1162
|
+
def convert(input_path, output_format:, output_path: nil)
|
|
1163
|
+
manifest = parse(input_path)
|
|
1164
|
+
export(manifest, format: output_format, path: output_path)
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
# Load and instantiate as ActiveAgent
|
|
1168
|
+
def load_agent(path, format: nil)
|
|
1169
|
+
manifest = parse(path, format: format)
|
|
1170
|
+
AgentBuilder.build(manifest)
|
|
1171
|
+
end
|
|
1172
|
+
end
|
|
1173
|
+
end
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
## ActiveAgent Integration
|
|
1177
|
+
|
|
1178
|
+
```ruby
|
|
1179
|
+
# lib/agent_manifest/agent_builder.rb
|
|
1180
|
+
module AgentManifest
|
|
1181
|
+
class AgentBuilder
|
|
1182
|
+
class << self
|
|
1183
|
+
def build(manifest)
|
|
1184
|
+
# Create a dynamic agent class from manifest
|
|
1185
|
+
agent_class = Class.new(base_class(manifest)) do
|
|
1186
|
+
# Include concerns based on manifest
|
|
1187
|
+
end
|
|
1188
|
+
|
|
1189
|
+
# Configure from manifest
|
|
1190
|
+
configure_agent(agent_class, manifest)
|
|
1191
|
+
|
|
1192
|
+
agent_class
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
private
|
|
1196
|
+
|
|
1197
|
+
def base_class(manifest)
|
|
1198
|
+
config = manifest.activeagent_config
|
|
1199
|
+
parent = config[:parent_class] || 'ApplicationAgent'
|
|
1200
|
+
parent.constantize
|
|
1201
|
+
rescue NameError
|
|
1202
|
+
ActiveAgent::Base
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
def configure_agent(klass, manifest)
|
|
1206
|
+
config = manifest.activeagent_config
|
|
1207
|
+
|
|
1208
|
+
# Set model
|
|
1209
|
+
if manifest.model
|
|
1210
|
+
provider, model = manifest.model.split('/')
|
|
1211
|
+
klass.generate_with provider.to_sym, model: model
|
|
1212
|
+
end
|
|
1213
|
+
|
|
1214
|
+
# Include concerns
|
|
1215
|
+
concerns = config[:concerns] || []
|
|
1216
|
+
concerns.each do |concern|
|
|
1217
|
+
apply_concern(klass, concern)
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
# Define tools
|
|
1221
|
+
manifest.tools.each do |tool|
|
|
1222
|
+
define_tool(klass, tool) unless tool.reference?
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
# Set instructions
|
|
1226
|
+
klass.define_method(:system_instructions) do
|
|
1227
|
+
manifest.instructions
|
|
1228
|
+
end
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
def apply_concern(klass, concern)
|
|
1232
|
+
case concern
|
|
1233
|
+
when Hash
|
|
1234
|
+
concern.each do |name, options|
|
|
1235
|
+
case name.to_sym
|
|
1236
|
+
when :has_context
|
|
1237
|
+
klass.include(SolidAgent::HasContext)
|
|
1238
|
+
klass.has_context(**options.symbolize_keys)
|
|
1239
|
+
when :has_tools
|
|
1240
|
+
klass.include(SolidAgent::HasTools)
|
|
1241
|
+
klass.has_tools(*options) if options.is_a?(Array)
|
|
1242
|
+
when :streams_tool_updates
|
|
1243
|
+
klass.include(SolidAgent::StreamsToolUpdates)
|
|
1244
|
+
end
|
|
1245
|
+
end
|
|
1246
|
+
when String, Symbol
|
|
1247
|
+
klass.include("SolidAgent::#{concern.to_s.camelize}".constantize)
|
|
1248
|
+
end
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
def define_tool(klass, tool)
|
|
1252
|
+
klass.tool tool.name.to_sym do
|
|
1253
|
+
description tool.description
|
|
1254
|
+
if tool.input_schema && tool.input_schema['properties']
|
|
1255
|
+
tool.input_schema['properties'].each do |param_name, param_schema|
|
|
1256
|
+
required = tool.input_schema['required']&.include?(param_name)
|
|
1257
|
+
parameter param_name.to_sym,
|
|
1258
|
+
type: param_schema['type']&.to_sym,
|
|
1259
|
+
required: required,
|
|
1260
|
+
description: param_schema['description']
|
|
1261
|
+
end
|
|
1262
|
+
end
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
end
|
|
1266
|
+
end
|
|
1267
|
+
end
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
## CLI Commands
|
|
1271
|
+
|
|
1272
|
+
```ruby
|
|
1273
|
+
# lib/agent_manifest/cli.rb
|
|
1274
|
+
module AgentManifest
|
|
1275
|
+
class CLI < Thor
|
|
1276
|
+
desc "parse PATH", "Parse an agent file and display info"
|
|
1277
|
+
option :format, type: :string, desc: "Force input format"
|
|
1278
|
+
option :json, type: :boolean, desc: "Output as JSON"
|
|
1279
|
+
def parse(path)
|
|
1280
|
+
manifest = AgentManifest.parse(path, format: options[:format]&.to_sym)
|
|
1281
|
+
|
|
1282
|
+
if options[:json]
|
|
1283
|
+
puts JSON.pretty_generate(manifest.to_h)
|
|
1284
|
+
else
|
|
1285
|
+
display_manifest(manifest)
|
|
1286
|
+
end
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1289
|
+
desc "convert INPUT OUTPUT", "Convert between agent formats"
|
|
1290
|
+
option :from, type: :string, desc: "Input format"
|
|
1291
|
+
option :to, type: :string, required: true, desc: "Output format"
|
|
1292
|
+
def convert(input, output)
|
|
1293
|
+
manifest = AgentManifest.parse(input, format: options[:from]&.to_sym)
|
|
1294
|
+
AgentManifest.export(manifest, format: options[:to].to_sym, path: output)
|
|
1295
|
+
say "Converted #{input} -> #{output}", :green
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
desc "validate PATH", "Validate an agent file"
|
|
1299
|
+
def validate(path)
|
|
1300
|
+
manifest = AgentManifest.parse(path)
|
|
1301
|
+
errors = Validator.validate(manifest)
|
|
1302
|
+
|
|
1303
|
+
if errors.empty?
|
|
1304
|
+
say "✓ Valid agent definition", :green
|
|
1305
|
+
else
|
|
1306
|
+
say "✗ Validation errors:", :red
|
|
1307
|
+
errors.each { |e| say " - #{e}", :red }
|
|
1308
|
+
exit 1
|
|
1309
|
+
end
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
desc "init NAME", "Create a new agent file"
|
|
1313
|
+
option :format, type: :string, default: "agent_md", desc: "Output format"
|
|
1314
|
+
option :template, type: :string, desc: "Template to use"
|
|
1315
|
+
def init(name)
|
|
1316
|
+
# Generate from template
|
|
1317
|
+
end
|
|
1318
|
+
|
|
1319
|
+
private
|
|
1320
|
+
|
|
1321
|
+
def display_manifest(manifest)
|
|
1322
|
+
say "Agent: #{manifest.name}", :green
|
|
1323
|
+
say "Version: #{manifest.version}"
|
|
1324
|
+
say "Model: #{manifest.model}" if manifest.model
|
|
1325
|
+
say "Tools: #{manifest.tools.map(&:name).join(', ')}" if manifest.tools.any?
|
|
1326
|
+
say "Description: #{manifest.description}" if manifest.description
|
|
1327
|
+
end
|
|
1328
|
+
end
|
|
1329
|
+
end
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
## Usage Examples
|
|
1333
|
+
|
|
1334
|
+
```ruby
|
|
1335
|
+
# Parse any format
|
|
1336
|
+
manifest = AgentManifest.parse("research-assistant.agent.md")
|
|
1337
|
+
manifest = AgentManifest.parse("agent.prompt") # Dotprompt
|
|
1338
|
+
manifest = AgentManifest.parse("agents.yaml") # CrewAI
|
|
1339
|
+
|
|
1340
|
+
# Convert formats
|
|
1341
|
+
AgentManifest.convert("agent.prompt", output_format: :agent_md, output_path: "agent.agent.md")
|
|
1342
|
+
AgentManifest.convert("agents.yaml", output_format: :agent_md, output_path: "agent.agent.md")
|
|
1343
|
+
|
|
1344
|
+
# Export to different format
|
|
1345
|
+
content = AgentManifest.export(manifest, format: :crewai)
|
|
1346
|
+
AgentManifest.export(manifest, format: :dotprompt, path: "output.prompt")
|
|
1347
|
+
|
|
1348
|
+
# Load and use as ActiveAgent
|
|
1349
|
+
agent_class = AgentManifest.load_agent("research-assistant.agent.md")
|
|
1350
|
+
agent = agent_class.new(params: { query: "What is AI?" })
|
|
1351
|
+
result = agent.research.generate_now
|
|
1352
|
+
|
|
1353
|
+
# Parse from string
|
|
1354
|
+
content = File.read("agent.agent.md")
|
|
1355
|
+
manifest = AgentManifest.parse_string(content, format: :agent_md)
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
## Supported Formats Summary
|
|
1359
|
+
|
|
1360
|
+
| Format | Extension | Parse | Export | Notes |
|
|
1361
|
+
|--------|-----------|-------|--------|-------|
|
|
1362
|
+
| AgentMd | `.agent.md` | ✅ | ✅ | Native format |
|
|
1363
|
+
| Dotprompt | `.prompt` | ✅ | ✅ | Google/Firebase |
|
|
1364
|
+
| CrewAI | `agents.yaml` | ✅ | ✅ | Python framework |
|
|
1365
|
+
| GitHub Prompt | `.prompt.md` | ✅ | ⚠️ | Copilot prompts |
|
|
1366
|
+
| LangChain | `*.py` | 🚧 | 🚧 | Code-based |
|
|
1367
|
+
| MCP Tool | `.tool.json` | ✅ | ✅ | Tool definitions only |
|
|
1368
|
+
|
|
1369
|
+
✅ Full support | ⚠️ Partial | 🚧 Planned
|