console_agent 0.1.0 → 0.2.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 +126 -26
- data/lib/console_agent/configuration.rb +5 -2
- data/lib/console_agent/console_methods.rb +43 -0
- data/lib/console_agent/context_builder.rb +29 -0
- data/lib/console_agent/executor.rb +62 -0
- data/lib/console_agent/repl.rb +65 -9
- data/lib/console_agent/storage/base.rb +27 -0
- data/lib/console_agent/storage/file_storage.rb +63 -0
- data/lib/console_agent/tools/memory_tools.rb +136 -0
- data/lib/console_agent/tools/registry.rb +73 -1
- data/lib/console_agent/version.rb +1 -1
- data/lib/console_agent.rb +18 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53925e32cd4919e550bfdc081ede866613ce3ea18a585aca6cb81d1687ce3901
|
|
4
|
+
data.tar.gz: ae601be1b81ff2fcb89addda1c008c0646118f2af21c9009a1792556ffd9b4ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54ec67e116e4a05de7a5d915f93dc7babf2d64d2b008bc507718dce92d58ad032beeaa1d7710d8190943454f937f91780ff5614e44f5ef173ab5feff6dddb339
|
|
7
|
+
data.tar.gz: f12af224368e44b149d740f8d0b2dee4a38ce86564ee9337c5a8ed569a83bb387d3edcf2ae76142d4b510f507c5e6d64d82aab70c16eb94f66d83e3811eb0cd6
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
An AI-powered assistant for your Rails console. Ask questions in plain English, get executable Ruby code.
|
|
4
4
|
|
|
5
|
+
It's like Claude Code embedded in your Rails Console.
|
|
6
|
+
|
|
5
7
|
ConsoleAgent connects your `rails console` to an LLM (Claude or GPT) and gives it tools to introspect your app's database schema, models, and source code. It figures out what it needs on its own, so you don't pay for 50K tokens of context on every query.
|
|
6
8
|
|
|
7
9
|
## Quick Start
|
|
@@ -19,19 +21,29 @@ bundle install
|
|
|
19
21
|
rails generate console_agent:install
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
Set your API key
|
|
24
|
+
Set your API key (pick one):
|
|
23
25
|
|
|
24
26
|
```bash
|
|
27
|
+
# Option A: environment variable
|
|
25
28
|
export ANTHROPIC_API_KEY=sk-ant-...
|
|
26
|
-
|
|
29
|
+
|
|
30
|
+
# Option B: in the initializer
|
|
31
|
+
# config.api_key = 'sk-ant-...'
|
|
27
32
|
```
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
Open a console and go:
|
|
30
35
|
|
|
31
36
|
```ruby
|
|
37
|
+
rails console
|
|
32
38
|
ai "show me all users who signed up this week"
|
|
33
39
|
```
|
|
34
40
|
|
|
41
|
+
You can also set or change your API key at runtime in the console:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
ConsoleAgent.configure { |c| c.api_key = 'sk-ant-...' }
|
|
45
|
+
```
|
|
46
|
+
|
|
35
47
|
## Console Commands
|
|
36
48
|
|
|
37
49
|
| Command | Description |
|
|
@@ -137,12 +149,12 @@ ConsoleAgent.configure do |config|
|
|
|
137
149
|
end
|
|
138
150
|
```
|
|
139
151
|
|
|
140
|
-
|
|
152
|
+
All settings can be changed at runtime in the console:
|
|
141
153
|
|
|
142
154
|
```ruby
|
|
155
|
+
ConsoleAgent.configure { |c| c.api_key = 'sk-ant-...' }
|
|
143
156
|
ConsoleAgent.configure { |c| c.debug = true }
|
|
144
|
-
ConsoleAgent.configure { |c| c.provider = :openai }
|
|
145
|
-
ENV['OPENAI_API_KEY'] = 'sk-...'
|
|
157
|
+
ConsoleAgent.configure { |c| c.provider = :openai; c.api_key = 'sk-...' }
|
|
146
158
|
```
|
|
147
159
|
|
|
148
160
|
## Context Modes
|
|
@@ -176,9 +188,111 @@ ConsoleAgent.configure { |c| c.context_mode = :full }
|
|
|
176
188
|
| `list_files` | Ruby files in a directory |
|
|
177
189
|
| `read_file` | Read a source file (capped at 200 lines) |
|
|
178
190
|
| `search_code` | Grep for a pattern across Ruby files |
|
|
191
|
+
| `ask_user` | Ask the console user a clarifying question |
|
|
192
|
+
| `save_memory` | Persist a learned fact for future sessions |
|
|
193
|
+
| `recall_memories` | Search saved memories |
|
|
194
|
+
| `load_skill` | Load full instructions for a skill |
|
|
179
195
|
|
|
180
196
|
The LLM decides which tools to call based on your question. You can see the tool calls happening in real time.
|
|
181
197
|
|
|
198
|
+
## Memories & Skills
|
|
199
|
+
|
|
200
|
+
ConsoleAgent can remember what it learns about your codebase across sessions and load reusable skill instructions on demand.
|
|
201
|
+
|
|
202
|
+
### Memories
|
|
203
|
+
|
|
204
|
+
When the AI investigates something complex (like how sharding works in your app), it can save what it learned for next time. Memories are stored in `.console_agent/memories.yml` in your Rails app.
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
ai> how many users are on this shard?
|
|
208
|
+
Thinking...
|
|
209
|
+
-> search_code("shard")
|
|
210
|
+
Found 50 matches
|
|
211
|
+
-> read_file("config/initializers/sharding.rb")
|
|
212
|
+
202 lines
|
|
213
|
+
-> save_memory("Sharding architecture")
|
|
214
|
+
Memory saved: "Sharding architecture"
|
|
215
|
+
|
|
216
|
+
Each shard is a separate database. User.count returns the count for the current shard only.
|
|
217
|
+
|
|
218
|
+
User.count
|
|
219
|
+
|
|
220
|
+
[tokens in: 2,340 | out: 120 | total: 2,460]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Next session, the AI already knows:
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
ai> count users on this shard
|
|
227
|
+
Thinking...
|
|
228
|
+
|
|
229
|
+
User.count
|
|
230
|
+
|
|
231
|
+
[tokens in: 580 | out: 45 | total: 625]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The memory file is human-editable YAML:
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
# .console_agent/memories.yml
|
|
238
|
+
memories:
|
|
239
|
+
- id: mem_1709123456_42
|
|
240
|
+
name: Sharding architecture
|
|
241
|
+
description: "App uses database-per-shard. Each shard is a separate DB. User.count only counts the current shard."
|
|
242
|
+
tags: [database, sharding]
|
|
243
|
+
created_at: "2026-02-27T10:30:00Z"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
You can commit this file to your repo so the whole team benefits, or gitignore it for personal use.
|
|
247
|
+
|
|
248
|
+
### Skills
|
|
249
|
+
|
|
250
|
+
Skills are reusable instruction files that teach the AI how to handle specific tasks. Store them in `.console_agent/skills/` as markdown files with YAML frontmatter:
|
|
251
|
+
|
|
252
|
+
```markdown
|
|
253
|
+
# .console_agent/skills/sharding.md
|
|
254
|
+
---
|
|
255
|
+
name: Sharded database queries
|
|
256
|
+
description: How to query across database shards. Use when user asks about counting records across shards or shard-specific operations.
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Instructions
|
|
260
|
+
|
|
261
|
+
This app uses Apartment with one database per shard.
|
|
262
|
+
- `User.count` only counts the current shard
|
|
263
|
+
- To count across all shards: `Shard.all.sum { |s| s.switch { User.count } }`
|
|
264
|
+
- Current shard: `Apartment::Tenant.current`
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Only the skill name and description are sent to the AI on every request (~100 tokens per skill). The full instructions are loaded on demand via the `load_skill` tool only when relevant.
|
|
268
|
+
|
|
269
|
+
### Storage
|
|
270
|
+
|
|
271
|
+
By default, memories and skills are stored on the filesystem at `Rails.root/.console_agent/`. This works for development and for production environments with a persistent filesystem.
|
|
272
|
+
|
|
273
|
+
For containers or other environments where the filesystem is ephemeral, you have two options:
|
|
274
|
+
|
|
275
|
+
**Option A: Commit to your repo.** Create `.console_agent/memories.yml` and `.console_agent/skills/*.md` in your codebase. They'll be baked into your Docker image.
|
|
276
|
+
|
|
277
|
+
**Option B: Custom storage adapter.** Implement the storage interface and plug it in:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
ConsoleAgent.configure do |config|
|
|
281
|
+
config.storage_adapter = MyS3Storage.new(bucket: 'my-bucket', prefix: 'console_agent/')
|
|
282
|
+
end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
A storage adapter just needs four methods: `read(key)`, `write(key, content)`, `list(pattern)`, and `exists?(key)`.
|
|
286
|
+
|
|
287
|
+
To disable memories or skills:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
ConsoleAgent.configure do |config|
|
|
291
|
+
config.memories_enabled = false
|
|
292
|
+
config.skills_enabled = false
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
182
296
|
## Providers
|
|
183
297
|
|
|
184
298
|
### Anthropic (default)
|
|
@@ -196,34 +310,20 @@ ConsoleAgent.configure do |config|
|
|
|
196
310
|
end
|
|
197
311
|
```
|
|
198
312
|
|
|
199
|
-
##
|
|
313
|
+
## Local Development
|
|
200
314
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
In `docker-compose.yml`:
|
|
204
|
-
|
|
205
|
-
```yaml
|
|
206
|
-
volumes:
|
|
207
|
-
- /path/to/console_agent:/console_agent
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
In `Gemfile`:
|
|
315
|
+
To develop the gem locally against a Rails app, use a path reference in your Gemfile:
|
|
211
316
|
|
|
212
317
|
```ruby
|
|
213
|
-
gem 'console_agent', path: '/console_agent'
|
|
318
|
+
gem 'console_agent', path: '/path/to/console_agent'
|
|
214
319
|
```
|
|
215
320
|
|
|
216
|
-
|
|
321
|
+
Switch back to the published gem when you're done:
|
|
217
322
|
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
echo "module ConsoleAgent; VERSION = '0.1.0'; end" > /console_agent/lib/console_agent/version.rb && \
|
|
221
|
-
echo "require 'console_agent/version'" > /console_agent/lib/console_agent.rb && \
|
|
222
|
-
printf "require_relative 'lib/console_agent/version'\nGem::Specification.new do |s|\n s.name = 'console_agent'\n s.version = ConsoleAgent::VERSION\n s.summary = 'stub'\n s.authors = ['x']\n s.files = ['lib/console_agent.rb']\n s.add_dependency 'rails', '>= 5.0'\n s.add_dependency 'faraday', '>= 1.0'\nend\n" > /console_agent/console_agent.gemspec
|
|
323
|
+
```ruby
|
|
324
|
+
gem 'console_agent'
|
|
223
325
|
```
|
|
224
326
|
|
|
225
|
-
The volume mount overwrites the stub at runtime with the real source.
|
|
226
|
-
|
|
227
327
|
## Requirements
|
|
228
328
|
|
|
229
329
|
- Ruby >= 2.5
|
|
@@ -5,7 +5,8 @@ module ConsoleAgent
|
|
|
5
5
|
|
|
6
6
|
attr_accessor :provider, :api_key, :model, :max_tokens,
|
|
7
7
|
:auto_execute, :context_mode, :temperature,
|
|
8
|
-
:timeout, :debug, :max_tool_rounds
|
|
8
|
+
:timeout, :debug, :max_tool_rounds,
|
|
9
|
+
:storage_adapter, :memories_enabled
|
|
9
10
|
|
|
10
11
|
def initialize
|
|
11
12
|
@provider = :anthropic
|
|
@@ -17,7 +18,9 @@ module ConsoleAgent
|
|
|
17
18
|
@temperature = 0.2
|
|
18
19
|
@timeout = 30
|
|
19
20
|
@debug = false
|
|
20
|
-
@max_tool_rounds =
|
|
21
|
+
@max_tool_rounds = 100
|
|
22
|
+
@storage_adapter = nil
|
|
23
|
+
@memories_enabled = true
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def resolved_api_key
|
|
@@ -4,6 +4,48 @@ module ConsoleAgent
|
|
|
4
4
|
ConsoleAgent.status
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
def ai_memories(n = nil)
|
|
8
|
+
require 'yaml'
|
|
9
|
+
require 'console_agent/tools/memory_tools'
|
|
10
|
+
storage = ConsoleAgent.storage
|
|
11
|
+
keys = storage.list('memories/*.md').sort
|
|
12
|
+
|
|
13
|
+
if keys.empty?
|
|
14
|
+
$stdout.puts "\e[2mNo memories stored yet.\e[0m"
|
|
15
|
+
return nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
memories = keys.filter_map do |key|
|
|
19
|
+
content = storage.read(key)
|
|
20
|
+
next if content.nil? || content.strip.empty?
|
|
21
|
+
next unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m
|
|
22
|
+
fm = YAML.safe_load($1, permitted_classes: [Time, Date]) || {}
|
|
23
|
+
fm.merge('description' => $2.strip, 'file' => key)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if memories.empty?
|
|
27
|
+
$stdout.puts "\e[2mNo memories stored yet.\e[0m"
|
|
28
|
+
return nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
shown = n ? memories.last(n) : memories.last(5)
|
|
32
|
+
total = memories.length
|
|
33
|
+
|
|
34
|
+
$stdout.puts "\e[36m[Memories — showing last #{shown.length} of #{total}]\e[0m"
|
|
35
|
+
shown.each do |m|
|
|
36
|
+
$stdout.puts "\e[33m #{m['name']}\e[0m"
|
|
37
|
+
$stdout.puts "\e[2m #{m['description']}\e[0m"
|
|
38
|
+
tags = Array(m['tags'])
|
|
39
|
+
$stdout.puts "\e[2m tags: #{tags.join(', ')}\e[0m" unless tags.empty?
|
|
40
|
+
$stdout.puts
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
path = storage.respond_to?(:root_path) ? File.join(storage.root_path, 'memories') : 'memories/'
|
|
44
|
+
$stdout.puts "\e[2mStored in: #{path}/\e[0m"
|
|
45
|
+
$stdout.puts "\e[2mUse ai_memories(n) to show last n.\e[0m"
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
7
49
|
def ai(query = nil)
|
|
8
50
|
if query.nil?
|
|
9
51
|
$stderr.puts "\e[33mUsage: ai \"your question here\"\e[0m"
|
|
@@ -11,6 +53,7 @@ module ConsoleAgent
|
|
|
11
53
|
$stderr.puts "\e[33m ai! \"query\" - enter interactive mode (or ai! with no args)\e[0m"
|
|
12
54
|
$stderr.puts "\e[33m ai? \"query\" - explain only, no execution\e[0m"
|
|
13
55
|
$stderr.puts "\e[33m ai_status - show current configuration\e[0m"
|
|
56
|
+
$stderr.puts "\e[33m ai_memories - show recent memories (ai_memories(n) for last n)\e[0m"
|
|
14
57
|
return nil
|
|
15
58
|
end
|
|
16
59
|
|
|
@@ -30,6 +30,7 @@ module ConsoleAgent
|
|
|
30
30
|
parts = []
|
|
31
31
|
parts << smart_system_instructions
|
|
32
32
|
parts << environment_context
|
|
33
|
+
parts << memory_context
|
|
33
34
|
parts.compact.join("\n\n")
|
|
34
35
|
end
|
|
35
36
|
|
|
@@ -48,6 +49,17 @@ module ConsoleAgent
|
|
|
48
49
|
you need specific information to write accurate code — such as which user they are, which
|
|
49
50
|
record to target, or what value to use.
|
|
50
51
|
|
|
52
|
+
You have memory tools to persist what you learn across sessions:
|
|
53
|
+
- save_memory: persist facts or procedures you learn about this codebase.
|
|
54
|
+
If a memory with the same name already exists, it will be updated in place.
|
|
55
|
+
- delete_memory: remove a memory by name
|
|
56
|
+
- recall_memories: search your saved memories for details
|
|
57
|
+
|
|
58
|
+
IMPORTANT: Check the Memories section below BEFORE answering. If a memory is relevant,
|
|
59
|
+
use recall_memories to get full details and apply that knowledge to your answer.
|
|
60
|
+
When you use a memory, mention it briefly (e.g. "Based on what I know about sharding...").
|
|
61
|
+
When you discover important patterns about this app, save them as memories.
|
|
62
|
+
|
|
51
63
|
RULES:
|
|
52
64
|
- Give ONE concise answer. Do not offer multiple alternatives or variations.
|
|
53
65
|
- Respond with a single ```ruby code block that directly answers the question.
|
|
@@ -187,6 +199,23 @@ module ConsoleAgent
|
|
|
187
199
|
nil
|
|
188
200
|
end
|
|
189
201
|
|
|
202
|
+
def memory_context
|
|
203
|
+
return nil unless @config.memories_enabled
|
|
204
|
+
|
|
205
|
+
require 'console_agent/tools/memory_tools'
|
|
206
|
+
summaries = Tools::MemoryTools.new.memory_summaries
|
|
207
|
+
return nil if summaries.nil? || summaries.empty?
|
|
208
|
+
|
|
209
|
+
lines = ["## Memories"]
|
|
210
|
+
lines.concat(summaries)
|
|
211
|
+
lines << ""
|
|
212
|
+
lines << "Call recall_memories to get details before answering. Do NOT guess from the name alone."
|
|
213
|
+
lines.join("\n")
|
|
214
|
+
rescue => e
|
|
215
|
+
ConsoleAgent.logger.debug("ConsoleAgent: memory context failed: #{e.message}")
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
190
219
|
def eager_load_app!
|
|
191
220
|
return unless defined?(Rails) && Rails.respond_to?(:application)
|
|
192
221
|
|
|
@@ -1,4 +1,43 @@
|
|
|
1
|
+
require 'stringio'
|
|
2
|
+
|
|
1
3
|
module ConsoleAgent
|
|
4
|
+
# Writes to two IO streams simultaneously
|
|
5
|
+
class TeeIO
|
|
6
|
+
def initialize(primary, secondary)
|
|
7
|
+
@primary = primary
|
|
8
|
+
@secondary = secondary
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def write(str)
|
|
12
|
+
@primary.write(str)
|
|
13
|
+
@secondary.write(str)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def puts(*args)
|
|
17
|
+
@primary.puts(*args)
|
|
18
|
+
# Capture what puts would output
|
|
19
|
+
args.each { |a| @secondary.write("#{a}\n") }
|
|
20
|
+
@secondary.write("\n") if args.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def print(*args)
|
|
24
|
+
@primary.print(*args)
|
|
25
|
+
args.each { |a| @secondary.write(a.to_s) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def flush
|
|
29
|
+
@primary.flush if @primary.respond_to?(:flush)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def respond_to_missing?(method, include_private = false)
|
|
33
|
+
@primary.respond_to?(method, include_private) || super
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def method_missing(method, *args, &block)
|
|
37
|
+
@primary.send(method, *args, &block)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
2
41
|
class Executor
|
|
3
42
|
CODE_REGEX = /```ruby\s*\n(.*?)```/m
|
|
4
43
|
|
|
@@ -33,21 +72,43 @@ module ConsoleAgent
|
|
|
33
72
|
def execute(code)
|
|
34
73
|
return nil if code.nil? || code.strip.empty?
|
|
35
74
|
|
|
75
|
+
captured_output = StringIO.new
|
|
76
|
+
old_stdout = $stdout
|
|
77
|
+
# Tee output: capture it and also print to the real stdout
|
|
78
|
+
$stdout = TeeIO.new(old_stdout, captured_output)
|
|
79
|
+
|
|
36
80
|
result = binding_context.eval(code, "(console_agent)", 1)
|
|
81
|
+
|
|
82
|
+
$stdout = old_stdout
|
|
37
83
|
$stdout.puts colorize("=> #{result.inspect}", :green)
|
|
84
|
+
|
|
85
|
+
@last_output = captured_output.string
|
|
38
86
|
result
|
|
39
87
|
rescue SyntaxError => e
|
|
88
|
+
$stdout = old_stdout if old_stdout
|
|
40
89
|
$stderr.puts colorize("SyntaxError: #{e.message}", :red)
|
|
90
|
+
@last_output = nil
|
|
41
91
|
nil
|
|
42
92
|
rescue => e
|
|
93
|
+
$stdout = old_stdout if old_stdout
|
|
43
94
|
$stderr.puts colorize("Error: #{e.class}: #{e.message}", :red)
|
|
44
95
|
e.backtrace.first(3).each { |line| $stderr.puts colorize(" #{line}", :red) }
|
|
96
|
+
@last_output = captured_output&.string
|
|
45
97
|
nil
|
|
46
98
|
end
|
|
47
99
|
|
|
100
|
+
def last_output
|
|
101
|
+
@last_output
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def last_cancelled?
|
|
105
|
+
@last_cancelled
|
|
106
|
+
end
|
|
107
|
+
|
|
48
108
|
def confirm_and_execute(code)
|
|
49
109
|
return nil if code.nil? || code.strip.empty?
|
|
50
110
|
|
|
111
|
+
@last_cancelled = false
|
|
51
112
|
$stdout.print colorize("Execute? [y/N/edit] ", :yellow)
|
|
52
113
|
answer = $stdin.gets.to_s.strip.downcase
|
|
53
114
|
|
|
@@ -71,6 +132,7 @@ module ConsoleAgent
|
|
|
71
132
|
end
|
|
72
133
|
else
|
|
73
134
|
$stdout.puts colorize("Cancelled.", :yellow)
|
|
135
|
+
@last_cancelled = true
|
|
74
136
|
nil
|
|
75
137
|
end
|
|
76
138
|
end
|
data/lib/console_agent/repl.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'readline'
|
|
2
|
+
|
|
1
3
|
module ConsoleAgent
|
|
2
4
|
class Repl
|
|
3
5
|
def initialize(binding_context)
|
|
@@ -9,6 +11,7 @@ module ConsoleAgent
|
|
|
9
11
|
@history = []
|
|
10
12
|
@total_input_tokens = 0
|
|
11
13
|
@total_output_tokens = 0
|
|
14
|
+
@input_history = []
|
|
12
15
|
end
|
|
13
16
|
|
|
14
17
|
def one_shot(query)
|
|
@@ -52,17 +55,26 @@ module ConsoleAgent
|
|
|
52
55
|
@total_output_tokens = 0
|
|
53
56
|
|
|
54
57
|
loop do
|
|
55
|
-
|
|
56
|
-
input
|
|
57
|
-
break if input.nil?
|
|
58
|
+
input = Readline.readline("\e[33mai> \e[0m", false)
|
|
59
|
+
break if input.nil? # Ctrl-D
|
|
58
60
|
|
|
59
61
|
input = input.strip
|
|
60
62
|
break if input.downcase == 'exit' || input.downcase == 'quit'
|
|
61
63
|
next if input.empty?
|
|
62
64
|
|
|
65
|
+
# Add to Readline history (avoid consecutive duplicates)
|
|
66
|
+
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
67
|
+
|
|
63
68
|
@history << { role: :user, content: input }
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
begin
|
|
71
|
+
result = send_query(input, conversation: @history)
|
|
72
|
+
rescue Interrupt
|
|
73
|
+
$stdout.puts "\n\e[33m Aborted.\e[0m"
|
|
74
|
+
@history.pop # Remove the user message that never got a response
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
|
|
66
78
|
track_usage(result)
|
|
67
79
|
code = @executor.display_response(result.text)
|
|
68
80
|
display_usage(result, show_session: true)
|
|
@@ -76,10 +88,26 @@ module ConsoleAgent
|
|
|
76
88
|
exec_result = @executor.confirm_and_execute(code)
|
|
77
89
|
end
|
|
78
90
|
|
|
79
|
-
if
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
if @executor.last_cancelled?
|
|
92
|
+
@history << { role: :user, content: "User declined to execute the code." }
|
|
93
|
+
else
|
|
94
|
+
output_parts = []
|
|
95
|
+
|
|
96
|
+
# Capture printed output (puts, print, etc.)
|
|
97
|
+
if @executor.last_output && !@executor.last_output.strip.empty?
|
|
98
|
+
output_parts << "Output:\n#{@executor.last_output.strip}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Capture return value
|
|
102
|
+
if exec_result
|
|
103
|
+
output_parts << "Return value: #{exec_result.inspect}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
unless output_parts.empty?
|
|
107
|
+
result_str = output_parts.join("\n\n")
|
|
108
|
+
result_str = result_str[0..1000] + '...' if result_str.length > 1000
|
|
109
|
+
@history << { role: :user, content: "Code was executed. #{result_str}" }
|
|
110
|
+
end
|
|
83
111
|
end
|
|
84
112
|
end
|
|
85
113
|
end
|
|
@@ -87,6 +115,7 @@ module ConsoleAgent
|
|
|
87
115
|
display_session_summary
|
|
88
116
|
$stdout.puts "\e[36mLeft ConsoleAgent interactive mode.\e[0m"
|
|
89
117
|
rescue Interrupt
|
|
118
|
+
# Ctrl-C during Readline input — exit cleanly
|
|
90
119
|
$stdout.puts
|
|
91
120
|
display_session_summary
|
|
92
121
|
$stdout.puts "\e[36mLeft ConsoleAgent interactive mode.\e[0m"
|
|
@@ -132,6 +161,8 @@ module ConsoleAgent
|
|
|
132
161
|
total_output = 0
|
|
133
162
|
result = nil
|
|
134
163
|
|
|
164
|
+
exhausted = false
|
|
165
|
+
|
|
135
166
|
max_rounds.times do |round|
|
|
136
167
|
if round == 0
|
|
137
168
|
$stdout.puts "\e[2m Thinking...\e[0m"
|
|
@@ -163,7 +194,8 @@ module ConsoleAgent
|
|
|
163
194
|
tool_result = tools.execute(tc[:name], tc[:arguments])
|
|
164
195
|
|
|
165
196
|
preview = compact_tool_result(tc[:name], tool_result)
|
|
166
|
-
|
|
197
|
+
cached_tag = tools.last_cached? ? " (cached)" : ""
|
|
198
|
+
$stdout.puts "\e[2m #{preview}#{cached_tag}\e[0m"
|
|
167
199
|
end
|
|
168
200
|
|
|
169
201
|
if ConsoleAgent.configuration.debug
|
|
@@ -172,6 +204,17 @@ module ConsoleAgent
|
|
|
172
204
|
|
|
173
205
|
messages << provider.format_tool_result(tc[:id], tool_result)
|
|
174
206
|
end
|
|
207
|
+
|
|
208
|
+
exhausted = true if round == max_rounds - 1
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# If we hit the tool round limit, force a final response without tools
|
|
212
|
+
if exhausted
|
|
213
|
+
$stdout.puts "\e[33m Hit tool round limit (#{max_rounds}). Forcing final answer. Increase with: ConsoleAgent.configure { |c| c.max_tool_rounds = 200 }\e[0m"
|
|
214
|
+
messages << { role: :user, content: "You've used all available tool rounds. Please provide your best answer now based on what you've learned so far." }
|
|
215
|
+
result = provider.chat(messages, system_prompt: context)
|
|
216
|
+
total_input += result.input_tokens || 0
|
|
217
|
+
total_output += result.output_tokens || 0
|
|
175
218
|
end
|
|
176
219
|
|
|
177
220
|
Providers::ChatResult.new(
|
|
@@ -197,6 +240,12 @@ module ConsoleAgent
|
|
|
197
240
|
"(\"#{args['query']}\"#{dir})"
|
|
198
241
|
when 'list_files'
|
|
199
242
|
args['directory'] ? "(\"#{args['directory']}\")" : ''
|
|
243
|
+
when 'save_memory'
|
|
244
|
+
"(\"#{args['name']}\")"
|
|
245
|
+
when 'delete_memory'
|
|
246
|
+
"(\"#{args['name']}\")"
|
|
247
|
+
when 'recall_memories'
|
|
248
|
+
args['query'] ? "(\"#{args['query']}\")" : ''
|
|
200
249
|
else
|
|
201
250
|
''
|
|
202
251
|
end
|
|
@@ -244,6 +293,13 @@ module ConsoleAgent
|
|
|
244
293
|
else
|
|
245
294
|
truncate(result, 80)
|
|
246
295
|
end
|
|
296
|
+
when 'save_memory'
|
|
297
|
+
(result.start_with?('Memory saved') || result.start_with?('Memory updated')) ? result : truncate(result, 80)
|
|
298
|
+
when 'delete_memory'
|
|
299
|
+
result.start_with?('Memory deleted') ? result : truncate(result, 80)
|
|
300
|
+
when 'recall_memories'
|
|
301
|
+
chunks = result.split("\n\n")
|
|
302
|
+
chunks.length > 1 ? "#{chunks.length} memories found" : truncate(result, 80)
|
|
247
303
|
else
|
|
248
304
|
truncate(result, 80)
|
|
249
305
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module ConsoleAgent
|
|
2
|
+
module Storage
|
|
3
|
+
class StorageError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class Base
|
|
6
|
+
def read(key)
|
|
7
|
+
raise NotImplementedError
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def write(key, content)
|
|
11
|
+
raise NotImplementedError
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def list(pattern)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def exists?(key)
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def delete(key)
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
require 'console_agent/storage/base'
|
|
3
|
+
|
|
4
|
+
module ConsoleAgent
|
|
5
|
+
module Storage
|
|
6
|
+
class FileStorage < Base
|
|
7
|
+
attr_reader :root_path
|
|
8
|
+
|
|
9
|
+
def initialize(root_path = nil)
|
|
10
|
+
@root_path = root_path || default_root
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def read(key)
|
|
14
|
+
path = full_path(key)
|
|
15
|
+
return nil unless File.exist?(path)
|
|
16
|
+
File.read(path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def write(key, content)
|
|
20
|
+
path = full_path(key)
|
|
21
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
22
|
+
File.write(path, content)
|
|
23
|
+
true
|
|
24
|
+
rescue Errno::EACCES, Errno::EROFS, IOError => e
|
|
25
|
+
raise StorageError, "Cannot write #{key}: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def list(pattern)
|
|
29
|
+
Dir.glob(File.join(@root_path, pattern)).sort.map do |path|
|
|
30
|
+
path.sub("#{@root_path}/", '')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def exists?(key)
|
|
35
|
+
File.exist?(full_path(key))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def delete(key)
|
|
39
|
+
path = full_path(key)
|
|
40
|
+
return false unless File.exist?(path)
|
|
41
|
+
File.delete(path)
|
|
42
|
+
true
|
|
43
|
+
rescue Errno::EACCES, Errno::EROFS, IOError => e
|
|
44
|
+
raise StorageError, "Cannot delete #{key}: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def full_path(key)
|
|
50
|
+
sanitized = key.gsub('..', '').gsub(%r{\A/+}, '')
|
|
51
|
+
File.join(@root_path, sanitized)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def default_root
|
|
55
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
56
|
+
File.join(Rails.root.to_s, '.console_agent')
|
|
57
|
+
else
|
|
58
|
+
File.join(Dir.pwd, '.console_agent')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require 'yaml'
|
|
2
|
+
|
|
3
|
+
module ConsoleAgent
|
|
4
|
+
module Tools
|
|
5
|
+
class MemoryTools
|
|
6
|
+
MEMORIES_DIR = 'memories'
|
|
7
|
+
|
|
8
|
+
def initialize(storage = nil)
|
|
9
|
+
@storage = storage || ConsoleAgent.storage
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def save_memory(name:, description:, tags: [])
|
|
13
|
+
key = memory_key(name)
|
|
14
|
+
existing = load_memory(key)
|
|
15
|
+
|
|
16
|
+
frontmatter = {
|
|
17
|
+
'name' => name,
|
|
18
|
+
'tags' => Array(tags).empty? && existing ? (existing['tags'] || []) : Array(tags),
|
|
19
|
+
'created_at' => existing ? existing['created_at'] : Time.now.utc.iso8601
|
|
20
|
+
}
|
|
21
|
+
frontmatter['updated_at'] = Time.now.utc.iso8601 if existing
|
|
22
|
+
|
|
23
|
+
content = "---\n#{YAML.dump(frontmatter).sub("---\n", '').strip}\n---\n\n#{description}\n"
|
|
24
|
+
@storage.write(key, content)
|
|
25
|
+
|
|
26
|
+
path = @storage.respond_to?(:root_path) ? File.join(@storage.root_path, key) : key
|
|
27
|
+
if existing
|
|
28
|
+
"Memory updated: \"#{name}\" (#{path})"
|
|
29
|
+
else
|
|
30
|
+
"Memory saved: \"#{name}\" (#{path})"
|
|
31
|
+
end
|
|
32
|
+
rescue Storage::StorageError => e
|
|
33
|
+
"FAILED to save (#{e.message}). Add this manually to .console_agent/#{key}:\n" \
|
|
34
|
+
"---\nname: #{name}\ntags: #{Array(tags).inspect}\n---\n\n#{description}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delete_memory(name:)
|
|
38
|
+
key = memory_key(name)
|
|
39
|
+
unless @storage.exists?(key)
|
|
40
|
+
# Try to find by name match across all memory files
|
|
41
|
+
found_key = find_memory_key_by_name(name)
|
|
42
|
+
return "No memory found: \"#{name}\"" unless found_key
|
|
43
|
+
key = found_key
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
memory = load_memory(key)
|
|
47
|
+
@storage.delete(key)
|
|
48
|
+
"Memory deleted: \"#{memory ? memory['name'] : name}\""
|
|
49
|
+
rescue Storage::StorageError => e
|
|
50
|
+
"FAILED to delete memory (#{e.message})."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def recall_memories(query: nil, tag: nil)
|
|
54
|
+
memories = load_all_memories
|
|
55
|
+
return "No memories stored yet." if memories.empty?
|
|
56
|
+
|
|
57
|
+
results = memories
|
|
58
|
+
if tag && !tag.empty?
|
|
59
|
+
results = results.select { |m|
|
|
60
|
+
Array(m['tags']).any? { |t| t.downcase.include?(tag.downcase) }
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
if query && !query.empty?
|
|
64
|
+
q = query.downcase
|
|
65
|
+
results = results.select { |m|
|
|
66
|
+
m['name'].to_s.downcase.include?(q) ||
|
|
67
|
+
m['description'].to_s.downcase.include?(q) ||
|
|
68
|
+
Array(m['tags']).any? { |t| t.downcase.include?(q) }
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
return "No memories matching your search." if results.empty?
|
|
73
|
+
|
|
74
|
+
results.map { |m|
|
|
75
|
+
line = "**#{m['name']}**\n#{m['description']}"
|
|
76
|
+
line += "\nTags: #{m['tags'].join(', ')}" if m['tags'] && !m['tags'].empty?
|
|
77
|
+
line
|
|
78
|
+
}.join("\n\n")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def memory_summaries
|
|
82
|
+
memories = load_all_memories
|
|
83
|
+
return nil if memories.empty?
|
|
84
|
+
|
|
85
|
+
memories.map { |m|
|
|
86
|
+
tags = Array(m['tags'])
|
|
87
|
+
tag_str = tags.empty? ? '' : " [#{tags.join(', ')}]"
|
|
88
|
+
"- #{m['name']}#{tag_str}"
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def memory_key(name)
|
|
95
|
+
slug = name.downcase.strip
|
|
96
|
+
.gsub(/[^a-z0-9\s-]/, '')
|
|
97
|
+
.gsub(/[\s]+/, '-')
|
|
98
|
+
.gsub(/-+/, '-')
|
|
99
|
+
.sub(/^-/, '').sub(/-$/, '')
|
|
100
|
+
"#{MEMORIES_DIR}/#{slug}.md"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def load_memory(key)
|
|
104
|
+
content = @storage.read(key)
|
|
105
|
+
return nil if content.nil? || content.strip.empty?
|
|
106
|
+
parse_memory(content)
|
|
107
|
+
rescue => e
|
|
108
|
+
ConsoleAgent.logger.warn("ConsoleAgent: failed to load memory #{key}: #{e.message}")
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def load_all_memories
|
|
113
|
+
keys = @storage.list("#{MEMORIES_DIR}/*.md")
|
|
114
|
+
keys.map { |key| load_memory(key) }.compact
|
|
115
|
+
rescue => e
|
|
116
|
+
ConsoleAgent.logger.warn("ConsoleAgent: failed to load memories: #{e.message}")
|
|
117
|
+
[]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_memory(content)
|
|
121
|
+
return nil unless content =~ /\A---\s*\n(.*?\n)---\s*\n(.*)/m
|
|
122
|
+
frontmatter = YAML.safe_load($1, permitted_classes: [Time, Date]) || {}
|
|
123
|
+
description = $2.strip
|
|
124
|
+
frontmatter.merge('description' => description)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def find_memory_key_by_name(name)
|
|
128
|
+
keys = @storage.list("#{MEMORIES_DIR}/*.md")
|
|
129
|
+
keys.find do |key|
|
|
130
|
+
memory = load_memory(key)
|
|
131
|
+
memory && memory['name'].to_s.downcase == name.downcase
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -5,12 +5,21 @@ module ConsoleAgent
|
|
|
5
5
|
class Registry
|
|
6
6
|
attr_reader :definitions
|
|
7
7
|
|
|
8
|
+
# Tools that should never be cached (side effects or user interaction)
|
|
9
|
+
NO_CACHE = %w[ask_user save_memory delete_memory].freeze
|
|
10
|
+
|
|
8
11
|
def initialize
|
|
9
12
|
@definitions = []
|
|
10
13
|
@handlers = {}
|
|
14
|
+
@cache = {}
|
|
15
|
+
@last_cached = false
|
|
11
16
|
register_all
|
|
12
17
|
end
|
|
13
18
|
|
|
19
|
+
def last_cached?
|
|
20
|
+
@last_cached
|
|
21
|
+
end
|
|
22
|
+
|
|
14
23
|
def execute(tool_name, arguments = {})
|
|
15
24
|
handler = @handlers[tool_name]
|
|
16
25
|
unless handler
|
|
@@ -27,7 +36,18 @@ module ConsoleAgent
|
|
|
27
36
|
arguments || {}
|
|
28
37
|
end
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
unless NO_CACHE.include?(tool_name)
|
|
40
|
+
cache_key = [tool_name, args].hash
|
|
41
|
+
if @cache.key?(cache_key)
|
|
42
|
+
@last_cached = true
|
|
43
|
+
return @cache[cache_key]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@last_cached = false
|
|
48
|
+
result = handler.call(args)
|
|
49
|
+
@cache[[tool_name, args].hash] = result unless NO_CACHE.include?(tool_name)
|
|
50
|
+
result
|
|
31
51
|
rescue => e
|
|
32
52
|
"Error executing #{tool_name}: #{e.message}"
|
|
33
53
|
end
|
|
@@ -158,6 +178,58 @@ module ConsoleAgent
|
|
|
158
178
|
},
|
|
159
179
|
handler: ->(args) { ask_user(args['question']) }
|
|
160
180
|
)
|
|
181
|
+
|
|
182
|
+
register_memory_tools
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def register_memory_tools
|
|
186
|
+
return unless ConsoleAgent.configuration.memories_enabled
|
|
187
|
+
|
|
188
|
+
require 'console_agent/tools/memory_tools'
|
|
189
|
+
memory = MemoryTools.new
|
|
190
|
+
|
|
191
|
+
register(
|
|
192
|
+
name: 'save_memory',
|
|
193
|
+
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).',
|
|
194
|
+
parameters: {
|
|
195
|
+
'type' => 'object',
|
|
196
|
+
'properties' => {
|
|
197
|
+
'name' => { 'type' => 'string', 'description' => 'Short name for this memory (e.g. "Sharding architecture")' },
|
|
198
|
+
'description' => { 'type' => 'string', 'description' => 'Detailed description of what you learned' },
|
|
199
|
+
'tags' => { 'type' => 'array', 'items' => { 'type' => 'string' }, 'description' => 'Optional tags (e.g. ["database", "sharding"])' }
|
|
200
|
+
},
|
|
201
|
+
'required' => ['name', 'description']
|
|
202
|
+
},
|
|
203
|
+
handler: ->(args) {
|
|
204
|
+
memory.save_memory(name: args['name'], description: args['description'], tags: args['tags'] || [])
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
register(
|
|
209
|
+
name: 'delete_memory',
|
|
210
|
+
description: 'Delete a memory by name.',
|
|
211
|
+
parameters: {
|
|
212
|
+
'type' => 'object',
|
|
213
|
+
'properties' => {
|
|
214
|
+
'name' => { 'type' => 'string', 'description' => 'The memory name to delete (e.g. "Sharding architecture")' }
|
|
215
|
+
},
|
|
216
|
+
'required' => ['name']
|
|
217
|
+
},
|
|
218
|
+
handler: ->(args) { memory.delete_memory(name: args['name']) }
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
register(
|
|
222
|
+
name: 'recall_memories',
|
|
223
|
+
description: 'Search your saved memories about this codebase. Call with no args to list all, or pass a query/tag to filter.',
|
|
224
|
+
parameters: {
|
|
225
|
+
'type' => 'object',
|
|
226
|
+
'properties' => {
|
|
227
|
+
'query' => { 'type' => 'string', 'description' => 'Search term to filter by name, description, or tags' },
|
|
228
|
+
'tag' => { 'type' => 'string', 'description' => 'Filter by a specific tag' }
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
handler: ->(args) { memory.recall_memories(query: args['query'], tag: args['tag']) }
|
|
232
|
+
)
|
|
161
233
|
end
|
|
162
234
|
|
|
163
235
|
def ask_user(question)
|
data/lib/console_agent.rb
CHANGED
|
@@ -13,6 +13,23 @@ module ConsoleAgent
|
|
|
13
13
|
|
|
14
14
|
def reset_configuration!
|
|
15
15
|
@configuration = Configuration.new
|
|
16
|
+
reset_storage!
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def storage
|
|
20
|
+
@storage ||= begin
|
|
21
|
+
adapter = configuration.storage_adapter
|
|
22
|
+
if adapter
|
|
23
|
+
adapter
|
|
24
|
+
else
|
|
25
|
+
require 'console_agent/storage/file_storage'
|
|
26
|
+
Storage::FileStorage.new
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reset_storage!
|
|
32
|
+
@storage = nil
|
|
16
33
|
end
|
|
17
34
|
|
|
18
35
|
def logger
|
|
@@ -48,6 +65,7 @@ module ConsoleAgent
|
|
|
48
65
|
lines << " Timeout: #{c.timeout}s"
|
|
49
66
|
lines << " Max tool rounds:#{c.max_tool_rounds}" if c.context_mode == :smart
|
|
50
67
|
lines << " Auto-execute: #{c.auto_execute}"
|
|
68
|
+
lines << " Memories: #{c.memories_enabled}"
|
|
51
69
|
lines << " Debug: #{c.debug}"
|
|
52
70
|
$stdout.puts lines.join("\n")
|
|
53
71
|
nil
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: console_agent
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cortfr
|
|
@@ -98,7 +98,10 @@ files:
|
|
|
98
98
|
- lib/console_agent/providers/openai.rb
|
|
99
99
|
- lib/console_agent/railtie.rb
|
|
100
100
|
- lib/console_agent/repl.rb
|
|
101
|
+
- lib/console_agent/storage/base.rb
|
|
102
|
+
- lib/console_agent/storage/file_storage.rb
|
|
101
103
|
- lib/console_agent/tools/code_tools.rb
|
|
104
|
+
- lib/console_agent/tools/memory_tools.rb
|
|
102
105
|
- lib/console_agent/tools/model_tools.rb
|
|
103
106
|
- lib/console_agent/tools/registry.rb
|
|
104
107
|
- lib/console_agent/tools/schema_tools.rb
|