ruby_coded 0.3.1 → 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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.md +113 -1
  4. data/lib/ruby_coded/chat/app.rb +35 -34
  5. data/lib/ruby_coded/chat/bridge_common/auto_switch.rb +45 -0
  6. data/lib/ruby_coded/chat/bridge_common/mode_transitions.rb +70 -0
  7. data/lib/ruby_coded/chat/bridge_common/tool_flow.rb +61 -0
  8. data/lib/ruby_coded/chat/bridge_common.rb +31 -0
  9. data/lib/ruby_coded/chat/bridge_factory.rb +61 -0
  10. data/lib/ruby_coded/chat/codex_bridge/error_handling.rb +1 -20
  11. data/lib/ruby_coded/chat/codex_bridge/request_builder.rb +3 -15
  12. data/lib/ruby_coded/chat/codex_bridge/tool_approval.rb +8 -32
  13. data/lib/ruby_coded/chat/codex_bridge/tool_handling.rb +3 -34
  14. data/lib/ruby_coded/chat/codex_bridge.rb +31 -46
  15. data/lib/ruby_coded/chat/command_handler/agent_commands.rb +3 -3
  16. data/lib/ruby_coded/chat/command_handler/plan_commands.rb +1 -1
  17. data/lib/ruby_coded/chat/command_handler/skill_commands.rb +87 -0
  18. data/lib/ruby_coded/chat/command_handler.rb +3 -0
  19. data/lib/ruby_coded/chat/llm_bridge/chat_configuration.rb +38 -0
  20. data/lib/ruby_coded/chat/llm_bridge/plan_mode.rb +3 -32
  21. data/lib/ruby_coded/chat/llm_bridge/tool_call_handling.rb +9 -50
  22. data/lib/ruby_coded/chat/llm_bridge.rb +31 -65
  23. data/lib/ruby_coded/chat/prompt_builder.rb +66 -0
  24. data/lib/ruby_coded/chat/renderer/chat_panel.rb +48 -62
  25. data/lib/ruby_coded/chat/renderer/chat_panel_cache.rb +35 -0
  26. data/lib/ruby_coded/chat/renderer/chat_panel_formatting.rb +72 -0
  27. data/lib/ruby_coded/chat/renderer/chat_panel_layout.rb +81 -0
  28. data/lib/ruby_coded/chat/renderer/chat_panel_sections.rb +93 -0
  29. data/lib/ruby_coded/chat/renderer/chat_panel_thinking.rb +20 -34
  30. data/lib/ruby_coded/chat/renderer/chat_panel_thinking_render.rb +49 -0
  31. data/lib/ruby_coded/chat/renderer/rich_text.rb +92 -0
  32. data/lib/ruby_coded/chat/renderer/rich_text_inline.rb +81 -0
  33. data/lib/ruby_coded/chat/renderer.rb +12 -0
  34. data/lib/ruby_coded/chat/runtime_mode.rb +97 -0
  35. data/lib/ruby_coded/commands/core_provider.rb +7 -0
  36. data/lib/ruby_coded/commands.rb +11 -0
  37. data/lib/ruby_coded/plugins.rb +10 -1
  38. data/lib/ruby_coded/skills/catalog.rb +103 -0
  39. data/lib/ruby_coded/skills/markdown_loader.rb +112 -0
  40. data/lib/ruby_coded/skills/prompt_formatter.rb +42 -0
  41. data/lib/ruby_coded/skills/skill_definition.rb +29 -0
  42. data/lib/ruby_coded/skills.rb +18 -0
  43. data/lib/ruby_coded/tools/base_tool.rb +14 -0
  44. data/lib/ruby_coded/tools/delete_path_tool.rb +5 -8
  45. data/lib/ruby_coded/tools/edit_file_tool.rb +6 -8
  46. data/lib/ruby_coded/tools/execution_pipeline.rb +60 -0
  47. data/lib/ruby_coded/tools/execution_policy.rb +89 -0
  48. data/lib/ruby_coded/tools/write_file_tool.rb +6 -9
  49. data/lib/ruby_coded/version.rb +1 -1
  50. data/lib/ruby_coded.rb +1 -0
  51. metadata +25 -2
