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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +68 -0
- data/LICENSE +21 -0
- data/README.md +209 -18
- data/Rakefile +22 -2
- 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 +2 -2
- data/lib/generators/solid_agent/agent/templates/agent.rb.erb +3 -3
- data/lib/generators/solid_agent/context/templates/context_model.rb.erb +50 -16
- data/lib/generators/solid_agent/context/templates/create_generations.rb.erb +8 -0
- data/lib/generators/solid_agent/context/templates/create_messages.rb.erb +4 -0
- data/lib/generators/solid_agent/context/templates/generation_model.rb.erb +11 -0
- data/lib/generators/solid_agent/install/install_generator.rb +9 -0
- data/lib/generators/solid_agent/install/templates/agent_context.rb.erb +60 -17
- data/lib/generators/solid_agent/install/templates/agent_generation.rb.erb +23 -6
- 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_run.rb.erb +122 -0
- data/lib/generators/solid_agent/install/templates/create_agent_generations.rb.erb +13 -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 +5 -0
- data/lib/generators/solid_agent/install/templates/create_agent_runs.rb.erb +46 -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/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/has_context.rb +251 -30
- data/lib/solid_agent/has_memory.rb +136 -0
- data/lib/solid_agent/has_reasons.rb +230 -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/tool_cache.rb +91 -0
- data/lib/solid_agent/version.rb +1 -1
- data/lib/solid_agent.rb +70 -3
- 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
|
-
|
|
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
|
|
49
|
-
provider: response
|
|
50
|
-
finish_reason: response
|
|
51
|
-
input_tokens:
|
|
52
|
-
output_tokens:
|
|
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
|
|
55
|
-
duration_seconds: response
|
|
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,
|
|
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
|
-
|
|
160
|
+
message = response.message
|
|
161
|
+
return [] unless message.respond_to?(:tool_calls) && message.tool_calls.present?
|
|
119
162
|
|
|
120
|
-
|
|
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:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|