rails-hyperdrive-layered-rails 0.1.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 (62) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +30 -0
  3. data/README.md +89 -0
  4. data/UPSTREAM +4 -0
  5. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/SKILL.md.erb +315 -0
  6. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/authorization-to-policy.md +79 -0
  7. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/callbacks-to-service.md +72 -0
  8. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/complex-input-to-form-object.md +119 -0
  9. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/current-from-model.md +63 -0
  10. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/god-object-decomposition.md +81 -0
  11. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/implicit-to-explicit-state-machine.md +77 -0
  12. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/query-to-query-object.md +63 -0
  13. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/examples/view-logic-to-presenter.md +60 -0
  14. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/callbacks.md +114 -0
  15. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/concerns.md +92 -0
  16. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/helpers.md +66 -0
  17. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/jobs.md +71 -0
  18. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/layer-violations.md +176 -0
  19. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/service-objects.md +131 -0
  20. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/anti-patterns/testing.md +37 -0
  21. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/architecture-layers.md +204 -0
  22. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/extraction-signals.md +314 -0
  23. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/core/specification-test.md +229 -0
  24. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/action-policy.md +437 -0
  25. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-agent.md +275 -0
  26. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-delivery.md +221 -0
  27. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/active-job-performs.md +171 -0
  28. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/alba.md +257 -0
  29. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/anyway-config.md +212 -0
  30. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/rubanok.md +246 -0
  31. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/view-component.md +223 -0
  32. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/gems/workflow.md +305 -0
  33. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/collaborator-objects.md +209 -0
  34. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/concerns.md +381 -0
  35. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/filter-objects.md +222 -0
  36. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/form-objects.md +267 -0
  37. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/policy-objects.md +298 -0
  38. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/presenters.md +257 -0
  39. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/query-objects.md +208 -0
  40. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/repositories.md +382 -0
  41. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/serializers.md +256 -0
  42. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/service-objects.md +170 -0
  43. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/state-machines.md +368 -0
  44. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/patterns/value-objects.md +267 -0
  45. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/ai-integration.md +372 -0
  46. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/authorization.md +398 -0
  47. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/callbacks.md +336 -0
  48. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/configuration.md +383 -0
  49. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/current-attributes.md +307 -0
  50. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/instrumentation.md +386 -0
  51. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/notifications.md +373 -0
  52. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/references/topics/view-components.md +454 -0
  53. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-callbacks.md +255 -0
  54. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-gods.md +279 -0
  55. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze-services.md +1444 -0
  56. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/analyze.md +376 -0
  57. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/plan.md +249 -0
  58. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/review.md +473 -0
  59. data/lib/rails-hyperdrive-layered-rails/hyperdrive/skills/layered-rails/workflows/spec-test.md +282 -0
  60. data/lib/rails-hyperdrive-layered-rails/version.rb +3 -0
  61. data/lib/rails-hyperdrive-layered-rails.rb +7 -0
  62. metadata +106 -0