@@ -3,46 +3,22 @@
3
3
  module RubyCoded
4
4
  module Chat
5
5
  class CodexBridge
6
- # Manages interactive approval flow for tool calls in agentic mode.
6
+ # Interactive approval flow for tool calls in the Codex backend.
7
+ # Uses the shared ExecutionPolicy for the confirmation decision
8
+ # and BridgeCommon for polling/applying the user's response.
7
9
  module ToolApproval
8
10
  private
9
11
 
10
12
  def request_approval(_tool_call, display_name, args, risk)
11
13
  args_summary = args.map { |k, v| "#{k}: #{v}" }.join(", ")
12
14
 
13
- if risk == Tools::BaseTool::SAFE_RISK || @state.auto_approve_tools?
14
- @state.add_message(:tool_call, "[#{display_name}] #{args_summary}")
15
- else
16
- risk_label = risk == Tools::BaseTool::DANGEROUS_RISK ? "DANGEROUS" : "WRITE"
17
- @state.request_tool_confirmation!(display_name, args, risk_label: risk_label)
15
+ if @policy.requires_confirmation?(risk, @mode)
16
+ @state.request_tool_confirmation!(display_name, args,
17
+ risk_label: @policy.risk_label_for(risk))
18
18
  decision = poll_tool_decision
19
19
  apply_tool_decision(decision, display_name)
20
- end
21
- end
22
-
23
- def poll_tool_decision
24
- @state.mutex.synchronize do
25
- loop do
26
- return :cancelled if @cancel_requested
27
-
28
- resp = @state.instance_variable_get(:@tool_confirmation_response)
29
- return resp if %i[approved rejected].include?(resp)
30
-
31
- @state.tool_cv.wait(@state.mutex, 0.1)
32
- end
33
- end
34
- end
35
-
36
- def apply_tool_decision(decision, display_name)
37
- case decision
38
- when :cancelled
39
- @state.clear_tool_confirmation!
40
- raise Tools::AgentCancelledError, "Operation cancelled by user"
41
- when :approved
42
- @state.resolve_tool_confirmation!(:approved)
43
- when :rejected
44
- @state.resolve_tool_confirmation!(:rejected)
45
- raise Tools::ToolRejectedError, "User rejected #{display_name}"
20
+ else
21
+ @state.add_message(:tool_call, "[#{display_name}] #{args_summary}")
46
22
  end
47
23
  end
48
24
  end
@@ -29,11 +29,9 @@ module RubyCoded
29
29
  name = tool_call[:name]
30
30
  args = parse_tool_arguments(tool_call[:arguments])
31
31
  display_name = short_tool_name(name)
32
- risk = @tool_registry.risk_level_for(name)
33
32
 
34
- increment_call_counts(risk)
35
- check_tool_limits!
36
- warn_approaching_limit
33
+ risk = @policy.register_call!(name)
34
+ @policy.warn_if_approaching_limit!
37
35
 
38
36
  request_approval(tool_call, display_name, args, risk)
39
37
  result = run_tool(name, args)
@@ -49,7 +47,7 @@ module RubyCoded
49
47
  end
50
48
 
51
49
  def run_tool(name, args)
52
- tool_instances = @agentic_mode ? @tool_registry.build_tools : @tool_registry.build_readonly_tools
50
+ tool_instances = @mode.allows_mutation? ? @tool_registry.build_tools : @tool_registry.build_readonly_tools
53
51
  tool = tool_instances.find { |t| tool_name_match?(t, name) }
54
52
 
55
53
  return { error: "Unknown tool: #{name}" } unless tool
@@ -90,35 +88,6 @@ module RubyCoded
90
88
  perform_codex_request(nil)
