rcrewai-rails 0.6.0 → 0.7.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -1
  3. data/README.md +149 -3
  4. data/app/controllers/rcrewai/rails/observations_controller.rb +27 -0
  5. data/app/jobs/rcrewai/rails/crew_execution_job.rb +18 -29
  6. data/app/models/rcrewai/rails/execution.rb +6 -0
  7. data/app/models/rcrewai/rails/span.rb +57 -0
  8. data/app/models/rcrewai/rails/span_event.rb +19 -0
  9. data/app/views/rcrewai/rails/observations/_span.html.erb +51 -0
  10. data/app/views/rcrewai/rails/observations/costs.html.erb +52 -0
  11. data/app/views/rcrewai/rails/observations/show.html.erb +47 -0
  12. data/config/routes.rb +6 -0
  13. data/db/migrate/010_create_rcrewai_spans.rb +42 -0
  14. data/db/migrate/011_add_observation_rollups_to_rcrewai_executions.rb +8 -0
  15. data/docs/api.html +223 -0
  16. data/docs/capabilities.html +268 -0
  17. data/docs/examples.html +257 -0
  18. data/docs/getting-started.html +275 -0
  19. data/docs/index.html +57 -207
  20. data/lib/generators/rcrewai/rails/install/install_generator.rb +28 -16
  21. data/lib/generators/rcrewai/rails/install/templates/create_rcrewai_tables.rb +60 -0
  22. data/lib/generators/rcrewai/rails/install/templates/rcrewai.rb +17 -0
  23. data/lib/rcrewai/rails/configuration.rb +10 -1
  24. data/lib/rcrewai/rails/observation/collector.rb +301 -0
  25. data/lib/rcrewai/rails/observation/pruner.rb +48 -0
  26. data/lib/rcrewai/rails/observation/rollup.rb +60 -0
  27. data/lib/rcrewai/rails/observation/span_stack.rb +76 -0
  28. data/lib/rcrewai/rails/observation/writer.rb +89 -0
  29. data/lib/rcrewai/rails/version.rb +1 -1
  30. data/lib/rcrewai/rails.rb +13 -0
  31. data/lib/tasks/rcrewai_observation.rake +10 -0
  32. data/rcrewai-rails.gemspec +1 -1
  33. metadata +21 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92b441696cd97a26aeca352027a7f8e4bdef639f34c452494599d51cb302b8b6
4
- data.tar.gz: 00466757e89f9b12e46956dcdb38d67a40a938bb4a68d43e0292f79babfb6260
3
+ metadata.gz: e21a3a1057f9221cca3d280f2307b219d15a636995284c781404c4ed3d283fa6
4
+ data.tar.gz: 9b715b0cc17839eb5fb23f578905d962c4b6450d0aae2b8b1f494d549497a8b3
5
5
  SHA512:
