ollama_agent 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +14 -3
- data/lib/ollama_agent/agent/agent_config.rb +19 -2
- data/lib/ollama_agent/agent/client_wiring.rb +3 -8
- data/lib/ollama_agent/agent/session_wiring.rb +37 -3
- data/lib/ollama_agent/agent.rb +82 -6
- data/lib/ollama_agent/cli/repl.rb +159 -0
- data/lib/ollama_agent/cli/repl_shared.rb +229 -0
- data/lib/ollama_agent/cli/tui_repl.rb +149 -0
- data/lib/ollama_agent/cli.rb +129 -49
- data/lib/ollama_agent/core/action_envelope.rb +82 -0
- data/lib/ollama_agent/core/budget.rb +90 -0
- data/lib/ollama_agent/core/loop_detector.rb +67 -0
- data/lib/ollama_agent/core/schema_validator.rb +136 -0
- data/lib/ollama_agent/core/trace_logger.rb +138 -0
- data/lib/ollama_agent/external_agents/probe.rb +23 -3
- data/lib/ollama_agent/indexing/context_packer.rb +140 -0
- data/lib/ollama_agent/indexing/diff_summarizer.rb +125 -0
- data/lib/ollama_agent/indexing/file_indexer.rb +129 -0
- data/lib/ollama_agent/indexing/repo_scanner.rb +158 -0
- data/lib/ollama_agent/memory/long_term.rb +109 -0
- data/lib/ollama_agent/memory/manager.rb +121 -0
- data/lib/ollama_agent/memory/session_memory.rb +93 -0
- data/lib/ollama_agent/memory/short_term.rb +66 -0
- data/lib/ollama_agent/ollama_cloud_catalog.rb +66 -0
- data/lib/ollama_agent/ollama_connection.rb +30 -0
- data/lib/ollama_agent/plugins/loader.rb +95 -0
- data/lib/ollama_agent/plugins/registry.rb +103 -0
- data/lib/ollama_agent/providers/anthropic.rb +245 -0
- data/lib/ollama_agent/providers/base.rb +79 -0
- data/lib/ollama_agent/providers/ollama.rb +118 -0
- data/lib/ollama_agent/providers/openai.rb +215 -0
- data/lib/ollama_agent/providers/registry.rb +76 -0
- data/lib/ollama_agent/providers/router.rb +93 -0
- data/lib/ollama_agent/resilience/retry_middleware.rb +5 -0
- data/lib/ollama_agent/runner.rb +25 -4
- data/lib/ollama_agent/runtime/approval_gate.rb +74 -0
- data/lib/ollama_agent/runtime/permissions.rb +103 -0
- data/lib/ollama_agent/runtime/policies.rb +100 -0
- data/lib/ollama_agent/runtime/sandbox.rb +130 -0
- data/lib/ollama_agent/streaming/hooks.rb +3 -1
- data/lib/ollama_agent/tools/base.rb +108 -0
- data/lib/ollama_agent/tools/git_tools.rb +176 -0
- data/lib/ollama_agent/tools/http_tools.rb +202 -0
- data/lib/ollama_agent/tools/memory_tools.rb +116 -0
- data/lib/ollama_agent/tools/shell_tools.rb +208 -0
- data/lib/ollama_agent/tui.rb +183 -0
- data/lib/ollama_agent/tui_slash_reader.rb +147 -0
- data/lib/ollama_agent/tui_user_prompt.rb +45 -0
- data/lib/ollama_agent/version.rb +1 -1
- data/lib/ollama_agent.rb +46 -1
- metadata +142 -5
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
class CLI
|
|
5
|
+
# Slash-command handlers shared by {Repl} and {TuiRepl}.
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength, Layout/HashAlignment, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Style/RescueModifier, Style/NumericPredicate -- legacy Repl extraction
|
|
7
|
+
module ReplShared
|
|
8
|
+
SLASH_COMMANDS = {
|
|
9
|
+
"/help" => "Show this help message",
|
|
10
|
+
"/status" => "Show run budget, provider, memory summary",
|
|
11
|
+
"/session" => "Show or switch session (usage: /session [id])",
|
|
12
|
+
"/memory" => "Query long-term memory (usage: /memory [key])",
|
|
13
|
+
"/remember" => "Store a fact (usage: /remember key = value)",
|
|
14
|
+
"/clear" => "Clear short-term context for this session",
|
|
15
|
+
"/config" => "Show current agent configuration",
|
|
16
|
+
"/model" => "Show or set chat model (usage: /model [name] | /model list)",
|
|
17
|
+
"/models" => "List Ollama cloud catalog (ollama.com/api/tags); /model <name> to switch",
|
|
18
|
+
"/provider" => "Show or switch provider (usage: /provider [name])",
|
|
19
|
+
"/index" => "Summarise the project repository index",
|
|
20
|
+
"/exit" => "Exit the REPL"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def repl_memory
|
|
26
|
+
@memory || @agent.instance_variable_get(:@memory_manager)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def repl_budget
|
|
30
|
+
@budget || @agent.instance_variable_get(:@budget)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def slash_completer_candidates
|
|
34
|
+
base = SLASH_COMMANDS.keys
|
|
35
|
+
extras = plugin_slash_command_strings
|
|
36
|
+
(base + extras).uniq.sort
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def plugin_slash_command_strings
|
|
40
|
+
OllamaAgent::Plugins::Registry.all_command_handlers.map { |h| h[:slash_command].to_s }
|
|
41
|
+
rescue StandardError
|
|
42
|
+
[]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def handle_slash(line)
|
|
46
|
+
parts = line.split(" ", 2)
|
|
47
|
+
command = parts[0].downcase
|
|
48
|
+
arg = parts[1]
|
|
49
|
+
|
|
50
|
+
case command
|
|
51
|
+
when "/help" then print_help
|
|
52
|
+
when "/status" then print_status
|
|
53
|
+
when "/session" then handle_session(arg)
|
|
54
|
+
when "/memory" then handle_memory(arg)
|
|
55
|
+
when "/remember" then handle_remember(arg)
|
|
56
|
+
when "/clear" then handle_clear
|
|
57
|
+
when "/config" then print_config
|
|
58
|
+
when "/model" then handle_model(arg)
|
|
59
|
+
when "/models" then print_model_list
|
|
60
|
+
when "/provider" then handle_provider(arg)
|
|
61
|
+
when "/index" then handle_index
|
|
62
|
+
else
|
|
63
|
+
check_plugin_commands(command, arg)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def print_help
|
|
68
|
+
@stdout.puts "\n\e[1mSlash commands:\e[0m"
|
|
69
|
+
SLASH_COMMANDS.each do |cmd, desc|
|
|
70
|
+
@stdout.puts " \e[33m#{cmd.ljust(14)}\e[0m #{desc}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
plugin_cmds = OllamaAgent::Plugins::Registry.all_command_handlers rescue []
|
|
74
|
+
if plugin_cmds.any?
|
|
75
|
+
@stdout.puts "\n\e[1mPlugin commands:\e[0m"
|
|
76
|
+
plugin_cmds.each { |h| @stdout.puts " \e[35m#{h[:slash_command]}\e[0m" }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@stdout.puts ""
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def print_status
|
|
83
|
+
@stdout.puts "\n\e[1mStatus:\e[0m"
|
|
84
|
+
@stdout.puts " Model: #{@agent.model}"
|
|
85
|
+
|
|
86
|
+
if (b = repl_budget)
|
|
87
|
+
h = b.to_h
|
|
88
|
+
@stdout.puts " Steps: #{h[:steps]} / #{h[:max_steps]}"
|
|
89
|
+
@stdout.puts " Tokens: #{h[:tokens_used]} / #{h[:max_tokens]}"
|
|
90
|
+
@stdout.puts " Cost: $#{h[:cost_usd].round(4)}" if h[:cost_usd] > 0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
mem = repl_memory
|
|
94
|
+
if mem
|
|
95
|
+
s = mem.summary
|
|
96
|
+
@stdout.puts " Memory: #{s[:short_term_entries]} short-term, " \
|
|
97
|
+
"#{s[:session_keys]} session keys, " \
|
|
98
|
+
"#{s[:long_term_namespaces]} LT namespaces"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
@stdout.puts ""
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def handle_session(arg)
|
|
105
|
+
if arg
|
|
106
|
+
@stdout.puts " Switching session is not supported mid-run. " \
|
|
107
|
+
"Restart with: ollama_agent ask --session #{arg} --resume"
|
|
108
|
+
else
|
|
109
|
+
id = @agent.instance_variable_get(:@session_id) rescue nil
|
|
110
|
+
@stdout.puts " Current session: #{id || "(none)"}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_memory(arg)
|
|
115
|
+
return print_memory_list unless arg
|
|
116
|
+
|
|
117
|
+
mem = repl_memory
|
|
118
|
+
val = mem&.recall(arg)
|
|
119
|
+
if val
|
|
120
|
+
@stdout.puts " \e[33m#{arg}\e[0m = #{val}"
|
|
121
|
+
else
|
|
122
|
+
@stdout.puts " No memory found for: #{arg}"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def print_memory_list
|
|
127
|
+
mem = repl_memory
|
|
128
|
+
return @stdout.puts " No memory manager attached" unless mem
|
|
129
|
+
|
|
130
|
+
entries = mem.list
|
|
131
|
+
if entries.empty?
|
|
132
|
+
@stdout.puts " No long-term memories stored yet."
|
|
133
|
+
else
|
|
134
|
+
@stdout.puts "\n\e[1mLong-term memory:\e[0m"
|
|
135
|
+
entries.each { |k, v| @stdout.puts " \e[33m#{k}\e[0m: #{v.to_s[0, 80]}" }
|
|
136
|
+
end
|
|
137
|
+
@stdout.puts ""
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def handle_remember(arg)
|
|
141
|
+
return @stdout.puts " Usage: /remember key = value" unless arg&.include?("=")
|
|
142
|
+
|
|
143
|
+
mem = repl_memory
|
|
144
|
+
key, value = arg.split("=", 2).map(&:strip)
|
|
145
|
+
mem&.remember(key, value, tier: :long_term)
|
|
146
|
+
@stdout.puts " Stored: \e[33m#{key}\e[0m = #{value}"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def handle_clear
|
|
150
|
+
repl_memory&.flush_short_term!
|
|
151
|
+
@stdout.puts " Short-term memory cleared."
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def print_config
|
|
155
|
+
@stdout.puts "\n\e[1mConfiguration:\e[0m"
|
|
156
|
+
ivars = %i[@model @root @read_only @max_tokens @session_id @orchestrator]
|
|
157
|
+
ivars.each do |ivar|
|
|
158
|
+
val = @agent.instance_variable_get(ivar) rescue nil
|
|
159
|
+
next if val.nil?
|
|
160
|
+
|
|
161
|
+
@stdout.puts " \e[36m#{ivar.to_s.delete_prefix("@").ljust(16)}\e[0m #{val}"
|
|
162
|
+
end
|
|
163
|
+
@stdout.puts ""
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def handle_provider(arg)
|
|
167
|
+
if arg
|
|
168
|
+
@stdout.puts " Provider switching mid-run is not yet supported. Restart with --provider #{arg}"
|
|
169
|
+
@stdout.puts " Chat model can be changed anytime: /model <name>"
|
|
170
|
+
else
|
|
171
|
+
@stdout.puts " Current provider: #{@agent.instance_variable_get(:@provider_name) || "ollama"}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def handle_model(arg)
|
|
176
|
+
return print_current_model if arg.nil? || arg.strip.empty?
|
|
177
|
+
|
|
178
|
+
if arg.strip.casecmp("list").zero?
|
|
179
|
+
print_model_list
|
|
180
|
+
return
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
name = @agent.assign_chat_model!(arg)
|
|
184
|
+
@stdout.puts " Chat model set to: #{name}"
|
|
185
|
+
rescue OllamaAgent::Error => e
|
|
186
|
+
@stdout.puts " #{e.message}"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def print_current_model
|
|
190
|
+
@stdout.puts " Current chat model: #{@agent.model}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def print_model_list
|
|
194
|
+
names = @agent.list_cloud_model_names
|
|
195
|
+
if names.empty?
|
|
196
|
+
@stdout.puts " Could not load the cloud model catalog (network or ollama.com)."
|
|
197
|
+
@stdout.puts " Set OLLAMA_API_KEY if your account requires it; or set /model <name> manually."
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
@stdout.puts "\n\e[1mOllama cloud models (ollama.com/api/tags):\e[0m"
|
|
202
|
+
names.each { |n| @stdout.puts " #{n}" }
|
|
203
|
+
@stdout.puts ""
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def handle_index
|
|
207
|
+
root = @agent.instance_variable_get(:@root) || Dir.pwd
|
|
208
|
+
packer = OllamaAgent::Indexing::ContextPacker.new(root: root) rescue nil
|
|
209
|
+
if packer
|
|
210
|
+
@stdout.puts packer.repo_summary
|
|
211
|
+
else
|
|
212
|
+
@stdout.puts " Index unavailable — require 'ollama_agent/indexing/context_packer' first"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def check_plugin_commands(command, arg)
|
|
217
|
+
handlers = OllamaAgent::Plugins::Registry.all_command_handlers rescue []
|
|
218
|
+
match = handlers.find { |h| h[:slash_command] == command }
|
|
219
|
+
|
|
220
|
+
if match
|
|
221
|
+
match[:handler].call(arg, agent: @agent, stdout: @stdout)
|
|
222
|
+
else
|
|
223
|
+
@stdout.puts " Unknown command: #{command}. Type /help for available commands."
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
# rubocop:enable Metrics/ModuleLength, Layout/HashAlignment, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength, Style/RescueModifier, Style/NumericPredicate
|
|
228
|
+
end
|
|
229
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../tui"
|
|
4
|
+
require_relative "../tui_user_prompt"
|
|
5
|
+
require_relative "repl_shared"
|
|
6
|
+
|
|
7
|
+
module OllamaAgent
|
|
8
|
+
class CLI
|
|
9
|
+
# Interactive REPL using TTY toolkit (box, table, markdown, prompt).
|
|
10
|
+
# rubocop:disable Metrics/ClassLength -- session loop + dashboard + agent wiring
|
|
11
|
+
class TuiRepl
|
|
12
|
+
include ReplShared
|
|
13
|
+
|
|
14
|
+
# rubocop:disable Metrics/ParameterLists -- mirrors {Repl} IO + optional memory injection
|
|
15
|
+
def initialize(agent:, tui:, stdout: $stdout, stderr: $stderr, memory: nil, budget: nil)
|
|
16
|
+
@agent = agent
|
|
17
|
+
@tui = tui
|
|
18
|
+
@stdout = stdout
|
|
19
|
+
@stderr = stderr
|
|
20
|
+
@memory = memory
|
|
21
|
+
@budget = budget
|
|
22
|
+
@assistant_hook_installed = false
|
|
23
|
+
end
|
|
24
|
+
# rubocop:enable Metrics/ParameterLists
|
|
25
|
+
|
|
26
|
+
# rubocop:disable Metrics/MethodLength -- straight-line session loop
|
|
27
|
+
def start
|
|
28
|
+
show_boot_dashboard
|
|
29
|
+
@tui.log(:info, "Session ready — type / then Tab to complete slash commands; /help lists all.")
|
|
30
|
+
|
|
31
|
+
loop do
|
|
32
|
+
line = read_user_line
|
|
33
|
+
break if line.nil?
|
|
34
|
+
|
|
35
|
+
line = line.to_s.chomp.strip
|
|
36
|
+
next if line.empty?
|
|
37
|
+
|
|
38
|
+
break if %w[/exit exit].include?(line)
|
|
39
|
+
|
|
40
|
+
if line.start_with?("/")
|
|
41
|
+
dispatch_slash(line)
|
|
42
|
+
else
|
|
43
|
+
run_agent_query(line)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@tui.goodbye
|
|
48
|
+
end
|
|
49
|
+
# rubocop:enable Metrics/MethodLength
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def read_user_line
|
|
54
|
+
@tui.ask_user_line(completion_candidates: slash_completer_candidates)
|
|
55
|
+
rescue Interrupt
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dispatch_slash(line)
|
|
60
|
+
if line == "/status"
|
|
61
|
+
show_context_dashboard
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
handle_slash(line)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# rubocop:disable Metrics/MethodLength -- capture hook + errors + ensure
|
|
69
|
+
def run_agent_query(query)
|
|
70
|
+
ensure_assistant_hook
|
|
71
|
+
@capture_assistant = true
|
|
72
|
+
@pending_messages = []
|
|
73
|
+
@agent.run(query)
|
|
74
|
+
flush_assistant_messages
|
|
75
|
+
rescue OllamaAgent::Error => e
|
|
76
|
+
@tui.print_error("Error: #{e.message}")
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
@tui.print_error("#{e.class}: #{e.message}")
|
|
79
|
+
@tui.print_error(e.backtrace.first(5).join("\n")) if ENV["OLLAMA_AGENT_DEBUG"] == "1"
|
|
80
|
+
ensure
|
|
81
|
+
@capture_assistant = false
|
|
82
|
+
end
|
|
83
|
+
# rubocop:enable Metrics/MethodLength
|
|
84
|
+
|
|
85
|
+
def ensure_assistant_hook
|
|
86
|
+
return if @assistant_hook_installed
|
|
87
|
+
|
|
88
|
+
@agent.hooks.on(:on_assistant_message) do |payload|
|
|
89
|
+
@pending_messages << payload[:message] if @capture_assistant
|
|
90
|
+
end
|
|
91
|
+
@assistant_hook_installed = true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def flush_assistant_messages
|
|
95
|
+
@pending_messages.each { |m| @tui.render_assistant_message(m) }
|
|
96
|
+
@pending_messages.clear
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def show_boot_dashboard
|
|
100
|
+
show_context_dashboard
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# rubocop:disable Metrics/MethodLength -- single dashboard assembly call
|
|
104
|
+
def show_context_dashboard
|
|
105
|
+
skills = skills_summary_for_agent
|
|
106
|
+
scripts = scripts_placeholder
|
|
107
|
+
mem_line = memory_summary_line
|
|
108
|
+
@tui.render_dashboard(
|
|
109
|
+
config: dashboard_config_hash,
|
|
110
|
+
skills: skills,
|
|
111
|
+
scripts: scripts,
|
|
112
|
+
status: "ACTIVE",
|
|
113
|
+
budget: repl_budget,
|
|
114
|
+
memory_line: mem_line
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
# rubocop:enable Metrics/MethodLength
|
|
118
|
+
|
|
119
|
+
def dashboard_config_hash
|
|
120
|
+
{
|
|
121
|
+
model: @agent.instance_variable_get(:@model),
|
|
122
|
+
endpoint: ENV.fetch("OLLAMA_BASE_URL", "localhost (default)")
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def skills_summary_for_agent
|
|
127
|
+
enabled = @agent.instance_variable_get(:@skills_enabled)
|
|
128
|
+
paths = @agent.instance_variable_get(:@skill_paths)
|
|
129
|
+
parts = []
|
|
130
|
+
parts << (enabled == false ? "off" : "on")
|
|
131
|
+
parts << "paths: #{Array(paths).join(", ")}" if paths && !paths.empty?
|
|
132
|
+
parts.join(" · ")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def scripts_placeholder
|
|
136
|
+
[]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def memory_summary_line
|
|
140
|
+
mem = repl_memory
|
|
141
|
+
return nil unless mem
|
|
142
|
+
|
|
143
|
+
s = mem.summary
|
|
144
|
+
"#{s[:short_term_entries]} short-term · #{s[:session_keys]} session keys"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
# rubocop:enable Metrics/ClassLength
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/ollama_agent/cli.rb
CHANGED
|
@@ -5,18 +5,28 @@ require "thor"
|
|
|
5
5
|
require_relative "agent"
|
|
6
6
|
require_relative "external_agents"
|
|
7
7
|
require_relative "prompt_skills"
|
|
8
|
+
require_relative "runtime/permissions"
|
|
9
|
+
require_relative "plugins/registry"
|
|
10
|
+
require_relative "plugins/loader"
|
|
8
11
|
|
|
9
12
|
module OllamaAgent
|
|
10
13
|
# Thor CLI for single-shot and interactive agent sessions.
|
|
11
14
|
# rubocop:disable Metrics/ClassLength -- Thor commands and shared helpers
|
|
12
15
|
class CLI < Thor
|
|
16
|
+
default_task :ask
|
|
17
|
+
|
|
13
18
|
def self.exit_on_failure?
|
|
14
19
|
true
|
|
15
20
|
end
|
|
16
21
|
|
|
17
|
-
desc "ask [QUERY]", "Run a
|
|
22
|
+
desc "ask [QUERY]", "Run a task, or interactive TUI when QUERY is omitted (default when no subcommand)"
|
|
18
23
|
method_option :model, type: :string, desc: "Ollama model (default: OLLAMA_AGENT_MODEL or ollama-client default)"
|
|
19
|
-
method_option :interactive, type: :boolean, aliases: "-i",
|
|
24
|
+
method_option :interactive, type: :boolean, aliases: "-i",
|
|
25
|
+
desc: "-i without --tui: line REPL; empty QUERY defaults to TUI"
|
|
26
|
+
method_option :tui, type: :boolean, default: false,
|
|
27
|
+
desc: "TTY UI; on by default for empty QUERY unless line REPL (-i without --tui)"
|
|
28
|
+
method_option :tui_god, type: :boolean, default: false,
|
|
29
|
+
desc: "With --tui: auto-select first option in interactive lists (dangerous)"
|
|
20
30
|
method_option :read_only, type: :boolean, default: false, aliases: "-R",
|
|
21
31
|
desc: "Read/search only (no edit_file, write_file, patches, or delegation)"
|
|
22
32
|
method_option :yes, type: :boolean, aliases: "-y", desc: "Apply patches without confirmation"
|
|
@@ -40,22 +50,27 @@ module OllamaAgent
|
|
|
40
50
|
desc: "Context window budget (OLLAMA_AGENT_MAX_TOKENS)"
|
|
41
51
|
method_option :context_summarize, type: :boolean, default: false,
|
|
42
52
|
desc: "Summarize dropped context vs sliding window"
|
|
53
|
+
method_option :provider, type: :string,
|
|
54
|
+
desc: "Model provider: ollama (default) | openai | anthropic | auto"
|
|
55
|
+
method_option :permissions, type: :string,
|
|
56
|
+
desc: "Permission profile: read_only | standard (default) | developer | full"
|
|
57
|
+
method_option :trace, type: :boolean, default: false,
|
|
58
|
+
desc: "Enable structured trace logging (OLLAMA_AGENT_TRACE=1)"
|
|
43
59
|
def ask(query = nil)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
elsif query
|
|
49
|
-
run_single_shot_agent!(agent, query)
|
|
50
|
-
else
|
|
51
|
-
puts Console.error_line("Error: provide a QUERY or use --interactive")
|
|
52
|
-
exit 1
|
|
53
|
-
end
|
|
60
|
+
load_plugins!
|
|
61
|
+
apply_session_interactive_tui_flags!(query)
|
|
62
|
+
validate_tui_options!
|
|
63
|
+
run_ask!(query)
|
|
54
64
|
end
|
|
55
65
|
|
|
56
|
-
desc "orchestrate [QUERY]", "Like ask,
|
|
66
|
+
desc "orchestrate [QUERY]", "Like ask, plus delegate to external CLI agents (Claude, Gemini, …)"
|
|
57
67
|
method_option :model, type: :string, desc: "Ollama model (default: OLLAMA_AGENT_MODEL or ollama-client default)"
|
|
58
|
-
method_option :interactive, type: :boolean, aliases: "-i",
|
|
68
|
+
method_option :interactive, type: :boolean, aliases: "-i",
|
|
69
|
+
desc: "-i without --tui: line REPL; empty QUERY defaults to TUI"
|
|
70
|
+
method_option :tui, type: :boolean, default: false,
|
|
71
|
+
desc: "TTY UI; on by default for empty QUERY unless line REPL (-i without --tui)"
|
|
72
|
+
method_option :tui_god, type: :boolean, default: false,
|
|
73
|
+
desc: "With --tui: auto-select first option in interactive lists (dangerous)"
|
|
59
74
|
method_option :read_only, type: :boolean, default: false, aliases: "-R",
|
|
60
75
|
desc: "Read/search only (no edit_file, write_file, patches, or delegation)"
|
|
61
76
|
method_option :yes, type: :boolean, aliases: "-y", desc: "Apply patches and run delegations without confirmation"
|
|
@@ -77,16 +92,10 @@ module OllamaAgent
|
|
|
77
92
|
method_option :context_summarize, type: :boolean, default: false,
|
|
78
93
|
desc: "Summarize dropped context vs sliding window"
|
|
79
94
|
def orchestrate(query = nil)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
elsif query
|
|
85
|
-
run_single_shot_agent!(agent, query)
|
|
86
|
-
else
|
|
87
|
-
puts Console.error_line("Error: provide a QUERY or use --interactive")
|
|
88
|
-
exit 1
|
|
89
|
-
end
|
|
95
|
+
load_plugins!
|
|
96
|
+
apply_session_interactive_tui_flags!(query)
|
|
97
|
+
validate_tui_options!
|
|
98
|
+
run_orchestrate!(query)
|
|
90
99
|
end
|
|
91
100
|
|
|
92
101
|
desc "sessions", "List saved sessions for the current project root"
|
|
@@ -188,6 +197,53 @@ module OllamaAgent
|
|
|
188
197
|
|
|
189
198
|
private
|
|
190
199
|
|
|
200
|
+
def run_ask!(query)
|
|
201
|
+
if session_tui?
|
|
202
|
+
start_tui_interactive { |up| build_agent(user_prompt: up, attach_stream: false) }
|
|
203
|
+
return
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
interactive_or_single_shot!(query) { build_agent }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def run_orchestrate!(query)
|
|
210
|
+
if session_tui?
|
|
211
|
+
start_tui_interactive { |up| build_orchestrator_agent(user_prompt: up, attach_stream: false) }
|
|
212
|
+
return
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
interactive_or_single_shot!(query) { build_orchestrator_agent }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def interactive_or_single_shot!(query)
|
|
219
|
+
agent = yield
|
|
220
|
+
|
|
221
|
+
if @session_interactive
|
|
222
|
+
start_interactive(agent)
|
|
223
|
+
elsif query
|
|
224
|
+
run_single_shot_agent!(agent, query)
|
|
225
|
+
else
|
|
226
|
+
puts Console.error_line("Error: provide a QUERY or use --interactive")
|
|
227
|
+
exit 1
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Thor 1.5+ freezes +options+ after parse; keep effective flags on the instance.
|
|
232
|
+
def apply_session_interactive_tui_flags!(query)
|
|
233
|
+
interactive = options[:interactive]
|
|
234
|
+
tui = options[:tui]
|
|
235
|
+
if query.to_s.strip.empty? && !(interactive && !tui)
|
|
236
|
+
interactive = true
|
|
237
|
+
tui = true
|
|
238
|
+
end
|
|
239
|
+
@session_interactive = interactive
|
|
240
|
+
@session_tui = tui
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def session_tui?
|
|
244
|
+
@session_interactive && @session_tui
|
|
245
|
+
end
|
|
246
|
+
|
|
191
247
|
def ensure_improve_mode_only_automated!
|
|
192
248
|
m = SelfImprovement::Modes.normalize(options[:mode])
|
|
193
249
|
return if m == "automated"
|
|
@@ -341,8 +397,9 @@ module OllamaAgent
|
|
|
341
397
|
# Build an Agent for the `ask` command.
|
|
342
398
|
# Same root as `self_review` / interactive: cwd when unset (see README).
|
|
343
399
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- session + orchestrator + audit kwargs exceed limits
|
|
344
|
-
def build_agent
|
|
400
|
+
def build_agent(user_prompt: nil, attach_stream: true)
|
|
345
401
|
orch = orchestrator_mode?
|
|
402
|
+
perms = resolved_permissions
|
|
346
403
|
agent = Agent.new(
|
|
347
404
|
model: options[:model],
|
|
348
405
|
root: resolved_root_for_self_review,
|
|
@@ -358,13 +415,25 @@ module OllamaAgent
|
|
|
358
415
|
resume: options[:resume] || false,
|
|
359
416
|
max_tokens: options[:max_tokens],
|
|
360
417
|
context_summarize: options[:context_summarize],
|
|
418
|
+
provider_name: options[:provider],
|
|
419
|
+
permissions: perms,
|
|
420
|
+
user_prompt: user_prompt,
|
|
361
421
|
**skill_agent_options
|
|
362
422
|
)
|
|
363
|
-
attach_console_streamer(agent) if stream_enabled?
|
|
423
|
+
attach_console_streamer(agent) if stream_enabled? && attach_stream
|
|
364
424
|
agent
|
|
365
425
|
end
|
|
366
426
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
367
427
|
|
|
428
|
+
def resolved_permissions
|
|
429
|
+
profile = options[:permissions]&.to_sym
|
|
430
|
+
return nil unless profile
|
|
431
|
+
|
|
432
|
+
Runtime::Permissions.new(profile: profile)
|
|
433
|
+
rescue ArgumentError
|
|
434
|
+
nil
|
|
435
|
+
end
|
|
436
|
+
|
|
368
437
|
def resolved_session_id
|
|
369
438
|
return options[:session] if options[:session]
|
|
370
439
|
return nil unless options[:resume]
|
|
@@ -380,7 +449,7 @@ module OllamaAgent
|
|
|
380
449
|
end
|
|
381
450
|
|
|
382
451
|
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- mirrors build_agent; stream attachment adds one line
|
|
383
|
-
def build_orchestrator_agent
|
|
452
|
+
def build_orchestrator_agent(user_prompt: nil, attach_stream: true)
|
|
384
453
|
agent = Agent.new(
|
|
385
454
|
model: options[:model],
|
|
386
455
|
root: resolved_root_for_self_review,
|
|
@@ -394,9 +463,10 @@ module OllamaAgent
|
|
|
394
463
|
max_retries: options[:max_retries],
|
|
395
464
|
max_tokens: options[:max_tokens],
|
|
396
465
|
context_summarize: options[:context_summarize],
|
|
466
|
+
user_prompt: user_prompt,
|
|
397
467
|
**skill_agent_options
|
|
398
468
|
)
|
|
399
|
-
attach_console_streamer(agent) if stream_enabled?
|
|
469
|
+
attach_console_streamer(agent) if stream_enabled? && attach_stream
|
|
400
470
|
agent
|
|
401
471
|
end
|
|
402
472
|
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
@@ -429,35 +499,45 @@ module OllamaAgent
|
|
|
429
499
|
end
|
|
430
500
|
|
|
431
501
|
def start_interactive(agent)
|
|
432
|
-
|
|
433
|
-
|
|
502
|
+
CLI::Repl.new(agent: agent).start
|
|
503
|
+
end
|
|
434
504
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
505
|
+
def validate_tui_options!
|
|
506
|
+
return unless @session_tui
|
|
507
|
+
return if @session_interactive
|
|
438
508
|
|
|
439
|
-
|
|
440
|
-
|
|
509
|
+
puts Console.error_line("Error: --tui requires --interactive (-i)")
|
|
510
|
+
exit 1
|
|
511
|
+
end
|
|
441
512
|
|
|
442
|
-
|
|
443
|
-
|
|
513
|
+
def tui_god_mode?
|
|
514
|
+
options[:tui_god] || ENV.fetch("OLLAMA_AGENT_TUI_GOD_MODE", "0") == "1"
|
|
444
515
|
end
|
|
445
516
|
|
|
446
|
-
def
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
false
|
|
517
|
+
def warn_if_tui_stream_conflict
|
|
518
|
+
return unless stream_enabled?
|
|
519
|
+
|
|
520
|
+
warn Console.error_line("ollama_agent: token streaming is disabled when using --tui.")
|
|
451
521
|
end
|
|
452
522
|
|
|
453
|
-
def
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
523
|
+
def start_tui_interactive
|
|
524
|
+
require_relative "cli/tui_repl"
|
|
525
|
+
warn_if_tui_stream_conflict
|
|
526
|
+
tui = OllamaAgent::TUI.new(god_mode: tui_god_mode?)
|
|
527
|
+
up = OllamaAgent::TuiUserPrompt.new(prompt: tui.prompt, stdout: $stdout)
|
|
528
|
+
agent = yield(up)
|
|
529
|
+
CLI::TuiRepl.new(agent: agent, tui: tui).start
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def load_plugins!
|
|
533
|
+
root = resolved_root_for_self_review
|
|
534
|
+
Plugins::Loader.new(root: root).load_all(skip_gems: false)
|
|
535
|
+
rescue StandardError => e
|
|
536
|
+
warn "ollama_agent: plugin load error: #{e.message}" if ENV["OLLAMA_AGENT_DEBUG"] == "1"
|
|
460
537
|
end
|
|
461
538
|
end
|
|
462
539
|
# rubocop:enable Metrics/ClassLength
|
|
540
|
+
|
|
541
|
+
require_relative "cli/repl_shared"
|
|
542
|
+
require_relative "cli/repl"
|
|
463
543
|
end
|