rails_console_ai 0.29.0 → 0.30.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +48 -0
  4. data/app/controllers/rails_console_ai/agent_versions_controller.rb +36 -0
  5. data/app/controllers/rails_console_ai/agents_controller.rb +199 -0
  6. data/app/controllers/rails_console_ai/application_controller.rb +5 -0
  7. data/app/controllers/rails_console_ai/memories_controller.rb +159 -0
  8. data/app/controllers/rails_console_ai/memory_versions_controller.rb +33 -0
  9. data/app/controllers/rails_console_ai/skill_versions_controller.rb +35 -0
  10. data/app/controllers/rails_console_ai/skills_controller.rb +200 -0
  11. data/app/helpers/rails_console_ai/diff_helper.rb +114 -0
  12. data/app/models/rails_console_ai/agent.rb +175 -0
  13. data/app/models/rails_console_ai/agent_version.rb +46 -0
  14. data/app/models/rails_console_ai/memory.rb +98 -0
  15. data/app/models/rails_console_ai/memory_version.rb +46 -0
  16. data/app/models/rails_console_ai/session.rb +1 -1
  17. data/app/models/rails_console_ai/skill.rb +198 -0
  18. data/app/models/rails_console_ai/skill_version.rb +54 -0
  19. data/app/views/layouts/rails_console_ai/application.html.erb +78 -1
  20. data/app/views/rails_console_ai/agent_versions/index.html.erb +28 -0
  21. data/app/views/rails_console_ai/agent_versions/show.html.erb +25 -0
  22. data/app/views/rails_console_ai/agents/_form.html.erb +65 -0
  23. data/app/views/rails_console_ai/agents/diff.html.erb +19 -0
  24. data/app/views/rails_console_ai/agents/edit.html.erb +7 -0
  25. data/app/views/rails_console_ai/agents/index.html.erb +80 -0
  26. data/app/views/rails_console_ai/agents/new.html.erb +24 -0
  27. data/app/views/rails_console_ai/agents/show.html.erb +108 -0
  28. data/app/views/rails_console_ai/memories/_form.html.erb +36 -0
  29. data/app/views/rails_console_ai/memories/diff.html.erb +19 -0
  30. data/app/views/rails_console_ai/memories/edit.html.erb +7 -0
  31. data/app/views/rails_console_ai/memories/index.html.erb +67 -0
  32. data/app/views/rails_console_ai/memories/new.html.erb +23 -0
  33. data/app/views/rails_console_ai/memories/show.html.erb +65 -0
  34. data/app/views/rails_console_ai/memory_versions/index.html.erb +26 -0
  35. data/app/views/rails_console_ai/memory_versions/show.html.erb +21 -0
  36. data/app/views/rails_console_ai/skill_versions/index.html.erb +28 -0
  37. data/app/views/rails_console_ai/skill_versions/show.html.erb +23 -0
  38. data/app/views/rails_console_ai/skills/_form.html.erb +65 -0
  39. data/app/views/rails_console_ai/skills/diff.html.erb +22 -0
  40. data/app/views/rails_console_ai/skills/edit.html.erb +7 -0
  41. data/app/views/rails_console_ai/skills/index.html.erb +79 -0
  42. data/app/views/rails_console_ai/skills/new.html.erb +25 -0
  43. data/app/views/rails_console_ai/skills/show.html.erb +94 -0
  44. data/config/routes.rb +42 -0
  45. data/lib/rails_console_ai/agent_loader.rb +131 -43
  46. data/lib/rails_console_ai/agent_runner.rb +158 -0
  47. data/lib/rails_console_ai/channel/api.rb +139 -0
  48. data/lib/rails_console_ai/conversation_engine.rb +19 -13
  49. data/lib/rails_console_ai/session_logger.rb +6 -0
  50. data/lib/rails_console_ai/skill_loader.rb +119 -27
  51. data/lib/rails_console_ai/storage/database_storage.rb +201 -0
  52. data/lib/rails_console_ai/tools/memory_tools.rb +102 -32
  53. data/lib/rails_console_ai/tools/registry.rb +99 -8
  54. data/lib/rails_console_ai/version.rb +1 -1
  55. data/lib/rails_console_ai.rb +256 -0
  56. data/lib/tasks/rails_console_ai.rake +7 -0
  57. metadata +55 -1