6
- metadata.gz: ec0516a3dba432d2b1a6308f89981a65905e110a4ef543ed5a516476ab62f47c9ba86cdc538288e52512841c34e01f5c2e9146970420d6435144922c4e2926a7
7
- data.tar.gz: 26fb59a9bed5be7db14f7520a57d037c23e46bf572adf7946c98cf6fbe2d67fd3dd682e85942c84b38d9807e612aeb575b104f4c411f1dcd450f3de2cb8ab7b1
6
+ metadata.gz: 1be59f83c847e48c36b0ac61527ca9dedc61b903763be6145d6dd26354575a74d2dee3fc807a804e6f847660347c8368a28d70f33df6cb57d75d2b952b98657f
7
+ data.tar.gz: 0f37c9c304ef0264afeb363f07ca1b23c72045d9686869fe9325e336a79f30bc1930d08f97470a6235ad7d4dc8d1e5ebab4c78eb3de9d9ae1129ea42cb48dca8
data/CHANGELOG.md CHANGED
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-08-13
11
+
12
+ ### Added
13
+ - Observation engine: span-tree tracing of every crew execution with per-agent,
14
+ per-LLM-call and per-tool-call detail (timings, token counts, cost). Includes a
15
+ trace waterfall at `/rcrewai/executions/:id/observation`, a cost/performance
16
+ dashboard at `/rcrewai/observations/costs`, live monitoring over Turbo Streams,
17
+ and a `rcrewai:observation:prune` rake task with retention configuration
18
+ (`observation_retention_days`).
19
+
20
+ ### Fixed
21
+ - The install generator only wrote an initializer: it never copied a migration
22
+ and never mounted the engine, so following the documented install produced no
23
+ database tables and no reachable UI. It now creates a timestamped
24
+ `create_rcrewai_tables` migration, mounts the engine at `/rcrewai`, and
25
+ installs the fully documented initializer.
26
+ - The install migration template was missing the `rcrewai_tools` table and the
27
+ tasks table's `agent` reference, so a fresh install shipped a `Tool` model with
28
+ no backing table. The template now matches the schema the test suite validates.
29
+
30
+ ### Deprecated
31
+ - `Execution#log` and `ExecutionLog`, superseded by the observation engine. Both
32
+ still work and now emit a deprecation warning; scheduled for removal one minor
33
+ version after this release.
34
+
35
+ ### Requires
36
+ - `rcrewai >= 0.7.1` for agent-level tracing. On earlier 0.7.x versions traces
37
+ contain only crew-level spans.
38
+
39
+ ## [0.6.1] - 2026-07-07
40
+
41
+ ### Fixed
42
+ - Refresh the README for rcrewai 0.7: corrected the install-generator namespace
43
+ (`rcrewai:rails:install`) and removed a broken crew-level `memory_enabled`
44
+ example (that DSL method no longer exists — memory is agent-level). Added a
45
+ "rcrewai 0.7 capabilities" section documenting the new agent/task/crew/
46
+ knowledge/flow configuration. Docs only.
47
+
10
48
  ## [0.6.0] - 2026-07-07
11
49
 
12
50
  Tracks rcrewai 0.7.0: adds the `:consensual` crew process and agent-level
@@ -114,7 +152,9 @@ existing agents, tasks, and crews build unchanged.
114
152
  ### Changed
115
153
  - Rename generators from `rcrew_a_i` to `rcrewai` namespacing.
116
154
 
117
- [Unreleased]: https://github.com/gkosmo/rcrewai-rails/compare/v0.6.0...HEAD
155
+ [Unreleased]: https://github.com/gkosmo/rcrewai-rails/compare/v0.7.0...HEAD
156
+ [0.7.0]: https://github.com/gkosmo/rcrewai-rails/compare/v0.6.1...v0.7.0
157
+ [0.6.1]: https://github.com/gkosmo/rcrewai-rails/compare/v0.6.0...v0.6.1
118
158
  [0.6.0]: https://github.com/gkosmo/rcrewai-rails/compare/v0.5.1...v0.6.0
119
159
  [0.5.1]: https://github.com/gkosmo/rcrewai-rails/compare/v0.5.0...v0.5.1
120
160
  [0.5.0]: https://github.com/gkosmo/rcrewai-rails/compare/v0.4.0...v0.5.0
