rubik 0.3.4 → 0.4.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/ATTRIBUTION.md +1 -1
- data/CHANGELOG.md +105 -2
- data/lib/rubik/adapter/process.rb +11 -8
- data/lib/rubik/ast/go_extractor.rb +9 -2
- data/lib/rubik/ast/graph.rb +20 -1
- data/lib/rubik/ast/graph_extensions.rb +89 -1
- data/lib/rubik/ast/python_extractor.rb +5 -5
- data/lib/rubik/ast/rust_extractor.rb +9 -2
- data/lib/rubik/ast/typescript_extractor.rb +13 -7
- data/lib/rubik/buffer/manager.rb +78 -66
- data/lib/rubik/config/defaults.rb +1 -0
- data/lib/rubik/context/git_lister.rb +3 -1
- data/lib/rubik/diagnostics/parser.rb +53 -1
- data/lib/rubik/llm/anthropic.rb +5 -2
- data/lib/rubik/llm/openai_compatible.rb +83 -68
- data/lib/rubik/llm/registry.rb +81 -74
- data/lib/rubik/lsp/client.rb +320 -96
- data/lib/rubik/lsp/manager.rb +83 -12
- data/lib/rubik/lsp/server_command.rb +102 -0
- data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
- data/lib/rubik/orchestrator/step_executor.rb +138 -154
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
- data/lib/rubik/prompt/token_estimator.rb +16 -12
- data/lib/rubik/repl/commands/agent_context.rb +126 -104
- data/lib/rubik/repl/commands/agent_execution.rb +83 -68
- data/lib/rubik/repl/commands/agent_models.rb +112 -87
- data/lib/rubik/repl/commands/buffer.rb +122 -88
- data/lib/rubik/repl/commands/mcp.rb +120 -92
- data/lib/rubik/repl/commands/session.rb +90 -59
- data/lib/rubik/repl/commands/workspace.rb +110 -94
- data/lib/rubik/repl/shell.rb +10 -1
- data/lib/rubik/runtime/discovery.rb +12 -6
- data/lib/rubik/security/classifier/classifier.rb +3 -1
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +6 -2
- data/lib/rubik/storage/vakanuvis/gate.rb +37 -16
- data/lib/rubik/storage/vakanuvis/harness.rb +36 -21
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +12 -6
- data/lib/rubik/tools/builtins/lifecycle.rb +34 -7
- data/lib/rubik/tools/builtins/lsp.rb +36 -8
- data/lib/rubik/tools/truncator.rb +21 -8
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
|
@@ -19,20 +19,9 @@ module Rubik
|
|
|
19
19
|
include MaestroGate
|
|
20
20
|
|
|
21
21
|
ERROR_PREFIXES = [
|
|
22
|
-
"-- ",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"Usage:",
|
|
26
|
-
"Pre-flight verification failed",
|
|
27
|
-
"Bilinmeyen komut",
|
|
28
|
-
"Failed",
|
|
29
|
-
"Security blocked",
|
|
30
|
-
"Command error",
|
|
31
|
-
"[EXIT ",
|
|
32
|
-
"Test run [FAILED]",
|
|
33
|
-
"Lint [FAILED]",
|
|
34
|
-
"[CONFLICT]",
|
|
35
|
-
"[CONFLICT "
|
|
22
|
+
"-- ", "Error:", "Error ", "Usage:", "Pre-flight verification failed",
|
|
23
|
+
"Bilinmeyen komut", "Failed", "Security blocked", "Command error",
|
|
24
|
+
"[EXIT ", "Test run [FAILED]", "Lint [FAILED]", "[CONFLICT]", "[CONFLICT "
|
|
36
25
|
].freeze
|
|
37
26
|
|
|
38
27
|
# Plan 87 E-1: state is reached through the ShellDeps façade. Accepts a
|
|
@@ -98,7 +87,6 @@ module Rubik
|
|
|
98
87
|
|
|
99
88
|
def execute_registered_tool(tool_instance, tool_name, tool_args, call_id, auto_apply_mode = nil)
|
|
100
89
|
@deps.on_step&.call(type: :tool_call, tool: tool_name, args: tool_args, step: @deps.step_count, max_steps: @deps.max_steps, call_id: call_id)
|
|
101
|
-
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
102
90
|
|
|
103
91
|
# Opt-in circuit breaker (G19): when enabled and the circuit for this
|
|
104
92
|
# tool is open, raise into the normal error-result plumbing below so
|
|
@@ -107,111 +95,13 @@ module Rubik
|
|
|
107
95
|
breaker = circuit_breaker_for
|
|
108
96
|
blocked_by_circuit = breaker && !breaker.allow?(tool_instance.name)
|
|
109
97
|
|
|
110
|
-
tool_result =
|
|
111
|
-
if blocked_by_circuit
|
|
112
|
-
raise ToolCircuitBreaker::CircuitOpenError,
|
|
113
|
-
"circuit open for '#{tool_instance.name}' — repeated failures; waiting out the cooldown before retrying"
|
|
114
|
-
end
|
|
98
|
+
raw_output, tool_result = invoke_tool_and_normalize(tool_instance, tool_name, tool_args, call_id, blocked_by_circuit: blocked_by_circuit)
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
118
|
-
dur = ((t1 - t0) * 1000).round(1)
|
|
119
|
-
|
|
120
|
-
max_chars = @deps.shell&.config&.fetch("tools.max_output_chars", 12_000) || 12_000
|
|
121
|
-
max_lines = @deps.shell&.config&.fetch("tools.max_output_lines", 300) || 300
|
|
122
|
-
trunc_strat = @deps.shell&.config&.fetch("tools.truncation_strategy", "head_tail") || "head_tail"
|
|
123
|
-
|
|
124
|
-
if raw_output.is_a?(Rubik::Tools::Result)
|
|
125
|
-
trunc_res = Rubik::Tools::Truncator.truncate(raw_output.output, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
|
|
126
|
-
effective_dur = raw_output.duration_ms.positive? ? raw_output.duration_ms : dur
|
|
127
|
-
first_line = raw_output.output.lines.first.to_s.strip
|
|
128
|
-
first_line = nil if first_line.empty?
|
|
129
|
-
Rubik::Tools::Result.new(
|
|
130
|
-
tool_name: raw_output.tool_name.to_s.empty? ? tool_name : raw_output.tool_name,
|
|
131
|
-
call_id: raw_output.call_id.to_s.empty? ? call_id : raw_output.call_id,
|
|
132
|
-
status: raw_output.status,
|
|
133
|
-
error: raw_output.error || (raw_output.error? ? first_line : nil),
|
|
134
|
-
exit_code: raw_output.exit_code,
|
|
135
|
-
output: trunc_res[:text],
|
|
136
|
-
duration_ms: effective_dur,
|
|
137
|
-
truncated: raw_output.truncated || trunc_res[:truncated],
|
|
138
|
-
metadata: raw_output.metadata
|
|
139
|
-
)
|
|
140
|
-
elsif raw_output.respond_to?(:status) && raw_output.respond_to?(:output)
|
|
141
|
-
out_str = raw_output.output.to_s
|
|
142
|
-
trunc_res = Rubik::Tools::Truncator.truncate(out_str, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
|
|
143
|
-
st = raw_output.status.to_sym
|
|
144
|
-
first_line = out_str.lines.first.to_s.strip
|
|
145
|
-
first_line = nil if first_line.empty?
|
|
146
|
-
err = (raw_output.respond_to?(:error) && raw_output.error) || (st == :error ? first_line : nil)
|
|
147
|
-
Rubik::Tools::Result.new(
|
|
148
|
-
tool_name: tool_name,
|
|
149
|
-
call_id: call_id,
|
|
150
|
-
status: st,
|
|
151
|
-
error: err,
|
|
152
|
-
output: trunc_res[:text],
|
|
153
|
-
duration_ms: dur,
|
|
154
|
-
truncated: trunc_res[:truncated],
|
|
155
|
-
# Structured fields ride along when the tool provides them
|
|
156
|
-
# (issue #127): the same Result contract for every carrier.
|
|
157
|
-
exit_code: raw_output.respond_to?(:exit_code) ? raw_output.exit_code : nil,
|
|
158
|
-
metadata: raw_output.respond_to?(:metadata) ? raw_output.metadata : {}
|
|
159
|
-
)
|
|
160
|
-
else
|
|
161
|
-
out_str = raw_output.to_s
|
|
162
|
-
trunc_res = Rubik::Tools::Truncator.truncate(out_str, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
|
|
163
|
-
|
|
164
|
-
is_err = if raw_output.respond_to?(:error?)
|
|
165
|
-
raw_output.error?
|
|
166
|
-
elsif raw_output.respond_to?(:failed?)
|
|
167
|
-
raw_output.failed?
|
|
168
|
-
elsif raw_output.is_a?(String)
|
|
169
|
-
ERROR_PREFIXES.any? { |pfx| raw_output.start_with?(pfx) }
|
|
170
|
-
else
|
|
171
|
-
false
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
first_line = out_str.lines.first.to_s.strip
|
|
175
|
-
first_line = nil if first_line.empty?
|
|
176
|
-
|
|
177
|
-
Rubik::Tools::Result.new(
|
|
178
|
-
tool_name: tool_name,
|
|
179
|
-
call_id: call_id,
|
|
180
|
-
status: is_err ? :error : :ok,
|
|
181
|
-
error: is_err ? first_line : nil,
|
|
182
|
-
output: trunc_res[:text],
|
|
183
|
-
duration_ms: dur,
|
|
184
|
-
truncated: trunc_res[:truncated]
|
|
185
|
-
)
|
|
186
|
-
end
|
|
187
|
-
rescue StandardError => e
|
|
188
|
-
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
189
|
-
dur = ((t1 - t0) * 1000).round(1)
|
|
190
|
-
err_msg = "Error executing tool '#{tool_name}': #{e.class} - #{e.message}"
|
|
191
|
-
Rubik::Tools::Result.new(
|
|
192
|
-
tool_name: tool_name,
|
|
193
|
-
call_id: call_id,
|
|
194
|
-
status: :error,
|
|
195
|
-
error: e.message,
|
|
196
|
-
output: err_msg,
|
|
197
|
-
duration_ms: dur
|
|
198
|
-
)
|
|
199
|
-
end
|
|
100
|
+
record_circuit_breaker_result(breaker, tool_instance.name, tool_result.status) if breaker && !blocked_by_circuit
|
|
200
101
|
|
|
201
102
|
# always-mode flush after every tool — was historically silent; the
|
|
202
103
|
# post_tool event makes the disk write visible like every other apply
|
|
203
|
-
|
|
204
|
-
if tool_result.status == :ok
|
|
205
|
-
breaker.record_success(tool_instance.name)
|
|
206
|
-
else
|
|
207
|
-
breaker.record_failure(tool_instance.name)
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
if AutoApply.apply?(@deps.config, trigger: :post_tool, mode: auto_apply_mode) && @deps.buffer_manager&.dirty?
|
|
212
|
-
applied = AutoApply.apply_and_sync(@deps.buffer_manager, @deps.shell)
|
|
213
|
-
@deps.on_step&.call(type: :auto_apply, reason: "post_tool", files: applied, step: @deps.step_count, max_steps: @deps.max_steps)
|
|
214
|
-
end
|
|
104
|
+
perform_post_tool_auto_apply(auto_apply_mode)
|
|
215
105
|
|
|
216
106
|
# Real-time LSP In-Buffer Diagnostics feedback loop (Plan 56 D-5,
|
|
217
107
|
# extracted to LspFeedback for the line budget).
|
|
@@ -222,39 +112,9 @@ module Rubik
|
|
|
222
112
|
# çıktısı tam içerikle kullanıcıya :user_output olarak akar; model,
|
|
223
113
|
# history ve :tool_result özeti yalnız stub görür. Event birincil
|
|
224
114
|
# teslimat olduğu için onsuz stub yalan olur — kaynak yoksa warn.
|
|
225
|
-
|
|
226
|
-
sink = @deps.respond_to?(:on_step) ? @deps.on_step : nil
|
|
227
|
-
if sink
|
|
228
|
-
sink.call(type: :user_output, content: raw_output.to_s, label: raw_output.display_stub,
|
|
229
|
-
step: @deps.step_count, max_steps: @deps.max_steps)
|
|
230
|
-
else
|
|
231
|
-
warn("[show_file] no :user_output sink wired — display content dropped: #{raw_output.display_stub}")
|
|
232
|
-
end
|
|
233
|
-
tool_result = Rubik::Tools::Result.new(tool_name: tool_name, call_id: call_id, status: :ok,
|
|
234
|
-
output: raw_output.display_stub, duration_ms: tool_result.duration_ms)
|
|
235
|
-
end
|
|
115
|
+
tool_result = handle_user_display_output(raw_output, tool_result, tool_name, call_id)
|
|
236
116
|
|
|
237
|
-
|
|
238
|
-
type: :tool_result,
|
|
239
|
-
tool: tool_name,
|
|
240
|
-
output: tool_result.output,
|
|
241
|
-
status: tool_result.status,
|
|
242
|
-
duration_ms: tool_result.duration_ms,
|
|
243
|
-
step: @deps.step_count,
|
|
244
|
-
max_steps: @deps.max_steps,
|
|
245
|
-
call_id: call_id,
|
|
246
|
-
exit_code: tool_result.exit_code,
|
|
247
|
-
metadata: tool_result.metadata,
|
|
248
|
-
line_count: tool_result.output.to_s.lines.size,
|
|
249
|
-
byte_size: tool_result.output.to_s.bytesize,
|
|
250
|
-
tokens: @deps.total_tokens
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
Rubik::Orchestrator::ToolHistory.write(@deps.history, tool_call_id: call_id, tool_name: tool_name,
|
|
254
|
-
status: tool_result.status, duration_ms: tool_result.duration_ms,
|
|
255
|
-
content: tool_result.to_xml, exit_code: tool_result.exit_code,
|
|
256
|
-
metadata: tool_result.metadata)
|
|
257
|
-
@deps.activity.record(tool_name, tool_args, tool_result.status) if @deps.respond_to?(:activity)
|
|
117
|
+
record_tool_execution_results(tool_name, tool_args, call_id, tool_result)
|
|
258
118
|
@deps.reset_invalid_tools!
|
|
259
119
|
tool_result
|
|
260
120
|
end
|
|
@@ -280,12 +140,8 @@ module Rubik
|
|
|
280
140
|
diagnostic_msg = Rubik::Tools::DiagnosticReporter.render_unknown_tool(tool_name, candidate_tools: candidate_tools, all_tools: all_tool_names)
|
|
281
141
|
|
|
282
142
|
err_result = Rubik::Tools::Result.new(
|
|
283
|
-
tool_name: tool_name,
|
|
284
|
-
|
|
285
|
-
status: :error,
|
|
286
|
-
error: "Tool '#{tool_name}' does not exist.",
|
|
287
|
-
output: diagnostic_msg,
|
|
288
|
-
duration_ms: 0.0
|
|
143
|
+
tool_name: tool_name, call_id: call_id, status: :error,
|
|
144
|
+
error: "Tool '#{tool_name}' does not exist.", output: diagnostic_msg, duration_ms: 0.0
|
|
289
145
|
)
|
|
290
146
|
Rubik::Orchestrator::ToolHistory.write(@deps.history, tool_call_id: call_id, tool_name: tool_name,
|
|
291
147
|
status: :error, duration_ms: 0.0, content: err_result.to_xml)
|
|
@@ -294,6 +150,134 @@ module Rubik
|
|
|
294
150
|
|
|
295
151
|
private
|
|
296
152
|
|
|
153
|
+
def invoke_tool_and_normalize(tool_instance, tool_name, tool_args, call_id, blocked_by_circuit:)
|
|
154
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
155
|
+
raw_output = nil
|
|
156
|
+
|
|
157
|
+
tool_result = begin
|
|
158
|
+
if blocked_by_circuit
|
|
159
|
+
raise ToolCircuitBreaker::CircuitOpenError,
|
|
160
|
+
"circuit open for '#{tool_instance.name}' — repeated failures; waiting out the cooldown before retrying"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
raw_output = tool_instance.execute(tool_args)
|
|
164
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
165
|
+
dur = ((t1 - t0) * 1000).round(1)
|
|
166
|
+
|
|
167
|
+
normalize_raw_output(raw_output, tool_name, call_id, dur)
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
170
|
+
dur = ((t1 - t0) * 1000).round(1)
|
|
171
|
+
err_msg = "Error executing tool '#{tool_name}': #{e.class} - #{e.message}"
|
|
172
|
+
Rubik::Tools::Result.new(tool_name: tool_name, call_id: call_id, status: :error, error: e.message, output: err_msg, duration_ms: dur)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
[raw_output, tool_result]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def normalize_raw_output(raw_output, tool_name, call_id, dur)
|
|
179
|
+
max_chars = @deps.shell&.config&.fetch("tools.max_output_chars", 12_000) || 12_000
|
|
180
|
+
max_lines = @deps.shell&.config&.fetch("tools.max_output_lines", 300) || 300
|
|
181
|
+
trunc_strat = @deps.shell&.config&.fetch("tools.truncation_strategy", "head_tail") || "head_tail"
|
|
182
|
+
|
|
183
|
+
if raw_output.is_a?(Rubik::Tools::Result)
|
|
184
|
+
trunc_res = Rubik::Tools::Truncator.truncate(raw_output.output, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
|
|
185
|
+
effective_dur = raw_output.duration_ms.positive? ? raw_output.duration_ms : dur
|
|
186
|
+
first_line = raw_output.output.lines.first.to_s.strip
|
|
187
|
+
first_line = nil if first_line.empty?
|
|
188
|
+
Rubik::Tools::Result.new(
|
|
189
|
+
tool_name: raw_output.tool_name.to_s.empty? ? tool_name : raw_output.tool_name,
|
|
190
|
+
call_id: raw_output.call_id.to_s.empty? ? call_id : raw_output.call_id,
|
|
191
|
+
status: raw_output.status, error: raw_output.error || (raw_output.error? ? first_line : nil),
|
|
192
|
+
# Structured fields ride along when the tool provides them
|
|
193
|
+
# (issue #127): the same Result contract for every carrier.
|
|
194
|
+
exit_code: raw_output.exit_code, output: trunc_res[:text], duration_ms: effective_dur,
|
|
195
|
+
truncated: raw_output.truncated || trunc_res[:truncated], metadata: raw_output.metadata
|
|
196
|
+
)
|
|
197
|
+
elsif raw_output.respond_to?(:status) && raw_output.respond_to?(:output)
|
|
198
|
+
out_str = raw_output.output.to_s
|
|
199
|
+
trunc_res = Rubik::Tools::Truncator.truncate(out_str, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
|
|
200
|
+
st = raw_output.status.to_sym
|
|
201
|
+
first_line = out_str.lines.first.to_s.strip
|
|
202
|
+
first_line = nil if first_line.empty?
|
|
203
|
+
err = (raw_output.respond_to?(:error) && raw_output.error) || (st == :error ? first_line : nil)
|
|
204
|
+
Rubik::Tools::Result.new(
|
|
205
|
+
tool_name: tool_name, call_id: call_id, status: st, error: err, output: trunc_res[:text],
|
|
206
|
+
duration_ms: dur, truncated: trunc_res[:truncated],
|
|
207
|
+
exit_code: raw_output.respond_to?(:exit_code) ? raw_output.exit_code : nil,
|
|
208
|
+
metadata: raw_output.respond_to?(:metadata) ? raw_output.metadata : {}
|
|
209
|
+
)
|
|
210
|
+
else
|
|
211
|
+
out_str = raw_output.to_s
|
|
212
|
+
trunc_res = Rubik::Tools::Truncator.truncate(out_str, max_chars: max_chars, max_lines: max_lines, strategy: trunc_strat)
|
|
213
|
+
|
|
214
|
+
is_err = if raw_output.respond_to?(:error?)
|
|
215
|
+
raw_output.error?
|
|
216
|
+
elsif raw_output.respond_to?(:failed?)
|
|
217
|
+
raw_output.failed?
|
|
218
|
+
elsif raw_output.is_a?(String)
|
|
219
|
+
ERROR_PREFIXES.any? { |pfx| raw_output.start_with?(pfx) }
|
|
220
|
+
else
|
|
221
|
+
false
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
first_line = out_str.lines.first.to_s.strip
|
|
225
|
+
first_line = nil if first_line.empty?
|
|
226
|
+
|
|
227
|
+
Rubik::Tools::Result.new(
|
|
228
|
+
tool_name: tool_name, call_id: call_id, status: is_err ? :error : :ok,
|
|
229
|
+
error: is_err ? first_line : nil, output: trunc_res[:text], duration_ms: dur, truncated: trunc_res[:truncated]
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def record_circuit_breaker_result(breaker, tool_name, status)
|
|
235
|
+
if status == :ok
|
|
236
|
+
breaker.record_success(tool_name)
|
|
237
|
+
else
|
|
238
|
+
breaker.record_failure(tool_name)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def perform_post_tool_auto_apply(auto_apply_mode)
|
|
243
|
+
return unless AutoApply.apply?(@deps.config, trigger: :post_tool, mode: auto_apply_mode) && @deps.buffer_manager&.dirty?
|
|
244
|
+
|
|
245
|
+
applied = AutoApply.apply_and_sync(@deps.buffer_manager, @deps.shell)
|
|
246
|
+
@deps.on_step&.call(type: :auto_apply, reason: "post_tool", files: applied, step: @deps.step_count, max_steps: @deps.max_steps)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def handle_user_display_output(raw_output, tool_result, tool_name, call_id)
|
|
250
|
+
return tool_result unless raw_output.respond_to?(:user_display?) && raw_output.user_display? && tool_result.status == :ok
|
|
251
|
+
|
|
252
|
+
sink = @deps.respond_to?(:on_step) ? @deps.on_step : nil
|
|
253
|
+
if sink
|
|
254
|
+
sink.call(type: :user_output, content: raw_output.to_s, label: raw_output.display_stub,
|
|
255
|
+
step: @deps.step_count, max_steps: @deps.max_steps)
|
|
256
|
+
else
|
|
257
|
+
warn("[show_file] no :user_output sink wired — display content dropped: #{raw_output.display_stub}")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
Rubik::Tools::Result.new(
|
|
261
|
+
tool_name: tool_name, call_id: call_id, status: :ok, output: raw_output.display_stub, duration_ms: tool_result.duration_ms
|
|
262
|
+
)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def record_tool_execution_results(tool_name, tool_args, call_id, tool_result)
|
|
266
|
+
@deps.on_step&.call(
|
|
267
|
+
type: :tool_result, tool: tool_name, output: tool_result.output, status: tool_result.status,
|
|
268
|
+
duration_ms: tool_result.duration_ms, step: @deps.step_count, max_steps: @deps.max_steps,
|
|
269
|
+
call_id: call_id, exit_code: tool_result.exit_code, metadata: tool_result.metadata,
|
|
270
|
+
line_count: tool_result.output.to_s.lines.size, byte_size: tool_result.output.to_s.bytesize, tokens: @deps.total_tokens
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
Rubik::Orchestrator::ToolHistory.write(
|
|
274
|
+
@deps.history, tool_call_id: call_id, tool_name: tool_name, status: tool_result.status,
|
|
275
|
+
duration_ms: tool_result.duration_ms, content: tool_result.to_xml, exit_code: tool_result.exit_code, metadata: tool_result.metadata
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
@deps.activity.record(tool_name, tool_args, tool_result.status) if @deps.respond_to?(:activity)
|
|
279
|
+
end
|
|
280
|
+
|
|
297
281
|
# G19: one breaker per executor, built from the first enabled config
|
|
298
282
|
# sighting. Threshold/cooldown changes via live !config set are picked
|
|
299
283
|
# up on the next executor instantiation — acceptable for an opt-in
|
|
@@ -20,10 +20,7 @@ module Rubik
|
|
|
20
20
|
def spawn(role:, prompt:, type_name: "research", system_prompt: nil,
|
|
21
21
|
parent_id: ConversationId::ROOT_ID, workspace: "inherit", model: "inherit",
|
|
22
22
|
max_steps: nil, provider: nil, depends_on: nil, reserved_id: nil, owner: nil)
|
|
23
|
-
role_str = role
|
|
24
|
-
prompt_str = prompt.to_s.strip
|
|
25
|
-
raise ArgumentError, "Subagent role cannot be empty" if role_str.empty?
|
|
26
|
-
raise ArgumentError, "Subagent prompt cannot be empty" if prompt_str.empty?
|
|
23
|
+
role_str, prompt_str = validate_spawn_args!(role, prompt)
|
|
27
24
|
|
|
28
25
|
# Issue #125: the owner's task was cancelled — refuse before any
|
|
29
26
|
# reservation, slot or context work happens.
|
|
@@ -44,7 +41,6 @@ module Rubik
|
|
|
44
41
|
|
|
45
42
|
profile_snapshot = resolve_profile_snapshot(type_name: type_name, role: role_str,
|
|
46
43
|
system_prompt: system_prompt)
|
|
47
|
-
effective_sys_prompt = profile_snapshot.effective_system_prompt
|
|
48
44
|
# A profile update must not redirect rollback to a newly provided
|
|
49
45
|
# init supervisor after this spawn registered its unit.
|
|
50
46
|
activation_init = init_for_activation
|
|
@@ -52,40 +48,23 @@ module Rubik
|
|
|
52
48
|
# unchanged built-in prompt can be reconstructed from its type.
|
|
53
49
|
prompt_reconstructible = system_prompt.to_s.strip.empty? && !profile_snapshot.custom_prompt
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
# next sequence number here and return it if activation rolls back.
|
|
57
|
-
generated_id = reserved_id.nil?
|
|
58
|
-
subagent_id = generated_id ? "subagent-#{@counter + 1}" : reserved_id.to_s
|
|
59
|
-
id_in_use = @instances.key?(subagent_id) || (@finished || {}).key?(subagent_id)
|
|
60
|
-
raise ArgumentError, "Subagent id '#{subagent_id}' is already in use." if id_in_use
|
|
61
|
-
|
|
62
|
-
@counter += 1 if generated_id
|
|
51
|
+
subagent_id, generated_id = reserve_subagent_id!(reserved_id)
|
|
63
52
|
|
|
64
53
|
child_ctx = nil
|
|
65
54
|
inst = nil
|
|
66
55
|
begin
|
|
67
56
|
child_ctx = extend_root_context!(:spawn)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
inst = Instance.new(
|
|
74
|
-
id: subagent_id, parent_id: parent_id, role: role_str, type_name: profile_snapshot.name,
|
|
75
|
-
prompt: prompt_str, system_prompt: effective_sys_prompt, workspace: workspace,
|
|
76
|
-
model: effective_model, context: child_ctx, tool_registry: scoped_registry,
|
|
77
|
-
provider: prov, buffer_manager: effective_buffer, max_steps: (max_steps || @default_max_steps).to_i,
|
|
78
|
-
provider_gate: @provider_gate, root_dir: @root_dir, config: @config,
|
|
79
|
-
context_limit: (@config.respond_to?(:fetch) ? @config.fetch("context.max_tokens", 32_768) : 32_768),
|
|
80
|
-
event_bus: @event_bus, transcript_storage: absorb_storage, owner_task: owner,
|
|
81
|
-
prompt_reconstructible: prompt_reconstructible, profile_snapshot: profile_snapshot
|
|
57
|
+
inst = build_subagent_instance(
|
|
58
|
+
subagent_id: subagent_id, parent_id: parent_id, role_str: role_str, prompt_str: prompt_str,
|
|
59
|
+
profile_snapshot: profile_snapshot, workspace: workspace, model: model, max_steps: max_steps,
|
|
60
|
+
provider: provider, owner: owner, prompt_reconstructible: prompt_reconstructible,
|
|
61
|
+
child_ctx: child_ctx
|
|
82
62
|
)
|
|
83
|
-
|
|
84
63
|
@instances[subagent_id] = inst
|
|
85
|
-
activate_instance!(inst, child_ctx: child_ctx, owner_token: owner,
|
|
86
|
-
init_supervisor: activation_init, defer_start: true)
|
|
87
|
-
raise "Subagent worker start gate could not be released." unless inst.release_start!
|
|
88
64
|
|
|
65
|
+
activate_and_release_subagent!(
|
|
66
|
+
inst: inst, child_ctx: child_ctx, owner: owner, activation_init: activation_init
|
|
67
|
+
)
|
|
89
68
|
inst
|
|
90
69
|
rescue StandardError
|
|
91
70
|
rollback_spawn!(subagent_id: subagent_id, inst: inst, child_ctx: child_ctx,
|
|
@@ -93,6 +72,7 @@ module Rubik
|
|
|
93
72
|
raise
|
|
94
73
|
end
|
|
95
74
|
end
|
|
75
|
+
|
|
96
76
|
# SS bırakma kancası: yeni başlayan instance, onu bekleyen kuyruk
|
|
97
77
|
# spesifikasyonlarını serbest bırakır (kilit DIŞINDA — emit kuralı).
|
|
98
78
|
begin
|
|
@@ -105,6 +85,55 @@ module Rubik
|
|
|
105
85
|
|
|
106
86
|
private
|
|
107
87
|
|
|
88
|
+
def validate_spawn_args!(role, prompt)
|
|
89
|
+
role_str = role.to_s.strip
|
|
90
|
+
prompt_str = prompt.to_s.strip
|
|
91
|
+
raise ArgumentError, "Subagent role cannot be empty" if role_str.empty?
|
|
92
|
+
raise ArgumentError, "Subagent prompt cannot be empty" if prompt_str.empty?
|
|
93
|
+
|
|
94
|
+
[role_str, prompt_str]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def reserve_subagent_id!(reserved_id)
|
|
98
|
+
# Queued specs already reserved this id; fresh spawns reserve the
|
|
99
|
+
# next sequence number here and return it if activation rolls back.
|
|
100
|
+
generated_id = reserved_id.nil?
|
|
101
|
+
subagent_id = generated_id ? "subagent-#{@counter + 1}" : reserved_id.to_s
|
|
102
|
+
id_in_use = @instances.key?(subagent_id) || (@finished || {}).key?(subagent_id)
|
|
103
|
+
raise ArgumentError, "Subagent id '#{subagent_id}' is already in use." if id_in_use
|
|
104
|
+
|
|
105
|
+
@counter += 1 if generated_id
|
|
106
|
+
[subagent_id, generated_id]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_subagent_instance(subagent_id:, parent_id:, role_str:, prompt_str:,
|
|
110
|
+
profile_snapshot:, workspace:, model:, max_steps:,
|
|
111
|
+
provider:, owner:, prompt_reconstructible:, child_ctx:)
|
|
112
|
+
effective_buffer = branch_or_inherit_buffer(workspace)
|
|
113
|
+
scoped_registry = build_scoped_registry(profile_snapshot, child_ctx, effective_buffer)
|
|
114
|
+
prov, effective_model = resolve_provider_and_model(model)
|
|
115
|
+
prov = provider if provider
|
|
116
|
+
|
|
117
|
+
context_limit = @config.respond_to?(:fetch) ? @config.fetch("context.max_tokens", 32_768) : 32_768
|
|
118
|
+
|
|
119
|
+
Instance.new(
|
|
120
|
+
id: subagent_id, parent_id: parent_id, role: role_str, type_name: profile_snapshot.name,
|
|
121
|
+
prompt: prompt_str, system_prompt: profile_snapshot.effective_system_prompt, workspace: workspace,
|
|
122
|
+
model: effective_model, context: child_ctx, tool_registry: scoped_registry,
|
|
123
|
+
provider: prov, buffer_manager: effective_buffer, max_steps: (max_steps || @default_max_steps).to_i,
|
|
124
|
+
provider_gate: @provider_gate, root_dir: @root_dir, config: @config,
|
|
125
|
+
context_limit: context_limit, event_bus: @event_bus,
|
|
126
|
+
transcript_storage: absorb_storage, owner_task: owner,
|
|
127
|
+
prompt_reconstructible: prompt_reconstructible, profile_snapshot: profile_snapshot
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def activate_and_release_subagent!(inst:, child_ctx:, owner:, activation_init:)
|
|
132
|
+
activate_instance!(inst, child_ctx: child_ctx, owner_token: owner,
|
|
133
|
+
init_supervisor: activation_init, defer_start: true)
|
|
134
|
+
raise "Subagent worker start gate could not be released." unless inst.release_start!
|
|
135
|
+
end
|
|
136
|
+
|
|
108
137
|
def resolve_profile_snapshot(type_name:, role:, system_prompt:)
|
|
109
138
|
reserved_name = RESERVED_ROLE_PROFILES[role.to_s.downcase.strip]
|
|
110
139
|
if reserved_name
|
|
@@ -22,27 +22,31 @@ module Rubik
|
|
|
22
22
|
((len * 10) + 37) / 38
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# PERF: Fast-path message token estimator respecting ConversationRows abstraction.
|
|
25
26
|
def estimate_message(msg)
|
|
26
27
|
return 0 unless ConversationRows.row?(msg)
|
|
27
28
|
|
|
28
29
|
tokens = 4 # Base message overhead (role delimiter, headers)
|
|
29
|
-
role = ConversationRows.value_of(msg, :role)
|
|
30
|
-
tokens += estimate_text(role) if role
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
if (role = ConversationRows.value_of(msg, :role))
|
|
32
|
+
tokens += estimate_text(role)
|
|
33
|
+
end
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
tool_calls_str = serialize_for_estimation(tool_calls)
|
|
38
|
-
tokens += estimate_text(tool_calls_str)
|
|
35
|
+
if (content = ConversationRows.value_of(msg, :content))
|
|
36
|
+
tokens += estimate_text(content)
|
|
39
37
|
end
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
if (tool_calls = ConversationRows.value_of(msg, :tool_calls)) && !tool_calls.empty?
|
|
40
|
+
tokens += estimate_text(serialize_for_estimation(tool_calls))
|
|
41
|
+
end
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
if (tool_call_id = ConversationRows.value_of(msg, :tool_call_id))
|
|
44
|
+
tokens += estimate_text(tool_call_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if (tool_name = ConversationRows.value_of(msg, :tool_name))
|
|
48
|
+
tokens += estimate_text(tool_name)
|
|
49
|
+
end
|
|
46
50
|
|
|
47
51
|
tokens
|
|
48
52
|
end
|