@@ -0,0 +1,437 @@
1
+ # Action Policy
2
+
3
+ Authorization framework for Ruby and Rails applications.
4
+
5
+ **GitHub**: https://github.com/palkan/action_policy
6
+ **Layer**: Application
7
+
8
+ ## Contents
9
+
10
+ - Installation
11
+ - Basic Usage
12
+ - Scoping
13
+ - Rule Aliases
14
+ - Minimal CRUD Rules
15
+ - Reason Tracking with `check?`
16
+ - Policy Composition
17
+ - Pre-Checks
18
+ - Authorization Context
19
+ - Error Handling
20
+ - Gotchas
21
+ - Testing
22
+
23
+ ## Installation
24
+
25
+ ```ruby
26
+ # Gemfile
27
+ gem "action_policy"
28
+
29
+ # Generate policy
30
+ rails generate action_policy:policy Post
31
+ ```
32
+
33
+ ## Basic Usage
34
+
35
+ ### Define Policy
36
+
37
+ ```ruby
38
+ class PostPolicy < ApplicationPolicy
39
+ def show?
40
+ true
41
+ end
42
+
43
+ def update?
44
+ owner? || user.admin?
45
+ end
46
+
47
+ def destroy?
48
+ user.admin?
49
+ end
50
+
51
+ private
52
+
53
+ def owner?
54
+ record.author_id == user.id
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Controller Integration
60
+
61
+ ```ruby
62
+ class PostsController < ApplicationController
63
+ def show
64
+ @post = Post.find(params[:id])
65
+ authorize! @post
66
+ end
67
+
68
+ def update
69
+ @post = Post.find(params[:id])
70
+ authorize! @post
71
+
72
+ @post.update!(post_params)
73
+ redirect_to @post
74
+ end
75
+ end
76
+ ```
77
+
78
+ ### View Integration
79
+
80
+ ```erb
81
+ <% if allowed_to?(:update?, @post) %>
82
+ <%= link_to "Edit", edit_post_path(@post) %>
83
+ <% end %>
84
+ ```
85
+
86
+ ## Scoping
87
+
88
+ Load only authorized records:
89
+
90
+ ```ruby
91
+ class PostsController < ApplicationController
92
+ def index
93
+ @posts = authorized_scope(Post.all)
94
+ end
95
+
96
+ def destroy
97
+ @post = authorized_scope(Post.all, as: :destroyable).find(params[:id])
98
+ @post.destroy!
99
+ end
100
+ end
101
+
102
+ class PostPolicy < ApplicationPolicy
103
+ # Default scope for index
104
+ relation_scope do |scope|
105
+ if user.admin?
106
+ scope.all
107
+ else
108
+ scope.published.or(scope.where(author: user))
109
+ end
110
+ end
111
+
112
+ # Named scope
113
+ relation_scope(:destroyable) do |scope|
114
+ user.admin? ? scope.all : scope.where(author: user)
115
+ end
116
+ end
117
+ ```
118
+
119
+ ## Rule Aliases
120
+
121
+ ```ruby
122
+ class PostPolicy < ApplicationPolicy
123
+ def manage?
124
+ owner? || user.admin?
125
+ end
126
+
127
+ # Alias multiple rules to manage?
128
+ alias_rule :create?, :update?, :destroy?, to: :manage?
129
+ end
130
+ ```
131
+
132
+ ## Minimal CRUD Rules
133
+
134
+ Keep policies small. Most policies need only two rules:
135
+
136
+ ```ruby
137
+ class BoardPolicy < ApplicationPolicy
138
+ def show?
139
+ # Visibility - who can see this resource
140
+ record.all_access? || has_access?
141
+ end
142
+
143
+ def manage?
144
+ # Management - who can modify this resource
145
+ board_admin?
146
+ end
147
+
148
+ # update?, edit?, destroy? automatically fall back to manage?
149
+ end
150
+ ```
151
+
152
+ Action Policy provides:
153
+ - **Default rule:** `manage?` as fallback for undefined rules
154
+ - **Default alias:** `new? -> create?`
155
+
156
+ ### When Custom Rules Make Sense
157
+
158
+ Custom rules are appropriate for:
159
+
160
+ **Domain operations** beyond CRUD:
161
+ ```ruby
162
+ class AccountPolicy < ApplicationPolicy
163
+ def transfer?
164
+ owner? && record.transferable?
165
+ end
166
+
167
+ def cancel?
168
+ owner? || admin?
169
+ end
170
+ end
171
+
172
+ class ReportPolicy < ApplicationPolicy
173
+ def show_sources?
174
+ allowed_to?(:show?) && user.researcher?
175
+ end
176
+ end
177
+ ```
178
+
179
+ **Parent policy groupings** for related resources:
180
+ ```ruby
181
+ class OrganizationPolicy < ApplicationPolicy
182
+ def manage_billing?
183
+ owner? || billing_admin?
184
+ end
185
+
186
+ def manage_members?
187
+ owner? || admin?
188
+ end
189
+ end
190
+
191
+ # Used across multiple controllers
192
+ authorize! @organization, to: :manage_billing?
193
+ ```
194
+
195
+ ### When to Consider New Resources
196
+
197
+ If a custom rule maps directly to a controller action with its own views and routes, consider a dedicated resource:
198
+
199
+ ```ruby
200
+ # If you have:
201
+ # POST /cards/:id/archive
202
+ # GET /cards/:id/archive (confirmation page)
203
+ # DELETE /cards/:id/archive (unarchive)
204
+
205
+ # Consider: Cards::ArchivesController with ArchivePolicy
206
+ # Instead of: CardPolicy#can_archive?, #can_unarchive?
207
+ ```
208
+
209
+ The goal is clarity, not strict adherence to patterns.
210
+
211
+ ## Reason Tracking with `check?`
212
+
213
+ Use `check?` instead of direct predicate calls to track failure reasons:
214
+
215
+ ```ruby
216
+ class AccountPolicy < ApplicationPolicy
217
+ def manage?
218
+ check?(:admin?) # Tracks "admin?" as failure reason
219
+ end
220
+
221
+ def destroy?
222
+ check?(:owner?) # Tracks "owner?" as failure reason
223
+ end
224
+ end
225
+ ```
226
+
227
+ This enables debugging authorization failures:
228
+ ```ruby
229
+ policy = AccountPolicy.new(user, account)
230
+ policy.apply(:manage?)
231
+ policy.result.reasons #=> { admin?: false }
232
+ ```
233
+
234
+ ## Policy Composition
235
+
236
+ Delegate to other policies for nested authorization:
237
+
238
+ ```ruby
239
+ class CommentPolicy < ApplicationPolicy
240
+ authorize :card, optional: true
241
+
242
+ def create?
243
+ allowed_to?(:show?, card) # Delegates to CardPolicy
244
+ end
245
+
246
+ def show?
247
+ allowed_to?(:show?, record.card) # Uses record's association
248
+ end
249
+
250
+ def manage?
251
+ allowed_to?(:show?) && check?(:creator?) # Combines both
252
+ end
253
+ end
254
+ ```
255
+
256
+ ## Pre-Checks
257
+
258
+ Run before every rule:
259
+
260
+ ```ruby
261
+ class ApplicationPolicy < ActionPolicy::Base
262
+ pre_check :allow_admins
263
+
264
+ private
265
+
266
+ def allow_admins
267
+ allow! if user.admin?
268
+ end
269
+ end
270
+ ```
271
+
272
+ ## Authorization Context
273
+
274
+ For `create?` and `index?` rules where `record` isn't available, use authorization contexts:
275
+
276
+ ```ruby
277
+ class WebhookPolicy < ApplicationPolicy
278
+ authorize :board, optional: true # optional: only needed for create?/index?
279
+
280
+ def index?
281
+ allowed_to?(:manage?, board) # Uses context
282
+ end
283
+
284
+ def create?
285
+ allowed_to?(:manage?, board) # Uses context
286
+ end
287
+
288
+ def show?
289
+ allowed_to?(:manage?, record.board) # Uses record
290
+ end
291
+ end
292
+ ```
293
+
294
+ Register contexts via controller concerns:
295
+
296
+ ```ruby
297
+ module BoardScoped
298
+ extend ActiveSupport::Concern
299
+
300
+ included do
301
+ before_action :set_board
302
+ authorize :board, through: -> { @board }
303
+ end
304
+
305
+ private
306
+
307
+ def set_board
308
+ @board = Current.account.boards.find_by!(slug: params[:board_id])
309
+ authorize! @board, to: :show?
310
+ end
311
+ end
312
+
313
+ class WebhooksController < ApplicationController
314
+ include BoardScoped
315
+
316
+ def index
317
+ authorize! # Uses WebhookPolicy with board context
318
+ @webhooks = @board.webhooks
319
+ end
320
+ end
321
+ ```
322
+
323
+ ## Error Handling
324
+
325
+ Return 404 for visibility failures (don't leak existence), 403 for permission failures:
326
+
327
+ ```ruby
328
+ module Authorization
329
+ extend ActiveSupport::Concern
330
+
331
+ included do
332
+ rescue_from ActionPolicy::Unauthorized, with: :handle_unauthorized
333
+ end
334
+
335
+ private
336
+
337
+ def handle_unauthorized(exception)
338
+ if exception.rule == :show?
339
+ raise ActiveRecord::RecordNotFound # 404
340
+ else
341
+ head :forbidden # 403
342
+ end
343
+ end
344
+ end
345
+ ```
346
+
347
+ ## Gotchas
348
+
349
+ ### View Context Syntax
350
+
351
+ Context must use `context:` hash:
352
+
353
+ ```ruby
354
+ # Wrong
355
+ allowed_to?(:create?, Webhook, board: @board)
356
+
357
+ # Correct
358
+ allowed_to?(:create?, Webhook, context: {board: @board})
359
+ ```
360
+
361
+ ### Optional Contexts
362
+
363
+ Mark context as `optional: true` when only needed for some rules:
364
+
365
+ ```ruby
366
+ authorize :board, optional: true # Required for create?, not show?
367
+ ```
368
+
369
+ ## Testing
370
+
371
+ ### Policy Unit Tests
372
+
373
+ ```ruby
374
+ # RSpec
375
+ RSpec.describe PostPolicy do
376
+ let(:user) { create(:user) }
377
+ let(:post) { create(:post) }
378
+
379
+ describe "#update?" do
380
+ it "allows owner" do
381
+ post = create(:post, author: user)
382
+ expect(described_class.new(user, post).update?).to be true
383
+ end
384
+
385
+ it "denies non-owner" do
386
+ expect(described_class.new(user, post).update?).to be false
387
+ end
388
+ end
389
+ end
390
+
391
+ # Minitest
392
+ class BoardPolicyTest < ActiveSupport::TestCase
393
+ test "board creator can manage" do
394
+ policy = BoardPolicy.new(@board, user: users(:david), account: @account)
395
+ assert policy.apply(:manage?)
396
+ end
397
+
398
+ test "create uses board context" do
399
+ # Pass context as named argument, no record
400
+ policy = WebhookPolicy.new(board: @board, user: users(:david), account: @account)
401
+ assert policy.apply(:create?)
402
+ end
403
+ end
404
+ ```
405
+
406
+ ### Controller Authorization Tests
407
+
408
+ ```ruby
409
+ # RSpec
410
+ RSpec.describe PostsController do
411
+ include ActionPolicy::TestHelper
412
+
413
+ it "authorizes update" do
414
+ expect {
415
+ put :update, params: { id: post.id, post: { title: "New" } }
416
+ }.to be_authorized_to(:update?, post)
417
+ end
418
+ end
419
+
420
+ # Minitest
421
+ class WebhooksControllerTest < ActionDispatch::IntegrationTest
422
+ include ActionPolicy::TestHelper
423
+
424
+ test "index authorizes" do
425
+ assert_authorized_to(:index?, Webhook) do
426
+ get board_webhooks_path(boards(:writebook))
427
+ end
428
+ end
429
+
430
+ test "show authorizes specific record" do
431
+ webhook = webhooks(:active)
432
+ assert_authorized_to(:show?, webhook) do
433
+ get board_webhook_path(webhook.board, webhook)
434
+ end
435
+ end
436
+ end
437
+ ```
@@ -0,0 +1,275 @@
1
+ # Active Agent
2
+
3
+ Framework for building AI agents in Rails applications.
4
+
5
+ **GitHub**: https://github.com/activeagents/activeagent
6
+ **Layer**: Infrastructure (LLM calls) / Application (orchestration)
7
+
8
+ ## Contents
9
+
10
+ - Installation
11
+ - Basic Usage
12
+ - Agents with Tools
13
+ - System Prompts
14
+ - Structured Output
15
+ - Service Integration
16
+ - Background Processing
17
+ - Configuration
18
+ - Testing
19
+ - Error Handling
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ # Gemfile
25
+ gem "activeagent"
26
+ ```
27
+
28
+ ## Basic Usage
29
+
30
+ ### Define Agent
31
+
32
+ ```ruby
33
+ # app/agents/application_agent.rb
34
+ class ApplicationAgent < ActiveAgent::Base
35
+ # Default provider configuration
36
+ end
37
+
38
+ # app/agents/summarizer_agent.rb
39
+ class SummarizerAgent < ApplicationAgent
40
+ def summarize(text, max_length: 200)
41
+ prompt <<~PROMPT
42
+ Summarize the following text in #{max_length} characters or less.
43
+ Maintain the key points and tone.
44
+
45
+ Text:
46
+ #{text}
47
+ PROMPT
48
+ end
49
+ end
50
+ ```
51
+
52
+ ### Use Agent
53
+
54
+ ```ruby
55
+ agent = SummarizerAgent.new
56
+ summary = agent.summarize(article.body, max_length: 150)
57
+ ```
58
+
59
+ ## Agents with Tools
60
+
61
+ ```ruby
62
+ class ResearchAgent < ApplicationAgent
63
+ tool :search_web,
64
+ description: "Search the web for information" do |query|
65
+ WebSearchService.search(query).map do |result|
66
+ { title: result.title, url: result.url, snippet: result.snippet }
67
+ end
68
+ end
69
+
70
+ tool :read_url,
71
+ description: "Read and extract content from a URL" do |url|
72
+ UrlReaderService.read(url)
73
+ end
74
+
75
+ tool :save_note,
76
+ description: "Save a research note" do |title, content|
77
+ ResearchNote.create!(title: title, content: content)
78
+ "Note saved: #{title}"
79
+ end
80
+
81
+ def research(topic)
82
+ prompt <<~PROMPT
83
+ Research the topic: #{topic}
84
+
85
+ Use the search_web tool to find relevant information.
86
+ Use read_url to get more details from promising results.
87
+ Use save_note to record important findings.
88
+
89
+ Provide a comprehensive summary of your research.
90
+ PROMPT
91
+ end
92
+ end
93
+ ```
94
+
95
+ ## System Prompts
96
+
97
+ ```ruby
98
+ class CustomerSupportAgent < ApplicationAgent
99
+ system_prompt <<~PROMPT
100
+ You are a helpful customer support agent for Acme Corp.
101
+ Be polite, professional, and helpful.
102
+ If you don't know something, say so honestly.
103
+ Never make up information about products or policies.
104
+ PROMPT
105
+
106
+ def respond(customer_message, context: {})
107
+ prompt <<~PROMPT
108
+ Customer: #{customer_message}
109
+
110
+ Context:
111
+ - Customer name: #{context[:customer_name]}
112
+ - Account type: #{context[:account_type]}
113
+ - Previous interactions: #{context[:interaction_count]}
114
+
115
+ Respond helpfully to the customer's message.
116
+ PROMPT
117
+ end
118
+ end
119
+ ```
120
+
121
+ ## Structured Output
122
+
123
+ ```ruby
124
+ class ClassifierAgent < ApplicationAgent
125
+ def classify(content)
126
+ response = prompt <<~PROMPT
127
+ Classify the following content into one of these categories:
128
+ - technology
129
+ - business
130
+ - entertainment
131
+ - sports
132
+ - science
133
+
134
+ Also provide a confidence score from 0 to 1.
135
+
136
+ Content: #{content}
137
+
138
+ Respond in JSON format:
139
+ {"category": "...", "confidence": 0.XX}
140
+ PROMPT
141
+
142
+ JSON.parse(response)
143
+ end
144
+ end
145
+ ```
146
+
147
+ ## Service Integration
148
+
149
+ ```ruby
150
+ class SummarizeArticle
151
+ def initialize(agent: SummarizerAgent.new)
152
+ @agent = agent
153
+ end
154
+
155
+ def call(article)
156
+ return article.summary if article.summary.present?
157
+
158
+ summary = agent.summarize(article.body)
159
+ article.update!(summary: summary, summarized_at: Time.current)
160
+ summary
161
+ rescue ActiveAgent::Error => e
162
+ Rails.logger.error("Summarization failed: #{e.message}")
163
+ article.body.truncate(200) # Fallback
164
+ end
165
+
166
+ private
167
+
168
+ attr_reader :agent
169
+ end
170
+ ```
171
+
172
+ ## Background Processing
173
+
174
+ ```ruby
175
+ class SummarizeArticleJob < ApplicationJob
176
+ retry_on ActiveAgent::RateLimitError,
177
+ wait: :polynomially_longer,
178
+ attempts: 5
179
+
180
+ discard_on ActiveAgent::InvalidRequestError
181
+
182
+ def perform(article_id)
183
+ article = Article.find(article_id)
184
+ SummarizeArticle.new.call(article)
185
+ end
186
+ end
187
+ ```
188
+
189
+ ## Configuration
190
+
191
+ ```ruby
192
+ # config/initializers/active_agent.rb
193
+ ActiveAgent.configure do |config|
194
+ config.default_provider = :anthropic
195
+ config.default_model = "claude-3-sonnet"
196
+
197
+ config.providers[:anthropic] = {
198
+ api_key: Rails.application.credentials.anthropic_api_key
199
+ }
200
+
201
+ config.providers[:openai] = {
202
+ api_key: Rails.application.credentials.openai_api_key
203
+ }
204
+ end
205
+ ```
206
+
207
+ ## Testing
208
+
209
+ ```ruby
210
+ RSpec.describe SummarizeArticle do
211
+ let(:article) { create(:article, body: "Long article content...") }
212
+
213
+ describe "#call" do
214
+ it "generates and saves summary" do
215
+ mock_agent = instance_double(SummarizerAgent)
216
+ allow(mock_agent).to receive(:summarize).and_return("Brief summary")
217
+
218
+ service = described_class.new(agent: mock_agent)
219
+ result = service.call(article)
220
+
221
+ expect(result).to eq("Brief summary")
222
+ expect(article.reload.summary).to eq("Brief summary")
223
+ end
224
+
225
+ it "handles API errors gracefully" do
226
+ mock_agent = instance_double(SummarizerAgent)
227
+ allow(mock_agent).to receive(:summarize)
228
+ .and_raise(ActiveAgent::RateLimitError)
229
+
230
+ service = described_class.new(agent: mock_agent)
231
+ result = service.call(article)
232
+
233
+ expect(result).to eq(article.body.truncate(200))
234
+ end
235
+ end
236
+ end
237
+
238
+ RSpec.describe ResearchAgent do
239
+ describe "#research" do
240
+ it "uses tools to gather information" do
241
+ agent = described_class.new
242
+
243
+ # Mock tool responses
244
+ allow(WebSearchService).to receive(:search)
245
+ .and_return([{ title: "Result", url: "http://...", snippet: "..." }])
246
+
247
+ result = agent.research("Ruby on Rails best practices")
248
+
249
+ expect(result).to be_present
250
+ end
251
+ end
252
+ end
253
+ ```
254
+
255
+ ## Error Handling
256
+
257
+ ```ruby
258
+ class AIService
259
+ def call(prompt)
260
+ agent.prompt(prompt)
261
+ rescue ActiveAgent::RateLimitError
262
+ # Retry after delay
263
+ sleep(60)
264
+ retry
265
+ rescue ActiveAgent::InvalidRequestError => e
266
+ # Log and return fallback
267
+ Rails.logger.error("Invalid AI request: #{e.message}")
268
+ nil
269
+ rescue ActiveAgent::AuthenticationError
270
+ # Critical - alert ops
271
+ Rails.logger.fatal("AI authentication failed")
272
+ raise
273
+ end
274
+ end
275
+ ```