aia 0.9.23 → 0.9.24

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38ae30b2341fb1e96738601b0c7cac5459772217d005aa5e22f18ecde270e2e0
4
- data.tar.gz: 34555fad44d99436b4171887c0804c88f88e6fdc572e8a2d05476f485b0b80c6
3
+ metadata.gz: f9592d9ea1bbbbdc04f92dc822cfbdae91c1e04084530583e50f6cdb8a90460d
4
+ data.tar.gz: 897e53ec02de0fcc46a0e5ddcf15ca13a462b7cc20e4f355950f5872e5b04f00
5
5
  SHA512:
6
- metadata.gz: 67182004edf9443ad3fc9190c1d57daa18c8f997d0425fa642e6f4c0c94d74490a1943ae1f79e7b18f55f65c0237a8fba31e765b750c374c8885926cbc08ca62
7
- data.tar.gz: 3a86c4b7734a955180e7282a5462f7cda7ff83d1eac49c7106e63a69eaf9554a318ef2195ef61c4a61aec2340be6fdac8d19fd0d6a681f450e6c6c55b7c02940
6
+ metadata.gz: 5e8b63a8ca892346e4dd1090ef487e4fc6dc6b9395238ecc6208128d8908a5dca87d730a9193a15b05dbb005b8d9eb84e2bb7f63100982130eeec69e0bb9483d
7
+ data.tar.gz: 660c1c40117300223a7bd88c67ec826225a44ada72d459ee6c8fd3e3c015e81a7874a9618220c4dc7caf3de930ee140d29ef1c7c8dfc205ecfeedef8e629b36a
data/.version CHANGED
@@ -1 +1 @@
1
- 0.9.23
1
+ 0.9.24
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
1
  # Changelog
2
2
  ## [Unreleased]
3
3
 
4
+ ## [0.9.24] 2025-12-17
5
+ ### Fixes
6
+ - Ran into a problem with the `shared_tools` gem and the --require parameter of AIA which required changes to both gems.
7
+
8
+ ### Improvements
9
+ - **`//tools` Directive Filter**: Added optional filter parameter to the `//tools` directive
10
+ - Filter tools by name substring (case-insensitive)
11
+ - Example: `//tools error` lists only tools with "error" in the name
12
+ - Shows "No tools match the filter: [filter]" when no matches found
13
+ - Header indicates when filtering is active: "Available Tools (filtered by 'filter')"
14
+
4
15
  ## [0.9.23] 2025-12-06
5
16
 
6
17
  ### New Features
data/README.md CHANGED
@@ -351,7 +351,7 @@ Directives are special commands in prompt files that begin with `//` and provide
351
351
  | `//help` | Show available directives | `//help` |
352
352
  | `//model` | Show current model configuration | `//model` |
353
353
  | `//available_models` | List available models | `//available_models` |
354
- | `//tools` | Show a list of available tools and their description | `//tools` |
354
+ | `//tools` | Show available tools (optional filter by name) | `//tools` or `//tools file` |
355
355
  | `//review` | Review current context with checkpoint markers | `//review` |
356
356
 
357
357
  Directives can also be used in the interactive chat sessions.
@@ -1058,6 +1058,13 @@ Available Tools:
1058
1058
  - htm_query: Execute a query against the HTM database
1059
1059
  - htm_insert: Insert a record into HTM
1060
1060
  ...
1061
+
1062
+ # Filter tools by name (case-insensitive)
1063
+ > //tools github
1064
+
1065
+ Available Tools (filtered by 'github')
1066
+ - github_create_issue: Create a new GitHub issue
1067
+ - github_list_repos: List repositories for the authenticated user
1061
1068
  ```
1062
1069
 
1063
1070
  #### Troubleshooting MCP Servers
@@ -214,11 +214,21 @@ Speak text using system text-to-speech (macOS/Linux).
214
214
  ## Utility Directives
215
215
 
216
216
  ### `//tools`
217
- Display available RubyLLM tools.
217
+ Display available RubyLLM tools with optional filtering.
218
218
 
219
- **Syntax**: `//tools`
219
+ **Syntax**: `//tools [filter]`
220
220
 
221
- **Example Output**:
221
+ **Parameters**:
222
+ - `filter` (optional) - Case-insensitive substring to filter tool names
223
+
224
+ **Examples**:
225
+ ```markdown
226
+ //tools # List all available tools
227
+ //tools file # List tools with "file" in the name
228
+ //tools analyzer # List tools with "analyzer" in the name
229
+ ```
230
+
231
+ **Example Output** (unfiltered):
222
232
  ```
223
233
  Available Tools
224
234
  ===============
@@ -234,6 +244,21 @@ WebScraper
234
244
  selectors and filters.
235
245
  ```
236
246
 
247
+ **Example Output** (filtered with `//tools file`):
248
+ ```
249
+ Available Tools (filtered by 'file')
250
+ ====================================
251
+
252
+ FileReader
253
+ ----------
254
+ Read and analyze file contents with support for multiple formats
255
+ including text, JSON, YAML, and CSV files.
256
+ ```
257
+
258
+ **Notes**:
259
+ - When no tools match the filter, displays "No tools match the filter: [filter]"
260
+ - Filtering is case-insensitive (e.g., "File", "FILE", and "file" all match)
261
+
237
262
  ### `//next`
238
263
  Set the next prompt to execute in a workflow.
239
264
 
@@ -509,11 +534,6 @@ SyntaxError: unexpected token
509
534
  ERROR: PUREMD_API_KEY is required in order to include a webpage
510
535
  ```
511
536
 
512
- **Missing Context Manager**:
513
- ```
514
- Error: Context manager not available for //clear directive.
515
- ```
516
-
517
537
  ### Custom Directives
518
538
 
519
539
  You can extend AIA with custom directives by creating Ruby files that define new directive methods:
data/docs/index.md CHANGED
@@ -126,13 +126,13 @@ graph TD
126
126
  - **AIA::PromptHandler** - Main prompt processing orchestrator
127
127
  - **AIA::ChatProcessorService** - Interactive chat session management
128
128
  - **AIA::DirectiveProcessor** - Processes embedded directives (`//command params`)