@@ -6,7 +6,7 @@ module RailsConsoleAi
6
6
  attr_reader :definitions, :last_sub_agent_usage
7
7
 
8
8
  # Tools that should never be cached (side effects or user interaction)
9
- NO_CACHE = %w[ask_user save_memory delete_memory recall_memory execute_code execute_plan activate_skill save_skill delete_skill delegate_task explore_output].freeze
9
+ NO_CACHE = %w[ask_user save_memory delete_memory recall_memory execute_code execute_plan activate_skill save_skill delete_skill save_agent delete_agent delegate_task explore_output].freeze
10
10
 
11
11
  def initialize(executor: nil, mode: :default, channel: nil, allowed_tools: nil)
12
12
  @executor = executor
@@ -254,6 +254,7 @@ module RailsConsoleAi
254
254
 
255
255
  register_memory_tools
256
256
  register_skill_tools
257
+ register_agent_tools
257
258
  register_execute_plan
258
259
  register_delegate_task
259
260
  end
@@ -381,9 +382,20 @@ module RailsConsoleAi
381
382
  loader = AgentLoader.new
382
383
  agent_config = loader.find_agent(agent_name)
383
384
  unless agent_config
384
- available = loader.load_all_agents.map { |a| a['name'] }
385
+ # Distinguish "doesn't exist" from "exists but isn't approved yet".
386
+ proposed = loader.find_any_agent(agent_name)
387
+ if proposed && proposed['source'] == :db && proposed['status'] != 'approved'
388
+ return "Agent \"#{agent_name}\" exists but is awaiting human approval and cannot be invoked yet. " \
389
+ "Ask the user to approve it in the web UI at /rails_console_ai/agents."
390
+ end
391
+ available = loader.load_activatable_agents.map { |a| a['name'] }
385
392
  return "Agent not found: \"#{agent_name}\". Available agents: #{available.join(', ')}"
386
393
  end
394
+
395
+ # Usage tracking — DB-backed agents only.
396
+ if agent_config['source'] == :db && agent_config['id']
397
+ RailsConsoleAi::Agent.record_use!(agent_config['id'])
398
+ end
387
399
  end
388
400
 
389
401
  sub = SubAgent.new(
@@ -406,18 +418,26 @@ module RailsConsoleAi
406
418
 
407
419
  register(
408
420
  name: 'save_memory',
409
- description: 'Save a fact or pattern you learned about this codebase for future sessions. Use after discovering how something works (e.g. sharding, auth, custom business logic).',
421
+ description: 'Save a fact or pattern you learned about this codebase for future sessions. Use after discovering how something works (e.g. sharding, auth, custom business logic). Defaults to the versioned DB store; pass target: "file" to write to the on-disk .rails_console_ai/memories directory instead.',
410
422
  parameters: {
411
423
  'type' => 'object',
412
424
  'properties' => {
413
425
  'name' => { 'type' => 'string', 'description' => 'Short name for this memory (e.g. "Sharding architecture")' },
414
426
  'description' => { 'type' => 'string', 'description' => 'Detailed description of what you learned' },
415
- 'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags (e.g. ["database", "sharding"])' }
427
+ 'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags (e.g. ["database", "sharding"])' },
428
+ 'target' => { 'type' => 'string', 'enum' => ['db', 'file'], 'description' => 'Where to store this memory. "db" (default) is versioned and editable via the web UI; "file" writes a Markdown file under .rails_console_ai/memories/.' },
429
+ 'change_note' => { 'type' => 'string', 'description' => 'Optional one-line note describing this edit (DB store only).' }
416
430
  },
417
431
  'required' => ['name', 'description']
418
432
  },
419
433
  handler: ->(args) {
420
- memory.save_memory(name: args['name'], description: args['description'], tags: args['tags'] || [])
434
+ memory.save_memory(
435
+ name: args['name'],
436
+ description: args['description'],
437
+ tags: args['tags'] || [],
438
+ target: (args['target'] || 'db').to_sym,
439
+ change_note: args['change_note']
440
+ )
421
441
  }
422
442
  )
423
443
 
