ollama_agent 0.1.0 → 0.3.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/.cursor/skills/ruby-code-review-levels/SKILL.md +115 -0
- data/.cursor/skills/self-improvement-sandbox-safety/SKILL.md +65 -0
- data/.env.example +25 -0
- data/CHANGELOG.md +40 -0
- data/README.md +135 -4
- data/docs/ARCHITECTURE.md +42 -0
- data/docs/PERFORMANCE.md +22 -0
- data/docs/SESSIONS.md +48 -0
- data/docs/TOOLS.md +53 -0
- data/docs/TOOL_RUNTIME.md +154 -0
- data/docs/superpowers/plans/2026-03-26-production-ready-ollama-agent.md +2454 -0
- data/docs/superpowers/specs/2026-03-26-production-ready-ollama-agent-design.md +400 -0
- data/lib/ollama_agent/agent/agent_config.rb +53 -0
- data/lib/ollama_agent/agent/client_wiring.rb +76 -0
- data/lib/ollama_agent/agent/prompt_wiring.rb +55 -0
- data/lib/ollama_agent/agent/session_wiring.rb +53 -0
- data/lib/ollama_agent/agent.rb +148 -73
- data/lib/ollama_agent/agent_prompt.rb +31 -1
- data/lib/ollama_agent/chat_stream_carry.rb +88 -0
- data/lib/ollama_agent/chat_stream_thinking_format.rb +29 -0
- data/lib/ollama_agent/cli.rb +394 -4
- data/lib/ollama_agent/console.rb +177 -5
- data/lib/ollama_agent/context/manager.rb +100 -0
- data/lib/ollama_agent/context/token_counter.rb +33 -0
- data/lib/ollama_agent/diff_path_validator.rb +32 -10
- data/lib/ollama_agent/env_config.rb +44 -0
- data/lib/ollama_agent/external_agents/TODO-plan.md +1948 -0
- data/lib/ollama_agent/external_agents/argv_interp.rb +21 -0
- data/lib/ollama_agent/external_agents/default_agents.yml +60 -0
- data/lib/ollama_agent/external_agents/delegate_logger.rb +31 -0
- data/lib/ollama_agent/external_agents/delegate_timeout_status.rb +12 -0
- data/lib/ollama_agent/external_agents/env_helpers.rb +38 -0
- data/lib/ollama_agent/external_agents/path_validator.rb +32 -0
- data/lib/ollama_agent/external_agents/probe.rb +122 -0
- data/lib/ollama_agent/external_agents/registry.rb +50 -0
- data/lib/ollama_agent/external_agents/runner.rb +118 -0
- data/lib/ollama_agent/external_agents.rb +9 -0
- data/lib/ollama_agent/global_dotenv.rb +39 -0
- data/lib/ollama_agent/model_env.rb +26 -0
- data/lib/ollama_agent/ollama_chat_thinking_stream.rb +41 -0
- data/lib/ollama_agent/ollama_connection.rb +6 -1
- data/lib/ollama_agent/patch_risk.rb +81 -0
- data/lib/ollama_agent/patch_support.rb +27 -1
- data/lib/ollama_agent/path_sandbox.rb +62 -0
- data/lib/ollama_agent/prompt_skills/clean_ruby.md +131 -0
- data/lib/ollama_agent/prompt_skills/code_review.md +112 -0
- data/lib/ollama_agent/prompt_skills/design_patterns.md +56 -0
- data/lib/ollama_agent/prompt_skills/manifest.yml +25 -0
- data/lib/ollama_agent/prompt_skills/ollama_agent_patterns.md +132 -0
- data/lib/ollama_agent/prompt_skills/rails_best_practices.md +41 -0
- data/lib/ollama_agent/prompt_skills/rails_style.md +138 -0
- data/lib/ollama_agent/prompt_skills/rspec.md +280 -0
- data/lib/ollama_agent/prompt_skills/rubocop.md +7 -0
- data/lib/ollama_agent/prompt_skills/ruby_style.md +121 -0
- data/lib/ollama_agent/prompt_skills/solid.md +270 -0
- data/lib/ollama_agent/prompt_skills/solid_ruby.md +223 -0
- data/lib/ollama_agent/prompt_skills.rb +169 -0
- data/lib/ollama_agent/repo_list.rb +4 -1
- data/lib/ollama_agent/resilience/audit_logger.rb +79 -0
- data/lib/ollama_agent/resilience/retry_middleware.rb +45 -0
- data/lib/ollama_agent/resilience/retry_policy.rb +51 -0
- data/lib/ollama_agent/ruby_index_tool_support.rb +17 -6
- data/lib/ollama_agent/runner.rb +123 -0
- data/lib/ollama_agent/sandboxed_tools/delegate_external.rb +62 -0
- data/lib/ollama_agent/sandboxed_tools/file_read_write.rb +100 -0
- data/lib/ollama_agent/sandboxed_tools/search_text.rb +60 -0
- data/lib/ollama_agent/sandboxed_tools.rb +55 -116
- data/lib/ollama_agent/search_backend.rb +93 -0
- data/lib/ollama_agent/self_improvement/analyzer.rb +34 -0
- data/lib/ollama_agent/self_improvement/improver.rb +340 -0
- data/lib/ollama_agent/self_improvement/modes.rb +25 -0
- data/lib/ollama_agent/self_improvement/ruby_mastery_context.rb +66 -0
- data/lib/ollama_agent/self_improvement.rb +5 -0
- data/lib/ollama_agent/session/session.rb +8 -0
- data/lib/ollama_agent/session/store.rb +68 -0
- data/lib/ollama_agent/streaming/console_streamer.rb +29 -0
- data/lib/ollama_agent/streaming/hooks.rb +39 -0
- data/lib/ollama_agent/tool_arguments.rb +13 -1
- data/lib/ollama_agent/tool_content_parser.rb +1 -1
- data/lib/ollama_agent/tool_runtime/executor.rb +34 -0
- data/lib/ollama_agent/tool_runtime/json_extractor.rb +62 -0
- data/lib/ollama_agent/tool_runtime/loop.rb +72 -0
- data/lib/ollama_agent/tool_runtime/memory.rb +32 -0
- data/lib/ollama_agent/tool_runtime/ollama_json_planner.rb +98 -0
- data/lib/ollama_agent/tool_runtime/plan_extractor.rb +12 -0
- data/lib/ollama_agent/tool_runtime/registry.rb +60 -0
- data/lib/ollama_agent/tool_runtime/tool.rb +24 -0
- data/lib/ollama_agent/tool_runtime.rb +24 -0
- data/lib/ollama_agent/tools/registry.rb +55 -0
- data/lib/ollama_agent/tools_schema.rb +74 -1
- data/lib/ollama_agent/user_prompt.rb +35 -0
- data/lib/ollama_agent/version.rb +1 -1
- data/lib/ollama_agent.rb +25 -0
- data/reproduce_429.rb +40 -0
- data/sig/ollama_agent.rbs +111 -1
- metadata +78 -2
data/lib/ollama_agent/cli.rb
CHANGED
|
@@ -3,40 +3,429 @@
|
|
|
3
3
|
require "thor"
|
|
4
4
|
|
|
5
5
|
require_relative "agent"
|
|
6
|
+
require_relative "external_agents"
|
|
7
|
+
require_relative "prompt_skills"
|
|
6
8
|
|
|
7
9
|
module OllamaAgent
|
|
8
10
|
# Thor CLI for single-shot and interactive agent sessions.
|
|
11
|
+
# rubocop:disable Metrics/ClassLength -- Thor commands and shared helpers
|
|
9
12
|
class CLI < Thor
|
|
13
|
+
def self.exit_on_failure?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
desc "ask [QUERY]", "Run a natural-language task (reads, search, patch)"
|
|
11
18
|
method_option :model, type: :string, desc: "Ollama model (default: OLLAMA_AGENT_MODEL or ollama-client default)"
|
|
12
19
|
method_option :interactive, type: :boolean, aliases: "-i", desc: "Interactive REPL"
|
|
20
|
+
method_option :read_only, type: :boolean, default: false, aliases: "-R",
|
|
21
|
+
desc: "Read/search only (no edit_file, write_file, patches, or delegation)"
|
|
13
22
|
method_option :yes, type: :boolean, aliases: "-y", desc: "Apply patches without confirmation"
|
|
14
23
|
method_option :root, type: :string, desc: "Project root (default: OLLAMA_AGENT_ROOT or cwd)"
|
|
15
24
|
method_option :timeout, type: :numeric, aliases: "-t", desc: "HTTP timeout seconds (default 120)"
|
|
16
25
|
method_option :think, type: :string, desc: "Thinking mode: true|false|high|medium|low (see OLLAMA_AGENT_THINK)"
|
|
26
|
+
method_option :no_skills, type: :boolean, default: false,
|
|
27
|
+
desc: "Disable bundled prompt skills (same as OLLAMA_AGENT_SKILLS=0)"
|
|
28
|
+
method_option :skill_paths, type: :string,
|
|
29
|
+
desc: "Extra .md paths or dirs, colon-separated; merged with OLLAMA_AGENT_SKILL_PATHS"
|
|
30
|
+
method_option :stream, type: :boolean, default: false,
|
|
31
|
+
desc: "Stream tokens to terminal as they arrive (OLLAMA_AGENT_STREAM=1)"
|
|
32
|
+
method_option :audit, type: :boolean, default: false,
|
|
33
|
+
desc: "Enable structured audit log under .ollama_agent/logs/ (OLLAMA_AGENT_AUDIT=1)"
|
|
34
|
+
method_option :max_retries, type: :numeric,
|
|
35
|
+
desc: "HTTP retry attempts (0=disable, default 3)"
|
|
36
|
+
method_option :session, type: :string, desc: "Named session id (saves/resumes conversation)"
|
|
37
|
+
method_option :resume, type: :boolean, default: false,
|
|
38
|
+
desc: "Resume the named (or most recent) session"
|
|
39
|
+
method_option :max_tokens, type: :numeric,
|
|
40
|
+
desc: "Context window budget (OLLAMA_AGENT_MAX_TOKENS)"
|
|
41
|
+
method_option :context_summarize, type: :boolean, default: false,
|
|
42
|
+
desc: "Summarize dropped context vs sliding window"
|
|
17
43
|
def ask(query = nil)
|
|
18
44
|
agent = build_agent
|
|
19
45
|
|
|
20
46
|
if options[:interactive]
|
|
21
47
|
start_interactive(agent)
|
|
22
48
|
elsif query
|
|
23
|
-
agent
|
|
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
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
desc "orchestrate [QUERY]", "Like ask, with tools to list/delegate to external CLI agents (Claude, Gemini, …)"
|
|
57
|
+
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"
|
|
59
|
+
method_option :read_only, type: :boolean, default: false, aliases: "-R",
|
|
60
|
+
desc: "Read/search only (no edit_file, write_file, patches, or delegation)"
|
|
61
|
+
method_option :yes, type: :boolean, aliases: "-y", desc: "Apply patches and run delegations without confirmation"
|
|
62
|
+
method_option :root, type: :string, desc: "Project root (default: OLLAMA_AGENT_ROOT or cwd)"
|
|
63
|
+
method_option :timeout, type: :numeric, aliases: "-t", desc: "HTTP timeout seconds (default 120)"
|
|
64
|
+
method_option :think, type: :string, desc: "Thinking mode: true|false|high|medium|low (see OLLAMA_AGENT_THINK)"
|
|
65
|
+
method_option :no_skills, type: :boolean, default: false,
|
|
66
|
+
desc: "Disable bundled prompt skills (same as OLLAMA_AGENT_SKILLS=0)"
|
|
67
|
+
method_option :skill_paths, type: :string,
|
|
68
|
+
desc: "Extra .md paths or dirs, colon-separated; merged with OLLAMA_AGENT_SKILL_PATHS"
|
|
69
|
+
method_option :stream, type: :boolean, default: false,
|
|
70
|
+
desc: "Stream tokens to terminal as they arrive (OLLAMA_AGENT_STREAM=1)"
|
|
71
|
+
method_option :audit, type: :boolean, default: false,
|
|
72
|
+
desc: "Enable structured audit log under .ollama_agent/logs/ (OLLAMA_AGENT_AUDIT=1)"
|
|
73
|
+
method_option :max_retries, type: :numeric,
|
|
74
|
+
desc: "HTTP retry attempts (0=disable, default 3)"
|
|
75
|
+
method_option :max_tokens, type: :numeric,
|
|
76
|
+
desc: "Context window budget (OLLAMA_AGENT_MAX_TOKENS)"
|
|
77
|
+
method_option :context_summarize, type: :boolean, default: false,
|
|
78
|
+
desc: "Summarize dropped context vs sliding window"
|
|
79
|
+
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)
|
|
24
86
|
else
|
|
25
87
|
puts Console.error_line("Error: provide a QUERY or use --interactive")
|
|
26
88
|
exit 1
|
|
27
89
|
end
|
|
28
90
|
end
|
|
29
91
|
|
|
92
|
+
desc "sessions", "List saved sessions for the current project root"
|
|
93
|
+
method_option :root, type: :string, desc: "Project root (default: OLLAMA_AGENT_ROOT or cwd)"
|
|
94
|
+
def sessions
|
|
95
|
+
root = resolved_root_for_self_review
|
|
96
|
+
list = Session::Store.list(root: root)
|
|
97
|
+
if list.empty?
|
|
98
|
+
puts "No sessions found in #{root}"
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
puts "SESSION ID STARTED"
|
|
102
|
+
list.each { |s| puts format("%-30<id>s %<started>s", id: s.session_id, started: s.started_at) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
desc "agents", "List configured external CLI agents and whether they are on PATH"
|
|
106
|
+
def agents
|
|
107
|
+
reg = ExternalAgents::Registry.load
|
|
108
|
+
ExternalAgents::Probe.print_table(reg)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
desc "doctor", "Alias for agents"
|
|
112
|
+
def doctor
|
|
113
|
+
agents
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
desc "self_review", "Self-review / improvement: --mode analysis | interactive | automated (see help)"
|
|
117
|
+
method_option :mode, type: :string, default: "analysis",
|
|
118
|
+
desc: "analysis (1)=read-only report; interactive (2)=confirm patches in tree; " \
|
|
119
|
+
"automated (3)=sandbox+RSpec+optional --apply"
|
|
120
|
+
long_desc <<~HELP
|
|
121
|
+
Modes:
|
|
122
|
+
analysis Read-only tools; prints a report; no writes (default).
|
|
123
|
+
interactive Full tools on --root; you confirm each patch (like `ask`); optional -y / --semi.
|
|
124
|
+
automated Temp sandbox, agent edits, `bundle exec rspec`; optional --apply to merge back.
|
|
125
|
+
|
|
126
|
+
Aliases: 1/2/3, readonly, fix, confirm, sandbox, full.
|
|
127
|
+
HELP
|
|
128
|
+
method_option :model, type: :string, desc: "Ollama model (default: OLLAMA_AGENT_MODEL or ollama-client default)"
|
|
129
|
+
method_option :root, type: :string, desc: "Project root (default: OLLAMA_AGENT_ROOT or cwd)"
|
|
130
|
+
method_option :timeout, type: :numeric, aliases: "-t", desc: "HTTP timeout seconds (default 120)"
|
|
131
|
+
method_option :think, type: :string, desc: "Thinking mode: true|false|high|medium|low (see OLLAMA_AGENT_THINK)"
|
|
132
|
+
method_option :yes, type: :boolean, aliases: "-y",
|
|
133
|
+
desc: "interactive/automated: apply patches without confirmation"
|
|
134
|
+
method_option :semi, type: :boolean, default: true,
|
|
135
|
+
desc: "interactive/automated: auto-approve obvious patches; prompt for risky (default: true)"
|
|
136
|
+
method_option :apply, type: :boolean, default: false,
|
|
137
|
+
desc: "automated only: after green tests, copy changed files from sandbox into --root"
|
|
138
|
+
method_option :no_skills, type: :boolean, default: false,
|
|
139
|
+
desc: "Disable bundled prompt skills (same as OLLAMA_AGENT_SKILLS=0)"
|
|
140
|
+
method_option :skill_paths, type: :string,
|
|
141
|
+
desc: "Extra .md paths or dirs, colon-separated; merged with OLLAMA_AGENT_SKILL_PATHS"
|
|
142
|
+
method_option :stream, type: :boolean, default: false,
|
|
143
|
+
desc: "Stream tokens to terminal as they arrive (OLLAMA_AGENT_STREAM=1)"
|
|
144
|
+
method_option :max_tokens, type: :numeric,
|
|
145
|
+
desc: "Context window budget (OLLAMA_AGENT_MAX_TOKENS)"
|
|
146
|
+
method_option :context_summarize, type: :boolean, default: false,
|
|
147
|
+
desc: "Summarize dropped context vs sliding window"
|
|
148
|
+
method_option :verify, type: :string,
|
|
149
|
+
desc: "automated only: comma-separated checks after agent (default: rspec or " \
|
|
150
|
+
"OLLAMA_AGENT_IMPROVE_VERIFY). Steps: syntax, rubocop, rspec"
|
|
151
|
+
method_option :no_ruby_mastery, type: :boolean, default: false,
|
|
152
|
+
desc: "Skip prepending ruby_mastery static analysis (see OLLAMA_AGENT_RUBY_MASTERY)"
|
|
153
|
+
def self_review
|
|
154
|
+
dispatch_self_review_mode(SelfImprovement::Modes.normalize(options[:mode]))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
desc "improve", "Shortcut for: self_review --mode automated"
|
|
158
|
+
method_option :mode, type: :string, default: "automated",
|
|
159
|
+
desc: "Optional; must be automated (or 3, sandbox, full). Other modes: use self_review --mode"
|
|
160
|
+
method_option :model, type: :string, desc: "Ollama model (default: OLLAMA_AGENT_MODEL or ollama-client default)"
|
|
161
|
+
method_option :root, type: :string, desc: "Source tree to copy and test (default: OLLAMA_AGENT_ROOT or cwd)"
|
|
162
|
+
method_option :timeout, type: :numeric, aliases: "-t", desc: "HTTP timeout seconds (default 120)"
|
|
163
|
+
method_option :think, type: :string, desc: "Thinking mode: true|false|high|medium|low (see OLLAMA_AGENT_THINK)"
|
|
164
|
+
method_option :yes, type: :boolean, aliases: "-y", desc: "Apply all patches without confirmation"
|
|
165
|
+
method_option :semi, type: :boolean, default: true,
|
|
166
|
+
desc: "Without -y: auto-approve obvious patches; prompt for risky (default: true)"
|
|
167
|
+
method_option :apply, type: :boolean, default: false,
|
|
168
|
+
desc: "After green tests, copy changed files from sandbox into --root"
|
|
169
|
+
method_option :no_skills, type: :boolean, default: false,
|
|
170
|
+
desc: "Disable bundled prompt skills (same as OLLAMA_AGENT_SKILLS=0)"
|
|
171
|
+
method_option :skill_paths, type: :string,
|
|
172
|
+
desc: "Extra .md paths or dirs, colon-separated; merged with OLLAMA_AGENT_SKILL_PATHS"
|
|
173
|
+
method_option :stream, type: :boolean, default: false,
|
|
174
|
+
desc: "Stream tokens to terminal as they arrive (OLLAMA_AGENT_STREAM=1)"
|
|
175
|
+
method_option :max_tokens, type: :numeric,
|
|
176
|
+
desc: "Context window budget (OLLAMA_AGENT_MAX_TOKENS)"
|
|
177
|
+
method_option :context_summarize, type: :boolean, default: false,
|
|
178
|
+
desc: "Summarize dropped context vs sliding window"
|
|
179
|
+
method_option :verify, type: :string,
|
|
180
|
+
desc: "Comma-separated post-agent checks (default: rspec; env " \
|
|
181
|
+
"OLLAMA_AGENT_IMPROVE_VERIFY). Steps: syntax, rubocop, rspec"
|
|
182
|
+
method_option :no_ruby_mastery, type: :boolean, default: false,
|
|
183
|
+
desc: "Skip prepending ruby_mastery static analysis (see OLLAMA_AGENT_RUBY_MASTERY)"
|
|
184
|
+
def improve
|
|
185
|
+
ensure_improve_mode_only_automated!
|
|
186
|
+
dispatch_self_review_mode("automated")
|
|
187
|
+
end
|
|
188
|
+
|
|
30
189
|
private
|
|
31
190
|
|
|
191
|
+
def ensure_improve_mode_only_automated!
|
|
192
|
+
m = SelfImprovement::Modes.normalize(options[:mode])
|
|
193
|
+
return if m == "automated"
|
|
194
|
+
|
|
195
|
+
warn Console.error_line(improve_mode_error_message(m))
|
|
196
|
+
exit 1
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def improve_mode_error_message(normalized_mode)
|
|
200
|
+
return invalid_improve_mode_message unless SelfImprovement::Modes.valid?(normalized_mode)
|
|
201
|
+
|
|
202
|
+
"Command \"improve\" only runs automated (sandbox) mode. For other modes use: " \
|
|
203
|
+
"ollama_agent self_review --mode analysis | interactive"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def invalid_improve_mode_message
|
|
207
|
+
"Invalid --mode for improve: use automated (or 3, sandbox, full). Got: #{options[:mode].inspect}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def dispatch_self_review_mode(mode)
|
|
211
|
+
validate_self_review_mode!(mode)
|
|
212
|
+
send(:"run_mode_#{mode}")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def validate_self_review_mode!(mode)
|
|
216
|
+
return if SelfImprovement::Modes.valid?(mode)
|
|
217
|
+
|
|
218
|
+
warn Console.error_line(
|
|
219
|
+
"Invalid --mode: use analysis, interactive, or automated (or 1, 2, 3). Got: #{options[:mode].inspect}"
|
|
220
|
+
)
|
|
221
|
+
exit 1
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- stream + context kwargs exceed limits
|
|
225
|
+
def run_mode_analysis
|
|
226
|
+
agent = Agent.new(
|
|
227
|
+
model: options[:model],
|
|
228
|
+
root: resolved_root_for_self_review,
|
|
229
|
+
read_only: true,
|
|
230
|
+
confirm_patches: false,
|
|
231
|
+
http_timeout: options[:timeout],
|
|
232
|
+
think: options[:think],
|
|
233
|
+
max_tokens: options[:max_tokens],
|
|
234
|
+
context_summarize: options[:context_summarize],
|
|
235
|
+
**skill_agent_options
|
|
236
|
+
)
|
|
237
|
+
attach_console_streamer(agent) if stream_enabled?
|
|
238
|
+
preamble = ruby_mastery_preamble(resolved_root_for_self_review)
|
|
239
|
+
SelfImprovement::Analyzer.new(agent).run(preamble: preamble)
|
|
240
|
+
end
|
|
241
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
242
|
+
|
|
243
|
+
def run_mode_interactive
|
|
244
|
+
agent = Agent.new(**interactive_agent_keywords)
|
|
245
|
+
attach_console_streamer(agent) if stream_enabled?
|
|
246
|
+
preamble = ruby_mastery_preamble(resolved_root_for_self_review)
|
|
247
|
+
SelfImprovement::Analyzer.new(agent).run(SelfImprovement::Analyzer::INTERACTIVE_PROMPT, preamble: preamble)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- context kwargs push over limit
|
|
251
|
+
def interactive_agent_keywords
|
|
252
|
+
semi = options[:semi] != false
|
|
253
|
+
{
|
|
254
|
+
model: options[:model],
|
|
255
|
+
root: resolved_root_for_self_review,
|
|
256
|
+
read_only: false,
|
|
257
|
+
confirm_patches: !options[:yes],
|
|
258
|
+
patch_policy: semi ? PatchRisk.method(:assess).to_proc : nil,
|
|
259
|
+
http_timeout: options[:timeout],
|
|
260
|
+
think: options[:think],
|
|
261
|
+
max_tokens: options[:max_tokens],
|
|
262
|
+
context_summarize: options[:context_summarize]
|
|
263
|
+
}.merge(skill_agent_options)
|
|
264
|
+
end
|
|
265
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
266
|
+
|
|
267
|
+
def run_mode_automated
|
|
268
|
+
result = SelfImprovement::Improver.new.run(**improve_run_options)
|
|
269
|
+
report_improve_result(result)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def resolved_root_for_self_review
|
|
273
|
+
raw = options[:root] || ENV.fetch("OLLAMA_AGENT_ROOT", nil)
|
|
274
|
+
base = raw.to_s.strip.empty? ? Dir.pwd : raw
|
|
275
|
+
File.expand_path(base)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def ruby_mastery_enabled?
|
|
279
|
+
return false if options[:no_ruby_mastery]
|
|
280
|
+
return false if ENV.fetch("OLLAMA_AGENT_RUBY_MASTERY", "1") == "0"
|
|
281
|
+
|
|
282
|
+
true
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def ruby_mastery_preamble(root)
|
|
286
|
+
return nil unless ruby_mastery_enabled?
|
|
287
|
+
|
|
288
|
+
SelfImprovement::RubyMasteryContext.markdown_section(root)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- context kwargs push over limit
|
|
292
|
+
def improve_run_options
|
|
293
|
+
{
|
|
294
|
+
model: options[:model],
|
|
295
|
+
root: options[:root] || ENV.fetch("OLLAMA_AGENT_ROOT", nil),
|
|
296
|
+
yes: options[:yes],
|
|
297
|
+
semi: options[:semi] != false,
|
|
298
|
+
apply: options[:apply],
|
|
299
|
+
http_timeout: options[:timeout],
|
|
300
|
+
think: options[:think],
|
|
301
|
+
max_tokens: options[:max_tokens],
|
|
302
|
+
context_summarize: options[:context_summarize],
|
|
303
|
+
stream: stream_enabled?,
|
|
304
|
+
verify: options[:verify],
|
|
305
|
+
ruby_mastery: ruby_mastery_enabled?
|
|
306
|
+
}.merge(skill_agent_options)
|
|
307
|
+
end
|
|
308
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
309
|
+
|
|
310
|
+
def report_improve_result(result)
|
|
311
|
+
unless result[:success]
|
|
312
|
+
warn Console.error_line("Tests failed in sandbox.")
|
|
313
|
+
puts result[:test_output]
|
|
314
|
+
exit 1
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
root = result[:source_root]
|
|
318
|
+
puts "ollama_agent: tests passed in sandbox (#{root})"
|
|
319
|
+
copied = result[:copied_to_source]
|
|
320
|
+
report_improve_copied_files(copied, root)
|
|
321
|
+
report_improve_merge_outcome(copied, root)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def report_improve_merge_outcome(copied, root)
|
|
325
|
+
if options[:apply]
|
|
326
|
+
puts "ollama_agent: no changed files to copy from sandbox" if copied.empty?
|
|
327
|
+
return
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
puts "ollama_agent: sandbox was discarded — your project tree was not updated. " \
|
|
331
|
+
"Re-run with --apply to merge sandbox changes into #{root} after a green run."
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def report_improve_copied_files(copied, root)
|
|
335
|
+
return if copied.empty?
|
|
336
|
+
|
|
337
|
+
puts "ollama_agent: copied #{copied.size} file(s) to #{root}"
|
|
338
|
+
copied.sort.each { |rel| puts " #{rel}" }
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Build an Agent for the `ask` command.
|
|
342
|
+
# Same root as `self_review` / interactive: cwd when unset (see README).
|
|
343
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- session + orchestrator + audit kwargs exceed limits
|
|
32
344
|
def build_agent
|
|
33
|
-
|
|
345
|
+
orch = orchestrator_mode?
|
|
346
|
+
agent = Agent.new(
|
|
34
347
|
model: options[:model],
|
|
35
|
-
root:
|
|
348
|
+
root: resolved_root_for_self_review,
|
|
349
|
+
read_only: options[:read_only],
|
|
36
350
|
confirm_patches: !options[:yes],
|
|
37
351
|
http_timeout: options[:timeout],
|
|
38
|
-
think: options[:think]
|
|
352
|
+
think: options[:think],
|
|
353
|
+
orchestrator: orch,
|
|
354
|
+
confirm_delegation: orch ? !options[:yes] : true,
|
|
355
|
+
audit: options[:audit],
|
|
356
|
+
max_retries: options[:max_retries],
|
|
357
|
+
session_id: resolved_session_id,
|
|
358
|
+
resume: options[:resume] || false,
|
|
359
|
+
max_tokens: options[:max_tokens],
|
|
360
|
+
context_summarize: options[:context_summarize],
|
|
361
|
+
**skill_agent_options
|
|
39
362
|
)
|
|
363
|
+
attach_console_streamer(agent) if stream_enabled?
|
|
364
|
+
agent
|
|
365
|
+
end
|
|
366
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
367
|
+
|
|
368
|
+
def resolved_session_id
|
|
369
|
+
return options[:session] if options[:session]
|
|
370
|
+
return nil unless options[:resume]
|
|
371
|
+
|
|
372
|
+
list = Session::Store.list(root: resolved_root_for_self_review)
|
|
373
|
+
list.first&.session_id
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def orchestrator_mode?
|
|
377
|
+
return true if ENV.fetch("OLLAMA_AGENT_ORCHESTRATOR", "0").to_s == "1"
|
|
378
|
+
|
|
379
|
+
ENV.fetch("OLLAMA_AGENT_MODE", "").to_s.strip.casecmp("orchestrator").zero?
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- mirrors build_agent; stream attachment adds one line
|
|
383
|
+
def build_orchestrator_agent
|
|
384
|
+
agent = Agent.new(
|
|
385
|
+
model: options[:model],
|
|
386
|
+
root: resolved_root_for_self_review,
|
|
387
|
+
read_only: options[:read_only],
|
|
388
|
+
confirm_patches: !options[:yes],
|
|
389
|
+
http_timeout: options[:timeout],
|
|
390
|
+
think: options[:think],
|
|
391
|
+
orchestrator: true,
|
|
392
|
+
confirm_delegation: !options[:yes],
|
|
393
|
+
audit: options[:audit],
|
|
394
|
+
max_retries: options[:max_retries],
|
|
395
|
+
max_tokens: options[:max_tokens],
|
|
396
|
+
context_summarize: options[:context_summarize],
|
|
397
|
+
**skill_agent_options
|
|
398
|
+
)
|
|
399
|
+
attach_console_streamer(agent) if stream_enabled?
|
|
400
|
+
agent
|
|
401
|
+
end
|
|
402
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
403
|
+
|
|
404
|
+
def skill_agent_options
|
|
405
|
+
out = {}
|
|
406
|
+
out[:skills_enabled] = false if options[:no_skills]
|
|
407
|
+
paths = PromptSkills.split_paths(options[:skill_paths])
|
|
408
|
+
out[:skill_paths] = paths unless paths.empty?
|
|
409
|
+
out
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def stream_enabled?
|
|
413
|
+
options[:stream] || ENV.fetch("OLLAMA_AGENT_STREAM", "0") == "1"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def attach_console_streamer(agent)
|
|
417
|
+
Streaming::ConsoleStreamer.new.attach(agent.hooks)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def run_single_shot_agent!(agent, query)
|
|
421
|
+
agent.run(query)
|
|
422
|
+
rescue Ollama::Error, OllamaAgent::Error => e
|
|
423
|
+
warn Console.error_line("#{e.class}: #{e.message}")
|
|
424
|
+
exit 1
|
|
425
|
+
rescue StandardError => e
|
|
426
|
+
warn Console.error_line("#{e.class}: #{e.message}")
|
|
427
|
+
warn e.full_message(order: :top, highlight: false) if ENV["OLLAMA_AGENT_DEBUG"] == "1"
|
|
428
|
+
exit 1
|
|
40
429
|
end
|
|
41
430
|
|
|
42
431
|
def start_interactive(agent)
|
|
@@ -70,4 +459,5 @@ module OllamaAgent
|
|
|
70
459
|
end
|
|
71
460
|
end
|
|
72
461
|
end
|
|
462
|
+
# rubocop:enable Metrics/ClassLength
|
|
73
463
|
end
|
data/lib/ollama_agent/console.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module OllamaAgent
|
|
4
4
|
# ANSI styling for TTY output. Respects https://no-color.org/ via NO_COLOR.
|
|
5
5
|
# Assistant replies use tty-markdown when enabled (headings, lists, bold, code blocks).
|
|
6
|
+
# rubocop:disable Metrics/ModuleLength -- single-responsibility output module; methods are all short
|
|
6
7
|
module Console
|
|
7
8
|
module_function
|
|
8
9
|
|
|
@@ -38,6 +39,116 @@ module OllamaAgent
|
|
|
38
39
|
markdown_enabled? && ENV["OLLAMA_AGENT_THINKING_MARKDOWN"] == "1"
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
# +compact+ (default): one "Thinking" label per agent run; later reasoning uses blank lines only (Cursor-like).
|
|
43
|
+
# +framed+: repeat the full banner + rulers on every assistant message (legacy).
|
|
44
|
+
def thinking_framed_style?
|
|
45
|
+
ENV.fetch("OLLAMA_AGENT_THINKING_STYLE", "compact").to_s.strip.downcase == "framed"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @api private Reset at the start of each {Agent#run} so multi-turn tool loops share one thinking header.
|
|
49
|
+
def reset_thinking_session!
|
|
50
|
+
Thread.current[:ollama_agent_thinking_shown] = false
|
|
51
|
+
Thread.current[:ollama_agent_stream_thinking_open] = false
|
|
52
|
+
Thread.current[:ollama_agent_stream_had_thinking] = false
|
|
53
|
+
Thread.current[:ollama_agent_stream_thinking_buffer] = nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def thinking_already_shown_in_session?
|
|
57
|
+
Thread.current[:ollama_agent_thinking_shown] == true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def mark_thinking_shown_in_session!
|
|
61
|
+
Thread.current[:ollama_agent_thinking_shown] = true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# --- Streaming (ollama-client passes thinking only via patched hooks[:on_thinking]) ---
|
|
65
|
+
|
|
66
|
+
# Print one dim "Thinking" label, then stream fragments in dim until content tokens arrive.
|
|
67
|
+
# Handles both cumulative thinking strings (common from Ollama) and plain deltas; sanitizes UTF-8.
|
|
68
|
+
def write_streaming_thinking_fragment(fragment)
|
|
69
|
+
text = utf8_for_stream(fragment)
|
|
70
|
+
return if text.empty?
|
|
71
|
+
|
|
72
|
+
open_streaming_thinking_section_if_needed
|
|
73
|
+
to_print = streaming_thinking_increment_to_print(text)
|
|
74
|
+
return if to_print.empty?
|
|
75
|
+
|
|
76
|
+
print to_print
|
|
77
|
+
$stdout.flush
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def write_stream_token(fragment)
|
|
81
|
+
print utf8_for_stream(fragment)
|
|
82
|
+
$stdout.flush
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Call before the first streamed content token: closes dim reasoning, optional Assistant heading.
|
|
86
|
+
def finalize_streaming_thinking_before_content!
|
|
87
|
+
return unless Thread.current[:ollama_agent_stream_thinking_open]
|
|
88
|
+
|
|
89
|
+
Thread.current[:ollama_agent_stream_thinking_open] = false
|
|
90
|
+
had = Thread.current[:ollama_agent_stream_had_thinking]
|
|
91
|
+
Thread.current[:ollama_agent_stream_had_thinking] = false
|
|
92
|
+
Thread.current[:ollama_agent_stream_thinking_buffer] = nil
|
|
93
|
+
print "\e[0m" if color_enabled?
|
|
94
|
+
puts
|
|
95
|
+
puts assistant_reply_heading if had
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# When the model returns only thinking (no content), close ANSI state on stream end.
|
|
99
|
+
def close_streaming_thinking_if_still_open!
|
|
100
|
+
return unless Thread.current[:ollama_agent_stream_thinking_open]
|
|
101
|
+
|
|
102
|
+
Thread.current[:ollama_agent_stream_thinking_open] = false
|
|
103
|
+
Thread.current[:ollama_agent_stream_had_thinking] = false
|
|
104
|
+
Thread.current[:ollama_agent_stream_thinking_buffer] = nil
|
|
105
|
+
print "\e[0m" if color_enabled?
|
|
106
|
+
puts
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def open_streaming_thinking_section_if_needed
|
|
110
|
+
return if Thread.current[:ollama_agent_stream_thinking_open]
|
|
111
|
+
|
|
112
|
+
Thread.current[:ollama_agent_stream_thinking_open] = true
|
|
113
|
+
Thread.current[:ollama_agent_stream_had_thinking] = true
|
|
114
|
+
label = color_enabled? ? "#{dim("Thinking")}\n" : "Thinking\n"
|
|
115
|
+
print label
|
|
116
|
+
print "\e[2m" if color_enabled?
|
|
117
|
+
end
|
|
118
|
+
private_class_method :open_streaming_thinking_section_if_needed
|
|
119
|
+
|
|
120
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- cumulative vs delta stream shapes
|
|
121
|
+
def streaming_thinking_increment_to_print(next_utf8)
|
|
122
|
+
prev = Thread.current[:ollama_agent_stream_thinking_buffer].to_s
|
|
123
|
+
if prev.empty?
|
|
124
|
+
Thread.current[:ollama_agent_stream_thinking_buffer] = next_utf8
|
|
125
|
+
return next_utf8
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if next_utf8.start_with?(prev) && next_utf8.bytesize >= prev.bytesize
|
|
129
|
+
slice = next_utf8.byteslice(prev.bytesize..)
|
|
130
|
+
Thread.current[:ollama_agent_stream_thinking_buffer] = next_utf8
|
|
131
|
+
return slice
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
return +"" if next_utf8 == prev
|
|
135
|
+
|
|
136
|
+
if next_utf8.bytesize < prev.bytesize
|
|
137
|
+
Thread.current[:ollama_agent_stream_thinking_buffer] = next_utf8
|
|
138
|
+
return next_utf8
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
Thread.current[:ollama_agent_stream_thinking_buffer] = prev + next_utf8
|
|
142
|
+
next_utf8
|
|
143
|
+
end
|
|
144
|
+
private_class_method :streaming_thinking_increment_to_print
|
|
145
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
146
|
+
|
|
147
|
+
def utf8_for_stream(fragment)
|
|
148
|
+
fragment.to_s.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
149
|
+
end
|
|
150
|
+
private_class_method :utf8_for_stream
|
|
151
|
+
|
|
41
152
|
def style(text, *codes)
|
|
42
153
|
return text.to_s unless color_enabled?
|
|
43
154
|
|
|
@@ -85,6 +196,36 @@ module OllamaAgent
|
|
|
85
196
|
"#{header}#{body}\n#{line}"
|
|
86
197
|
end
|
|
87
198
|
|
|
199
|
+
def format_thinking_compact_open(text)
|
|
200
|
+
label = color_enabled? ? dim("Thinking") : "Thinking"
|
|
201
|
+
body = thinking_compact_body(text)
|
|
202
|
+
"#{label}\n#{body}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Later thinking in the same run (tool rounds, new chat chunks): same block, separated by a blank line.
|
|
206
|
+
def format_thinking_compact_merge(text)
|
|
207
|
+
"\n#{thinking_compact_body(text)}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def thinking_compact_body(text)
|
|
211
|
+
if thinking_markdown_enabled?
|
|
212
|
+
parsed = markdown_parse(text, thinking: true)
|
|
213
|
+
return parsed if parsed
|
|
214
|
+
|
|
215
|
+
return dim_indent_body(text)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
dim_indent_body(text)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def dim_indent_body(text)
|
|
222
|
+
s = text.to_s.rstrip
|
|
223
|
+
return "" if s.empty?
|
|
224
|
+
|
|
225
|
+
indent = " "
|
|
226
|
+
dim(s.lines.map { |l| "#{indent}#{l.rstrip}" }.join("\n"))
|
|
227
|
+
end
|
|
228
|
+
|
|
88
229
|
def assistant_reply_heading
|
|
89
230
|
bold(green("Assistant"))
|
|
90
231
|
end
|
|
@@ -113,13 +254,33 @@ module OllamaAgent
|
|
|
113
254
|
|
|
114
255
|
# Prints thinking (if any) then main content; duck-types #thinking and #content.
|
|
115
256
|
def puts_assistant_message(message)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
257
|
+
thinking_present = assistant_message_thinking_present?(message.thinking)
|
|
258
|
+
if thinking_present
|
|
259
|
+
puts thinking_output_chunk(message.thinking)
|
|
260
|
+
mark_thinking_shown_in_session! unless thinking_framed_style?
|
|
261
|
+
end
|
|
262
|
+
assistant_reply_if_present(message.content, thinking_present)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def assistant_message_thinking_present?(text)
|
|
266
|
+
text && !text.to_s.strip.empty?
|
|
267
|
+
end
|
|
268
|
+
private_class_method :assistant_message_thinking_present?
|
|
269
|
+
|
|
270
|
+
def assistant_reply_if_present(content, thinking_present)
|
|
271
|
+
return unless content && !content.to_s.strip.empty?
|
|
119
272
|
|
|
120
|
-
|
|
121
|
-
write_assistant_reply(c, thinking_present) if c && !c.to_s.empty?
|
|
273
|
+
write_assistant_reply(content, thinking_present)
|
|
122
274
|
end
|
|
275
|
+
private_class_method :assistant_reply_if_present
|
|
276
|
+
|
|
277
|
+
def thinking_output_chunk(text)
|
|
278
|
+
return format_thinking(text) if thinking_framed_style?
|
|
279
|
+
return format_thinking_compact_open(text) unless thinking_already_shown_in_session?
|
|
280
|
+
|
|
281
|
+
format_thinking_compact_merge(text)
|
|
282
|
+
end
|
|
283
|
+
private_class_method :thinking_output_chunk
|
|
123
284
|
|
|
124
285
|
def patch_title(text)
|
|
125
286
|
bold(yellow(text))
|
|
@@ -132,5 +293,16 @@ module OllamaAgent
|
|
|
132
293
|
def error_line(text)
|
|
133
294
|
red(text)
|
|
134
295
|
end
|
|
296
|
+
|
|
297
|
+
def tool_call_line(name, args)
|
|
298
|
+
keys = args.is_a?(Hash) ? args.keys.first(2).join(", ") : ""
|
|
299
|
+
cyan("[tool→] #{name}(#{keys})")
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def tool_result_line(name, result)
|
|
303
|
+
preview = result.to_s[0, 60].gsub(/\s+/, " ")
|
|
304
|
+
dim("[tool←] #{name}: #{preview}")
|
|
305
|
+
end
|
|
135
306
|
end
|
|
307
|
+
# rubocop:enable Metrics/ModuleLength
|
|
136
308
|
end
|