solid_agent 0.1.1 → 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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +21 -0
  4. data/README.md +209 -18
  5. data/Rakefile +22 -2
  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 +2 -2
  31. data/lib/generators/solid_agent/agent/templates/agent.rb.erb +3 -3
  32. data/lib/generators/solid_agent/context/templates/context_model.rb.erb +50 -16
  33. data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +8 -0
  34. data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +4 -0
  35. data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +11 -0
  36. data/lib/generators/solid_agent/install/install_generator.rb +9 -0
  37. data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +60 -17
  38. data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +23 -6
  39. data/lib/generators/solid_agent/install/templates/agent_memory.rb.erb +51 -0
  40. data/lib/generators/solid_agent/install/templates/agent_memory_entry.rb.erb +12 -0
  41. data/lib/generators/solid_agent/install/templates/agent_run.rb.erb +122 -0
  42. data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +13 -0
  43. data/lib/generators/solid_agent/install/templates/create_agent_memories.rb.erb +35 -0
  44. data/lib/generators/solid_agent/install/templates/create_agent_messages.rb.erb +5 -0
  45. data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -0
  46. data/lib/generators/solid_agent/manifest/manifest_generator.rb +209 -0
  47. data/lib/generators/solid_agent/manifest/templates/agent.md.erb +39 -0
  48. data/lib/generators/solid_agent/manifest/templates/prompt.erb +13 -0
  49. data/lib/generators/solid_agent/reasons/reasons_generator.rb +83 -0
  50. data/lib/generators/solid_agent/reasons/templates/add_reasoning_columns.rb.erb +12 -0
  51. data/lib/solid_agent/agent_manifest/agent_builder.rb +323 -0
  52. data/lib/solid_agent/agent_manifest/errors.rb +26 -0
  53. data/lib/solid_agent/agent_manifest/exporter_registry.rb +117 -0
  54. data/lib/solid_agent/agent_manifest/exporters/agent_md_exporter.rb +115 -0
  55. data/lib/solid_agent/agent_manifest/exporters/base_exporter.rb +152 -0
  56. data/lib/solid_agent/agent_manifest/exporters/crewai_exporter.rb +125 -0
  57. data/lib/solid_agent/agent_manifest/exporters/dotprompt_exporter.rb +92 -0
  58. data/lib/solid_agent/agent_manifest/input_schema.rb +154 -0
  59. data/lib/solid_agent/agent_manifest/manifest.rb +306 -0
  60. data/lib/solid_agent/agent_manifest/parser_registry.rb +185 -0
  61. data/lib/solid_agent/agent_manifest/parsers/agent_md_parser.rb +87 -0
  62. data/lib/solid_agent/agent_manifest/parsers/base_parser.rb +223 -0
  63. data/lib/solid_agent/agent_manifest/parsers/crewai_parser.rb +201 -0
  64. data/lib/solid_agent/agent_manifest/parsers/dotprompt_parser.rb +122 -0
  65. data/lib/solid_agent/agent_manifest/parsers/github_prompt_parser.rb +143 -0
  66. data/lib/solid_agent/agent_manifest/picoschema.rb +254 -0
  67. data/lib/solid_agent/agent_manifest/registry/auth.rb +103 -0
  68. data/lib/solid_agent/agent_manifest/registry/client.rb +384 -0
  69. data/lib/solid_agent/agent_manifest/resource.rb +103 -0
  70. data/lib/solid_agent/agent_manifest/tool.rb +160 -0
  71. data/lib/solid_agent/agent_manifest/validator.rb +368 -0
  72. data/lib/solid_agent/agent_manifest.rb +381 -0
  73. data/lib/solid_agent/has_context.rb +251 -30
  74. data/lib/solid_agent/has_memory.rb +136 -0
  75. data/lib/solid_agent/has_reasons.rb +230 -0
  76. data/lib/solid_agent/model_naming.rb +42 -0
  77. data/lib/solid_agent/model_pricing.rb +93 -0
  78. data/lib/solid_agent/reasonable/reason.rb +205 -0
  79. data/lib/solid_agent/reasonable.rb +181 -0
  80. data/lib/solid_agent/records/agent.rb +520 -0
  81. data/lib/solid_agent/records/agent_run.rb +520 -0
  82. data/lib/solid_agent/records/agent_template.rb +142 -0
  83. data/lib/solid_agent/records/agent_version.rb +141 -0
  84. data/lib/solid_agent/records/ownable.rb +130 -0
  85. data/lib/solid_agent/records.rb +152 -0
  86. data/lib/solid_agent/run_fingerprint.rb +51 -0
  87. data/lib/solid_agent/tool_cache.rb +91 -0
  88. data/lib/solid_agent/version.rb +1 -1
  89. data/lib/solid_agent.rb +70 -3
  90. metadata +87 -1