@@ -480,19 +500,30 @@ module RailsConsoleAi
480
500
  handler: ->(args) {
481
501
  skill = loader.find_skill(args['name'])
482
502
  unless skill
503
+ # Distinguish "doesn't exist" from "exists but isn't approved yet".
504
+ proposed = loader.find_any_skill(args['name'])
505
+ if proposed && proposed['source'] == :db && proposed['status'] != 'approved'
506
+ return "Skill \"#{args['name']}\" exists but is awaiting human approval and cannot be activated yet. " \
507
+ "Ask the user to approve it in the web UI at /rails_console_ai/skills."
508
+ end
483
509
  return "Skill not found: \"#{args['name']}\". Use the skills listed in the system prompt."
484
510
  end
485
511
 
486
512
  bypass_methods = Array(skill['bypass_guards_for_methods'])
487
513
  @executor.activate_skill_bypasses(bypass_methods) unless bypass_methods.empty?
488
514
 
515
+ # Usage tracking — DB-backed skills only (file skills have no row to update).
516
+ if skill['source'] == :db && skill['id']
517
+ RailsConsoleAi::Skill.record_use!(skill['id'])
518
+ end
519
+
489
520
  skill['body']
490
521
  }
491
522
  )
492
523
 
493
524
  register(
494
525
  name: 'save_skill',
495
- description: 'Create or update a skill — a reusable procedure for a specific operation. Use when the user asks you to create a skill, recipe, or runbook. Skills differ from memories: a skill is a step-by-step procedure to follow, while a memory is a fact or pattern you learned.',
526
+ description: 'Create or update a skill — a reusable procedure for a specific operation. Use when the user asks you to create a skill, recipe, or runbook. Skills differ from memories: a skill is a step-by-step procedure to follow, while a memory is a fact or pattern you learned. Defaults to the versioned DB store; pass target: "file" to write to the on-disk .rails_console_ai/skills directory instead. IMPORTANT: skills saved to the DB start in "proposed" state and must be approved by a human in the web UI before you can activate them. Edits to an approved skill also revert it to proposed. Tell the user to visit /rails_console_ai/skills to approve.',
496
527
  parameters: {
497
528
  'type' => 'object',
498
529
  'properties' => {
@@ -500,7 +531,9 @@ module RailsConsoleAi
500
531
  'description' => { 'type' => 'string', 'description' => 'One-line description of when to use this skill' },
501
532
  'body' => { 'type' => 'string', 'description' => 'The full skill recipe in markdown. Include: ## When to use, ## Recipe (numbered steps with code blocks), ## Notes (optional).' },
502
533
  'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags for categorization (e.g. ["booking-page", "admin"])' },
503
- 'bypass_guards_for_methods' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Methods that should bypass safety guards when this skill is active (e.g. ["BookingPage#save!", "BookingPage#ensure_subdomain_set!"])' }
534
+ 'bypass_guards_for_methods' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Methods that should bypass safety guards when this skill is active (e.g. ["BookingPage#save!", "BookingPage#ensure_subdomain_set!"])' },
535
+ 'target' => { 'type' => 'string', 'enum' => ['db', 'file'], 'description' => 'Where to store this skill. "db" (default) is versioned and editable via the web UI; "file" writes a Markdown file under .rails_console_ai/skills/.' },
536
+ 'change_note' => { 'type' => 'string', 'description' => 'Optional one-line note describing this edit (DB store only).' }
504
537
  },
505
538
  'required' => %w[name description body]
506
539
  },
@@ -510,7 +543,9 @@ module RailsConsoleAi
510
543
  description: args['description'],
511
544
  body: args['body'],
512
545
  tags: args['tags'] || [],
513
- bypass_guards_for_methods: args['bypass_guards_for_methods'] || []
546
+ bypass_guards_for_methods: args['bypass_guards_for_methods'] || [],
547
+ target: (args['target'] || 'db').to_sym,
548
+ change_note: args['change_note']
514
549
  )
515
550
  }
516
551
  )
@@ -529,6 +564,62 @@ module RailsConsoleAi
529
564
  )
530
565
  end
531
566
 