91
89
  end
92
90
 
93
- def increment_call_counts(risk)
94
- @tool_call_count += 1
95
- @write_tool_call_count += 1 unless risk == Tools::BaseTool::SAFE_RISK
96
- end
97
-
98
- def check_tool_limits!
99
- if @write_tool_call_count >= MAX_WRITE_TOOL_ROUNDS
100
- @write_tool_call_count = 0
101
- @state.add_message(:system,
102
- "Write tool call budget (#{MAX_WRITE_TOOL_ROUNDS}) reached — auto-resetting counter.")
103
- end
104
-
105
- return unless @tool_call_count > MAX_TOTAL_TOOL_ROUNDS
106
-
107
- raise Tools::AgentIterationLimitError,
108
- "Reached maximum of #{MAX_TOTAL_TOOL_ROUNDS} total tool calls. " \
109
- "Send a new message to continue, or use /agent on to reset counters."
110
- end
111
-
112
- def warn_approaching_limit
113
- threshold = (MAX_TOTAL_TOOL_ROUNDS * TOOL_ROUNDS_WARNING_THRESHOLD).to_i
114
- return unless @tool_call_count == threshold
115
-
116
- remaining = MAX_TOTAL_TOOL_ROUNDS - threshold
117
- @state.add_message(:system,
118
- "Approaching total tool call limit: #{remaining} calls remaining. " \
119
- "Prioritize completing the most important work.")
120
- end
121
-
122
91
  def short_tool_name(name)
123
92
  name.split("--").last
124
93
  end
@@ -9,7 +9,12 @@ require_relative "../tools/system_prompt"
9
9
  require_relative "../tools/plan_system_prompt"
10
10
  require_relative "../tools/agent_cancelled_error"
11
11
  require_relative "../tools/agent_iteration_limit_error"
12
+ require_relative "../tools/execution_policy"
13
+ require_relative "../skills"
12
14
  require_relative "../auth/jwt_decoder"
15
+ require_relative "runtime_mode"
16
+ require_relative "bridge_common"
17
+ require_relative "prompt_builder"
13
18
  require_relative "codex_bridge/request_builder"
14
19
  require_relative "codex_bridge/sse_parser"
15
20
  require_relative "codex_bridge/tool_handling"
@@ -32,6 +37,7 @@ module RubyCoded
32
37
  # Implements the same public interface as LLMBridge so App can
33
38
  # swap between them based on the active auth_method.
34
39
  class CodexBridge
40
+ include BridgeCommon
35
41
  include RequestBuilder
36
42
  include SSEParser
37
43
  include ToolHandling
@@ -44,22 +50,27 @@ module RubyCoded
44
50
 
45
51
  MAX_RATE_LIMIT_RETRIES = 2
46
52
  RATE_LIMIT_BASE_DELAY = 2
47
- MAX_WRITE_TOOL_ROUNDS = 50
48
- MAX_TOTAL_TOOL_ROUNDS = 200
49
- TOOL_ROUNDS_WARNING_THRESHOLD = 0.8
53
+ MAX_WRITE_TOOL_ROUNDS = Tools::ExecutionPolicy::MAX_WRITE_TOOL_ROUNDS
54
+ MAX_TOTAL_TOOL_ROUNDS = Tools::ExecutionPolicy::MAX_TOTAL_TOOL_ROUNDS
50
55
  MAX_TOOL_RESULT_CHARS = 10_000
51
56
 
52
- attr_reader :agentic_mode, :plan_mode, :project_root
57
+ attr_reader :mode, :project_root
53
58
 
54
- def initialize(state, credentials_store:, auth_manager:, project_root: Dir.pwd)
59
+ def initialize(state, credentials_store:, auth_manager:, project_root: Dir.pwd, skill_catalog: nil)
55
60
  @state = state
56
61
  @credentials_store = credentials_store
57
62
  @auth_manager = auth_manager
