rails_console_ai 0.30.0 → 0.32.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 +33 -11
- data/app/controllers/rails_console_ai/agent_versions_controller.rb +2 -9
- data/app/controllers/rails_console_ai/agents_controller.rb +25 -48
- data/app/controllers/rails_console_ai/memories_controller.rb +35 -38
- data/app/controllers/rails_console_ai/memory_versions_controller.rb +2 -6
- data/app/controllers/rails_console_ai/sessions_controller.rb +1 -1
- data/app/controllers/rails_console_ai/skill_versions_controller.rb +2 -8
- data/app/controllers/rails_console_ai/skills_controller.rb +19 -48
- data/app/models/rails_console_ai/agent.rb +33 -65
- data/app/models/rails_console_ai/agent_version.rb +8 -20
- data/app/models/rails_console_ai/memory.rb +67 -23
- data/app/models/rails_console_ai/memory_version.rb +5 -20
- data/app/models/rails_console_ai/skill.rb +55 -105
- data/app/models/rails_console_ai/skill_version.rb +7 -28
- data/app/views/rails_console_ai/agents/_form.html.erb +9 -34
- data/app/views/rails_console_ai/agents/diff.html.erb +1 -5
- data/app/views/rails_console_ai/agents/new.html.erb +0 -16
- data/app/views/rails_console_ai/memories/_form.html.erb +6 -13
- data/app/views/rails_console_ai/memories/diff.html.erb +1 -5
- data/app/views/rails_console_ai/memories/index.html.erb +13 -1
- data/app/views/rails_console_ai/memories/new.html.erb +0 -15
- data/app/views/rails_console_ai/memories/show.html.erb +23 -0
- data/app/views/rails_console_ai/skills/_form.html.erb +7 -29
- data/app/views/rails_console_ai/skills/diff.html.erb +1 -8
- data/app/views/rails_console_ai/skills/new.html.erb +0 -17
- data/config/routes.rb +3 -3
- data/lib/generators/rails_console_ai/templates/initializer.rb +5 -5
- data/lib/rails_console_ai/agent_loader.rb +18 -10
- data/lib/rails_console_ai/agent_runner.rb +66 -4
- data/lib/rails_console_ai/channel/console.rb +1 -1
- data/lib/rails_console_ai/configuration.rb +51 -33
- data/lib/rails_console_ai/conversation_engine.rb +3 -3
- data/lib/rails_console_ai/safety_guards.rb +36 -0
- data/lib/rails_console_ai/session_logger.rb +4 -0
- data/lib/rails_console_ai/skill_loader.rb +18 -9
- data/lib/rails_console_ai/slack_bot.rb +2 -2
- data/lib/rails_console_ai/storage/database_storage.rb +14 -20
- data/lib/rails_console_ai/sub_agent.rb +1 -1
- data/lib/rails_console_ai/tools/memory_tools.rb +37 -6
- data/lib/rails_console_ai/tools/registry.rb +18 -6
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +92 -71
- metadata +1 -1
|
@@ -1,47 +1,23 @@
|
|
|
1
1
|
<% if @agent.persisted? && @agent.respond_to?(:approved?) && @agent.approved? %>
|
|
2
2
|
<div class="flash flash-alert" style="margin-bottom:16px;">
|
|
3
|
-
Heads up: this agent is currently <strong>approved</strong>. Editing it will revert its status to <strong>proposed</strong
|
|
3
|
+
Heads up: this agent is currently <strong>approved</strong>. Editing it will revert its status to <strong>proposed</strong>.
|
|
4
4
|
</div>
|
|
5
5
|
<% elsif !@agent.persisted? %>
|
|
6
6
|
<div class="flash flash-notice" style="margin-bottom:16px;">
|
|
7
|
-
New DB agents start as <strong>proposed</strong>. After saving, an approver (likely you) needs to click the Approve button
|
|
7
|
+
New DB agents start as <strong>proposed</strong>. After saving, an approver (likely you) needs to click the Approve button before delegate_task can invoke it.
|
|
8
8
|
</div>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
11
|
<%= form_with model: @agent, url: form_url, method: form_method, local: true do |f| %>
|
|
12
12
|
<div class="form-card">
|
|
13
13
|
<div class="form-row">
|
|
14
|
-
<label>
|
|
15
|
-
<%= f.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="form-row">
|
|
24
|
-
<label>Body / system instructions (markdown)</label>
|
|
25
|
-
<%= f.text_area :body, value: @agent.body, rows: 18 %>
|
|
26
|
-
<div class="hint">Persona + strategy + rules. Be specific about what tools/approaches the sub-agent should use and what format its summary should take.</div>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<div class="form-row">
|
|
30
|
-
<label>Max rounds (optional)</label>
|
|
31
|
-
<%= f.number_field :max_rounds, value: @agent.max_rounds, min: 1, max: 100 %>
|
|
32
|
-
<div class="hint">Maximum tool-loop iterations for the sub-agent. Defaults to the global <code>config.sub_agent_max_rounds</code>. Smaller = tighter scope, fewer tokens.</div>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<div class="form-row">
|
|
36
|
-
<label>Model (optional)</label>
|
|
37
|
-
<%= f.text_field :model, value: @agent.model, placeholder: 'e.g. claude-haiku-4-5' %>
|
|
38
|
-
<div class="hint">Model override. Leave blank to use the global model. Useful for cheap/fast agents.</div>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<div class="form-row">
|
|
42
|
-
<label>Tool whitelist (one per line, optional)</label>
|
|
43
|
-
<%= f.text_area :tools, value: Array(@agent.tools).join("\n"), rows: 4 %>
|
|
44
|
-
<div class="hint">Tool names the sub-agent is allowed to call (e.g. <code>execute_code</code>, <code>search_code</code>, <code>read_file</code>). Leave blank to use the default tool set. Adding tools here can broaden what the sub-agent can do — this is why DB agents need approval.</div>
|
|
14
|
+
<label>Markdown (frontmatter + body)</label>
|
|
15
|
+
<%= f.text_area :content, value: @agent.content, rows: 28, required: true, style: 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;' %>
|
|
16
|
+
<div class="hint">
|
|
17
|
+
YAML frontmatter between <code>---</code> lines, then the agent persona / instructions. Required:
|
|
18
|
+
<code>name</code>, <code>description</code>. Optional: <code>max_rounds</code>, <code>model</code>,
|
|
19
|
+
<code>tools</code> (whitelist; empty/omitted = default set).
|
|
20
|
+
</div>
|
|
45
21
|
</div>
|
|
46
22
|
|
|
47
23
|
<hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
|
|
@@ -49,7 +25,6 @@
|
|
|
49
25
|
<div class="form-row">
|
|
50
26
|
<label>Edited by</label>
|
|
51
27
|
<input type="text" name="edited_by" value="<%= params[:edited_by] %>" placeholder="Your name or handle">
|
|
52
|
-
<div class="hint">Recorded with this version. Free text — the gem does not assume authentication beyond the admin login.</div>
|
|
53
28
|
</div>
|
|
54
29
|
|
|
55
30
|
<div class="form-row">
|
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<%= render_text_diff(@from.body, @to_body, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
10
|
-
|
|
11
|
-
<h3 style="margin:24px 0 6px;">Tool whitelist</h3>
|
|
12
|
-
<%= render_json_diff(Array(@from.tools), @to_tools, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
8
|
+
<%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
13
9
|
|
|
14
10
|
<div class="btn-bar" style="margin-top:24px;">
|
|
15
11
|
<% if @to.nil? %>
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
<%= link_to '← All agents', agents_path, class: 'back-link' %>
|
|
2
2
|
<h2 style="margin-bottom:16px;">New agent</h2>
|
|
3
3
|
|
|
4
|
-
<%= form_with url: import_agents_path, method: :post, local: true do %>
|
|
5
|
-
<div class="form-card" style="background:#f8f9fa;">
|
|
6
|
-
<div class="form-row" style="margin-bottom:8px;">
|
|
7
|
-
<label>Paste a .md file (optional)</label>
|
|
8
|
-
<textarea name="content" rows="14" placeholder="--- name: My agent description: What it specializes in max_rounds: 10 model: claude-haiku-4-5 tools: [execute_code, search_code] --- Persona, strategy, rules …"></textarea>
|
|
9
|
-
<div class="hint">
|
|
10
|
-
Paste the full contents of a Markdown file (YAML frontmatter + body) — same
|
|
11
|
-
format as <code>.rails_console_ai/agents/*.md</code> or the built-in agents
|
|
12
|
-
shipped with the gem. Useful for moving an existing on-disk agent into the
|
|
13
|
-
versioned DB store.
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
<button type="submit" class="btn btn-secondary">Parse pasted content ↓</button>
|
|
17
|
-
</div>
|
|
18
|
-
<% end %>
|
|
19
|
-
|
|
20
4
|
<%= render 'form',
|
|
21
5
|
form_url: agents_path,
|
|
22
6
|
form_method: :post,
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
<%= form_with model: @memory, url: form_url, method: form_method, local: true do |f| %>
|
|
2
2
|
<div class="form-card">
|
|
3
3
|
<div class="form-row">
|
|
4
|
-
<label>
|
|
5
|
-
<%= f.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<%= f.text_area :description, value: @memory.description, rows: 14 %>
|
|
11
|
-
<div class="hint">The fact or pattern you're persisting. Markdown is fine.</div>
|
|
12
|
-
</div>
|
|
13
|
-
|
|
14
|
-
<div class="form-row">
|
|
15
|
-
<label>Tags (comma-separated)</label>
|
|
16
|
-
<%= f.text_field :tags, value: Array(@memory.tags).join(', '), placeholder: 'database, sharding' %>
|
|
4
|
+
<label>Markdown (frontmatter + body)</label>
|
|
5
|
+
<%= f.text_area :content, value: @memory.content, rows: 22, required: true, style: 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;' %>
|
|
6
|
+
<div class="hint">
|
|
7
|
+
YAML frontmatter between <code>---</code> lines (required: <code>name</code>; optional: <code>tags</code>),
|
|
8
|
+
then the memory body — the fact or pattern you're persisting.
|
|
9
|
+
</div>
|
|
17
10
|
</div>
|
|
18
11
|
|
|
19
12
|
<hr style="margin: 20px 0; border: 0; border-top: 1px solid #eee;">
|
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<%= render_text_diff(@from.description, @to_description, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
10
|
-
|
|
11
|
-
<h3 style="margin:24px 0 6px;">Tags</h3>
|
|
12
|
-
<%= render_json_diff(Array(@from.tags), @to_tags, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
8
|
+
<%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
13
9
|
|
|
14
10
|
<div class="btn-bar" style="margin-top:24px;">
|
|
15
11
|
<% if @to.nil? %>
|
|
@@ -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 %>
|
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
<%= link_to '← All memories', memories_path, class: 'back-link' %>
|
|
2
2
|
<h2 style="margin-bottom:16px;">New memory</h2>
|
|
3
3
|
|
|
4
|
-
<%= form_with url: import_memories_path, method: :post, local: true do %>
|
|
5
|
-
<div class="form-card" style="background:#f8f9fa;">
|
|
6
|
-
<div class="form-row" style="margin-bottom:8px;">
|
|
7
|
-
<label>Paste a .md file (optional)</label>
|
|
8
|
-
<textarea name="content" rows="12" placeholder="--- name: My memory tags: [database] --- The fact or pattern …"></textarea>
|
|
9
|
-
<div class="hint">
|
|
10
|
-
Paste the full contents of a Markdown file (YAML frontmatter + body) — same
|
|
11
|
-
format as <code>.rails_console_ai/memories/*.md</code>. The fields below will
|
|
12
|
-
be prefilled from the paste.
|
|
13
|
-
</div>
|
|
14
|
-
</div>
|
|
15
|
-
<button type="submit" class="btn btn-secondary">Parse pasted content ↓</button>
|
|
16
|
-
</div>
|
|
17
|
-
<% end %>
|
|
18
|
-
|
|
19
4
|
<%= render 'form',
|
|
20
5
|
form_url: memories_path,
|
|
21
6
|
form_method: :post,
|
|
@@ -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>
|
|
@@ -11,36 +11,14 @@
|
|
|
11
11
|
<%= form_with model: @skill, url: form_url, method: form_method, local: true do |f| %>
|
|
12
12
|
<div class="form-card">
|
|
13
13
|
<div class="form-row">
|
|
14
|
-
<label>
|
|
15
|
-
<%= f.
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
<div class="form-row">
|
|
19
|
-
<label>Description</label>
|
|
20
|
-
<%= f.text_field :description, value: @skill.description, placeholder: 'One-line description of when to use this skill' %>
|
|
21
|
-
</div>
|
|
22
|
-
|
|
23
|
-
<div class="form-row">
|
|
24
|
-
<label>Body (markdown)</label>
|
|
25
|
-
<%= f.text_area :body, value: @skill.body, rows: 18 %>
|
|
26
|
-
<div class="hint">Include "## When to use", "## Recipe" (numbered steps with code blocks), "## Notes".</div>
|
|
27
|
-
</div>
|
|
28
|
-
|
|
29
|
-
<div class="form-row">
|
|
30
|
-
<label>Tags (comma-separated)</label>
|
|
31
|
-
<%= f.text_field :tags, value: Array(@skill.tags).join(', '), placeholder: 'booking-page, admin' %>
|
|
32
|
-
</div>
|
|
33
|
-
|
|
34
|
-
<div class="form-row">
|
|
35
|
-
<label>Bypass guards for methods (one per line)</label>
|
|
36
|
-
<%= f.text_area :bypass_guards_for_methods, value: Array(@skill.bypass_guards_for_methods).join("\n"), rows: 4 %>
|
|
14
|
+
<label>Markdown (frontmatter + body)</label>
|
|
15
|
+
<%= f.text_area :content, value: @skill.content, rows: 28, required: true, style: 'font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px;' %>
|
|
37
16
|
<div class="hint">
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Namespaced classes work: <code>Acme::Booking#destroy</code>. The <code>!</code> is part of the match, so <code>save</code> and <code>save!</code> are different specs. While the skill is active, calls to these methods skip the configured safety guards (DB writes, HTTP mutations, email, etc.) <strong>for that tool-call only</strong>.
|
|
17
|
+
YAML frontmatter between <code>---</code> lines, then the markdown body. Required frontmatter:
|
|
18
|
+
<code>name</code>, <code>description</code>. Optional: <code>tags</code>,
|
|
19
|
+
<code>bypass_guards_for_methods</code> (entries shaped like <code>ClassName#method</code> or
|
|
20
|
+
<code>ClassName.method</code> — while the skill is active, calls to these methods skip the
|
|
21
|
+
configured safety guards for that tool-call only).
|
|
44
22
|
</div>
|
|
45
23
|
</div>
|
|
46
24
|
|
|
@@ -5,14 +5,7 @@
|
|
|
5
5
|
Version #<%= @from.id %> (<%= @from.created_at.strftime('%Y-%m-%d %H:%M') %>) → <%= @to_label %>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
<%= render_text_diff(@from.body, @to_body, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
10
|
-
|
|
11
|
-
<h3 style="margin:24px 0 6px;">Tags</h3>
|
|
12
|
-
<%= render_json_diff(Array(@from.tags), @to_tags, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
13
|
-
|
|
14
|
-
<h3 style="margin:24px 0 6px;">Bypass guards</h3>
|
|
15
|
-
<%= render_json_diff(Array(@from.bypass_guards_for_methods), @to_bypass, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
8
|
+
<%= render_text_diff(@from.content, @to_content, left_label: "v##{@from.id}", right_label: @to_label) %>
|
|
16
9
|
|
|
17
10
|
<div class="btn-bar" style="margin-top:24px;">
|
|
18
11
|
<% if @to.nil? %>
|
|
@@ -1,23 +1,6 @@
|
|
|
1
1
|
<%= link_to '← All skills', skills_path, class: 'back-link' %>
|
|
2
2
|
<h2 style="margin-bottom:16px;">New skill</h2>
|
|
3
3
|
|
|
4
|
-
<%= form_with url: import_skills_path, method: :post, local: true do %>
|
|
5
|
-
<div class="form-card" style="background:#f8f9fa;">
|
|
6
|
-
<div class="form-row" style="margin-bottom:8px;">
|
|
7
|
-
<label>Paste a .md file (optional)</label>
|
|
8
|
-
<textarea name="content" rows="14" placeholder="--- name: My skill description: What it does tags: [admin] bypass_guards_for_methods: [] --- ## When to use … ## Recipe 1. …"></textarea>
|
|
9
|
-
<div class="hint">
|
|
10
|
-
Paste the full contents of a Markdown file (YAML frontmatter between
|
|
11
|
-
<code>---</code> lines, then a markdown body) — same format as
|
|
12
|
-
<code>.rails_console_ai/skills/*.md</code>. The fields below will be
|
|
13
|
-
prefilled from the paste. You can review and tweak before clicking
|
|
14
|
-
<strong>Create skill</strong>.
|
|
15
|
-
</div>
|
|
16
|
-
</div>
|
|
17
|
-
<button type="submit" class="btn btn-secondary">Parse pasted content ↓</button>
|
|
18
|
-
</div>
|
|
19
|
-
<% end %>
|
|
20
|
-
|
|
21
4
|
<%= render 'form',
|
|
22
5
|
form_url: skills_path,
|
|
23
6
|
form_method: :post,
|
data/config/routes.rb
CHANGED
|
@@ -8,7 +8,6 @@ RailsConsoleAi::Engine.routes.draw do
|
|
|
8
8
|
end
|
|
9
9
|
collection do
|
|
10
10
|
get :diff
|
|
11
|
-
post :import
|
|
12
11
|
end
|
|
13
12
|
resources :versions, only: [:index, :show], controller: 'skill_versions' do
|
|
14
13
|
member do
|
|
@@ -18,9 +17,11 @@ RailsConsoleAi::Engine.routes.draw do
|
|
|
18
17
|
end
|
|
19
18
|
|
|
20
19
|
resources :memories do
|
|
20
|
+
member do
|
|
21
|
+
post :approve
|
|
22
|
+
end
|
|
21
23
|
collection do
|
|
22
24
|
get :diff
|
|
23
|
-
post :import
|
|
24
25
|
end
|
|
25
26
|
resources :versions, only: [:index, :show], controller: 'memory_versions' do
|
|
26
27
|
member do
|
|
@@ -35,7 +36,6 @@ RailsConsoleAi::Engine.routes.draw do
|
|
|
35
36
|
end
|
|
36
37
|
collection do
|
|
37
38
|
get :diff
|
|
38
|
-
post :import
|
|
39
39
|
end
|
|
40
40
|
resources :versions, only: [:index, :show], controller: 'agent_versions' do
|
|
41
41
|
member do
|
|
@@ -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
|
|
@@ -159,16 +159,10 @@ module RailsConsoleAi
|
|
|
159
159
|
key = agent_key(name)
|
|
160
160
|
existing = load_agent_file(key)
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
frontmatter['max_rounds'] = max_rounds if max_rounds
|
|
167
|
-
frontmatter['model'] = model if model
|
|
168
|
-
tool_list = Array(tools)
|
|
169
|
-
frontmatter['tools'] = tool_list unless tool_list.empty?
|
|
170
|
-
|
|
171
|
-
content = "---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{body}\n"
|
|
162
|
+
content = self.class.dump(
|
|
163
|
+
name: name, description: description, body: body,
|
|
164
|
+
max_rounds: max_rounds, model: model, tools: tools
|
|
165
|
+
)
|
|
172
166
|
@storage.write(key, content)
|
|
173
167
|
|
|
174
168
|
path = @storage.respond_to?(:root_path) ? File.join(@storage.root_path, key) : key
|
|
@@ -210,5 +204,19 @@ module RailsConsoleAi
|
|
|
210
204
|
rescue Psych::SyntaxError
|
|
211
205
|
nil
|
|
212
206
|
end
|
|
207
|
+
|
|
208
|
+
# Inverse of parse: emit a canonical .md (frontmatter + body) string from
|
|
209
|
+
# structured attrs.
|
|
210
|
+
def self.dump(name:, description:, body:, max_rounds: nil, model: nil, tools: nil)
|
|
211
|
+
frontmatter = {
|
|
212
|
+
'name' => name,
|
|
213
|
+
'description' => description
|
|
214
|
+
}
|
|
215
|
+
frontmatter['max_rounds'] = max_rounds if max_rounds
|
|
216
|
+
frontmatter['model'] = model if model
|
|
217
|
+
tool_list = Array(tools)
|
|
218
|
+
frontmatter['tools'] = tool_list unless tool_list.empty?
|
|
219
|
+
"---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{body}\n"
|
|
220
|
+
end
|
|
213
221
|
end
|
|
214
222
|
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'json'
|
|
1
2
|
require 'rails_console_ai/prefixed_io'
|
|
2
3
|
require 'rails_console_ai/session_logger'
|
|
3
4
|
require 'rails_console_ai/channel/api'
|
|
@@ -7,6 +8,8 @@ require 'rails_console_ai/providers/base'
|
|
|
7
8
|
require 'rails_console_ai/executor'
|
|
8
9
|
|
|
9
10
|
module RailsConsoleAi
|
|
11
|
+
class RunnerTimeoutError < StandardError; end
|
|
12
|
+
|
|
10
13
|
# Long-running worker that polls the sessions table for queued agent_api
|
|
11
14
|
# rows, claims them atomically, and runs each in its own Thread via
|
|
12
15
|
# ConversationEngine#one_shot. Started by `rake rails_console_ai:agents`.
|
|
@@ -87,13 +90,20 @@ module RailsConsoleAi
|
|
|
87
90
|
|
|
88
91
|
def spawn(session)
|
|
89
92
|
tag = "[agent/#{session.id}] @#{session.user_name || '?'}"
|
|
90
|
-
|
|
93
|
+
opts = parse_options(session)
|
|
94
|
+
banner_extras = []
|
|
95
|
+
banner_extras << 'thinking' if opts['use_thinking_model']
|
|
96
|
+
if (cap = opts['max_wall_clock_seconds'])
|
|
97
|
+
banner_extras << "cap=#{cap}s"
|
|
98
|
+
end
|
|
99
|
+
banner = banner_extras.empty? ? '' : " (#{banner_extras.join(', ')})"
|
|
100
|
+
puts "#{tag}#{banner} << #{session.query.to_s.strip}"
|
|
91
101
|
|
|
92
102
|
t = Thread.new do
|
|
93
103
|
Thread.current.report_on_exception = false
|
|
94
104
|
Thread.current[:log_prefix] = tag
|
|
95
105
|
begin
|
|
96
|
-
run_one(session)
|
|
106
|
+
run_one(session, opts)
|
|
97
107
|
ensure
|
|
98
108
|
ActiveRecord::Base.clear_active_connections! if defined?(ActiveRecord::Base)
|
|
99
109
|
end
|
|
@@ -101,30 +111,82 @@ module RailsConsoleAi
|
|
|
101
111
|
@mutex.synchronize { @threads[session.id] = t }
|
|
102
112
|
end
|
|
103
113
|
|
|
104
|
-
def
|
|
114
|
+
def parse_options(session)
|
|
115
|
+
raw = session.respond_to?(:options) ? session.options : nil
|
|
116
|
+
return {} if raw.nil? || raw.to_s.empty?
|
|
117
|
+
return raw if raw.is_a?(Hash)
|
|
118
|
+
JSON.parse(raw)
|
|
119
|
+
rescue => e
|
|
120
|
+
warn "AgentRunner: failed to parse session.options (#{e.class}: #{e.message}); ignoring."
|
|
121
|
+
{}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def run_one(session, opts = nil)
|
|
125
|
+
opts ||= parse_options(session)
|
|
105
126
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
106
127
|
channel = Channel::Api.new(user_name: session.user_name)
|
|
107
128
|
sandbox_binding = Object.new.instance_eval { binding }
|
|
108
129
|
engine = ConversationEngine.new(binding_context: sandbox_binding, channel: channel)
|
|
130
|
+
engine.upgrade_to_thinking_model if opts['use_thinking_model']
|
|
109
131
|
|
|
110
|
-
exec_result =
|
|
132
|
+
exec_result = run_with_deadline(opts['max_wall_clock_seconds']) do
|
|
133
|
+
engine.one_shot(session.query, existing_session_id: session.id)
|
|
134
|
+
end
|
|
111
135
|
result_text = compose_result(channel.captured_output, exec_result)
|
|
112
136
|
|
|
137
|
+
if aborted?(session.id)
|
|
138
|
+
puts ">> aborted (result discarded)"
|
|
139
|
+
return
|
|
140
|
+
end
|
|
113
141
|
SessionLogger.update(session.id, status: 'ready', result: result_text)
|
|
114
142
|
|
|
115
143
|
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round
|
|
116
144
|
preview = result_text.to_s.strip.lines.first.to_s.strip
|
|
117
145
|
preview = preview[0, 120] + '…' if preview.length > 120
|
|
118
146
|
puts ">> ready (#{elapsed}ms) #{preview}"
|
|
147
|
+
rescue RunnerTimeoutError => e
|
|
148
|
+
warn ">> TIMEOUT #{e.message}"
|
|
149
|
+
SessionLogger.update(session.id, status: 'failed', error_message: e.message) unless aborted?(session.id)
|
|
119
150
|
rescue => e
|
|
120
151
|
warn ">> FAILED #{e.class}: #{e.message}"
|
|
121
152
|
e.backtrace&.first(5)&.each { |line| warn " #{line}" }
|
|
153
|
+
return if aborted?(session.id)
|
|
122
154
|
SessionLogger.update(session.id,
|
|
123
155
|
status: 'failed',
|
|
124
156
|
error_message: "#{e.class}: #{e.message}\n#{Array(e.backtrace).first(10).join("\n")}"
|
|
125
157
|
)
|
|
126
158
|
end
|
|
127
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
|
+
|
|
166
|
+
# When cap is nil or non-positive, run inline. Otherwise spawn a nested
|
|
167
|
+
# worker thread, join with the deadline, and kill + raise on overshoot.
|
|
168
|
+
# Kept localized (vs Timeout.timeout) so a runaway provider call can't
|
|
169
|
+
# raise from inside our own bookkeeping code.
|
|
170
|
+
def run_with_deadline(cap)
|
|
171
|
+
return yield if cap.nil? || cap.to_f <= 0
|
|
172
|
+
result = nil
|
|
173
|
+
error = nil
|
|
174
|
+
worker = Thread.new do
|
|
175
|
+
Thread.current.report_on_exception = false
|
|
176
|
+
begin
|
|
177
|
+
result = yield
|
|
178
|
+
rescue => e
|
|
179
|
+
error = e
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
if worker.join(cap.to_f).nil?
|
|
183
|
+
worker.kill
|
|
184
|
+
raise RunnerTimeoutError, "exceeded max_wall_clock_seconds (#{cap}s)"
|
|
185
|
+
end
|
|
186
|
+
raise error if error
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
|
|
128
190
|
# Build the `result` payload returned via get_agent_response. The
|
|
129
191
|
# LLM's prose lands in the channel's captured_output; the value the
|
|
130
192
|
# generated code returned lands in exec_result. Without including the
|
|
@@ -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"
|