rails_console_ai 0.29.0 → 0.31.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 +46 -0
  3. data/README.md +65 -0
  4. data/app/controllers/rails_console_ai/agent_versions_controller.rb +29 -0
  5. data/app/controllers/rails_console_ai/agents_controller.rb +176 -0
  6. data/app/controllers/rails_console_ai/application_controller.rb +5 -0
  7. data/app/controllers/rails_console_ai/memories_controller.rb +136 -0
  8. data/app/controllers/rails_console_ai/memory_versions_controller.rb +29 -0
  9. data/app/controllers/rails_console_ai/skill_versions_controller.rb +29 -0
  10. data/app/controllers/rails_console_ai/skills_controller.rb +171 -0
  11. data/app/helpers/rails_console_ai/diff_helper.rb +114 -0
  12. data/app/models/rails_console_ai/agent.rb +143 -0
  13. data/app/models/rails_console_ai/agent_version.rb +34 -0
  14. data/app/models/rails_console_ai/memory.rb +103 -0
  15. data/app/models/rails_console_ai/memory_version.rb +31 -0
  16. data/app/models/rails_console_ai/session.rb +1 -1
  17. data/app/models/rails_console_ai/skill.rb +148 -0
  18. data/app/models/rails_console_ai/skill_version.rb +33 -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 +40 -0
  23. data/app/views/rails_console_ai/agents/diff.html.erb +15 -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 +8 -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 +29 -0
  29. data/app/views/rails_console_ai/memories/diff.html.erb +15 -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 +8 -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 +43 -0
  39. data/app/views/rails_console_ai/skills/diff.html.erb +15 -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 +8 -0
  43. data/app/views/rails_console_ai/skills/show.html.erb +94 -0
  44. data/config/routes.rb +39 -0
  45. data/lib/rails_console_ai/agent_loader.rb +139 -43
  46. data/lib/rails_console_ai/agent_runner.rb +209 -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 +10 -0
  50. data/lib/rails_console_ai/skill_loader.rb +130 -29
  51. data/lib/rails_console_ai/storage/database_storage.rb +195 -0
  52. data/lib/rails_console_ai/tools/memory_tools.rb +110 -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 +240 -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.31.0'.freeze
3
3
  end
@@ -55,6 +55,49 @@ 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
+ #
61
+ # use_thinking_model: run on the configured thinking-tier model
62
+ # max_wall_clock_seconds: hard kill the run after N seconds (nil = no cap)
63
+ def run_agent(query, name: nil, user_name: nil,
64
+ use_thinking_model: false,
65
+ max_wall_clock_seconds: 600)
66
+ require 'rails_console_ai/session_logger'
67
+ options = {
68
+ 'use_thinking_model' => !!use_thinking_model,
69
+ 'max_wall_clock_seconds' => max_wall_clock_seconds
70
+ }
71
+ id = SessionLogger.log(
72
+ query: query,
73
+ conversation: [],
74
+ mode: 'agent_api',
75
+ name: name,
76
+ user_name: user_name,
77
+ status: 'queued',
78
+ executed: false,
79
+ options: options
80
+ )
81
+ raise 'Failed to enqueue agent run (session logging disabled or table missing)' unless id
82
+ id
83
+ end
84
+
85
+ # Returns the current status string for an enqueued agent run, or nil
86
+ # if the session id is not found. Status is one of:
87
+ # 'queued' | 'running' | 'ready' | 'failed'.
88
+ def check_agent(session_id)
89
+ Session.where(id: session_id).pluck(:status).first
90
+ end
91
+
92
+ # Returns a hash describing an agent run:
93
+ # { status:, result:, error: }
94
+ # All three keys are nil when the session id is not found.
95
+ def get_agent_response(session_id)
96
+ row = Session.where(id: session_id).select(:status, :result, :error_message).first
97
+ return { status: nil, result: nil, error: nil } unless row
98
+ { status: row.status, result: row.result, error: row.error_message }
99
+ end
100
+
58
101
  def status
59
102
  c = configuration
60
103
  key = c.resolved_api_key
@@ -112,6 +155,7 @@ module RailsConsoleAi
112
155
  t.string :slack_thread_ts, limit: 255
113
156
  t.string :slack_channel_name, limit: 255
114
157
  t.integer :duration_ms
158
+ t.text :options
115
159
  t.datetime :created_at, null: false
116
160
  end
117
161
 
@@ -123,11 +167,151 @@ module RailsConsoleAi
123
167
  $stdout.puts "\e[32mRailsConsoleAi: created #{table} table.\e[0m"
124
168
  end
125
169
 
170
+ setup_skills_tables!(conn)
171
+ setup_memories_tables!(conn)
172
+ setup_agents_tables!(conn)
173
+
126
174
  migrate!