567
+ def register_agent_tools
568
+ return unless @executor
569
+
570
+ require 'rails_console_ai/agent_loader'
571
+ loader = RailsConsoleAi::AgentLoader.new
572
+
573
+ register(
574
+ name: 'save_agent',
575
+ description: 'Create or update a sub-agent definition — a reusable specialist that the main assistant can invoke via delegate_task. ' \
576
+ 'Use when the user asks you to create a new agent, recipe-agent, or sub-task specialist. ' \
577
+ 'Agents differ from skills: an agent runs in its own sub-conversation with a constrained tool set, while a skill is a procedure followed inline. ' \
578
+ 'Defaults to the versioned DB store; pass target: "file" to write to the on-disk .rails_console_ai/agents directory instead. ' \
579
+ 'IMPORTANT: agents saved to the DB start in "proposed" state and must be approved by a human in the web UI before delegate_task can invoke them. ' \
580
+ 'Edits to an approved agent also revert it to proposed. Tell the user to visit /rails_console_ai/agents to approve.',
581
+ parameters: {
582
+ 'type' => 'object',
583
+ 'properties' => {
584
+ 'name' => { 'type' => 'string', 'description' => 'Agent name (e.g. "Investigate billing"), shown in the Agents list and used as the delegate_task `agent` parameter' },
585
+ 'description' => { 'type' => 'string', 'description' => 'One-line description of what this agent specializes in' },
586
+ 'body' => { 'type' => 'string', 'description' => 'The agent\'s system instructions in markdown. Include: persona, strategy, rules, expected output format.' },
587
+ 'max_rounds' => { 'type' => 'integer', 'description' => 'Optional: maximum tool-loop iterations for the sub-agent (defaults to global config). Use a smaller number for tightly-scoped agents.' },
588
+ 'model' => { 'type' => 'string', 'description' => 'Optional: model override for this agent (e.g. "claude-haiku-4" for cheap fast agents)' },
589
+ 'tools' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional: whitelist of tool names the sub-agent is allowed to call. Omit to allow the default set.' },
590
+ 'target' => { 'type' => 'string', 'enum' => ['db', 'file'], 'description' => 'Where to store this agent. "db" (default) is versioned and editable via the web UI; "file" writes a Markdown file under .rails_console_ai/agents/.' },
591
+ 'change_note' => { 'type' => 'string', 'description' => 'Optional one-line note describing this edit (DB store only).' }
592
+ },
593
+ 'required' => %w[name description body]
594
+ },
595
+ handler: ->(args) {
596
+ loader.save_agent(
597
+ name: args['name'],
598
+ description: args['description'],
599
+ body: args['body'],
600
+ max_rounds: args['max_rounds'],
601
+ model: args['model'],
602
+ tools: args['tools'] || [],
603
+ target: (args['target'] || 'db').to_sym,
604
+ change_note: args['change_note']
605
+ )
606
+ }
607
+ )
608
+
609
+ register(
610
+ name: 'delete_agent',
611
+ description: 'Delete a sub-agent by name. Built-in (gem-shipped) agents cannot be deleted; this tool will tell you that and suggest creating a same-named override instead.',
612
+ parameters: {
613
+ 'type' => 'object',
614
+ 'properties' => {
615
+ 'name' => { 'type' => 'string', 'description' => 'The agent name to delete' }
616
+ },
617
+ 'required' => ['name']
618
+ },
619
+ handler: ->(args) { loader.delete_agent(name: args['name']) }
620
+ )
621
+ end
622
+
532
623
  def register_execute_plan
533
624
  return unless @executor
534
625
 
@@ -1,3 +1,3 @@
1
1
  module RailsConsoleAi
2
- VERSION = '0.29.0'.freeze
2
+ VERSION = '0.30.0'.freeze
3
3
  end
@@ -55,6 +55,39 @@ module RailsConsoleAi
55
55
  @current_user = name
56
56
  end
57
57
 
58
+ # Enqueue an agent run. Returns the Integer session id immediately;
59
+ # the actual work is picked up by `rake rails_console_ai:agents`.
60
+ def run_agent(query, name: nil, user_name: nil)
61
+ require 'rails_console_ai/session_logger'
62
+ id = SessionLogger.log(
63
+ query: query,
64
+ conversation: [],
65
+ mode: 'agent_api',
66
+ name: name,
67
+ user_name: user_name,
68
+ status: 'queued',
69
+ executed: false
70
+ )
71
+ raise 'Failed to enqueue agent run (session logging disabled or table missing)' unless id
72
+ id
73
+ end
74
+
75
+ # Returns the current status string for an enqueued agent run, or nil
76
+ # if the session id is not found. Status is one of:
77
+ # 'queued' | 'running' | 'ready' | 'failed'.
78
+ def check_agent(session_id)
79
+ Session.where(id: session_id).pluck(:status).first
80
+ end
81
+
82
+ # Returns a hash describing an agent run:
83
+ # { status:, result:, error: }
84
+ # All three keys are nil when the session id is not found.
85
+ def get_agent_response(session_id)
86
+ row = Session.where(id: session_id).select(:status, :result, :error_message).first
87
+ return { status: nil, result: nil, error: nil } unless row
88
+ { status: row.status, result: row.result, error: row.error_message }
89
+ end
90
+
58
91
  def status
