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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +113 -1
- data/lib/ruby_coded/chat/app.rb +35 -34
- data/lib/ruby_coded/chat/bridge_common/auto_switch.rb +45 -0
- data/lib/ruby_coded/chat/bridge_common/mode_transitions.rb +70 -0
- data/lib/ruby_coded/chat/bridge_common/tool_flow.rb +61 -0
- data/lib/ruby_coded/chat/bridge_common.rb +31 -0
- data/lib/ruby_coded/chat/bridge_factory.rb +61 -0
- data/lib/ruby_coded/chat/codex_bridge/error_handling.rb +1 -20
- data/lib/ruby_coded/chat/codex_bridge/request_builder.rb +3 -15
- data/lib/ruby_coded/chat/codex_bridge/tool_approval.rb +8 -32
- data/lib/ruby_coded/chat/codex_bridge/tool_handling.rb +3 -34
- data/lib/ruby_coded/chat/codex_bridge.rb +31 -46
- data/lib/ruby_coded/chat/command_handler/agent_commands.rb +3 -3
- data/lib/ruby_coded/chat/command_handler/plan_commands.rb +1 -1
- data/lib/ruby_coded/chat/command_handler/skill_commands.rb +87 -0
- data/lib/ruby_coded/chat/command_handler.rb +3 -0
- data/lib/ruby_coded/chat/llm_bridge/chat_configuration.rb +38 -0
- data/lib/ruby_coded/chat/llm_bridge/plan_mode.rb +3 -32
- data/lib/ruby_coded/chat/llm_bridge/tool_call_handling.rb +9 -50
- data/lib/ruby_coded/chat/llm_bridge.rb +31 -65
- data/lib/ruby_coded/chat/prompt_builder.rb +66 -0
- data/lib/ruby_coded/chat/renderer/chat_panel.rb +48 -62
- data/lib/ruby_coded/chat/renderer/chat_panel_cache.rb +35 -0
- data/lib/ruby_coded/chat/renderer/chat_panel_formatting.rb +72 -0
- data/lib/ruby_coded/chat/renderer/chat_panel_layout.rb +81 -0
- data/lib/ruby_coded/chat/renderer/chat_panel_sections.rb +93 -0
- data/lib/ruby_coded/chat/renderer/chat_panel_thinking.rb +20 -34
- data/lib/ruby_coded/chat/renderer/chat_panel_thinking_render.rb +49 -0
- data/lib/ruby_coded/chat/renderer/rich_text.rb +92 -0
- data/lib/ruby_coded/chat/renderer/rich_text_inline.rb +81 -0
- data/lib/ruby_coded/chat/renderer.rb +12 -0
- data/lib/ruby_coded/chat/runtime_mode.rb +97 -0
- data/lib/ruby_coded/commands/core_provider.rb +7 -0
- data/lib/ruby_coded/commands.rb +11 -0
- data/lib/ruby_coded/plugins.rb +10 -1
- data/lib/ruby_coded/skills/catalog.rb +103 -0
- data/lib/ruby_coded/skills/markdown_loader.rb +112 -0
- data/lib/ruby_coded/skills/prompt_formatter.rb +42 -0
- data/lib/ruby_coded/skills/skill_definition.rb +29 -0
- data/lib/ruby_coded/skills.rb +18 -0
- data/lib/ruby_coded/tools/base_tool.rb +14 -0
- data/lib/ruby_coded/tools/delete_path_tool.rb +5 -8
- data/lib/ruby_coded/tools/edit_file_tool.rb +6 -8
- data/lib/ruby_coded/tools/execution_pipeline.rb +60 -0
- data/lib/ruby_coded/tools/execution_policy.rb +89 -0
- data/lib/ruby_coded/tools/write_file_tool.rb +6 -9
- data/lib/ruby_coded/version.rb +1 -1
- data/lib/ruby_coded.rb +1 -0
- metadata +25 -2
|
@@ -3,46 +3,22 @@
|
|
|
3
3
|
module RubyCoded
|
|
4
4
|
module Chat
|
|
5
5
|
class CodexBridge
|
|
6
|
-
#
|
|
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
|
|
14
|
-
@state.
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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 = @
|
|
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 =
|
|
48
|
-
MAX_TOTAL_TOOL_ROUNDS =
|
|
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 :
|
|
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
|
-
@
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
@
|
|
89
|
-
@
|
|
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
|
-
|
|
111
|
-
@state.enable_auto_approve!
|
|
112
|
-
@state.tool_confirmation_response = :approved
|
|
113
|
-
end
|
|
98
|
+
private
|
|
114
99
|
|
|
115
|
-
def
|
|
116
|
-
@
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
12
|
-
chat.
|
|
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 = @
|
|
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
|
|
45
|
-
@state.
|
|
46
|
-
|
|
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
|