rubik 0.3.3 → 0.3.5
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 +102 -0
- data/lib/rubik/adapter/process.rb +11 -8
- data/lib/rubik/ast/go_extractor.rb +9 -2
- data/lib/rubik/ast/rust_extractor.rb +9 -2
- data/lib/rubik/ast/typescript_extractor.rb +13 -7
- data/lib/rubik/buffer/manager.rb +99 -69
- data/lib/rubik/config/defaults.rb +17 -20
- data/lib/rubik/config.rb +0 -6
- data/lib/rubik/context/git_lister.rb +3 -1
- data/lib/rubik/llm/anthropic.rb +5 -2
- data/lib/rubik/llm/client.rb +102 -72
- data/lib/rubik/llm/openai_compatible.rb +83 -68
- 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/goal_ledger/patch.rb +48 -30
- data/lib/rubik/orchestrator/lsp_feedback.rb +20 -4
- data/lib/rubik/orchestrator/subagent/reuse.rb +2 -2
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +60 -31
- data/lib/rubik/platform/process.rb +6 -6
- data/lib/rubik/platform/sanitize.rb +37 -16
- data/lib/rubik/prompt/doctrine.rb +4 -0
- data/lib/rubik/repl/commands/agent_context.rb +126 -104
- data/lib/rubik/repl/commands/agent_execution.rb +183 -155
- data/lib/rubik/repl/commands/agent_models.rb +112 -87
- data/lib/rubik/repl/commands/agent_providers.rb +190 -156
- data/lib/rubik/repl/commands/buffer.rb +122 -88
- data/lib/rubik/repl/commands/lsp.rb +212 -179
- 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 +266 -246
- data/lib/rubik/repl/shell.rb +10 -1
- data/lib/rubik/runtime/discovery.rb +12 -6
- data/lib/rubik/runtime/overlay_storage.rb +59 -2
- data/lib/rubik/sandbox/driver/docker.rb +2 -2
- data/lib/rubik/session/replayer.rb +1 -2
- data/lib/rubik/storage/store.rb +3 -2
- 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/fuzzy_matcher.rb +27 -11
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
|
@@ -80,107 +80,120 @@ module Rubik
|
|
|
80
80
|
next resume_suspended_goal(ctx) if args.length == 1 && sub == "resume"
|
|
81
81
|
next list_suspended_goals(ctx) if args.length == 1 && sub == "list"
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
filtered_args = []
|
|
86
|
-
i = 0
|
|
87
|
-
while i < args.length
|
|
88
|
-
arg = args[i]
|
|
89
|
-
if (m = arg.match(/\A--steps=(\d+)\z/))
|
|
90
|
-
custom_steps = m[1].to_i
|
|
91
|
-
elsif arg.start_with?("--steps=")
|
|
92
|
-
# --steps=X ama X sayı değil: flag metne sızmasın.
|
|
93
|
-
bad_steps = true
|
|
94
|
-
elsif ["--steps", "-s"].include?(arg)
|
|
95
|
-
if i + 1 < args.length && args[i + 1] =~ /\A\d+\z/
|
|
96
|
-
custom_steps = args[i + 1].to_i
|
|
97
|
-
i += 1
|
|
98
|
-
else
|
|
99
|
-
# Değersiz/sayı-olmayan değer: kullanım hatası (sessiz
|
|
100
|
-
# yanlış koşu yerine).
|
|
101
|
-
bad_steps = true
|
|
102
|
-
end
|
|
103
|
-
else
|
|
104
|
-
filtered_args << arg
|
|
105
|
-
end
|
|
106
|
-
i += 1
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
if bad_steps || (custom_steps && custom_steps < 1)
|
|
83
|
+
parsed = parse_goal_args(args)
|
|
84
|
+
if !parsed[:valid]
|
|
110
85
|
ctx.out.puts USAGE_GOAL
|
|
111
86
|
ctx.out.puts "⚠️ [Rubik::Goal] --steps pozitif bir tam sayı bekliyor (örn. !goal --steps 200 <hedef>)"
|
|
112
87
|
next
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
goal_text = filtered_args.join(" ").strip
|
|
116
|
-
if goal_text.empty?
|
|
88
|
+
elsif parsed[:goal_text].empty?
|
|
117
89
|
ctx.out.puts USAGE_GOAL
|
|
118
90
|
next
|
|
119
91
|
end
|
|
120
92
|
|
|
121
|
-
ctx
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
routed = ctx.router.route(task_type: :goal, prompt: goal_text)
|
|
125
|
-
ctx.runtime_manager.provide(:llm_client, routed[:provider]) if routed[:provider]
|
|
93
|
+
execute_goal(ctx, parsed[:goal_text], parsed[:custom_steps])
|
|
94
|
+
end
|
|
95
|
+
end
|
|
126
96
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
97
|
+
private_class_method def self.parse_goal_args(args)
|
|
98
|
+
custom_steps = nil
|
|
99
|
+
bad_steps = false
|
|
100
|
+
filtered_args = []
|
|
101
|
+
i = 0
|
|
102
|
+
while i < args.length
|
|
103
|
+
arg = args[i]
|
|
104
|
+
if (m = arg.match(/\A--steps=(\d+)\z/))
|
|
105
|
+
custom_steps = m[1].to_i
|
|
106
|
+
elsif arg.start_with?("--steps=")
|
|
107
|
+
# --steps=X ama X sayı değil: flag metne sızmasın.
|
|
108
|
+
bad_steps = true
|
|
109
|
+
elsif ["--steps", "-s"].include?(arg)
|
|
110
|
+
if i + 1 < args.length && args[i + 1] =~ /\A\d+\z/
|
|
111
|
+
custom_steps = args[i + 1].to_i
|
|
112
|
+
i += 1
|
|
113
|
+
else
|
|
114
|
+
# Değersiz/sayı-olmayan değer: kullanım hatası (sessiz
|
|
115
|
+
# yanlış koşu yerine).
|
|
116
|
+
bad_steps = true
|
|
117
|
+
end
|
|
118
|
+
else
|
|
119
|
+
filtered_args << arg
|
|
150
120
|
end
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# (Σ satır id'si her kabulde değişir; kimlik olarak kullanılamaz.)
|
|
154
|
-
goal_id = "goal-#{Process.pid}-#{SecureRandom.hex(4)}"
|
|
121
|
+
i += 1
|
|
122
|
+
end
|
|
155
123
|
|
|
156
|
-
|
|
157
|
-
|
|
124
|
+
goal_text = filtered_args.join(" ").strip
|
|
125
|
+
valid = !bad_steps && (custom_steps.nil? || custom_steps >= 1)
|
|
126
|
+
{ custom_steps: custom_steps, goal_text: goal_text, valid: valid }
|
|
127
|
+
end
|
|
158
128
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
model: routed[:model],
|
|
162
|
-
max_steps: goal_steps,
|
|
163
|
-
warn_at_ratio: warn_ratio,
|
|
164
|
-
context_limit: routed[:context_limit],
|
|
165
|
-
max_output_tokens: routed[:max_output_tokens],
|
|
166
|
-
on_step: build_goal_on_step(ctx, goal_state)
|
|
167
|
-
)
|
|
129
|
+
private_class_method def self.initialize_goal_ledger(ctx, goal_text)
|
|
130
|
+
return nil unless Rubik::Orchestrator::GoalLedger::Ledger.enabled?(ctx.config)
|
|
168
131
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
132
|
+
store = Rubik::Coeffects.get(ctx.runtime_manager&.context, Rubik::Coeffects::STORAGE_STORE)
|
|
133
|
+
dmesg = Rubik::Coeffects.get(ctx.runtime_manager&.context, Rubik::Coeffects::DMESG)
|
|
134
|
+
goal_ledger = Rubik::Orchestrator::GoalLedger::Ledger.create!(
|
|
135
|
+
goal_text: goal_text, config: ctx.config, store: store, dmesg: dmesg,
|
|
136
|
+
event_bus: ctx.event_bus
|
|
137
|
+
)
|
|
138
|
+
ctx.runtime_manager.provide(Rubik::Coeffects::GOAL_LEDGER, goal_ledger)
|
|
139
|
+
goal_ledger
|
|
140
|
+
end
|
|
174
141
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
142
|
+
private_class_method def self.execute_goal(ctx, goal_text, custom_steps)
|
|
143
|
+
ctx.recorder&.record_user_turn("!goal #{goal_text}")
|
|
144
|
+
goal_steps = custom_steps || ctx.config.fetch("agent.goal_max_steps", 100).to_i
|
|
145
|
+
warn_ratio = ctx.config.fetch("agent.warn_at_ratio", 0.75).to_f
|
|
146
|
+
routed = ctx.router.route(task_type: :goal, prompt: goal_text)
|
|
147
|
+
ctx.runtime_manager.provide(:llm_client, routed[:provider]) if routed[:provider]
|
|
178
148
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
# Canlı hedef durumu: maestro ve alt ajanlar read_file
|
|
150
|
+
# rubik://goal/state ile aynı anlık görüntüyü okur. Provide swap
|
|
151
|
+
# semantiğine sahiptir — tekrarlanan !goal koşuları Ω'ya inverse
|
|
152
|
+
# yığmaz; session dispose'u tek delete-inverse ile temizler.
|
|
153
|
+
# event_bus: bitiş olayı (GoalState::FINISH_EVENT) Vakanüvis
|
|
154
|
+
# kronikçisinin goal kancasına akar (K3) — notice/wake yok.
|
|
155
|
+
goal_state = Rubik::Orchestrator::GoalState.new(event_bus: ctx.event_bus)
|
|
156
|
+
goal_state.start!(goal_text, goal_steps)
|
|
157
|
+
ctx.runtime_manager.provide(Rubik::Coeffects::GOAL_STATE, goal_state)
|
|
158
|
+
|
|
159
|
+
# Yapılandırılmış Σ defteri (goal-ledger planı A): knob ON iken
|
|
160
|
+
# koşuyla doğar, terminal dallarda finalize olur, provide swap
|
|
161
|
+
# semantiğiyle tekrarlanan !goal koşuları Ω'ya inverse yığmaz.
|
|
162
|
+
# OFF iken hiçbir yüz kurulmaz (araç/URI/prompt bayt-özdeş).
|
|
163
|
+
goal_ledger = initialize_goal_ledger(ctx, goal_text)
|
|
164
|
+
|
|
165
|
+
# Askı kimliği (issue #106): koşu boyunca SABİT — registry
|
|
166
|
+
# girdisinin anahtarı ve Σ damgası meta'sının goal_id'si.
|
|
167
|
+
# (Σ satır id'si her kabulde değişir; kimlik olarak kullanılamaz.)
|
|
168
|
+
goal_id = "goal-#{Process.pid}-#{SecureRandom.hex(4)}"
|
|
169
|
+
|
|
170
|
+
ctx.out.puts "[Rubik::Goal] Starting autonomous goal loop (Budget: #{goal_steps} steps) with #{routed[:role]} model (#{routed[:model]})..."
|
|
171
|
+
ctx.out.puts "[Rubik::Goal] Target: #{goal_text}\n"
|
|
172
|
+
|
|
173
|
+
orchestrator = ctx.orchestrator_loop(
|
|
174
|
+
provider: routed[:provider],
|
|
175
|
+
model: routed[:model],
|
|
176
|
+
max_steps: goal_steps,
|
|
177
|
+
warn_at_ratio: warn_ratio,
|
|
178
|
+
context_limit: routed[:context_limit],
|
|
179
|
+
max_output_tokens: routed[:max_output_tokens],
|
|
180
|
+
on_step: build_goal_on_step(ctx, goal_state)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Plan 53 D-8 yardımcısı: nötr çift + kayıtçı eşlemesi tek yerde.
|
|
184
|
+
# Session restore kayıtçı OLAYLARINDAN hidrate eder — eşlenmiş
|
|
185
|
+
# restore için conversation_history VE record_assistant_response
|
|
186
|
+
# birlikte yazılmalı (başarısızlıkta da).
|
|
187
|
+
record_goal_pair = build_goal_record_pair(ctx, goal_text)
|
|
188
|
+
|
|
189
|
+
# Pruner apply seam: hedef döngüsü de aynı sınırda tohumlanır —
|
|
190
|
+
# aşamalı budama planı varsa goal promptundan ÖNCE uygulanır.
|
|
191
|
+
ctx.apply_staged_prune_plan!
|
|
192
|
+
|
|
193
|
+
drive_goal_turn(ctx, orchestrator: orchestrator, goal_state: goal_state,
|
|
194
|
+
goal_ledger: goal_ledger, goal_text: goal_text, goal_id: goal_id,
|
|
195
|
+
record_pair: record_goal_pair,
|
|
196
|
+
start: ->(loop_inst) { loop_inst.run(goal_text, goal_mode: true, conversation_history: ctx.conversation_history) })
|
|
184
197
|
end
|
|
185
198
|
|
|
186
199
|
# ------------------------------------------------------------------
|
|
@@ -195,75 +208,16 @@ module Rubik
|
|
|
195
208
|
# çıktısını bayt-baytına korur.
|
|
196
209
|
private_class_method def self.drive_goal_turn(ctx, orchestrator:, goal_state:, goal_ledger:,
|
|
197
210
|
goal_text:, goal_id:, record_pair:, start:)
|
|
198
|
-
finalize_ledger = lambda do |status|
|
|
199
|
-
next unless goal_ledger
|
|
200
|
-
|
|
201
|
-
goal_ledger.finalize!(status)
|
|
202
|
-
goal_state.ledger_id = goal_ledger.snapshot[:storage_id]
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
fail_goal = lambda do |reason, tokens: nil, steps: nil|
|
|
206
|
-
sanitized = Rubik::Platform.safe_utf8(reason.to_s)
|
|
207
|
-
steps_used = steps || goal_state.step_count
|
|
208
|
-
goal_state.fail!(sanitized, tokens: tokens)
|
|
209
|
-
finalize_ledger.call(:failed)
|
|
210
|
-
forget_suspended_goal(ctx, goal_id)
|
|
211
|
-
ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
|
|
212
|
-
record_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: tokens)
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
suspend_goal = lambda do |reason, tokens: nil|
|
|
216
|
-
attempt = suspended_attempt(ctx, goal_id) + 1
|
|
217
|
-
goal_state.suspend!(reason, tokens: tokens)
|
|
218
|
-
goal_ledger&.suspend!(suspend_stamp_meta(goal_state, goal_id, reason, attempt))
|
|
219
|
-
registry = goal_registry_for(ctx)
|
|
220
|
-
registry.register(Rubik::Orchestrator::GoalRegistry::Entry.new(
|
|
221
|
-
goal_id: goal_id, goal_text: goal_text, loop: orchestrator,
|
|
222
|
-
goal_state: goal_state, ledger: goal_ledger, reason: reason.to_s,
|
|
223
|
-
suspended_at: Time.now, attempt: attempt
|
|
224
|
-
))
|
|
225
|
-
ctx.out.puts "\n⏸️ [Rubik::Goal] Suspended after #{goal_state.step_count} steps (transient provider error): #{reason}"
|
|
226
|
-
ctx.out.puts " Loop, counters and Σ ledger are preserved — resume with `!goal resume`."
|
|
227
|
-
record_pair.call("[Goal suspended after #{goal_state.step_count} steps: #{reason}]", tokens: tokens)
|
|
228
|
-
end
|
|
229
|
-
|
|
230
211
|
ctx.active_loop = orchestrator
|
|
231
212
|
begin
|
|
232
213
|
res = start.call(orchestrator)
|
|
233
|
-
|
|
234
|
-
# Bütçe tükenince sor (execute_free_text simetrisi; aynı knob,
|
|
235
|
-
# aynı +20). continue_loop boyunca active_loop DOLU kalmalı:
|
|
236
|
-
# notice yönlendirmesi aktif döngüye bakar (lifecycle.rb
|
|
237
|
-
# route_subagent_notice) — soruya beklerken gelen subagent
|
|
238
|
-
# olayları loop inbox'a girer ve devam adımında boşaltılır.
|
|
239
|
-
auto_extensions = 0
|
|
240
|
-
max_auto = ctx.config.fetch("agent.goal_auto_continue_limit", 0).to_i
|
|
241
|
-
while !res[:ok] && res[:error]&.include?("Max steps budget exceeded")
|
|
242
|
-
if auto_extensions < max_auto
|
|
243
|
-
auto_extensions += 1
|
|
244
|
-
ctx.out.puts "\n🤖 [Rubik::Goal] Adım bütçesi doldu (#{res[:error]}). " \
|
|
245
|
-
"Otonom devam hakkı (#{auto_extensions}/#{max_auto}) kullanılarak +20 adım ekleniyor..."
|
|
246
|
-
goal_state.extend!(goal_state.max_steps + 20)
|
|
247
|
-
res = orchestrator.continue_loop(20, auto: true)
|
|
248
|
-
next
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
break unless ctx.config.fetch("agent.ask_continue_on_limit", true)
|
|
252
|
-
|
|
253
|
-
ctx.out.puts "\n⚠️ [Rubik::Goal] #{res[:error]}"
|
|
254
|
-
ans = ctx.ask_yes_no(" [Rubik::Goal] +20 adım ile devam edilsin mi?", default: "n")
|
|
255
|
-
break unless ["y", "yes", "evet"].include?(ans)
|
|
256
|
-
|
|
257
|
-
ctx.out.puts " [Rubik::Goal] Adım bütçesi +20 artırılıyor ve hedef sürdürülüyor..."
|
|
258
|
-
goal_state.extend!(goal_state.max_steps + 20)
|
|
259
|
-
res = orchestrator.continue_loop(20)
|
|
260
|
-
end
|
|
214
|
+
res = handle_goal_budget_extensions(ctx, orchestrator: orchestrator, goal_state: goal_state, res: res)
|
|
261
215
|
rescue Interrupt
|
|
262
216
|
# ^C hedefi 'running' bırakmasın; eşlenmiş kayıt otursun. Interrupt
|
|
263
217
|
# yeniden yükselir — kullanıcı mesajını execute_command basar.
|
|
264
218
|
# Resume üstüne ^C = askıyı terk (terminal :stopped damgası).
|
|
265
219
|
goal_state.stop!("Interrupted by user (^C)")
|
|
266
|
-
|
|
220
|
+
finalize_goal_ledger(goal_ledger, goal_state, :stopped)
|
|
267
221
|
forget_suspended_goal(ctx, goal_id)
|
|
268
222
|
record_pair.call("[Goal attempt interrupted after #{goal_state.step_count} steps]")
|
|
269
223
|
raise
|
|
@@ -276,18 +230,92 @@ module Rubik
|
|
|
276
230
|
# next yerine return: rescue sonrası res'siz terminal
|
|
277
231
|
# eşlemesine düşülmesin.
|
|
278
232
|
if e.respond_to?(:transient?) && e.transient?
|
|
279
|
-
|
|
233
|
+
suspend_goal_turn(ctx, orchestrator: orchestrator, goal_state: goal_state, goal_ledger: goal_ledger,
|
|
234
|
+
goal_text: goal_text, goal_id: goal_id, record_pair: record_pair, reason: e.message)
|
|
280
235
|
else
|
|
281
|
-
|
|
236
|
+
fail_goal_turn(ctx, goal_state: goal_state, goal_ledger: goal_ledger, goal_id: goal_id,
|
|
237
|
+
record_pair: record_pair, reason: e.message)
|
|
282
238
|
end
|
|
283
239
|
return
|
|
284
240
|
rescue StandardError => e
|
|
285
|
-
|
|
241
|
+
fail_goal_turn(ctx, goal_state: goal_state, goal_ledger: goal_ledger, goal_id: goal_id,
|
|
242
|
+
record_pair: record_pair, reason: "#{e.class}: #{e.message}")
|
|
286
243
|
return
|
|
287
244
|
ensure
|
|
288
245
|
ctx.active_loop = nil
|
|
289
246
|
end
|
|
290
247
|
|
|
248
|
+
handle_goal_completion(ctx, res: res, goal_state: goal_state, goal_ledger: goal_ledger,
|
|
249
|
+
goal_id: goal_id, record_pair: record_pair)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private_class_method def self.finalize_goal_ledger(goal_ledger, goal_state, status)
|
|
253
|
+
return unless goal_ledger
|
|
254
|
+
|
|
255
|
+
goal_ledger.finalize!(status)
|
|
256
|
+
goal_state.ledger_id = goal_ledger.snapshot[:storage_id]
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
private_class_method def self.fail_goal_turn(ctx, goal_state:, goal_ledger:, goal_id:, record_pair:,
|
|
260
|
+
reason:, tokens: nil, steps: nil)
|
|
261
|
+
sanitized = Rubik::Platform.safe_utf8(reason.to_s)
|
|
262
|
+
steps_used = steps || goal_state.step_count
|
|
263
|
+
goal_state.fail!(sanitized, tokens: tokens)
|
|
264
|
+
finalize_goal_ledger(goal_ledger, goal_state, :failed)
|
|
265
|
+
forget_suspended_goal(ctx, goal_id)
|
|
266
|
+
ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
|
|
267
|
+
record_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: tokens)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private_class_method def self.suspend_goal_turn(ctx, orchestrator:, goal_state:, goal_ledger:,
|
|
271
|
+
goal_text:, goal_id:, record_pair:, reason:, tokens: nil)
|
|
272
|
+
attempt = suspended_attempt(ctx, goal_id) + 1
|
|
273
|
+
goal_state.suspend!(reason, tokens: tokens)
|
|
274
|
+
goal_ledger&.suspend!(suspend_stamp_meta(goal_state, goal_id, reason, attempt))
|
|
275
|
+
registry = goal_registry_for(ctx)
|
|
276
|
+
registry.register(Rubik::Orchestrator::GoalRegistry::Entry.new(
|
|
277
|
+
goal_id: goal_id, goal_text: goal_text, loop: orchestrator,
|
|
278
|
+
goal_state: goal_state, ledger: goal_ledger, reason: reason.to_s,
|
|
279
|
+
suspended_at: Time.now, attempt: attempt
|
|
280
|
+
))
|
|
281
|
+
ctx.out.puts "\n⏸️ [Rubik::Goal] Suspended after #{goal_state.step_count} steps (transient provider error): #{reason}"
|
|
282
|
+
ctx.out.puts " Loop, counters and Σ ledger are preserved — resume with `!goal resume`."
|
|
283
|
+
record_pair.call("[Goal suspended after #{goal_state.step_count} steps: #{reason}]", tokens: tokens)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
private_class_method def self.handle_goal_budget_extensions(ctx, orchestrator:, goal_state:, res:)
|
|
287
|
+
# Bütçe tükenince sor (execute_free_text simetrisi; aynı knob,
|
|
288
|
+
# aynı +20). continue_loop boyunca active_loop DOLU kalmalı:
|
|
289
|
+
# notice yönlendirmesi aktif döngüye bakar (lifecycle.rb
|
|
290
|
+
# route_subagent_notice) — soruya beklerken gelen subagent
|
|
291
|
+
# olayları loop inbox'a girer ve devam adımında boşaltılır.
|
|
292
|
+
auto_extensions = 0
|
|
293
|
+
max_auto = ctx.config.fetch("agent.goal_auto_continue_limit", 0).to_i
|
|
294
|
+
while !res[:ok] && res[:error]&.include?("Max steps budget exceeded")
|
|
295
|
+
if auto_extensions < max_auto
|
|
296
|
+
auto_extensions += 1
|
|
297
|
+
ctx.out.puts "\n🤖 [Rubik::Goal] Adım bütçesi doldu (#{res[:error]}). " \
|
|
298
|
+
"Otonom devam hakkı (#{auto_extensions}/#{max_auto}) kullanılarak +20 adım ekleniyor..."
|
|
299
|
+
goal_state.extend!(goal_state.max_steps + 20)
|
|
300
|
+
res = orchestrator.continue_loop(20, auto: true)
|
|
301
|
+
next
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
break unless ctx.config.fetch("agent.ask_continue_on_limit", true)
|
|
305
|
+
|
|
306
|
+
ctx.out.puts "\n⚠️ [Rubik::Goal] #{res[:error]}"
|
|
307
|
+
ans = ctx.ask_yes_no(" [Rubik::Goal] +20 adım ile devam edilsin mi?", default: "n")
|
|
308
|
+
break unless ["y", "yes", "evet"].include?(ans)
|
|
309
|
+
|
|
310
|
+
ctx.out.puts " [Rubik::Goal] Adım bütçesi +20 artırılıyor ve hedef sürdürülüyor..."
|
|
311
|
+
goal_state.extend!(goal_state.max_steps + 20)
|
|
312
|
+
res = orchestrator.continue_loop(20)
|
|
313
|
+
end
|
|
314
|
+
res
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
private_class_method def self.handle_goal_completion(ctx, res:, goal_state:, goal_ledger:,
|
|
318
|
+
goal_id:, record_pair:)
|
|
291
319
|
if res[:ok]
|
|
292
320
|
active = goal_active_subagent_count(ctx)
|
|
293
321
|
if active.positive?
|
|
@@ -297,12 +325,12 @@ module Rubik
|
|
|
297
325
|
# "Goal Achieved" (field incident 2026-10-05).
|
|
298
326
|
reason = "Goal loop concluded with #{active} active subagent(s) still running — not marked complete"
|
|
299
327
|
goal_state.stop!(reason)
|
|
300
|
-
|
|
328
|
+
finalize_goal_ledger(goal_ledger, goal_state, :stopped)
|
|
301
329
|
ctx.out.puts "\n⚠️ [Rubik::Goal] #{reason}."
|
|
302
330
|
record_pair.call("[Goal attempt ended after #{goal_state.step_count} steps: #{reason}]", tokens: res[:tokens])
|
|
303
331
|
else
|
|
304
332
|
goal_state.complete!(res[:output].to_s, tokens: res[:tokens])
|
|
305
|
-
|
|
333
|
+
finalize_goal_ledger(goal_ledger, goal_state, :completed)
|
|
306
334
|
forget_suspended_goal(ctx, goal_id)
|
|
307
335
|
ctx.out.puts "\n🎉 Goal Achieved in #{res[:steps]} steps!"
|
|
308
336
|
ctx.out.puts res[:output]
|
|
@@ -316,10 +344,10 @@ module Rubik
|
|
|
316
344
|
# Başarısızlık eşlemesi yukarıdaki tek hata kanalından akar:
|
|
317
345
|
# GoalState → ledger finalize → kayıtçı çifti, istisna yoluyla
|
|
318
346
|
# aynı sırayla.
|
|
319
|
-
|
|
347
|
+
fail_goal_turn(ctx, goal_state: goal_state, goal_ledger: goal_ledger, goal_id: goal_id,
|
|
348
|
+
record_pair: record_pair, reason: res[:error], tokens: res[:tokens], steps: res[:steps])
|
|
320
349
|
end
|
|
321
350
|
end
|
|
322
|
-
|
|
323
351
|
# ------------------------------------------------------------------
|
|
324
352
|
# !goal resume (issue #106). İki yol, dönüşüm sırasına göre:
|
|
325
353
|
# 1) Oturum kaydında canlı girdi varsa süreç-içi resume — hayatta
|
|
@@ -110,99 +110,124 @@ module Rubik
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
private_class_method def self.handle_list_models(ctx)
|
|
114
|
+
mode = ctx.config.fetch("router.mode", "auto")
|
|
115
|
+
def_spec = ctx.config.model_spec(:default)
|
|
116
|
+
fast_spec = ctx.config.model_spec(:fast)
|
|
117
|
+
adv_spec = ctx.config.model_spec(:advanced)
|
|
118
|
+
|
|
119
|
+
def_limit = ctx.llm_registry.context_limit_for(def_spec["provider"], def_spec["name"])
|
|
120
|
+
fast_limit = ctx.llm_registry.context_limit_for(fast_spec["provider"], fast_spec["name"])
|
|
121
|
+
adv_limit = ctx.llm_registry.context_limit_for(adv_spec["provider"], adv_spec["name"])
|
|
122
|
+
|
|
123
|
+
def_ep = def_spec["endpoint"] || def_spec["host"] || "cloud"
|
|
124
|
+
fast_ep = fast_spec["endpoint"] || fast_spec["host"] || "cloud"
|
|
125
|
+
adv_ep = adv_spec["endpoint"] || adv_spec["host"] || "cloud"
|
|
126
|
+
|
|
127
|
+
ctx.out.puts "Model Router Configuration (Mode: #{mode}):"
|
|
128
|
+
ctx.out.puts " • default : #{def_spec['provider']}/#{def_spec['name']} [Context: #{def_limit} tokens] (#{def_ep})"
|
|
129
|
+
ctx.out.puts " • fast : #{fast_spec['provider']}/#{fast_spec['name']} [Context: #{fast_limit} tokens] (#{fast_ep})"
|
|
130
|
+
ctx.out.puts " • advanced : #{adv_spec['provider']}/#{adv_spec['name']} [Context: #{adv_limit} tokens] (#{adv_ep})"
|
|
131
|
+
ctx.out.puts "Global Config : #{ctx.config.global_config_path}"
|
|
132
|
+
ctx.out.puts "Project Config : #{ctx.config.project_config_path}"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private_class_method def self.handle_route_command(clean_args, ctx, scope)
|
|
136
|
+
mode = clean_args[1].to_s.downcase
|
|
137
|
+
if ["auto", "manual"].include?(mode)
|
|
138
|
+
res = ctx.config.set("router.mode", mode, persist: scope)
|
|
139
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
140
|
+
|
|
141
|
+
ctx.out.puts "Router mode set to #{mode} [#{Base.scope_label(scope)}]."
|
|
142
|
+
else
|
|
143
|
+
ctx.out.puts "Usage: !model route <auto|manual>"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private_class_method def self.handle_add_model(clean_args, ctx, scope)
|
|
148
|
+
p_name = clean_args[1]&.downcase
|
|
149
|
+
m_name = clean_args[2]
|
|
150
|
+
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
|
|
151
|
+
ctx.out.puts "Usage: !model add <provider_name> <model_name> [--context=LIMIT] [--project|--global]"
|
|
152
|
+
else
|
|
153
|
+
context_arg = clean_args.find { |a| a.start_with?("--context=") }
|
|
154
|
+
c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
|
|
155
|
+
res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: c_limit, persist: scope)
|
|
156
|
+
ctx.refresh_llm_registry!
|
|
157
|
+
if res[:ok]
|
|
158
|
+
ctx.out.puts "Added model '#{m_name}' (#{c_limit} tokens) to provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
159
|
+
elsif res[:reason] == :unknown_provider
|
|
160
|
+
ctx.out.puts "Provider '#{p_name}' not found. Add provider first via !provider add."
|
|
161
|
+
else
|
|
162
|
+
Base.report_persist_failure(ctx, res, scope)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private_class_method def self.handle_context_command(clean_args, ctx, scope)
|
|
168
|
+
if clean_args.size == 2 && clean_args[1] =~ /^\d+$/
|
|
169
|
+
def_spec = ctx.config.model_spec(:default)
|
|
170
|
+
update_context_limit(ctx, scope, def_spec["provider"], def_spec["name"],
|
|
171
|
+
clean_args[1].to_i, "default model #{def_spec['provider']}/#{def_spec['name']}")
|
|
172
|
+
elsif clean_args.size == 3 && ["default", "fast", "advanced"].include?(clean_args[1].downcase) && clean_args[2] =~ /^\d+$/
|
|
173
|
+
role = clean_args[1].downcase.to_sym
|
|
174
|
+
spec = ctx.config.model_spec(role)
|
|
175
|
+
update_context_limit(ctx, scope, spec["provider"], spec["name"],
|
|
176
|
+
clean_args[2].to_i, "#{role} model #{spec['provider']}/#{spec['name']}")
|
|
177
|
+
elsif clean_args.size == 4 && clean_args[3] =~ /^\d+$/
|
|
178
|
+
update_context_limit(ctx, scope, clean_args[1].downcase, clean_args[2],
|
|
179
|
+
clean_args[3].to_i, "#{clean_args[1].downcase}/#{clean_args[2]}")
|
|
180
|
+
else
|
|
181
|
+
ctx.out.puts "Usage: !model context <limit> | !model context <default|fast|advanced> <limit> | " \
|
|
182
|
+
"!model context <provider> <model> <limit> [--project|--global]"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private_class_method def self.handle_set_role_command(clean_args, ctx, scope)
|
|
187
|
+
role = clean_args[1].to_s.downcase
|
|
188
|
+
model_args = clean_args[2..]
|
|
189
|
+
if model_args.nil? || model_args.empty?
|
|
190
|
+
ctx.out.puts "Usage: !model set <default|fast|advanced> [<provider>] <model_name> [--project|--global]"
|
|
191
|
+
else
|
|
192
|
+
prov, mod = parse_model_args(model_args, ctx)
|
|
193
|
+
res = ctx.config.set_model_spec(role, provider: prov, name: mod, persist: scope)
|
|
194
|
+
ctx.resync_llm_route!
|
|
195
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
196
|
+
|
|
197
|
+
limit = ctx.llm_registry.context_limit_for(prov, mod)
|
|
198
|
+
ctx.out.puts "Set model role '#{role}' to #{prov}/#{mod} [Context: #{limit} tokens] [#{Base.scope_label(scope)}]."
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private_class_method def self.handle_switch_default_model(clean_args, ctx, scope)
|
|
203
|
+
prov, mod = parse_model_args(clean_args, ctx)
|
|
204
|
+
res = ctx.config.set_model_spec(:default, provider: prov, name: mod, persist: scope)
|
|
205
|
+
ctx.resync_llm_route!
|
|
206
|
+
return if Base.report_persist_failure(ctx, res, scope)
|
|
207
|
+
|
|
208
|
+
limit = ctx.llm_registry.context_limit_for(prov, mod)
|
|
209
|
+
ctx.out.puts "Switched default model to #{prov}/#{mod} [Context: #{limit} tokens] [#{Base.scope_label(scope)}]."
|
|
210
|
+
end
|
|
211
|
+
|
|
113
212
|
private_class_method def self.register_model_command(shell)
|
|
114
213
|
shell.register_command("model",
|
|
115
214
|
audience: :both,
|
|
116
215
|
description: "Switch or view LLM provider/model roles (default, fast, advanced) with auto-persistence") do |args, ctx|
|
|
117
216
|
scope, clean_args = Base.parse_scope(args)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
fast_ep = fast_spec["endpoint"] || fast_spec["host"] || "cloud"
|
|
131
|
-
adv_ep = adv_spec["endpoint"] || adv_spec["host"] || "cloud"
|
|
132
|
-
|
|
133
|
-
ctx.out.puts "Model Router Configuration (Mode: #{mode}):"
|
|
134
|
-
ctx.out.puts " • default : #{def_spec['provider']}/#{def_spec['name']} [Context: #{def_limit} tokens] (#{def_ep})"
|
|
135
|
-
ctx.out.puts " • fast : #{fast_spec['provider']}/#{fast_spec['name']} [Context: #{fast_limit} tokens] (#{fast_ep})"
|
|
136
|
-
ctx.out.puts " • advanced : #{adv_spec['provider']}/#{adv_spec['name']} [Context: #{adv_limit} tokens] (#{adv_ep})"
|
|
137
|
-
ctx.out.puts "Global Config : #{ctx.config.global_config_path}"
|
|
138
|
-
ctx.out.puts "Project Config : #{ctx.config.project_config_path}"
|
|
139
|
-
elsif clean_args.first.downcase == "route"
|
|
140
|
-
mode = clean_args[1].to_s.downcase
|
|
141
|
-
if ["auto", "manual"].include?(mode)
|
|
142
|
-
res = ctx.config.set("router.mode", mode, persist: scope)
|
|
143
|
-
next if Base.report_persist_failure(ctx, res, scope)
|
|
144
|
-
|
|
145
|
-
ctx.out.puts "Router mode set to #{mode} [#{Base.scope_label(scope)}]."
|
|
146
|
-
else
|
|
147
|
-
ctx.out.puts "Usage: !model route <auto|manual>"
|
|
148
|
-
end
|
|
149
|
-
elsif clean_args.first.downcase == "add"
|
|
150
|
-
p_name = clean_args[1]&.downcase
|
|
151
|
-
m_name = clean_args[2]
|
|
152
|
-
if p_name.nil? || p_name.empty? || m_name.nil? || m_name.empty?
|
|
153
|
-
ctx.out.puts "Usage: !model add <provider_name> <model_name> [--context=LIMIT] [--project|--global]"
|
|
154
|
-
else
|
|
155
|
-
context_arg = clean_args.find { |a| a.start_with?("--context=") }
|
|
156
|
-
c_limit = context_arg ? context_arg.split("=", 2).last.to_i : 32_768
|
|
157
|
-
res = ctx.config.upsert_provider_model(p_name, m_name, context_limit: c_limit, persist: scope)
|
|
158
|
-
ctx.refresh_llm_registry!
|
|
159
|
-
if res[:ok]
|
|
160
|
-
ctx.out.puts "Added model '#{m_name}' (#{c_limit} tokens) to provider '#{p_name}' [#{Base.scope_label(scope)}]."
|
|
161
|
-
elsif res[:reason] == :unknown_provider
|
|
162
|
-
ctx.out.puts "Provider '#{p_name}' not found. Add provider first via !provider add."
|
|
163
|
-
else
|
|
164
|
-
Base.report_persist_failure(ctx, res, scope)
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
elsif ["context", "limit"].include?(clean_args.first.downcase)
|
|
168
|
-
if clean_args.size == 2 && clean_args[1] =~ /^\d+$/
|
|
169
|
-
def_spec = ctx.config.model_spec(:default)
|
|
170
|
-
update_context_limit(ctx, scope, def_spec["provider"], def_spec["name"],
|
|
171
|
-
clean_args[1].to_i, "default model #{def_spec['provider']}/#{def_spec['name']}")
|
|
172
|
-
elsif clean_args.size == 3 && ["default", "fast", "advanced"].include?(clean_args[1].downcase) && clean_args[2] =~ /^\d+$/
|
|
173
|
-
role = clean_args[1].downcase.to_sym
|
|
174
|
-
spec = ctx.config.model_spec(role)
|
|
175
|
-
update_context_limit(ctx, scope, spec["provider"], spec["name"],
|
|
176
|
-
clean_args[2].to_i, "#{role} model #{spec['provider']}/#{spec['name']}")
|
|
177
|
-
elsif clean_args.size == 4 && clean_args[3] =~ /^\d+$/
|
|
178
|
-
update_context_limit(ctx, scope, clean_args[1].downcase, clean_args[2],
|
|
179
|
-
clean_args[3].to_i, "#{clean_args[1].downcase}/#{clean_args[2]}")
|
|
180
|
-
else
|
|
181
|
-
ctx.out.puts "Usage: !model context <limit> | !model context <default|fast|advanced> <limit> | " \
|
|
182
|
-
"!model context <provider> <model> <limit> [--project|--global]"
|
|
183
|
-
end
|
|
184
|
-
elsif clean_args.first.downcase == "set"
|
|
185
|
-
role = clean_args[1].to_s.downcase
|
|
186
|
-
model_args = clean_args[2..]
|
|
187
|
-
if model_args.nil? || model_args.empty?
|
|
188
|
-
ctx.out.puts "Usage: !model set <default|fast|advanced> [<provider>] <model_name> [--project|--global]"
|
|
189
|
-
else
|
|
190
|
-
prov, mod = parse_model_args(model_args, ctx)
|
|
191
|
-
res = ctx.config.set_model_spec(role, provider: prov, name: mod, persist: scope)
|
|
192
|
-
ctx.resync_llm_route!
|
|
193
|
-
next if Base.report_persist_failure(ctx, res, scope)
|
|
194
|
-
|
|
195
|
-
limit = ctx.llm_registry.context_limit_for(prov, mod)
|
|
196
|
-
ctx.out.puts "Set model role '#{role}' to #{prov}/#{mod} [Context: #{limit} tokens] [#{Base.scope_label(scope)}]."
|
|
197
|
-
end
|
|
217
|
+
subcmd = clean_args.first&.downcase
|
|
218
|
+
|
|
219
|
+
if clean_args.empty? || subcmd == "list"
|
|
220
|
+
handle_list_models(ctx)
|
|
221
|
+
elsif subcmd == "route"
|
|
222
|
+
handle_route_command(clean_args, ctx, scope)
|
|
223
|
+
elsif subcmd == "add"
|
|
224
|
+
handle_add_model(clean_args, ctx, scope)
|
|
225
|
+
elsif ["context", "limit"].include?(subcmd)
|
|
226
|
+
handle_context_command(clean_args, ctx, scope)
|
|
227
|
+
elsif subcmd == "set"
|
|
228
|
+
handle_set_role_command(clean_args, ctx, scope)
|
|
198
229
|
else
|
|
199
|
-
|
|
200
|
-
res = ctx.config.set_model_spec(:default, provider: prov, name: mod, persist: scope)
|
|
201
|
-
ctx.resync_llm_route!
|
|
202
|
-
next if Base.report_persist_failure(ctx, res, scope)
|
|
203
|
-
|
|
204
|
-
limit = ctx.llm_registry.context_limit_for(prov, mod)
|
|
205
|
-
ctx.out.puts "Switched default model to #{prov}/#{mod} [Context: #{limit} tokens] [#{Base.scope_label(scope)}]."
|
|
230
|
+
handle_switch_default_model(clean_args, ctx, scope)
|
|
206
231
|
end
|
|
207
232
|
end
|
|
208
233
|
end
|