59
92
  c = configuration
60
93
  key = c.resolved_api_key
@@ -123,11 +156,183 @@ module RailsConsoleAi
123
156
  $stdout.puts "\e[32mRailsConsoleAi: created #{table} table.\e[0m"
124
157
  end
125
158
 
159
+ setup_skills_tables!(conn)
160
+ setup_memories_tables!(conn)
161
+ setup_agents_tables!(conn)
162
+
126
163
  migrate!
127
164
  rescue => e
128
165
  $stderr.puts "\e[31mRailsConsoleAi setup failed: #{e.class}: #{e.message}\e[0m"
129
166
  end
130
167
 
168
+ def setup_skills_tables!(conn)
169
+ skills_table = 'rails_console_ai_skills'
170
+ versions_table = 'rails_console_ai_skill_versions'
171
+
172
+ unless conn.table_exists?(skills_table)
173
+ conn.create_table(skills_table) do |t|
174
+ t.string :name, limit: 255, null: false
175
+ t.text :description
176
+ t.text :body
177
+ t.text :tags
178
+ t.text :bypass_guards_for_methods
179
+ t.string :status, limit: 20, default: 'proposed', null: false
180
+ t.string :approved_by, limit: 255
181
+ t.datetime :approved_at
182
+ t.integer :use_count, default: 0, null: false
183
+ t.datetime :last_used_at
184
+ t.datetime :created_at, null: false
185
+ t.datetime :updated_at, null: false
186
+ end
187
+ conn.add_index(skills_table, :name, unique: true)
188
+ conn.add_index(skills_table, :status)
189
+ $stdout.puts "\e[32mRailsConsoleAi: created #{skills_table} table.\e[0m"
190
+ else
191
+ # Idempotent column-add probes for existing installs.
192
+ unless conn.column_exists?(skills_table, :status)
193
+ conn.add_column(skills_table, :status, :string, limit: 20, default: 'proposed', null: false)
194
+ conn.add_index(skills_table, :status) unless conn.index_exists?(skills_table, :status)
195
+ end
196
+ unless conn.column_exists?(skills_table, :approved_by)
197
+ conn.add_column(skills_table, :approved_by, :string, limit: 255)
198
+ end
199
+ unless conn.column_exists?(skills_table, :approved_at)
200
+ conn.add_column(skills_table, :approved_at, :datetime)
201
+ end
202
+ unless conn.column_exists?(skills_table, :use_count)
203
+ conn.add_column(skills_table, :use_count, :integer, default: 0, null: false)
204
+ end
205
+ unless conn.column_exists?(skills_table, :last_used_at)
206
+ conn.add_column(skills_table, :last_used_at, :datetime)
207
+ end
208
+ end
209
+
210
+ unless conn.table_exists?(versions_table)
211
+ conn.create_table(versions_table) do |t|
212
+ t.integer :skill_id
213
+ t.string :name, limit: 255
214
+ t.text :description
215
+ t.text :body
216
+ t.text :tags
217
+ t.text :bypass_guards_for_methods
218
+ t.string :status, limit: 20
219
+ t.string :edited_by, limit: 255
220
+ t.text :change_note
221
+ t.datetime :created_at, null: false
222
+ end
223
+ conn.add_index(versions_table, :skill_id)
224
+ conn.add_index(versions_table, :created_at)
225
+ $stdout.puts "\e[32mRailsConsoleAi: created #{versions_table} table.\e[0m"
226
+ else
227
+ unless conn.column_exists?(versions_table, :status)
228
+ conn.add_column(versions_table, :status, :string, limit: 20)
229
+ end
230
+ end
231
+ end
232
+
233
+ def setup_memories_tables!(conn)
234
+ memories_table = 'rails_console_ai_memories'
235
+ versions_table = 'rails_console_ai_memory_versions'
236
+
237
+ unless conn.table_exists?(memories_table)
238
+ conn.create_table(memories_table) do |t|
239
+ t.string :name, limit: 255, null: false
240
+ t.text :description
241
+ t.text :tags
242
+ t.integer :use_count, default: 0, null: false
243
+ t.datetime :last_used_at
244
+ t.datetime :created_at, null: false
245
+ t.datetime :updated_at, null: false
246
+ end
247
+ conn.add_index(memories_table, :name, unique: true)
248
+ $stdout.puts "\e[32mRailsConsoleAi: created #{memories_table} table.\e[0m"
249
+ else
250
+ unless conn.column_exists?(memories_table, :use_count)
251
+ conn.add_column(memories_table, :use_count, :integer, default: 0, null: false)
252
+ end
253
+ unless conn.column_exists?(memories_table, :last_used_at)
254
+ conn.add_column(memories_table, :last_used_at, :datetime)
255
+ end
256
+ end
257
+
258
+ unless conn.table_exists?(versions_table)
259
+ conn.create_table(versions_table) do |t|
260
+ t.integer :memory_id
261
+ t.string :name, limit: 255
262
+ t.text :description
263
+ t.text :tags
264
+ t.string :edited_by, limit: 255
265
+ t.text :change_note
266
+ t.datetime :created_at, null: false
267
+ end
268
+ conn.add_index(versions_table, :memory_id)
269
+ conn.add_index(versions_table, :created_at)
270
+ $stdout.puts "\e[32mRailsConsoleAi: created #{versions_table} table.\e[0m"
271
+ end
272
+ end
273
+
274
+ def setup_agents_tables!(conn)
275
+ agents_table = 'rails_console_ai_agents'
276
+ versions_table = 'rails_console_ai_agent_versions'
277
+
278
+ unless conn.table_exists?(agents_table)
279
+ conn.create_table(agents_table) do |t|
280
+ t.string :name, limit: 255, null: false
281
+ t.text :description
282
+ t.text :body
283
+ t.integer :max_rounds
284
+ t.string :model, limit: 100
285
+ t.text :tools
286
+ t.string :status, limit: 20, default: 'proposed', null: false
287
+ t.string :approved_by, limit: 255
288
+ t.datetime :approved_at
289
+ t.integer :use_count, default: 0, null: false
290
+ t.datetime :last_used_at
291
+ t.datetime :created_at, null: false
292
+ t.datetime :updated_at, null: false
293
+ end
294
+ conn.add_index(agents_table, :name, unique: true)
295
+ conn.add_index(agents_table, :status)
296
+ $stdout.puts "\e[32mRailsConsoleAi: created #{agents_table} table.\e[0m"
297
+ else
298
+ unless conn.column_exists?(agents_table, :status)
299
+ conn.add_column(agents_table, :status, :string, limit: 20, default: 'proposed', null: false)
300
+ conn.add_index(agents_table, :status) unless conn.index_exists?(agents_table, :status)
301
+ end
302
+ unless conn.column_exists?(agents_table, :approved_by)
303
+ conn.add_column(agents_table, :approved_by, :string, limit: 255)
304
+ end
305
+ unless conn.column_exists?(agents_table, :approved_at)
306
+ conn.add_column(agents_table, :approved_at, :datetime)
307
+ end
308
+ unless conn.column_exists?(agents_table, :use_count)
309
+ conn.add_column(agents_table, :use_count, :integer, default: 0, null: false)
310
+ end
311
+ unless conn.column_exists?(agents_table, :last_used_at)
312
+ conn.add_column(agents_table, :last_used_at, :datetime)
313
+ end
314
+ end
315
+
316
+ unless conn.table_exists?(versions_table)
317
+ conn.create_table(versions_table) do |t|
318
+ t.integer :agent_id
319
+ t.string :name, limit: 255
320
+ t.text :description
321
+ t.text :body
322
+ t.integer :max_rounds
323
+ t.string :model, limit: 100
324
+ t.text :tools
325
+ t.string :status, limit: 20
326
+ t.string :edited_by, limit: 255
327
+ t.text :change_note
328
+ t.datetime :created_at, null: false
329
+ end
330
+ conn.add_index(versions_table, :agent_id)
331
+ conn.add_index(versions_table, :created_at)
332
+ $stdout.puts "\e[32mRailsConsoleAi: created #{versions_table} table.\e[0m"
333
+ end
334
+ end
335
+
131
336
  def migrate!