@@ -40,31 +40,60 @@ class AgentContext < ApplicationRecord
40
40
 
41
41
  # Records a generation response and updates token counts
42
42
  #
43
+ # Response attributes are read defensively: ActiveAgent 1.x response
44
+ # objects don't expose #provider or #duration, and provider messages may
45
+ # not respond to #tool_calls — a hard read would raise inside
46
+ # SolidAgent's rescued persistence callback and silently drop the
47
+ # generation.
48
+ #
43
49
  # @param response [ActiveAgent::GenerationResponse] the generation response
50
+ # @param extra_attributes [Hash] additional column values (e.g. trace_id, provenance)
44
51
  # @return [AgentGeneration] the created generation record
45
- def record_generation!(response)
46
- generation = generations.create!(
52
+ def record_generation!(response, extra_attributes = {})
53
+ usage = response.respond_to?(:usage) ? response.usage : nil
54
+
55
+ generation = generations.create!({
47
56
  content: response.message&.content,
48
- model: response.model,
49
- provider: response.provider,
50
- finish_reason: response.finish_reason,
51
- input_tokens: response.usage&.input_tokens || 0,
52
- output_tokens: response.usage&.output_tokens || 0,
57
+ model: response_value(response, :model),
58
+ provider: response_value(response, :provider),
59
+ finish_reason: response_value(response, :finish_reason),
60
+ input_tokens: usage&.input_tokens || 0,
61
+ output_tokens: usage&.output_tokens || 0,
62
+ cached_tokens: response_value(usage, :cached_tokens) || 0,
63
+ reasoning_tokens: response_value(usage, :reasoning_tokens) || 0,
53
64
  tool_calls: extract_tool_calls(response),
54
- raw_response: response.raw_response,
55
- duration_seconds: response.duration
56
- )
65
+ raw_response: response_value(response, :raw_response),
66
+ duration_seconds: extract_duration_seconds(response, usage)
67
+ }.merge(extra_attributes))
57
68
 
58
69
  # Update cumulative token counts
59
70
  increment!(:total_input_tokens, generation.input_tokens)
60
71
  increment!(:total_output_tokens, generation.output_tokens)
61
72
 
62
73
  # Also add assistant message to the conversation
63
- add_assistant_message(response.message&.content, tool_calls: generation.tool_calls)
74
+ add_assistant_message(response.message&.content, metadata: { "tool_calls" => generation.tool_calls })
64
75
 
65
76
  generation
66
77
  end
67
78
 
79
+ # Records a generation together with its provenance snapshot, correlating
80
+ # it with the distributed trace via provenance[:trace_id].
81
+ #
82
+ # Called automatically by SolidAgent::HasContext when this method exists.
83
+ #
84
+ # @param response [ActiveAgent::GenerationResponse] the generation response
85
+ # @param provenance [Hash] provenance hash from HasContext#current_provenance
86
+ # @return [AgentGeneration] the created generation record
87
+ def record_generation_with_provenance!(response, provenance)
88
+ provenance = (provenance || {}).deep_stringify_keys
89
+
90
+ record_generation!(
91
+ response,
92
+ trace_id: provenance["trace_id"],
93
+ provenance: provenance
94
+ )
95
+ end
96
+
68
97
  # Adds a user message to the context