58
63
  @project_root = project_root
59
- @cancel_requested = @agentic_mode = @plan_mode = false
60
- @model = state.model
64
+ @skill_catalog = skill_catalog || RubyCoded::Skills::Catalog.new(project_root: @project_root)
65
+ initialize_runtime_state
66
+ end
67
+
68
+ def initialize_runtime_state
69
+ @cancel_requested = false
70
+ @mode = RuntimeMode.chat
71
+ @model = @state.model
61
72
  @conversation_history = []
62
- @tool_registry = Tools::Registry.new(project_root: @project_root)
73
+ setup_agent_pipeline!
63
74
  reset_call_counts
64
75
  @conn = build_connection
65
76
  end
@@ -74,52 +85,26 @@ module RubyCoded
74
85
  end
75
86
  end
76
87
 
77
- def cancel!
78
- @cancel_requested = true
79
- @state.mutex.synchronize { @state.tool_cv.signal }
80
- end
81
-
82
88
  def reset_chat!(model_name)
83
89
  @model = model_name
84
90
  @conversation_history = []
85
91
  end
86
92
 
87
- def toggle_agentic_mode!(enabled)
88
- @agentic_mode = enabled
89
- @state.agentic_mode = enabled
90
- if enabled && @plan_mode
91
- @plan_mode = false
92
- @state.deactivate_plan_mode!
93
- end
94
- @state.disable_auto_approve! unless enabled
95
- end
96
-
97
- def toggle_plan_mode!(enabled)
98
- @plan_mode = enabled
99
- return unless enabled && @agentic_mode
100
-
101
- @agentic_mode = false
102
- @state.agentic_mode = false
103
- @state.disable_auto_approve!
104
- end
105
-
106
- def approve_tool!
107
- @state.tool_confirmation_response = :approved
93
+ def reset_agent_session!
94
+ @policy.reset_counters!
95
+ @conversation_history = []
108
96
  end
109
97
 
110
- def approve_all_tools!
111
- @state.enable_auto_approve!
112
- @state.tool_confirmation_response = :approved
113
- end
98
+ private
114
99
 
115
- def reject_tool!
116
- @state.tool_confirmation_response = :rejected
117
- end
118
-
119
- def reset_agent_session!
120
- @tool_call_count = 0
121
- @write_tool_call_count = 0
122
- @conversation_history = []
100
+ def setup_agent_pipeline!
101
+ @tool_registry = Tools::Registry.new(project_root: @project_root)
102
+ @policy = Tools::ExecutionPolicy.new(state: @state, registry: @tool_registry)
103
+ @prompt_builder = PromptBuilder.new(
104
+ project_root: @project_root,
105
+ skill_catalog: @skill_catalog,
106
+ chat_base: :simple
107
+ )
123
108
  end
124
109
  end
125
110
  end
@@ -21,7 +21,7 @@ module RubyCoded
21
21
  end
22
22
 
23
23
  def enable_agent_mode
24
- if @llm_bridge.agentic_mode
24
+ if @llm_bridge.agentic_mode?
25
25
  @llm_bridge.reset_agent_session!
26
26
  @state.add_message(:system,
27
27
  "Agent session reset. Tool call counters cleared — ready for a new task.")
@@ -34,7 +34,7 @@ module RubyCoded
34
34
  end
35
35
 
36
36
  def disable_agent_mode
37
- unless @llm_bridge.agentic_mode
37
+ unless @llm_bridge.agentic_mode?
38
38
  @state.add_message(:system, "Agent mode is already disabled.")
39
39
  return
40
40
  end
@@ -44,7 +44,7 @@ module RubyCoded
44
44
  end
45
45
 
46
46
  def show_agent_status
47
- status = @llm_bridge.agentic_mode ? "enabled" : "disabled"
47
+ status = @llm_bridge.agentic_mode? ? "enabled" : "disabled"
48
48
  @state.add_message(:system, "Agent mode: #{status}. Use /agent on or /agent off to toggle.")