data/README.md CHANGED
@@ -10,6 +10,55 @@ Rails engine for integrating [RcrewAI](https://github.com/gkosmo/rcrewai-rails)
10
10
  - **Web UI**: Monitor and manage crews through a built-in interface
11
11
  - **Rails-Specific Tools**: Pre-built tools for ActiveRecord, ActionMailer, Rails cache, and more
12
12
  - **Configuration**: Flexible configuration through Rails initializers
13
+ - **Full rcrewai 0.7 feature coverage** (see [rcrewai 0.7 capabilities](#rcrewai-07-capabilities)):
14
+ - Agent config: reasoning, per-agent LLM, rate limiting, context-window trimming, cognitive memory
15
+ - Task output: structured output schemas, guardrails, file output, multimodal attachments
16
+ - Crew: `before_kickoff`/`after_kickoff` hooks, planning, the `consensual` process, batch execution
17
+ - Knowledge (RAG) sources and Flow persistence
18
+
19
+ ## Observation Engine
20
+
21
+ Every crew execution is traced as a tree of spans — crew, agent, task, LLM call, and
22
+ tool call — carrying timings, token counts, and cost.
23
+
24
+ - **Trace view** at `/rcrewai/executions/:id/observation`: a waterfall of the run, with
25
+ prompts, tool arguments, and errors on each span.
26
+ - **Cost and performance** at `/rcrewai/observations/costs`: spend and token totals
27
+ across recent executions.
28
+ - **Live monitoring**: the trace view updates over Turbo Streams while a run is in progress.
29
+
30
+ Configure it in `config/initializers/rcrewai.rb`:
31
+
32
+ ```ruby
33
+ config.observation_enabled = true
34
+ config.observation_capture_prompts = :truncated # :none | :truncated | :full
35
+ config.observation_prompt_max_bytes = 4_096
36
+ config.observation_flush_mode = :batched # :batched | :immediate
37
+ config.observation_retention_days = 30
38
+ ```
39
+
40
+ Prompt text is truncated by default: full prompts can be large and may contain personal
41
+ data. Set `:full` only when you need lossless replay.
42
+
43
+ Prune old spans with the bundled rake task:
44
+
45
+ ```bash
46
+ rake rcrewai:observation:prune # uses observation_retention_days
47
+ rake rcrewai:observation:prune DAYS=7
48
+ ```
49
+
50
+ ### Limitations
51
+
52
+ Token and cost data depend on rcrewai's **streaming** execution path. `Usage` events are
53
+ only emitted when an agent runs via `ToolRunner`, which passes a `stream:` to the LLM
54
+ client — not via `LegacyReactRunner`, which does not. `ToolRunner` is selected when the
55
+ tools have JSON schemas **and** the LLM client reports `supports_native_tools?`. OpenAI,
56
+ Anthropic, and Google all report true, so cost capture works normally with them. With a
57
+ provider or configuration that falls back to `LegacyReactRunner` (for example Ollama
58
+ without native tools), **the cost dashboard will be empty rather than showing an error**.
59
+
60
+ Agent-level tracing requires **rcrewai >= 0.7.1**, the version that threads the event
61
+ stream down to agent execution. On earlier versions traces contain only crew-level spans.
13
62
 
14
63
  ## Installation
15
64
 
@@ -28,7 +77,7 @@ $ bundle install
28
77
  Run the installation generator:
29
78
 
30
79
  ```bash
31
- $ rails generate rcrew_a_i:rails:install
80
+ $ rails generate rcrewai:rails:install
32
81
  $ rails db:migrate
33
82
  ```
34
83
 
@@ -103,8 +152,7 @@ class ResearchCrew
103
152
 
104
153
  crew_name "research_team"
105
154
  crew_description "AI-powered research team"
106
- process_type :sequential
107
- memory_enabled true
155
+ process_type :sequential # :sequential, :hierarchical, or :consensual
108
156
 
109
157
  def setup_agents
110
158
  @researcher = create_agent("researcher",
@@ -187,6 +235,101 @@ crew.execute_sync(inputs)
187
235
  CrewExecutionJob.set(wait: 5.minutes).perform_later(crew, inputs)
188
236
  ```
189
237
 
238
+ ## rcrewai 0.7 capabilities
239
+
240
+ This engine tracks [rcrewai](https://github.com/gkosmo/rcrewAI) `~> 0.7`. The
241
+ following capabilities are configured through columns on the persisted models and
242
+ forwarded to the core objects at build time. All are **off/absent by default**, so
243
+ existing records are unaffected — set only what you need.
244
+
245
+ ### Agent configuration (`RcrewAI::Rails::Agent`)
246
+
247
+ | Column | Effect |
248
+ |---|---|
249
+ | `max_rpm` | Rate-limit the agent's LLM calls (requests per minute) |
250
+ | `reasoning` / `max_reasoning_attempts` | Run a planning/reasoning pass before answering |
251
+ | `respect_context_window` | Trim history to fit the model's context window |
252
+ | `llm_config` (JSON) | Per-agent LLM override, e.g. `{ "provider": "anthropic", "model": "claude-sonnet-5" }` |
253
+ | `memory_enabled` + `memory_scope` + `memory_short_term_limit` | Enable cognitive memory (see below) |
254
+
255
+ ```ruby
256
+ agent = crew.agents.create!(
257
+ name: "researcher", role: "Researcher", goal: "Find facts",
258
+ reasoning: true,
259
+ max_rpm: 30,
260
+ llm_config: { provider: "anthropic", model: "claude-sonnet-5" },
261
+ memory_enabled: true, memory_scope: "research", memory_short_term_limit: 20
262
+ )
263
+ ```
264
+
265
+ **Agent memory** (rcrewai 0.6+): set `memory_enabled: true` to turn on cognitive
266
+ memory. `memory_scope` isolates an agent's memories; `memory_short_term_limit`
267
+ caps recent-execution recall. The embedder and store are objects, so configure
268
+ them once in the initializer:
269
+
270
+ ```ruby
271
+ RcrewAI::Rails.configure do |config|
272
+ config.default_memory_embedder = RCrewAI::Knowledge::Embedder.new
273
+ config.default_memory_store = RCrewAI::Memory::SqliteStore.new(path: "db/rcrewai_memory.sqlite3")
274
+ end
275
+ ```
276
+
277
+ ### Task output processing (`RcrewAI::Rails::Task`)
278
+
279
+ | Column | Effect |
280
+ |---|---|
281
+ | `output_schema` (JSON) | Validate/coerce the result against a JSON schema (structured output) |
282
+ | `guardrail_class` + `guardrail_method_name` + `guardrail_max_retries` | Validate/transform output, retrying on failure |
283
+ | `output_file` + `create_directory` + `markdown` | Write the result to disk |
284
+ | `attachments` (JSON) | Multimodal image inputs, e.g. `[{ "type": "image", "url": "https://…" }]` |
285
+
286
+ A guardrail is resolved from a host class: `guardrail_class` names a class whose
287
+ `guardrail_method_name` accepts the output and returns `[ok, value_or_error]`.
288
+
289
+ ### Crew orchestration (`RcrewAI::Rails::Crew`)
290
+
291
+ | Column | Effect |
292
+ |---|---|
293
+ | `process_type` | `"sequential"`, `"hierarchical"`, or `"consensual"` |
294
+ | `consensus_agents` | Number of proposers for the `consensual` process (default 3) |
295
+ | `planning` / `planning_llm` | Run a planner pass before execution |
296
+ | `before_kickoff_class`/`_method`, `after_kickoff_class`/`_method` | Lifecycle hooks resolved from host classes |
297
+
298
+ **Batch execution** (rcrewai `kickoff_for_each` parity) runs the crew once per
299
+ input set, one `Execution` per input grouped by a shared `batch_id`:
300
+
301
+ ```ruby
302
+ result = crew.execute_batch_sync([{ topic: "a" }, { topic: "b" }])
303
+ crew.batch_executions(result[:batch_id]) # the runs, in order
304
+ crew.execute_batch_async(inputs_list) # enqueue N jobs, returns the batch_id
305
+ ```
306
+
307
+ ### Knowledge (RAG)
308
+
309
+ Attach sources to an agent (role-specific) or a crew (shared with all its agents):
310
+
311
+ ```ruby
312
+ agent.knowledge_sources.create!(source_type: "url", value: "https://example.com/doc")
313
+ crew.knowledge_sources.create!(source_type: "string", value: "Reference text…")
314
+ # source_type: "string" | "file" | "pdf" | "csv" | "url"
315
+ ```
316
+
317
+ Active sources are embedded lazily at execution. See the memory initializer above
318
+ for embedder configuration.
319
+
320
+ ### Flows
321
+
322
+ Define Flow subclasses in your app (Ruby); the engine persists their state and
323
+ runs. Pass `RcrewAI::Rails::ActiveRecordStateStore` so flows resume from the DB,
324
+ and use `FlowRun.execute` to track a kickoff:
325
+
326
+ ```ruby
327
+ run = RcrewAI::Rails::FlowRun.execute(MyFlow, inputs: { topic: "ruby" })
328
+ run.status # "completed" / "failed"
329
+ run.result # the final flow state
330
+ RcrewAI::Rails::FlowState.find_by(state_id: run.state_id) # the persisted state
331
+ ```
332
+
190
333
  ## Database Models
191
334
 
192
335
  The gem provides these ActiveRecord models:
@@ -196,6 +339,9 @@ The gem provides these ActiveRecord models:
196
339
  - `RcrewAI::Rails::Task` - Task definitions
197
340
  - `RcrewAI::Rails::Execution` - Execution history
198
341
  - `RcrewAI::Rails::ExecutionLog` - Detailed execution logs
342
+ - `RcrewAI::Rails::KnowledgeSource` - Knowledge (RAG) sources, owned by an agent or a crew
343
+ - `RcrewAI::Rails::FlowState` - Persisted rcrewai Flow state (resume flows across restarts)
344
+ - `RcrewAI::Rails::FlowRun` - Flow-run tracking (status, inputs, result, timing)
199
345
 
200
346
  ## API Endpoints
201
347
 
@@ -0,0 +1,27 @@
1
+ module RcrewAI
2
+ module Rails
3
+ class ObservationsController < ApplicationController
4
+ # Hard ceiling on how deep the span partial will recurse. Real traces
5
+ # are a handful of levels deep; this only exists so a malformed tree
6
+ # cannot recurse until the request dies.
7
+ MAX_SPAN_DEPTH = 50
8
+
9
+ def show
10
+ @execution = Execution.find(params[:execution_id])
11
+ # Load the whole tree in one query and nest in memory — a recursive
12
+ # per-node query would be N+1 on deep traces.
13
+ @spans = @execution.spans.ordered.to_a
14
+ @children = @spans.group_by(&:parent_span_id)
15
+ @roots = @children[nil] || []
16
+ end
17
+
18
+ def costs
19
+ @executions = Execution.where.not(total_cost_usd: nil)
20
+ .order(created_at: :desc)
21
+ .limit(100)
22
+ @total_cost = @executions.sum(&:total_cost_usd)
23
+ @total_tokens = @executions.sum { |e| e.total_tokens.to_i }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -11,6 +11,7 @@ module RcrewAI
11
11
  inputs: inputs,
12
12
  batch_id: batch_id
13
13
  )
14
+ collector = nil
14
15
 
15
16
  begin
16
17
  execution.start!
@@ -18,7 +19,16 @@ module RcrewAI
18
19
 
19
20
  rcrew = crew.to_rcrew
20
21
 
21
- result = rcrew.execute(stream: stream_sink_for(execution))
22
+ collector = collector_for(execution)
23
+ collector&.start_crew_span(crew_name: crew.name)
24
+
25
+ result = rcrew.execute(stream: collector)
26
+
27
+ # Close agent spans as successful before finish!, which treats
28
+ # anything still open as an aborted run.
29
+ collector&.finish_open_agent_spans(status: "ok")
30
+ collector&.finish_crew_span(status: "ok")
31
+ collector&.finish!
22
32
 
23
33
  execution.complete!(result)
24
34
  execution.log("info", "Crew execution completed", { result: result })
@@ -27,6 +37,7 @@ module RcrewAI
27
37
 
28
38
  result
29
39
  rescue => e
40
+ collector&.finish!
30
41
  execution.fail!(e)
31
42
  execution.log("error", "Crew execution failed", {
32
43
  error: e.message,
@@ -39,34 +50,12 @@ module RcrewAI
39
50
 
40
51
  private
41
52
 
42
- # Build a stream sink that translates rcrewai events into ExecutionLog rows.
43
- # Note: the gem currently builds the sink but does not yet thread it down
44
- # to per-agent execution, so this is wired up for forward-compatibility.
45
- def stream_sink_for(execution)
46
- lambda do |event|
47
- case event
48
- when RCrewAI::Events::IterationStart
49
- execution.log("debug", "Iteration #{event.iteration_index} start", { agent: event.agent })
50
- when RCrewAI::Events::IterationEnd
51
- execution.log("debug", "Iteration end", { agent: event.agent, finish_reason: event.finish_reason })
52
- when RCrewAI::Events::ToolCallStart
53
- execution.log("info", "Tool call: #{event.tool}", { args: event.args, agent: event.agent })
54
- when RCrewAI::Events::ToolCallResult
55
- execution.log("info", "Tool result: #{event.tool}", { duration_ms: event.duration_ms, agent: event.agent })
56
- when RCrewAI::Events::ToolCallError
57
- execution.log("error", "Tool error: #{event.tool}", { error: event.error, agent: event.agent })
58
- when RCrewAI::Events::Usage
59
- execution.log("debug", "Usage", {
60
- prompt_tokens: event.prompt_tokens,
61
- completion_tokens: event.completion_tokens,
62
- total_tokens: event.total_tokens,
63
- cost_usd: event.cost_usd,
64
- agent: event.agent
65
- })
66
- when RCrewAI::Events::Error
67
- execution.log("error", "Crew error", { error: event.error, agent: event.agent })
68
- end
69
- end
53
+ # Translates rcrewai events into the span tree. Returns nil when
54
+ # observation is disabled so no sink is attached at all.
55
+ def collector_for(execution)
56
+ return nil unless RcrewAI::Rails.config.observation_enabled
57
+
58
+ RcrewAI::Rails::Observation::Collector.new(execution: execution)
70
59
  end
71
60
 
72
61
  def notify_completion(crew, execution, result)
@@ -5,6 +5,7 @@ module RcrewAI
5
5
 
6
6
  belongs_to :crew
7
7
  has_many :execution_logs, dependent: :destroy
8
+ has_many :spans, dependent: :destroy
8
9
 
9
10
  validates :status, inclusion: { in: %w[pending running completed failed cancelled] }
10
11
 
@@ -74,7 +75,12 @@ module RcrewAI
74
75
  %w[completed failed cancelled].include?(status)
75
76
  end
76
77
 
78
+ # Deprecated: superseded by the observation engine's span tree.
79
+ # Scheduled for removal one minor version after the engine ships.
77
80
  def log(level, message, details = {})
81
+ RcrewAI::Rails.deprecator_warn(
82
+ "Execution#log and ExecutionLog are deprecated; use the observation engine (Execution#spans)."
83
+ )
78
84
  execution_logs.create!(
79
85
  level: level,
80
86
  message: message,
@@ -0,0 +1,57 @@
1
+ module RcrewAI
2
+ module Rails
3
+ class Span < ApplicationRecord
4
+ self.table_name = "rcrewai_spans"
5
+
6
+ KINDS = %w[crew agent task llm_call tool_call].freeze
7
+ STATUSES = %w[running ok error].freeze
8
+
9
+ belongs_to :execution
10
+ belongs_to :parent, class_name: "RcrewAI::Rails::Span",
11
+ foreign_key: :parent_span_id, optional: true
12
+ has_many :children, class_name: "RcrewAI::Rails::Span",
13
+ foreign_key: :parent_span_id, dependent: :destroy
14
+ has_many :span_events, dependent: :destroy
15
+
16
+ validates :kind, inclusion: { in: KINDS }
17
+ validates :status, inclusion: { in: STATUSES }
18
+ validates :name, :trace_id, :started_at, :sequence, presence: true
19
+
20
+ scope :roots, -> { where(parent_span_id: nil).order(:sequence) }
21
+ scope :ordered, -> { order(:sequence) }
22
+ scope :errored, -> { where(status: "error") }
23
+ scope :running, -> { where(status: "running") }
24
+ scope :llm_calls, -> { where(kind: "llm_call") }
25
+ scope :tool_calls, -> { where(kind: "tool_call") }
26
+
27
+ def attributes_hash
28
+ raw = self[:attributes_json]
29
+ return {} if raw.blank?
30
+
31
+ JSON.parse(raw)
32
+ rescue JSON::ParserError
33
+ {}
34
+ end
35
+
36
+ def attributes_hash=(hash)
37
+ self[:attributes_json] = hash.nil? ? nil : JSON.generate(hash)
38
+ end
39
+
40
+ def finish!(status: "ok", ended_at: Time.current)
41
+ update!(
42
+ status: status,
43
+ ended_at: ended_at,
44
+ duration_ms: ((ended_at - started_at) * 1000).round
45
+ )
46
+ end
47
+
48
+ def running?
49
+ status == "running"
50
+ end
51
+
52
+ def errored?
53
+ status == "error"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ module RcrewAI
2
+ module Rails
3
+ class SpanEvent < ApplicationRecord
4
+ self.table_name = "rcrewai_span_events"
5
+
6
+ LEVELS = %w[debug info warn error].freeze
7
+
8
+ belongs_to :span
9
+
10
+ validates :level, inclusion: { in: LEVELS }
11
+ validates :name, presence: true
12
+
13
+ serialize :details, coder: JSON
14
+
15
+ scope :errors, -> { where(level: "error") }
16
+ scope :recent, -> { order(timestamp: :desc) }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ <%#
2
+ Renders one span and, recursively, its children.
3
+
4
+ `children` is the parent_span_id => [spans] map built once by the
5
+ controller. Children are read from that map, never via `span.children`,
6
+ which would issue a query per node and turn a deep trace into an N+1.
7
+
8
+ A malformed tree (a parent/child cycle, e.g. a bad backfill) would
9
+ otherwise recurse until the request dies, so recursion is bounded by
10
+ MAX_SPAN_DEPTH and by `seen`, which drops any span already on the path.
11
+ %>
12
+ <% depth = local_assigns.fetch(:depth, 0) %>
13
+ <% children = local_assigns.fetch(:children, {}) %>
14
+ <% seen = local_assigns.fetch(:seen, []) %>
15
+ <% max_depth = RcrewAI::Rails::ObservationsController::MAX_SPAN_DEPTH %>
16
+
17
+ <div class="span-row span-<%= span.status %> span-kind-<%= span.kind %>" id="span-<%= span.id %>">
18
+ <div class="span-line" style="padding-left: <%= depth * 20 %>px">
19
+ <span class="span-kind"><%= span.kind %></span>
20
+ <span class="span-name"><%= span.name %></span>
21
+ <span class="span-status status-<%= span.status %>"><%= span.status %></span>
22
+ <% if span.duration_ms %>
23
+ <span class="span-duration"><%= span.duration_ms %>ms</span>
24
+ <% end %>
25
+ <% if span.total_tokens.to_i.positive? %>
26
+ <span class="span-tokens"><%= span.total_tokens %> tokens</span>
27
+ <% end %>
28
+ <% if span.cost_usd.present? %>
29
+ <span class="span-cost">$<%= span.cost_usd %></span>
30
+ <% end %>
31
+ </div>
32
+
33
+ <% attrs = span.attributes_hash %>
34
+ <% if attrs.present? %>
35
+ <details class="span-attributes" style="margin-left: <%= (depth * 20) + 20 %>px">
36
+ <summary>Details</summary>
37
+ <pre class="code-block"><%= JSON.pretty_generate(attrs) %></pre>
38
+ </details>
39
+ <% end %>
40
+ </div>
41
+
42
+ <% if depth < max_depth %>
43
+ <% (children[span.id] || []).each do |child| %>
44
+ <% next if seen.include?(child.id) %>
45
+ <%= render "rcrewai/rails/observations/span",
46
+ span: child,
47
+ children: children,
48
+ depth: depth + 1,
49
+ seen: seen + [span.id] %>
50
+ <% end %>
51
+ <% end %>
@@ -0,0 +1,52 @@
1
+ <div class="page-header">
2
+ <h1>Cost &amp; Performance</h1>
3
+ </div>
4
+
5
+ <div class="metadata-grid">
6
+ <div class="meta-item">
7
+ <strong>Total Cost:</strong> $<%= @total_cost %>
8
+ </div>
9
+ <div class="meta-item">
10
+ <strong>Total Tokens:</strong> <%= @total_tokens %>
11
+ </div>
12
+ <div class="meta-item">
13
+ <strong>Executions:</strong> <%= @executions.size %>
14
+ </div>
15
+ </div>
16
+
17
+ <div class="section">
18
+ <h2>Recent Executions</h2>
19
+
20
+ <% if @executions.empty? %>
21
+ <p class="empty-state">No executions with recorded cost yet.</p>
22
+ <% else %>
23
+ <table class="table">
24
+ <thead>
25
+ <tr>
26
+ <th>Execution</th>
27
+ <th>Crew</th>
28
+ <th>Status</th>
29
+ <th>Spans</th>
30
+ <th>Errors</th>
31
+ <th>Tokens</th>
32
+ <th>Cost</th>
33
+ <th></th>
34
+ </tr>
35
+ </thead>
36
+ <tbody>
37
+ <% @executions.each do |execution| %>
38
+ <tr>
39
+ <td><%= link_to "##{execution.id}", execution_path(execution) %></td>
40
+ <td><%= execution.crew&.name %></td>
41
+ <td><span class="status-<%= execution.status %>"><%= execution.status.humanize %></span></td>
42
+ <td><%= execution.span_count.to_i %></td>
43
+ <td><%= execution.error_count.to_i %></td>
44
+ <td><%= execution.total_tokens.to_i %></td>
45
+ <td>$<%= execution.total_cost_usd %></td>
46
+ <td><%= link_to "Trace", execution_observation_path(execution), class: "btn btn-sm" %></td>
47
+ </tr>
48
+ <% end %>
49
+ </tbody>
50
+ </table>
51
+ <% end %>
52
+ </div>
@@ -0,0 +1,47 @@
1
+ <div class="page-header">
2
+ <h1>Trace for Execution #<%= @execution.id %></h1>
3
+ <div class="actions">
4
+ <%= link_to "Back to Execution", execution_path(@execution), class: "btn" %>
5
+ <%= link_to "Costs", observation_costs_path, class: "btn" %>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="execution-details">
10
+ <div class="status-banner status-<%= @execution.status %>">
11
+ <strong>Status:</strong> <%= @execution.status.humanize %>
12
+ </div>
13
+
14
+ <div class="metadata-grid">
15
+ <div class="meta-item">
16
+ <strong>Spans:</strong> <%= @execution.span_count.to_i %>
17
+ </div>
18
+ <div class="meta-item">
19
+ <strong>Errors:</strong> <%= @execution.error_count.to_i %>
20
+ </div>
21
+ <div class="meta-item">
22
+ <strong>Tokens:</strong> <%= @execution.total_tokens.to_i %>
23
+ </div>
24
+ <div class="meta-item">
25
+ <strong>Cost:</strong> <%= @execution.total_cost_usd.present? ? "$#{@execution.total_cost_usd}" : "N/A" %>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="section">
31
+ <h2>Waterfall</h2>
32
+
33
+ <div class="span-tree" id="execution-<%= @execution.id %>-spans">
34
+ <% if @roots.empty? %>
35
+ <p class="empty-state">No spans recorded for this execution.</p>
36
+ <% else %>
37
+ <% @roots.each do |span| %>
38
+ <%= render "rcrewai/rails/observations/span",
39
+ span: span, children: @children, depth: 0, seen: [] %>
40
+ <% end %>
41
+ <% end %>
42
+ </div>
43
+ </div>
44
+
45
+ <% if defined?(turbo_stream_from) && @execution.running? %>
46
+ <%= turbo_stream_from "rcrewai_execution_#{@execution.id}" %>
47
+ <% end %>
data/config/routes.rb CHANGED
@@ -16,6 +16,12 @@ RcrewAI::Rails::Engine.routes.draw do
16
16
  end
17
17
  end
18
18
 
19
+ resources :executions, only: [] do
20
+ resource :observation, only: [:show], controller: "observations"
21
+ end
22
+
23
+ get "observations/costs", to: "observations#costs", as: :observation_costs
24
+
19
25
  resources :agents do
20
26
  resources :tools
21
27
  end
@@ -0,0 +1,42 @@
1
+ class CreateRcrewaiSpans < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :rcrewai_spans do |t|
4
+ t.references :execution, null: false, foreign_key: { to_table: :rcrewai_executions }
5
+ t.bigint :parent_span_id
6
+ t.string :trace_id, null: false
7
+ t.string :kind, null: false
8
+ t.string :name, null: false
9
+ t.string :status, null: false, default: "running"
10
+ t.datetime :started_at, null: false
11
+ t.datetime :ended_at
12
+ t.integer :duration_ms
13
+ t.integer :prompt_tokens
14
+ t.integer :completion_tokens
15
+ t.integer :total_tokens
16
+ t.decimal :cost_usd, precision: 12, scale: 6
17
+ t.text :attributes_json
18
+ t.integer :sequence, null: false
19
+
20
+ t.timestamps
21
+ end
22
+
23
+ add_index :rcrewai_spans, :parent_span_id
24
+ add_index :rcrewai_spans, :trace_id
25
+ add_index :rcrewai_spans, :kind
26
+ add_index :rcrewai_spans, :status
27
+ add_index :rcrewai_spans, %i[execution_id sequence]
28
+
29
+ create_table :rcrewai_span_events do |t|
30
+ t.references :span, null: false, foreign_key: { to_table: :rcrewai_spans }
31
+ t.string :level, null: false, default: "info"
32
+ t.string :name, null: false
33
+ t.text :details
34
+ t.datetime :timestamp, null: false
35
+
36
+ t.timestamps
37
+ end
38
+
39
+ add_index :rcrewai_span_events, :level
40
+ add_index :rcrewai_span_events, :timestamp
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ class AddObservationRollupsToRcrewaiExecutions < ActiveRecord::Migration[7.0]
2
+ def change
3
+ add_column :rcrewai_executions, :total_cost_usd, :decimal, precision: 12, scale: 6
4
+ add_column :rcrewai_executions, :total_tokens, :integer
5
+ add_column :rcrewai_executions, :span_count, :integer, default: 0, null: false
6
+ add_column :rcrewai_executions, :error_count, :integer, default: 0, null: false
7
+ end
8
+ end