openclacky 1.2.5 → 1.2.7
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 +43 -0
- data/README.md +34 -0
- data/README_CN.md +34 -0
- data/lib/clacky/agent/cost_tracker.rb +24 -10
- data/lib/clacky/agent/llm_caller.rb +25 -3
- data/lib/clacky/agent/message_compressor.rb +2 -1
- data/lib/clacky/agent/message_compressor_helper.rb +6 -2
- data/lib/clacky/agent/session_serializer.rb +23 -4
- data/lib/clacky/agent/tool_executor.rb +14 -0
- data/lib/clacky/agent/tool_registry.rb +0 -7
- data/lib/clacky/agent.rb +43 -10
- data/lib/clacky/agent_config.rb +54 -6
- data/lib/clacky/billing/billing_store.rb +62 -4
- data/lib/clacky/brand_config.rb +5 -0
- data/lib/clacky/cli.rb +76 -24
- data/lib/clacky/client.rb +59 -4
- data/lib/clacky/default_parsers/wps_parser.rb +82 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +5 -2
- data/lib/clacky/message_format/anthropic.rb +13 -3
- data/lib/clacky/message_format/bedrock.rb +2 -2
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/platform_http_client.rb +28 -1
- data/lib/clacky/providers.rb +11 -29
- data/lib/clacky/server/channel/channel_manager.rb +148 -12
- data/lib/clacky/server/channel/channel_ui_controller.rb +4 -2
- data/lib/clacky/server/http_server.rb +133 -13
- data/lib/clacky/server/session_registry.rb +30 -4
- data/lib/clacky/server/web_ui_controller.rb +6 -3
- data/lib/clacky/tools/browser.rb +4 -13
- data/lib/clacky/tools/terminal.rb +23 -27
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/file_processor.rb +3 -0
- data/lib/clacky/utils/parser_manager.rb +3 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +659 -75
- data/lib/clacky/web/app.js +0 -1
- data/lib/clacky/web/billing.js +371 -99
- data/lib/clacky/web/i18n.js +48 -2
- data/lib/clacky/web/index.html +34 -1
- data/lib/clacky/web/sessions.js +213 -82
- data/lib/clacky/web/settings.js +59 -17
- data/lib/clacky/web/workspace.js +204 -0
- data/lib/clacky/web/ws-dispatcher.js +19 -3
- data/lib/clacky.rb +9 -3
- metadata +4 -5
- data/lib/clacky/tools/list_tasks.rb +0 -54
- data/lib/clacky/tools/redo_task.rb +0 -41
- data/lib/clacky/tools/undo_task.rb +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8057fdabcb077c8ca378fcbbc0efa442abc6b9664464d2d8d1b737e0206d2ed9
|
|
4
|
+
data.tar.gz: 657f282a20664ef793d7ff663e963739f5fbdb478b8e174df9c5e459ec09231b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fad3e045271032a1150745f1d8531aeed24ea376efdcd99346aeaeec4eb5f1edec187e9dbe38ee8d19e5a9cf599bfb7e5431ad55e5993f1dd243bb4ebf5faa2d
|
|
7
|
+
data.tar.gz: 95b1a7ec783b459f5c70b99d3535f5a0054d4a8c72d855d9c98b9771cde9d59cb33dcf609be844fade932e1487c82c525cf4354438c5ad4b271494a4166dc729
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.7] - 2026-06-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Session workspace file explorer — browse and download files in Web UI sidebar
|
|
12
|
+
- Channel manager: new, clear, and skill commands
|
|
13
|
+
- Top-up link in Web UI under billing section
|
|
14
|
+
- Sub-model switching within active sessions
|
|
15
|
+
- Claude 4.8 model support
|
|
16
|
+
- Connection test improvements for custom Claude models
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Nil error in compress top match parsing
|
|
20
|
+
- Empty 0-token responses causing agent stalls (close #218)
|
|
21
|
+
- Brand config built-in skills loaded without MANIFEST.enc.json (C-5627)
|
|
22
|
+
- Terminal .exe stdin isolation at command-wrap on WSL (close #221)
|
|
23
|
+
- Overly verbose builtin skills installed message during onboarding
|
|
24
|
+
|
|
25
|
+
### More
|
|
26
|
+
- Add Docker installation section to README
|
|
27
|
+
|
|
28
|
+
## [1.2.6] - 2026-05-29
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- WPS format support for document processing
|
|
32
|
+
- `--json` flag support with `-m` option
|
|
33
|
+
|
|
34
|
+
### Improved
|
|
35
|
+
- Billing UI and model filtering experience
|
|
36
|
+
- Brand skill download now retries on failure
|
|
37
|
+
- DeepSeek compatibility handling within OpenRouter provider
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
- Claude `tool_use.id` error when switching between models
|
|
41
|
+
- Browser page navigation pageid bug
|
|
42
|
+
- Browser IIFE execution issue
|
|
43
|
+
- WSL UTF-8 command encoding bug
|
|
44
|
+
- File preview path directory resolution
|
|
45
|
+
|
|
46
|
+
### More
|
|
47
|
+
- Remove list/undo/redo task tool
|
|
48
|
+
- Clean up legacy provider code
|
|
49
|
+
- Improve platform error messages for easier diagnostics
|
|
50
|
+
|
|
8
51
|
## [1.2.5] - 2026-05-28
|
|
9
52
|
|
|
10
53
|
### Fixed
|
data/README.md
CHANGED
|
@@ -108,6 +108,40 @@ gem install openclacky
|
|
|
108
108
|
|
|
109
109
|
see more: https://www.openclacky.com/docs/installation
|
|
110
110
|
|
|
111
|
+
### Docker
|
|
112
|
+
|
|
113
|
+
Build:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/clacky-ai/openclacky.git
|
|
117
|
+
cd openclacky
|
|
118
|
+
docker build -t openclacky .
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Linux:**
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`--network=host` is required so the agent inside the container can reach Chrome's remote debugging port running on the host.
|
|
128
|
+
|
|
129
|
+
**macOS / Windows:**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> **Note:** On macOS/Windows, `--network=host` is not supported — browser automation may be limited.
|
|
136
|
+
|
|
137
|
+
Open **http://localhost:7070** after starting.
|
|
138
|
+
|
|
139
|
+
Environment variables:
|
|
140
|
+
|
|
141
|
+
| Variable | Description |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `CLACKY_ACCESS_KEY` | Protect the Web UI with an access key (empty = public mode) |
|
|
144
|
+
|
|
111
145
|
|
|
112
146
|
## Quick Start
|
|
113
147
|
|
data/README_CN.md
CHANGED
|
@@ -104,6 +104,40 @@ gem install openclacky
|
|
|
104
104
|
|
|
105
105
|
详见:https://www.openclacky.com/docs/installation
|
|
106
106
|
|
|
107
|
+
### Docker
|
|
108
|
+
|
|
109
|
+
构建:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
git clone https://github.com/clacky-ai/openclacky.git
|
|
113
|
+
cd openclacky
|
|
114
|
+
docker build -t openclacky .
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Linux:**
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`--network=host` 使容器与宿主机共享网络栈,Agent 可直接访问宿主机上运行的 Chrome 远程调试端口。
|
|
124
|
+
|
|
125
|
+
**macOS / Windows:**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
> **注意:** macOS/Windows 不支持 `--network=host`,浏览器自动化功能可能受限。
|
|
132
|
+
|
|
133
|
+
启动后访问 **http://localhost:7070**。
|
|
134
|
+
|
|
135
|
+
环境变量:
|
|
136
|
+
|
|
137
|
+
| 变量 | 说明 |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `CLACKY_ACCESS_KEY` | 设置访问密钥保护 Web UI(留空 = 公开模式) |
|
|
140
|
+
|
|
107
141
|
## 快速开始
|
|
108
142
|
|
|
109
143
|
### 终端(CLI)
|
|
@@ -17,7 +17,8 @@ module Clacky
|
|
|
17
17
|
# Updates total cost and displays iteration statistics
|
|
18
18
|
# @param usage [Hash] Usage data from API response
|
|
19
19
|
# @param raw_api_usage [Hash, nil] Raw API usage data for debugging
|
|
20
|
-
|
|
20
|
+
# @param model [String, nil] Model name to use for billing (defaults to current_model)
|
|
21
|
+
def track_cost(usage, raw_api_usage: nil, model: nil)
|
|
21
22
|
# Priority 1: Use API-provided cost if available (OpenRouter, LiteLLM, etc.)
|
|
22
23
|
iteration_cost = nil
|
|
23
24
|
if usage[:api_cost]
|
|
@@ -28,7 +29,10 @@ module Clacky
|
|
|
28
29
|
@ui&.log("Using API-provided cost: $#{usage[:api_cost]}", level: :debug) if @config.verbose
|
|
29
30
|
else
|
|
30
31
|
# Priority 2: Calculate from tokens using ModelPricing
|
|
31
|
-
|
|
32
|
+
# Use provided model name (from API call time) to ensure accurate billing
|
|
33
|
+
# even if the user switches models during the API call
|
|
34
|
+
billing_model = model || current_model
|
|
35
|
+
result = ModelPricing.calculate_cost(model: billing_model, usage: usage)
|
|
32
36
|
cost = result[:cost]
|
|
33
37
|
pricing_source = result[:source]
|
|
34
38
|
|
|
@@ -101,7 +105,7 @@ module Clacky
|
|
|
101
105
|
|
|
102
106
|
# Persist billing record (skip for subagents to avoid double-counting)
|
|
103
107
|
unless @is_subagent
|
|
104
|
-
persist_billing_record(usage, iteration_cost)
|
|
108
|
+
persist_billing_record(usage, iteration_cost, model: model)
|
|
105
109
|
end
|
|
106
110
|
|
|
107
111
|
# Return token_data so the caller can display it at the right moment
|
|
@@ -111,25 +115,29 @@ module Clacky
|
|
|
111
115
|
# Persist a billing record to the billing store
|
|
112
116
|
# @param usage [Hash] Usage data from API
|
|
113
117
|
# @param cost [Float, nil] Calculated cost for this iteration
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
# @param model [String, nil] Model name to use for billing (defaults to current_model)
|
|
119
|
+
def persist_billing_record(usage, cost, model: nil)
|
|
120
|
+
# Always save billing records for usage tracking, even if cost is unknown (nil).
|
|
121
|
+
# This ensures all API calls are recorded for statistics purposes.
|
|
122
|
+
billing_model = model || current_model
|
|
123
|
+
effective_cost = cost || 0.0 # Use 0 if pricing is unknown
|
|
116
124
|
|
|
117
125
|
record = Billing::BillingRecord.new(
|
|
118
126
|
session_id: @session_id,
|
|
119
127
|
timestamp: Time.now,
|
|
120
|
-
model:
|
|
128
|
+
model: billing_model,
|
|
121
129
|
prompt_tokens: usage[:prompt_tokens] || 0,
|
|
122
130
|
completion_tokens: usage[:completion_tokens] || 0,
|
|
123
131
|
cache_read_tokens: usage[:cache_read_input_tokens] || 0,
|
|
124
132
|
cache_write_tokens: usage[:cache_creation_input_tokens] || 0,
|
|
125
|
-
cost_usd:
|
|
126
|
-
cost_source: @cost_source
|
|
133
|
+
cost_usd: effective_cost,
|
|
134
|
+
cost_source: cost.nil? ? :unknown : @cost_source
|
|
127
135
|
)
|
|
128
136
|
|
|
129
137
|
billing_store.append(record)
|
|
130
138
|
rescue => e
|
|
131
139
|
# Billing persistence is non-critical; log and continue
|
|
132
|
-
|
|
140
|
+
Clacky::Logger.warn("billing.persist_error", error: e.message, model: billing_model)
|
|
133
141
|
end
|
|
134
142
|
|
|
135
143
|
# Estimate token count for a message content
|
|
@@ -169,7 +177,13 @@ module Clacky
|
|
|
169
177
|
else
|
|
170
178
|
total_tokens - @previous_total_tokens
|
|
171
179
|
end
|
|
172
|
-
|
|
180
|
+
|
|
181
|
+
# Guard: do NOT overwrite @previous_total_tokens with 0 when the upstream
|
|
182
|
+
# returns missing/zero usage (observed when history overflows the model
|
|
183
|
+
# context: response comes back as content="" + finish_reason="stop" +
|
|
184
|
+
# zero usage). Resetting to 0 would disable the compression trigger on
|
|
185
|
+
# subsequent turns and poison the session permanently.
|
|
186
|
+
@previous_total_tokens = total_tokens if total_tokens > 0
|
|
173
187
|
|
|
174
188
|
{
|
|
175
189
|
delta_tokens: delta_tokens,
|
|
@@ -88,6 +88,11 @@ module Clacky
|
|
|
88
88
|
# the error propagate.
|
|
89
89
|
context_overflow_retry_attempted = false
|
|
90
90
|
|
|
91
|
+
# Capture model name at API call time for accurate billing tracking.
|
|
92
|
+
# If the user switches models while the API call is in progress, we still
|
|
93
|
+
# want to bill under the model that actually handled the request.
|
|
94
|
+
api_call_model = current_model
|
|
95
|
+
|
|
91
96
|
begin
|
|
92
97
|
begin
|
|
93
98
|
# Use active_messages (Time Machine) when undone, otherwise send full history.
|
|
@@ -100,7 +105,7 @@ module Clacky
|
|
|
100
105
|
|
|
101
106
|
response = @client.send_messages_with_tools(
|
|
102
107
|
messages_to_send,
|
|
103
|
-
model:
|
|
108
|
+
model: api_call_model,
|
|
104
109
|
tools: tools_to_send,
|
|
105
110
|
max_tokens: @config.max_tokens,
|
|
106
111
|
enable_caching: @config.enable_prompt_caching,
|
|
@@ -124,8 +129,22 @@ module Clacky
|
|
|
124
129
|
# block below handles retry + fallback identically to 5xx/429.
|
|
125
130
|
detect_upstream_truncation!(response)
|
|
126
131
|
|
|
132
|
+
# Empty response detector: model returned nothing (no content, no
|
|
133
|
+
# tool_calls, finish_reason != "stop"). DeepSeek via OpenRouter
|
|
134
|
+
# occasionally does this. Treat as transient failure and retry.
|
|
135
|
+
if response[:content].to_s.strip.empty? &&
|
|
136
|
+
(response[:tool_calls].nil? || response[:tool_calls].empty?) &&
|
|
137
|
+
response[:finish_reason].to_s != "stop" &&
|
|
138
|
+
response[:finish_reason].to_s != "length"
|
|
139
|
+
Clacky::Logger.warn("llm.empty_response_detected",
|
|
140
|
+
model: api_call_model,
|
|
141
|
+
finish_reason: response[:finish_reason].to_s,
|
|
142
|
+
completion_tokens: response.dig(:token_usage, :completion_tokens)
|
|
143
|
+
)
|
|
144
|
+
raise RetryableError, "[LLM] Model returned empty response (no content, no tool_calls), retrying..."
|
|
145
|
+
end
|
|
146
|
+
|
|
127
147
|
rescue Faraday::TimeoutError => e
|
|
128
|
-
# ── Read-timeout path (distinct from connection-level failures) ──
|
|
129
148
|
# Faraday::TimeoutError on our non-streaming POST almost always means
|
|
130
149
|
# the *response* took longer than the 300s read-timeout to come back —
|
|
131
150
|
# i.e. the model is trying to produce a huge output in one shot
|
|
@@ -210,6 +229,7 @@ module Clacky
|
|
|
210
229
|
if retries <= current_max
|
|
211
230
|
if retries == RETRIES_BEFORE_FALLBACK && !@config.fallback_active?
|
|
212
231
|
if try_activate_fallback(current_model)
|
|
232
|
+
api_call_model = current_model
|
|
213
233
|
retries = 0
|
|
214
234
|
retry
|
|
215
235
|
end
|
|
@@ -299,7 +319,9 @@ module Clacky
|
|
|
299
319
|
end
|
|
300
320
|
|
|
301
321
|
# Track cost and collect token usage data.
|
|
302
|
-
|
|
322
|
+
# Pass the model name captured at API call time to ensure accurate billing
|
|
323
|
+
# even if the user switched models during the (potentially long) API call.
|
|
324
|
+
token_data = track_cost(response[:usage], raw_api_usage: response[:raw_api_usage], model: api_call_model)
|
|
303
325
|
response[:token_usage] = token_data
|
|
304
326
|
|
|
305
327
|
# [DIAG] Log raw client response shape. Only emit when we see the
|
|
@@ -137,7 +137,8 @@ module Clacky
|
|
|
137
137
|
# Returns the topics string if found, nil otherwise.
|
|
138
138
|
# e.g. "<topics>Rails setup, database config</topics>" → "Rails setup, database config"
|
|
139
139
|
def parse_topics(content)
|
|
140
|
-
|
|
140
|
+
return nil if content.nil? || content.to_s.empty?
|
|
141
|
+
m = content.to_s.match(/<topics>(.*?)<\/topics>/m)
|
|
141
142
|
m ? m[1].strip : nil
|
|
142
143
|
end
|
|
143
144
|
|
|
@@ -124,8 +124,12 @@ module Clacky
|
|
|
124
124
|
# Check if compression is enabled
|
|
125
125
|
return nil unless @config.enable_compression
|
|
126
126
|
|
|
127
|
-
# Use
|
|
128
|
-
|
|
127
|
+
# Use the larger of: API-reported tokens from last response, or current
|
|
128
|
+
# estimated history size. The estimate guards the case where a single
|
|
129
|
+
# huge tool result was just appended after the last API call — without
|
|
130
|
+
# this, the next request can ship the bloated history before any
|
|
131
|
+
# compression triggers (issue #218).
|
|
132
|
+
total_tokens = [@previous_total_tokens, @history.estimate_tokens].max
|
|
129
133
|
message_count = @history.size
|
|
130
134
|
|
|
131
135
|
# Force compression (for idle compression) - use lower threshold
|
|
@@ -80,12 +80,28 @@ module Clacky
|
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Re-apply the per-session sub-model pin (if any). Done AFTER
|
|
84
|
+
# switch_model_by_id so the overlay isn't cleared by the card switch
|
|
85
|
+
# invariant. Validation happens at write-time (the WebUI/API enforces
|
|
86
|
+
# the name belongs to the card's provider) — at restore-time we trust
|
|
87
|
+
# what we previously wrote.
|
|
88
|
+
saved_sub_model = session_data.dig(:config, :sub_model)
|
|
89
|
+
if saved_sub_model && !saved_sub_model.to_s.empty?
|
|
90
|
+
set_session_sub_model(saved_sub_model)
|
|
91
|
+
end
|
|
92
|
+
|
|
83
93
|
# Rebuild and refresh the system prompt so any newly installed skills
|
|
84
94
|
# (or other configuration changes since the session was saved) are
|
|
85
95
|
# reflected immediately — without requiring the user to create a new session.
|
|
86
96
|
refresh_system_prompt
|
|
87
97
|
end
|
|
88
98
|
|
|
99
|
+
private def persisted_card_field(key)
|
|
100
|
+
card_id = @config.current_model_id
|
|
101
|
+
return nil unless card_id
|
|
102
|
+
@config.models.find { |m| m["id"] == card_id }&.dig(key)
|
|
103
|
+
end
|
|
104
|
+
|
|
89
105
|
# Generate session data for saving
|
|
90
106
|
# @param status [Symbol] Status of the last task: :success, :error, or :interrupted
|
|
91
107
|
# @param error_message [String] Error message if status is :error
|
|
@@ -134,10 +150,13 @@ module Clacky
|
|
|
134
150
|
reasoning_effort: @reasoning_effort,
|
|
135
151
|
# Persist the current model identity so the session can restore its
|
|
136
152
|
# original model on restart. model_name + model_base_url form a
|
|
137
|
-
# composite key
|
|
138
|
-
#
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
# composite key that points at the underlying card (NOT the
|
|
154
|
+
# sub-model overlay) — overlays are layered on top via :sub_model
|
|
155
|
+
# below so card lookup stays stable when the user toggles
|
|
156
|
+
# sub-models.
|
|
157
|
+
model_name: persisted_card_field("model"),
|
|
158
|
+
model_base_url: persisted_card_field("base_url"),
|
|
159
|
+
sub_model: @config.session_model_overlay_name
|
|
141
160
|
},
|
|
142
161
|
stats: stats_data,
|
|
143
162
|
messages: @history.to_a
|
|
@@ -108,6 +108,15 @@ module Clacky
|
|
|
108
108
|
preview_error
|
|
109
109
|
rescue JSON::ParserError
|
|
110
110
|
nil
|
|
111
|
+
rescue StandardError => e
|
|
112
|
+
@debug_logs << {
|
|
113
|
+
timestamp: Time.now.iso8601,
|
|
114
|
+
event: "tool_preview_error",
|
|
115
|
+
tool_name: call[:name],
|
|
116
|
+
error_class: e.class.name,
|
|
117
|
+
error_message: e.message
|
|
118
|
+
}
|
|
119
|
+
nil
|
|
111
120
|
end
|
|
112
121
|
end
|
|
113
122
|
|
|
@@ -397,6 +406,11 @@ module Clacky
|
|
|
397
406
|
return { error: "File not found: #{path}", path: path }
|
|
398
407
|
end
|
|
399
408
|
|
|
409
|
+
if File.directory?(expanded_path)
|
|
410
|
+
@ui&.show_file_error("Path is a directory, not a file: #{path}")
|
|
411
|
+
return { error: "Path is a directory, not a file: #{path}", path: path }
|
|
412
|
+
end
|
|
413
|
+
|
|
400
414
|
if old_string.empty?
|
|
401
415
|
@ui&.show_file_error("No old_string provided (nothing to replace)")
|
|
402
416
|
return { error: "No old_string provided (nothing to replace)" }
|
|
@@ -59,13 +59,6 @@ module Clacky
|
|
|
59
59
|
"ask_user" => "request_user_feedback",
|
|
60
60
|
"user_feedback" => "request_user_feedback",
|
|
61
61
|
"ask" => "request_user_feedback",
|
|
62
|
-
# undo_task aliases
|
|
63
|
-
"undo" => "undo_task",
|
|
64
|
-
# redo_task aliases
|
|
65
|
-
"redo" => "redo_task",
|
|
66
|
-
# list_tasks aliases
|
|
67
|
-
"tasks" => "list_tasks",
|
|
68
|
-
"task_history" => "list_tasks",
|
|
69
62
|
# trash_manager aliases
|
|
70
63
|
"trash" => "trash_manager",
|
|
71
64
|
"delete" => "trash_manager",
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -172,6 +172,18 @@ module Clacky
|
|
|
172
172
|
true
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
+
# Pin this session to a sub-model name without changing its underlying
|
|
176
|
+
# card (credentials / base_url stay put). Pass nil or "" to clear and
|
|
177
|
+
# fall back to the card's default model. Validation that the name is
|
|
178
|
+
# listed under the current provider is the caller's job.
|
|
179
|
+
# @param model_name [String, nil]
|
|
180
|
+
# @return [Boolean]
|
|
181
|
+
def set_session_sub_model(model_name)
|
|
182
|
+
@config.session_model_overlay = model_name
|
|
183
|
+
rebuild_client_for_current_model!
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
175
187
|
# Rebuild the underlying Client (and dependent components) to pick up
|
|
176
188
|
# credentials/model name from the currently-selected model in @config.
|
|
177
189
|
private def rebuild_client_for_current_model!
|
|
@@ -206,10 +218,16 @@ module Clacky
|
|
|
206
218
|
model = @config.current_model
|
|
207
219
|
return nil unless model
|
|
208
220
|
|
|
221
|
+
card_id = @config.current_model_id
|
|
222
|
+
base_entry = card_id ? @config.models.find { |m| m["id"] == card_id } : nil
|
|
223
|
+
sub_model = @config.session_model_overlay_name
|
|
224
|
+
|
|
209
225
|
{
|
|
210
226
|
id: model["id"],
|
|
211
227
|
model: model["model"],
|
|
212
|
-
base_url: model["base_url"]
|
|
228
|
+
base_url: model["base_url"],
|
|
229
|
+
card_model: base_entry&.dig("model"),
|
|
230
|
+
sub_model: sub_model
|
|
213
231
|
}
|
|
214
232
|
end
|
|
215
233
|
|
|
@@ -902,11 +920,6 @@ module Clacky
|
|
|
902
920
|
args[:skill_loader] = @skill_loader
|
|
903
921
|
end
|
|
904
922
|
|
|
905
|
-
# Special handling for Time Machine tools: inject agent
|
|
906
|
-
if ["undo_task", "redo_task", "list_tasks"].include?(call[:name])
|
|
907
|
-
args[:agent] = self
|
|
908
|
-
end
|
|
909
|
-
|
|
910
923
|
# Inject working_dir so tools don't rely on Dir.chdir global state
|
|
911
924
|
args[:working_dir] = @working_dir if @working_dir
|
|
912
925
|
|
|
@@ -1024,7 +1037,10 @@ module Clacky
|
|
|
1024
1037
|
check_stale!
|
|
1025
1038
|
|
|
1026
1039
|
formatted_messages = @client.format_tool_results(response, tool_results, model: current_model)
|
|
1027
|
-
formatted_messages.each
|
|
1040
|
+
formatted_messages.each do |msg|
|
|
1041
|
+
truncated = truncate_oversized_tool_content(msg)
|
|
1042
|
+
@history.append(truncated.merge(task_id: @current_task_id))
|
|
1043
|
+
end
|
|
1028
1044
|
|
|
1029
1045
|
# Append a follow-up `role:"user"` message for any image payloads that
|
|
1030
1046
|
# could not be delivered inside the tool message.
|
|
@@ -1060,6 +1076,26 @@ module Clacky
|
|
|
1060
1076
|
end
|
|
1061
1077
|
end
|
|
1062
1078
|
|
|
1079
|
+
# Cap oversized tool result content to keep a single tool message from
|
|
1080
|
+
# blowing up the prompt budget (issue #218: a 7350-path glob produced a
|
|
1081
|
+
# ~890k-char result that pushed history past the model context window
|
|
1082
|
+
# and poisoned the session). Only string content is truncated — Array
|
|
1083
|
+
# content (multipart/image blocks) is left alone since image payloads
|
|
1084
|
+
# are handled by the image_inject path above.
|
|
1085
|
+
MAX_TOOL_RESULT_CHARS = 80_000
|
|
1086
|
+
|
|
1087
|
+
private def truncate_oversized_tool_content(msg)
|
|
1088
|
+
content = msg[:content]
|
|
1089
|
+
return msg unless content.is_a?(String) && content.length > MAX_TOOL_RESULT_CHARS
|
|
1090
|
+
|
|
1091
|
+
original_len = content.length
|
|
1092
|
+
head = content[0, MAX_TOOL_RESULT_CHARS]
|
|
1093
|
+
truncated = head + "\n\n[Tool result truncated: #{original_len} chars total, " \
|
|
1094
|
+
"showing first #{MAX_TOOL_RESULT_CHARS}. Use a more specific query/limit, " \
|
|
1095
|
+
"or read the raw output via file_reader/grep on the underlying source.]"
|
|
1096
|
+
msg.merge(content: truncated)
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1063
1099
|
# Enqueue an inline skill injection to be flushed after observe().
|
|
1064
1100
|
# Called by InvokeSkill#execute to avoid injecting during tool execution,
|
|
1065
1101
|
# which would break Bedrock's toolUse/toolResult pairing requirement.
|
|
@@ -1183,9 +1219,6 @@ module Clacky
|
|
|
1183
1219
|
@tool_registry.register(Tools::TodoManager.new)
|
|
1184
1220
|
@tool_registry.register(Tools::RequestUserFeedback.new)
|
|
1185
1221
|
@tool_registry.register(Tools::InvokeSkill.new)
|
|
1186
|
-
@tool_registry.register(Tools::UndoTask.new)
|
|
1187
|
-
@tool_registry.register(Tools::RedoTask.new)
|
|
1188
|
-
@tool_registry.register(Tools::ListTasks.new)
|
|
1189
1222
|
@tool_registry.register(Tools::Browser.new)
|
|
1190
1223
|
end
|
|
1191
1224
|
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -211,6 +211,14 @@ module Clacky
|
|
|
211
211
|
# Keys honored: "api_key", "base_url", "model", "anthropic_format".
|
|
212
212
|
# @return [Hash, nil]
|
|
213
213
|
@virtual_model_overlay = options[:virtual_model_overlay]
|
|
214
|
+
|
|
215
|
+
# Per-session sub-model override. Persists across restarts via the
|
|
216
|
+
# session file. Independent of @virtual_model_overlay (which is for
|
|
217
|
+
# short-lived subagent forks). Used by the WebUI sub-model switcher
|
|
218
|
+
# to pin a session to e.g. "dsk-deepseek-v4-pro" while the underlying
|
|
219
|
+
# card still says "abs-claude-sonnet-4-6". Only the "model" key is
|
|
220
|
+
# honored — sub-model switching never changes credentials.
|
|
221
|
+
@session_model_overlay = options[:session_model_overlay]
|
|
214
222
|
end
|
|
215
223
|
|
|
216
224
|
# Load configuration from file
|
|
@@ -354,6 +362,9 @@ module Clacky
|
|
|
354
362
|
if @virtual_model_overlay
|
|
355
363
|
copy.instance_variable_set(:@virtual_model_overlay, @virtual_model_overlay.dup)
|
|
356
364
|
end
|
|
365
|
+
if @session_model_overlay
|
|
366
|
+
copy.instance_variable_set(:@session_model_overlay, @session_model_overlay.dup)
|
|
367
|
+
end
|
|
357
368
|
copy
|
|
358
369
|
end
|
|
359
370
|
|
|
@@ -431,9 +442,12 @@ module Clacky
|
|
|
431
442
|
index = @models.find_index { |m| m["id"] == id }
|
|
432
443
|
return false if index.nil?
|
|
433
444
|
|
|
445
|
+
previous_id = @current_model_id
|
|
434
446
|
@current_model_id = id
|
|
435
447
|
@current_model_index = index
|
|
436
448
|
|
|
449
|
+
@session_model_overlay = nil if previous_id != id
|
|
450
|
+
|
|
437
451
|
true
|
|
438
452
|
end
|
|
439
453
|
|
|
@@ -760,14 +774,18 @@ module Clacky
|
|
|
760
774
|
resolved = resolve_current_model_entry
|
|
761
775
|
return nil unless resolved
|
|
762
776
|
|
|
763
|
-
#
|
|
764
|
-
#
|
|
765
|
-
#
|
|
777
|
+
# Merge order (low → high): base entry, session-level sub-model override,
|
|
778
|
+
# then short-lived subagent overlay. Both layers are kept separate so
|
|
779
|
+
# a subagent fork can stack its own credentials on top of an active
|
|
780
|
+
# sub-model pin without erasing it.
|
|
781
|
+
merged = resolved
|
|
782
|
+
if @session_model_overlay && !@session_model_overlay.empty?
|
|
783
|
+
merged = merged.merge(@session_model_overlay)
|
|
784
|
+
end
|
|
766
785
|
if @virtual_model_overlay && !@virtual_model_overlay.empty?
|
|
767
|
-
|
|
768
|
-
else
|
|
769
|
-
resolved
|
|
786
|
+
merged = merged.merge(@virtual_model_overlay)
|
|
770
787
|
end
|
|
788
|
+
merged.equal?(resolved) ? resolved : merged
|
|
771
789
|
end
|
|
772
790
|
|
|
773
791
|
# Internal: resolve the current model entry from @models (no overlay).
|
|
@@ -822,6 +840,36 @@ module Clacky
|
|
|
822
840
|
@virtual_model_overlay
|
|
823
841
|
end
|
|
824
842
|
|
|
843
|
+
# Apply a session-level sub-model override. Lives on this AgentConfig
|
|
844
|
+
# only (each session deep_copy's its own scalar ivar) and survives a
|
|
845
|
+
# restart through the session file. Pass nil or "" to clear.
|
|
846
|
+
#
|
|
847
|
+
# The override only rewrites the resolved current_model's "model" field —
|
|
848
|
+
# api_key / base_url / anthropic_format come from the underlying card,
|
|
849
|
+
# so the user's credentials and provider identity are untouched.
|
|
850
|
+
#
|
|
851
|
+
# @param model_name [String, nil] sub-model name, e.g. "dsk-deepseek-v4-pro"
|
|
852
|
+
def session_model_overlay=(model_name)
|
|
853
|
+
if model_name.nil? || model_name.to_s.strip.empty?
|
|
854
|
+
@session_model_overlay = nil
|
|
855
|
+
else
|
|
856
|
+
@session_model_overlay = { "model" => model_name.to_s.strip }
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# @return [Hash, nil] the active session sub-model overlay
|
|
861
|
+
def session_model_overlay
|
|
862
|
+
@session_model_overlay
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# Convenience accessor: the sub-model name currently pinned on this
|
|
866
|
+
# session, or nil when no override is active. Used by serializers and
|
|
867
|
+
# the WebUI to surface "card · sub-model" two-line displays.
|
|
868
|
+
# @return [String, nil]
|
|
869
|
+
def session_model_overlay_name
|
|
870
|
+
@session_model_overlay && @session_model_overlay["model"]
|
|
871
|
+
end
|
|
872
|
+
|
|
825
873
|
# Query whether the *current* model supports a given capability.
|
|
826
874
|
#
|
|
827
875
|
# This is the single entry-point callers (Agent, downgrade pipeline, UI)
|