49
49
  end
50
50
  end
@@ -41,7 +41,7 @@ module RubyCoded
41
41
  end
42
42
 
43
43
  def deactivate_agent_if_needed
44
- return unless @llm_bridge.agentic_mode
44
+ return unless @llm_bridge.agentic_mode?
45
45
 
46
46
  @llm_bridge.toggle_agentic_mode!(false)
47
47
  @state.add_message(:system, "Agent mode disabled.")
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ class CommandHandler
6
+ # Slash commands for managing project-local skills.
7
+ module SkillCommands
8
+ private
9
+
10
+ def cmd_skills(rest)
11
+ case rest&.strip&.downcase
12
+ when "reload"
13
+ reload_skills
14
+ when "list"
15
+ list_skills
16
+ else
17
+ @state.add_message(:system, "Usage: /skills [reload|list]")
18
+ end
19
+ end
20
+
21
+ def reload_skills
22
+ return missing_skill_catalog unless @skill_catalog
23
+
24
+ report = @skill_catalog.reload!
25
+ @state.add_message(:system, format_skill_reload_message(report))
26
+ end
27
+
28
+ def list_skills
29
+ return missing_skill_catalog unless @skill_catalog
30
+
31
+ skills = @skill_catalog.all_skills
32
+ return show_empty_skills if skills.empty?
33
+
34
+ @state.add_message(:system, formatted_skills(skills))
35
+ end
36
+
37
+ def missing_skill_catalog
38
+ @state.add_message(:system, "Skill catalog is not available.")
39
+ end
40
+
41
+ def show_empty_skills
42
+ @state.add_message(
43
+ :system,
44
+ "No project skills loaded. Add markdown files under .rubycoded/skills and run /skills reload."
45
+ )
46
+ end
47
+
48
+ def formatted_skills(skills)
49
+ lines = ["Project skills:"]
50
+ skills.each do |skill|
51
+ lines << format_skill_line(skill)
52
+ end
53
+ lines.join("\n")
54
+ end
55
+
56
+ def format_skill_line(skill)
57
+ modes = skill.modes.join(", ")
58
+ " #{skill.name.ljust(24)} #{skill.description} [modes: #{modes}]"
59
+ end
60
+
61
+ def format_skill_reload_message(report)
62
+ message = skill_reload_summary(report)
63
+ details = skill_reload_details(report)
64
+
65
+ details.empty? ? message : "#{message}\n#{details.join("\n")}"
66
+ end
67
+
68
+ def skill_reload_summary(report)
69
+ "Skills reloaded. " \
70
+ "Added: #{report[:added]}, removed: #{report[:removed]}, " \
71
+ "total skills: #{report[:total]}, invalid files ignored: #{report[:invalid]}, " \
72
+ "duplicates ignored: #{report[:duplicates]}."
73
+ end
74
+
75
+ def skill_reload_details(report)
76
+ invalid_files = Array(report[:invalid_files])
77
+ duplicate_skills = Array(report[:duplicate_skills])
78
+
79
+ details = []
80
+ details << "Invalid files: #{invalid_files.join(", ")}" if invalid_files.any?
81
+ details << "Duplicate skills: #{duplicate_skills.join(", ")}" if duplicate_skills.any?
82
+ details
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -10,6 +10,7 @@ require_relative "command_handler/agent_commands"
10
10
  require_relative "command_handler/plan_commands"
11
11
  require_relative "command_handler/login_commands"
12
12
  require_relative "command_handler/custom_commands"
13
+ require_relative "command_handler/skill_commands"
13
14
 
14
15
  module RubyCoded
15
16
  module Chat
@@ -25,6 +26,7 @@ module RubyCoded
25
26
  include PlanCommands
26
27
  include LoginCommands
27
28
  include CustomCommands
29
+ include SkillCommands
28
30
 