69
98
  #
70
99
  # @param content [String] the message content
@@ -97,12 +126,14 @@ class AgentContext < ApplicationRecord
97
126
  # @param tool_name [String] the name of the tool
98
127
  # @param result [Hash, String] the tool result
99
128
  # @return [AgentMessage] the created message
100
- def add_tool_message(tool_call_id:, tool_name:, result:)
129
+ def add_tool_message(tool_call_id:, tool_name:, result:, arguments: nil, duration_ms: nil)
101
130
  messages.create!(
102
131
  role: "tool",
103
132
  tool_call_id: tool_call_id,
104
133
  tool_name: tool_name,
105
134
  tool_result: result,
135
+ tool_arguments: arguments.presence || {},
136
+ metadata: duration_ms ? { "duration_ms" => duration_ms } : {},
106
137
  content: result.is_a?(String) ? result : result.to_json
107
138
  )
108
139
  end
@@ -114,14 +145,26 @@ class AgentContext < ApplicationRecord
114
145
 
115
146
  private
116
147
 
148
+ def response_value(response, method)
149
+ response.respond_to?(method) ? response.public_send(method) : nil
150
+ end
151
+
152
+ def extract_duration_seconds(response, usage)
153
+ return response.duration if response.respond_to?(:duration) && response.duration
154
+
155
+ duration_ms = usage.respond_to?(:duration_ms) ? usage.duration_ms : nil
156
+ duration_ms ? duration_ms / 1000.0 : nil
157
+ end
158
+
117
159
  def extract_tool_calls(response)
118
- return [] unless response.message&.tool_calls.present?
160
+ message = response.message
161
+ return [] unless message.respond_to?(:tool_calls) && message.tool_calls.present?
119
162
 
120
- response.message.tool_calls.map do |tc|
163
+ message.tool_calls.map do |tc|
121
164
  {
122
- id: tc.id,
123
- name: tc.name,
124
- arguments: tc.arguments
165
+ id: tc.respond_to?(:id) ? tc.id : nil,
166
+ name: tc.respond_to?(:name) ? tc.name : nil,
167
+ arguments: tc.respond_to?(:arguments) ? tc.arguments : nil
125
168
  }
126
169
  end
127
170
  end
@@ -19,6 +19,7 @@ class AgentGeneration < ApplicationRecord
19
19
  scope :recent, -> { order(created_at: :desc) }
20
20
  scope :by_model, ->(model) { where(model: model) }
21
21
  scope :with_tool_calls, -> { where.not(tool_calls: []) }
22
+ scope :with_trace, ->(trace_id) { where(trace_id: trace_id) }
22
23
  scope :completed, -> { where(finish_reason: "stop") }
23
24
 
24
25
  # Returns total token count for this generation
@@ -26,6 +27,16 @@ class AgentGeneration < ApplicationRecord
26
27
  input_tokens + output_tokens
27
28
  end
28
29
 
30
+ # Provider prompt-cache hit on this generation?
31
+ def cache_hit?
32
+ cached_tokens.to_i.positive?
33
+ end
34
+
35
+ # Extended thinking captured?
36
+ def thinking?
37
+ reasoning_tokens.to_i.positive?
38
+ end
39
+
29
40
  # Check if this generation included tool calls
30
41
  def has_tool_calls?
31
42
  tool_calls.present? && tool_calls.any?
@@ -49,11 +60,17 @@ class AgentGeneration < ApplicationRecord
49
60
  # Returns the cost estimate based on token usage (override with your pricing)
50
61
  #
51
62
  # @param input_price_per_million [Float] price per million input tokens