127
175
  rescue => e
128
176
  $stderr.puts "\e[31mRailsConsoleAi setup failed: #{e.class}: #{e.message}\e[0m"
129
177
  end
130
178
 
179
+ def setup_skills_tables!(conn)
180
+ skills_table = 'rails_console_ai_skills'
181
+ versions_table = 'rails_console_ai_skill_versions'
182
+
183
+ # Old shape had per-field columns (body, tags, bypass_guards_for_methods,
184
+ # description). New shape stores the raw .md in `content`. Pre-production,
185
+ # so we drop and recreate when the old shape is detected.
186
+ if conn.table_exists?(skills_table) && !conn.column_exists?(skills_table, :content)
187
+ conn.drop_table(skills_table)
188
+ $stdout.puts "\e[33mRailsConsoleAi: dropped legacy #{skills_table} (replaced with single-content schema).\e[0m"
189
+ end
190
+ if conn.table_exists?(versions_table) && !conn.column_exists?(versions_table, :content)
191
+ conn.drop_table(versions_table)
192
+ $stdout.puts "\e[33mRailsConsoleAi: dropped legacy #{versions_table}.\e[0m"
193
+ end
194
+
195
+ unless conn.table_exists?(skills_table)
196
+ conn.create_table(skills_table) do |t|
197
+ t.string :name, limit: 255, null: false
198
+ t.text :content, null: false
199
+ t.string :status, limit: 20, default: 'proposed', null: false
200
+ t.string :approved_by, limit: 255
201
+ t.datetime :approved_at
202
+ t.integer :use_count, default: 0, null: false
203
+ t.datetime :last_used_at
204
+ t.datetime :created_at, null: false
205
+ t.datetime :updated_at, null: false
206
+ end
207
+ conn.add_index(skills_table, :name, unique: true)
208
+ conn.add_index(skills_table, :status)
209
+ $stdout.puts "\e[32mRailsConsoleAi: created #{skills_table} table.\e[0m"
210
+ end
211
+
212
+ unless conn.table_exists?(versions_table)
213
+ conn.create_table(versions_table) do |t|
214
+ t.integer :skill_id
215
+ t.string :name, limit: 255
216
+ t.text :content
217
+ t.string :status, limit: 20
218
+ t.string :edited_by, limit: 255
219
+ t.text :change_note
220
+ t.datetime :created_at, null: false
221
+ end
222
+ conn.add_index(versions_table, :skill_id)
223
+ conn.add_index(versions_table, :created_at)
224
+ $stdout.puts "\e[32mRailsConsoleAi: created #{versions_table} table.\e[0m"
225
+ end
226
+ end
227
+
228
+ def setup_memories_tables!(conn)
229
+ memories_table = 'rails_console_ai_memories'
230
+ versions_table = 'rails_console_ai_memory_versions'
231
+
232
+ if conn.table_exists?(memories_table) && !conn.column_exists?(memories_table, :content)
233
+ conn.drop_table(memories_table)
234
+ $stdout.puts "\e[33mRailsConsoleAi: dropped legacy #{memories_table}.\e[0m"
235
+ end
236
+ if conn.table_exists?(versions_table) && !conn.column_exists?(versions_table, :content)
237
+ conn.drop_table(versions_table)
238
+ $stdout.puts "\e[33mRailsConsoleAi: dropped legacy #{versions_table}.\e[0m"
239
+ end
240
+
241
+ unless conn.table_exists?(memories_table)
242
+ conn.create_table(memories_table) do |t|
243
+ t.string :name, limit: 255, null: false
244
+ t.text :content, null: false
245
+ t.integer :use_count, default: 0, null: false
246
+ t.datetime :last_used_at
247
+ t.datetime :created_at, null: false
248
+ t.datetime :updated_at, null: false
249
+ end
250
+ conn.add_index(memories_table, :name, unique: true)
251
+ $stdout.puts "\e[32mRailsConsoleAi: created #{memories_table} table.\e[0m"
252
+ end
253
+
254
+ unless conn.table_exists?(versions_table)
255
+ conn.create_table(versions_table) do |t|
256
+ t.integer :memory_id
257
+ t.string :name, limit: 255
258
+ t.text :content
259
+ t.string :edited_by, limit: 255
260
+ t.text :change_note
261
+ t.datetime :created_at, null: false
262
+ end
263
+ conn.add_index(versions_table, :memory_id)
264
+ conn.add_index(versions_table, :created_at)
265
+ $stdout.puts "\e[32mRailsConsoleAi: created #{versions_table} table.\e[0m"
266
+ end
267
+ end
268
+
269
+ def setup_agents_tables!(conn)
270
+ agents_table = 'rails_console_ai_agents'
271
+ versions_table = 'rails_console_ai_agent_versions'
272
+
273
+ if conn.table_exists?(agents_table) && !conn.column_exists?(agents_table, :content)
274
+ conn.drop_table(agents_table)
275
+ $stdout.puts "\e[33mRailsConsoleAi: dropped legacy #{agents_table}.\e[0m"
276
+ end
277
+ if conn.table_exists?(versions_table) && !conn.column_exists?(versions_table, :content)
278
+ conn.drop_table(versions_table)
279
+ $stdout.puts "\e[33mRailsConsoleAi: dropped legacy #{versions_table}.\e[0m"
280
+ end
281
+
282
+ unless conn.table_exists?(agents_table)
283
+ conn.create_table(agents_table) do |t|
284
+ t.string :name, limit: 255, null: false
285
+ t.text :content, null: false
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
+ end
298
+
299
+ unless conn.table_exists?(versions_table)
300
+ conn.create_table(versions_table) do |t|
301
+ t.integer :agent_id
302
+ t.string :name, limit: 255
303
+ t.text :content
304
+ t.string :status, limit: 20
305
+ t.string :edited_by, limit: 255
306
+ t.text :change_note
307
+ t.datetime :created_at, null: false
308
+ end
309
+ conn.add_index(versions_table, :agent_id)
310
+ conn.add_index(versions_table, :created_at)
311
+ $stdout.puts "\e[32mRailsConsoleAi: created #{versions_table} table.\e[0m"
312
+ end
313
+ end
314
+
131
315
  def migrate!