29
31
  HELP_TEXT = File.read(File.join(__dir__, "help.txt")).freeze
30
32
 
@@ -35,6 +37,7 @@ module RubyCoded
35
37
  @credentials_store = deps[:credentials_store]
36
38
  @auth_manager = deps[:auth_manager]
37
39
  @command_catalog = command_catalog
40
+ @skill_catalog = deps[:skill_catalog]
38
41
  @commands = build_command_map
39
42
  end
40
43
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyCoded
4
+ module Chat
5
+ class LLMBridge
6
+ # Applies tool/instruction configuration to the RubyLLM chat based on
7
+ # the active mode (agentic, plan, or plain chat).
8
+ module ChatConfiguration
9
+ private
10
+
11
+ def apply_instructions_if_supported(chat, instructions)
12
+ return unless chat.respond_to?(:with_instructions)
13
+
14
+ chat.with_instructions(instructions)
15
+ end
16
+
17
+ def reconfigure_chat!
18
+ @chat_mutex.synchronize do
19
+ apply_mode_config!(@chat)
20
+ end
21
+ end
22
+
23
+ def apply_mode_config!(chat)
24
+ case @mode.name
25
+ when :agent then configure_agentic!(chat)
26
+ when :plan then configure_plan!(chat)
27
+ else configure_chat!(chat)
28
+ end
29
+ end
30
+
31
+ def configure_chat!(chat)
32
+ chat.with_tools(replace: true)
33
+ apply_instructions_if_supported(chat, @prompt_builder.build(RuntimeMode.chat))
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -3,44 +3,15 @@
3
3
  module RubyCoded
4
4
  module Chat
5
5
  class LLMBridge
6
- # Plan mode configuration, auto-switching to agent, and clarification handling.
6
+ # Plan mode configuration and plan/clarification post-processing.
7
+ # Auto-switch heuristic lives in BridgeCommon.
7
8
  module PlanMode
8
- IMPLEMENTATION_PATTERNS = [
9
- /\bimplement/i,
10
- /\bgo ahead/i,
11
- /\bproceed/i,
12
- /\bexecut/i,
13
- /\bejecutar?/i,
14
- /\bcomenz/i,
15
- /\bcomienz/i,
16
- /\bhazlo/i,
17
- /\bconstru[iy]/i,
18
- /\badelante/i,
19
- /\bdale\b/i,
20
- /\bdo it/i,
21
- /\bbuild it/i
22
- ].freeze
23
-
24
9
  private
25
10
 
26
- def should_auto_switch_to_agent?(input)
27
- @plan_mode && @state.current_plan && implementation_request?(input)
28
- end
29
-
30
- def implementation_request?(input)
31
- IMPLEMENTATION_PATTERNS.any? { |pattern| input.match?(pattern) }
32
- end
33
-
34
- def auto_switch_to_agent!
35
- toggle_agentic_mode!(true)
36
- @state.add_message(:system,
37
- "Plan mode disabled — switching to agent mode to implement the plan.")
38
- end
39
-
40
11
  def configure_plan!(chat)
41
12
  readonly_tools = @tool_registry.build_readonly_tools
42
13
  chat.with_tools(*readonly_tools, replace: true)
43
- chat.with_instructions(Tools::PlanSystemPrompt.build(project_root: @project_root))
14
+ apply_instructions_if_supported(chat, @prompt_builder.build(RuntimeMode.plan))
44
15
 
45
16
  chat.on_tool_call { |tool_call| handle_tool_call(tool_call) }
46
17
  chat.on_tool_result { |result| handle_tool_result(result) }
@@ -8,13 +8,8 @@ module RubyCoded
8
8
  private
9
9
 
10
10
  def configure_agentic!(chat)