129
- - **AIA::ContextManager** - Manages conversation context and history
130
- - **AIA::RubyLLMAdapter** - Interfaces with the ruby_llm gem for AI model communication
129
+ - **AIA::RubyLLMAdapter** - Interfaces with the ruby_llm gem for AI model communication (manages conversation history via RubyLLM's Chat.@messages)
131
130
  - **AIA::ShellCommandExecutor** - Executes shell commands safely within prompts
132
131
  - **AIA::HistoryManager** - Manages prompt parameter history and user input
133
132
  - **AIA::UIPresenter** - Terminal output formatting and presentation
134
133
  - **AIA::Session** - Manages chat sessions and state
135
134
  - **AIA::Fzf** - Fuzzy finder integration for prompt selection
135
+ - **AIA::Directives::Checkpoint** - Manages conversation checkpoints, restore, clear, and review operations
136
136
 
137
137
  ### External Dependencies
138
138
 
@@ -157,6 +157,9 @@ module AIA
157
157
  config.require_libs.each do |library|
158
158
  begin
159
159
  require(library)
160
+ # Check if the library provides tools that need eager loading
161
+ # Convert library name to module constant (e.g., 'shared_tools' -> 'SharedTools')
162
+ eager_load_tools_from_library(library)
160
163
  rescue => e
161
164
  STDERR.puts "Error loading library '#{library}' #{e.message}"
162
165
  exit_on_error = true
@@ -168,6 +171,23 @@ module AIA
168
171
  config
169
172
  end
170
173
 
174
+ # Attempt to eager load tools from a required library
175
+ # Libraries that use Zeitwerk need their tools eager loaded so they
176
+ # appear in ObjectSpace for AIA's tool discovery
177
+ def eager_load_tools_from_library(library)
178
+ # Convert library name to module constant (e.g., 'shared_tools' -> 'SharedTools')
179
+ module_name = library.split('/').first.split('_').map(&:capitalize).join
180
+
181
+ begin
182
+ mod = Object.const_get(module_name)
183
+ if mod.respond_to?(:load_all_tools)
184
+ mod.load_all_tools
185
+ end
186
+ rescue NameError
187
+ # Module doesn't exist with expected name, skip
188
+ end
189
+ end
190
+
171
191
  def load_tools(config)
172
192
  return if config.tool_paths.empty?
173
193
 
@@ -0,0 +1,283 @@
1
+ # lib/aia/directives/checkpoint.rb
2
+ #
3
+ # Checkpoint and restore directives for managing conversation state.
4
+ # Uses RubyLLM's Chat.@messages as the source of truth for conversation history.
5
+ #
6
+
7
+ module AIA
8
+ module Directives
9
+ module Checkpoint
10
+ # Module-level state for checkpoints
11
+ # Note: Using @checkpoint_store to avoid naming conflict with //checkpoints directive
12
+ @checkpoint_store = {}
13
+ @checkpoint_counter = 0
14
+ @last_checkpoint_name = nil
15
+
16
+ class << self
17
+ attr_accessor :checkpoint_store, :checkpoint_counter, :last_checkpoint_name
18
+
19
+ # Reset all checkpoint state (useful for testing)
20
+ def reset!
21
+ @checkpoint_store = {}
22
+ @checkpoint_counter = 0
23
+ @last_checkpoint_name = nil
24
+ end
25
+ end
26
+
27
+ # //checkpoint [name]
28
+ # Creates a named checkpoint of the current conversation state.
29
+ # If no name is provided, uses an auto-incrementing number.
30
+ def self.checkpoint(args, _unused = nil)
31
+ name = args.empty? ? nil : args.join(' ').strip
32
+
33
+ if name.nil? || name.empty?
34
+ self.checkpoint_counter += 1
35
+ name = checkpoint_counter.to_s
36
+ end
37
+
38
+ chats = get_chats
39
+ return "Error: No active chat sessions found." if chats.nil? || chats.empty?
40
+
41
+ # Deep copy messages from all chats
42
+ first_chat_messages = chats.values.first&.messages || []
43
+ checkpoint_store[name] = {
44
+ messages: chats.transform_values { |chat|
45
+ chat.messages.map { |msg| deep_copy_message(msg) }
46
+ },
47
+ position: first_chat_messages.size,
48
+ created_at: Time.now,
49
+ topic_preview: extract_last_user_message(first_chat_messages)
50
+ }
51
+ self.last_checkpoint_name = name
52
+
53
+ puts "Checkpoint '#{name}' created at position #{checkpoint_store[name][:position]}."
54
+ ""
55
+ end
56
+
57
+ # //restore [name]
58
+ # Restores the conversation state to a previously saved checkpoint.
59
+ # If no name is provided, restores to the previous checkpoint (one step back).
60
+ def self.restore(args, _unused = nil)
61
+ name = args.empty? ? nil : args.join(' ').strip
62
+
63
+ if name.nil? || name.empty?
64
+ # Find the previous checkpoint (second-to-last by position)
65
+ name = find_previous_checkpoint
66
+ if name.nil?
67
+ return "Error: No previous checkpoint to restore to."
68
+ end
69
+ end
70
+
71
+ unless checkpoint_store.key?(name)
72
+ available = checkpoint_names.empty? ? "none" : checkpoint_names.join(', ')
73
+ return "Error: Checkpoint '#{name}' not found. Available: #{available}"
74
+ end
75
+
76
+ checkpoint_data = checkpoint_store[name]
77
+ chats = get_chats
78
+
79
+ return "Error: No active chat sessions found." if chats.nil? || chats.empty?
80
+
81
+ # Restore messages to each chat
82
+ checkpoint_data[:messages].each do |model_id, saved_messages|
83
+ chat = chats[model_id]
84
+ next unless chat
85
+
86
+ # Replace the chat's messages with the saved ones
87
+ restored_messages = saved_messages.map { |msg| deep_copy_message(msg) }
88
+ chat.instance_variable_set(:@messages, restored_messages)
89
+ end
90
+
91
+ # Remove checkpoints that are now invalid (position > restored position)
92
+ # because they reference conversation states that no longer exist
93
+ restored_position = checkpoint_data[:position]
94
+ removed_count = remove_invalid_checkpoints(restored_position)
95
+
96
+ # Update last_checkpoint_name to this checkpoint
97
+ self.last_checkpoint_name = name
98
+
99
+ msg = "Context restored to checkpoint '#{name}' (position #{restored_position})."
100
+ msg += " Removed #{removed_count} checkpoint(s) that were beyond this position." if removed_count > 0
101
+ msg
102
+ end
103
+
104
+ # //clear
105
+ # Clears the conversation context, optionally keeping the system prompt.
106
+ def self.clear(args, _unused = nil)
107
+ keep_system = !args.include?('--all')
108
+
109
+ chats = get_chats
110
+ return "Error: No active chat sessions found." if chats.nil? || chats.empty?
111
+
112
+ chats.each do |_model_id, chat|
113
+ if keep_system
114
+ system_msg = chat.messages.find { |m| m.role == :system }
115
+ chat.instance_variable_set(:@messages, [])
116
+ chat.add_message(system_msg) if system_msg
117
+ else
118
+ chat.instance_variable_set(:@messages, [])
119
+ end
120
+ end
121
+
122
+ # Clear all checkpoints
123
+ checkpoint_store.clear
124
+ self.checkpoint_counter = 0
125
+ self.last_checkpoint_name = nil
126
+
127
+ "Chat context cleared."
128
+ end
129
+
130
+ # //review
131
+ # Displays the current conversation context with checkpoint markers.
132
+ def self.review(args, _unused = nil)
133
+ chats = get_chats
134
+ return "Error: No active chat sessions found." if chats.nil? || chats.empty?
135
+
136
+ # For multi-model, show first chat's messages (they should be similar for user messages)
137
+ first_chat = chats.values.first
138
+ messages = first_chat&.messages || []
139
+
140
+ puts "\n=== Chat Context (RubyLLM) ==="
141
+ puts "Total messages: #{messages.size}"
142
+ puts "Models: #{chats.keys.join(', ')}"
143
+ puts "Checkpoints: #{checkpoint_names.join(', ')}" if checkpoint_names.any?
144
+ puts
145
+
146
+ positions = checkpoint_positions
147
+
148
+ messages.each_with_index do |msg, index|
149
+ # Show checkpoint marker if one exists at this position
150
+ if positions[index]
151
+ puts "📍 [Checkpoint: #{positions[index].join(', ')}]"
152
+ puts "-" * 40
153
+ end
154
+
155
+ role = msg.role.to_s.capitalize
156
+ content = format_message_content(msg)
157
+
158
+ puts "#{index + 1}. [#{role}]: #{content}"
159
+ puts
160
+ end
161
+
162
+ # Check for checkpoint at the end
163
+ if positions[messages.size]
164
+ puts "📍 [Checkpoint: #{positions[messages.size].join(', ')}]"
165
+ puts "-" * 40
166
+ end
167
+
168
+ puts "=== End of Context ==="
169
+ ""
170
+ end
171
+
172
+ # //checkpoints
173
+ # Lists all available checkpoints with their details.
174
+ def self.checkpoints_list(args, _unused = nil)
175
+ if checkpoint_store.empty?
176
+ puts "No checkpoints available."
177
+ return ""
178
+ end
179
+
180
+ puts "\n=== Available Checkpoints ==="
181
+ checkpoint_store.each do |name, data|
182
+ created = data[:created_at]&.strftime('%H:%M:%S') || 'unknown'
183
+ puts " #{name}: position #{data[:position]}, created #{created}"
184
+ if data[:topic_preview] && !data[:topic_preview].empty?
185
+ puts " → \"#{data[:topic_preview]}\""
186
+ end
187
+ end
188
+ puts "=== End of Checkpoints ==="
189
+ ""
190
+ end
191
+
192
+ # Helper methods
193
+ def self.checkpoint_names
194
+ checkpoint_store.keys
195
+ end
196
+
197
+ def self.checkpoint_positions
198
+ positions = {}
199
+ checkpoint_store.each do |name, data|
200
+ pos = data[:position]
201
+ positions[pos] ||= []
202
+ positions[pos] << name
203
+ end
204
+ positions
205
+ end
206
+
207
+ # Remove checkpoints with position > the given position
208
+ # Returns the count of removed checkpoints
209
+ def self.remove_invalid_checkpoints(max_position)
210
+ invalid_names = checkpoint_store.select { |_name, data| data[:position] > max_position }.keys
211
+ invalid_names.each { |name| checkpoint_store.delete(name) }
212
+ invalid_names.size
213
+ end
214
+
215
+ # Find the previous checkpoint (second-to-last by position)
216
+ # This is used when //restore is called without a name
217
+ def self.find_previous_checkpoint
218
+ return nil if checkpoint_store.size < 2
219
+
220
+ # Sort checkpoints by position descending, return the second one
221
+ sorted = checkpoint_store.sort_by { |_name, data| -data[:position] }
222
+ sorted[1]&.first # Return the name of the second-to-last checkpoint
223
+ end
224
+
225
+ private
226
+
227
+ def self.get_chats
228
+ return nil unless AIA.client.respond_to?(:chats)
229
+
230
+ AIA.client.chats
231
+ end
232
+
233
+ def self.deep_copy_message(msg)
234
+ RubyLLM::Message.new(
235
+ role: msg.role,
236
+ content: msg.content,
237
+ tool_calls: msg.tool_calls&.transform_values { |tc| tc.dup rescue tc },
238
+ tool_call_id: msg.tool_call_id,
239
+ input_tokens: msg.input_tokens,
240
+ output_tokens: msg.output_tokens,
241
+ model_id: msg.model_id
242
+ )
243
+ end
244
+
245
+ def self.format_message_content(msg)
246
+ if msg.tool_call?
247
+ tool_names = msg.tool_calls.values.map(&:name).join(', ')
248
+ "[Tool calls: #{tool_names}]"
249
+ elsif msg.tool_result?
250
+ result_preview = msg.content.to_s[0..50]
251
+ "[Tool result for: #{msg.tool_call_id}] #{result_preview}..."
252
+ else
253
+ content = msg.content.to_s
254
+ content.length > 150 ? "#{content[0..147]}..." : content
255
+ end
256
+ end
257
+
258
+ # Extract a preview of the last user message for checkpoint context
259
+ def self.extract_last_user_message(messages, max_length: 70)
260
+ return "" if messages.nil? || messages.empty?
261
+
262
+ # Find the last user message (not system, not assistant, not tool)
263
+ last_user_msg = messages.reverse.find { |msg| msg.role == :user }
264
+ return "" unless last_user_msg
265
+
266
+ content = last_user_msg.content.to_s.strip
267
+ # Collapse whitespace and truncate
268
+ content = content.gsub(/\s+/, ' ')
269
+ content.length > max_length ? "#{content[0..max_length - 4]}..." : content
270
+ end
271
+
272
+ # Aliases
273
+ class << self
274
+ alias_method :ckp, :checkpoint
275
+ alias_method :cp, :checkpoint
276
+ alias_method :context, :review
277
+ # Route //checkpoints directive to checkpoints_list
278
+ # (safe now that we renamed attr_accessor to checkpoint_store)
279
+ alias_method :checkpoints, :checkpoints_list
280
+ end
281
+ end
282
+ end
283
+ end
@@ -88,100 +88,15 @@ module AIA
88
88
  send(:config, args.prepend('top_p'), context_manager)
89
89
  end
90
90
 
91
- def self.clear(args, context_manager = nil)
92
- if context_manager.nil?
93
- return "Error: Context manager not available for //clear directive."
94
- end
95
-
96
- context_manager.clear_context
97
- ''
98
- end
99
-
100
- def self.review(args, context_manager = nil)
101
- return "Error: Context manager not available for //review directive." if context_manager.nil?
102
-
103
- context = context_manager.get_context
104
- checkpoint_positions = context_manager.checkpoint_positions
105
-
106
- # Display context with checkpoint markers
107
- puts "\n=== Chat Context ==="
108
- puts "Total messages: #{context.size}"
109
-
110
- if checkpoint_positions.any?
111
- puts "Checkpoints: #{context_manager.checkpoint_names.join(', ')}"
112
- end
113
-
114
- puts "\n"
115
-
116
- context.each_with_index do |message, index|
117
- # Check if there's a checkpoint at this position
118
- if checkpoint_positions[index]
119
- checkpoint_names = checkpoint_positions[index].join(', ')
120
- puts "📍 [Checkpoint: #{checkpoint_names}]"
121
- puts "-" * 40
122
- end
123
-
124
- # Display the message
125
- role_display = message[:role].capitalize
126
- content_preview = message[:content].to_s
127
-
128
- # Truncate long content for display
129
- if content_preview.length > 200
130
- content_preview = content_preview[0..197] + "..."
131
- end
132
-
133
- puts "#{index + 1}. [#{role_display}]: #{content_preview}"
134
- puts ""
135
- end
136
-
137
- # Check if there's a checkpoint at the end (after all messages)
138
- if checkpoint_positions[context.size]
139
- checkpoint_names = checkpoint_positions[context.size].join(', ')
140
- puts "📍 [Checkpoint: #{checkpoint_names}]"
141
- puts "-" * 40
142
- end
143
-
144
- puts "=== End of Context ==="
145
- ''
146
- end
147
-
148
- def self.checkpoint(args, context_manager = nil)
149
- if context_manager.nil?
150
- return "Error: Context manager not available for //checkpoint directive."
151
- end
152
-
153
- name = args.empty? ? nil : args.join(' ').strip
154
- checkpoint_name = context_manager.create_checkpoint(name: name)
155
- puts "Checkpoint '#{checkpoint_name}' created."
156
- ""
157
- end
158
-
159
- def self.restore(args, context_manager = nil)
160
- if context_manager.nil?
161
- return "Error: Context manager not available for //restore directive."
162
- end
163
-
164
- name = args.empty? ? nil : args.join(' ').strip
165
-
166
- if context_manager.restore_checkpoint(name: name)
167
- restored_name = name || context_manager.checkpoint_names.last
168
- "Context restored to checkpoint '#{restored_name}'."
169
- else
170
- if name
171
- "Error: Checkpoint '#{name}' not found. Available checkpoints: #{context_manager.checkpoint_names.join(', ')}"
172
- else
173
- "Error: No checkpoints available to restore."
174
- end
175
- end
176
- end
91
+ # NOTE: clear, review, checkpoint, and restore directives have been moved to
92
+ # lib/aia/directives/checkpoint.rb which uses RubyLLM's Chat.@messages
93
+ # as the source of truth for conversation history.
177
94
 
178
95
  # Set up aliases - these work on the module's singleton class
179
96
  class << self
180
97
  alias_method :cfg, :config
181
98
  alias_method :temp, :temperature
182
99
  alias_method :topp, :top_p
183
- alias_method :context, :review
184
- alias_method :ckp, :checkpoint
185
100
  end
186
101
  end
187
102
  end
@@ -231,9 +231,10 @@ module AIA
231
231
  'review' => 'Display the current conversation context with checkpoint markers',
232
232
  'checkpoint' => 'Create a named checkpoint of the current context',
233
233
  'restore' => 'Restore context to a previous checkpoint',
234
+ 'checkpoints_list' => 'List all available checkpoints',
234
235
 
235
236
  # Utility directives
236
- 'tools' => 'List available tools',
237
+ 'tools' => 'List available tools (optional filter by name substring)',
237
238
  'next' => 'Set the next prompt in the sequence',
238
239
  'pipeline' => 'Set or view the prompt workflow sequence',
239
240
  'terse' => 'Add instruction for concise responses',
@@ -261,6 +262,7 @@ module AIA
261
262
  'temp' => nil, # alias for temperature
262
263
  'topp' => nil, # alias for top_p
263
264
  'context' => nil, # alias for review
265
+ 'ckp' => nil, # alias for checkpoint
264
266
  'cp' => nil, # alias for checkpoint
265
267
  'workflow' => nil, # alias for pipeline
266
268
  'rb' => nil, # alias for ruby
@@ -282,13 +284,18 @@ module AIA
282
284
  AIA::Directives::Utility,
283
285
  AIA::Directives::Configuration,
284
286
  AIA::Directives::Execution,
285
- AIA::Directives::Models
287
+ AIA::Directives::Models,
288
+ AIA::Directives::Checkpoint
286
289
  ]