132
316
  conn = session_connection
133
317
  table = 'rails_console_ai_sessions'
@@ -156,6 +340,55 @@ module RailsConsoleAi
156
340
  migrations << 'slack_channel_name'
157
341
  end
158
342
 
343
+ unless conn.column_exists?(table, :status)
344
+ conn.add_column(table, :status, :string, limit: 20)
345
+ migrations << 'status'
346
+ end
347
+
348
+ unless conn.column_exists?(table, :result)
349
+ conn.add_column(table, :result, :text)
350
+ migrations << 'result'
351
+ end
352
+
353
+ unless conn.column_exists?(table, :error_message)
354
+ conn.add_column(table, :error_message, :text)
355
+ migrations << 'error_message'
356
+ end
357
+
358
+ unless conn.column_exists?(table, :options)
359
+ conn.add_column(table, :options, :text)
360
+ migrations << 'options'
361
+ end
362
+
363
+ unless conn.index_exists?(table, [:mode, :status], name: 'idx_rca_sessions_mode_status')
364
+ conn.add_index(table, [:mode, :status], name: 'idx_rca_sessions_mode_status')
365
+ migrations << 'idx_rca_sessions_mode_status'
366
+ end
367
+
368
+ # Bring skills/memories/agents tables fully up to date. Each setup_* method is
369
+ # internally idempotent (guards both `create_table` and every `add_column` /
370
+ # `add_index`), so running it on an existing install adds any missing columns
371
+ # (e.g. `status`, `approved_by`, `approved_at`) and indexes without disturbing
372
+ # data. Note: we always call these — the previous version skipped them when
373
+ # the base table already existed, which meant column probes never ran on
374
+ # upgrade and methods like Skill#status hit NameError. See:
375
+ # https://github.com/cortfr/rails_console_ai/issues (whichever issue you file)
376
+ pre_columns = {
377
+ skills: table_columns(conn, 'rails_console_ai_skills'),
378
+ memories: table_columns(conn, 'rails_console_ai_memories'),
379
+ agents: table_columns(conn, 'rails_console_ai_agents')
380
+ }
381
+
382
+ setup_skills_tables!(conn)
383
+ setup_memories_tables!(conn)
384
+ setup_agents_tables!(conn)
385
+
386
+ [[:skills, 'rails_console_ai_skills'], [:memories, 'rails_console_ai_memories'], [:agents, 'rails_console_ai_agents']].each do |key, name|
387
+ post = table_columns(conn, name)
388
+ added = post - pre_columns[key]
389
+ migrations.concat(added.map { |c| "#{name}.#{c}" }) unless added.empty?
390
+ end
391
+
159
392
  if migrations.empty?
160
393
  $stdout.puts "\e[32mRailsConsoleAi: #{table} is up to date.\e[0m"
161
394
  else
@@ -214,6 +447,13 @@ module RailsConsoleAi
214
447
  ActiveRecord::Base.connection
215
448
  end
216
449
  end
450
+
451
+ def table_columns(conn, table_name)
452
+ return [] unless conn.table_exists?(table_name)
453
+ conn.columns(table_name).map { |c| c.name }
454
+ rescue
455
+ []
456
+ end
217
457
  end
218
458
  end
219
459
 
@@ -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.31.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