52
- # @param output_price_per_million [Float] price per million output tokens
53
- # @return [Float] estimated cost in dollars
54
- def estimated_cost(input_price_per_million: 3.0, output_price_per_million: 15.0)
55
- input_cost = (input_tokens / 1_000_000.0) * input_price_per_million
56
- output_cost = (output_tokens / 1_000_000.0) * output_price_per_million
57
- input_cost + output_cost
63
+ # @param output_price_per_million [Float, nil] price per million output tokens
64
+ # @return [Float, nil] estimated cost in dollars, nil when nothing to price
65
+ def estimated_cost(input_price_per_million: nil, output_price_per_million: nil)
66
+ if input_price_per_million && output_price_per_million
67
+ input_cost = (input_tokens / 1_000_000.0) * input_price_per_million
68
+ output_cost = (output_tokens / 1_000_000.0) * output_price_per_million
69
+ input_cost + output_cost
70
+ else
71
+ # Per-model rates from SolidAgent::ModelPricing (RubyLLM registry
72
+ # when available, static table otherwise)
73
+ SolidAgent::ModelPricing.estimate(model: model, input_tokens: input_tokens, output_tokens: output_tokens)
74
+ end
58
75
  end
59
76
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Agent-curated long-term memory for a subject record, written and read by
4
+ # agents through SolidAgent::HasMemory's save_memory/recall_memory tools.
5
+ #
6
+ # Memory is scoped to (memorable, scope) — not to an agent class — so any
7
+ # agent operating on the same subject shares it, making it a handoff
8
+ # channel between agents. Entry source_agent records who wrote each note.
9
+ class AgentMemory < ApplicationRecord
10
+ belongs_to :memorable, polymorphic: true, optional: true
11
+ has_many :entries, class_name: "AgentMemoryEntry", dependent: :destroy
12
+
13
+ validates :scope, presence: true
14
+
15
+ # Finds or creates the memory for a subject.
16
+ def self.for(memorable, scope: SolidAgent::HasMemory::DEFAULT_SCOPE)
17
+ find_or_create_by!(memorable: memorable, scope: scope.to_s)
18
+ end
19
+
20
+ # Appends a summary note.
21
+ def remember(content, source_agent: nil, category: nil)
22
+ entries.create!(content: content, source_agent: source_agent, category: category)
23
+ end
24
+
25
+ # Most recent notes first.
26
+ def recall(limit: 20, category: nil)
27
+ scope = entries.order(created_at: :desc)
28
+ scope = scope.where(category: category) if category.present?
29
+ scope.limit(limit || 20).to_a
30
+ end
31
+
32
+ def forget(entry_id)
33
+ entries.find(entry_id).destroy!
34
+ end
35
+
36
+ def summary_list
37
+ entries.order(:created_at).pluck(:content)
38
+ end
39
+
40
+ # Formatted block suitable for injecting into another agent's
41
+ # instructions when handing a subject off.
42
+ def to_prompt
43
+ notes = entries.order(:created_at).map do |entry|
44
+ source = entry.source_agent.present? ? " (#{entry.source_agent})" : ""
45
+ "- #{entry.content}#{source}"
46
+ end
47
+ return "" if notes.empty?
48
+
49
+ "Memory notes for this subject:\n#{notes.join("\n")}"
50
+ end
51
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # One agent-authored summary note in an AgentMemory.
4
+ class AgentMemoryEntry < ApplicationRecord
5
+ belongs_to :agent_memory
6
+
7
+ validates :content, presence: true
8
+
9
+ scope :chronological, -> { order(:created_at) }
10
+ scope :by_category, ->(category) { where(category: category) }
11
+ scope :from_agent, ->(agent_name) { where(source_agent: agent_name) }
12
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A single agent execution: lifecycle status, inputs/outputs, usage, and
4
+ # an append-only stream of progress events. Correlates with AgentContext/
5
+ # AgentGeneration rows and telemetry traces via trace_id.
6
+ class AgentRun < ApplicationRecord
7
+ belongs_to :runnable, polymorphic: true, optional: true
8
+
9
+ STATUSES = %w[pending running complete failed cancelled].freeze
10
+
11
+ validates :status, inclusion: { in: STATUSES }
12
+
13
+ scope :recent, -> { order(created_at: :desc) }
14
+ scope :for_agent, ->(agent_name) { where(agent_name: agent_name) }
15
+ scope :for_action, ->(action_name) { where(action_name: action_name) }
16
+ scope :with_trace, ->(trace_id) { where(trace_id: trace_id) }
17
+ scope :for_status, ->(status) { where(status: status) }
18
+
19
+ STATUSES.each do |status_name|
20
+ define_method("#{status_name}?") { status == status_name }
21
+ end
22
+
23
+ def in_progress?
24
+ pending? || running?
25
+ end
26
+
27
+ def finished?
28
+ complete? || failed? || cancelled?
29
+ end
30
+
31
+ # === Lifecycle ===
32
+
33
+ def start!
34
+ update!(status: "running", started_at: Time.current)
35
+ end
36
+
37
+ def complete!(output: nil, metadata: {}, input_tokens: nil, output_tokens: nil)
38
+ update!(
39
+ status: "complete",
40
+ output: output,
41
+ output_metadata: (output_metadata || {}).merge(metadata),
42
+ input_tokens: input_tokens || self.input_tokens,
43
+ output_tokens: output_tokens || self.output_tokens,
44
+ completed_at: Time.current,
45
+ duration_ms: calculated_duration_ms(fallback_end: Time.current)
46
+ )
47
+ end
48
+
49
+ def fail!(error)
50
+ update!(
51
+ status: "failed",
52
+ error_message: error.respond_to?(:message) ? error.message : error.to_s,
53
+ completed_at: Time.current,
54
+ duration_ms: calculated_duration_ms(fallback_end: Time.current)
55
+ )
56
+ end
57
+
58
+ def cancel!
59
+ return false if finished?
60
+
61
+ update!(status: "cancelled", completed_at: Time.current)
62
+ true
63
+ end
64
+
65
+ # === Progress events ===
66
+
67
+ # Appends a progress event mid-run so pollers can stream what the agent
68
+ # is doing (pending llm/tool/agent calls). Events pair up by eid: a
69
+ # "started" event is pending until a "done"/"error" with the same eid
70
+ # lands. update_column: no validations/callbacks, so this is safe to call
71
+ # from the run's own execution thread.
72
+ #
73
+ # Read-modify-write on a JSON column drops entries when two writers race,
74
+ # so the re-read and the write are serialized by a row lock.
75
+ def append_event(kind:, label:, eid: nil, status: "done", detail: nil, duration_ms: nil)
76
+ event = {
77
+ "at" => Time.current.iso8601(3),
78
+ "eid" => eid,
79
+ "kind" => kind.to_s,
80
+ "label" => label.to_s,
81
+ "status" => status.to_s
82
+ }.compact
83
+ event["detail"] = detail.to_s.byteslice(0, 1200).to_s.scrub if detail
84
+ event["duration_ms"] = duration_ms if duration_ms
85
+
86
+ with_lock do
87
+ current = self.class.where(id: id).pick(:events) || []
88
+ update_column(:events, current + [ event ])
89
+ end
90
+
91
+ event
92
+ end
93
+
94
+ # === Cohort fingerprinting ===
95
+
96
+ # Records the instructions this run executed under as a stable digest —
97
+ # the grouping key (with model) for configuration cohorts.
98
+ def record_instructions(instructions)
99
+ self.instructions_digest = SolidAgent::RunFingerprint.digest(instructions)
100
+ end
101
+
102
+ # Deterministic memorable name for the digest ("calm-heron") — reads far
103
+ # better than hex when comparing cohorts.
104
+ def instructions_codename
105
+ SolidAgent::RunFingerprint.codename(instructions_digest)
106
+ end
107
+
108
+ # === Usage ===
109
+
110
+ def total_tokens
111
+ input_tokens.to_i + output_tokens.to_i
112
+ end
113
+
114
+ def calculated_duration_ms(fallback_end: nil)
115
+ return duration_ms if duration_ms.present?
116
+
117
+ finish = completed_at || fallback_end
118
+ return nil unless started_at && finish
119
+
120
+ ((finish - started_at) * 1000).to_i
121
+ end
122
+ end
@@ -18,6 +18,9 @@ class CreateAgentGenerations < ActiveRecord::Migration<%= migration_version %>
18
18
  # Token usage for this generation
