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/agent.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "agent_prompt"
|
|
4
|
+
require_relative "prompt_skills"
|
|
4
5
|
require_relative "console"
|
|
5
6
|
require_relative "ollama_connection"
|
|
6
7
|
require_relative "tools_schema"
|
|
@@ -8,52 +9,139 @@ require_relative "sandboxed_tools"
|
|
|
8
9
|
require_relative "think_param"
|
|
9
10
|
require_relative "timeout_param"
|
|
10
11
|
require_relative "tool_content_parser"
|
|
12
|
+
require_relative "streaming/hooks"
|
|
13
|
+
require_relative "resilience/retry_middleware"
|
|
14
|
+
require_relative "resilience/audit_logger"
|
|
15
|
+
require_relative "context/manager"
|
|
16
|
+
require_relative "session/store"
|
|
17
|
+
require_relative "env_config"
|
|
18
|
+
require_relative "model_env"
|
|
19
|
+
require_relative "agent/agent_config"
|
|
20
|
+
require_relative "agent/client_wiring"
|
|
21
|
+
require_relative "agent/prompt_wiring"
|
|
22
|
+
require_relative "agent/session_wiring"
|
|
11
23
|
|
|
12
24
|
module OllamaAgent
|
|
13
25
|
# Runs a tool-calling loop against Ollama: read files, search, apply unified diffs.
|
|
26
|
+
# Public entry: {#run}. Other instance methods are internal to the agent loop.
|
|
27
|
+
# rubocop:disable Metrics/ClassLength -- facade coordinates includes and turn loop
|
|
14
28
|
class Agent
|
|
15
29
|
include SandboxedTools
|
|
30
|
+
include ClientWiring
|
|
31
|
+
include PromptWiring
|
|
32
|
+
include SessionWiring
|
|
16
33
|
|
|
17
34
|
MAX_TURNS = 64
|
|
18
|
-
# ollama-client defaults to 30s; multi-turn tool chats often need longer on local hardware.
|
|
19
35
|
DEFAULT_HTTP_TIMEOUT = 120
|
|
20
36
|
|
|
21
|
-
attr_reader :client, :root
|
|
22
|
-
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
attr_reader :client, :root, :hooks
|
|
38
|
+
|
|
39
|
+
# @param config [AgentConfig, nil] when set, keyword options are ignored (use {Runner} or build {AgentConfig}).
|
|
40
|
+
# rubocop:disable Metrics/ParameterLists
|
|
41
|
+
# rubocop:disable Metrics/MethodLength
|
|
42
|
+
def initialize(client: nil, config: nil, model: nil, root: nil, confirm_patches: true, http_timeout: nil,
|
|
43
|
+
think: nil,
|
|
44
|
+
read_only: false, patch_policy: nil,
|
|
45
|
+
skill_paths: nil, skills_enabled: nil, skills_include: nil, skills_exclude: nil,
|
|
46
|
+
external_skills_enabled: nil,
|
|
47
|
+
orchestrator: false, confirm_delegation: nil,
|
|
48
|
+
max_retries: nil, audit: nil,
|
|
49
|
+
session_id: nil, resume: false,
|
|
50
|
+
max_tokens: nil, context_summarize: nil,
|
|
51
|
+
stdin: $stdin, stdout: $stdout)
|
|
52
|
+
cfg = config || AgentConfig.new(
|
|
53
|
+
model: model, root: root, confirm_patches: confirm_patches, http_timeout: http_timeout, think: think,
|
|
54
|
+
read_only: read_only, patch_policy: patch_policy,
|
|
55
|
+
skill_paths: skill_paths, skills_enabled: skills_enabled, skills_include: skills_include,
|
|
56
|
+
skills_exclude: skills_exclude, external_skills_enabled: external_skills_enabled,
|
|
57
|
+
orchestrator: orchestrator, confirm_delegation: confirm_delegation,
|
|
58
|
+
max_retries: max_retries, audit: audit, session_id: session_id, resume: resume,
|
|
59
|
+
max_tokens: max_tokens, context_summarize: context_summarize, stdin: stdin, stdout: stdout
|
|
60
|
+
)
|
|
61
|
+
apply_agent_config(cfg)
|
|
62
|
+
@user_prompt = UserPrompt.new(stdin: cfg.stdin, stdout: cfg.stdout)
|
|
63
|
+
@context_manager = Context::Manager.new(max_tokens: @max_tokens, context_summarize: @context_summarize)
|
|
64
|
+
@hooks = Streaming::Hooks.new
|
|
65
|
+
attach_audit_logger if resolved_audit_enabled
|
|
30
66
|
@client = client || build_default_client
|
|
31
67
|
end
|
|
68
|
+
# rubocop:enable Metrics/MethodLength
|
|
32
69
|
# rubocop:enable Metrics/ParameterLists
|
|
33
70
|
|
|
34
71
|
def run(query)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{ role: "user", content: query }
|
|
38
|
-
]
|
|
39
|
-
|
|
72
|
+
Console.reset_thinking_session!
|
|
73
|
+
messages = build_messages_for_run(query)
|
|
40
74
|
execute_agent_turns(messages)
|
|
41
75
|
end
|
|
42
76
|
|
|
43
77
|
private
|
|
44
78
|
|
|
79
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- maps AgentConfig to ivars + resolved max turns
|
|
80
|
+
def apply_agent_config(cfg)
|
|
81
|
+
@model = cfg.model || default_model
|
|
82
|
+
@root = File.expand_path(cfg.root || ENV.fetch("OLLAMA_AGENT_ROOT", Dir.pwd))
|
|
83
|
+
@confirm_patches = cfg.confirm_patches
|
|
84
|
+
@read_only = cfg.read_only
|
|
85
|
+
@patch_policy = cfg.patch_policy
|
|
86
|
+
@http_timeout_override = cfg.http_timeout
|
|
87
|
+
@think = cfg.think
|
|
88
|
+
@skill_paths = cfg.skill_paths
|
|
89
|
+
@skills_enabled = cfg.skills_enabled
|
|
90
|
+
@skills_include = cfg.skills_include
|
|
91
|
+
@skills_exclude = cfg.skills_exclude
|
|
92
|
+
@external_skills_enabled = cfg.external_skills_enabled
|
|
93
|
+
@orchestrator = cfg.orchestrator
|
|
94
|
+
@confirm_delegation = cfg.resolved_confirm_delegation
|
|
95
|
+
@max_retries = cfg.max_retries
|
|
96
|
+
@audit = cfg.audit
|
|
97
|
+
@session_id = cfg.session_id
|
|
98
|
+
@resume = cfg.resume
|
|
99
|
+
@max_tokens = cfg.max_tokens
|
|
100
|
+
@context_summarize = cfg.context_summarize
|
|
101
|
+
strict = EnvConfig.strict_env?
|
|
102
|
+
@max_turns = EnvConfig.fetch_int("OLLAMA_AGENT_MAX_TURNS", MAX_TURNS, strict: strict)
|
|
103
|
+
end
|
|
104
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
105
|
+
|
|
106
|
+
# rubocop:disable Metrics/MethodLength -- turn loop with early break
|
|
45
107
|
def execute_agent_turns(messages)
|
|
46
|
-
|
|
47
|
-
|
|
108
|
+
@current_turn = 0
|
|
109
|
+
@max_turns.times do
|
|
110
|
+
@current_turn += 1
|
|
111
|
+
trimmed = trimmed_messages_for_chat(messages)
|
|
112
|
+
message = chat_assistant_message(trimmed)
|
|
48
113
|
tool_calls = tool_calls_from(message)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return if tool_calls.empty?
|
|
114
|
+
persist_assistant_turn(messages, message)
|
|
115
|
+
break if tool_calls.empty?
|
|
52
116
|
|
|
53
117
|
append_tool_results(messages, tool_calls)
|
|
54
118
|
end
|
|
119
|
+
emit_turn_complete(messages)
|
|
120
|
+
warn_max_turns_if_needed
|
|
121
|
+
end
|
|
122
|
+
# rubocop:enable Metrics/MethodLength
|
|
55
123
|
|
|
56
|
-
|
|
124
|
+
def trimmed_messages_for_chat(messages)
|
|
125
|
+
@context_manager.trim(messages)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def persist_assistant_turn(messages, message)
|
|
129
|
+
messages << message.to_h
|
|
130
|
+
save_message_to_session(message.to_h)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def emit_turn_complete(messages)
|
|
134
|
+
@hooks.emit(:on_complete, { messages: messages, turns: @current_turn })
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def warn_max_turns_if_needed
|
|
138
|
+
return unless ENV["OLLAMA_AGENT_DEBUG"] == "1" && @current_turn >= @max_turns
|
|
139
|
+
|
|
140
|
+
warn "ollama_agent: maximum tool rounds (#{@max_turns}) reached"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def current_turn
|
|
144
|
+
@current_turn || 0
|
|
57
145
|
end
|
|
58
146
|
|
|
59
147
|
def tool_calls_from(message)
|
|
@@ -63,15 +151,16 @@ module OllamaAgent
|
|
|
63
151
|
ToolContentParser.synthetic_calls(message.content)
|
|
64
152
|
end
|
|
65
153
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
154
|
+
def chat_assistant_message(messages)
|
|
155
|
+
if @hooks.subscribed?(:on_token)
|
|
156
|
+
stream_assistant_message(messages)
|
|
157
|
+
else
|
|
158
|
+
block_assistant_message(messages)
|
|
159
|
+
end
|
|
70
160
|
end
|
|
71
161
|
|
|
72
|
-
def
|
|
162
|
+
def block_assistant_message(messages)
|
|
73
163
|
response = @client.chat(**chat_request_args(messages))
|
|
74
|
-
|
|
75
164
|
message = response.message
|
|
76
165
|
raise Error, "Empty assistant message" if message.nil?
|
|
77
166
|
|
|
@@ -79,16 +168,41 @@ module OllamaAgent
|
|
|
79
168
|
message
|
|
80
169
|
end
|
|
81
170
|
|
|
171
|
+
def stream_assistant_message(messages)
|
|
172
|
+
response = @client.chat(**chat_request_args(messages), hooks: ollama_stream_hooks)
|
|
173
|
+
message = response.message
|
|
174
|
+
raise Error, "Empty assistant message" if message.nil?
|
|
175
|
+
|
|
176
|
+
message
|
|
177
|
+
end
|
|
178
|
+
|
|
82
179
|
def chat_request_args(messages)
|
|
83
|
-
|
|
180
|
+
base_chat_request_args(messages).tap do |args|
|
|
181
|
+
th = resolve_think
|
|
182
|
+
args[:think] = th unless th.nil?
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def base_chat_request_args(messages)
|
|
187
|
+
{
|
|
84
188
|
messages: messages,
|
|
85
|
-
tools:
|
|
189
|
+
tools: OllamaAgent.tools_for(read_only: @read_only, orchestrator: @orchestrator),
|
|
86
190
|
model: @model,
|
|
87
191
|
options: { temperature: 0.2 }
|
|
88
192
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def ollama_stream_hooks
|
|
196
|
+
{
|
|
197
|
+
on_thinking: ->(fragment) { @hooks.emit(:on_thinking, { token: fragment.to_s, turn: current_turn }) },
|
|
198
|
+
on_token: lambda do |*args|
|
|
199
|
+
token = args[0]
|
|
200
|
+
logprobs = args[1]
|
|
201
|
+
payload = { token: token, turn: current_turn }
|
|
202
|
+
payload[:logprobs] = logprobs unless logprobs.nil?
|
|
203
|
+
@hooks.emit(:on_token, payload)
|
|
204
|
+
end
|
|
205
|
+
}
|
|
92
206
|
end
|
|
93
207
|
|
|
94
208
|
def announce_assistant_content(message)
|
|
@@ -100,47 +214,8 @@ module OllamaAgent
|
|
|
100
214
|
end
|
|
101
215
|
|
|
102
216
|
def default_model
|
|
103
|
-
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def build_default_client
|
|
107
|
-
config = Ollama::Config.new
|
|
108
|
-
@http_timeout_seconds = resolved_http_timeout_seconds
|
|
109
|
-
config.timeout = @http_timeout_seconds
|
|
110
|
-
OllamaConnection.apply_env_to_config(config)
|
|
111
|
-
Ollama::Client.new(config: config)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def resolved_http_timeout_seconds
|
|
115
|
-
parsed = TimeoutParam.parse_positive(@http_timeout_override)
|
|
116
|
-
return parsed if parsed
|
|
117
|
-
|
|
118
|
-
parsed = TimeoutParam.parse_positive(ENV.fetch("OLLAMA_AGENT_TIMEOUT", nil))
|
|
119
|
-
return parsed if parsed
|
|
120
|
-
|
|
121
|
-
DEFAULT_HTTP_TIMEOUT
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def system_prompt
|
|
125
|
-
AgentPrompt.text
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def append_tool_results(messages, tool_calls)
|
|
129
|
-
tool_calls.each do |tool_call|
|
|
130
|
-
result = execute_tool(tool_call.name, tool_call.arguments || {})
|
|
131
|
-
messages << tool_message(tool_call, result)
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def tool_message(tool_call, result)
|
|
136
|
-
msg = {
|
|
137
|
-
role: "tool",
|
|
138
|
-
name: tool_call.name,
|
|
139
|
-
content: result.to_s
|
|
140
|
-
}
|
|
141
|
-
id = tool_call.id
|
|
142
|
-
msg[:tool_call_id] = id if id && !id.to_s.empty?
|
|
143
|
-
msg
|
|
217
|
+
ModelEnv.default_chat_model
|
|
144
218
|
end
|
|
145
219
|
end
|
|
220
|
+
# rubocop:enable Metrics/ClassLength
|
|
146
221
|
end
|
|
@@ -5,7 +5,7 @@ module OllamaAgent
|
|
|
5
5
|
module AgentPrompt
|
|
6
6
|
def self.text
|
|
7
7
|
<<~PROMPT
|
|
8
|
-
You are a coding assistant with tools: list_files, read_file, search_code, edit_file.
|
|
8
|
+
You are a coding assistant with tools: list_files, read_file, search_code, edit_file, write_file.
|
|
9
9
|
Work only under the project root. Briefly state your plan, then use tools.
|
|
10
10
|
|
|
11
11
|
Large Ruby codebases: use search_code with mode "method", "class", "module", or "constant" to locate definitions
|
|
@@ -15,6 +15,9 @@ module OllamaAgent
|
|
|
15
15
|
Do not paste JSON tool calls or {"name": ...} blocks in your reply text. Tools run only when the host
|
|
16
16
|
receives native tool calls from the model API—not from prose. Never put commas after --- or +++ file lines.
|
|
17
17
|
|
|
18
|
+
Use write_file to create a new file or fully replace an existing file with complete content.
|
|
19
|
+
Prefer edit_file for surgical changes to existing files; reserve write_file for new files or full rewrites.
|
|
20
|
+
|
|
18
21
|
For README or documentation updates that should reflect the codebase:
|
|
19
22
|
1) list_files on "." or "lib" (and read ollama_agent.gemspec if present) to see structure.
|
|
20
23
|
2) read_file every file you will change before editing (e.g. README.md, lib/ollama_agent.rb).
|
|
@@ -40,5 +43,32 @@ module OllamaAgent
|
|
|
40
43
|
them as the assistant message.
|
|
41
44
|
PROMPT
|
|
42
45
|
end
|
|
46
|
+
|
|
47
|
+
def self.self_review_text
|
|
48
|
+
<<~PROMPT
|
|
49
|
+
You are reviewing the ollama_agent Ruby gem. Tools available: list_files, read_file, search_code only.
|
|
50
|
+
Do not call edit_file and do not output unified diffs—this run is analysis-only.
|
|
51
|
+
|
|
52
|
+
The user message may begin with a "## Static analysis (ruby_mastery)" section from automated tooling; weigh it
|
|
53
|
+
against what you verify in the tree.
|
|
54
|
+
|
|
55
|
+
Work only under the project root. Briefly state your plan, then use tools.
|
|
56
|
+
|
|
57
|
+
Large Ruby trees: use search_code with mode "method", "class", "module", or "constant" to locate definitions
|
|
58
|
+
via the Prism index, then read_file with start_line/end_line for only the lines you need.
|
|
59
|
+
|
|
60
|
+
Final reply: strengths, risks, and concrete suggestions with file paths (and line numbers when clear).
|
|
61
|
+
Do not paste JSON tool calls in prose; tools run only via native tool calls from the API.
|
|
62
|
+
PROMPT
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.orchestrator_addon
|
|
66
|
+
<<~PROMPT
|
|
67
|
+
Orchestrator mode: you may call list_external_agents to see which external CLI tools are installed,
|
|
68
|
+
then delegate_to_agent with a valid agent_id from that list. Gather context with read_file and
|
|
69
|
+
search_code first; keep task and context_summary short. Do not invent agent_id values.
|
|
70
|
+
External runs use non-interactive argv only; cwd is the project root.
|
|
71
|
+
PROMPT
|
|
72
|
+
end
|
|
43
73
|
end
|
|
44
74
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
# ollama-client 1.1.0 +process_chat_stream_chunk+ returns the *previous* +last_data+ for every
|
|
5
|
+
# non-+done+ line, so +message.tool_calls+ seen on an intermediate NDJSON row are dropped when the
|
|
6
|
+
# final +done+ row omits them. Carry forward merged state and copy +tool_calls+ onto the +done+ row.
|
|
7
|
+
module ChatStreamCarry
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def next_last_data(prev, obj)
|
|
11
|
+
return json_dup(obj) if prev.nil? && !truthy_done?(obj)
|
|
12
|
+
return prev if truthy_done?(obj)
|
|
13
|
+
|
|
14
|
+
merge_carry(prev, obj)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def stitch_done_message_tool_calls!(done_obj, prev_carry)
|
|
18
|
+
prev_tc = tool_calls_from_carry(prev_carry)
|
|
19
|
+
return if prev_tc.nil? || !truthy_done?(done_obj)
|
|
20
|
+
|
|
21
|
+
apply_tool_calls_to_done!(done_obj, prev_tc)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def merge_carry(prev, obj)
|
|
25
|
+
merged = json_dup(prev)
|
|
26
|
+
chunk_msg = obj["message"]
|
|
27
|
+
return merged if chunk_msg.nil? || !chunk_msg.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
merge_message_fields!(merged, chunk_msg)
|
|
30
|
+
merged
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def truthy_done?(obj)
|
|
34
|
+
obj.is_a?(Hash) && (obj["done"] == true || obj[:done] == true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def json_dup(payload)
|
|
38
|
+
return payload if payload.nil?
|
|
39
|
+
|
|
40
|
+
JSON.parse(JSON.generate(payload))
|
|
41
|
+
rescue JSON::GeneratorError, JSON::ParserError, TypeError
|
|
42
|
+
dup_via_marshal(payload)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def tool_calls_from_carry(prev_carry)
|
|
46
|
+
return unless prev_carry.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
prev_msg = prev_carry["message"]
|
|
49
|
+
return unless prev_msg.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
tc = prev_msg["tool_calls"]
|
|
52
|
+
return tc if tc.is_a?(Array) && !tc.empty?
|
|
53
|
+
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
private_class_method :tool_calls_from_carry
|
|
57
|
+
|
|
58
|
+
def apply_tool_calls_to_done!(done_obj, prev_tc)
|
|
59
|
+
done_msg = done_obj["message"]
|
|
60
|
+
unless done_msg.is_a?(Hash)
|
|
61
|
+
done_obj["message"] = { "role" => "assistant", "tool_calls" => prev_tc }
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
done_tc = done_msg["tool_calls"]
|
|
66
|
+
return if done_tc.is_a?(Array) && !done_tc.empty?
|
|
67
|
+
|
|
68
|
+
done_msg["tool_calls"] = prev_tc
|
|
69
|
+
end
|
|
70
|
+
private_class_method :apply_tool_calls_to_done!
|
|
71
|
+
|
|
72
|
+
def merge_message_fields!(merged, chunk_msg)
|
|
73
|
+
mm = (merged["message"] ||= {})
|
|
74
|
+
tc = chunk_msg["tool_calls"]
|
|
75
|
+
mm["tool_calls"] = tc if tc.is_a?(Array) && !tc.empty?
|
|
76
|
+
role = chunk_msg["role"]
|
|
77
|
+
mm["role"] = role if role && !role.to_s.strip.empty?
|
|
78
|
+
end
|
|
79
|
+
private_class_method :merge_message_fields!
|
|
80
|
+
|
|
81
|
+
def dup_via_marshal(payload)
|
|
82
|
+
Marshal.load(Marshal.dump(payload))
|
|
83
|
+
rescue ArgumentError, TypeError
|
|
84
|
+
payload.dup
|
|
85
|
+
end
|
|
86
|
+
private_class_method :dup_via_marshal
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OllamaAgent
|
|
4
|
+
# Coerces streamed +message.thinking+ payloads to a String before ollama-client appends
|
|
5
|
+
# with +full_thinking << thinking+ (which raises TypeError on Hash/Array for some models/APIs).
|
|
6
|
+
module ChatStreamThinkingFormat
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def normalize_message_thinking!(message_hash)
|
|
10
|
+
return unless message_hash.is_a?(Hash)
|
|
11
|
+
|
|
12
|
+
raw = message_hash["thinking"]
|
|
13
|
+
return if raw.nil? || raw.is_a?(String)
|
|
14
|
+
|
|
15
|
+
message_hash["thinking"] = coerce_thinking_to_string(raw)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def coerce_thinking_to_string(raw)
|
|
19
|
+
case raw
|
|
20
|
+
when String then raw
|
|
21
|
+
when Array then raw.map { |elem| coerce_thinking_to_string(elem) }.join
|
|
22
|
+
else
|
|
23
|
+
JSON.generate(raw)
|
|
24
|
+
end
|
|
25
|
+
rescue JSON::GeneratorError, TypeError
|
|
26
|
+
raw.to_s
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|