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
|
@@ -31,121 +31,143 @@ module Rubik
|
|
|
31
31
|
sub = args.first&.downcase
|
|
32
32
|
case sub
|
|
33
33
|
when "scan"
|
|
34
|
-
|
|
35
|
-
ctx.context_index.full_scan
|
|
36
|
-
ctx.warm_ast_graph(async: false)
|
|
37
|
-
ctx.out.puts "Context index and AST symbol graph updated."
|
|
38
|
-
rescue Errno::EACCES => e
|
|
39
|
-
root_path = begin
|
|
40
|
-
ctx.buffer_manager.root_dir
|
|
41
|
-
rescue
|
|
42
|
-
Dir.pwd
|
|
43
|
-
end
|
|
44
|
-
ctx.out.puts Rubik::Tools::DiagnosticReporter.render_permission_denied(root_path, operation: "scan", reason: e.message)
|
|
45
|
-
rescue StandardError => e
|
|
46
|
-
ctx.out.puts "Context scan failed: #{e.message}"
|
|
47
|
-
end
|
|
34
|
+
handle_context_scan(ctx)
|
|
48
35
|
when "add"
|
|
49
|
-
|
|
50
|
-
if file
|
|
51
|
-
ctx.context_index.update_file(file)
|
|
52
|
-
full_path = File.join(ctx.buffer_manager.root_dir, file)
|
|
53
|
-
ctx.ast_graph.index_file(full_path) if File.exist?(full_path)
|
|
54
|
-
end
|
|
55
|
-
ctx.out.puts "Added #{file} to context index and AST graph."
|
|
36
|
+
handle_context_add(args, ctx)
|
|
56
37
|
when "drop"
|
|
57
|
-
|
|
58
|
-
if file
|
|
59
|
-
ctx.context_index.files.delete(file)
|
|
60
|
-
ctx.out.puts "Removed #{file} from context index."
|
|
61
|
-
else
|
|
62
|
-
ctx.out.puts "Usage: !context drop <relative_file_path>"
|
|
63
|
-
end
|
|
38
|
+
handle_context_drop(args, ctx)
|
|
64
39
|
when "compact"
|
|
65
|
-
|
|
66
|
-
spec = ctx.current_model_spec
|
|
67
|
-
context_limit = ctx.llm_registry.context_limit_for(spec["provider"], spec["name"])
|
|
68
|
-
max_output = ctx.llm_registry.max_output_tokens_for(spec["provider"], spec["name"])
|
|
69
|
-
context_cfg = ctx.config.fetch("context", {})
|
|
70
|
-
|
|
71
|
-
ctx.conversation_history = Rubik::Prompt::ContextCompactor.compact_history(
|
|
72
|
-
ctx.conversation_history,
|
|
73
|
-
context_limit: context_limit,
|
|
74
|
-
max_output_tokens: max_output,
|
|
75
|
-
config: context_cfg,
|
|
76
|
-
force: true
|
|
77
|
-
)
|
|
78
|
-
after_tokens = Rubik::Prompt::TokenEstimator.estimate_history(ctx.conversation_history)
|
|
79
|
-
delta = before_tokens - after_tokens
|
|
80
|
-
ctx.out.puts "Context compaction completed: #{before_tokens} -> #{after_tokens} tokens (reduced #{delta} tokens)."
|
|
40
|
+
handle_context_compact(ctx)
|
|
81
41
|
when "stats"
|
|
82
|
-
|
|
83
|
-
p_name = spec["provider"]
|
|
84
|
-
m_name = spec["name"]
|
|
85
|
-
context_limit = ctx.llm_registry.context_limit_for(p_name, m_name)
|
|
86
|
-
max_output = ctx.llm_registry.max_output_tokens_for(p_name, m_name)
|
|
87
|
-
ceiling = ctx.llm_registry.effective_input_limit_for(p_name, m_name)
|
|
88
|
-
est_tokens = Rubik::Prompt::TokenEstimator.estimate_history(ctx.conversation_history)
|
|
89
|
-
pct = ceiling.positive? ? ((est_tokens.to_f / ceiling) * 100).clamp(0.0, 100.0).round(1) : 0.0
|
|
90
|
-
summary = ctx.context_index.status_summary
|
|
91
|
-
ast_symbols_count = ctx.ast_graph ? ctx.ast_graph.symbols.size : 0
|
|
92
|
-
|
|
93
|
-
ctx.out.puts "=== Context & Token Statistics ==="
|
|
94
|
-
ctx.out.puts " • Model : #{m_name} (#{p_name})"
|
|
95
|
-
ctx.out.puts " • Context Limit : #{context_limit} tokens"
|
|
96
|
-
ctx.out.puts " • Usable Capacity : #{ceiling} tokens (Max Output: #{max_output} tokens)"
|
|
97
|
-
ctx.out.puts " • Consumed Tokens : #{est_tokens} tokens (#{pct}% of usable capacity)"
|
|
98
|
-
ctx.out.puts " • Active Turns : #{ctx.conversation_history.size} turn(s)"
|
|
99
|
-
ctx.out.puts " • Indexed Files : #{summary[:file_count]} files"
|
|
100
|
-
ctx.out.puts " • AST Symbol Count : #{ast_symbols_count} symbols"
|
|
101
|
-
ctx.out.puts "=================================="
|
|
102
|
-
guidance = Rubik::Tools::DiagnosticReporter.render_context_status_guidance(
|
|
103
|
-
{ model: "#{p_name}/#{m_name}", consumed_tokens: est_tokens, context_limit: ceiling },
|
|
104
|
-
token_breakdown: { "Conversation Turns" => est_tokens }
|
|
105
|
-
)
|
|
106
|
-
ctx.out.puts "\n#{guidance}"
|
|
42
|
+
handle_context_stats(ctx)
|
|
107
43
|
when "status", nil
|
|
108
|
-
|
|
109
|
-
p_name = spec["provider"]
|
|
110
|
-
m_name = spec["name"]
|
|
111
|
-
context_limit = ctx.llm_registry.context_limit_for(p_name, m_name)
|
|
112
|
-
max_output = ctx.llm_registry.max_output_tokens_for(p_name, m_name)
|
|
113
|
-
ceiling = ctx.llm_registry.effective_input_limit_for(p_name, m_name)
|
|
114
|
-
context_cfg = ctx.config.fetch("context", {})
|
|
115
|
-
strategy = context_cfg["compaction_strategy"] || "hybrid"
|
|
116
|
-
|
|
117
|
-
est_tokens = Rubik::Prompt::TokenEstimator.estimate_history(ctx.conversation_history)
|
|
118
|
-
pct = ceiling.positive? ? ((est_tokens.to_f / ceiling) * 100).clamp(0.0, 100.0).round(1) : 0.0
|
|
119
|
-
bar_width = 12
|
|
120
|
-
filled = ((pct / 100.0) * bar_width).round
|
|
121
|
-
empty = bar_width - filled
|
|
122
|
-
bar_str = "[#{'█' * filled}#{'░' * empty}] #{pct}%"
|
|
123
|
-
|
|
124
|
-
summary = ctx.context_index.status_summary
|
|
125
|
-
ast_symbols_count = ctx.ast_graph ? ctx.ast_graph.symbols.size : 0
|
|
126
|
-
|
|
127
|
-
ctx.out.puts "=== LLM Context Window & In-Memory Status ==="
|
|
128
|
-
ctx.out.puts "Active Model: #{m_name} (#{p_name})"
|
|
129
|
-
ctx.out.puts "Context Window Limit: #{context_limit} tokens"
|
|
130
|
-
ctx.out.puts "Max Output Tokens: #{max_output} tokens"
|
|
131
|
-
ctx.out.puts "Usable Input Ceiling: #{ceiling} tokens"
|
|
132
|
-
ctx.out.puts "Estimated Prompt Tokens: #{est_tokens} / #{ceiling} tokens #{bar_str}"
|
|
133
|
-
ctx.out.puts "Compaction Strategy: #{strategy}"
|
|
134
|
-
ctx.out.puts "Indexed Files: #{summary[:file_count]}, AST Symbols: #{ast_symbols_count}"
|
|
135
|
-
ctx.out.puts "============================================="
|
|
136
|
-
if (args && (args.include?("--guidance") || args.include?("-g"))) || est_tokens > (ceiling * 0.8)
|
|
137
|
-
guidance = Rubik::Tools::DiagnosticReporter.render_context_status_guidance(
|
|
138
|
-
{ model: "#{p_name}/#{m_name}", consumed_tokens: est_tokens, context_limit: ceiling },
|
|
139
|
-
token_breakdown: { "Conversation History" => est_tokens }
|
|
140
|
-
)
|
|
141
|
-
ctx.out.puts "\n#{guidance}"
|
|
142
|
-
end
|
|
44
|
+
handle_context_status(args, ctx)
|
|
143
45
|
else
|
|
144
46
|
ctx.out.puts USAGE_CONTEXT
|
|
145
47
|
end
|
|
146
48
|
end
|
|
147
49
|
end
|
|
148
50
|
|
|
51
|
+
private_class_method def self.handle_context_scan(ctx)
|
|
52
|
+
ctx.context_index.full_scan
|
|
53
|
+
ctx.warm_ast_graph(async: false)
|
|
54
|
+
ctx.out.puts "Context index and AST symbol graph updated."
|
|
55
|
+
rescue Errno::EACCES => e
|
|
56
|
+
root_path = begin
|
|
57
|
+
ctx.buffer_manager.root_dir
|
|
58
|
+
rescue
|
|
59
|
+
Dir.pwd
|
|
60
|
+
end
|
|
61
|
+
ctx.out.puts Rubik::Tools::DiagnosticReporter.render_permission_denied(root_path, operation: "scan", reason: e.message)
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
ctx.out.puts "Context scan failed: #{e.message}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private_class_method def self.handle_context_add(args, ctx)
|
|
67
|
+
file = args[1]
|
|
68
|
+
if file
|
|
69
|
+
ctx.context_index.update_file(file)
|
|
70
|
+
full_path = File.join(ctx.buffer_manager.root_dir, file)
|
|
71
|
+
ctx.ast_graph.index_file(full_path) if File.exist?(full_path)
|
|
72
|
+
end
|
|
73
|
+
ctx.out.puts "Added #{file} to context index and AST graph."
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private_class_method def self.handle_context_drop(args, ctx)
|
|
77
|
+
file = args[1]
|
|
78
|
+
if file
|
|
79
|
+
ctx.context_index.files.delete(file)
|
|
80
|
+
ctx.out.puts "Removed #{file} from context index."
|
|
81
|
+
else
|
|
82
|
+
ctx.out.puts "Usage: !context drop <relative_file_path>"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private_class_method def self.handle_context_compact(ctx)
|
|
87
|
+
before_tokens = Rubik::Prompt::TokenEstimator.estimate_history(ctx.conversation_history)
|
|
88
|
+
spec = ctx.current_model_spec
|
|
89
|
+
context_limit = ctx.llm_registry.context_limit_for(spec["provider"], spec["name"])
|
|
90
|
+
max_output = ctx.llm_registry.max_output_tokens_for(spec["provider"], spec["name"])
|
|
91
|
+
context_cfg = ctx.config.fetch("context", {})
|
|
92
|
+
|
|
93
|
+
ctx.conversation_history = Rubik::Prompt::ContextCompactor.compact_history(
|
|
94
|
+
ctx.conversation_history,
|
|
95
|
+
context_limit: context_limit,
|
|
96
|
+
max_output_tokens: max_output,
|
|
97
|
+
config: context_cfg,
|
|
98
|
+
force: true
|
|
99
|
+
)
|
|
100
|
+
after_tokens = Rubik::Prompt::TokenEstimator.estimate_history(ctx.conversation_history)
|
|
101
|
+
delta = before_tokens - after_tokens
|
|
102
|
+
ctx.out.puts "Context compaction completed: #{before_tokens} -> #{after_tokens} tokens (reduced #{delta} tokens)."
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private_class_method def self.handle_context_stats(ctx)
|
|
106
|
+
spec = ctx.current_model_spec
|
|
107
|
+
p_name = spec["provider"]
|
|
108
|
+
m_name = spec["name"]
|
|
109
|
+
context_limit = ctx.llm_registry.context_limit_for(p_name, m_name)
|
|
110
|
+
max_output = ctx.llm_registry.max_output_tokens_for(p_name, m_name)
|
|
111
|
+
ceiling = ctx.llm_registry.effective_input_limit_for(p_name, m_name)
|
|
112
|
+
est_tokens = Rubik::Prompt::TokenEstimator.estimate_history(ctx.conversation_history)
|
|
113
|
+
pct = ceiling.positive? ? ((est_tokens.to_f / ceiling) * 100).clamp(0.0, 100.0).round(1) : 0.0
|
|
114
|
+
summary = ctx.context_index.status_summary
|
|
115
|
+
ast_symbols_count = ctx.ast_graph ? ctx.ast_graph.symbols.size : 0
|
|
116
|
+
|
|
117
|
+
ctx.out.puts "=== Context & Token Statistics ==="
|
|
118
|
+
ctx.out.puts " • Model : #{m_name} (#{p_name})"
|
|
119
|
+
ctx.out.puts " • Context Limit : #{context_limit} tokens"
|
|
120
|
+
ctx.out.puts " • Usable Capacity : #{ceiling} tokens (Max Output: #{max_output} tokens)"
|
|
121
|
+
ctx.out.puts " • Consumed Tokens : #{est_tokens} tokens (#{pct}% of usable capacity)"
|
|
122
|
+
ctx.out.puts " • Active Turns : #{ctx.conversation_history.size} turn(s)"
|
|
123
|
+
ctx.out.puts " • Indexed Files : #{summary[:file_count]} files"
|
|
124
|
+
ctx.out.puts " • AST Symbol Count : #{ast_symbols_count} symbols"
|
|
125
|
+
ctx.out.puts "=================================="
|
|
126
|
+
guidance = Rubik::Tools::DiagnosticReporter.render_context_status_guidance(
|
|
127
|
+
{ model: "#{p_name}/#{m_name}", consumed_tokens: est_tokens, context_limit: ceiling },
|
|
128
|
+
token_breakdown: { "Conversation Turns" => est_tokens }
|
|
129
|
+
)
|
|
130
|
+
ctx.out.puts "\n#{guidance}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private_class_method def self.handle_context_status(args, ctx)
|
|
134
|
+
spec = ctx.current_model_spec
|
|
135
|
+
p_name = spec["provider"]
|
|
136
|
+
m_name = spec["name"]
|
|
137
|
+
context_limit = ctx.llm_registry.context_limit_for(p_name, m_name)
|
|
138
|
+
max_output = ctx.llm_registry.max_output_tokens_for(p_name, m_name)
|
|
139
|
+
ceiling = ctx.llm_registry.effective_input_limit_for(p_name, m_name)
|
|
140
|
+
context_cfg = ctx.config.fetch("context", {})
|
|
141
|
+
strategy = context_cfg["compaction_strategy"] || "hybrid"
|
|
142
|
+
|
|
143
|
+
est_tokens = Rubik::Prompt::TokenEstimator.estimate_history(ctx.conversation_history)
|
|
144
|
+
pct = ceiling.positive? ? ((est_tokens.to_f / ceiling) * 100).clamp(0.0, 100.0).round(1) : 0.0
|
|
145
|
+
bar_width = 12
|
|
146
|
+
filled = ((pct / 100.0) * bar_width).round
|
|
147
|
+
empty = bar_width - filled
|
|
148
|
+
bar_str = "[#{'█' * filled}#{'░' * empty}] #{pct}%"
|
|
149
|
+
|
|
150
|
+
summary = ctx.context_index.status_summary
|
|
151
|
+
ast_symbols_count = ctx.ast_graph ? ctx.ast_graph.symbols.size : 0
|
|
152
|
+
|
|
153
|
+
ctx.out.puts "=== LLM Context Window & In-Memory Status ==="
|
|
154
|
+
ctx.out.puts "Active Model: #{m_name} (#{p_name})"
|
|
155
|
+
ctx.out.puts "Context Window Limit: #{context_limit} tokens"
|
|
156
|
+
ctx.out.puts "Max Output Tokens: #{max_output} tokens"
|
|
157
|
+
ctx.out.puts "Usable Input Ceiling: #{ceiling} tokens"
|
|
158
|
+
ctx.out.puts "Estimated Prompt Tokens: #{est_tokens} / #{ceiling} tokens #{bar_str}"
|
|
159
|
+
ctx.out.puts "Compaction Strategy: #{strategy}"
|
|
160
|
+
ctx.out.puts "Indexed Files: #{summary[:file_count]}, AST Symbols: #{ast_symbols_count}"
|
|
161
|
+
ctx.out.puts "============================================="
|
|
162
|
+
return unless (args && (args.include?("--guidance") || args.include?("-g"))) || est_tokens > (ceiling * 0.8)
|
|
163
|
+
|
|
164
|
+
guidance = Rubik::Tools::DiagnosticReporter.render_context_status_guidance(
|
|
165
|
+
{ model: "#{p_name}/#{m_name}", consumed_tokens: est_tokens, context_limit: ceiling },
|
|
166
|
+
token_breakdown: { "Conversation History" => est_tokens }
|
|
167
|
+
)
|
|
168
|
+
ctx.out.puts "\n#{guidance}"
|
|
169
|
+
end
|
|
170
|
+
|
|
149
171
|
private_class_method def self.register_steps_command(shell)
|
|
150
172
|
shell.register_command("steps", description: "Manage or view agentic step budget (default|goal|plan|<n>|status)",
|
|
151
173
|
usage: USAGE_STEPS, audience: :both) do |args, ctx|
|
|
@@ -208,75 +208,16 @@ module Rubik
|
|
|
208
208
|
# çıktısını bayt-baytına korur.
|
|
209
209
|
private_class_method def self.drive_goal_turn(ctx, orchestrator:, goal_state:, goal_ledger:,
|
|
210
210
|
goal_text:, goal_id:, record_pair:, start:)
|
|
211
|
-
finalize_ledger = lambda do |status|
|
|
212
|
-
next unless goal_ledger
|
|
213
|
-
|
|
214
|
-
goal_ledger.finalize!(status)
|
|
215
|
-
goal_state.ledger_id = goal_ledger.snapshot[:storage_id]
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
fail_goal = lambda do |reason, tokens: nil, steps: nil|
|
|
219
|
-
sanitized = Rubik::Platform.safe_utf8(reason.to_s)
|
|
220
|
-
steps_used = steps || goal_state.step_count
|
|
221
|
-
goal_state.fail!(sanitized, tokens: tokens)
|
|
222
|
-
finalize_ledger.call(:failed)
|
|
223
|
-
forget_suspended_goal(ctx, goal_id)
|
|
224
|
-
ctx.out.puts "\n❌ Goal could not be completed: #{sanitized}"
|
|
225
|
-
record_pair.call("[Goal attempt ended after #{steps_used} steps: #{sanitized}]", tokens: tokens)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
suspend_goal = lambda do |reason, tokens: nil|
|
|
229
|
-
attempt = suspended_attempt(ctx, goal_id) + 1
|
|
230
|
-
goal_state.suspend!(reason, tokens: tokens)
|
|
231
|
-
goal_ledger&.suspend!(suspend_stamp_meta(goal_state, goal_id, reason, attempt))
|
|
232
|
-
registry = goal_registry_for(ctx)
|
|
233
|
-
registry.register(Rubik::Orchestrator::GoalRegistry::Entry.new(
|
|
234
|
-
goal_id: goal_id, goal_text: goal_text, loop: orchestrator,
|
|
235
|
-
goal_state: goal_state, ledger: goal_ledger, reason: reason.to_s,
|
|
236
|
-
suspended_at: Time.now, attempt: attempt
|
|
237
|
-
))
|
|
238
|
-
ctx.out.puts "\n⏸️ [Rubik::Goal] Suspended after #{goal_state.step_count} steps (transient provider error): #{reason}"
|
|
239
|
-
ctx.out.puts " Loop, counters and Σ ledger are preserved — resume with `!goal resume`."
|
|
240
|
-
record_pair.call("[Goal suspended after #{goal_state.step_count} steps: #{reason}]", tokens: tokens)
|
|
241
|
-
end
|
|
242
|
-
|
|
243
211
|
ctx.active_loop = orchestrator
|
|
244
212
|
begin
|
|
245
213
|
res = start.call(orchestrator)
|
|
246
|
-
|
|
247
|
-
# Bütçe tükenince sor (execute_free_text simetrisi; aynı knob,
|
|
248
|
-
# aynı +20). continue_loop boyunca active_loop DOLU kalmalı:
|
|
249
|
-
# notice yönlendirmesi aktif döngüye bakar (lifecycle.rb
|
|
250
|
-
# route_subagent_notice) — soruya beklerken gelen subagent
|
|
251
|
-
# olayları loop inbox'a girer ve devam adımında boşaltılır.
|
|
252
|
-
auto_extensions = 0
|
|
253
|
-
max_auto = ctx.config.fetch("agent.goal_auto_continue_limit", 0).to_i
|
|
254
|
-
while !res[:ok] && res[:error]&.include?("Max steps budget exceeded")
|
|
255
|
-
if auto_extensions < max_auto
|
|
256
|
-
auto_extensions += 1
|
|
257
|
-
ctx.out.puts "\n🤖 [Rubik::Goal] Adım bütçesi doldu (#{res[:error]}). " \
|
|
258
|
-
"Otonom devam hakkı (#{auto_extensions}/#{max_auto}) kullanılarak +20 adım ekleniyor..."
|
|
259
|
-
goal_state.extend!(goal_state.max_steps + 20)
|
|
260
|
-
res = orchestrator.continue_loop(20, auto: true)
|
|
261
|
-
next
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
break unless ctx.config.fetch("agent.ask_continue_on_limit", true)
|
|
265
|
-
|
|
266
|
-
ctx.out.puts "\n⚠️ [Rubik::Goal] #{res[:error]}"
|
|
267
|
-
ans = ctx.ask_yes_no(" [Rubik::Goal] +20 adım ile devam edilsin mi?", default: "n")
|
|
268
|
-
break unless ["y", "yes", "evet"].include?(ans)
|
|
269
|
-
|
|
270
|
-
ctx.out.puts " [Rubik::Goal] Adım bütçesi +20 artırılıyor ve hedef sürdürülüyor..."
|
|
271
|
-
goal_state.extend!(goal_state.max_steps + 20)
|
|
272
|
-
res = orchestrator.continue_loop(20)
|
|
273
|
-
end
|
|
214
|
+
res = handle_goal_budget_extensions(ctx, orchestrator: orchestrator, goal_state: goal_state, res: res)
|
|
274
215
|
rescue Interrupt
|
|
275
216
|
# ^C hedefi 'running' bırakmasın; eşlenmiş kayıt otursun. Interrupt
|
|
276
217
|
# yeniden yükselir — kullanıcı mesajını execute_command basar.
|
|
277
218
|
# Resume üstüne ^C = askıyı terk (terminal :stopped damgası).
|
|
278
219
|
goal_state.stop!("Interrupted by user (^C)")
|
|
279
|
-
|
|
220
|
+
finalize_goal_ledger(goal_ledger, goal_state, :stopped)
|
|
280
221
|
forget_suspended_goal(ctx, goal_id)
|
|
281
222
|
record_pair.call("[Goal attempt interrupted after #{goal_state.step_count} steps]")
|
|
282
223
|
raise
|
|
@@ -289,18 +230,92 @@ module Rubik
|
|
|
289
230
|
# next yerine return: rescue sonrası res'siz terminal
|
|
290
231
|
# eşlemesine düşülmesin.
|
|
291
232
|
if e.respond_to?(:transient?) && e.transient?
|
|
292
|
-
|
|
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)
|
|
293
235
|
else
|
|
294
|
-
|
|
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)
|
|
295
238
|
end
|
|
296
239
|
return
|
|
297
240
|
rescue StandardError => e
|
|
298
|
-
|
|
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}")
|
|
299
243
|
return
|
|
300
244
|
ensure
|
|
301
245
|
ctx.active_loop = nil
|
|
302
246
|
end
|
|
303
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:)
|
|
304
319
|
if res[:ok]
|
|
305
320
|
active = goal_active_subagent_count(ctx)
|
|
306
321
|
if active.positive?
|
|
@@ -310,12 +325,12 @@ module Rubik
|
|
|
310
325
|
# "Goal Achieved" (field incident 2026-10-05).
|
|
311
326
|
reason = "Goal loop concluded with #{active} active subagent(s) still running — not marked complete"
|
|
312
327
|
goal_state.stop!(reason)
|
|
313
|
-
|
|
328
|
+
finalize_goal_ledger(goal_ledger, goal_state, :stopped)
|
|
314
329
|
ctx.out.puts "\n⚠️ [Rubik::Goal] #{reason}."
|
|
315
330
|
record_pair.call("[Goal attempt ended after #{goal_state.step_count} steps: #{reason}]", tokens: res[:tokens])
|
|
316
331
|
else
|
|
317
332
|
goal_state.complete!(res[:output].to_s, tokens: res[:tokens])
|
|
318
|
-
|
|
333
|
+
finalize_goal_ledger(goal_ledger, goal_state, :completed)
|
|
319
334
|
forget_suspended_goal(ctx, goal_id)
|
|
320
335
|
ctx.out.puts "\n🎉 Goal Achieved in #{res[:steps]} steps!"
|
|
321
336
|
ctx.out.puts res[:output]
|
|
@@ -329,10 +344,10 @@ module Rubik
|
|
|
329
344
|
# Başarısızlık eşlemesi yukarıdaki tek hata kanalından akar:
|
|
330
345
|
# GoalState → ledger finalize → kayıtçı çifti, istisna yoluyla
|
|
331
346
|
# aynı sırayla.
|
|
332
|
-
|
|
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])
|
|
333
349
|
end
|
|
334
350
|
end
|
|
335
|
-
|
|
336
351
|
# ------------------------------------------------------------------
|
|
337
352
|
# !goal resume (issue #106). İki yol, dönüşüm sırasına göre:
|
|
338
353
|
# 1) Oturum kaydında canlı girdi varsa süreç-içi resume — hayatta
|