287
290
 
288
291
  all_directives = {}
289
292
  excluded_methods = ['run', 'initialize', 'private?', 'descriptions', 'aliases', 'build_aliases',
290
293
  'desc', 'method_added', 'register_directive_module', 'process',
291
- 'directive?', 'prefix_size']
294
+ 'directive?', 'prefix_size', 'reset!', 'checkpoint_names',
295
+ 'checkpoint_positions', 'get_chats', 'deep_copy_message',
296
+ 'format_message_content', 'checkpoints', 'checkpoint_counter',
297
+ 'last_checkpoint_name', 'checkpoints=', 'checkpoint_counter=',
298
+ 'last_checkpoint_name=']
292
299
 
293
300
  # Collect directives from all modules
294
301
  all_modules.each do |mod|
@@ -314,7 +321,7 @@ module AIA
314
321
  'temperature' => ['temp'],
315
322
  'top_p' => ['topp'],
316
323
  'review' => ['context'],
317
- 'checkpoint' => ['cp'],
324
+ 'checkpoint' => ['ckp', 'cp'],
318
325
  'pipeline' => ['workflow'],
319
326
  'ruby' => ['rb'],
320
327
  'shell' => ['sh'],
@@ -333,7 +340,7 @@ module AIA
333
340
 
334
341
  # Sort and display directives by category