132
337
  conn = session_connection
133
338
  table = 'rails_console_ai_sessions'
@@ -156,6 +361,50 @@ module RailsConsoleAi
156
361
  migrations << 'slack_channel_name'
157
362
  end
158
363
 
364
+ unless conn.column_exists?(table, :status)
365
+ conn.add_column(table, :status, :string, limit: 20)
366
+ migrations << 'status'
367
+ end
368
+
369
+ unless conn.column_exists?(table, :result)
370
+ conn.add_column(table, :result, :text)
371
+ migrations << 'result'
372
+ end
373
+
374
+ unless conn.column_exists?(table, :error_message)
375
+ conn.add_column(table, :error_message, :text)
376
+ migrations << 'error_message'
377
+ end
378
+
379
+ unless conn.index_exists?(table, [:mode, :status], name: 'idx_rca_sessions_mode_status')
380
+ conn.add_index(table, [:mode, :status], name: 'idx_rca_sessions_mode_status')
381
+ migrations << 'idx_rca_sessions_mode_status'
382
+ end
383
+
384
+ # Bring skills/memories/agents tables fully up to date. Each setup_* method is
385
+ # internally idempotent (guards both `create_table` and every `add_column` /
386
+ # `add_index`), so running it on an existing install adds any missing columns
387
+ # (e.g. `status`, `approved_by`, `approved_at`) and indexes without disturbing
388
+ # data. Note: we always call these — the previous version skipped them when
389
+ # the base table already existed, which meant column probes never ran on
390
+ # upgrade and methods like Skill#status hit NameError. See:
391
+ # https://github.com/cortfr/rails_console_ai/issues (whichever issue you file)
392
+ pre_columns = {
393
+ skills: table_columns(conn, 'rails_console_ai_skills'),
394
+ memories: table_columns(conn, 'rails_console_ai_memories'),
395
+ agents: table_columns(conn, 'rails_console_ai_agents')
396
+ }
397
+
398
+ setup_skills_tables!(conn)
399
+ setup_memories_tables!(conn)
400
+ setup_agents_tables!(conn)
401
+
402
+ [[:skills, 'rails_console_ai_skills'], [:memories, 'rails_console_ai_memories'], [:agents, 'rails_console_ai_agents']].each do |key, name|
403
+ post = table_columns(conn, name)
404
+ added = post - pre_columns[key]
405
+ migrations.concat(added.map { |c| "#{name}.#{c}" }) unless added.empty?
406
+ end
407
+
159
408
  if migrations.empty?