11
- tools = @tool_registry.build_tools
12
- chat.with_tools(*tools, replace: true)
13
- chat.with_instructions(Tools::SystemPrompt.build(
14
- project_root: @project_root,
15
- max_write_rounds: MAX_WRITE_TOOL_ROUNDS,
16
- max_total_rounds: MAX_TOTAL_TOOL_ROUNDS
17
- ))
11
+ chat.with_tools(*@tool_registry.build_tools, replace: true)
12
+ apply_instructions_if_supported(chat, @prompt_builder.build(RuntimeMode.agent))
18
13
 
19
14
  chat.on_tool_call { |tool_call| handle_tool_call(tool_call) }
20
15
  chat.on_tool_result { |result| handle_tool_result(result) }
@@ -24,29 +19,21 @@ module RubyCoded
24
19
  raise Tools::AgentCancelledError, "Operation cancelled by user" if @cancel_requested
25
20
 
26
21
  display_name = short_tool_name(tool_call.name)
27
- risk = @tool_registry.risk_level_for(tool_call.name)
28
-
29
- increment_call_counts(risk)
30
- check_tool_limits!
31
- warn_approaching_limit
22
+ risk = @policy.register_call!(tool_call.name)
23
+ @policy.warn_if_approaching_limit!
32
24
 
33
25
  process_tool_approval(tool_call, display_name, risk)
34
26
  end
35
27
 
36
- def increment_call_counts(risk)
37
- @tool_call_count += 1
38
- @write_tool_call_count += 1 unless risk == Tools::BaseTool::SAFE_RISK
39
- end
40
-
41
28
  def process_tool_approval(tool_call, display_name, risk)
42
29
  args_summary = tool_call.arguments.map { |k, v| "#{k}: #{v}" }.join(", ")
43
30
 
44
- if risk == Tools::BaseTool::SAFE_RISK || @state.auto_approve_tools?
45
- @state.add_message(:tool_call, "[#{display_name}] #{args_summary}")
46
- else
47
- risk_label = risk == Tools::BaseTool::DANGEROUS_RISK ? "DANGEROUS" : "WRITE"
48
- @state.request_tool_confirmation!(display_name, tool_call.arguments, risk_label: risk_label)
31
+ if @policy.requires_confirmation?(risk, @mode)
32
+ @state.request_tool_confirmation!(display_name, tool_call.arguments,
33
+ risk_label: @policy.risk_label_for(risk))
49
34
  wait_for_confirmation(tool_call)
35
+ else
36
+ @state.add_message(:tool_call, "[#{display_name}] #{args_summary}")
50
37
  end
51
38
  end
52
39
 
@@ -92,34 +79,6 @@ module RubyCoded
92
79
  @state.add_message(:tool_result, text)
93
80
  end
94
81
 
95
- def check_tool_limits!
96
- if @write_tool_call_count >= MAX_WRITE_TOOL_ROUNDS
97
- @write_tool_call_count = 0
98
- @state.add_message(:system,
99
- "Write tool call budget (#{MAX_WRITE_TOOL_ROUNDS}) reached — auto-resetting counter.")
100
- end
101
-
102
- return unless @tool_call_count > MAX_TOTAL_TOOL_ROUNDS
103
-
104
- raise Tools::AgentIterationLimitError,
105
- "Reached maximum of #{MAX_TOTAL_TOOL_ROUNDS} total tool calls. " \
106
- "Send a new message to continue, or use /agent on to reset counters."
107
- end
108
-
109
- def warn_approaching_limit
110
- warn_limit(@tool_call_count, MAX_TOTAL_TOOL_ROUNDS, "total")
111
- end
112
-
113
- def warn_limit(count, max, label)
114
- warning_at = (max * TOOL_ROUNDS_WARNING_THRESHOLD).to_i
115
- return unless count == warning_at
116
-
117
- remaining = max - count
118
- @state.add_message(:system,
119
- "Approaching #{label} tool call limit: #{remaining} calls remaining. " \
120
- "Prioritize completing the most important work.")
121
- end
122
-
123
82
  def short_tool_name(name)
124
83
  name.split("--").last
125
84
  end