19
19
  t.integer :input_tokens, default: 0
20
20
  t.integer :output_tokens, default: 0
21
+ # Provider prompt-cache hits and extended-thinking usage
22
+ t.integer :cached_tokens, default: 0
23
+ t.integer :reasoning_tokens, default: 0
21
24
 
22
25
  # Tool calls made in this generation
23
26
  t.jsonb :tool_calls, default: []
@@ -28,11 +31,21 @@ class CreateAgentGenerations < ActiveRecord::Migration<%= migration_version %>
28
31
  # Timing
29
32
  t.float :duration_seconds
30
33
 
34
+ # Telemetry correlation: the distributed trace this generation belongs
35
+ # to (e.g. ActiveAgent::Telemetry trace_id), threaded through
36
+ # prompt_options[:trace_id]
37
+ t.string :trace_id
38
+
39
+ # Provenance snapshot (agent/prompt/context checksums) captured at
40
+ # generation time — see SolidAgent::HasContext#current_provenance
41
+ t.jsonb :provenance, default: {}
42
+
31
43
  t.timestamps
32
44
  end
33
45
 
34
46
  add_index :agent_generations, :model
35
47
  add_index :agent_generations, :finish_reason
48
+ add_index :agent_generations, :trace_id
36
49
  add_index :agent_generations, [:agent_context_id, :created_at]
