rubik 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/lib/rubik/config/defaults.rb +3 -6
- data/lib/rubik/config/store.rb +1 -1
- data/lib/rubik/orchestrator/subagent/budget.rb +17 -10
- data/lib/rubik/orchestrator/subagent/instance.rb +14 -2
- data/lib/rubik/orchestrator/subagent/journal.rb +56 -13
- data/lib/rubik/orchestrator/subagent/reuse.rb +1 -1
- data/lib/rubik/orchestrator/subagent/run_cycle.rb +8 -5
- data/lib/rubik/orchestrator/subagent/supervisor.rb +7 -1
- data/lib/rubik/platform/process.rb +1 -1
- data/lib/rubik/platform/sanitize.rb +4 -4
- data/lib/rubik/platform.rb +2 -2
- data/lib/rubik/repl/commands/subagents.rb +12 -11
- data/lib/rubik/repl/interrupt_handler.rb +10 -10
- data/lib/rubik/tools/builtins/subagent.rb +7 -2
- data/lib/rubik/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29ee7f0d84218a3bf3d2fe0699c2c37d7f260f6406231a46338f169410ae5c60
|
|
4
|
+
data.tar.gz: a600c600071a3267b8ab4cdfd0b2f755a2068f2cd8289949ae57e41383355d1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fabc3ee65ba68ea6244727ba265232c1b05ac2bd06eee192a376dbfc6b6da746070bac6f129b750011803c2945f092096b5cd122065dd79bde224fcf9018892f
|
|
7
|
+
data.tar.gz: 30313695e55c4ae537d9d3acb27b1dbc7365d6db817e93b487e7612ce7c2921235deb7e92db6b2232e33b71569b65e1190f9c8c948bef8090fce093412fcc14c
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,35 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
10
10
|
|
|
11
11
|
Nothing yet.
|
|
12
12
|
|
|
13
|
+
## [0.3.3] - 2026-10-21
|
|
14
|
+
|
|
15
|
+
A field-incident patch: subagent runs get a lifetime step ceiling,
|
|
16
|
+
budget grants stop lying about what they applied, and execution
|
|
17
|
+
journals keep their terminal outcomes after truncation.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Subagent runs have a lifetime step ceiling (#219):** a live
|
|
22
|
+
incident showed one subagent at 950 model steps, 1,004 tool calls
|
|
23
|
+
and 10 budget grants — repeated grants could extend a run forever.
|
|
24
|
+
The new `subagents.max_total_steps` ceiling (default 256) bounds
|
|
25
|
+
the initial allocation plus every grant; grants that would exceed
|
|
26
|
+
it are truncated to the remaining room and partial or rejected
|
|
27
|
+
grants are reported as such, so the REPL and `manage_subagents`
|
|
28
|
+
echo the number that actually applied.
|
|
29
|
+
- **Execution journals keep their terminal outcomes (#219):** when a
|
|
30
|
+
journal hits its byte cap, truncation now preserves the terminal
|
|
31
|
+
`run_finished` and `cancelled` records instead of cutting exactly
|
|
32
|
+
the facts a post-mortem needs; the default per-execution journal
|
|
33
|
+
cap rises from 2 MiB to 8 MiB (`subagents.journal.max_bytes`).
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Bare `rescue` clauses are explicit `rescue StandardError`
|
|
38
|
+
(#218):** standardization across platform, process, sanitize,
|
|
39
|
+
config store and the interrupt handler — same semantics, harder to
|
|
40
|
+
misread.
|
|
41
|
+
|
|
13
42
|
## [0.3.2] - 2026-10-21
|
|
14
43
|
|
|
15
44
|
A reliability patch: LSP requests tell timeouts and crashes apart and
|
|
@@ -142,6 +142,7 @@ module Rubik
|
|
|
142
142
|
"max_concurrency" => 10,
|
|
143
143
|
"max_depth" => 2,
|
|
144
144
|
"default_max_steps" => 88,
|
|
145
|
+
"max_total_steps" => 256, # Hard per-run ceiling across the initial allocation and all grants.
|
|
145
146
|
"provider_concurrency" => 2,
|
|
146
147
|
"budget_wait_timeout" => 180,
|
|
147
148
|
"auto_apply_buffer" => true,
|
|
@@ -156,7 +157,7 @@ module Rubik
|
|
|
156
157
|
# Sonuç arşivi (issue #122): tamamlanan koşular oturum-içi saklanır; redeploy öncekini EZMEZ.
|
|
157
158
|
"result_archive" => { "ttl_seconds" => 3600, "max_runs_per_instance" => 8, "page_chars" => 4000 },
|
|
158
159
|
# Append-only execution journal (issue #124): per-execution JSONL, capped per file.
|
|
159
|
-
"journal" => { "max_bytes" =>
|
|
160
|
+
"journal" => { "max_bytes" => 8_388_608 },
|
|
160
161
|
# Maestro kapısı: maestro write_file/replace_file_content/run_command/run_tests'ı ÇALIŞTIRAMAZ:
|
|
161
162
|
"maestro_gate" => true,
|
|
162
163
|
"auto_pause_on_network_error" => true,
|
|
@@ -223,11 +224,7 @@ module Rubik
|
|
|
223
224
|
validate_prompt_grace_value!(current_key, val)
|
|
224
225
|
if schema.is_a?(Hash) && !schema.key?(key.to_s)
|
|
225
226
|
candidate_keys = schema.keys
|
|
226
|
-
candidates =
|
|
227
|
-
Rubik::Tools::FuzzyMatcher.best_matches(key.to_s, candidate_keys)
|
|
228
|
-
else
|
|
229
|
-
[]
|
|
230
|
-
end
|
|
227
|
+
candidates = defined?(Rubik::Tools::FuzzyMatcher) ? Rubik::Tools::FuzzyMatcher.best_matches(key.to_s, candidate_keys) : []
|
|
231
228
|
|
|
232
229
|
suggestion_msg = if candidates && !candidates.empty?
|
|
233
230
|
suggested_full = prefix.empty? ? candidates.first : "#{prefix}.#{candidates.first}"
|
data/lib/rubik/config/store.rb
CHANGED
|
@@ -28,9 +28,11 @@ module Rubik
|
|
|
28
28
|
class Budget
|
|
29
29
|
DEFAULT_MAX_STEPS = 88
|
|
30
30
|
DEFAULT_GRANT_STEPS = 20
|
|
31
|
+
DEFAULT_MAX_TOTAL_STEPS = 256
|
|
31
32
|
|
|
32
|
-
def initialize(max_steps: nil)
|
|
33
|
-
@
|
|
33
|
+
def initialize(max_steps: nil, max_total_steps: DEFAULT_MAX_TOTAL_STEPS)
|
|
34
|
+
@max_total_steps = [(max_total_steps || DEFAULT_MAX_TOTAL_STEPS).to_i, 1].max
|
|
35
|
+
@max_steps = [(max_steps || DEFAULT_MAX_STEPS).to_i, @max_total_steps].min
|
|
34
36
|
@current_step = 0
|
|
35
37
|
@monitor = Monitor.new
|
|
36
38
|
end
|
|
@@ -39,6 +41,10 @@ module Rubik
|
|
|
39
41
|
@monitor.synchronize { @max_steps }
|
|
40
42
|
end
|
|
41
43
|
|
|
44
|
+
def max_total_steps
|
|
45
|
+
@monitor.synchronize { @max_total_steps }
|
|
46
|
+
end
|
|
47
|
+
|
|
42
48
|
def current_step
|
|
43
49
|
@monitor.synchronize { @current_step }
|
|
44
50
|
end
|
|
@@ -62,14 +68,16 @@ module Rubik
|
|
|
62
68
|
@monitor.synchronize { "[Step #{@current_step}/#{@max_steps}]" }
|
|
63
69
|
end
|
|
64
70
|
|
|
65
|
-
# Normalize -> floor ->
|
|
66
|
-
#
|
|
71
|
+
# Normalize -> floor -> cap, atomically. Returns the [extra, total]
|
|
72
|
+
# pair Instance's note lambda needs. Repeated grants can never push a
|
|
73
|
+
# run past its configured lifetime ceiling. Terminal-state
|
|
67
74
|
# refusal, the :running transition, the history note, and the condvar
|
|
68
75
|
# broadcast stay on Instance — lifecycle facts, not budget facts.
|
|
69
76
|
# Called with the Instance monitor held (lock order: instance -> budget).
|
|
70
77
|
def grant!(steps)
|
|
71
78
|
@monitor.synchronize do
|
|
72
|
-
|
|
79
|
+
available = [@max_total_steps - @max_steps, 0].max
|
|
80
|
+
extra = [Budget.normalize_grant(steps), available].min
|
|
73
81
|
@max_steps += extra
|
|
74
82
|
[extra, @max_steps]
|
|
75
83
|
end
|
|
@@ -80,11 +88,10 @@ module Rubik
|
|
|
80
88
|
@monitor.synchronize { { current_step: @current_step, max_steps: @max_steps } }
|
|
81
89
|
end
|
|
82
90
|
|
|
83
|
-
# Pure half
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
# grant! applies. Idempotent.
|
|
91
|
+
# Pure normalization half shared by grant! and the reporting edges
|
|
92
|
+
# (manage_subagents steps arg, REPL !subagents extend arg). A grant
|
|
93
|
+
# may be smaller than this normalized request when the lifetime
|
|
94
|
+
# ceiling has less room remaining. Idempotent.
|
|
88
95
|
def self.normalize_grant(steps)
|
|
89
96
|
extra = (steps || DEFAULT_GRANT_STEPS).to_i
|
|
90
97
|
extra = DEFAULT_GRANT_STEPS if extra <= 0
|
|
@@ -52,6 +52,7 @@ module Rubik
|
|
|
52
52
|
# Budget delegation: step counters live in Subagent::Budget (reader contract preserved).
|
|
53
53
|
def current_step = @budget.current_step
|
|
54
54
|
def max_steps = @budget.max_steps
|
|
55
|
+
def max_total_steps = @budget.max_total_steps
|
|
55
56
|
|
|
56
57
|
def initialize(id:, parent_id:, role:, type_name:, prompt:, context:, tool_registry:,
|
|
57
58
|
system_prompt: nil, workspace: "inherit", model: "inherit",
|
|
@@ -72,7 +73,15 @@ module Rubik
|
|
|
72
73
|
@tool_registry = tool_registry
|
|
73
74
|
@provider = provider
|
|
74
75
|
@buffer_manager = buffer_manager
|
|
75
|
-
|
|
76
|
+
max_total_steps = if config.is_a?(Hash)
|
|
77
|
+
config["subagents.max_total_steps"] || config.dig("subagents", "max_total_steps") ||
|
|
78
|
+
Budget::DEFAULT_MAX_TOTAL_STEPS
|
|
79
|
+
elsif config.respond_to?(:fetch)
|
|
80
|
+
config.fetch("subagents.max_total_steps", Budget::DEFAULT_MAX_TOTAL_STEPS)
|
|
81
|
+
else
|
|
82
|
+
Budget::DEFAULT_MAX_TOTAL_STEPS
|
|
83
|
+
end
|
|
84
|
+
@budget = Budget.new(max_steps: max_steps, max_total_steps: max_total_steps)
|
|
76
85
|
@root_dir = root_dir
|
|
77
86
|
@config = config
|
|
78
87
|
@event_bus = event_bus
|
|
@@ -189,7 +198,8 @@ module Rubik
|
|
|
189
198
|
def to_h
|
|
190
199
|
@monitor.synchronize do
|
|
191
200
|
{ id: @id, parent_id: @parent_id, role: @role, type: @type_name, state: @state.to_s,
|
|
192
|
-
**@budget.to_h,
|
|
201
|
+
**@budget.to_h, max_total_steps: @budget.max_total_steps,
|
|
202
|
+
grants_received: @grants_received, run: @run_seq,
|
|
193
203
|
result_ref: result_ref, owner: @owner_task&.id, detached: @owner_task.nil?, uptime_seconds: uptime.round(2),
|
|
194
204
|
operation: operations.current, timings: operations.timings,
|
|
195
205
|
last_progress_seconds_ago: operations.last_progress_seconds_ago,
|
|
@@ -230,6 +240,8 @@ module Rubik
|
|
|
230
240
|
return false if terminal?
|
|
231
241
|
|
|
232
242
|
extra, total = @budget.grant!(steps)
|
|
243
|
+
return false if extra <= 0
|
|
244
|
+
|
|
233
245
|
transition!(:running)
|
|
234
246
|
@grants_received += 1
|
|
235
247
|
@pause_facts = nil # the wake retires the wait fact with the pause (B3's clearer, now fact-only)
|
|
@@ -22,9 +22,9 @@ module Rubik
|
|
|
22
22
|
#
|
|
23
23
|
# Retention is bounded independently of the model context: at most
|
|
24
24
|
# `subagents.journal.max_bytes` per execution file. Once the cap is
|
|
25
|
-
# reached a
|
|
26
|
-
#
|
|
27
|
-
#
|
|
25
|
+
# reached a `journal_truncated` record marks the boundary; detail events
|
|
26
|
+
# stop, while reserved space preserves the terminal run/cancellation
|
|
27
|
+
# outcome. Tool payloads are previewed
|
|
28
28
|
# (PREVIEW_CHARS) with explicit `output_chars` / `output_truncated`
|
|
29
29
|
# metadata plus an `artifact_ref` to the conversation snapshot, so replay
|
|
30
30
|
# can distinguish originals from summaries from clipped output.
|
|
@@ -37,10 +37,12 @@ module Rubik
|
|
|
37
37
|
# in Instance#to_h), never raised into the run.
|
|
38
38
|
class Journal
|
|
39
39
|
FILENAME = "journal.jsonl"
|
|
40
|
-
DEFAULT_MAX_BYTES =
|
|
40
|
+
DEFAULT_MAX_BYTES = 8_388_608
|
|
41
41
|
# Headroom kept free for the final truncation marker: accepted records
|
|
42
42
|
# never consume the last bytes, so the marker always fits inside the cap.
|
|
43
43
|
MARKER_RESERVE = 512
|
|
44
|
+
TERMINAL_RESERVE = 16_384
|
|
45
|
+
TERMINAL_EVENTS = ["run_finished", "cancelled"].freeze
|
|
44
46
|
MIN_MAX_BYTES = 1024
|
|
45
47
|
PREVIEW_CHARS = 4000
|
|
46
48
|
ARGS_CHARS = 2000
|
|
@@ -54,9 +56,12 @@ module Rubik
|
|
|
54
56
|
@path = path.to_s
|
|
55
57
|
@monitor = Monitor.new
|
|
56
58
|
@max_bytes = [config_get(config, "subagents.journal.max_bytes", DEFAULT_MAX_BYTES).to_i, MIN_MAX_BYTES].max
|
|
57
|
-
|
|
59
|
+
existing_records = self.class.read(@path).records
|
|
60
|
+
@seq = existing_records.map { |record| record["seq"].to_i }.max || 0
|
|
58
61
|
@bytes = File.exist?(@path) ? File.size(@path) : 0
|
|
59
|
-
@
|
|
62
|
+
@marker_reserve = [MARKER_RESERVE, @max_bytes / 4].min
|
|
63
|
+
@terminal_reserve = [TERMINAL_RESERVE, @max_bytes / 3].min
|
|
64
|
+
@closed = existing_records.any? { |record| record["event"] == "journal_truncated" }
|
|
60
65
|
@last_error = nil
|
|
61
66
|
@run_metrics = {}
|
|
62
67
|
end
|
|
@@ -65,16 +70,24 @@ module Rubik
|
|
|
65
70
|
# record envelope; the payload is masked HERE (single redaction gate).
|
|
66
71
|
def append(event, run: 1, data: {}, call_id: nil)
|
|
67
72
|
@monitor.synchronize do
|
|
68
|
-
|
|
73
|
+
event_name = event.to_s
|
|
74
|
+
terminal = TERMINAL_EVENTS.include?(event_name)
|
|
75
|
+
return nil if @closed && !terminal
|
|
69
76
|
|
|
70
77
|
@last_run = run.to_i
|
|
71
78
|
row = { "seq" => @seq + 1, "run" => @last_run, "ts" => Time.now.utc.iso8601(3),
|
|
72
|
-
"event" =>
|
|
79
|
+
"event" => event_name, "data" => Rubik::Security::Redactor.mask_value(data) }
|
|
73
80
|
row["call_id"] = call_id.to_s if call_id
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
|
|
82
|
+
return append_terminal!(row) if @closed
|
|
83
|
+
|
|
84
|
+
line = serialize(row)
|
|
85
|
+
if @bytes + line.bytesize + @marker_reserve + @terminal_reserve > @max_bytes
|
|
76
86
|
mark_truncated
|
|
77
|
-
return nil
|
|
87
|
+
return nil unless terminal
|
|
88
|
+
|
|
89
|
+
row["seq"] = @seq + 1
|
|
90
|
+
return append_terminal!(row)
|
|
78
91
|
end
|
|
79
92
|
return nil unless write!(line)
|
|
80
93
|
|
|
@@ -199,6 +212,35 @@ module Rubik
|
|
|
199
212
|
|
|
200
213
|
private
|
|
201
214
|
|
|
215
|
+
def serialize(row) = "#{Rubik::Platform.safe_json_generate(row)}\n"
|
|
216
|
+
|
|
217
|
+
def append_terminal!(row)
|
|
218
|
+
line = serialize(row)
|
|
219
|
+
if @bytes + line.bytesize > @max_bytes
|
|
220
|
+
row["data"] = compact_terminal_data(row["data"])
|
|
221
|
+
line = serialize(row)
|
|
222
|
+
end
|
|
223
|
+
return nil if @bytes + line.bytesize > @max_bytes || !write!(line)
|
|
224
|
+
|
|
225
|
+
@seq += 1
|
|
226
|
+
@bytes += line.bytesize
|
|
227
|
+
row
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def compact_terminal_data(data)
|
|
231
|
+
return {} unless data.is_a?(Hash)
|
|
232
|
+
|
|
233
|
+
summary = data.slice("state", "outcome", "duration_ms", "step_count")
|
|
234
|
+
["state", "outcome"].each do |key|
|
|
235
|
+
summary[key] = summary[key].to_s.slice(0, 64) if summary.key?(key)
|
|
236
|
+
end
|
|
237
|
+
summary["reason"] = data["reason"].to_s.slice(0, 256) if data["reason"]
|
|
238
|
+
result_ref = data["result_ref"]
|
|
239
|
+
summary["result_ref"] = result_ref.to_s.slice(0, 128) if result_ref
|
|
240
|
+
summary["terminal_summary_truncated"] = true
|
|
241
|
+
summary
|
|
242
|
+
end
|
|
243
|
+
|
|
202
244
|
def empty_run_metrics
|
|
203
245
|
{ tool_usage: {}, model_responses: 0, input_tokens: 0, output_tokens: 0,
|
|
204
246
|
input_token_responses: 0, output_token_responses: 0 }
|
|
@@ -240,8 +282,9 @@ module Rubik
|
|
|
240
282
|
@closed = true
|
|
241
283
|
marker = { "seq" => @seq + 1, "run" => (@last_run || 1).to_i, "ts" => Time.now.utc.iso8601(3),
|
|
242
284
|
"event" => "journal_truncated",
|
|
243
|
-
"data" => { "max_bytes" => @max_bytes,
|
|
244
|
-
|
|
285
|
+
"data" => { "max_bytes" => @max_bytes,
|
|
286
|
+
"reason" => "retention cap reached; detail events stop, terminal outcomes use reserved space" } }
|
|
287
|
+
line = serialize(marker)
|
|
245
288
|
return nil if @bytes + line.bytesize > @max_bytes || !write!(line)
|
|
246
289
|
|
|
247
290
|
@seq += 1
|
|
@@ -59,7 +59,7 @@ module Rubik
|
|
|
59
59
|
drain_inbox_into_history
|
|
60
60
|
@history << { role: "user", content: "[New task from maestro]: #{prompt}" }
|
|
61
61
|
@prompt = prompt.to_s
|
|
62
|
-
@budget = Budget.new(max_steps: max_steps.to_i)
|
|
62
|
+
@budget = Budget.new(max_steps: max_steps.to_i, max_total_steps: @budget.max_total_steps)
|
|
63
63
|
# @result reset is the ONE sanctioned clearer (terminal-only
|
|
64
64
|
# contract): a new lineage begins. @pause_facts rides along —
|
|
65
65
|
# the old run's wait fact must not narrate the new one.
|
|
@@ -69,11 +69,14 @@ module Rubik
|
|
|
69
69
|
def build_effective_system_prompt
|
|
70
70
|
base = "You are a specialized subagent operating under autonomous supervision.\n\n" \
|
|
71
71
|
"Subagent ID: #{@id} | Role: #{@role} | Type: #{@type_name}\n" \
|
|
72
|
-
"Allocated Step Budget: #{@budget.max_steps} steps
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
72
|
+
"Allocated Step Budget: #{@budget.max_steps} steps; lifetime ceiling: " \
|
|
73
|
+
"#{@budget.max_total_steps} steps across all grants.\n\n" \
|
|
74
|
+
"A [Step X/Y] status line precedes each of your steps (X = current step, Y = total budget). " \
|
|
75
|
+
"Pace yourself: when remaining steps (Y - X) run low, conclude the task or prepare a progress " \
|
|
76
|
+
"summary for the maestro supervisor — it may extend your budget up to the lifetime ceiling or " \
|
|
77
|
+
"steer you. If a requirement is genuinely ambiguous, ask the maestro directly with the " \
|
|
78
|
+
"ask_question tool — it blocks until the maestro answers or times out; ask only when the answer " \
|
|
79
|
+
"truly changes your work.\n\n" \
|
|
77
80
|
"Perform your assigned task with high precision and provide a structured synthesis report."
|
|
78
81
|
@system_prompt && !@system_prompt.strip.empty? ? "#{base}\n\nCustom Instructions:\n#{@system_prompt}" : base
|
|
79
82
|
end
|
|
@@ -38,6 +38,7 @@ module Rubik
|
|
|
38
38
|
class Supervisor
|
|
39
39
|
attr_accessor :tool_registry, :router, :init
|
|
40
40
|
attr_reader :root_context, :type_registry, :instances, :max_depth, :max_concurrency, :default_max_steps,
|
|
41
|
+
:max_total_steps,
|
|
41
42
|
:provider_gate, :result_archive
|
|
42
43
|
|
|
43
44
|
include RedeploySupervisor
|
|
@@ -70,7 +71,12 @@ module Rubik
|
|
|
70
71
|
@counter = 0
|
|
71
72
|
@max_depth = (config&.fetch("subagents.max_depth", 2) || 2).to_i
|
|
72
73
|
@max_concurrency = (config&.fetch("subagents.max_concurrency", 10) || 10).to_i
|
|
73
|
-
|
|
74
|
+
configured_max_total_steps = config&.fetch("subagents.max_total_steps", Budget::DEFAULT_MAX_TOTAL_STEPS) ||
|
|
75
|
+
Budget::DEFAULT_MAX_TOTAL_STEPS
|
|
76
|
+
@max_total_steps = [configured_max_total_steps.to_i, 1].max
|
|
77
|
+
configured_default_steps = config&.fetch("subagents.default_max_steps", Budget::DEFAULT_MAX_STEPS) ||
|
|
78
|
+
Budget::DEFAULT_MAX_STEPS
|
|
79
|
+
@default_max_steps = [configured_default_steps.to_i, @max_total_steps].min
|
|
74
80
|
@provider_concurrency = (config&.fetch("subagents.provider_concurrency", 2) || 2).to_i
|
|
75
81
|
@provider_gate = ConcurrencyGate.new(@provider_concurrency)
|
|
76
82
|
# Retained completed-run results (issue #122): session-scoped,
|
|
@@ -92,7 +92,7 @@ module Rubik
|
|
|
92
92
|
# Plan 55 D-1 ile tutarlı dizi-form çağrı: cmd.exe ara katmanı ve kabuk
|
|
93
93
|
# yönlendirmesi ("> NUL 2>&1") yerine out:/err: seçenekleri kullanılır.
|
|
94
94
|
system("taskkill", "/F", "/T", "/PID", pid.to_s, out: File::NULL, err: File::NULL)
|
|
95
|
-
rescue
|
|
95
|
+
rescue StandardError
|
|
96
96
|
nil
|
|
97
97
|
end
|
|
98
98
|
else
|
|
@@ -39,7 +39,7 @@ module Rubik
|
|
|
39
39
|
debug_warn("clean_utf8 scrub fallback: #{e.class}: #{e.message}")
|
|
40
40
|
begin
|
|
41
41
|
str.to_s.scrub("")
|
|
42
|
-
rescue
|
|
42
|
+
rescue StandardError
|
|
43
43
|
""
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -108,7 +108,7 @@ module Rubik
|
|
|
108
108
|
fallback = sanitize_for_json_fallback(obj)
|
|
109
109
|
begin
|
|
110
110
|
JSON.generate(fallback, *)
|
|
111
|
-
rescue
|
|
111
|
+
rescue StandardError
|
|
112
112
|
"{}"
|
|
113
113
|
end
|
|
114
114
|
end
|
|
@@ -122,7 +122,7 @@ module Rubik
|
|
|
122
122
|
fallback = sanitize_for_json_fallback(obj)
|
|
123
123
|
begin
|
|
124
124
|
JSON.dump(fallback, *)
|
|
125
|
-
rescue
|
|
125
|
+
rescue StandardError
|
|
126
126
|
"{}"
|
|
127
127
|
end
|
|
128
128
|
end
|
|
@@ -148,7 +148,7 @@ module Rubik
|
|
|
148
148
|
debug_warn("sanitize_for_json_fallback rescue: #{e.class}: #{e.message}")
|
|
149
149
|
begin
|
|
150
150
|
obj.to_s.scrub("")
|
|
151
|
-
rescue
|
|
151
|
+
rescue StandardError
|
|
152
152
|
""
|
|
153
153
|
end
|
|
154
154
|
end
|
data/lib/rubik/platform.rb
CHANGED
|
@@ -87,7 +87,7 @@ module Rubik
|
|
|
87
87
|
|
|
88
88
|
def home_dir
|
|
89
89
|
Dir.home
|
|
90
|
-
rescue
|
|
90
|
+
rescue StandardError
|
|
91
91
|
(ENV["USERPROFILE"] || (ENV.fetch("HOMEDRIVE",
|
|
92
92
|
nil) && ENV.fetch("HOMEPATH",
|
|
93
93
|
nil) && "#{ENV.fetch('HOMEDRIVE',
|
|
@@ -96,7 +96,7 @@ module Rubik
|
|
|
96
96
|
|
|
97
97
|
def tmp_dir
|
|
98
98
|
Dir.tmpdir
|
|
99
|
-
rescue
|
|
99
|
+
rescue StandardError
|
|
100
100
|
(ENV["TEMP"] || ENV["TMP"] || "/tmp")
|
|
101
101
|
end
|
|
102
102
|
end
|
|
@@ -113,10 +113,7 @@ module Rubik
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def self.handle_kill(ctx, sup, id)
|
|
116
|
-
if id.to_s.strip.empty?
|
|
117
|
-
ctx.out.puts "Usage: !subagents kill <subagent_id>"
|
|
118
|
-
return
|
|
119
|
-
end
|
|
116
|
+
return ctx.out.puts "Usage: !subagents kill <subagent_id>" if id.to_s.strip.empty?
|
|
120
117
|
|
|
121
118
|
if sup.kill(id)
|
|
122
119
|
ctx.out.puts "Subagent '#{id}' and child tree killed."
|
|
@@ -126,17 +123,21 @@ module Rubik
|
|
|
126
123
|
end
|
|
127
124
|
|
|
128
125
|
def self.handle_extend(ctx, sup, id, steps_arg)
|
|
129
|
-
if id.to_s.strip.empty?
|
|
130
|
-
ctx.out.puts "Usage: !subagents extend <subagent_id> [additional_steps]"
|
|
131
|
-
return
|
|
132
|
-
end
|
|
126
|
+
return ctx.out.puts "Usage: !subagents extend <subagent_id> [additional_steps]" if id.to_s.strip.empty?
|
|
133
127
|
|
|
134
128
|
steps = Rubik::Orchestrator::Subagent::Budget.normalize_grant(steps_arg)
|
|
129
|
+
inst = sup.get(id)
|
|
135
130
|
if sup.extend_budget(id, steps)
|
|
136
|
-
|
|
137
|
-
ctx.out.puts "Extended budget for #{id} by #{steps} steps (total: #{inst.max_steps}
|
|
131
|
+
ceiling = inst.respond_to?(:max_total_steps) ? "/#{inst.max_total_steps}" : ""
|
|
132
|
+
ctx.out.puts "Extended budget for #{id} by #{steps} steps (total: #{inst.max_steps}#{ceiling}, " \
|
|
133
|
+
"state: #{inst.state.to_s.upcase})."
|
|
138
134
|
else
|
|
139
|
-
|
|
135
|
+
msg = if inst.respond_to?(:max_total_steps) && inst.max_steps >= inst.max_total_steps
|
|
136
|
+
"Could not extend budget for #{id}: lifetime step limit of #{inst.max_total_steps} reached."
|
|
137
|
+
else
|
|
138
|
+
"Could not extend budget for #{id} (not found or inactive)."
|
|
139
|
+
end
|
|
140
|
+
ctx.out.puts msg
|
|
140
141
|
end
|
|
141
142
|
end
|
|
142
143
|
|
|
@@ -89,14 +89,14 @@ module Rubik
|
|
|
89
89
|
if get_std_handle
|
|
90
90
|
new_in = begin
|
|
91
91
|
get_std_handle.call(Reline::Windows::STD_INPUT_HANDLE)
|
|
92
|
-
rescue
|
|
92
|
+
rescue StandardError
|
|
93
93
|
nil
|
|
94
94
|
end
|
|
95
95
|
io.instance_variable_set(:@hConsoleInputHandle, new_in) if new_in && new_in != 0
|
|
96
96
|
|
|
97
97
|
new_out = begin
|
|
98
98
|
get_std_handle.call(Reline::Windows::STD_OUTPUT_HANDLE)
|
|
99
|
-
rescue
|
|
99
|
+
rescue StandardError
|
|
100
100
|
nil
|
|
101
101
|
end
|
|
102
102
|
io.instance_variable_set(:@hConsoleHandle, new_out) if new_out && new_out != 0
|
|
@@ -105,14 +105,14 @@ module Rubik
|
|
|
105
105
|
flush_func = io.instance_variable_get(:@FlushConsoleInputBuffer) ||
|
|
106
106
|
begin
|
|
107
107
|
Reline::Windows::Win32API.new("kernel32", "FlushConsoleInputBuffer", ["L"], "I")
|
|
108
|
-
rescue
|
|
108
|
+
rescue StandardError
|
|
109
109
|
nil
|
|
110
110
|
end
|
|
111
111
|
if flush_func
|
|
112
112
|
io.instance_variable_set(:@FlushConsoleInputBuffer, flush_func)
|
|
113
113
|
begin
|
|
114
114
|
flush_func.call(new_in)
|
|
115
|
-
rescue
|
|
115
|
+
rescue StandardError
|
|
116
116
|
nil
|
|
117
117
|
end
|
|
118
118
|
end
|
|
@@ -143,7 +143,7 @@ if defined?(Reline::Windows)
|
|
|
143
143
|
if @hConsoleInputHandle.nil? || @hConsoleInputHandle.zero? || @hConsoleInputHandle == -1
|
|
144
144
|
@hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
145
145
|
@GetStdHandle.call(STD_INPUT_HANDLE)
|
|
146
|
-
rescue
|
|
146
|
+
rescue StandardError
|
|
147
147
|
nil
|
|
148
148
|
end
|
|
149
149
|
end
|
|
@@ -156,18 +156,18 @@ if defined?(Reline::Windows)
|
|
|
156
156
|
if [0xFFFFFFFF, 4_294_967_295, -1].include?(wait_res)
|
|
157
157
|
@hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
158
158
|
@GetStdHandle.call(STD_INPUT_HANDLE)
|
|
159
|
-
rescue
|
|
159
|
+
rescue StandardError
|
|
160
160
|
nil
|
|
161
161
|
end
|
|
162
162
|
@hConsoleHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
163
163
|
@GetStdHandle.call(STD_OUTPUT_HANDLE)
|
|
164
|
-
rescue
|
|
164
|
+
rescue StandardError
|
|
165
165
|
nil
|
|
166
166
|
end
|
|
167
167
|
end
|
|
168
168
|
@legacy_console = begin
|
|
169
169
|
getconsolemode.nobits?(ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
170
|
-
rescue
|
|
170
|
+
rescue StandardError
|
|
171
171
|
false
|
|
172
172
|
end
|
|
173
173
|
next
|
|
@@ -175,7 +175,7 @@ if defined?(Reline::Windows)
|
|
|
175
175
|
if @GetNumberOfConsoleInputEvents.call(@hConsoleInputHandle, num_of_events).zero? || num_of_events.unpack1("L").zero?
|
|
176
176
|
@hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
177
177
|
@GetStdHandle.call(STD_INPUT_HANDLE)
|
|
178
|
-
rescue
|
|
178
|
+
rescue StandardError
|
|
179
179
|
nil
|
|
180
180
|
end
|
|
181
181
|
next
|
|
@@ -185,7 +185,7 @@ if defined?(Reline::Windows)
|
|
|
185
185
|
if @ReadConsoleInputW.call(@hConsoleInputHandle, input_records, 80, read_event).zero?
|
|
186
186
|
@hConsoleInputHandle = begin # rubocop:disable Naming/VariableName -- Win32 FFI SDK adlandırması; yalnızca Windows konsol yolu
|
|
187
187
|
@GetStdHandle.call(STD_INPUT_HANDLE)
|
|
188
|
-
rescue
|
|
188
|
+
rescue StandardError
|
|
189
189
|
nil
|
|
190
190
|
end
|
|
191
191
|
else
|
|
@@ -251,10 +251,15 @@ module Rubik
|
|
|
251
251
|
return ToolOutput.error("Missing required parameter 'conversation_id' for action 'extend_budget'.") if id.empty?
|
|
252
252
|
|
|
253
253
|
steps = Rubik::Orchestrator::Subagent::Budget.normalize_grant(args["steps"] || args[:steps])
|
|
254
|
+
inst = sup.get(id)
|
|
254
255
|
if sup.extend_budget(id, steps)
|
|
255
|
-
|
|
256
|
+
ceiling = inst.respond_to?(:max_total_steps) ? "/#{inst.max_total_steps}" : ""
|
|
257
|
+
"Step budget for subagent '#{id}' successfully extended by #{steps} steps (total budget: #{inst.max_steps}#{ceiling}, " \
|
|
258
|
+
"state: #{inst.state.to_s.upcase})."
|
|
259
|
+
elsif inst.respond_to?(:max_total_steps) && inst.max_steps >= inst.max_total_steps
|
|
260
|
+
ToolOutput.error("Subagent '#{id}' reached its lifetime step limit (#{inst.max_total_steps}); no more steps can be granted.")
|
|
256
261
|
else
|
|
257
|
-
ToolOutput.error("Subagent '#{id}' not found or cannot extend budget (current state: #{
|
|
262
|
+
ToolOutput.error("Subagent '#{id}' not found or cannot extend budget (current state: #{inst&.state || 'not found'}).")
|
|
258
263
|
end
|
|
259
264
|
when "resume"
|
|
260
265
|
id = resolve_id(args)
|
data/lib/rubik/version.rb
CHANGED