160
409
  $stdout.puts "\e[32mRailsConsoleAi: #{table} is up to date.\e[0m"
161
410
  else
@@ -214,6 +463,13 @@ module RailsConsoleAi
214
463
  ActiveRecord::Base.connection
215
464
  end
216
465
  end
466
+
467
+ def table_columns(conn, table_name)
468
+ return [] unless conn.table_exists?(table_name)
469
+ conn.columns(table_name).map { |c| c.name }
470
+ rescue
471
+ []
472
+ end
217
473
  end
218
474
  end
219
475
 
@@ -4,4 +4,11 @@ namespace :rails_console_ai do
4
4
  require 'rails_console_ai/slack_bot'
5
5
  RailsConsoleAi::SlackBot.new.start
6
6
  end
7
+
8
+ desc "Run the RailsConsoleAi agent runner (polls DB for queued agent runs)"
9
+ task agents: :environment do
10
+ require 'rails_console_ai/agent_runner'
11
+ concurrency = Integer(ENV['AGENT_CONCURRENCY'] || RailsConsoleAi::AgentRunner::DEFAULT_CONCURRENCY)
12
+ RailsConsoleAi::AgentRunner.new(concurrency: concurrency).start
13
+ end
7
14
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_console_ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.29.0
4
+ version: 0.30.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cortfr
@@ -79,6 +79,20 @@ dependencies:
79
79
  - - ">="
80
80
  - !ruby/object:Gem::Version
81
81
  version: '12.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: sqlite3
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '1.4'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '1.4'
82
96
  description: An LLM-powered agent for your Rails console. Ask questions in natural
83
97
  language, get executable Ruby code.
84
98
  email: cortfr@gmail.com
@@ -89,20 +103,59 @@ files:
89
103
  - CHANGELOG.md
90
104
  - LICENSE
91
105
  - README.md