335
342
  categories = {
336
- 'Configuration' => ['config', 'model', 'temperature', 'top_p', 'clear', 'review', 'checkpoint', 'restore'],
343
+ 'Configuration' => ['config', 'model', 'temperature', 'top_p', 'clear', 'review', 'checkpoint', 'restore', 'checkpoints_list'],
337
344
  'Utility' => ['tools', 'next', 'pipeline', 'terse', 'robot', 'help'],
338
345
  'Execution' => ['ruby', 'shell', 'say'],
339
346
  'Web & Files' => ['webpage', 'include'],
@@ -5,6 +5,7 @@ require_relative 'utility'
5
5
  require_relative 'configuration'
6
6
  require_relative 'execution'
7
7
  require_relative 'models'
8
+ require_relative 'checkpoint'
8
9
 
9
10
  module AIA
10
11
  module Directives
@@ -115,6 +116,7 @@ module AIA
115
116
  register_directive_module(Configuration)
116
117
  register_directive_module(Execution)
117
118
  register_directive_module(Models)
119
+ register_directive_module(Checkpoint)
118
120
  end
119
121
  end
120
122
  end
@@ -12,19 +12,34 @@ module AIA
12
12
  indent = 4
13
13
  spaces = " " * indent
14
14
  width = TTY::Screen.width - indent - 2
15
+ filter = args.first&.downcase
15
16
 
16
17
  if AIA.config.tools.empty?
17
18
  puts "No tools are available"
18
19
  else
19
- puts
20
- puts "Available Tools"
21
- puts "==============="
20
+ tools_to_display = AIA.config.tools
22
21
 
23
- AIA.config.tools.each do |tool|
24
- name = tool.respond_to?(:name) ? tool.name : tool.class.name
25
- puts "\n#{name}"
26
- puts "-" * name.size
27
- puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map { |s| spaces + s + "\n" }.join
22
+ if filter
23
+ tools_to_display = tools_to_display.select do |tool|
24
+ name = tool.respond_to?(:name) ? tool.name : tool.class.name
25
+ name.downcase.include?(filter)
26
+ end
27
+ end
28
+
29
+ if tools_to_display.empty?
30
+ puts "No tools match the filter: #{args.first}"
31
+ else
32
+ puts
33
+ header = filter ? "Available Tools (filtered by '#{args.first}')" : "Available Tools"
34
+ puts header
35
+ puts "=" * header.length
36
+
37
+ tools_to_display.each do |tool|
38
+ name = tool.respond_to?(:name) ? tool.name : tool.class.name
39
+ puts "\n#{name}"
40
+ puts "-" * name.size
41
+ puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map { |s| spaces + s + "\n" }.join
42
+ end
28
43
  end
29
44
  end
30
45
  puts
@@ -5,7 +5,7 @@ require_relative '../extensions/ruby_llm/provider_fix'
5
5
 
6
6
  module AIA
7
7
  class RubyLLMAdapter
8
- attr_reader :tools, :model_specs
8
+ attr_reader :tools, :model_specs, :chats
9
9
 
10
10
  def initialize
11
11
  @model_specs = extract_models_config # Full specs with role info
@@ -228,7 +228,17 @@ module AIA
228
228
 
229
229
  def support_local_tools
230
230
  @tools += ObjectSpace.each_object(Class).select do |klass|
231
- klass < RubyLLM::Tool
231
+ next false unless klass < RubyLLM::Tool
232
+
233
+ # Filter out tools that can't be instantiated without arguments
234
+ # RubyLLM calls tool.new without args, so we must verify each tool works
235
+ begin
236
+ klass.new
237
+ true
238
+ rescue ArgumentError, LoadError, StandardError
239
+ # Skip tools that require arguments or have missing dependencies
240
+ false
241
+ end
232
242
  end
233
243
  end
234
244
 
@@ -634,7 +644,10 @@ module AIA
634
644
 
635
645
  @tools.select! do |tool|
636
646
  tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
637
- AIA.config.allowed_tools.any? { |allowed| tool_name.include?(allowed) }
647
+ AIA.config.allowed_tools
648
+ .split(',')
649
+ .map(&:strip)
650
+ .any? { |allowed| tool_name.include?(allowed) }
638
651
  end
639
652
  end
640
653
 
@@ -644,7 +657,10 @@ module AIA
644
657
 
645
658
  @tools.reject! do |tool|
646
659
  tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
647
- AIA.config.rejected_tools.any? { |rejected| tool_name.include?(rejected) }
660
+ AIA.config.rejected_tools
661
+ .split(',')
662
+ .map(&:strip)
663
+ .any? { |rejected| tool_name.include?(rejected) }
648
664
  end
649
665
  end
650
666
 
data/lib/aia/session.rb CHANGED
@@ -9,7 +9,6 @@ require "fileutils"
9
9
  require "amazing_print"
10
10
  require_relative "directive_processor"
11
11
  require_relative "history_manager"
12
- require_relative "context_manager"
13
12
  require_relative "ui_presenter"
14
13
  require_relative "chat_processor_service"
15
14
  require_relative "prompt_handler"
@@ -45,28 +44,9 @@ module AIA
45
44
  end
46
45
 
47
46
  def initialize_components
48
- # For multi-model: create separate context manager per model (ADR-002 revised + ADR-005)
49
- # For single-model: maintain backward compatibility with single context manager
50
- if AIA.config.model.is_a?(Array) && AIA.config.model.size > 1
51
- @context_managers = {}
52
- AIA.config.model.each do |model_spec|
53
- # Handle both old string format and new hash format (ADR-005)
54
- internal_id = if model_spec.is_a?(Hash)
55
- model_spec[:internal_id]
56
- else
57
- model_spec
58
- end
59
-
60
- @context_managers[internal_id] = ContextManager.new(
61
- system_prompt: AIA.config.system_prompt
62
- )
63
- end
64
- @context_manager = nil # Signal we're using per-model managers
65
- else
66
- @context_manager = ContextManager.new(system_prompt: AIA.config.system_prompt)
67
- @context_managers = nil
68
- end
69
-
47
+ # RubyLLM's Chat instances maintain conversation history internally
48
+ # via @messages array. No separate context manager needed.
49
+ # Checkpoint/restore directives access Chat.@messages directly via AIA.client.chats
70
50
  @ui_presenter = UIPresenter.new
71
51
  @directive_processor = DirectiveProcessor.new
72
52
  @chat_processor = ChatProcessorService.new(@ui_presenter, @directive_processor)
@@ -225,23 +205,21 @@ module AIA
225
205
  end
226
206
 
227
207
  # Send prompt to AI and handle the response
208
+ # RubyLLM's Chat automatically adds user messages and responses to its internal @messages
228
209
  def send_prompt_and_get_response(prompt_text)
229
- # Add prompt to conversation context
230
- @context_manager.add_to_context(role: "user", content: prompt_text)
231
-
232
- # Process the prompt
210
+ # Process the prompt - RubyLLM Chat maintains conversation history internally
233
211
  @ui_presenter.display_thinking_animation
234
- response = @chat_processor.process_prompt(@context_manager.get_context)
212
+ response_data = @chat_processor.process_prompt(prompt_text)
235
213
 
236
- # Add AI response to context
237
- @context_manager.add_to_context(role: "assistant", content: response)
214
+ # Handle response format (may include metrics)
215
+ content = response_data.is_a?(Hash) ? response_data[:content] : response_data
238
216
 
239
217
  # Output the response
240
- @chat_processor.output_response(response)
218
+ @chat_processor.output_response(content)
241
219
 
242
220
  # Process any directives in the response
243
- if @directive_processor.directive?(response)
244
- directive_result = @directive_processor.process(response, @context_manager)
221
+ if @directive_processor.directive?(content)
222
+ directive_result = @directive_processor.process(content, nil)
245
223
  puts "\nDirective output: #{directive_result}" if directive_result && !directive_result.strip.empty?
246
224
  end
247
225
  end
@@ -315,19 +293,16 @@ module AIA
315
293
 
316
294
  return if context.empty?
317
295
 
318
- # Add context files content to context
319
- @context_manager.add_to_context(role: "user", content: context)
320
-
321
- # Process the context
296
+ # Process the context - RubyLLM Chat maintains conversation history internally
322
297
  @ui_presenter.display_thinking_animation
323
- response = @chat_processor.process_prompt(@context_manager.get_context)
298
+ response_data = @chat_processor.process_prompt(context)
324
299
 
325
- # Add AI response to context
326
- @context_manager.add_to_context(role: "assistant", content: response)
300
+ # Handle response format (may include metrics)
301
+ content = response_data.is_a?(Hash) ? response_data[:content] : response_data
327
302
 
328
303
  # Output the response
329
- @chat_processor.output_response(response)
330
- @chat_processor.speak(response)
304
+ @chat_processor.output_response(content)
305
+ @chat_processor.speak(content)
331
306
  @ui_presenter.display_separator
332
307
  end
333
308
 
@@ -347,14 +322,15 @@ module AIA
347
322
  @chat_prompt.text = piped_input
348
323
  processed_input = @chat_prompt.to_s
349
324
 
350
- @context_manager.add_to_context(role: "user", content: processed_input)
351
-
325
+ # Process the piped input - RubyLLM Chat maintains conversation history internally
352
326
  @ui_presenter.display_thinking_animation
353
- response = @chat_processor.process_prompt(@context_manager.get_context)
327
+ response_data = @chat_processor.process_prompt(processed_input)
328
+
329
+ # Handle response format (may include metrics)
330
+ content = response_data.is_a?(Hash) ? response_data[:content] : response_data
354
331
 
355
- @context_manager.add_to_context(role: "assistant", content: response)
356
- @chat_processor.output_response(response)
357
- @chat_processor.speak(response) if AIA.speak?
332
+ @chat_processor.output_response(content)
333
+ @chat_processor.speak(content) if AIA.speak?
358
334
  @ui_presenter.display_separator
359
335
 
360
336
  STDIN.reopen(original_stdin)
@@ -389,29 +365,10 @@ module AIA
389
365
  @chat_prompt.text = follow_up_prompt
390
366
  processed_prompt = @chat_prompt.to_s
391
367
 
392
- # Handle per-model contexts (ADR-002 revised)
393
- if @context_managers
394
- # Multi-model: add user prompt to each model's context
395
- @context_managers.each_value do |ctx_mgr|
396
- ctx_mgr.add_to_context(role: "user", content: processed_prompt)
397
- end
398
-
399
- # Get per-model conversations
400
- conversations = {}
401
- @context_managers.each do |model_name, ctx_mgr|
402
- conversations[model_name] = ctx_mgr.get_context
403
- end
404
-
405
- @ui_presenter.display_thinking_animation
406
- response_data = @chat_processor.process_prompt(conversations)
407
- else
408
- # Single-model: use original logic
409
- @context_manager.add_to_context(role: "user", content: processed_prompt)
410
- conversation = @context_manager.get_context
411
-
412
- @ui_presenter.display_thinking_animation
413
- response_data = @chat_processor.process_prompt(conversation)
414
- end
368
+ # Process the prompt - RubyLLM Chat maintains conversation history internally
369
+ # via @messages array. Each model's Chat instance tracks its own conversation.
370
+ @ui_presenter.display_thinking_animation
371
+ response_data = @chat_processor.process_prompt(processed_prompt)
415
372
 
416
373
  # Handle new response format with metrics
417
374
  if response_data.is_a?(Hash)
@@ -437,21 +394,6 @@ module AIA
437
394
  end
438
395
  end
439
396
 
440
- # Add responses to context (ADR-002 revised)
441
- if @context_managers
442
- # Multi-model: parse combined response and add each model's response to its own context
443
- parsed_responses = parse_multi_model_response(content)
444
- parsed_responses.each do |model_name, model_response|
445
- @context_managers[model_name]&.add_to_context(
446
- role: "assistant",
447
- content: model_response
448
- )
449
- end
450
- else
451
- # Single-model: add response to single context
452
- @context_manager.add_to_context(role: "assistant", content: content)
453
- end
454
-
455
397
  @chat_processor.speak(content)
456
398
 
457
399
  @ui_presenter.display_separator
@@ -459,71 +401,21 @@ module AIA
459
401
  end
460
402
 
461
403
  def process_chat_directive(follow_up_prompt)
462
- # For multi-model, use first context manager for directives (ADR-002 revised)
463
- # TODO: Consider if directives should affect all contexts or just one
464
- context_for_directive = @context_managers ? @context_managers.values.first : @context_manager
465
- directive_output = @directive_processor.process(follow_up_prompt, context_for_directive)
466
-
467
- return handle_clear_directive if follow_up_prompt.strip.start_with?("//clear")
468
- return handle_checkpoint_directive(directive_output) if follow_up_prompt.strip.start_with?("//checkpoint")
469
- return handle_restore_directive(directive_output) if follow_up_prompt.strip.start_with?("//restore")
470
- return handle_empty_directive_output if directive_output.nil? || directive_output.strip.empty?
471
-
472
- handle_successful_directive(follow_up_prompt, directive_output)
473
- end
474
-
475
- def handle_clear_directive
476
- # Clear context manager(s) - ADR-002 revised
477
- if @context_managers
478
- # Multi-model: clear all context managers
479
- @context_managers.each_value { |ctx_mgr| ctx_mgr.clear_context(keep_system_prompt: true) }
480
- else
481
- # Single-model: clear single context manager
482
- @context_manager.clear_context(keep_system_prompt: true)
483
- end
484
-
485
- # Try clearing the client's context
486
- if AIA.config.client && AIA.config.client.respond_to?(:clear_context)
487
- begin
488
- AIA.config.client.clear_context
489
- rescue => e
490
- STDERR.puts "Warning: Error clearing client context: #{e.message}"
491
- # Continue anyway - the context manager has been cleared which is the main goal
492
- end
404
+ # Directives now access RubyLLM's Chat.@messages directly via AIA.client
405
+ # The second parameter is no longer used by checkpoint/restore/clear/review
406
+ directive_output = @directive_processor.process(follow_up_prompt, nil)
407
+
408
+ # Checkpoint-related directives (clear, checkpoint, restore, review) handle
409
+ # everything internally via the Checkpoint module, which operates directly
410
+ # on RubyLLM's Chat.@messages - no additional handling needed here.
411
+ if follow_up_prompt.strip.start_with?("//clear", "//checkpoint", "//restore", "//review", "//context")
412
+ @ui_presenter.display_info(directive_output) unless directive_output.nil? || directive_output.strip.empty?
413
+ return nil
493
414
  end
494
415
 
495
- # Note: We intentionally do NOT reinitialize the client here
496
- # as that could cause termination if model initialization fails
497
-
498
- @ui_presenter.display_info("Chat context cleared.")
499
- nil
500
- end
501
-
502
- def handle_checkpoint_directive(directive_output)
503
- @ui_presenter.display_info(directive_output)
504
- nil
505
- end
506
-
507
- def handle_restore_directive(directive_output)
508
- # If the restore was successful, we also need to refresh the client's context - ADR-002 revised
509
- if directive_output.start_with?("Context restored")
510
- # Clear the client's context without reinitializing the entire adapter
511
- if AIA.config.client && AIA.config.client.respond_to?(:clear_context)
512
- begin
513
- AIA.config.client.clear_context
514
- rescue => e
515
- STDERR.puts "Warning: Error clearing client context after restore: #{e.message}"
516
- # Continue anyway - the context manager has been restored which is the main goal
517
- end
518
- end
519
-
520
- # Note: For multi-model, only the first context manager was used for restore
521
- # This is a limitation of the current directive system
522
- # TODO: Consider supporting restore for all context managers
523
- end
416
+ return handle_empty_directive_output if directive_output.nil? || directive_output.strip.empty?
524
417
 
525
- @ui_presenter.display_info(directive_output)
526
- nil
418
+ handle_successful_directive(follow_up_prompt, directive_output)
527
419
  end
528
420
 
529
421
  def handle_empty_directive_output
@@ -0,0 +1,125 @@
1
+ # lib/aia/topic_context.rb
2
+ # Just thinking about the problem ...
3
+ # maybe a directive like //topic [topic]
4
+ # sets manually (when present) or dynamically when not present
5
+ # and //topics - will list current topics
6
+ # thinking about the //checkpoint and //restore directives
7
+ #
8
+ module AIA
9
+ class TopicContext
10
+ attr_reader :context_size
11
+
12
+ # Initialize topic context manager
13
+ # @param context_size [Integer] max allowed bytes per topic
14
+ def initialize(context_size = 128_000)
15
+ @storage = Hash.new { |h, k| h[k] = [] } # auto-initialize empty array
16
+ @context_size = context_size
17
+ @total_chars = 0
18
+ @mutex = Mutex.new # ensure thread safety
19
+ end
20
+
21
+ # Store a request/response pair under the given topic (or auto-generate one)
22
+ # @param request [String]
23
+ # @param response [String]
24
+ # @param topic [String, nil]
25
+ # @return [String] topic name used
26
+ def store_conversation(request, response, topic = nil)
27
+ raise ArgumentError, "request and response must be strings" unless request.is_a?(String) && response.is_a?(String)
28
+
29
+ topic ||= generate_topic(request)
30
+ size = request.bytesize + response.bytesize
31
+
32
+ @mutex.synchronize do
33
+ # Add the new context
34
+ @storage[topic] << { request:, response:, size:, time: Time.now }
35
+
36
+ # Update the global total
37
+ @total_chars += size
38
+
39
+ # Trim old entries if we exceeded the per-topic limit
40
+ trim_topic(topic)
41
+ end
42
+
43
+ topic
44
+ end
45
+
46
+ # Return an array of contexts for the given topic
47
+ # @param topic [String]
48
+ # @return [Array<Hash>]
49
+ def get_conversation(topic)
50
+ @mutex.synchronize { @storage[topic] || [] }
51
+ end
52
+
53
+ # All topic names
54
+ # @return [Array<String>]
55
+ def topics
56
+ @mutex.synchronize { @storage.keys }
57
+ end
58
+
59
+ # Hash of topic => array_of_contexts
60
+ # @return [Hash<String, Array<Hash>>]
61
+ def all_conversations
62
+ @mutex.synchronize { @storage.dup }
63
+ end
64
+
65
+ # Total number of characters stored across all topics
66
+ # @return [Integer]
67
+ def total_chars
68
+ @mutex.synchronize { @total_chars }
69
+ end
70
+
71
+ # Empty the storage and reset counters
72
+ def clear
73
+ @mutex.synchronize do
74
+ @storage.clear
75
+ @total_chars = 0
76
+ end
77
+ end
78
+
79
+ # Get memory usage statistics for a topic
80
+ # @param topic [String]
81
+ # @return [Hash{Symbol => Integer}]
82
+ def topic_stats(topic)
83
+ @mutex.synchronize do
84
+ return {} unless @storage.key?(topic)
85
+
86
+ {
87
+ count: @storage[topic].length,
88
+ size: topic_total_size(topic),
89
+ avg_size: topic_total_size(topic).fdiv(@storage[topic].length),
90
+ }
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ # Topic extractor with better heuristic - uses first meaningful 3 words
97
+ # @param request [String]
98
+ # @return [String]
99
+ def generate_topic(request)
100
+ cleaned = request.downcase.gsub(/[^a-z0-9\s]/, "")
101
+ words = cleaned.split
102
+ return "general" if words.empty?
103
+
104
+ words.first(3).join("_")
105
+ end
106
+
107
+ # Remove oldest contexts from the topic until size <= @context_size
108
+ # @param topic [String]
109
+ def trim_topic(topic)
110
+ return unless @storage.key?(topic) && @storage[topic].size > 1
111
+
112
+ while topic_total_size(topic) > @context_size
113
+ removed = @storage[topic].shift # oldest context
114
+ @total_chars -= removed[:size] # adjust global counter
115
+ end
116
+ end
117
+
118
+ # Helper to compute the sum of sizes for a topic
119
+ # @param topic [String]
120
+ # @return [Integer]
121
+ def topic_total_size(topic)
122
+ @storage[topic].sum { |ctx| ctx[:size] }
123
+ end
124
+ end
125
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aia
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.23
4
+ version: 0.9.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -426,8 +426,8 @@ files:
426
426
  - lib/aia/config/defaults.rb
427
427
  - lib/aia/config/file_loader.rb
428
428
  - lib/aia/config/validator.rb
429
- - lib/aia/context_manager.rb
430
429
  - lib/aia/directive_processor.rb
430
+ - lib/aia/directives/checkpoint.rb
431
431
  - lib/aia/directives/configuration.rb
432
432
  - lib/aia/directives/execution.rb
433
433
  - lib/aia/directives/models.rb
@@ -439,6 +439,7 @@ files:
439
439
  - lib/aia/prompt_handler.rb
440
440
  - lib/aia/ruby_llm_adapter.rb
441
441
  - lib/aia/session.rb
442
+ - lib/aia/topic_context.rb
442
443
  - lib/aia/ui_presenter.rb
443
444
  - lib/aia/utility.rb
444
445
  - lib/aia/version.rb
@@ -477,7 +478,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
477
478
  - !ruby/object:Gem::Version
478
479
  version: '0'
479
480
  requirements: []
480
- rubygems_version: 4.0.0
481
+ rubygems_version: 4.0.1
481
482
  specification_version: 4
482
483
  summary: Multi-model AI CLI with dynamic prompts, consensus responses, shell & Ruby
483
484
  integration, and seamless chat workflows.
@@ -1,134 +0,0 @@
1
- # lib/aia/context_manager.rb
2
-
3
- module AIA
4
- # Manages the conversation context for chat sessions.
5
- class ContextManager
6
- attr_reader :context, :checkpoints
7
-
8
- # Initializes the ContextManager with an optional system prompt.
9
- def initialize(system_prompt: nil)
10
- @context = []
11
- @checkpoints = {}
12
- @checkpoint_counter = 0
13
- add_system_prompt(system_prompt) if system_prompt && !system_prompt.strip.empty?
14
- end
15
-
16
- # Adds a message to the conversation context.
17
- #
18
- # @param role [String] The role of the message sender ('user' or 'assistant').
19
- # @param content [String] The content of the message.
20
- def add_to_context(role:, content:)
21
- @context << { role: role, content: content }
22
- end
23
-
24
- # Returns the current conversation context.
25
- # Optionally adds the system prompt if it wasn't added during initialization
26
- # or needs to be re-added after clearing.
27
- #
28
- # @param system_prompt [String, nil] The system prompt to potentially prepend.
29
- # @return [Array<Hash>] The conversation context array.
30
- def get_context(system_prompt: nil)
31
- # Add or replace system prompt if provided and not empty
32
- if system_prompt && !system_prompt.strip.empty?
33
- add_system_prompt(system_prompt)
34
- end
35
- @context
36
- end
37
-
38
- # Clears the conversation context, optionally keeping the system prompt.
39
- #
40
- # @param keep_system_prompt [Boolean] Whether to retain the initial system prompt.
41
- def clear_context(keep_system_prompt: true)
42
- if keep_system_prompt && !@context.empty? && @context.first[:role] == 'system'
43
- @context = [@context.first]
44
- else
45
- @context = []
46
- end
47
-
48
- # Clear all checkpoints when clearing context
49
- @checkpoints.clear
50
- @checkpoint_counter = 0
51
-
52
- # Attempt to clear the LLM client's context as well
53
- begin
54
- if AIA.config.client && AIA.config.client.respond_to?(:clear_context)
55
- AIA.config.client.clear_context
56
- end
57
-
58
- if AIA.config.respond_to?(:llm) && AIA.config.llm && AIA.config.llm.respond_to?(:clear_context)
59
- AIA.config.llm.clear_context
60
- end
61
-
62
- if defined?(RubyLLM) && RubyLLM.respond_to?(:chat) && RubyLLM.chat.respond_to?(:clear_history)
63
- RubyLLM.chat.clear_history
64
- end
65
- rescue => e
66
- STDERR.puts "ERROR: context_manager clear_context error #{e.message}"
67
- end
68
- end
69
-
70
- # Creates a checkpoint of the current context with an optional name.
71
- #
72
- # @param name [String, nil] The name of the checkpoint. If nil, uses an incrementing integer.
73
- # @return [String] The name of the created checkpoint.
74
- def create_checkpoint(name: nil)
75
- if name.nil?
76
- @checkpoint_counter += 1
77
- name = @checkpoint_counter.to_s
78
- end
79
-
80
- # Store a deep copy of the current context and its position
81
- @checkpoints[name] = {
82
- context: @context.map(&:dup),
83
- position: @context.size
84
- }
85
- @last_checkpoint_name = name
86
- name
87
- end
88
-
89
- # Restores the context to a previously saved checkpoint.
90
- #
91
- # @param name [String, nil] The name of the checkpoint to restore. If nil, uses the last checkpoint.
92
- # @return [Boolean] True if restore was successful, false otherwise.
93
- def restore_checkpoint(name: nil)
94
- name = @last_checkpoint_name if name.nil?
95
-
96
- return false if name.nil? || !@checkpoints.key?(name)
97
-
98
- # Restore the context from the checkpoint
99
- checkpoint_data = @checkpoints[name]
100
- @context = checkpoint_data[:context].map(&:dup)
101
- true
102
- end
103
-
104
- # Returns the list of available checkpoint names.
105
- #
106
- # @return [Array<String>] The names of all checkpoints.
107
- def checkpoint_names
108
- @checkpoints.keys
109
- end
110
-
111
- # Returns checkpoint information mapped to context positions.
112
- #
113
- # @return [Hash<Integer, Array<String>>] Position to checkpoint names mapping.
114
- def checkpoint_positions
115
- positions = {}
116
- @checkpoints.each do |name, data|
117
- position = data[:position]
118
- positions[position] ||= []
119
- positions[position] << name
120
- end
121
- positions
122
- end
123
-
124
- private
125
-
126
- # Adds or replaces the system prompt at the beginning of the context.
127
- def add_system_prompt(system_prompt)
128
- # Remove existing system prompt if present
129
- @context.shift if !@context.empty? && @context.first[:role] == 'system'
130
- # Add the new system prompt at the beginning
131
- @context.unshift({ role: 'system', content: system_prompt })
132
- end
133
- end
134
- end