37
50
  end
38
51
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAgentMemories < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :agent_memories do |t|
6
+ # The subject the memory is about (an Agent record, User, Project...).
7
+ # Any agent operating on the same subject + scope shares the memory,
8
+ # which is what makes it a handoff channel between agents.
9
+ t.references :memorable, polymorphic: true, index: true
10
+
11
+ # Namespace so a subject can carry independent memory streams.
12
+ t.string :scope, null: false, default: "default"
13
+
14
+ t.timestamps
15
+ end
16
+ add_index :agent_memories, [ :memorable_type, :memorable_id, :scope ], unique: true
17
+
18
+ create_table :agent_memory_entries do |t|
19
+ t.references :agent_memory, null: false, foreign_key: true
20
+
21
+ # The summary note the agent chose to persist.
22
+ t.text :content, null: false
23
+
24
+ # Which agent class wrote it (handoff provenance).
25
+ t.string :source_agent
26
+
27
+ # Optional label: fact, task, handoff, ...
28
+ t.string :category
29
+
30
+ t.timestamps
31
+ end
32
+ add_index :agent_memory_entries, [ :agent_memory_id, :created_at ]
33
+ add_index :agent_memory_entries, :category
34
+ end
35
+ end
@@ -23,6 +23,11 @@ class CreateAgentMessages < ActiveRecord::Migration<%= migration_version %>
23
23
  # Metadata
24
24
  t.jsonb :metadata, default: {}
25
25
 
26
+ # Provenance snapshot + content checksum, populated automatically by
27
+ # SolidAgent::HasContext when present
28
+ t.jsonb :provenance, default: {}
29
+ t.string :content_checksum
30
+
26
31
  t.timestamps
27
32
  end
