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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +14 -3
  4. data/lib/ollama_agent/agent/agent_config.rb +19 -2
  5. data/lib/ollama_agent/agent/client_wiring.rb +3 -8
  6. data/lib/ollama_agent/agent/session_wiring.rb +37 -3
  7. data/lib/ollama_agent/agent.rb +82 -6
  8. data/lib/ollama_agent/cli/repl.rb +159 -0
  9. data/lib/ollama_agent/cli/repl_shared.rb +229 -0
  10. data/lib/ollama_agent/cli/tui_repl.rb +149 -0
  11. data/lib/ollama_agent/cli.rb +129 -49
  12. data/lib/ollama_agent/core/action_envelope.rb +82 -0
  13. data/lib/ollama_agent/core/budget.rb +90 -0
  14. data/lib/ollama_agent/core/loop_detector.rb +67 -0
  15. data/lib/ollama_agent/core/schema_validator.rb +136 -0
  16. data/lib/ollama_agent/core/trace_logger.rb +138 -0
  17. data/lib/ollama_agent/external_agents/probe.rb +23 -3
  18. data/lib/ollama_agent/indexing/context_packer.rb +140 -0
  19. data/lib/ollama_agent/indexing/diff_summarizer.rb +125 -0
  20. data/lib/ollama_agent/indexing/file_indexer.rb +129 -0
  21. data/lib/ollama_agent/indexing/repo_scanner.rb +158 -0
  22. data/lib/ollama_agent/memory/long_term.rb +109 -0
  23. data/lib/ollama_agent/memory/manager.rb +121 -0
  24. data/lib/ollama_agent/memory/session_memory.rb +93 -0
  25. data/lib/ollama_agent/memory/short_term.rb +66 -0
  26. data/lib/ollama_agent/ollama_cloud_catalog.rb +66 -0
  27. data/lib/ollama_agent/ollama_connection.rb +30 -0
  28. data/lib/ollama_agent/plugins/loader.rb +95 -0
  29. data/lib/ollama_agent/plugins/registry.rb +103 -0
  30. data/lib/ollama_agent/providers/anthropic.rb +245 -0
  31. data/lib/ollama_agent/providers/base.rb +79 -0
  32. data/lib/ollama_agent/providers/ollama.rb +118 -0
  33. data/lib/ollama_agent/providers/openai.rb +215 -0
  34. data/lib/ollama_agent/providers/registry.rb +76 -0
  35. data/lib/ollama_agent/providers/router.rb +93 -0
  36. data/lib/ollama_agent/resilience/retry_middleware.rb +5 -0
  37. data/lib/ollama_agent/runner.rb +25 -4
  38. data/lib/ollama_agent/runtime/approval_gate.rb +74 -0
  39. data/lib/ollama_agent/runtime/permissions.rb +103 -0
  40. data/lib/ollama_agent/runtime/policies.rb +100 -0
  41. data/lib/ollama_agent/runtime/sandbox.rb +130 -0
  42. data/lib/ollama_agent/streaming/hooks.rb +3 -1
  43. data/lib/ollama_agent/tools/base.rb +108 -0
  44. data/lib/ollama_agent/tools/git_tools.rb +176 -0
  45. data/lib/ollama_agent/tools/http_tools.rb +202 -0
  46. data/lib/ollama_agent/tools/memory_tools.rb +116 -0
  47. data/lib/ollama_agent/tools/shell_tools.rb +208 -0
  48. data/lib/ollama_agent/tui.rb +183 -0
  49. data/lib/ollama_agent/tui_slash_reader.rb +147 -0
  50. data/lib/ollama_agent/tui_user_prompt.rb +45 -0
  51. data/lib/ollama_agent/version.rb +1 -1
  52. data/lib/ollama_agent.rb +46 -1
  53. 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
@@ -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 natural-language task (reads, search, patch)"
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", desc: "Interactive REPL"
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
- agent = build_agent
45
-
46
- if options[:interactive]
47
- start_interactive(agent)
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, with tools to list/delegate to external CLI agents (Claude, Gemini, …)"
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", desc: "Interactive REPL"
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
- agent = build_orchestrator_agent
81
-
82
- if options[:interactive]
83
- start_interactive(agent)
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
- puts Console.welcome_banner("Ollama Agent (type 'exit' to quit)")
433
- use_readline = interactive_readline_usable?
502
+ CLI::Repl.new(agent: agent).start
503
+ end
434
504
 
435
- loop do
436
- input = interactive_readline_line(use_readline)
437
- break if input.nil?
505
+ def validate_tui_options!
506
+ return unless @session_tui
507
+ return if @session_interactive
438
508
 
439
- line = input.chomp
440
- break if line == "exit"
509
+ puts Console.error_line("Error: --tui requires --interactive (-i)")
510
+ exit 1
511
+ end
441
512
 
442
- agent.run(line)
443
- end
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 interactive_readline_usable?
447
- require "readline"
448
- true
449
- rescue LoadError
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 interactive_readline_line(use_readline)
454
- if use_readline
455
- Readline.readline(Console.prompt_prefix, true)
456
- else
457
- print Console.prompt_prefix
458
- $stdin.gets
459
- end
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