106
+ - app/controllers/rails_console_ai/agent_versions_controller.rb
107
+ - app/controllers/rails_console_ai/agents_controller.rb
92
108
  - app/controllers/rails_console_ai/application_controller.rb
109
+ - app/controllers/rails_console_ai/memories_controller.rb
110
+ - app/controllers/rails_console_ai/memory_versions_controller.rb
93
111
  - app/controllers/rails_console_ai/sessions_controller.rb
112
+ - app/controllers/rails_console_ai/skill_versions_controller.rb
113
+ - app/controllers/rails_console_ai/skills_controller.rb
114
+ - app/helpers/rails_console_ai/diff_helper.rb
94
115
  - app/helpers/rails_console_ai/sessions_helper.rb
116
+ - app/models/rails_console_ai/agent.rb
117
+ - app/models/rails_console_ai/agent_version.rb
118
+ - app/models/rails_console_ai/memory.rb
119
+ - app/models/rails_console_ai/memory_version.rb
95
120
  - app/models/rails_console_ai/session.rb
121
+ - app/models/rails_console_ai/skill.rb
122
+ - app/models/rails_console_ai/skill_version.rb
96
123
  - app/views/layouts/rails_console_ai/application.html.erb
124
+ - app/views/rails_console_ai/agent_versions/index.html.erb
125
+ - app/views/rails_console_ai/agent_versions/show.html.erb
126
+ - app/views/rails_console_ai/agents/_form.html.erb
127
+ - app/views/rails_console_ai/agents/diff.html.erb
128
+ - app/views/rails_console_ai/agents/edit.html.erb
129
+ - app/views/rails_console_ai/agents/index.html.erb
130
+ - app/views/rails_console_ai/agents/new.html.erb
131
+ - app/views/rails_console_ai/agents/show.html.erb
132
+ - app/views/rails_console_ai/memories/_form.html.erb
133
+ - app/views/rails_console_ai/memories/diff.html.erb
134
+ - app/views/rails_console_ai/memories/edit.html.erb
135
+ - app/views/rails_console_ai/memories/index.html.erb
136
+ - app/views/rails_console_ai/memories/new.html.erb
137
+ - app/views/rails_console_ai/memories/show.html.erb
138
+ - app/views/rails_console_ai/memory_versions/index.html.erb
139
+ - app/views/rails_console_ai/memory_versions/show.html.erb
97
140
  - app/views/rails_console_ai/sessions/index.html.erb
98
141
  - app/views/rails_console_ai/sessions/show.html.erb
142
+ - app/views/rails_console_ai/skill_versions/index.html.erb
143
+ - app/views/rails_console_ai/skill_versions/show.html.erb
144
+ - app/views/rails_console_ai/skills/_form.html.erb
145
+ - app/views/rails_console_ai/skills/diff.html.erb
146
+ - app/views/rails_console_ai/skills/edit.html.erb
147
+ - app/views/rails_console_ai/skills/index.html.erb
148
+ - app/views/rails_console_ai/skills/new.html.erb
149
+ - app/views/rails_console_ai/skills/show.html.erb
99
150
  - config/routes.rb
100
151
  - lib/generators/rails_console_ai/install_generator.rb
101
152
  - lib/generators/rails_console_ai/templates/initializer.rb
102
153
  - lib/rails_console_ai.rb
103
154
  - lib/rails_console_ai/agent_loader.rb
155
+ - lib/rails_console_ai/agent_runner.rb
104
156
  - lib/rails_console_ai/agents/explore-data.md
105
157
  - lib/rails_console_ai/agents/investigate-code.md
158
+ - lib/rails_console_ai/channel/api.rb
106
159
  - lib/rails_console_ai/channel/base.rb
107
160
  - lib/rails_console_ai/channel/console.rb
108
161
  - lib/rails_console_ai/channel/slack.rb
@@ -126,6 +179,7 @@ files:
126
179
  - lib/rails_console_ai/skill_loader.rb
127
180
  - lib/rails_console_ai/slack_bot.rb
128
181
  - lib/rails_console_ai/storage/base.rb
182
+ - lib/rails_console_ai/storage/database_storage.rb
129
183
  - lib/rails_console_ai/storage/file_storage.rb
130
184
  - lib/rails_console_ai/sub_agent.rb
131
185
  - lib/rails_console_ai/tools/code_tools.rb