28
33
 
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateAgentRuns < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :agent_runs do |t|
6
+ # What the run executed against (an Agent record, a workflow, any
7
+ # AR subject) — optional, so bare executors can still record runs.
8
+ t.references :runnable, polymorphic: true, index: true
9
+
10
+ # Correlation keys shared with agent_contexts/agent_generations and
11
+ # telemetry traces.
12
+ t.string :agent_name
13
+ t.string :action_name
14
+ t.string :trace_id
15
+
16
+ t.string :status, null: false, default: "pending"
17
+
18
+ t.text :input_prompt
19
+ t.jsonb :input_params, default: {}
20
+ t.text :output
21
+ t.jsonb :output_metadata, default: {}
22
+ t.text :error_message
23
+
24
+ # Append-only progress events ({at, eid, kind, label, status,
25
+ # detail, duration_ms}) streamed to pollers mid-run.
26
+ t.jsonb :events, default: []
27
+
28
+ # 8-char fingerprint of the instructions the run executed under —
29
+ # the cohort grouping key for instruction/model comparisons.
30
+ t.string :instructions_digest
31
+
32
+ t.integer :input_tokens, default: 0
33
+ t.integer :output_tokens, default: 0
34
+ t.integer :duration_ms
35
+ t.datetime :started_at
36
+ t.datetime :completed_at
37
+
38
+ t.timestamps
39
+ end
40
+
41
+ add_index :agent_runs, :status
42
+ add_index :agent_runs, :trace_id
43
+ add_index :agent_runs, :instructions_digest
44
+ add_index :agent_runs, :created_at
45
+ end
46
+ end
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module SolidAgent
6
+ module Generators
7
+ class ManifestGenerator < Rails::Generators::NamedBase
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Generates a .agent.md manifest file for an agent"
11
+
12
+ class_option :model, type: :string, default: "anthropic/claude-sonnet-4-20250514",
13
+ desc: "Model identifier (provider/model format)"
14
+
15
+ class_option :tools, type: :array, default: [],
16
+ desc: "Tool names to include"
17
+
18
+ class_option :template, type: :string, default: nil,
19
+ desc: "Use a preset template (research, assistant, reviewer, chat)"
20
+
21
+ class_option :format, type: :string, default: "agent_md",
22
+ desc: "Output format (agent_md, dotprompt)"
23
+
24
+ class_option :context, type: :string, default: nil,
25
+ desc: "Contextual param key for HasContext (e.g., user, document)"
26
+
27
+ class_option :description, type: :string, default: nil,
28
+ desc: "Agent description"
29
+
30
+ def create_manifest_file
31
+ @model = options[:model]
32
+ @tools = options[:tools]
33
+ @preset = options[:template]
34
+ @format = options[:format].to_sym
35
+ @contextual = options[:context]
36
+ @description = options[:description] || default_description
37
+
38
+ # Apply preset template configuration
39
+ apply_preset if @preset
40
+
41
+ case @format
42
+ when :agent_md
43
+ template "agent.md.erb", manifest_path(".agent.md")
44
+ when :dotprompt
45
+ template "prompt.erb", manifest_path(".prompt")
46
+ else
47
+ template "agent.md.erb", manifest_path(".agent.md")
48
+ end
49
+ end
50
+
51
+ def show_next_steps
52
+ say ""
53
+ say "Manifest created successfully!", :green
54
+ say ""
55
+ say "File generated:"
56
+ say " #{manifest_path(extension_for_format)}"
57
+ say ""
58
+ say "Usage:", :yellow
59
+ say " # Parse the manifest"
60
+ say " manifest = SolidAgent::AgentManifest.parse(\"#{manifest_path(extension_for_format)}\")"
61
+ say ""
62
+ say " # Load as agent class"
63
+ say " klass = SolidAgent::AgentManifest.load_agent(\"#{manifest_path(extension_for_format)}\")"
64
+ say ""
65
+ say " # Validate the manifest"
66
+ say " SolidAgent::AgentManifest.validate!(\"#{manifest_path(extension_for_format)}\")"
67
+ say ""
68
+
69
+ if @tools.any?
70
+ say "Tool stubs added. Implement the tool methods in your agent class:", :yellow
71
+ @tools.each do |tool|
72
+ say " def #{tool}(args)"
73
+ say " # Implement #{tool} logic"
74
+ say " end"
75
+ end
76
+ say ""
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def file_name
83
+ name.underscore
84
+ end
85
+
86
+ def class_name
87
+ name.camelize
88
+ end
89
+
90
+ def agent_class_name
91
+ "#{class_name}Agent"
92
+ end
93
+
94
+ def agent_title
95
+ class_name.gsub(/([A-Z])/, ' \1').strip
96
+ end
97
+
98
+ def manifest_path(extension)
99
+ "app/views/#{file_name}_agent/agent#{extension}"
100
+ end
101
+
102
+ def extension_for_format
103
+ case @format
104
+ when :agent_md then ".agent.md"
105
+ when :dotprompt then ".prompt"
106
+ else ".agent.md"
107
+ end
108
+ end
109
+
110
+ def default_description
111
+ "#{agent_title} agent"
112
+ end
113
+
114
+ def apply_preset
115
+ case @preset.to_s
116
+ when "research"
117
+ @description ||= "Research and analyze topics thoroughly, providing well-sourced information"
118
+ @tools = %w[search fetch analyze] if @tools.empty?
119
+ @preset_instructions = research_instructions
120
+ when "assistant"
121
+ @description ||= "General-purpose assistant for answering questions and helping with tasks"
122
+ @preset_instructions = assistant_instructions
123
+ when "reviewer"
124
+ @description ||= "Review and provide feedback on content, code, or documents"
125
+ @tools = %w[read_file analyze_diff] if @tools.empty?
126
+ @preset_instructions = reviewer_instructions
127
+ when "chat"
128
+ @description ||= "Conversational agent for multi-turn dialogue"
129
+ @contextual ||= "user"
130
+ @preset_instructions = chat_instructions
131
+ end
132
+ end
133
+
134
+ def research_instructions
135
+ <<~INSTRUCTIONS
136
+ You are a thorough research assistant. Your goal is to provide accurate,
137
+ well-sourced information on any topic.
138
+
139
+ ## Guidelines
140
+
141
+ - Always cite your sources when providing information
142
+ - Present multiple perspectives when topics are controversial
143
+ - Acknowledge uncertainty when information is incomplete
144
+ - Break down complex topics into understandable explanations
145
+ - Verify facts before presenting them
146
+ INSTRUCTIONS
147
+ end
148
+
149
+ def assistant_instructions
150
+ <<~INSTRUCTIONS
151
+ You are a helpful assistant. Your goal is to assist users with their
152
+ questions and tasks effectively.
153
+
154
+ ## Guidelines
155
+
156
+ - Be concise but thorough in your responses
157
+ - Ask clarifying questions when requests are ambiguous
158
+ - Provide step-by-step guidance for complex tasks
159
+ - Offer alternatives when appropriate
160
+ INSTRUCTIONS
161
+ end
162
+
163
+ def reviewer_instructions
164
+ <<~INSTRUCTIONS
165
+ You are a thorough reviewer. Your goal is to provide constructive feedback
166
+ that helps improve the quality of the work.
167
+
168
+ ## Guidelines
169
+
170
+ - Focus on both strengths and areas for improvement
171
+ - Be specific with your feedback
172
+ - Provide actionable suggestions
173
+ - Maintain a constructive and respectful tone
174
+ - Consider the context and goals of the work
175
+ INSTRUCTIONS
176
+ end
177
+
178
+ def chat_instructions
179
+ <<~INSTRUCTIONS
180
+ You are a conversational assistant. Engage in helpful, natural dialogue
181
+ while maintaining context from previous messages.
182
+
183
+ ## Guidelines
184
+
185
+ - Remember context from earlier in the conversation
186
+ - Ask follow-up questions to better understand the user's needs
187
+ - Be friendly and approachable
188
+ - Know when to be concise vs. detailed based on the question
189
+ INSTRUCTIONS
190
+ end
191
+
192
+ def default_instructions
193
+ @preset_instructions || <<~INSTRUCTIONS
194
+ You are the #{agent_title} agent.
195
+
196
+ ## Instructions
197
+
198
+ TODO - Add your agent's instructions here.
199
+
200
+ ## Guidelines
201
+
202
+ - Be helpful and accurate
203
+ - Follow the user's instructions carefully
204
+ - Ask for clarification when needed
205
+ INSTRUCTIONS
206
+ end
207
+ end
208
+ end
209
+ end