rubino-agent 0.3.0 → 0.5.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/.rubocop_todo.yml +11 -2
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +172 -5
- data/CONTRIBUTING.md +10 -1
- data/README.md +14 -5
- data/Rakefile +31 -0
- data/docs/agents.md +42 -23
- data/docs/architecture.md +2 -2
- data/docs/commands.md +35 -3
- data/docs/configuration.md +20 -23
- data/docs/getting-started.md +5 -3
- data/docs/security.md +16 -5
- data/docs/skills.md +31 -0
- data/docs/troubleshooting.md +1 -1
- data/exe/rubino +16 -2
- data/install.sh +721 -59
- data/lib/rubino/active_agent.rb +73 -0
- data/lib/rubino/agent/action_claim_guard.rb +881 -0
- data/lib/rubino/agent/agent_registry.rb +5 -2
- data/lib/rubino/agent/definition.rb +1 -9
- data/lib/rubino/agent/fallback_chain.rb +0 -6
- data/lib/rubino/agent/iteration_budget.rb +109 -3
- data/lib/rubino/agent/loop.rb +476 -20
- data/lib/rubino/agent/model_call_runner.rb +81 -3
- data/lib/rubino/agent/prompts/build.txt +22 -5
- data/lib/rubino/agent/response_validator.rb +8 -0
- data/lib/rubino/agent/runner.rb +133 -8
- data/lib/rubino/agent/tool_executor.rb +166 -14
- data/lib/rubino/agent/truncation_continuation.rb +4 -1
- data/lib/rubino/api/server.rb +19 -0
- data/lib/rubino/attachments/classify.rb +35 -17
- data/lib/rubino/boot/config_guard.rb +71 -0
- data/lib/rubino/cli/chat/completion_builder.rb +42 -6
- data/lib/rubino/cli/chat/idle_card_host.rb +7 -1
- data/lib/rubino/cli/chat/session_resolver.rb +87 -21
- data/lib/rubino/cli/chat_command.rb +1189 -50
- data/lib/rubino/cli/commands.rb +282 -2
- data/lib/rubino/cli/config_command.rb +68 -8
- data/lib/rubino/cli/doctor_command.rb +204 -12
- data/lib/rubino/cli/jobs_command.rb +12 -0
- data/lib/rubino/cli/memory_command.rb +53 -20
- data/lib/rubino/cli/onboarding_wizard.rb +79 -6
- data/lib/rubino/cli/session_command.rb +172 -18
- data/lib/rubino/cli/setup_command.rb +131 -8
- data/lib/rubino/cli/skills_command.rb +183 -9
- data/lib/rubino/cli/trust_gate.rb +16 -7
- data/lib/rubino/commands/built_ins.rb +2 -0
- data/lib/rubino/commands/command.rb +12 -2
- data/lib/rubino/commands/executor.rb +149 -12
- data/lib/rubino/commands/handlers/agent_switch.rb +100 -0
- data/lib/rubino/commands/handlers/agents.rb +156 -41
- data/lib/rubino/commands/handlers/config.rb +4 -1
- data/lib/rubino/commands/handlers/help.rb +113 -14
- data/lib/rubino/commands/handlers/memory.rb +15 -5
- data/lib/rubino/commands/handlers/sessions.rb +26 -3
- data/lib/rubino/commands/handlers/status.rb +9 -4
- data/lib/rubino/commands/loader.rb +12 -0
- data/lib/rubino/config/configuration.rb +86 -24
- data/lib/rubino/config/defaults.rb +140 -33
- data/lib/rubino/config/loader.rb +62 -12
- data/lib/rubino/config/validator.rb +341 -0
- data/lib/rubino/config/writer.rb +123 -31
- data/lib/rubino/context/compressor.rb +184 -22
- data/lib/rubino/context/environment_inspector.rb +2 -2
- data/lib/rubino/context/file_discovery.rb +2 -2
- data/lib/rubino/context/message_boundary.rb +27 -1
- data/lib/rubino/context/project_languages.rb +90 -0
- data/lib/rubino/context/prompt_assembler.rb +105 -22
- data/lib/rubino/context/summary_builder.rb +45 -4
- data/lib/rubino/context/token_budget.rb +36 -11
- data/lib/rubino/context/token_estimate.rb +45 -0
- data/lib/rubino/context/tool_result_pruner.rb +81 -0
- data/lib/rubino/database/connection.rb +154 -3
- data/lib/rubino/database/migrations/001_create_initial_schema.rb +314 -40
- data/lib/rubino/database/migrator.rb +98 -5
- data/lib/rubino/documents/cap_exceeded.rb +13 -0
- data/lib/rubino/documents/converters/csv.rb +4 -3
- data/lib/rubino/documents/converters/docx.rb +29 -5
- data/lib/rubino/documents/converters/html.rb +5 -1
- data/lib/rubino/documents/converters/json.rb +2 -1
- data/lib/rubino/documents/converters/pdf.rb +11 -2
- data/lib/rubino/documents/converters/plain.rb +2 -1
- data/lib/rubino/documents/converters/pptx.rb +11 -2
- data/lib/rubino/documents/converters/xlsx.rb +35 -4
- data/lib/rubino/documents/converters/xml.rb +2 -1
- data/lib/rubino/documents/limits.rb +210 -0
- data/lib/rubino/documents.rb +10 -3
- data/lib/rubino/errors.rb +36 -5
- data/lib/rubino/interaction/cancel_token.rb +19 -3
- data/lib/rubino/interaction/events.rb +13 -0
- data/lib/rubino/interaction/lifecycle.rb +99 -13
- data/lib/rubino/interaction/polishing.rb +176 -0
- data/lib/rubino/jobs/cron_job_repository.rb +5 -8
- data/lib/rubino/jobs/handlers/cleanup_sessions_job.rb +11 -0
- data/lib/rubino/jobs/handlers/distill_skill_job.rb +65 -9
- data/lib/rubino/jobs/queue.rb +63 -8
- data/lib/rubino/jobs/runner.rb +24 -6
- data/lib/rubino/jobs/worker.rb +0 -4
- data/lib/rubino/llm/adapter_response.rb +47 -4
- data/lib/rubino/llm/credential_check.rb +15 -16
- data/lib/rubino/llm/error_classifier.rb +89 -1
- data/lib/rubino/llm/inline_think_filter.rb +69 -12
- data/lib/rubino/llm/request.rb +30 -3
- data/lib/rubino/llm/ruby_llm_adapter.rb +394 -46
- data/lib/rubino/llm/tool_bridge.rb +113 -9
- data/lib/rubino/mcp/manager.rb +18 -1
- data/lib/rubino/mcp/mcp_tool_wrapper.rb +14 -3
- data/lib/rubino/memory/aux_retry.rb +107 -0
- data/lib/rubino/memory/backends/sqlite.rb +73 -44
- data/lib/rubino/memory/backends.rb +23 -7
- data/lib/rubino/memory/salience_gate.rb +103 -0
- data/lib/rubino/memory/sqlite_extraction.rb +70 -0
- data/lib/rubino/memory/sqlite_extraction_prompt.rb +11 -0
- data/lib/rubino/memory/store.rb +33 -5
- data/lib/rubino/memory/threat_scanner.rb +52 -0
- data/lib/rubino/output/cost.rb +52 -0
- data/lib/rubino/output/headless_block_latch.rb +53 -0
- data/lib/rubino/output/result_serializer.rb +222 -0
- data/lib/rubino/output/turn_recorder.rb +77 -0
- data/lib/rubino/security/approval_policy.rb +227 -32
- data/lib/rubino/security/command_allowlist.rb +79 -4
- data/lib/rubino/security/doom_loop_detector.rb +21 -2
- data/lib/rubino/security/hardline_guard.rb +189 -16
- data/lib/rubino/security/pattern_matcher.rb +28 -5
- data/lib/rubino/security/prefix_deriver.rb +25 -6
- data/lib/rubino/security/readonly_commands.rb +145 -5
- data/lib/rubino/security/secret_path.rb +134 -0
- data/lib/rubino/security/url_safety.rb +255 -0
- data/lib/rubino/session/repository.rb +212 -11
- data/lib/rubino/session/store.rb +139 -14
- data/lib/rubino/skills/installer.rb +230 -0
- data/lib/rubino/skills/prompt_index.rb +2 -2
- data/lib/rubino/skills/registry.rb +52 -1
- data/lib/rubino/skills/skill.rb +64 -3
- data/lib/rubino/skills/skill_tool.rb +16 -5
- data/lib/rubino/tools/background_tasks.rb +157 -13
- data/lib/rubino/tools/base.rb +204 -3
- data/lib/rubino/tools/edit_tool.rb +73 -18
- data/lib/rubino/tools/glob_tool.rb +48 -9
- data/lib/rubino/tools/grep_tool.rb +103 -9
- data/lib/rubino/tools/multi_edit_tool.rb +64 -9
- data/lib/rubino/tools/patch_tool.rb +5 -0
- data/lib/rubino/tools/read_attachment_tool.rb +3 -1
- data/lib/rubino/tools/read_tool.rb +33 -15
- data/lib/rubino/tools/read_tracker.rb +153 -35
- data/lib/rubino/tools/registry.rb +113 -12
- data/lib/rubino/tools/result.rb +9 -1
- data/lib/rubino/tools/ruby_tool.rb +0 -0
- data/lib/rubino/tools/shell_registry.rb +70 -0
- data/lib/rubino/tools/shell_tool.rb +40 -1
- data/lib/rubino/tools/summarize_file_tool.rb +6 -0
- data/lib/rubino/tools/task_stop_tool.rb +10 -16
- data/lib/rubino/tools/task_tool.rb +36 -8
- data/lib/rubino/tools/vision_tool.rb +5 -0
- data/lib/rubino/tools/webfetch_tool.rb +39 -7
- data/lib/rubino/tools/websearch_tool.rb +92 -30
- data/lib/rubino/tools/write_tool.rb +23 -4
- data/lib/rubino/ui/api.rb +10 -1
- data/lib/rubino/ui/base.rb +11 -0
- data/lib/rubino/ui/bottom_composer.rb +382 -74
- data/lib/rubino/ui/cli.rb +515 -83
- data/lib/rubino/ui/completion_menu.rb +11 -7
- data/lib/rubino/ui/headless_trace.rb +63 -0
- data/lib/rubino/ui/live_region.rb +70 -7
- data/lib/rubino/ui/markdown_renderer.rb +142 -7
- data/lib/rubino/ui/notifier.rb +0 -2
- data/lib/rubino/ui/null.rb +52 -5
- data/lib/rubino/ui/paste_store.rb +16 -2
- data/lib/rubino/ui/queued_indicators.rb +6 -1
- data/lib/rubino/ui/status_bar.rb +61 -7
- data/lib/rubino/ui/streaming_markdown.rb +59 -6
- data/lib/rubino/ui/subagent_view.rb +29 -4
- data/lib/rubino/ui/tool_label.rb +52 -0
- data/lib/rubino/update_check.rb +39 -4
- data/lib/rubino/util/atomic_file.rb +117 -0
- data/lib/rubino/util/ignore_rules.rb +120 -0
- data/lib/rubino/util/output.rb +229 -12
- data/lib/rubino/util/secrets_mask.rb +70 -7
- data/lib/rubino/util/spill_store.rb +153 -0
- data/lib/rubino/version.rb +1 -1
- data/lib/rubino/workspace.rb +9 -1
- data/lib/rubino.rb +191 -7
- data/rubino-agent.gemspec +1 -0
- data/skills/ruby-expert/SKILL.md +1 -0
- metadata +42 -12
- data/lib/rubino/agent/router.rb +0 -65
- data/lib/rubino/database/migrations/002_create_runs.rb +0 -45
- data/lib/rubino/database/migrations/003_create_skill_states.rb +0 -15
- data/lib/rubino/database/migrations/004_create_cron_jobs.rb +0 -36
- data/lib/rubino/database/migrations/005_create_oauth_connections.rb +0 -27
- data/lib/rubino/database/migrations/006_create_webhook_deliveries.rb +0 -34
- data/lib/rubino/database/migrations/007_create_messages_fts.rb +0 -59
- data/lib/rubino/database/migrations/008_create_memory_facts.rb +0 -75
- data/lib/rubino/database/migrations/009_create_memory_graph.rb +0 -55
- data/lib/rubino/database/migrations/010_add_owner_pid_to_sessions.rb +0 -20
|
@@ -110,10 +110,13 @@ module Rubino
|
|
|
110
110
|
return override if override
|
|
111
111
|
|
|
112
112
|
path = File.join(PROMPTS_DIR, "#{name}.txt")
|
|
113
|
-
|
|
113
|
+
# Read as UTF-8 explicitly: the built-in prompts carry non-ASCII glyphs
|
|
114
|
+
# (em-dashes), so relying on the locale default_external crashes under a
|
|
115
|
+
# bare C/POSIX locale (#273; consistent with #250/#251).
|
|
116
|
+
File.exist?(path) ? File.read(path, encoding: "UTF-8").strip : ""
|
|
114
117
|
rescue StandardError
|
|
115
118
|
path = File.join(PROMPTS_DIR, "#{name}.txt")
|
|
116
|
-
File.exist?(path) ? File.read(path).strip : ""
|
|
119
|
+
File.exist?(path) ? File.read(path, encoding: "UTF-8").strip : ""
|
|
117
120
|
end
|
|
118
121
|
end
|
|
119
122
|
end
|
|
@@ -9,8 +9,6 @@ module Rubino
|
|
|
9
9
|
:permissions, :tools, :hidden
|
|
10
10
|
|
|
11
11
|
# Types: :primary (user-switchable), :subagent (invokable), :utility (hidden)
|
|
12
|
-
TYPES = %i[primary subagent utility].freeze
|
|
13
|
-
|
|
14
12
|
def initialize(attrs = {})
|
|
15
13
|
@name = attrs[:name]
|
|
16
14
|
@type = attrs[:type] || :primary
|
|
@@ -32,10 +30,6 @@ module Rubino
|
|
|
32
30
|
@type == :subagent
|
|
33
31
|
end
|
|
34
32
|
|
|
35
|
-
def utility?
|
|
36
|
-
@type == :utility
|
|
37
|
-
end
|
|
38
|
-
|
|
39
33
|
def hidden?
|
|
40
34
|
@hidden
|
|
41
35
|
end
|
|
@@ -72,9 +66,7 @@ module Rubino
|
|
|
72
66
|
# its companions `task_result`/`task_stop`) so it can spawn its own
|
|
73
67
|
# subagents. Runaway recursion / fan-out is no longer prevented by hiding
|
|
74
68
|
# the tool here — it is bounded in ONE place, Tools::BackgroundTasks#reserve,
|
|
75
|
-
# by the depth / per-owner / global caps.
|
|
76
|
-
# named set for any reader that still wants to reason about the group.)
|
|
77
|
-
DELEGATION_TOOLS = %w[task task_result task_stop].freeze
|
|
69
|
+
# by the depth / per-owner / global caps.
|
|
78
70
|
|
|
79
71
|
# Tools that ONLY make sense for a subagent and must be hidden from a
|
|
80
72
|
# primary/top-level agent. ask_parent escalates a question to the PARENT — a
|
|
@@ -68,12 +68,6 @@ module Rubino
|
|
|
68
68
|
@active
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
# True once a fallback has been activated this turn — lets callers emit the
|
|
72
|
-
# "switched to fallback" status only when something actually changed.
|
|
73
|
-
def active?
|
|
74
|
-
@index.positive?
|
|
75
|
-
end
|
|
76
|
-
|
|
77
71
|
# Advance to the next usable, non-duplicate fallback and rebuild the
|
|
78
72
|
# adapter. Returns true if it actually switched, false when the chain is
|
|
79
73
|
# exhausted (or empty). Mirrors try_activate_fallback (helpers.py:1020):
|
|
@@ -4,14 +4,30 @@ module Rubino
|
|
|
4
4
|
module Agent
|
|
5
5
|
# Manages turn and iteration budgets to prevent runaway loops.
|
|
6
6
|
class IterationBudget
|
|
7
|
+
# Upper bound on a turn/iteration cap (F2). A 0/negative cap is rejected as
|
|
8
|
+
# "would never run"; a value this large is just as nonsensical the other way
|
|
9
|
+
# — `--max-turns 99999999999999` was silently accepted, defeating the whole
|
|
10
|
+
# point of a runaway guard. Reject anything above this sane ceiling with the
|
|
11
|
+
# same clear message rather than letting an effectively-unbounded cap
|
|
12
|
+
# through. 10_000 is far past any legitimate agentic turn yet small enough
|
|
13
|
+
# to keep the comparison meaningful.
|
|
14
|
+
MAX_CAP = 10_000
|
|
15
|
+
|
|
7
16
|
def initialize(config: nil, max_tool_iterations: nil)
|
|
8
17
|
@config = config || Rubino.configuration
|
|
9
|
-
|
|
18
|
+
# A 0/negative cap is nonsense (the turn could never run a single
|
|
19
|
+
# iteration), so REJECT it with a clear message at both entry points —
|
|
20
|
+
# the configured `agent.max_turns` and the CLI `--max-turns N` override —
|
|
21
|
+
# rather than silently coercing it to "unbounded" / the default and
|
|
22
|
+
# surprising the user. nil/absent stays meaningful (unbounded rail /
|
|
23
|
+
# config default).
|
|
24
|
+
@max_turns = require_positive_cap!(@config.agent_max_turns, "agent.max_turns")
|
|
10
25
|
# An explicit override (the CLI `--max-turns N` flag, threaded through
|
|
11
26
|
# Runner → Lifecycle) wins over the config default so the documented
|
|
12
27
|
# control knob actually caps tool iterations (#141). A nil/blank
|
|
13
28
|
# override falls back to the configured budget, unchanged.
|
|
14
|
-
|
|
29
|
+
override = require_positive_cap!(max_tool_iterations, "--max-turns")
|
|
30
|
+
@max_tool_iterations = override || @config.agent_max_tool_iterations
|
|
15
31
|
@max_turn_seconds = @config.agent_max_turn_seconds
|
|
16
32
|
@turn_started_at = Time.now
|
|
17
33
|
end
|
|
@@ -21,6 +37,34 @@ module Rubino
|
|
|
21
37
|
within_iteration_limit?(iteration) && within_time_limit?
|
|
22
38
|
end
|
|
23
39
|
|
|
40
|
+
# True ONLY when offering the interactive Continue extension would actually
|
|
41
|
+
# help: the SOFT iteration ceiling (@max_tool_iterations) is what's
|
|
42
|
+
# exhausted, and neither non-extendable rail is the blocker (#403).
|
|
43
|
+
# extend! raises only the soft ceiling, so it is impotent against the TIME
|
|
44
|
+
# limit AND the max_turns OUTER rail. When either of those is what's spent,
|
|
45
|
+
# extending is a no-op and re-prompting would loop forever — callers must
|
|
46
|
+
# force-summarize instead. Hence extendable? is FALSE when the time limit
|
|
47
|
+
# OR the max_turns outer rail is the blocker, and only TRUE when the soft
|
|
48
|
+
# iteration ceiling is what's exhausted. Also false on an unbounded soft
|
|
49
|
+
# cap (nothing to extend).
|
|
50
|
+
def extendable?(iteration)
|
|
51
|
+
within_time_limit? && within_turns_rail?(iteration) && !within_soft_iteration_limit?(iteration)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Grants `by` more tool iterations so a turn that hit the cap can resume
|
|
55
|
+
# the SAME turn with full context (#399, the Cline/Roo "reset the counter,
|
|
56
|
+
# keep context" pattern). Only the soft iteration ceiling moves — the
|
|
57
|
+
# max_turns OUTER rail and the max_turn_seconds safety-net are untouched,
|
|
58
|
+
# so repeated extensions can never bypass the max_turns/clock ceiling (a
|
|
59
|
+
# runaway still stops at max_turns). No-op on an unbounded (nil) cap.
|
|
60
|
+
# Returns the new ceiling.
|
|
61
|
+
def extend!(by)
|
|
62
|
+
amount = positive_int(by)
|
|
63
|
+
return @max_tool_iterations if amount.nil? || @max_tool_iterations.nil?
|
|
64
|
+
|
|
65
|
+
@max_tool_iterations += amount
|
|
66
|
+
end
|
|
67
|
+
|
|
24
68
|
private
|
|
25
69
|
|
|
26
70
|
# Coerce an override to a positive Integer, or nil if it's absent/garbage
|
|
@@ -33,9 +77,71 @@ module Rubino
|
|
|
33
77
|
n if n && n.positive?
|
|
34
78
|
end
|
|
35
79
|
|
|
80
|
+
# A turn/iteration cap must be a POSITIVE integer when a NUMBER is given.
|
|
81
|
+
# nil/absent or non-numeric garbage (e.g. an empty Thor option) is treated
|
|
82
|
+
# as "unset" → use the default / unbounded rail, preserving the lenient
|
|
83
|
+
# prior behaviour. A genuinely-numeric 0 or NEGATIVE value, however, is an
|
|
84
|
+
# unambiguous misconfiguration (the turn could never run a single
|
|
85
|
+
# iteration) → reject it with a clear, actionable error naming the knob,
|
|
86
|
+
# instead of silently degrading to "unbounded" / the default.
|
|
87
|
+
def require_positive_cap!(value, label)
|
|
88
|
+
n = numeric_cap(value)
|
|
89
|
+
return positive_int(value) if n.nil? # nil/non-numeric ⇒ unset
|
|
90
|
+
|
|
91
|
+
# A 0/negative cap (the turn could never run a single iteration).
|
|
92
|
+
unless n.positive?
|
|
93
|
+
raise Rubino::ConfigurationError,
|
|
94
|
+
"invalid #{label}: #{value.inspect} — must be a positive integer " \
|
|
95
|
+
"(a 0 or negative cap would never let the turn run). " \
|
|
96
|
+
"Set it to 1 or more, or leave it unset for the default."
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# An absurdly large cap (F2) is as nonsensical the OTHER way — e.g.
|
|
100
|
+
# `--max-turns 99999999999999` was silently accepted, making the runaway
|
|
101
|
+
# guard a no-op. Reject anything above the sane ceiling with an equally
|
|
102
|
+
# clear, actionable message naming the knob and the accepted range.
|
|
103
|
+
if n > MAX_CAP
|
|
104
|
+
raise Rubino::ConfigurationError,
|
|
105
|
+
"invalid #{label}: #{value.inspect} — must be a positive integer no greater than " \
|
|
106
|
+
"#{MAX_CAP} (a larger cap effectively removes the runaway guard). " \
|
|
107
|
+
"Leave it unset for the default."
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
positive_int(value)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The value as a number iff it IS one (Integer/Float, or a numeric string
|
|
114
|
+
# like "0"/"-5"); nil for nil or non-numeric garbage. Used to tell a
|
|
115
|
+
# genuine 0/negative cap (reject) from "unset" (lenient default).
|
|
116
|
+
def numeric_cap(value)
|
|
117
|
+
return value if value.is_a?(Numeric)
|
|
118
|
+
return nil if value.nil?
|
|
119
|
+
|
|
120
|
+
s = value.to_s.strip
|
|
121
|
+
return nil if s.empty?
|
|
122
|
+
|
|
123
|
+
Integer(s, exception: false) || Float(s, exception: false)
|
|
124
|
+
end
|
|
125
|
+
|
|
36
126
|
# A nil cap means "unbounded": never stop on that dimension rather than
|
|
37
|
-
# crashing the turn comparing a number with nil (#139).
|
|
127
|
+
# crashing the turn comparing a number with nil (#139). The full iteration
|
|
128
|
+
# limit is the conjunction of the OUTER max_turns rail (#414) and the SOFT
|
|
129
|
+
# @max_tool_iterations ceiling — even after extend! lifts the soft ceiling,
|
|
130
|
+
# the iteration count may never exceed max_turns, so a runaway that keeps
|
|
131
|
+
# extending still terminates at the hard outer bound.
|
|
38
132
|
def within_iteration_limit?(iteration)
|
|
133
|
+
within_turns_rail?(iteration) && within_soft_iteration_limit?(iteration)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The OUTER max_turns rail (#414): a hard ceiling extend! cannot move. A
|
|
137
|
+
# nil max_turns means this rail is unbounded.
|
|
138
|
+
def within_turns_rail?(iteration)
|
|
139
|
+
@max_turns.nil? || iteration <= @max_turns
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The SOFT @max_tool_iterations ceiling: the only dimension extend! can
|
|
143
|
+
# lift. A nil ceiling means it is unbounded (nothing to extend).
|
|
144
|
+
def within_soft_iteration_limit?(iteration)
|
|
39
145
|
@max_tool_iterations.nil? || iteration <= @max_tool_iterations
|
|
40
146
|
end
|
|
41
147
|
|