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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c0c96bdf1a87ce1f830d0a36b9fb78600f8ee1df2bd81603630f7bf87b0cac08
|
|
4
|
+
data.tar.gz: 3a08a46116afd6c8b1f82ec0c085c4758b65f349631854a47027883578ff3c3c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9a5f8e7e7587b8e87dd830b9a2a20976611ec4d04a2b61f6c805a2877727e4b9299865b947a89929b3e83a71149ac0f5a868de69fbdd02262d00de1b86595c6a
|
|
7
|
+
data.tar.gz: 3739c33f411b92918553b02816218f7ea54a11664802b10a7f15fa37138b9f4631315373e2624fbfd2a4246c44f27455454f0111385ccc855f88b08e3e75e597
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-07-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Highlight the users' messages in the chat section**: Now the chat sections highlights and spots the users' mesagges in blocks
|
|
8
|
+
for a easely reading of the chat
|
|
9
|
+
- **Rich text support in the TUI chat renderer**: Added rich text rendering support for chat output in the terminal UI.
|
|
10
|
+
- **Project-local skills system**: RubyCoded can now load Markdown skill files from `.rubycoded/skills/` using YAML frontmatter metadata (`name`, `description`, `modes`, optional `tags`, `trigger`, and `priority`) and expose them as reusable project instructions.
|
|
11
|
+
- **Skill management commands**: Added `/skills reload` and `/skills list` to reload and inspect project-local skills without restarting the application.
|
|
12
|
+
- **Skill validation and cataloging**: Added a dedicated `RubyCoded::Skills` subsystem with a markdown loader, normalized skill definitions, duplicate detection, invalid file reporting, and mode-aware in-memory cataloging.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **Refactor chat panel rendering system into multiple modules**
|
|
17
|
+
- **Prompt construction now supports project skills**: Chat, plan, and agent mode instructions now append relevant project-local skills so project-specific guidance is consistently available across runtime modes, including Codex-backed sessions.
|
|
18
|
+
- **Ruby-conventional predicate names on bridges**: `LLMBridge` and `CodexBridge` now expose `agentic_mode?` and `plan_mode?` (previously `agentic_mode` / `plan_mode`). Plugins that read these predicates need to update the call site.
|
|
19
|
+
- **Internal chat bridge refactor**: Extracted `RuntimeMode` as the single source of truth for chat/agent/plan modes, `BridgeFactory` for backend selection, `Tools::ExecutionPolicy` for centralized risk/approval/limits, `PromptBuilder` for mode-aware system instructions, and `BridgeCommon` (`ModeTransitions`, `ToolFlow`, `AutoSwitch`) to share behavior between `LLMBridge` and `CodexBridge`.
|
|
20
|
+
- **Tool execution pipeline**: Added `Tools::ExecutionPipeline` and `BaseTool#run_pipeline` to standardize path validation, project-root guarding and filesystem error normalization for side-effecting tools; adopted in `WriteFileTool`, `EditFileTool` and `DeletePathTool`.
|
|
21
|
+
- **Extension model documented**: The README now describes the boundary between commands, skills and plugins.
|
|
22
|
+
|
|
3
23
|
## [0.3.1] - 2026-06-28
|
|
4
24
|
|
|
5
25
|
### Added
|
data/README.md
CHANGED
|
@@ -28,7 +28,7 @@ An AI-powered terminal coding assistant built in Ruby. Chat with LLMs, let an ag
|
|
|
28
28
|
- **Tool confirmation** — Write and dangerous operations require explicit approval; safe operations (read, list) run automatically
|
|
29
29
|
- **Token & cost tracking** — Live status bar showing token usage, estimated session cost, and auth mode indicator
|
|
30
30
|
- **Plugin system** — Extend the chat with custom state, input handlers, renderer overlays, and commands
|
|
31
|
-
- **Slash commands** — `/agent`, `/plan`, `/model`, `/login`, `/history`, `/tokens`, `/commands reload`, `/commands list`, `/help`, and more
|
|
31
|
+
- **Slash commands** — `/agent`, `/plan`, `/model`, `/login`, `/history`, `/tokens`, `/commands reload`, `/commands list`, `/skills reload`, `/skills list`, `/help`, and more
|
|
32
32
|
|
|
33
33
|
## Requirements
|
|
34
34
|
|
|
@@ -64,6 +64,8 @@ On first launch you'll be asked to authenticate with a provider. After that, you
|
|
|
64
64
|
| `/login` | Authenticate with a provider (OpenAI, Anthropic) |
|
|
65
65
|
| `/commands reload` | Reload custom markdown commands from the current project |
|
|
66
66
|
| `/commands list` | List currently loaded custom markdown commands |
|
|
67
|
+
| `/skills reload` | Reload project-local skills from the current project |
|
|
68
|
+
| `/skills list` | List currently loaded project-local skills |
|
|
67
69
|
| `/tokens` | Show detailed token usage breakdown |
|
|
68
70
|
| `/history` | Show conversation history |
|
|
69
71
|
| `/clear` | Clear the conversation |
|
|
@@ -93,6 +95,22 @@ Safe tools run without asking. Confirm and dangerous tools show the operation de
|
|
|
93
95
|
|
|
94
96
|
Plan mode restricts the model to read-only tools and a planning-oriented system prompt. The model will analyze your project and propose a structured plan. If the model needs clarification, it presents interactive options you can select or answer with custom text.
|
|
95
97
|
|
|
98
|
+
### Extension model
|
|
99
|
+
|
|
100
|
+
RubyCoded exposes three distinct extension mechanisms. When adding new behavior, pick the one that matches the intent:
|
|
101
|
+
|
|
102
|
+
| Mechanism | What it is | Trigger | Where it lives |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| **Commands** | Explicit user-invoked actions (built-in or Markdown-defined slash commands) | User types `/foo` | `lib/ruby_coded/chat/command_handler/` (built-in) or `.ruby_coded/commands/*.md` (project-local) |
|
|
105
|
+
| **Skills** | Automatic instruction overlays merged into the system prompt when they match the active mode/context | Mode + optional tags/trigger match the current request | `.rubycoded/skills/*.md` (project-local) |
|
|
106
|
+
| **Plugins** | Code-level extensions that hook into state, input, renderer, or add new commands at runtime | Loaded on startup | Ruby classes registered via `RubyCoded::Plugins` |
|
|
107
|
+
|
|
108
|
+
Rule of thumb:
|
|
109
|
+
|
|
110
|
+
- Use a **command** if the user should invoke it explicitly.
|
|
111
|
+
- Use a **skill** if you want extra instructions injected automatically for a mode.
|
|
112
|
+
- Use a **plugin** if you need Ruby code to observe or mutate runtime behavior.
|
|
113
|
+
|
|
96
114
|
### Custom commands
|
|
97
115
|
|
|
98
116
|
You can define your own project-specific slash commands using Markdown files inside:
|
|
@@ -173,6 +191,100 @@ List currently loaded custom commands:
|
|
|
173
191
|
- Plugin commands also take precedence over custom Markdown commands.
|
|
174
192
|
- Invalid Markdown command files are ignored during reload.
|
|
175
193
|
|
|
194
|
+
### Project skills
|
|
195
|
+
|
|
196
|
+
RubyCoded also supports project-local skills: reusable Markdown instructions that are automatically injected into chat, plan, and agent behavior when they match the active mode.
|
|
197
|
+
|
|
198
|
+
Skills are loaded from:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
.rubycoded/skills/
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
This path is intentionally different from custom commands, which use:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
.ruby_coded/commands/
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Each `.md` file defines one skill.
|
|
211
|
+
|
|
212
|
+
#### Skill file format
|
|
213
|
+
|
|
214
|
+
Use YAML frontmatter followed by the skill body:
|
|
215
|
+
|
|
216
|
+
```md
|
|
217
|
+
---
|
|
218
|
+
name: Rails Migration Safety
|
|
219
|
+
description: Improve safety when changing schema or migrations
|
|
220
|
+
modes:
|
|
221
|
+
- agent
|
|
222
|
+
- plan
|
|
223
|
+
tags:
|
|
224
|
+
- rails
|
|
225
|
+
- migration
|
|
226
|
+
trigger: schema
|
|
227
|
+
priority: 10
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
Before proposing or applying migration changes:
|
|
231
|
+
- inspect existing schema and recent migrations
|
|
232
|
+
- prefer additive, reversible changes
|
|
233
|
+
- call out data backfill or locking risks explicitly
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### Required fields
|
|
237
|
+
|
|
238
|
+
- `name` — unique skill name within the project
|
|
239
|
+
- `description` — short explanation shown in `/skills list`
|
|
240
|
+
- `modes` — one or more supported modes: `chat`, `plan`, `agent`
|
|
241
|
+
|
|
242
|
+
#### Optional fields
|
|
243
|
+
|
|
244
|
+
- `tags` — keywords used for simple relevance matching
|
|
245
|
+
- `trigger` — plain-text trigger checked against the current request
|
|
246
|
+
- `priority` — higher-priority skills are listed first and applied first
|
|
247
|
+
|
|
248
|
+
#### How skills are activated
|
|
249
|
+
|
|
250
|
+
Current behavior is intentionally simple:
|
|
251
|
+
|
|
252
|
+
- all skills compatible with the active mode are loaded
|
|
253
|
+
- if a request matches skill `tags` or `trigger`, matching skills are preferred
|
|
254
|
+
- if nothing matches, all mode-compatible skills remain available
|
|
255
|
+
|
|
256
|
+
This means skills are useful immediately without requiring manual activation UI.
|
|
257
|
+
|
|
258
|
+
#### Managing skills
|
|
259
|
+
|
|
260
|
+
Reload skills after adding, editing, or deleting skill files:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
/skills reload
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
List the currently loaded skills:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
/skills list
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The reload command reports:
|
|
273
|
+
|
|
274
|
+
- how many skills were added
|
|
275
|
+
- how many were removed
|
|
276
|
+
- how many are currently available
|
|
277
|
+
- how many invalid files were ignored
|
|
278
|
+
- how many duplicate skill names were ignored
|
|
279
|
+
|
|
280
|
+
#### Skill behavior notes
|
|
281
|
+
|
|
282
|
+
- Skills are project-local and loaded relative to the current project root.
|
|
283
|
+
- Skills are injected into system instructions for `chat`, `plan`, and `agent` modes.
|
|
284
|
+
- If a skill conflicts with higher-priority system instructions or the user’s explicit request, the higher-priority instruction should win.
|
|
285
|
+
- Invalid or malformed skill files are ignored during reload.
|
|
286
|
+
- If duplicate skill names exist, the first loaded definition wins and later duplicates are ignored.
|
|
287
|
+
|
|
176
288
|
## Keyboard shortcuts
|
|
177
289
|
|
|
178
290
|
| Key | Action |
|
data/lib/ruby_coded/chat/app.rb
CHANGED
|
@@ -10,9 +10,7 @@ require_relative "state"
|
|
|
10
10
|
require_relative "input_handler"
|
|
11
11
|
require_relative "renderer"
|
|
12
12
|
require_relative "command_handler"
|
|
13
|
-
require_relative "
|
|
14
|
-
require_relative "codex_bridge"
|
|
15
|
-
require_relative "codex_models"
|
|
13
|
+
require_relative "bridge_factory"
|
|
16
14
|
require_relative "../commands/catalog"
|
|
17
15
|
require_relative "../auth/credentials_store"
|
|
18
16
|
require_relative "../auth/jwt_decoder"
|
|
@@ -42,16 +40,31 @@ module RubyCoded
|
|
|
42
40
|
end
|
|
43
41
|
|
|
44
42
|
def build_components!
|
|
43
|
+
build_catalogs!
|
|
44
|
+
@state = State.new(model: @model, command_catalog: @command_catalog)
|
|
45
|
+
@credentials_store = Auth::CredentialsStore.new(user_config: @user_config)
|
|
46
|
+
@bridge_factory = build_bridge_factory
|
|
47
|
+
@llm_bridge = @bridge_factory.build
|
|
48
|
+
@input_handler = InputHandler.new(@state)
|
|
49
|
+
@command_handler = build_command_handler
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_catalogs!
|
|
45
53
|
@command_catalog = RubyCoded::Commands::Catalog.new(
|
|
46
54
|
project_root: Dir.pwd,
|
|
47
55
|
plugin_registry: RubyCoded.plugin_registry
|
|
48
56
|
)
|
|
57
|
+
@skill_catalog = RubyCoded::Skills::Catalog.new(project_root: Dir.pwd)
|
|
58
|
+
end
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
def build_bridge_factory
|
|
61
|
+
BridgeFactory.new(
|
|
62
|
+
state: @state,
|
|
63
|
+
credentials_store: @credentials_store,
|
|
64
|
+
auth_manager: @auth_manager,
|
|
65
|
+
skill_catalog: @skill_catalog,
|
|
66
|
+
user_config: @user_config
|
|
67
|
+
)
|
|
55
68
|
end
|
|
56
69
|
|
|
57
70
|
IDLE_POLL_TIMEOUT = 0.016
|
|
@@ -109,7 +122,8 @@ module RubyCoded
|
|
|
109
122
|
user_config: @user_config,
|
|
110
123
|
credentials_store: @credentials_store,
|
|
111
124
|
auth_manager: @auth_manager,
|
|
112
|
-
command_catalog: @command_catalog
|
|
125
|
+
command_catalog: @command_catalog,
|
|
126
|
+
skill_catalog: @skill_catalog
|
|
113
127
|
)
|
|
114
128
|
end
|
|
115
129
|
|
|
@@ -125,7 +139,7 @@ module RubyCoded
|
|
|
125
139
|
end
|
|
126
140
|
|
|
127
141
|
def enable_default_agent_mode!
|
|
128
|
-
return if @llm_bridge.agentic_mode
|
|
142
|
+
return if @llm_bridge.agentic_mode?
|
|
129
143
|
|
|
130
144
|
@llm_bridge.toggle_agentic_mode!(true)
|
|
131
145
|
end
|
|
@@ -149,34 +163,21 @@ module RubyCoded
|
|
|
149
163
|
@state.add_message(:system, "Model switched to #{model_name}.")
|
|
150
164
|
end
|
|
151
165
|
|
|
152
|
-
def create_bridge
|
|
153
|
-
openai_creds = @credentials_store.retrieve(:openai)
|
|
154
|
-
if openai_creds && openai_creds["auth_method"] == "oauth"
|
|
155
|
-
@state.codex_mode = true
|
|
156
|
-
ensure_valid_codex_model!
|
|
157
|
-
CodexBridge.new(@state, credentials_store: @credentials_store, auth_manager: @auth_manager)
|
|
158
|
-
else
|
|
159
|
-
@state.codex_mode = false
|
|
160
|
-
LLMBridge.new(@state)
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def ensure_valid_codex_model!
|
|
165
|
-
return if CodexModels.codex_model?(@state.model)
|
|
166
|
-
|
|
167
|
-
@state.model = CodexBridge::DEFAULT_MODEL
|
|
168
|
-
@user_config&.set_config("model", CodexBridge::DEFAULT_MODEL)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
166
|
def recreate_bridge!
|
|
172
167
|
@command_catalog.reload!
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
@llm_bridge =
|
|
176
|
-
|
|
177
|
-
@llm_bridge.toggle_plan_mode!(plan) if plan
|
|
168
|
+
@skill_catalog.reload!
|
|
169
|
+
previous_mode = @llm_bridge.mode
|
|
170
|
+
@llm_bridge = @bridge_factory.build
|
|
171
|
+
restore_bridge_mode!(previous_mode)
|
|
178
172
|
@command_handler = build_command_handler
|
|
179
173
|
end
|
|
174
|
+
|
|
175
|
+
def restore_bridge_mode!(mode)
|
|
176
|
+
case mode.name
|
|
177
|
+
when :agent then @llm_bridge.toggle_agentic_mode!(true)
|
|
178
|
+
when :plan then @llm_bridge.toggle_plan_mode!(true)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
180
181
|
end
|
|
181
182
|
end
|
|
182
183
|
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyCoded
|
|
4
|
+
module Chat
|
|
5
|
+
module BridgeCommon
|
|
6
|
+
# Recognizes "please implement the plan" style messages and
|
|
7
|
+
# switches the runtime from plan mode to agent mode so that
|
|
8
|
+
# write tools become available for execution.
|
|
9
|
+
module AutoSwitch
|
|
10
|
+
IMPLEMENTATION_PATTERNS = [
|
|
11
|
+
/\bimplement/i,
|
|
12
|
+
/\bgo ahead/i,
|
|
13
|
+
/\bproceed/i,
|
|
14
|
+
/\bexecut/i,
|
|
15
|
+
/\bejecutar?/i,
|
|
16
|
+
/\bcomenz/i,
|
|
17
|
+
/\bcomienz/i,
|
|
18
|
+
/\bhazlo/i,
|
|
19
|
+
/\bconstru[iy]/i,
|
|
20
|
+
/\badelante/i,
|
|
21
|
+
/\bdale\b/i,
|
|
22
|
+
/\bdo it/i,
|
|
23
|
+
/\bbuild it/i
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def should_auto_switch_to_agent?(input)
|
|
29
|
+
@mode.plan? && @state.respond_to?(:current_plan) && @state.current_plan &&
|
|
30
|
+
implementation_request?(input)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def implementation_request?(input)
|
|
34
|
+
IMPLEMENTATION_PATTERNS.any? { |pattern| input.match?(pattern) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def auto_switch_to_agent!
|
|
38
|
+
toggle_agentic_mode!(true)
|
|
39
|
+
@state.add_message(:system,
|
|
40
|
+
"Plan mode disabled — switching to agent mode to implement the plan.")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../runtime_mode"
|
|
4
|
+
|
|
5
|
+
module RubyCoded
|
|
6
|
+
module Chat
|
|
7
|
+
module BridgeCommon
|
|
8
|
+
# Encapsulates transitions between chat/agent/plan modes and
|
|
9
|
+
# keeps the associated State flags (agentic_mode, plan tracking,
|
|
10
|
+
# auto-approve) consistent.
|
|
11
|
+
module ModeTransitions
|
|
12
|
+
def agentic_mode?
|
|
13
|
+
@mode.agent?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def plan_mode?
|
|
17
|
+
@mode.plan?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def toggle_agentic_mode!(enabled)
|
|
21
|
+
if enabled
|
|
22
|
+
transition_to_agent!
|
|
23
|
+
else
|
|
24
|
+
transition_out_of_agent!
|
|
25
|
+
end
|
|
26
|
+
after_mode_change!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def toggle_plan_mode!(enabled)
|
|
30
|
+
if enabled
|
|
31
|
+
transition_to_plan!
|
|
32
|
+
elsif @mode.plan?
|
|
33
|
+
@mode = RuntimeMode.chat
|
|
34
|
+
end
|
|
35
|
+
after_mode_change!
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Override in the concrete bridge when reconfiguration is
|
|
41
|
+
# needed after a mode change (e.g. re-registering tools).
|
|
42
|
+
def after_mode_change!
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def transition_to_agent!
|
|
47
|
+
was_plan = @mode.plan?
|
|
48
|
+
@mode = RuntimeMode.agent
|
|
49
|
+
@state.agentic_mode = true
|
|
50
|
+
@state.deactivate_plan_mode! if was_plan
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def transition_out_of_agent!
|
|
54
|
+
@mode = RuntimeMode.chat if @mode.agent?
|
|
55
|
+
@state.agentic_mode = false
|
|
56
|
+
@state.disable_auto_approve!
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def transition_to_plan!
|
|
60
|
+
was_agent = @mode.agent?
|
|
61
|
+
@mode = RuntimeMode.plan
|
|
62
|
+
return unless was_agent
|
|
63
|
+
|
|
64
|
+
@state.agentic_mode = false
|
|
65
|
+
@state.disable_auto_approve!
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../tools/agent_cancelled_error"
|
|
4
|
+
require_relative "../../tools/tool_rejected_error"
|
|
5
|
+
|
|
6
|
+
module RubyCoded
|
|
7
|
+
module Chat
|
|
8
|
+
module BridgeCommon
|
|
9
|
+
# Shared tool-confirmation flow and cancellation signaling.
|
|
10
|
+
# Uses the State's condition variable to block the streaming
|
|
11
|
+
# thread until the TUI supplies a decision (approve/reject/cancel).
|
|
12
|
+
module ToolFlow
|
|
13
|
+
def cancel!
|
|
14
|
+
@cancel_requested = true
|
|
15
|
+
@state.mutex.synchronize { @state.tool_cv.signal }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def approve_tool!
|
|
19
|
+
@state.tool_confirmation_response = :approved
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def approve_all_tools!
|
|
23
|
+
@state.enable_auto_approve!
|
|
24
|
+
@state.tool_confirmation_response = :approved
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reject_tool!
|
|
28
|
+
@state.tool_confirmation_response = :rejected
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def poll_tool_decision
|
|
34
|
+
@state.mutex.synchronize do
|
|
35
|
+
loop do
|
|
36
|
+
return :cancelled if @cancel_requested
|
|
37
|
+
|
|
38
|
+
resp = @state.instance_variable_get(:@tool_confirmation_response)
|
|
39
|
+
return resp if %i[approved rejected].include?(resp)
|
|
40
|
+
|
|
41
|
+
@state.tool_cv.wait(@state.mutex, 0.1)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def apply_tool_decision(decision, display_name)
|
|
47
|
+
case decision
|
|
48
|
+
when :cancelled
|
|
49
|
+
@state.clear_tool_confirmation!
|
|
50
|
+
raise Tools::AgentCancelledError, "Operation cancelled by user"
|
|
51
|
+
when :approved
|
|
52
|
+
@state.resolve_tool_confirmation!(:approved)
|
|
53
|
+
when :rejected
|
|
54
|
+
@state.resolve_tool_confirmation!(:rejected)
|
|
55
|
+
raise Tools::ToolRejectedError, "User rejected #{display_name}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "runtime_mode"
|
|
4
|
+
require_relative "bridge_common/mode_transitions"
|
|
5
|
+
require_relative "bridge_common/tool_flow"
|
|
6
|
+
require_relative "bridge_common/auto_switch"
|
|
7
|
+
|
|
8
|
+
module RubyCoded
|
|
9
|
+
module Chat
|
|
10
|
+
# Shared behavior between LLMBridge and CodexBridge.
|
|
11
|
+
#
|
|
12
|
+
# Composes three concerns:
|
|
13
|
+
#
|
|
14
|
+
# - {ModeTransitions} — chat/agent/plan transitions + State sync,
|
|
15
|
+
# - {ToolFlow} — approve/reject/cancel + poll/apply decision,
|
|
16
|
+
# - {AutoSwitch} — "implement the plan" → agent mode heuristic.
|
|
17
|
+
#
|
|
18
|
+
# Consumers must:
|
|
19
|
+
# - initialize `@mode = RuntimeMode.chat` (or another mode),
|
|
20
|
+
# - initialize `@cancel_requested = false`,
|
|
21
|
+
# - override `after_mode_change!` if the backend needs to
|
|
22
|
+
# reconfigure state (e.g. the RubyLLM chat instance).
|
|
23
|
+
module BridgeCommon
|
|
24
|
+
def self.included(base)
|
|
25
|
+
base.include(ModeTransitions)
|
|
26
|
+
base.include(ToolFlow)
|
|
27
|
+
base.include(AutoSwitch)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "llm_bridge"
|
|
4
|
+
require_relative "codex_bridge"
|
|
5
|
+
require_relative "codex_models"
|
|
6
|
+
|
|
7
|
+
module RubyCoded
|
|
8
|
+
module Chat
|
|
9
|
+
# Decides which bridge (LLMBridge vs CodexBridge) to instantiate
|
|
10
|
+
# based on the stored OpenAI credentials.
|
|
11
|
+
#
|
|
12
|
+
# Extracted from Chat::App so that backend selection remains a
|
|
13
|
+
# single, testable responsibility and App stays focused on
|
|
14
|
+
# orchestration.
|
|
15
|
+
class BridgeFactory
|
|
16
|
+
def initialize(state:, credentials_store:, auth_manager:, skill_catalog:, user_config: nil)
|
|
17
|
+
@state = state
|
|
18
|
+
@credentials_store = credentials_store
|
|
19
|
+
@auth_manager = auth_manager
|
|
20
|
+
@skill_catalog = skill_catalog
|
|
21
|
+
@user_config = user_config
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build
|
|
25
|
+
return build_llm_bridge unless codex_backend?
|
|
26
|
+
|
|
27
|
+
build_codex_bridge
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def codex_backend?
|
|
33
|
+
credentials = @credentials_store.retrieve(:openai)
|
|
34
|
+
credentials && credentials["auth_method"] == "oauth"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_codex_bridge
|
|
38
|
+
@state.codex_mode = true
|
|
39
|
+
ensure_valid_codex_model!
|
|
40
|
+
CodexBridge.new(
|
|
41
|
+
@state,
|
|
42
|
+
credentials_store: @credentials_store,
|
|
43
|
+
auth_manager: @auth_manager,
|
|
44
|
+
skill_catalog: @skill_catalog
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def build_llm_bridge
|
|
49
|
+
@state.codex_mode = false
|
|
50
|
+
LLMBridge.new(@state, skill_catalog: @skill_catalog)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ensure_valid_codex_model!
|
|
54
|
+
return if CodexModels.codex_model?(@state.model)
|
|
55
|
+
|
|
56
|
+
@state.model = CodexBridge::DEFAULT_MODEL
|
|
57
|
+
@user_config&.set_config("model", CodexBridge::DEFAULT_MODEL)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -4,13 +4,7 @@ module RubyCoded
|
|
|
4
4
|
module Chat
|
|
5
5
|
class CodexBridge
|
|
6
6
|
# Retry logic and error message formatting for the Codex API client.
|
|
7
|
-
# rubocop:disable Metrics/ModuleLength
|
|
8
7
|
module ErrorHandling
|
|
9
|
-
AGENT_SWITCH_PATTERN = /
|
|
10
|
-
\b(implement|go[ ]ahead|proceed|execut|ejecutar?|comenz|
|
|
11
|
-
comienz|hazlo|constru[iy]|adelante|dale|do[ ]it|build[ ]it)\b
|
|
12
|
-
/ix
|
|
13
|
-
|
|
14
8
|
UNSUPPORTED_MODEL_PATTERN = /not supported when using Codex with a ChatGPT account/i
|
|
15
9
|
|
|
16
10
|
private
|
|
@@ -23,8 +17,7 @@ module RubyCoded
|
|
|
23
17
|
end
|
|
24
18
|
|
|
25
19
|
def reset_call_counts
|
|
26
|
-
@
|
|
27
|
-
@write_tool_call_count = 0
|
|
20
|
+
@policy.reset_counters!
|
|
28
21
|
end
|
|
29
22
|
|
|
30
23
|
def prepare_send(input)
|
|
@@ -35,17 +28,6 @@ module RubyCoded
|
|
|
35
28
|
@state.add_message(:assistant, "")
|
|
36
29
|
end
|
|
37
30
|
|
|
38
|
-
def should_auto_switch_to_agent?(input)
|
|
39
|
-
@plan_mode && @state.respond_to?(:current_plan) && @state.current_plan &&
|
|
40
|
-
input.match?(AGENT_SWITCH_PATTERN)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def auto_switch_to_agent!
|
|
44
|
-
toggle_agentic_mode!(true)
|
|
45
|
-
@state.add_message(:system,
|
|
46
|
-
"Plan mode disabled — switching to agent mode to implement the plan.")
|
|
47
|
-
end
|
|
48
|
-
|
|
49
31
|
def attempt_with_retries(input, retries = 0, fallback_attempted: false)
|
|
50
32
|
perform_codex_request(input)
|
|
51
33
|
rescue Tools::AgentCancelledError, Tools::AgentIterationLimitError, Tools::ToolRejectedError => e
|
|
@@ -145,7 +127,6 @@ module RubyCoded
|
|
|
145
127
|
MSG
|
|
146
128
|
end
|
|
147
129
|
end
|
|
148
|
-
# rubocop:enable Metrics/ModuleLength
|
|
149
130
|
end
|
|
150
131
|
end
|
|
151
132
|
end
|
|
@@ -12,9 +12,6 @@ module RubyCoded
|
|
|
12
12
|
"OpenAI-Beta" => "responses=experimental"
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
|
-
DEFAULT_INSTRUCTIONS = "You are a helpful coding assistant. " \
|
|
16
|
-
"Answer concisely and provide code examples when relevant."
|
|
17
|
-
|
|
18
15
|
private
|
|
19
16
|
|
|
20
17
|
def codex_headers
|
|
@@ -29,7 +26,7 @@ module RubyCoded
|
|
|
29
26
|
|
|
30
27
|
def build_request_body
|
|
31
28
|
body = base_request_body
|
|
32
|
-
body[:tools] = build_tools_spec if @
|
|
29
|
+
body[:tools] = build_tools_spec if @mode.allows_tools?
|
|
33
30
|
body
|
|
34
31
|
end
|
|
35
32
|
|
|
@@ -68,20 +65,11 @@ module RubyCoded
|
|
|
68
65
|
end
|
|
69
66
|
|
|
70
67
|
def build_instructions
|
|
71
|
-
|
|
72
|
-
Tools::SystemPrompt.build(
|
|
73
|
-
project_root: @project_root, max_write_rounds: MAX_WRITE_TOOL_ROUNDS,
|
|
74
|
-
max_total_rounds: MAX_TOTAL_TOOL_ROUNDS
|
|
75
|
-
)
|
|
76
|
-
elsif @plan_mode
|
|
77
|
-
Tools::PlanSystemPrompt.build(project_root: @project_root)
|
|
78
|
-
else
|
|
79
|
-
DEFAULT_INSTRUCTIONS
|
|
80
|
-
end
|
|
68
|
+
@prompt_builder.build(@mode)
|
|
81
69
|
end
|
|
82
70
|
|
|
83
71
|
def build_tools_spec
|
|
84
|
-
tool_instances = if @
|
|
72
|
+
tool_instances = if @mode.allows_mutation?
|
|
85
73
|
@tool_registry.build_tools
|
|
86
74
|
else
|
|
87
75
|
@tool_registry.build_readonly_tools
|