openclacky 0.9.26 → 0.9.28
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/.clacky/skills/gem-release/SKILL.md +8 -4
- data/CHANGELOG.md +41 -0
- data/lib/clacky/agent/llm_caller.rb +110 -10
- data/lib/clacky/agent/skill_manager.rb +11 -4
- data/lib/clacky/agent.rb +16 -22
- data/lib/clacky/agent_config.rb +115 -1
- data/lib/clacky/brand_config.rb +182 -105
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/browser-setup/SKILL.md +296 -71
- data/lib/clacky/default_skills/code-explorer/SKILL.md +1 -0
- data/lib/clacky/default_skills/new/SKILL.md +22 -2
- data/lib/clacky/default_skills/new/scripts/create_rails_project.sh +30 -33
- data/lib/clacky/default_skills/personal-website/publish.rb +59 -38
- data/lib/clacky/message_format/anthropic.rb +9 -1
- data/lib/clacky/message_format/bedrock.rb +4 -2
- data/lib/clacky/platform_http_client.rb +209 -0
- data/lib/clacky/providers.rb +39 -0
- data/lib/clacky/server/browser_manager.rb +66 -5
- data/lib/clacky/server/http_server.rb +132 -13
- data/lib/clacky/server/web_ui_controller.rb +42 -1
- data/lib/clacky/skill.rb +10 -7
- data/lib/clacky/skill_loader.rb +37 -5
- data/lib/clacky/tools/base.rb +3 -0
- data/lib/clacky/tools/browser.rb +3 -50
- data/lib/clacky/tools/file_reader.rb +9 -13
- data/lib/clacky/tools/glob.rb +5 -5
- data/lib/clacky/tools/grep.rb +1 -1
- data/lib/clacky/tools/safe_shell.rb +2 -2
- data/lib/clacky/tools/shell.rb +42 -42
- data/lib/clacky/ui2/ui_controller.rb +34 -30
- data/lib/clacky/ui_interface.rb +1 -0
- data/lib/clacky/utils/browser_detector.rb +73 -27
- data/lib/clacky/utils/file_processor.rb +122 -2
- data/lib/clacky/utils/scripts_manager.rb +1 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +574 -4
- data/lib/clacky/web/app.js +198 -16
- data/lib/clacky/web/brand.js +66 -16
- data/lib/clacky/web/creator.js +418 -0
- data/lib/clacky/web/i18n.js +86 -0
- data/lib/clacky/web/index.html +98 -0
- data/lib/clacky/web/sessions.js +136 -16
- data/lib/clacky/web/settings.js +15 -2
- data/lib/clacky/web/skills.js +62 -177
- data/lib/clacky/web/ws.js +0 -1
- data/lib/clacky.rb +3 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +36 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +156 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +164 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.sh +205 -307
- data/scripts/install_browser.sh +313 -114
- data/scripts/install_full.sh +528 -589
- data/scripts/install_rails_deps.sh +725 -0
- data/scripts/install_system_deps.sh +364 -128
- data/scripts/uninstall.sh +213 -89
- metadata +19 -2
- data/lib/clacky/default_skills/new/scripts/rails_env_checker.sh +0 -389
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e9d5f6bf142facc899ab2087b5f245e1e3b7aa5135548a42455fea73e685def
|
|
4
|
+
data.tar.gz: f3d3251db1a6245ff9bdbe37433c9e70409c1975c718f18a68b7af82d17a38a4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a300098516c8081374fa6d55a21f4884f8d70b43c12b1283643a86e03fdb81d96f765a81bc14e703b6e4584bbe3fec1a7b08eadb5488ff5de2223fda922ca9e8
|
|
7
|
+
data.tar.gz: 64d73189f3852cf6fc064a61d065e762c6ba2e32d4d254736aebdcaff2106be2b55ea3ad349a1898778460e17142a06977250d8053f7db6bfe5525093f701b55
|
|
@@ -132,10 +132,13 @@ To use this skill, simply say:
|
|
|
132
132
|
|
|
133
133
|
6. **Sync scripts/ to OSS**
|
|
134
134
|
|
|
135
|
-
After updating latest.txt,
|
|
135
|
+
After updating latest.txt, first rebuild all shell scripts from templates, then sync to OSS:
|
|
136
136
|
|
|
137
137
|
```bash
|
|
138
|
-
#
|
|
138
|
+
# Step 1: Rebuild .sh files from .sh.cc templates
|
|
139
|
+
bash scripts/build/build.sh
|
|
140
|
+
|
|
141
|
+
# Step 2: Upload each script file to OSS
|
|
139
142
|
for script in scripts/*; do
|
|
140
143
|
coscli cp "$script" cos://clackyai-1258723534/clacky-ai/openclacky/main/scripts/$(basename "$script")
|
|
141
144
|
done
|
|
@@ -144,7 +147,7 @@ To use this skill, simply say:
|
|
|
144
147
|
curl -fsSL https://oss.1024code.com/clacky-ai/openclacky/main/scripts/install.sh | head -5
|
|
145
148
|
```
|
|
146
149
|
|
|
147
|
-
This ensures `scripts/install.sh`, `scripts/install_simple.sh`, `scripts/install.ps1`, `scripts/uninstall.sh` and any future scripts are
|
|
150
|
+
This ensures `scripts/install.sh`, `scripts/install_simple.sh`, `scripts/install.ps1`, `scripts/uninstall.sh` and any future scripts are compiled from latest templates and mirrored on OSS.
|
|
148
151
|
|
|
149
152
|
> **Prerequisite**: Same `coscli` setup as above
|
|
150
153
|
|
|
@@ -338,7 +341,8 @@ echo "X.Y.Z" > /tmp/latest.txt
|
|
|
338
341
|
coscli cp /tmp/latest.txt cos://clackyai-1258723534/openclacky/latest.txt
|
|
339
342
|
curl -fsSL https://oss.1024code.com/openclacky/latest.txt # verify
|
|
340
343
|
|
|
341
|
-
# Sync scripts/ to OSS
|
|
344
|
+
# Sync scripts/ to OSS (build from templates first)
|
|
345
|
+
bash scripts/build/build.sh
|
|
342
346
|
for script in scripts/*; do
|
|
343
347
|
coscli cp "$script" cos://clackyai-1258723534/clacky-ai/openclacky/main/scripts/$(basename "$script")
|
|
344
348
|
done
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.28] - 2026-04-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Creator menu**: new creator-focused UI for managing brand skills and customizations
|
|
14
|
+
- **Provider fallback system**: automatic fallback to secondary AI providers when primary provider fails
|
|
15
|
+
- **Chinese localization**: full UI translation for skill descriptions and session lists
|
|
16
|
+
- **Session scroll improvements**: better session navigation and scrolling behavior in Web UI
|
|
17
|
+
- **Brand logo support**: custom logos and icons for white-label deployments
|
|
18
|
+
|
|
19
|
+
### Improved
|
|
20
|
+
- **Browser setup skill**: enhanced browser-setup SKILL with more detailed instructions and error handling
|
|
21
|
+
- **Browser port detection**: more robust detection logic for Chrome/Edge debugging port
|
|
22
|
+
|
|
23
|
+
### More
|
|
24
|
+
- Test suite improvements and fixes
|
|
25
|
+
|
|
26
|
+
## [0.9.27] - 2026-04-07
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- **Image understanding in file_reader**: the agent can now read and analyse images (PNG, JPG, GIF, WebP) by sending them to the vision API — just attach or reference an image file
|
|
30
|
+
- **Image auto-resize before upload**: large images are automatically resized to fit within model limits (max 5 MB base64), so vision requests no longer fail on high-resolution files
|
|
31
|
+
- **Rails project installer script**: new `install_rails_deps.sh` script sets up a complete Ruby on Rails development environment (Ruby, Bundler, Node, Yarn, PostgreSQL) in one command
|
|
32
|
+
- **Uninstall script**: new `scripts/uninstall.sh` to cleanly remove the openclacky gem and its associated files
|
|
33
|
+
- **Shell script build system**: `scripts/build/` now contains a template compiler (`.sh.cc` → `.sh`) with dependency checking — install scripts are generated from composable library modules
|
|
34
|
+
- **stdout streaming in Web UI**: agent tool output and shell results are now streamed live to the browser as they arrive, rather than waiting for a full response
|
|
35
|
+
- **Ctrl+O shortcut in CLI**: pressing Ctrl+O in the terminal UI opens a file/output viewer for the current session
|
|
36
|
+
|
|
37
|
+
### Improved
|
|
38
|
+
- **Smart error recovery on 400 responses**: the agent now rolls back its message history when an API request is rejected as malformed (BadRequestError), preventing the same bad message from being replayed on every subsequent turn
|
|
39
|
+
- **Brand skill reliability**: brand skills now auto-retry on transient failures and fall back gracefully if the remote skill is temporarily unavailable
|
|
40
|
+
- **Shell tool RC file loading**: shell commands now correctly source `.bashrc` / `.zshrc` so user-defined aliases and environment variables are available inside tool executions
|
|
41
|
+
- **Shell UTF-8 encoding**: fixed a warning about character encoding when shell output contains non-ASCII characters
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- **Shell UTF-8 warning suppression**: eliminated noisy encoding warnings that appeared in shell tool output on some macOS setups
|
|
45
|
+
|
|
46
|
+
### More
|
|
47
|
+
- Lite mode configuration groundwork for clackyai platform
|
|
48
|
+
- Rails new-project skill updated with improved environment checker
|
|
49
|
+
- `new` skill improvements: http_server integration and tool category support
|
|
50
|
+
|
|
10
51
|
## [0.9.26] - 2026-04-03
|
|
11
52
|
|
|
12
53
|
### Added
|
|
@@ -3,17 +3,46 @@
|
|
|
3
3
|
module Clacky
|
|
4
4
|
class Agent
|
|
5
5
|
# LLM API call management
|
|
6
|
-
# Handles API calls with retry logic and progress indication
|
|
6
|
+
# Handles API calls with retry logic, fallback model support, and progress indication
|
|
7
7
|
module LlmCaller
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# Number of consecutive RetryableError failures (503/429/5xx) before switching to fallback.
|
|
9
|
+
# Network-level errors (connection failures, timeouts) do NOT trigger fallback — they are
|
|
10
|
+
# retried on the primary model for the full max_retries budget, since they are likely
|
|
11
|
+
# transient infrastructure blips rather than a model-level outage.
|
|
12
|
+
RETRIES_BEFORE_FALLBACK = 3
|
|
13
|
+
|
|
14
|
+
# After switching to the fallback model, allow this many retries before giving up.
|
|
15
|
+
# Kept lower than max_retries (10) because we have already exhausted the primary model.
|
|
16
|
+
MAX_RETRIES_ON_FALLBACK = 5
|
|
17
|
+
|
|
18
|
+
# Execute LLM API call with progress indicator, retry logic, and cost tracking.
|
|
19
|
+
#
|
|
20
|
+
# Fallback / probing state machine (driven by AgentConfig):
|
|
21
|
+
#
|
|
22
|
+
# :primary_ok (nil)
|
|
23
|
+
# Normal operation — use the configured model.
|
|
24
|
+
# After RETRIES_BEFORE_FALLBACK consecutive failures → :fallback_active
|
|
25
|
+
#
|
|
26
|
+
# :fallback_active
|
|
27
|
+
# Use fallback model. After FALLBACK_COOLING_OFF_SECONDS (30 min) the
|
|
28
|
+
# config transitions to :probing on the next call_llm entry.
|
|
29
|
+
#
|
|
30
|
+
# :probing
|
|
31
|
+
# Silently attempt the primary model once.
|
|
32
|
+
# Success → config transitions back to :primary_ok, user notified.
|
|
33
|
+
# Failure → renew cooling-off clock, back to :fallback_active, then
|
|
34
|
+
# retry the *same* request with the fallback model so the
|
|
35
|
+
# user experiences no extra delay.
|
|
36
|
+
#
|
|
10
37
|
# @return [Hash] API response with :content, :tool_calls, :usage, etc.
|
|
11
38
|
private def call_llm
|
|
39
|
+
# Transition :fallback_active → :probing if cooling-off has expired.
|
|
40
|
+
@config.maybe_start_probing
|
|
41
|
+
|
|
12
42
|
@ui&.show_progress
|
|
13
43
|
|
|
14
44
|
tools_to_send = @tool_registry.all_definitions
|
|
15
45
|
|
|
16
|
-
# Retry logic for network failures
|
|
17
46
|
max_retries = 10
|
|
18
47
|
retry_delay = 5
|
|
19
48
|
retries = 0
|
|
@@ -34,9 +63,23 @@ module Clacky
|
|
|
34
63
|
max_tokens: @config.max_tokens,
|
|
35
64
|
enable_caching: @config.enable_prompt_caching
|
|
36
65
|
)
|
|
66
|
+
|
|
67
|
+
# Successful response — if we were probing, confirm primary is healthy.
|
|
68
|
+
handle_probe_success if @config.probing?
|
|
69
|
+
|
|
37
70
|
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
38
71
|
@ui&.clear_progress
|
|
39
72
|
retries += 1
|
|
73
|
+
|
|
74
|
+
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
75
|
+
if @config.probing?
|
|
76
|
+
handle_probe_failure
|
|
77
|
+
retry
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Network-level errors (timeouts, connection failures) are likely transient
|
|
81
|
+
# infrastructure blips — do NOT trigger fallback. Just retry on the current
|
|
82
|
+
# model (primary or already-active fallback) up to max_retries.
|
|
40
83
|
if retries <= max_retries
|
|
41
84
|
@ui&.show_warning("Network failed: #{e.message}. Retry #{retries}/#{max_retries}...")
|
|
42
85
|
sleep retry_delay
|
|
@@ -45,29 +88,86 @@ module Clacky
|
|
|
45
88
|
@ui&.show_error("Network failed after #{max_retries} retries: #{e.message}")
|
|
46
89
|
raise AgentError, "Network connection failed after #{max_retries} retries: #{e.message}"
|
|
47
90
|
end
|
|
91
|
+
|
|
48
92
|
rescue RetryableError => e
|
|
49
93
|
@ui&.clear_progress
|
|
50
94
|
retries += 1
|
|
51
|
-
|
|
52
|
-
|
|
95
|
+
|
|
96
|
+
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
97
|
+
if @config.probing?
|
|
98
|
+
handle_probe_failure
|
|
99
|
+
retry
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# RetryableError (503/429/5xx/ThrottlingException) signals a service-level outage.
|
|
103
|
+
# After RETRIES_BEFORE_FALLBACK attempts, switch to the fallback model and reset the
|
|
104
|
+
# retry counter — but cap fallback retries at MAX_RETRIES_ON_FALLBACK (< max_retries)
|
|
105
|
+
# since we have already confirmed the primary is struggling.
|
|
106
|
+
current_max = @config.fallback_active? ? MAX_RETRIES_ON_FALLBACK : max_retries
|
|
107
|
+
|
|
108
|
+
if retries <= current_max
|
|
109
|
+
if retries == RETRIES_BEFORE_FALLBACK && !@config.fallback_active?
|
|
110
|
+
if try_activate_fallback(current_model)
|
|
111
|
+
retries = 0
|
|
112
|
+
retry
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
@ui&.show_warning("#{e.message} (#{retries}/#{current_max})")
|
|
53
116
|
sleep retry_delay
|
|
54
117
|
retry
|
|
55
118
|
else
|
|
56
|
-
@ui&.show_error("LLM service unavailable after #{
|
|
57
|
-
raise AgentError, "LLM service unavailable after #{
|
|
119
|
+
@ui&.show_error("LLM service unavailable after #{current_max} retries. Please try again later.")
|
|
120
|
+
raise AgentError, "LLM service unavailable after #{current_max} retries"
|
|
58
121
|
end
|
|
122
|
+
|
|
59
123
|
ensure
|
|
60
124
|
@ui&.clear_progress
|
|
61
125
|
end
|
|
62
126
|
|
|
63
127
|
# Track cost and collect token usage data.
|
|
64
|
-
# token_data is returned to the caller so it can be displayed
|
|
65
|
-
# after show_assistant_message (ensuring correct ordering in WebUI).
|
|
66
128
|
token_data = track_cost(response[:usage], raw_api_usage: response[:raw_api_usage])
|
|
67
129
|
response[:token_usage] = token_data
|
|
68
130
|
|
|
69
131
|
response
|
|
70
132
|
end
|
|
133
|
+
|
|
134
|
+
# Attempt to activate the provider fallback model for the given primary model.
|
|
135
|
+
# Shows a user-visible warning when switching. Returns true if a fallback was found
|
|
136
|
+
# and activated, false if no fallback is configured.
|
|
137
|
+
# @param failed_model [String] the model name that is currently failing
|
|
138
|
+
# @return [Boolean]
|
|
139
|
+
private def try_activate_fallback(failed_model)
|
|
140
|
+
fallback = @config.fallback_model_for(failed_model)
|
|
141
|
+
return false unless fallback
|
|
142
|
+
|
|
143
|
+
@config.activate_fallback!(fallback)
|
|
144
|
+
@ui&.show_warning(
|
|
145
|
+
"Model #{failed_model} appears unavailable. " \
|
|
146
|
+
"Automatically switching to fallback model: #{fallback}"
|
|
147
|
+
)
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Called when a probe attempt (testing primary after cooling-off) succeeds.
|
|
152
|
+
# Resets the state machine to :primary_ok and notifies the user.
|
|
153
|
+
private def handle_probe_success
|
|
154
|
+
primary = @config.model_name
|
|
155
|
+
@config.confirm_fallback_ok!
|
|
156
|
+
@ui&.show_warning("Primary model #{primary} is healthy again. Switched back automatically.")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Called when a probe attempt fails.
|
|
160
|
+
# Renews the cooling-off clock (back to :fallback_active) so the *same*
|
|
161
|
+
# request is immediately retried with the fallback model — no extra delay.
|
|
162
|
+
private def handle_probe_failure
|
|
163
|
+
fallback = @config.instance_variable_get(:@fallback_model)
|
|
164
|
+
primary = @config.model_name
|
|
165
|
+
@config.activate_fallback!(fallback) # renews @fallback_since
|
|
166
|
+
@ui&.show_warning(
|
|
167
|
+
"Primary model #{primary} still unavailable. " \
|
|
168
|
+
"Continuing with fallback model: #{fallback}"
|
|
169
|
+
)
|
|
170
|
+
end
|
|
71
171
|
end
|
|
72
172
|
end
|
|
73
173
|
end
|
|
@@ -379,9 +379,6 @@ module Clacky
|
|
|
379
379
|
# @param arguments [String] Arguments for the skill
|
|
380
380
|
# @return [String] Summary of subagent execution
|
|
381
381
|
def execute_skill_with_subagent(skill, arguments)
|
|
382
|
-
# Log subagent fork
|
|
383
|
-
@ui&.show_info("Subagent start: #{skill.identifier}")
|
|
384
|
-
|
|
385
382
|
# For encrypted brand skills with supporting scripts: decrypt to a tmpdir.
|
|
386
383
|
# Subagent path has a clear boundary (subagent.run returns), so we shred inline
|
|
387
384
|
# rather than registering on the parent agent.
|
|
@@ -406,6 +403,10 @@ module Clacky
|
|
|
406
403
|
system_prompt_suffix: skill_instructions
|
|
407
404
|
)
|
|
408
405
|
|
|
406
|
+
# Log which model the subagent is actually using (may differ from requested
|
|
407
|
+
# when "lite" falls back to default due to no lite model configured)
|
|
408
|
+
@ui&.show_info("Subagent start: #{skill.identifier} [#{subagent.current_model_info[:model]}]")
|
|
409
|
+
|
|
409
410
|
# Run subagent with the actual task as the sole user turn.
|
|
410
411
|
# If the user typed the skill command with no arguments (e.g. "/jade-appraisal"),
|
|
411
412
|
# use a generic trigger phrase so the user message is never empty.
|
|
@@ -446,8 +447,14 @@ module Clacky
|
|
|
446
447
|
m[:skill_name] = skill.identifier
|
|
447
448
|
end
|
|
448
449
|
|
|
450
|
+
# Merge subagent cost into parent agent's total so the sessionbar reflects
|
|
451
|
+
# the real cumulative spend across all subagents
|
|
452
|
+
subagent_cost = result[:total_cost_usd] || 0.0
|
|
453
|
+
@total_cost += subagent_cost
|
|
454
|
+
@ui&.update_sessionbar(cost: @total_cost)
|
|
455
|
+
|
|
449
456
|
# Log completion
|
|
450
|
-
@ui&.show_info("Subagent completed: #{result[:iterations]} iterations, $#{
|
|
457
|
+
@ui&.show_info("Subagent completed: #{result[:iterations]} iterations, $#{subagent_cost.round(4)} (total: $#{@total_cost.round(4)})")
|
|
451
458
|
|
|
452
459
|
# Return summary as the skill execution result
|
|
453
460
|
summary
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -159,9 +159,9 @@ module Clacky
|
|
|
159
159
|
}
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
# Get current model name
|
|
162
|
+
# Get current model name (respects any active fallback override)
|
|
163
163
|
private def current_model
|
|
164
|
-
@config.
|
|
164
|
+
@config.effective_model_name
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
# Rename this session. Called by auto-naming (first message) or user explicit rename.
|
|
@@ -432,6 +432,11 @@ module Clacky
|
|
|
432
432
|
}
|
|
433
433
|
Clacky::Logger.error("agent_run_error", error: e)
|
|
434
434
|
|
|
435
|
+
# 400 errors mean our request was malformed — roll back history so the bad
|
|
436
|
+
# message is not replayed on the next user turn.
|
|
437
|
+
# Other errors (auth, network, etc.) leave history intact for retry.
|
|
438
|
+
@pending_error_rollback = true if e.is_a?(Clacky::BadRequestError)
|
|
439
|
+
|
|
435
440
|
# Build error result for session data, but let CLI handle error display
|
|
436
441
|
result = build_result(:error, error: e.message) # rubocop:disable Lint/UselessAssignment
|
|
437
442
|
raise
|
|
@@ -647,35 +652,24 @@ module Clacky
|
|
|
647
652
|
# Automatic progress display after 2 seconds for any tool execution
|
|
648
653
|
progress_shown = false
|
|
649
654
|
progress_timer = nil
|
|
650
|
-
output_buffer = nil
|
|
651
655
|
|
|
652
656
|
if @ui
|
|
653
657
|
progress_message = build_tool_progress_message(call[:name], args)
|
|
654
658
|
|
|
655
|
-
# For shell
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
+
# For shell/safe_shell: inject on_output callback for real-time stdout streaming.
|
|
660
|
+
# The callback fires immediately on each read_nonblock chunk — no polling delay.
|
|
661
|
+
if (call[:name] == "shell" || call[:name] == "safe_shell") &&
|
|
662
|
+
@ui.respond_to?(:show_tool_stdout)
|
|
663
|
+
args[:on_output] = ->(stream, data) {
|
|
664
|
+
@ui.show_tool_stdout([data]) if stream == :stdout
|
|
665
|
+
}
|
|
659
666
|
end
|
|
660
667
|
|
|
661
668
|
progress_timer = Thread.new do
|
|
662
669
|
sleep 2
|
|
663
|
-
@ui.show_progress(progress_message, prefix_newline: false
|
|
670
|
+
@ui.show_progress(progress_message, prefix_newline: false)
|
|
664
671
|
progress_shown = true
|
|
665
|
-
|
|
666
|
-
# For shell commands: stream new stdout lines to WebUI as they arrive
|
|
667
|
-
if output_buffer && @ui.respond_to?(:show_tool_stdout)
|
|
668
|
-
last_sent_count = 0
|
|
669
|
-
loop do
|
|
670
|
-
sleep 1
|
|
671
|
-
stdout_lines = output_buffer[:stdout_lines]&.to_a || []
|
|
672
|
-
new_lines = stdout_lines[last_sent_count..]
|
|
673
|
-
if new_lines && !new_lines.empty?
|
|
674
|
-
@ui.show_tool_stdout(new_lines)
|
|
675
|
-
last_sent_count = stdout_lines.size
|
|
676
|
-
end
|
|
677
|
-
end
|
|
678
|
-
end
|
|
672
|
+
# Streaming is handled by on_output callback — no polling loop needed here
|
|
679
673
|
end
|
|
680
674
|
end
|
|
681
675
|
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -222,9 +222,45 @@ module Clacky
|
|
|
222
222
|
end
|
|
223
223
|
end
|
|
224
224
|
|
|
225
|
+
# Auto-inject lite model from provider preset when:
|
|
226
|
+
# 1. A default model exists
|
|
227
|
+
# 2. No lite model is configured yet (neither in file nor env)
|
|
228
|
+
# 3. The default model's provider has a known lite_model
|
|
229
|
+
# The injected lite model is runtime-only (not persisted to config.yml)
|
|
230
|
+
inject_provider_lite_model(models)
|
|
231
|
+
|
|
225
232
|
new(models: models)
|
|
226
233
|
end
|
|
227
234
|
|
|
235
|
+
# Auto-inject a lite model entry if the default model's provider supports one
|
|
236
|
+
# and no lite model is already present. The injected entry reuses the same
|
|
237
|
+
# api_key and base_url as the default model — only the model name differs.
|
|
238
|
+
# @param models [Array<Hash>] mutable models array (modified in-place)
|
|
239
|
+
private_class_method def self.inject_provider_lite_model(models)
|
|
240
|
+
return if models.any? { |m| m["type"] == "lite" }
|
|
241
|
+
|
|
242
|
+
default_model = models.find { |m| m["type"] == "default" } || models.first
|
|
243
|
+
return unless default_model
|
|
244
|
+
|
|
245
|
+
provider_id = Clacky::Providers.find_by_base_url(default_model["base_url"])
|
|
246
|
+
return unless provider_id
|
|
247
|
+
|
|
248
|
+
lite_model_name = Clacky::Providers.lite_model(provider_id)
|
|
249
|
+
return unless lite_model_name
|
|
250
|
+
|
|
251
|
+
# Don't inject if the default model IS the lite model
|
|
252
|
+
return if default_model["model"] == lite_model_name
|
|
253
|
+
|
|
254
|
+
models << {
|
|
255
|
+
"api_key" => default_model["api_key"],
|
|
256
|
+
"base_url" => default_model["base_url"],
|
|
257
|
+
"model" => lite_model_name,
|
|
258
|
+
"anthropic_format" => default_model["anthropic_format"] || false,
|
|
259
|
+
"type" => "lite",
|
|
260
|
+
"auto_injected" => true # Mark as auto-injected (not saved to file)
|
|
261
|
+
}
|
|
262
|
+
end
|
|
263
|
+
|
|
228
264
|
# Save configuration to file
|
|
229
265
|
# Deep copy — models array contains mutable Hashes, so a shallow dup would
|
|
230
266
|
# let the copy share the same Hash objects with the original, causing
|
|
@@ -244,8 +280,11 @@ module Clacky
|
|
|
244
280
|
end
|
|
245
281
|
|
|
246
282
|
# Convert to YAML format (top-level array)
|
|
283
|
+
# Auto-injected lite models (auto_injected: true) are excluded from persistence —
|
|
284
|
+
# they are regenerated at load time from the provider preset.
|
|
247
285
|
def to_yaml
|
|
248
|
-
|
|
286
|
+
persistable = @models.reject { |m| m["auto_injected"] }
|
|
287
|
+
YAML.dump(persistable)
|
|
249
288
|
end
|
|
250
289
|
|
|
251
290
|
# Check if any model is configured
|
|
@@ -359,6 +398,81 @@ module Clacky
|
|
|
359
398
|
find_model_by_type("lite")
|
|
360
399
|
end
|
|
361
400
|
|
|
401
|
+
# How long to stay on the fallback model before probing the primary again.
|
|
402
|
+
FALLBACK_COOLING_OFF_SECONDS = 30 * 60 # 30 minutes
|
|
403
|
+
|
|
404
|
+
# Look up the fallback model name for the given model name.
|
|
405
|
+
# Uses the provider preset's fallback_models table.
|
|
406
|
+
# Returns nil if no fallback is configured for this model.
|
|
407
|
+
# @param model_name [String] the primary model name (e.g. "abs-claude-sonnet-4-6")
|
|
408
|
+
# @return [String, nil]
|
|
409
|
+
def fallback_model_for(model_name)
|
|
410
|
+
m = current_model
|
|
411
|
+
return nil unless m
|
|
412
|
+
|
|
413
|
+
provider_id = Clacky::Providers.find_by_base_url(m["base_url"])
|
|
414
|
+
return nil unless provider_id
|
|
415
|
+
|
|
416
|
+
Clacky::Providers.fallback_model(provider_id, model_name)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Switch to fallback model and start the cooling-off clock.
|
|
420
|
+
# Idempotent — calling again while already in :fallback_active renews the timestamp.
|
|
421
|
+
# @param fallback_model_name [String] the fallback model to use
|
|
422
|
+
def activate_fallback!(fallback_model_name)
|
|
423
|
+
@fallback_state = :fallback_active
|
|
424
|
+
@fallback_since = Time.now
|
|
425
|
+
@fallback_model = fallback_model_name
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Called at the start of every call_llm.
|
|
429
|
+
# If cooling-off has expired, transition from :fallback_active → :probing
|
|
430
|
+
# so the next request will silently test the primary model.
|
|
431
|
+
# No-op in any other state.
|
|
432
|
+
def maybe_start_probing
|
|
433
|
+
return unless @fallback_state == :fallback_active
|
|
434
|
+
return unless @fallback_since && (Time.now - @fallback_since) >= FALLBACK_COOLING_OFF_SECONDS
|
|
435
|
+
|
|
436
|
+
@fallback_state = :probing
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Called when a successful API response is received.
|
|
440
|
+
# If we were :probing (testing primary after cooling-off), this confirms
|
|
441
|
+
# the primary model is healthy again and resets everything.
|
|
442
|
+
# No-op in :primary_ok or :fallback_active states.
|
|
443
|
+
def confirm_fallback_ok!
|
|
444
|
+
return unless @fallback_state == :probing
|
|
445
|
+
|
|
446
|
+
@fallback_state = nil
|
|
447
|
+
@fallback_since = nil
|
|
448
|
+
@fallback_model = nil
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Returns true when a fallback model is currently being used
|
|
452
|
+
# (:fallback_active or :probing states).
|
|
453
|
+
def fallback_active?
|
|
454
|
+
@fallback_state == :fallback_active || @fallback_state == :probing
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Returns true only when we are silently probing the primary model.
|
|
458
|
+
def probing?
|
|
459
|
+
@fallback_state == :probing
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# The effective model name to use for API calls.
|
|
463
|
+
# - :primary_ok / nil → configured model_name (primary)
|
|
464
|
+
# - :fallback_active → fallback model
|
|
465
|
+
# - :probing → configured model_name (trying primary silently)
|
|
466
|
+
def effective_model_name
|
|
467
|
+
case @fallback_state
|
|
468
|
+
when :fallback_active
|
|
469
|
+
@fallback_model || model_name
|
|
470
|
+
else
|
|
471
|
+
# :primary_ok (nil) and :probing both use the primary model
|
|
472
|
+
model_name
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
362
476
|
# Get current model configuration
|
|
363
477
|
# Looks for type: default first, falls back to current_model_index
|
|
364
478
|
def current_model
|