rails_console_ai 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -26
- data/README.md +54 -10
- data/app/controllers/rails_console_ai/agent_versions_controller.rb +1 -1
- data/app/controllers/rails_console_ai/agents_controller.rb +1 -1
- data/app/controllers/rails_console_ai/memories_controller.rb +22 -2
- data/app/controllers/rails_console_ai/memory_versions_controller.rb +1 -1
- data/app/controllers/rails_console_ai/sessions_controller.rb +1 -1
- data/app/controllers/rails_console_ai/skill_versions_controller.rb +1 -1
- data/app/controllers/rails_console_ai/skills_controller.rb +1 -1
- data/app/models/rails_console_ai/memory.rb +40 -1
- data/app/views/rails_console_ai/memories/index.html.erb +13 -1
- data/app/views/rails_console_ai/memories/show.html.erb +23 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rails_console_ai/templates/initializer.rb +5 -5
- data/lib/rails_console_ai/agent_runner.rb +12 -1
- data/lib/rails_console_ai/channel/console.rb +1 -1
- data/lib/rails_console_ai/configuration.rb +76 -33
- data/lib/rails_console_ai/conversation_engine.rb +74 -6
- data/lib/rails_console_ai/executor.rb +30 -1
- data/lib/rails_console_ai/safety_guards.rb +36 -0
- data/lib/rails_console_ai/slack_bot.rb +2 -2
- data/lib/rails_console_ai/sub_agent.rb +1 -1
- data/lib/rails_console_ai/tools/memory_tools.rb +29 -6
- data/lib/rails_console_ai/tools/registry.rb +23 -7
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +38 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8af25f720aae1fe703b947bdc860dcace9a13e2e1db61d9d698634fa46838e59
|
|
4
|
+
data.tar.gz: efc3b4c00ea66d941e5fb03c61543398fed5ffdde700c44c080ee5e8c8e09ad6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b8de788b6391a303815cd160fb57ce4300c33dfd3f66645ce31f9bb256de0922b7ce8d26fd05c23dc8802c80da6201ffb9935c89cfd82b94ebde609b1631733
|
|
7
|
+
data.tar.gz: 5c73d678909c5e950cff5191b506a72570c2eb50bd2d6f01602bfe63ec2fade56c94b88d91f6d487803b073abd7b3565c9b92c355f90f7e32be82e465b72b7f9
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.33.0]
|
|
6
|
+
|
|
7
|
+
- Support Claude Opus 5 and make it the default model for the Anthropic and Bedrock providers
|
|
8
|
+
- Add runaway-loop circuit breakers to the tool loop — break when the same error signature repeats (warn at 3 occurrences, stop at 5) or when a token budget is exceeded (nudge at 500K input tokens, force a final answer at 1M), with a tailored wrap-up prompt for each break reason
|
|
9
|
+
- Fail fast on known environment errors: a built-in hint recognizes decryption key misconfiguration (`bad decrypt` / `ActiveRecord::Encryption`) and tells the model not to retry, and apps can register their own hints via `config.error_hints`
|
|
10
|
+
|
|
11
|
+
## [0.32.0]
|
|
12
|
+
|
|
13
|
+
- Switch default models to Claude Sonnet 5 and Opus 4.8, and rework pricing to match models by family so dated snapshots and Bedrock IDs are covered
|
|
14
|
+
- Add an abort mechanism for background sub-agents
|
|
15
|
+
- Require human approval before AI-proposed memory changes take effect
|
|
16
|
+
- Keep `explore_output` focused on exploring a previous command's output instead of spinning off into database reads
|
|
17
|
+
- Fix loading `ApplicationController` under the classic autoloader
|
|
18
|
+
|
|
5
19
|
## [0.31.0]
|
|
6
20
|
|
|
7
21
|
- Simplify DB-backed skills, sub-agents, and memories to a single content field per record, replacing the separate frontmatter/body columns and streamlining the models, controllers, and forms
|
|
@@ -22,32 +36,6 @@ All notable changes to this project will be documented in this file.
|
|
|
22
36
|
- Harden `Skill` / `Agent` model accessors to return safe defaults when newly added columns are not yet present
|
|
23
37
|
- Fix built-in agent `.md` files with UTF-8 characters failing to load under US-ASCII locales
|
|
24
38
|
|
|
25
|
-
## [Unreleased]
|
|
26
|
-
|
|
27
|
-
- New skill / memory / agent pages now have a "Paste a .md file" textarea at the top. Paste the contents of a Markdown file with YAML frontmatter (same format used by `.rails_console_ai/{skills,memories,agents}/*.md` and the gem's built-in agents), click "Parse pasted content ↓", and the form fields below are prefilled from the parse. You still click Create to actually save, so the normal proposed-status + version-row flow applies — and you can tweak the parsed content before saving. Useful for moving an existing on-disk record into the versioned DB store
|
|
28
|
-
- `RailsConsoleAi::SkillLoader.parse`, `RailsConsoleAi::AgentLoader.parse`, and `RailsConsoleAi::Tools::MemoryTools.parse` are now public class methods. They return a parsed frontmatter+body hash, or `nil` on malformed input
|
|
29
|
-
|
|
30
|
-
- Usage tracking for DB-backed skills, memories, and agents. New `use_count` and `last_used_at` columns are bumped atomically (one SQL `UPDATE … SET use_count = use_count + 1, last_used_at = NOW()`, no callbacks, no `updated_at` change) when:
|
|
31
|
-
- `activate_skill` resolves a DB skill
|
|
32
|
-
- `recall_memory` resolves a DB memory, or `recall_memories` returns a DB memory in its result set
|
|
33
|
-
- `delegate_task` resolves a DB sub-agent
|
|
34
|
-
File / built-in records have no DB row so they're not tracked (the web UI shows `—` for them). System-prompt summary inclusion does **not** count — counters only move when the AI actively invokes/loads the record. Surfaced in the index tables (with a "Sort: most used" toggle) and on each show page (Times activated / Times recalled / Times invoked, plus Last used)
|
|
35
|
-
- Bugfix: `RailsConsoleAi.migrate!` now always re-runs the `setup_*_tables!` methods so column-add probes execute on upgrades. Previously, when the base table already existed, the outer `unless table_exists?` guard skipped column probes entirely, leaving new columns un-added and causing `NameError: undefined local variable or method 'status'` from `Skill#proposed?` on host apps that hadn't fully migrated
|
|
36
|
-
- Bugfix: `Skill` / `Agent` model accessors for `status` / `approved_by` / `approved_at` / `use_count` / `last_used_at` are now defensive — they return safe defaults via `has_attribute?` rather than raising NameError when the column hasn't been added yet. The `status` inclusion validation is also gated on the column being present
|
|
37
|
-
|
|
38
|
-
- Sub-agents can now be stored in the database, versioned, and approved through the web UI — same workflow as skills. DB-backed agents start as `proposed` and are invisible to `delegate_task` until a human clicks Approve at `/rails_console_ai/agents`; editing an approved agent reverts it to proposed. Built-in (gem-shipped) and file-based agents are pre-approved and continue working unchanged
|
|
39
|
-
- New AI tools `save_agent` and `delete_agent` — the AI can now draft a sub-agent definition (lands as proposed, awaits human approval) and request deletion. `delegate_task` now emits a specific "awaiting human approval" error when the AI references a proposed DB agent
|
|
40
|
-
- New web UI section at `/rails_console_ai/agents` with three-source listing (DB / FILE / BUILTIN badges) plus the full skills-style CRUD + version history + diff + restore + approve. Built-in agents are read-only with a "Create DB override" link that prefills the new-agent form
|
|
41
|
-
- `ai_db_setup` / `ai_db_migrate` now also create `rails_console_ai_agents` and `rails_console_ai_agent_versions` tables idempotently
|
|
42
|
-
- Fix: built-in agent .md files containing UTF-8 (em-dashes, smart quotes) now load correctly. `safe_load_builtin_agents` was using `File.read`'s locale-default encoding, which silently swallowed `Encoding::CompatibilityError` on US-ASCII locales
|
|
43
|
-
|
|
44
|
-
- Skills and memories can now be stored in the database (in addition to the existing on-disk `.rails_console_ai/skills` and `.rails_console_ai/memories` files). DB-backed records are versioned — every save creates a `SkillVersion` / `MemoryVersion` row with `edited_by` and an optional change note
|
|
45
|
-
- DB-backed skills start in a **proposed** state and cannot be activated by the AI until a human approves them in the web UI. Editing an approved skill reverts it to proposed. File-backed skills are unaffected (already git-tracked, considered pre-approved). Memories are not gated
|
|
46
|
-
- `SkillLoader#load_all_skills` and `MemoryTools#load_all_memories` now return the union of DB and file records (DB wins on name collision). `SkillLoader#find_skill` / `skill_summaries` filter out proposed skills so the AI never sees them
|
|
47
|
-
- `save_skill` / `save_memory` AI tools accept an optional `target` parameter (`"db"` default, `"file"` to write to disk) plus `change_note`. When the AI saves a DB skill, the tool response tells it the skill is awaiting human approval
|
|
48
|
-
- New web UI sections at `/rails_console_ai/skills` and `/rails_console_ai/memories` provide list, view, create, edit, delete, version history, side-by-side diff, and restore. Skills also have an Approve button and PROPOSED / APPROVED badges. File-sourced records are surfaced but read-only in the UI
|
|
49
|
-
- `ai_db_setup` / `ai_db_migrate` now create the new `rails_console_ai_skills`, `rails_console_ai_skill_versions`, `rails_console_ai_memories`, and `rails_console_ai_memory_versions` tables idempotently, plus the `status` / `approved_by` / `approved_at` columns on skills
|
|
50
|
-
|
|
51
39
|
## [0.29.0]
|
|
52
40
|
|
|
53
41
|
- Allow steering Slack conversations mid-run by sending follow-up messages that are folded in as user guidance at the next tool-loop boundary
|
data/README.md
CHANGED
|
@@ -291,7 +291,7 @@ RailsConsoleAi.configure do |config|
|
|
|
291
291
|
end
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
-
Default model: `claude-sonnet-
|
|
294
|
+
Default model: `claude-sonnet-5`. Thinking model: `claude-opus-5`. Prompt caching is enabled automatically.
|
|
295
295
|
|
|
296
296
|
### OpenAI
|
|
297
297
|
|
|
@@ -317,8 +317,8 @@ gem 'aws-sdk-bedrockruntime'
|
|
|
317
317
|
RailsConsoleAi.configure do |config|
|
|
318
318
|
config.provider = :bedrock
|
|
319
319
|
config.bedrock_region = 'us-east-1'
|
|
320
|
-
# config.model = 'us.anthropic.claude-sonnet-
|
|
321
|
-
# config.thinking_model = 'us.anthropic.claude-opus-
|
|
320
|
+
# config.model = 'us.anthropic.claude-sonnet-5' # default
|
|
321
|
+
# config.thinking_model = 'us.anthropic.claude-opus-5' # default
|
|
322
322
|
end
|
|
323
323
|
```
|
|
324
324
|
|
|
@@ -365,25 +365,25 @@ Before adopting a new Claude model, smoke-test it against the Anthropic or Bedro
|
|
|
365
365
|
|
|
366
366
|
```bash
|
|
367
367
|
# Anthropic — provider inferred from the `claude-` prefix
|
|
368
|
-
ANTHROPIC_API_KEY=sk-ant-... bin/smoke_model.rb --model claude-opus-4-
|
|
368
|
+
ANTHROPIC_API_KEY=sk-ant-... bin/smoke_model.rb --model claude-opus-4-8
|
|
369
369
|
|
|
370
370
|
# Bedrock — provider inferred from the regional `us.anthropic.` prefix.
|
|
371
371
|
# Requires the aws-sdk-bedrockruntime gem and AWS credentials in the environment.
|
|
372
|
-
bin/smoke_model.rb --model us.anthropic.claude-opus-4-
|
|
372
|
+
bin/smoke_model.rb --model us.anthropic.claude-opus-4-8
|
|
373
373
|
|
|
374
374
|
# Bedrock in another region
|
|
375
|
-
bin/smoke_model.rb --model eu.anthropic.claude-opus-4-
|
|
375
|
+
bin/smoke_model.rb --model eu.anthropic.claude-opus-4-8 --region eu-west-1
|
|
376
376
|
|
|
377
377
|
# Subset of checks, e.g. when iterating on cache behavior
|
|
378
|
-
bin/smoke_model.rb --model claude-sonnet-
|
|
378
|
+
bin/smoke_model.rb --model claude-sonnet-5 --checks cache
|
|
379
379
|
|
|
380
380
|
# Force a provider when the model ID is ambiguous
|
|
381
|
-
bin/smoke_model.rb --provider anthropic --model claude-opus-4-
|
|
381
|
+
bin/smoke_model.rb --provider anthropic --model claude-opus-4-8
|
|
382
382
|
```
|
|
383
383
|
|
|
384
384
|
`DEBUG=1` enables the providers' raw request/response logging.
|
|
385
385
|
|
|
386
|
-
|
|
386
|
+
Pricing, default max tokens, and parameter support (e.g. which families reject `temperature`) are keyed by model family in `Configuration::MODEL_FAMILIES` (`lib/rails_console_ai/configuration.rb`). Family matching is by substring, so one entry covers bare IDs, dated snapshots, and Bedrock inference profiles (`us.` / `global.` prefixes). When adopting a new model family, add an entry there and smoke-test it.
|
|
387
387
|
|
|
388
388
|
## Configuration
|
|
389
389
|
|
|
@@ -396,9 +396,48 @@ RailsConsoleAi.configure do |config|
|
|
|
396
396
|
config.timeout = 30 # HTTP timeout in seconds
|
|
397
397
|
config.max_tool_rounds = 200 # safety cap on tool-use loops
|
|
398
398
|
config.code_search_paths = %w[app] # directories for list_files / search_code
|
|
399
|
+
|
|
400
|
+
# Runaway-loop circuit breakers (see "Runaway sessions" below)
|
|
401
|
+
config.token_nudge_threshold = 500_000 # input tokens in one tool loop → nudge model to wrap up (nil disables)
|
|
402
|
+
config.token_stop_threshold = 1_000_000 # input tokens in one tool loop → force a final answer (nil disables)
|
|
399
403
|
end
|
|
400
404
|
```
|
|
401
405
|
|
|
406
|
+
### Runaway sessions & known-issue hints
|
|
407
|
+
|
|
408
|
+
Two mechanisms stop the LLM from burning tokens on a dead end:
|
|
409
|
+
|
|
410
|
+
**Known-issue hints** — when executed code fails (or prints a rescued error) matching a
|
|
411
|
+
known environment-level problem, the tool result includes explicit guidance telling the
|
|
412
|
+
model not to retry. The built-in hint recognizes decryption failures
|
|
413
|
+
(`OpenSSL::Cipher::CipherError` / "bad decrypt" / `ActiveRecord::Encryption` errors),
|
|
414
|
+
which indicate a missing or placeholder encryption key in the console process — an
|
|
415
|
+
environment issue no amount of retrying can fix. Repeat occurrences escalate the message.
|
|
416
|
+
Apps can add their own:
|
|
417
|
+
|
|
418
|
+
```ruby
|
|
419
|
+
RailsConsoleAi.configure do |config|
|
|
420
|
+
config.error_hints << {
|
|
421
|
+
name: :vpn_required,
|
|
422
|
+
pattern: /Errno::ECONNREFUSED.*10\.8\./,
|
|
423
|
+
hint: "This host is only reachable over the VPN, which this console does not have. " \
|
|
424
|
+
"Do not retry; report the limitation to the user."
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Circuit breakers** — inside a single tool loop, the engine tracks:
|
|
430
|
+
|
|
431
|
+
- *Identical tool calls* (same tool + same args): warns at 3, breaks at 5.
|
|
432
|
+
- *Repeated error signatures* (same normalized error from **different** code): warns at 3,
|
|
433
|
+
breaks at 5. This catches "new code, same dead end" loops that identical-call detection
|
|
434
|
+
misses. Digits are normalized so varying record IDs don't disguise the same failure.
|
|
435
|
+
- *Token budget*: at `token_nudge_threshold` cumulative input tokens the model is told to
|
|
436
|
+
wrap up; at `token_stop_threshold` the loop is stopped and a final answer is forced.
|
|
437
|
+
|
|
438
|
+
When any breaker trips, the model is asked to summarize what it established, what it
|
|
439
|
+
could not determine and why, and what a human should do next — instead of iterating.
|
|
440
|
+
|
|
402
441
|
### Code Search Paths
|
|
403
442
|
|
|
404
443
|
By default, `list_files` and `search_code` only look in `app/`. If your project has code in other directories (e.g. a frontend in `public/portal`, or shared code in `lib`), add them:
|
|
@@ -513,12 +552,17 @@ id = RailsConsoleAi.run_agent("How many users signed up yesterday?", name: 'dail
|
|
|
513
552
|
# => 4821 (Integer session id, returned immediately)
|
|
514
553
|
|
|
515
554
|
RailsConsoleAi.check_agent(id)
|
|
516
|
-
# => 'queued' | 'running' | 'ready' | 'failed' | nil
|
|
555
|
+
# => 'queued' | 'running' | 'ready' | 'failed' | 'aborted' | nil
|
|
517
556
|
|
|
518
557
|
RailsConsoleAi.get_agent_response(id)
|
|
519
558
|
# => { status: 'ready', result: "1,432 users signed up yesterday.\n", error: nil }
|
|
559
|
+
|
|
560
|
+
RailsConsoleAi.abort_agent(id)
|
|
561
|
+
# => true (aborted) | false (already finished, or unknown id)
|
|
520
562
|
```
|
|
521
563
|
|
|
564
|
+
`abort_agent` cancels a run: a queued run is never picked up, and a run already executing keeps going but its result is discarded when it completes -- the session stays `status='aborted'`.
|
|
565
|
+
|
|
522
566
|
`run_agent` enqueues a row in the sessions table with `mode='agent_api'` and `status='queued'`. A separate long-running rake task picks them up and runs each in its own thread using the same engine that powers `ai "..."` in the console.
|
|
523
567
|
|
|
524
568
|
### Per-run options
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require 'rails_console_ai/agent_loader'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
|
-
class AgentsController < ApplicationController
|
|
4
|
+
class AgentsController < RailsConsoleAi::ApplicationController
|
|
5
5
|
before_action :load_agent, only: [:show, :edit, :update, :destroy, :approve]
|
|
6
6
|
|
|
7
7
|
def index
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
require 'rails_console_ai/tools/memory_tools'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
|
-
class MemoriesController < ApplicationController
|
|
5
|
-
before_action :load_memory, only: [:show, :edit, :update, :destroy]
|
|
4
|
+
class MemoriesController < RailsConsoleAi::ApplicationController
|
|
5
|
+
before_action :load_memory, only: [:show, :edit, :update, :destroy, :approve]
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
8
|
@memories = Tools::MemoryTools.new.load_all_memories
|
|
@@ -73,6 +73,26 @@ module RailsConsoleAi
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
def approve
|
|
77
|
+
redirect_to memories_path, alert: file_memory_message and return unless @memory.is_a?(RailsConsoleAi::Memory)
|
|
78
|
+
|
|
79
|
+
approver = params[:approved_by].presence ||
|
|
80
|
+
(request.respond_to?(:remote_user) && request.remote_user.presence) ||
|
|
81
|
+
'web'
|
|
82
|
+
|
|
83
|
+
if @memory.approved?
|
|
84
|
+
redirect_to memory_path(@memory), notice: 'Memory is already approved.'
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
begin
|
|
89
|
+
@memory.approve!(approved_by: approver)
|
|
90
|
+
redirect_to memory_path(@memory), notice: "Approved by #{approver}. The AI can now recall this memory."
|
|
91
|
+
rescue ArgumentError, ActiveRecord::RecordInvalid => e
|
|
92
|
+
redirect_to memory_path(@memory), alert: "Could not approve: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
76
96
|
def diff
|
|
77
97
|
@memory = Memory.find(params[:memory_id])
|
|
78
98
|
@from = @memory.versions.find(params[:from])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require 'rails_console_ai/skill_loader'
|
|
2
2
|
|
|
3
3
|
module RailsConsoleAi
|
|
4
|
-
class SkillsController < ApplicationController
|
|
4
|
+
class SkillsController < RailsConsoleAi::ApplicationController
|
|
5
5
|
before_action :load_skill, only: [:show, :edit, :update, :destroy, :approve]
|
|
6
6
|
|
|
7
7
|
def index
|
|
@@ -4,6 +4,10 @@ module RailsConsoleAi
|
|
|
4
4
|
class Memory < ActiveRecord::Base
|
|
5
5
|
self.table_name = 'rails_console_ai_memories'
|
|
6
6
|
|
|
7
|
+
STATUS_PROPOSED = 'proposed'.freeze
|
|
8
|
+
STATUS_APPROVED = 'approved'.freeze
|
|
9
|
+
STATUSES = [STATUS_PROPOSED, STATUS_APPROVED].freeze
|
|
10
|
+
|
|
7
11
|
has_many :versions,
|
|
8
12
|
-> { order(created_at: :desc) },
|
|
9
13
|
class_name: 'RailsConsoleAi::MemoryVersion',
|
|
@@ -12,11 +16,14 @@ module RailsConsoleAi
|
|
|
12
16
|
|
|
13
17
|
validates :content, presence: true
|
|
14
18
|
validates :name, presence: true, uniqueness: { case_sensitive: false }
|
|
19
|
+
validates :status, inclusion: { in: STATUSES }
|
|
15
20
|
validate :content_parses
|
|
16
21
|
|
|
17
22
|
before_validation :sync_name_from_content
|
|
18
23
|
|
|
19
24
|
scope :alphabetical, -> { order(Arel.sql('LOWER(name)')) }
|
|
25
|
+
scope :approved, -> { where(status: STATUS_APPROVED) }
|
|
26
|
+
scope :proposed, -> { where(status: STATUS_PROPOSED) }
|
|
20
27
|
|
|
21
28
|
def self.connection
|
|
22
29
|
klass = RailsConsoleAi.configuration.connection_class
|
|
@@ -42,6 +49,9 @@ module RailsConsoleAi
|
|
|
42
49
|
def description; parsed['description']; end
|
|
43
50
|
def tags; Array(parsed['tags']); end
|
|
44
51
|
|
|
52
|
+
def proposed?; status.to_s == STATUS_PROPOSED; end
|
|
53
|
+
def approved?; status.to_s == STATUS_APPROVED; end
|
|
54
|
+
|
|
45
55
|
def self.record_use!(id)
|
|
46
56
|
where(id: id).update_all([
|
|
47
57
|
'use_count = COALESCE(use_count, 0) + 1, last_used_at = ?',
|
|
@@ -60,6 +70,9 @@ module RailsConsoleAi
|
|
|
60
70
|
'description' => description,
|
|
61
71
|
'tags' => tags,
|
|
62
72
|
'content' => content,
|
|
73
|
+
'status' => status,
|
|
74
|
+
'approved_by' => approved_by,
|
|
75
|
+
'approved_at' => approved_at,
|
|
63
76
|
'use_count' => use_count,
|
|
64
77
|
'last_used_at' => last_used_at,
|
|
65
78
|
'source' => :db,
|
|
@@ -67,14 +80,25 @@ module RailsConsoleAi
|
|
|
67
80
|
}
|
|
68
81
|
end
|
|
69
82
|
|
|
70
|
-
|
|
83
|
+
# Assigns attrs, saves, and records one MemoryVersion snapshot.
|
|
84
|
+
# Any change to `content` reverts approval back to "proposed" unless
|
|
85
|
+
# `preserve_approval: true` is passed (approve! does this).
|
|
86
|
+
def update_with_version!(attrs, edited_by: nil, change_note: nil, preserve_approval: false)
|
|
71
87
|
transaction do
|
|
72
88
|
assign_attributes(attrs)
|
|
89
|
+
|
|
90
|
+
if !preserve_approval && approved? && changes.key?('content')
|
|
91
|
+
self.status = STATUS_PROPOSED
|
|
92
|
+
self.approved_by = nil
|
|
93
|
+
self.approved_at = nil
|
|
94
|
+
end
|
|
95
|
+
|
|
73
96
|
save!
|
|
74
97
|
RailsConsoleAi::MemoryVersion.create!(
|
|
75
98
|
memory_id: id,
|
|
76
99
|
name: name,
|
|
77
100
|
content: content,
|
|
101
|
+
status: status,
|
|
78
102
|
edited_by: edited_by,
|
|
79
103
|
change_note: change_note
|
|
80
104
|
)
|
|
@@ -82,6 +106,21 @@ module RailsConsoleAi
|
|
|
82
106
|
self
|
|
83
107
|
end
|
|
84
108
|
|
|
109
|
+
def approve!(approved_by:)
|
|
110
|
+
raise ArgumentError, 'approved_by is required' if approved_by.to_s.strip.empty?
|
|
111
|
+
|
|
112
|
+
update_with_version!(
|
|
113
|
+
{
|
|
114
|
+
status: STATUS_APPROVED,
|
|
115
|
+
approved_by: approved_by,
|
|
116
|
+
approved_at: Time.now.utc
|
|
117
|
+
},
|
|
118
|
+
edited_by: approved_by,
|
|
119
|
+
change_note: "Approved by #{approved_by}",
|
|
120
|
+
preserve_approval: true
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
85
124
|
private
|
|
86
125
|
|
|
87
126
|
def sync_name_from_content
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
</form>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
+
<% proposed_count = @memories.count { |m| m['source'] == :db && m['status'] != 'approved' } %>
|
|
15
|
+
<% if proposed_count > 0 %>
|
|
16
|
+
<div class="flash flash-alert" style="margin-bottom:16px;">
|
|
17
|
+
<strong><%= proposed_count %></strong> memor<%= proposed_count == 1 ? 'y' : 'ies' %> awaiting approval. The AI cannot recall them until a human approves.
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
14
21
|
<% if @memories.empty? %>
|
|
15
22
|
<div class="meta-card">
|
|
16
23
|
<p class="text-muted">No memories yet. <%= link_to 'Create one', new_memory_path %> or drop a Markdown file in <code>.rails_console_ai/memories/</code>.</p>
|
|
@@ -22,7 +29,7 @@
|
|
|
22
29
|
<th>Name</th>
|
|
23
30
|
<th>Description</th>
|
|
24
31
|
<th>Tags</th>
|
|
25
|
-
<th>Source</th>
|
|
32
|
+
<th>Source / Status</th>
|
|
26
33
|
<th title="Number of times the AI recalled this memory (DB only)">Uses</th>
|
|
27
34
|
<th>Last used</th>
|
|
28
35
|
<th></th>
|
|
@@ -40,6 +47,11 @@
|
|
|
40
47
|
<td>
|
|
41
48
|
<% if m['source'] == :db %>
|
|
42
49
|
<span class="badge source-db">DB</span>
|
|
50
|
+
<% if m['status'] == 'approved' %>
|
|
51
|
+
<span class="badge status-approved">APPROVED</span>
|
|
52
|
+
<% else %>
|
|
53
|
+
<span class="badge status-proposed">PROPOSED</span>
|
|
54
|
+
<% end %>
|
|
43
55
|
<% else %>
|
|
44
56
|
<span class="badge source-file">FILE</span>
|
|
45
57
|
<% end %>
|
|
@@ -8,11 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
<%= link_to '← All memories', memories_path, class: 'back-link' %>
|
|
10
10
|
|
|
11
|
+
<% if is_db && !@memory.approved? %>
|
|
12
|
+
<div class="flash flash-alert" style="margin-bottom:16px;">
|
|
13
|
+
<strong>Awaiting approval.</strong> The AI cannot recall this memory until a human approves it.
|
|
14
|
+
Review the content below, then click <strong>Approve</strong>.
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
11
18
|
<div class="meta-card">
|
|
12
19
|
<h2 style="margin-bottom:8px;">
|
|
13
20
|
<%= name %>
|
|
14
21
|
<% if is_db %>
|
|
15
22
|
<span class="badge source-db">DB</span>
|
|
23
|
+
<% if @memory.approved? %>
|
|
24
|
+
<span class="badge status-approved">APPROVED</span>
|
|
25
|
+
<% else %>
|
|
26
|
+
<span class="badge status-proposed">PROPOSED</span>
|
|
27
|
+
<% end %>
|
|
16
28
|
<% else %>
|
|
17
29
|
<span class="badge source-file">FILE</span>
|
|
18
30
|
<% end %>
|
|
@@ -24,6 +36,10 @@
|
|
|
24
36
|
<div class="meta-item"><label>Versions</label><span><%= @memory.versions.count %></span></div>
|
|
25
37
|
<div class="meta-item"><label>Times recalled</label><span><%= @memory.use_count %></span></div>
|
|
26
38
|
<div class="meta-item"><label>Last recalled</label><span><%= @memory.last_used_at&.strftime('%Y-%m-%d %H:%M') || 'never' %></span></div>
|
|
39
|
+
<% if @memory.approved? %>
|
|
40
|
+
<div class="meta-item"><label>Approved by</label><span><%= @memory.approved_by || '—' %></span></div>
|
|
41
|
+
<div class="meta-item"><label>Approved at</label><span><%= @memory.approved_at&.strftime('%Y-%m-%d %H:%M') || '—' %></span></div>
|
|
42
|
+
<% end %>
|
|
27
43
|
<% else %>
|
|
28
44
|
<div class="meta-item"><label>Path</label><span class="mono"><%= file_key %></span></div>
|
|
29
45
|
<% end %>
|
|
@@ -32,6 +48,12 @@
|
|
|
32
48
|
|
|
33
49
|
<div class="btn-bar">
|
|
34
50
|
<% if is_db %>
|
|
51
|
+
<% unless @memory.approved? %>
|
|
52
|
+
<%= form_with url: approve_memory_path(@memory), method: :post, local: true, style: 'display:inline' do %>
|
|
53
|
+
<input type="text" name="approved_by" placeholder="Your name (required)" style="padding:7px 10px; border:1px solid #ccc; border-radius:4px; margin-right:6px;">
|
|
54
|
+
<button type="submit" class="btn btn-danger" data-confirm="Approve this memory? The AI will then be able to recall it.">Approve</button>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% end %>
|
|
35
57
|
<%= link_to 'Edit', edit_memory_path(@memory), class: 'btn' %>
|
|
36
58
|
<%= link_to 'Versions', memory_versions_path(@memory), class: 'btn btn-secondary' %>
|
|
37
59
|
<%= button_to 'Delete', memory_path(@memory), method: :delete, class: 'btn btn-danger', form: { style: 'display:inline' },data: { confirm: 'Delete this memory? Past versions remain in history.' } %>
|
|
@@ -49,6 +71,7 @@
|
|
|
49
71
|
<div>
|
|
50
72
|
<strong>v<%= v.id %></strong>
|
|
51
73
|
<span class="ver-meta">— <%= v.created_at.strftime('%Y-%m-%d %H:%M') %> by <%= v.edited_by || 'unknown' %></span>
|
|
74
|
+
<% if v.status == 'approved' %><span class="badge status-approved">APPROVED</span><% end %>
|
|
52
75
|
<% if v.change_note.present? %><div class="ver-meta"><%= v.change_note %></div><% end %>
|
|
53
76
|
</div>
|
|
54
77
|
<div>
|
data/config/routes.rb
CHANGED
|
@@ -5,11 +5,11 @@ RailsConsoleAi.configure do |config|
|
|
|
5
5
|
# API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
|
|
6
6
|
# config.api_key = 'sk-...'
|
|
7
7
|
|
|
8
|
-
# Model override (defaults: claude-
|
|
9
|
-
# config.model = 'claude-opus-4-
|
|
8
|
+
# Model override (defaults: claude-sonnet-5 for Anthropic, gpt-5.3-codex for OpenAI)
|
|
9
|
+
# config.model = 'claude-opus-4-8'
|
|
10
10
|
|
|
11
|
-
# Max tokens for LLM response
|
|
12
|
-
config.max_tokens = 4096
|
|
11
|
+
# Max tokens for LLM response (default resolves per model family; 16000 for Claude models)
|
|
12
|
+
# config.max_tokens = 4096
|
|
13
13
|
|
|
14
14
|
# Temperature (0.0 - 1.0)
|
|
15
15
|
config.temperature = 0.2
|
|
@@ -35,7 +35,7 @@ RailsConsoleAi.configure do |config|
|
|
|
35
35
|
# AWS Bedrock provider (uses AWS credential chain — no API key needed):
|
|
36
36
|
# config.provider = :bedrock
|
|
37
37
|
# config.bedrock_region = 'us-east-1'
|
|
38
|
-
# config.model = 'us.anthropic.claude-sonnet-
|
|
38
|
+
# config.model = 'us.anthropic.claude-sonnet-5'
|
|
39
39
|
|
|
40
40
|
# Debug mode: prints full API requests/responses and tool calls to stderr
|
|
41
41
|
# config.debug = true
|
|
@@ -134,6 +134,10 @@ module RailsConsoleAi
|
|
|
134
134
|
end
|
|
135
135
|
result_text = compose_result(channel.captured_output, exec_result)
|
|
136
136
|
|
|
137
|
+
if aborted?(session.id)
|
|
138
|
+
puts ">> aborted (result discarded)"
|
|
139
|
+
return
|
|
140
|
+
end
|
|
137
141
|
SessionLogger.update(session.id, status: 'ready', result: result_text)
|
|
138
142
|
|
|
139
143
|
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
|
|
@@ -142,16 +146,23 @@ module RailsConsoleAi
|
|
|
142
146
|
puts ">> ready (#{elapsed}ms) #{preview}"
|
|
143
147
|
rescue RunnerTimeoutError => e
|
|
144
148
|
warn ">> TIMEOUT #{e.message}"
|
|
145
|
-
SessionLogger.update(session.id, status: 'failed', error_message: e.message)
|
|
149
|
+
SessionLogger.update(session.id, status: 'failed', error_message: e.message) unless aborted?(session.id)
|
|
146
150
|
rescue => e
|
|
147
151
|
warn ">> FAILED #{e.class}: #{e.message}"
|
|
148
152
|
e.backtrace&.first(5)&.each { |line| warn " #{line}" }
|
|
153
|
+
return if aborted?(session.id)
|
|
149
154
|
SessionLogger.update(session.id,
|
|
150
155
|
status: 'failed',
|
|
151
156
|
error_message: "#{e.class}: #{e.message}\n#{Array(e.backtrace).first(10).join("\n")}"
|
|
152
157
|
)
|
|
153
158
|
end
|
|
154
159
|
|
|
160
|
+
# An abort (RailsConsoleAi.abort_agent) flips the row to 'aborted' while we
|
|
161
|
+
# run; in that case the run's outcome must not overwrite the abort.
|
|
162
|
+
def aborted?(session_id)
|
|
163
|
+
Session.where(id: session_id).pluck(:status).first == 'aborted'
|
|
164
|
+
end
|
|
165
|
+
|
|
155
166
|
# When cap is nil or non-positive, run inline. Otherwise spawn a nested
|
|
156
167
|
# worker thread, join with the deadline, and kill + raise on overshoot.
|
|
157
168
|
# Kept localized (vs Timeout.timeout) so a runaway provider call can't
|
|
@@ -368,7 +368,7 @@ module RailsConsoleAi
|
|
|
368
368
|
config = RailsConsoleAi.configuration
|
|
369
369
|
model = @engine.effective_model
|
|
370
370
|
thinking = config.resolved_thinking_model
|
|
371
|
-
pricing = Configuration
|
|
371
|
+
pricing = Configuration.pricing_for(model)
|
|
372
372
|
|
|
373
373
|
@real_stdout.puts "\e[36m Model info:\e[0m"
|
|
374
374
|
@real_stdout.puts "\e[2m Provider: #{config.provider}\e[0m"
|