console_agent 0.7.0 → 0.8.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/README.md +40 -0
- data/app/controllers/console_agent/application_controller.rb +12 -8
- data/lib/console_agent/configuration.rb +36 -4
- data/lib/console_agent/providers/anthropic.rb +1 -1
- data/lib/console_agent/providers/openai.rb +1 -1
- data/lib/console_agent/repl.rb +130 -6
- data/lib/console_agent/version.rb +1 -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: fdcfb3c48b2f8421b2187980a453b80324e6dec40ab80e8e55aa1a938355c79c
|
|
4
|
+
data.tar.gz: 12fec02740fde7a87bb81e26dd6857263c96f9ebe5a8402040c72279e882e31f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7af9a3c4fdbdf71abb7452d8e6747ba2b22c64cd08d123b761c5ca7ebb870c3ba9a01e458defe0308dcbf9435ec71879f7f3562503defdfd54e2026d3069679d
|
|
7
|
+
data.tar.gz: c84e401d6b6f5c6c7840b5d701d3a0e653ba3ea46651141ae5101b2c93bdca35487566786a87e284dbba97f902c5dad597a00b8dc9fce6e1c01885b01e99a48d
|
data/README.md
CHANGED
|
@@ -77,15 +77,21 @@ end
|
|
|
77
77
|
| `/auto` | Toggle auto-execute (skip confirmations) |
|
|
78
78
|
| `/compact` | Compress history into a summary (saves tokens) |
|
|
79
79
|
| `/usage` | Show token stats |
|
|
80
|
+
| `/cost` | Show per-model cost breakdown |
|
|
81
|
+
| `/think` | Upgrade to thinking model (Opus) for the rest of the session |
|
|
80
82
|
| `/debug` | Toggle raw API output |
|
|
81
83
|
| `/name <label>` | Name the session for easy resume |
|
|
82
84
|
|
|
83
85
|
Prefix input with `>` to run Ruby directly (no LLM round-trip). The result is added to conversation context.
|
|
84
86
|
|
|
87
|
+
Say "think harder" in any query to auto-upgrade to the thinking model for that session. After 5+ tool rounds, you'll also be prompted to switch.
|
|
88
|
+
|
|
85
89
|
## Features
|
|
86
90
|
|
|
87
91
|
- **Tool use** — AI introspects your schema, models, files, and code to write accurate queries
|
|
88
92
|
- **Multi-step plans** — complex tasks are broken into steps, executed sequentially with `step1`/`step2` references
|
|
93
|
+
- **Two-tier models** — defaults to Sonnet for speed/cost; `/think` upgrades to Opus when you need it
|
|
94
|
+
- **Cost tracking** — `/cost` shows per-model token usage and estimated spend
|
|
89
95
|
- **Memories** — AI saves what it learns about your app across sessions
|
|
90
96
|
- **App guide** — `ai_init` generates a guide injected into every system prompt
|
|
91
97
|
- **Sessions** — name, list, and resume interactive conversations (`ai_setup` to enable)
|
|
@@ -98,9 +104,43 @@ ConsoleAgent.configure do |config|
|
|
|
98
104
|
config.provider = :anthropic # or :openai
|
|
99
105
|
config.auto_execute = false # true to skip confirmations
|
|
100
106
|
config.session_logging = true # requires ai_setup
|
|
107
|
+
config.model = 'claude-sonnet-4-6' # model used by /think (default)
|
|
108
|
+
config.thinking_model = 'claude-opus-4-6' # model used by /think (default)
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The default model is `claude-sonnet-4-6` (Anthropic) or `gpt-5.3-codex` (OpenAI). The thinking model defaults to `claude-opus-4-6` and is activated via `/think` or by saying "think harder".
|
|
113
|
+
|
|
114
|
+
## Web UI Authentication
|
|
115
|
+
|
|
116
|
+
The engine mounts a session viewer at `/console_agent`. By default it's open — you can protect it with basic auth or a custom authentication function.
|
|
117
|
+
|
|
118
|
+
### Basic Auth
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
ConsoleAgent.configure do |config|
|
|
122
|
+
config.admin_username = 'admin'
|
|
123
|
+
config.admin_password = ENV['CONSOLE_AGENT_PASSWORD']
|
|
101
124
|
end
|
|
102
125
|
```
|
|
103
126
|
|
|
127
|
+
### Custom Authentication
|
|
128
|
+
|
|
129
|
+
For apps with their own auth system, pass a proc to `authenticate`. It runs in the controller context, so you have access to `session`, `request`, `redirect_to`, etc.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
ConsoleAgent.configure do |config|
|
|
133
|
+
config.authenticate = proc {
|
|
134
|
+
user = User.find_by(id: session[:user_id])
|
|
135
|
+
unless user&.admin?
|
|
136
|
+
redirect_to '/login'
|
|
137
|
+
end
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
When `authenticate` is set, `admin_username` / `admin_password` are ignored.
|
|
143
|
+
|
|
104
144
|
## Requirements
|
|
105
145
|
|
|
106
146
|
Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
|
|
@@ -2,19 +2,23 @@ module ConsoleAgent
|
|
|
2
2
|
class ApplicationController < ActionController::Base
|
|
3
3
|
protect_from_forgery with: :exception
|
|
4
4
|
|
|
5
|
-
before_action :
|
|
5
|
+
before_action :console_agent_authenticate!
|
|
6
6
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
def console_agent_authenticate!
|
|
10
|
+
if (auth = ConsoleAgent.configuration.authenticate)
|
|
11
|
+
instance_exec(&auth)
|
|
12
|
+
else
|
|
13
|
+
username = ConsoleAgent.configuration.admin_username
|
|
14
|
+
password = ConsoleAgent.configuration.admin_password
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
return unless username && password
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
authenticate_or_request_with_http_basic('ConsoleAgent Admin') do |u, p|
|
|
19
|
+
ActiveSupport::SecurityUtils.secure_compare(u, username) &
|
|
20
|
+
ActiveSupport::SecurityUtils.secure_compare(p, password)
|
|
21
|
+
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -2,29 +2,44 @@ module ConsoleAgent
|
|
|
2
2
|
class Configuration
|
|
3
3
|
PROVIDERS = %i[anthropic openai].freeze
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
PRICING = {
|
|
6
|
+
'claude-sonnet-4-6' => { input: 3.0 / 1_000_000, output: 15.0 / 1_000_000 },
|
|
7
|
+
'claude-opus-4-6' => { input: 15.0 / 1_000_000, output: 75.0 / 1_000_000 },
|
|
8
|
+
'claude-haiku-4-5-20251001' => { input: 0.80 / 1_000_000, output: 4.0 / 1_000_000 },
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
DEFAULT_MAX_TOKENS = {
|
|
12
|
+
'claude-sonnet-4-6' => 16_000,
|
|
13
|
+
'claude-haiku-4-5-20251001' => 16_000,
|
|
14
|
+
'claude-opus-4-6' => 4_096,
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_accessor :provider, :api_key, :model, :thinking_model, :max_tokens,
|
|
6
18
|
:auto_execute, :temperature,
|
|
7
19
|
:timeout, :debug, :max_tool_rounds,
|
|
8
20
|
:storage_adapter, :memories_enabled,
|
|
9
21
|
:session_logging, :connection_class,
|
|
10
|
-
:admin_username, :admin_password
|
|
22
|
+
:admin_username, :admin_password,
|
|
23
|
+
:authenticate
|
|
11
24
|
|
|
12
25
|
def initialize
|
|
13
26
|
@provider = :anthropic
|
|
14
27
|
@api_key = nil
|
|
15
28
|
@model = nil
|
|
16
|
-
@
|
|
29
|
+
@thinking_model = nil
|
|
30
|
+
@max_tokens = nil
|
|
17
31
|
@auto_execute = false
|
|
18
32
|
@temperature = 0.2
|
|
19
33
|
@timeout = 30
|
|
20
34
|
@debug = false
|
|
21
|
-
@max_tool_rounds =
|
|
35
|
+
@max_tool_rounds = 200
|
|
22
36
|
@storage_adapter = nil
|
|
23
37
|
@memories_enabled = true
|
|
24
38
|
@session_logging = true
|
|
25
39
|
@connection_class = nil
|
|
26
40
|
@admin_username = nil
|
|
27
41
|
@admin_password = nil
|
|
42
|
+
@authenticate = nil
|
|
28
43
|
end
|
|
29
44
|
|
|
30
45
|
def resolved_api_key
|
|
@@ -41,6 +56,23 @@ module ConsoleAgent
|
|
|
41
56
|
def resolved_model
|
|
42
57
|
return @model if @model && !@model.empty?
|
|
43
58
|
|
|
59
|
+
case @provider
|
|
60
|
+
when :anthropic
|
|
61
|
+
'claude-sonnet-4-6'
|
|
62
|
+
when :openai
|
|
63
|
+
'gpt-5.3-codex'
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def resolved_max_tokens
|
|
68
|
+
return @max_tokens if @max_tokens
|
|
69
|
+
|
|
70
|
+
DEFAULT_MAX_TOKENS.fetch(resolved_model, 4096)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def resolved_thinking_model
|
|
74
|
+
return @thinking_model if @thinking_model && !@thinking_model.empty?
|
|
75
|
+
|
|
44
76
|
case @provider
|
|
45
77
|
when :anthropic
|
|
46
78
|
'claude-opus-4-6'
|
data/lib/console_agent/repl.rb
CHANGED
|
@@ -11,6 +11,7 @@ module ConsoleAgent
|
|
|
11
11
|
@history = []
|
|
12
12
|
@total_input_tokens = 0
|
|
13
13
|
@total_output_tokens = 0
|
|
14
|
+
@token_usage = Hash.new { |h, k| h[k] = { input: 0, output: 0 } }
|
|
14
15
|
@input_history = []
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -209,6 +210,7 @@ module ConsoleAgent
|
|
|
209
210
|
@history = []
|
|
210
211
|
@total_input_tokens = 0
|
|
211
212
|
@total_output_tokens = 0
|
|
213
|
+
@token_usage = Hash.new { |h, k| h[k] = { input: 0, output: 0 } }
|
|
212
214
|
@interactive_query = nil
|
|
213
215
|
@interactive_session_id = nil
|
|
214
216
|
@interactive_session_name = nil
|
|
@@ -224,7 +226,7 @@ module ConsoleAgent
|
|
|
224
226
|
name_display = @interactive_session_name ? " (#{@interactive_session_name})" : ""
|
|
225
227
|
# Write banner to real stdout (bypass TeeIO) so it doesn't accumulate on resume
|
|
226
228
|
@interactive_old_stdout.puts "\e[36mConsoleAgent interactive mode#{name_display}. Type 'exit' or 'quit' to leave.\e[0m"
|
|
227
|
-
@interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | > code
|
|
229
|
+
@interactive_old_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle) | > code | /usage | /cost | /compact | /think | /name <label>\e[0m"
|
|
228
230
|
|
|
229
231
|
# Bind Shift-Tab to insert /auto command and submit
|
|
230
232
|
if Readline.respond_to?(:parse_and_bind)
|
|
@@ -263,6 +265,16 @@ module ConsoleAgent
|
|
|
263
265
|
next
|
|
264
266
|
end
|
|
265
267
|
|
|
268
|
+
if input == '/cost'
|
|
269
|
+
display_cost_summary
|
|
270
|
+
next
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if input == '/think'
|
|
274
|
+
upgrade_to_thinking_model
|
|
275
|
+
next
|
|
276
|
+
end
|
|
277
|
+
|
|
266
278
|
if input.start_with?('/name')
|
|
267
279
|
name = input.sub('/name', '').strip
|
|
268
280
|
if name.empty?
|
|
@@ -314,6 +326,11 @@ module ConsoleAgent
|
|
|
314
326
|
# Add to Readline history (avoid consecutive duplicates)
|
|
315
327
|
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
316
328
|
|
|
329
|
+
# Auto-upgrade to thinking model on "think harder" phrases
|
|
330
|
+
if input =~ /think\s*harder/i
|
|
331
|
+
upgrade_to_thinking_model
|
|
332
|
+
end
|
|
333
|
+
|
|
317
334
|
@interactive_query ||= input
|
|
318
335
|
@history << { role: :user, content: input }
|
|
319
336
|
|
|
@@ -365,6 +382,20 @@ module ConsoleAgent
|
|
|
365
382
|
def send_and_execute
|
|
366
383
|
begin
|
|
367
384
|
result, tool_messages = send_query(nil, conversation: @history)
|
|
385
|
+
rescue Providers::ProviderError => e
|
|
386
|
+
if e.message.include?("prompt is too long") && @history.length >= 6
|
|
387
|
+
$stdout.puts "\e[33m Context limit reached. Auto-compacting history...\e[0m"
|
|
388
|
+
compact_history
|
|
389
|
+
begin
|
|
390
|
+
result, tool_messages = send_query(nil, conversation: @history)
|
|
391
|
+
rescue Providers::ProviderError => e2
|
|
392
|
+
$stderr.puts "\e[31m Still too large after compaction: #{e2.message}\e[0m"
|
|
393
|
+
return :error
|
|
394
|
+
end
|
|
395
|
+
else
|
|
396
|
+
$stderr.puts "\e[31mConsoleAgent Error: #{e.class}: #{e.message}\e[0m"
|
|
397
|
+
return :error
|
|
398
|
+
end
|
|
368
399
|
rescue Interrupt
|
|
369
400
|
$stdout.puts "\n\e[33m Aborted.\e[0m"
|
|
370
401
|
return :interrupted
|
|
@@ -533,8 +564,18 @@ module ConsoleAgent
|
|
|
533
564
|
last_tool_names = []
|
|
534
565
|
|
|
535
566
|
exhausted = false
|
|
567
|
+
thinking_suggested = false
|
|
536
568
|
|
|
537
569
|
max_rounds.times do |round|
|
|
570
|
+
if round == 5 && !thinking_suggested && !on_thinking_model?
|
|
571
|
+
thinking_suggested = true
|
|
572
|
+
thinking_name = ConsoleAgent.configuration.resolved_thinking_model
|
|
573
|
+
$stdout.puts "\e[33m This query is using many tool rounds. Switch to thinking model (#{thinking_name})? [y/N]\e[0m"
|
|
574
|
+
answer = Readline.readline(" ", false).to_s.strip.downcase
|
|
575
|
+
if answer == 'y'
|
|
576
|
+
upgrade_to_thinking_model
|
|
577
|
+
end
|
|
578
|
+
end
|
|
538
579
|
if round == 0
|
|
539
580
|
$stdout.puts "\e[2m Thinking...\e[0m"
|
|
540
581
|
else
|
|
@@ -547,8 +588,22 @@ module ConsoleAgent
|
|
|
547
588
|
$stdout.puts "\e[2m #{llm_status(round, messages, total_input, last_thinking, last_tool_names)}\e[0m"
|
|
548
589
|
end
|
|
549
590
|
|
|
550
|
-
|
|
551
|
-
|
|
591
|
+
begin
|
|
592
|
+
result = with_escape_monitoring do
|
|
593
|
+
provider.chat_with_tools(messages, tools: tools, system_prompt: active_system_prompt)
|
|
594
|
+
end
|
|
595
|
+
rescue Providers::ProviderError => e
|
|
596
|
+
if e.message.include?("prompt is too long") && messages.length >= 6
|
|
597
|
+
$stdout.puts "\e[33m Context limit hit mid-session. Compacting messages...\e[0m"
|
|
598
|
+
messages = compact_messages(messages)
|
|
599
|
+
unless @_retried_compact
|
|
600
|
+
@_retried_compact = true
|
|
601
|
+
retry
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
raise
|
|
605
|
+
ensure
|
|
606
|
+
@_retried_compact = nil
|
|
552
607
|
end
|
|
553
608
|
total_input += result.input_tokens || 0
|
|
554
609
|
total_output += result.output_tokens || 0
|
|
@@ -776,6 +831,10 @@ module ConsoleAgent
|
|
|
776
831
|
def track_usage(result)
|
|
777
832
|
@total_input_tokens += result.input_tokens || 0
|
|
778
833
|
@total_output_tokens += result.output_tokens || 0
|
|
834
|
+
|
|
835
|
+
model = ConsoleAgent.configuration.resolved_model
|
|
836
|
+
@token_usage[model][:input] += result.input_tokens || 0
|
|
837
|
+
@token_usage[model][:output] += result.output_tokens || 0
|
|
779
838
|
end
|
|
780
839
|
|
|
781
840
|
def display_usage(result, show_session: false)
|
|
@@ -883,12 +942,61 @@ module ConsoleAgent
|
|
|
883
942
|
$stdout.puts "\e[2m[session totals — in: #{@total_input_tokens} | out: #{@total_output_tokens} | total: #{@total_input_tokens + @total_output_tokens}]\e[0m"
|
|
884
943
|
end
|
|
885
944
|
|
|
945
|
+
def display_cost_summary
|
|
946
|
+
if @token_usage.empty?
|
|
947
|
+
$stdout.puts "\e[2m No usage yet.\e[0m"
|
|
948
|
+
return
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
total_cost = 0.0
|
|
952
|
+
$stdout.puts "\e[36m Cost estimate:\e[0m"
|
|
953
|
+
|
|
954
|
+
@token_usage.each do |model, usage|
|
|
955
|
+
pricing = Configuration::PRICING[model]
|
|
956
|
+
input_str = "in: #{format_tokens(usage[:input])}"
|
|
957
|
+
output_str = "out: #{format_tokens(usage[:output])}"
|
|
958
|
+
|
|
959
|
+
if pricing
|
|
960
|
+
cost = (usage[:input] * pricing[:input]) + (usage[:output] * pricing[:output])
|
|
961
|
+
total_cost += cost
|
|
962
|
+
$stdout.puts "\e[2m #{model}: #{input_str} #{output_str} ~$#{'%.2f' % cost}\e[0m"
|
|
963
|
+
else
|
|
964
|
+
$stdout.puts "\e[2m #{model}: #{input_str} #{output_str} (pricing unknown)\e[0m"
|
|
965
|
+
end
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
$stdout.puts "\e[36m Total: ~$#{'%.2f' % total_cost}\e[0m"
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
def upgrade_to_thinking_model
|
|
972
|
+
config = ConsoleAgent.configuration
|
|
973
|
+
current = config.resolved_model
|
|
974
|
+
thinking = config.resolved_thinking_model
|
|
975
|
+
|
|
976
|
+
if current == thinking
|
|
977
|
+
$stdout.puts "\e[36m Already using thinking model (#{current}).\e[0m"
|
|
978
|
+
else
|
|
979
|
+
config.model = thinking
|
|
980
|
+
@provider = nil
|
|
981
|
+
$stdout.puts "\e[36m Switched to thinking model: #{thinking}\e[0m"
|
|
982
|
+
end
|
|
983
|
+
end
|
|
984
|
+
|
|
985
|
+
def on_thinking_model?
|
|
986
|
+
config = ConsoleAgent.configuration
|
|
987
|
+
config.resolved_model == config.resolved_thinking_model
|
|
988
|
+
end
|
|
989
|
+
|
|
886
990
|
def warn_if_history_large
|
|
887
991
|
chars = @history.sum { |m| m[:content].to_s.length }
|
|
888
|
-
return if chars < 50_000 || @compact_warned
|
|
889
992
|
|
|
890
|
-
@
|
|
891
|
-
|
|
993
|
+
if chars > 120_000 && @history.length >= 6
|
|
994
|
+
$stdout.puts "\e[33m Context growing large (~#{format_tokens(chars)} chars). Auto-compacting...\e[0m"
|
|
995
|
+
compact_history
|
|
996
|
+
elsif chars > 50_000 && !@compact_warned
|
|
997
|
+
@compact_warned = true
|
|
998
|
+
$stdout.puts "\e[33m Conversation is getting large (~#{format_tokens(chars)} chars). Consider running /compact to reduce context size.\e[0m"
|
|
999
|
+
end
|
|
892
1000
|
end
|
|
893
1001
|
|
|
894
1002
|
def compact_history
|
|
@@ -941,6 +1049,22 @@ module ConsoleAgent
|
|
|
941
1049
|
end
|
|
942
1050
|
end
|
|
943
1051
|
|
|
1052
|
+
def compact_messages(messages)
|
|
1053
|
+
return messages if messages.length < 6
|
|
1054
|
+
|
|
1055
|
+
to_summarize = messages[0...-4]
|
|
1056
|
+
to_keep = messages[-4..]
|
|
1057
|
+
|
|
1058
|
+
history_text = to_summarize.map { |m| "#{m[:role]}: #{m[:content].to_s[0..500]}" }.join("\n\n")
|
|
1059
|
+
|
|
1060
|
+
summary_result = provider.chat(
|
|
1061
|
+
[{ role: :user, content: "Summarize this conversation context concisely, preserving key facts, IDs, and findings:\n\n#{history_text}" }],
|
|
1062
|
+
system_prompt: "You are a conversation summarizer. Be concise but preserve all actionable information."
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
[{ role: :user, content: "CONTEXT SUMMARY:\n#{summary_result.text}" }] + to_keep
|
|
1066
|
+
end
|
|
1067
|
+
|
|
944
1068
|
def display_exit_info
|
|
945
1069
|
display_session_summary
|
|
946
1070
|
if @interactive_session_id
|