openclacky 1.5.19 → 1.5.21
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 +95 -0
- data/README.md +19 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent/chunk_index.rb +8 -1
- data/lib/clacky/agent/feedback.rb +30 -0
- data/lib/clacky/agent/history_navigation.rb +17 -3
- data/lib/clacky/agent/hook_manager.rb +6 -1
- data/lib/clacky/agent/llm_caller.rb +17 -3
- data/lib/clacky/agent/message_compressor_helper.rb +36 -2
- data/lib/clacky/agent/session_serializer.rb +45 -4
- data/lib/clacky/agent.rb +135 -55
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +10 -5
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +5 -4
- data/lib/clacky/client.rb +12 -5
- data/lib/clacky/deepseek_web_tools.rb +156 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +97 -20
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/extension/loader.rb +5 -3
- data/lib/clacky/locales/en.rb +5 -5
- data/lib/clacky/locales/zh.rb +5 -5
- data/lib/clacky/message_format/anthropic.rb +20 -0
- data/lib/clacky/message_format/open_ai.rb +31 -14
- data/lib/clacky/message_history.rb +45 -4
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +38 -138
- data/lib/clacky/search_config.rb +47 -13
- data/lib/clacky/server/browser_manager.rb +35 -5
- data/lib/clacky/server/channel/channel_manager.rb +6 -6
- data/lib/clacky/server/discover.rb +5 -4
- data/lib/clacky/server/http_server.rb +285 -53
- data/lib/clacky/server/model_prices.rb +10 -5
- data/lib/clacky/server/pid_files.rb +25 -0
- data/lib/clacky/server/server_master.rb +9 -3
- data/lib/clacky/server/session_registry.rb +37 -7
- data/lib/clacky/server/web_ui_controller.rb +72 -0
- data/lib/clacky/session_manager.rb +28 -6
- data/lib/clacky/skill.rb +1 -1
- data/lib/clacky/tools/ask_user.rb +4 -1
- data/lib/clacky/tools/base.rb +3 -2
- data/lib/clacky/tools/browser.rb +18 -2
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/terminal/session_manager.rb +19 -1
- data/lib/clacky/tools/terminal.rb +65 -23
- data/lib/clacky/tools/trash_manager.rb +5 -3
- data/lib/clacky/tools/web_fetch.rb +81 -22
- data/lib/clacky/tools/web_search.rb +45 -4
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +4 -3
- data/lib/clacky/utils/file_processor.rb +51 -0
- data/lib/clacky/utils/model_pricing.rb +242 -39
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1158 -249
- data/lib/clacky/web/app.js +82 -13
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/code-editor.js +6 -2
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +92 -97
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/components/sidebar.js +6 -2
- data/lib/clacky/web/features/archive/store.js +196 -0
- data/lib/clacky/web/features/archive/view.js +409 -0
- data/lib/clacky/web/features/backup/view.js +17 -7
- data/lib/clacky/web/features/billing/view.js +84 -10
- data/lib/clacky/web/features/channels/view.js +75 -29
- data/lib/clacky/web/features/new-session/store.js +3 -3
- data/lib/clacky/web/features/new-session/view.js +1 -0
- data/lib/clacky/web/features/trash/store.js +14 -11
- data/lib/clacky/web/features/trash/view.js +66 -13
- data/lib/clacky/web/features/workspace/view.js +421 -16
- data/lib/clacky/web/i18n.js +242 -70
- data/lib/clacky/web/index.html +270 -117
- data/lib/clacky/web/projects.js +56 -1
- data/lib/clacky/web/sessions.js +609 -267
- data/lib/clacky/web/settings.js +186 -32
- data/lib/clacky/web/ws-dispatcher.js +33 -21
- data/lib/clacky.rb +4 -2
- metadata +6 -1
data/lib/clacky/agent.rb
CHANGED
|
@@ -24,6 +24,7 @@ require_relative "agent/skill_evolution"
|
|
|
24
24
|
require_relative "agent/skill_reflector"
|
|
25
25
|
require_relative "agent/skill_auto_creator"
|
|
26
26
|
require_relative "agent/fake_tool_call_detector"
|
|
27
|
+
require_relative "agent/feedback"
|
|
27
28
|
require_relative "agent/goal_state"
|
|
28
29
|
require_relative "agent/goal_manager"
|
|
29
30
|
|
|
@@ -31,6 +32,7 @@ module Clacky
|
|
|
31
32
|
class Agent
|
|
32
33
|
# Include all functionality modules
|
|
33
34
|
include MessageCompressorHelper
|
|
35
|
+
include Feedback
|
|
34
36
|
include ToolExecutor
|
|
35
37
|
include CostTracker
|
|
36
38
|
include SessionSerializer
|
|
@@ -52,8 +54,18 @@ module Clacky
|
|
|
52
54
|
attr_accessor :pinned
|
|
53
55
|
attr_accessor :channel_info
|
|
54
56
|
attr_accessor :project_id
|
|
57
|
+
# ISO8601 stamp set when the user archives the session; nil = active.
|
|
58
|
+
# Archived sessions stay on disk but drop out of every normal list.
|
|
59
|
+
attr_accessor :archived_at
|
|
55
60
|
|
|
56
61
|
REASONING_EFFORTS = %w[low medium high xhigh max].freeze
|
|
62
|
+
# Sessionbar/config values meaning "no explicit level". They stay a string
|
|
63
|
+
# instead of collapsing to nil so format adapters can tell an explicit
|
|
64
|
+
# "default" apart from "user never chose": for models whose provider
|
|
65
|
+
# default is the deepest level (MiniMax M3.1-Flash, GLM-5.3) the adapters
|
|
66
|
+
# map this value to their lightest effort.
|
|
67
|
+
REASONING_DEFAULT = "off"
|
|
68
|
+
REASONING_DEFAULT_ALIASES = %w[off none disabled nothink].freeze
|
|
57
69
|
MAX_VIDEO_UNDERSTANDING_BYTES = Utils::FileProcessor::MAX_FILE_BYTES
|
|
58
70
|
MAX_VIDEO_BASE64_BYTES = 45 * 1024 * 1024
|
|
59
71
|
MAX_VIDEO_DESCRIPTION_CHARS = 500
|
|
@@ -68,6 +80,16 @@ module Clacky
|
|
|
68
80
|
@config&.permission_mode&.to_s || ""
|
|
69
81
|
end
|
|
70
82
|
|
|
83
|
+
# Persist an explicit session choice separately from temporary goal mode.
|
|
84
|
+
def permission_mode=(value)
|
|
85
|
+
mode = value.to_s
|
|
86
|
+
raise ArgumentError, "Invalid permission mode" unless %w[confirm_all auto_approve].include?(mode)
|
|
87
|
+
@permission_mode_override = mode
|
|
88
|
+
@config.permission_mode = mode.to_sym
|
|
89
|
+
@pre_goal_permission_mode = nil
|
|
90
|
+
notify_permission_mode_change(mode.to_sym)
|
|
91
|
+
end
|
|
92
|
+
|
|
71
93
|
def reasoning_effort=(value)
|
|
72
94
|
@reasoning_effort = normalize_reasoning_effort(value)
|
|
73
95
|
end
|
|
@@ -75,7 +97,8 @@ module Clacky
|
|
|
75
97
|
private def normalize_reasoning_effort(value)
|
|
76
98
|
return nil if value.nil?
|
|
77
99
|
str = value.to_s.strip.downcase
|
|
78
|
-
return nil if str.empty?
|
|
100
|
+
return nil if str.empty?
|
|
101
|
+
return REASONING_DEFAULT if REASONING_DEFAULT_ALIASES.include?(str)
|
|
79
102
|
return str if REASONING_EFFORTS.include?(str)
|
|
80
103
|
nil
|
|
81
104
|
end
|
|
@@ -85,6 +108,7 @@ module Clacky
|
|
|
85
108
|
def initialize(client, config, working_dir:, ui:, profile:, session_id:, source:)
|
|
86
109
|
@client = client # Client for current model
|
|
87
110
|
@config = config.is_a?(AgentConfig) ? config : AgentConfig.new(config)
|
|
111
|
+
@client_options = @config.client_options
|
|
88
112
|
@agent_profile = AgentProfile.load(profile)
|
|
89
113
|
@source = source.to_sym # :manual | :cron | :channel
|
|
90
114
|
@channel_info = nil # { platform:, user_id:, user_name:, chat_id: } set by ChannelManager
|
|
@@ -93,6 +117,7 @@ module Clacky
|
|
|
93
117
|
@session_id = session_id
|
|
94
118
|
@name = ""
|
|
95
119
|
@pinned = false
|
|
120
|
+
@archived_at = nil
|
|
96
121
|
@history = MessageHistory.new
|
|
97
122
|
@todos = [] # Store todos in memory
|
|
98
123
|
@iterations = 0
|
|
@@ -113,7 +138,7 @@ module Clacky
|
|
|
113
138
|
@task_cost_source = :estimated # Track cost source for current task
|
|
114
139
|
@previous_total_tokens = 0 # Track tokens from previous iteration for delta calculation
|
|
115
140
|
@latest_latency = nil # Most recent LLM call's latency metrics (see Client#send_messages_with_tools)
|
|
116
|
-
@reasoning_effort =
|
|
141
|
+
@reasoning_effort = REASONING_DEFAULT # Sessionbar shows "Default" for a fresh session; adapters resolve it per model
|
|
117
142
|
@ui = ui # UIController for direct UI interaction
|
|
118
143
|
@debug_logs = [] # Debug logs for troubleshooting
|
|
119
144
|
@input_mutex = Mutex.new
|
|
@@ -235,23 +260,25 @@ module Clacky
|
|
|
235
260
|
# Rebuild the underlying Client (and dependent components) to pick up
|
|
236
261
|
# credentials/model name from the currently-selected model in @config.
|
|
237
262
|
private def rebuild_client_for_current_model!
|
|
238
|
-
|
|
239
|
-
@client = Clacky::Client.new(
|
|
240
|
-
@config.api_key,
|
|
241
|
-
base_url: @config.base_url,
|
|
242
|
-
model: @config.model_name,
|
|
243
|
-
anthropic_format: @config.anthropic_format?,
|
|
244
|
-
api_format: @config.api_format,
|
|
245
|
-
provider_id: @config.provider_id_for(entry),
|
|
246
|
-
capabilities: entry && entry["capabilities"]
|
|
247
|
-
)
|
|
248
|
-
# Update message compressor with new client and model
|
|
249
|
-
@message_compressor = MessageCompressor.new(@client, model: current_model)
|
|
263
|
+
build_client!
|
|
250
264
|
|
|
251
265
|
# Inject a new session context to notify the AI of the model switch
|
|
252
266
|
inject_session_context
|
|
253
267
|
end
|
|
254
268
|
|
|
269
|
+
private def build_client!
|
|
270
|
+
@client_options = @config.client_options
|
|
271
|
+
opts = @client_options.dup
|
|
272
|
+
@client = Clacky::Client.new(opts.delete(:api_key), **opts)
|
|
273
|
+
# Update message compressor with new client and model
|
|
274
|
+
@message_compressor = MessageCompressor.new(@client, model: current_model)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# The shared model card can be edited (Settings, config reload) under a live session.
|
|
278
|
+
private def refresh_client_if_card_changed!
|
|
279
|
+
build_client! unless @config.client_options == @client_options
|
|
280
|
+
end
|
|
281
|
+
|
|
255
282
|
# Change the working directory for this session
|
|
256
283
|
# Injects a new session context to notify the AI of the directory change
|
|
257
284
|
def change_working_dir(new_dir)
|
|
@@ -329,6 +356,14 @@ module Clacky
|
|
|
329
356
|
self
|
|
330
357
|
end
|
|
331
358
|
|
|
359
|
+
# Built-in run failures share the persisted event trail, without exposing
|
|
360
|
+
# built-in event types through the extension API.
|
|
361
|
+
def record_error_event(message, code: nil, top_up_url: nil, raw_message: nil)
|
|
362
|
+
data = { message: message, code: code, top_up_url: top_up_url, raw_message: raw_message }.compact
|
|
363
|
+
@history.append_ext_event({ type: "error", data: data })
|
|
364
|
+
self
|
|
365
|
+
end
|
|
366
|
+
|
|
332
367
|
# Resolve the model name the goal judge should use. Prefer the provider's
|
|
333
368
|
# lite model (cheap/fast for a one-line verdict); fall back to the primary
|
|
334
369
|
# model when no lite is resolvable.
|
|
@@ -757,7 +792,7 @@ module Clacky
|
|
|
757
792
|
@ui&.show_token_usage(response[:token_usage]) if response[:token_usage]
|
|
758
793
|
|
|
759
794
|
# Act: Execute tool calls
|
|
760
|
-
action_result = act(response[:tool_calls])
|
|
795
|
+
action_result = act(response[:tool_calls], assistant_message: response.delete(:history_message))
|
|
761
796
|
|
|
762
797
|
# Check if ask_user was called
|
|
763
798
|
if action_result[:awaiting_feedback]
|
|
@@ -928,6 +963,33 @@ module Clacky
|
|
|
928
963
|
private def append_user_input(user_input, files: nil, reference_contexts: nil,
|
|
929
964
|
display_text: nil, created_at: nil, references_display: nil,
|
|
930
965
|
task_id: @current_task_id)
|
|
966
|
+
# Resolve lightweight metadata first, then commit the accepted user turn
|
|
967
|
+
# before any attachment parser/OCR/sidecar work can block or fail. Session
|
|
968
|
+
# switching and graceful shutdown both read from @history, so the turn
|
|
969
|
+
# must already be authoritative while those slow operations are running.
|
|
970
|
+
skill_command = parse_skill_command(user_input)
|
|
971
|
+
skill_command_display = if skill_command[:found] && skill_command[:skill]
|
|
972
|
+
skill_command[:skill].display_name(Thread.current[:lang])
|
|
973
|
+
end
|
|
974
|
+
created_at ||= Time.now.to_f
|
|
975
|
+
message_id = SecureRandom.uuid
|
|
976
|
+
staged = Array(files).any?
|
|
977
|
+
staged_display_files = display_files_for(files)
|
|
978
|
+
message_metadata = {
|
|
979
|
+
role: "user", message_id: message_id, task_id: task_id, created_at: created_at,
|
|
980
|
+
display_text: display_text,
|
|
981
|
+
skill_command: skill_command[:found] ? skill_command[:skill_name] : nil,
|
|
982
|
+
skill_command_display: skill_command_display,
|
|
983
|
+
display_references: Array(references_display).empty? ? nil : references_display
|
|
984
|
+
}
|
|
985
|
+
if staged
|
|
986
|
+
@history.append(message_metadata.merge(
|
|
987
|
+
content: user_input,
|
|
988
|
+
display_files: staged_display_files.empty? ? nil : staged_display_files
|
|
989
|
+
))
|
|
990
|
+
staged_message_id = message_id
|
|
991
|
+
end
|
|
992
|
+
|
|
931
993
|
# Split files into vision images and disk files; downgrade oversized images to disk
|
|
932
994
|
image_files, disk_files = partition_files(Array(files))
|
|
933
995
|
vision_images, downgraded = resolve_vision_images(image_files)
|
|
@@ -999,37 +1061,21 @@ module Clacky
|
|
|
999
1061
|
# stored here — they recover from image_url blocks in user_content. Downgraded
|
|
1000
1062
|
# images (provider has no vision / too large / OCR'd) DO need path here so the
|
|
1001
1063
|
# UI can re-render them from the on-disk copy across session switches.
|
|
1002
|
-
display_files = all_disk_files
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1064
|
+
display_files = display_files_for(all_disk_files)
|
|
1065
|
+
resolved_message = message_metadata.merge(
|
|
1066
|
+
content: user_content,
|
|
1067
|
+
display_files: display_files.empty? ? nil : display_files
|
|
1068
|
+
)
|
|
1069
|
+
if staged
|
|
1070
|
+
replaced = @history.replace_user_message(
|
|
1071
|
+
message_id: message_id,
|
|
1072
|
+
message: resolved_message
|
|
1073
|
+
)
|
|
1074
|
+
raise AgentError, "Accepted user message disappeared during attachment processing" unless replaced
|
|
1075
|
+
else
|
|
1076
|
+
@history.append(resolved_message)
|
|
1012
1077
|
end
|
|
1013
1078
|
|
|
1014
|
-
# Resolved once here (not after append) so the user message can carry the
|
|
1015
|
-
# confirmed skill name: only a skill that actually dispatches gets marked,
|
|
1016
|
-
# so the UI never highlights a typo'd or unavailable command. The display
|
|
1017
|
-
# name is resolved against the client's language (Thread.current[:lang],
|
|
1018
|
-
# seeded from the WS message / X-Lang header) so the Web UI and third-party
|
|
1019
|
-
# clients can render a localized label without re-resolving the skill.
|
|
1020
|
-
skill_command = parse_skill_command(user_input)
|
|
1021
|
-
skill_command_display = if skill_command[:found] && skill_command[:skill]
|
|
1022
|
-
skill_command[:skill].display_name(Thread.current[:lang])
|
|
1023
|
-
end
|
|
1024
|
-
|
|
1025
|
-
created_at ||= Time.now.to_f
|
|
1026
|
-
@history.append({ role: "user", content: user_content, task_id: task_id, created_at: created_at,
|
|
1027
|
-
display_text: display_text,
|
|
1028
|
-
skill_command: skill_command[:found] ? skill_command[:skill_name] : nil,
|
|
1029
|
-
skill_command_display: skill_command_display,
|
|
1030
|
-
display_files: display_files.empty? ? nil : display_files,
|
|
1031
|
-
display_references: Array(references_display).empty? ? nil : references_display })
|
|
1032
|
-
|
|
1033
1079
|
# Inject disk file references as a system_injected message so:
|
|
1034
1080
|
# - LLM sees the file info (system_injected is NOT stripped from to_api)
|
|
1035
1081
|
# - replay_history skips it (next if ev[:system_injected]), keeping the user bubble clean
|
|
@@ -1144,6 +1190,36 @@ module Clacky
|
|
|
1144
1190
|
# skill-declared model not found) still stops the progress spinner.
|
|
1145
1191
|
inject_skill_command_as_assistant_message(skill_command, task_id)
|
|
1146
1192
|
|
|
1193
|
+
rescue Clacky::AgentInterrupted, StandardError
|
|
1194
|
+
# The staged turn exists only to make an accepted upload replayable while
|
|
1195
|
+
# attachment work is in progress. If that work crashes, remove this turn
|
|
1196
|
+
# and anything injected after it so the next request cannot inherit a
|
|
1197
|
+
# half-resolved attachment context. Parser failures returned as FileRef
|
|
1198
|
+
# metadata do not raise and therefore remain visible/actionable.
|
|
1199
|
+
@history.truncate_from_message(staged_message_id) if staged_message_id
|
|
1200
|
+
raise
|
|
1201
|
+
end
|
|
1202
|
+
|
|
1203
|
+
# Build replay-only attachment metadata without parsing file contents.
|
|
1204
|
+
# @mention references render through display_references and must not also
|
|
1205
|
+
# appear as plain attachment badges.
|
|
1206
|
+
private def display_files_for(files)
|
|
1207
|
+
Array(files).filter_map do |file|
|
|
1208
|
+
next if file[:reference] || file["reference"]
|
|
1209
|
+
|
|
1210
|
+
name = file[:name] || file["name"]
|
|
1211
|
+
next unless name
|
|
1212
|
+
|
|
1213
|
+
mime_type = file[:mime_type] || file["mime_type"]
|
|
1214
|
+
type = file[:type] || file["type"]
|
|
1215
|
+
type ||= "image" if mime_type.to_s.start_with?("image/")
|
|
1216
|
+
{
|
|
1217
|
+
name: name,
|
|
1218
|
+
type: type || "file",
|
|
1219
|
+
path: file[:path] || file["path"],
|
|
1220
|
+
preview_path: file[:preview_path] || file["preview_path"]
|
|
1221
|
+
}
|
|
1222
|
+
end
|
|
1147
1223
|
end
|
|
1148
1224
|
|
|
1149
1225
|
def enqueue_input(content, delivery: :queue, **options)
|
|
@@ -1482,7 +1558,7 @@ module Clacky
|
|
|
1482
1558
|
# "reasoning_content must be passed back" contract.
|
|
1483
1559
|
msg[:reasoning_content] = response[:reasoning_content] if response[:reasoning_content]
|
|
1484
1560
|
check_stale!
|
|
1485
|
-
@history.append(msg)
|
|
1561
|
+
@history.append(msg) { |stored_message| response[:history_message] = stored_message }
|
|
1486
1562
|
|
|
1487
1563
|
# Close the thinking spinner before returning. The caller (run loop)
|
|
1488
1564
|
# is about to render the assistant message and/or tool invocations,
|
|
@@ -1506,7 +1582,7 @@ module Clacky
|
|
|
1506
1582
|
raise Clacky::AgentInterrupted, "Task superseded by a newer task on another thread"
|
|
1507
1583
|
end
|
|
1508
1584
|
|
|
1509
|
-
private def act(tool_calls)
|
|
1585
|
+
private def act(tool_calls, assistant_message: nil)
|
|
1510
1586
|
return { denied: false, feedback: nil, tool_results: [], awaiting_feedback: false } unless tool_calls
|
|
1511
1587
|
|
|
1512
1588
|
denied = false
|
|
@@ -1687,20 +1763,21 @@ module Clacky
|
|
|
1687
1763
|
# result path so the model sees the error and can retry.
|
|
1688
1764
|
if Tools::AskUser.feedback_tool?(call[:name]) &&
|
|
1689
1765
|
result.is_a?(Hash) && result[:awaiting_feedback]
|
|
1690
|
-
#
|
|
1691
|
-
#
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
@ui&.show_tool_call(call[:name], call[:arguments])
|
|
1766
|
+
# Prepare and persist the complete request before displaying it or
|
|
1767
|
+
# starting the wait. Extensions and native questions share one result.
|
|
1768
|
+
request = prepare_feedback_request(call, result, assistant_message: assistant_message)
|
|
1769
|
+
@ui&.show_tool_call(call[:name], request)
|
|
1695
1770
|
|
|
1696
1771
|
if @config.permission_mode == :auto_approve
|
|
1697
1772
|
# auto_approve means the agent runs unattended by default, but a
|
|
1698
1773
|
# human MAY be watching the terminal. Show a short interactive
|
|
1699
1774
|
# countdown: if the user steps in, hand control over and wait for
|
|
1700
1775
|
# their answer; otherwise auto-decide and keep going.
|
|
1701
|
-
countdown = @ui&.request_feedback_with_countdown(seconds:
|
|
1776
|
+
countdown = @ui&.request_feedback_with_countdown(seconds: @config.ask_user_timeout_seconds)
|
|
1702
1777
|
|
|
1703
|
-
if
|
|
1778
|
+
if countdown == :timeout && @config.permission_mode != :auto_approve
|
|
1779
|
+
awaiting_feedback = true
|
|
1780
|
+
elsif @ui.nil? || countdown == :timeout
|
|
1704
1781
|
result = result.merge(
|
|
1705
1782
|
auto_reply: "No user is available. Please make a reasonable decision based on the context and continue."
|
|
1706
1783
|
)
|
|
@@ -2038,8 +2115,11 @@ module Clacky
|
|
|
2038
2115
|
@tool_registry.register(Tools::Edit.new)
|
|
2039
2116
|
@tool_registry.register(Tools::Glob.new)
|
|
2040
2117
|
@tool_registry.register(Tools::Grep.new)
|
|
2041
|
-
|
|
2042
|
-
|
|
2118
|
+
[Tools::WebSearch, Tools::WebFetch].each do |klass|
|
|
2119
|
+
tool = klass.new
|
|
2120
|
+
tool.agent = self
|
|
2121
|
+
@tool_registry.register(tool)
|
|
2122
|
+
end
|
|
2043
2123
|
@tool_registry.register(Tools::TodoManager.new)
|
|
2044
2124
|
@tool_registry.register(Tools::AskUser.new)
|
|
2045
2125
|
@tool_registry.register(Tools::InvokeSkill.new)
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -158,19 +158,21 @@ module Clacky
|
|
|
158
158
|
DEFAULT_COMPRESSION_THRESHOLD = 150_000
|
|
159
159
|
DEFAULT_MESSAGE_COUNT_THRESHOLD = 200
|
|
160
160
|
|
|
161
|
-
attr_accessor :input_behavior, :permission_mode, :max_tokens, :verbose,
|
|
161
|
+
attr_accessor :ask_user_timeout_seconds, :input_behavior, :permission_mode, :max_tokens, :verbose,
|
|
162
162
|
:enable_compression, :enable_idle_compression, :enable_prompt_caching,
|
|
163
163
|
:compression_threshold, :message_count_threshold,
|
|
164
164
|
:models, :current_model_index, :current_model_id,
|
|
165
165
|
:memory_update_enabled, :skill_evolution,
|
|
166
166
|
:max_running_agents, :max_idle_agents,
|
|
167
|
-
:default_working_dir,
|
|
167
|
+
:default_working_dir, :pid_dir,
|
|
168
168
|
:proxy_url,
|
|
169
169
|
:clacky_license_server
|
|
170
170
|
|
|
171
171
|
def initialize(options = {})
|
|
172
172
|
@permission_mode = validate_permission_mode(options[:permission_mode])
|
|
173
173
|
@input_behavior = %w[queue steer interrupt].include?(options[:input_behavior].to_s) ? options[:input_behavior].to_s : "queue"
|
|
174
|
+
timeout = options.fetch(:ask_user_timeout_seconds, 20)
|
|
175
|
+
@ask_user_timeout_seconds = timeout.is_a?(Integer) && timeout >= 0 ? timeout : 20
|
|
174
176
|
@max_tokens = options[:max_tokens] || 16384
|
|
175
177
|
@verbose = options[:verbose] || false
|
|
176
178
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
@@ -220,6 +222,7 @@ module Clacky
|
|
|
220
222
|
@max_idle_agents = options[:max_idle_agents] || 10
|
|
221
223
|
|
|
222
224
|
@default_working_dir = options[:default_working_dir] || ENV["CLACKY_WORKSPACE_DIR"]
|
|
225
|
+
@pid_dir = options[:pid_dir]
|
|
223
226
|
|
|
224
227
|
# HTTP proxy policy. The user's shell ENV (HTTP_PROXY etc.) is always
|
|
225
228
|
# ignored — set proxy_url here to route Clacky's outbound HTTP through
|
|
@@ -455,9 +458,9 @@ module Clacky
|
|
|
455
458
|
CONFIG_SETTINGS_KEYS = %w[
|
|
456
459
|
enable_compression enable_idle_compression enable_prompt_caching
|
|
457
460
|
compression_threshold message_count_threshold
|
|
458
|
-
memory_update_enabled input_behavior
|
|
461
|
+
memory_update_enabled input_behavior ask_user_timeout_seconds
|
|
459
462
|
skill_evolution max_running_agents max_idle_agents
|
|
460
|
-
default_working_dir
|
|
463
|
+
default_working_dir pid_dir
|
|
461
464
|
proxy_url clacky_license_server
|
|
462
465
|
].freeze
|
|
463
466
|
|
|
@@ -470,6 +473,7 @@ module Clacky
|
|
|
470
473
|
end
|
|
471
474
|
settings = {
|
|
472
475
|
"input_behavior" => @input_behavior,
|
|
476
|
+
"ask_user_timeout_seconds" => @ask_user_timeout_seconds,
|
|
473
477
|
"enable_compression" => @enable_compression,
|
|
474
478
|
"enable_idle_compression" => @enable_idle_compression,
|
|
475
479
|
"enable_prompt_caching" => @enable_prompt_caching,
|
|
@@ -480,6 +484,7 @@ module Clacky
|
|
|
480
484
|
"max_running_agents" => @max_running_agents,
|
|
481
485
|
"max_idle_agents" => @max_idle_agents,
|
|
482
486
|
"default_working_dir" => @default_working_dir,
|
|
487
|
+
"pid_dir" => @pid_dir,
|
|
483
488
|
"proxy_url" => @proxy_url,
|
|
484
489
|
"clacky_license_server" => @clacky_license_server
|
|
485
490
|
}
|
|
@@ -715,6 +720,20 @@ module Clacky
|
|
|
715
720
|
@models.find { |m| m["type"] == type }
|
|
716
721
|
end
|
|
717
722
|
|
|
723
|
+
# Everything Client.new needs from the current card; also the staleness key for a live Client.
|
|
724
|
+
def client_options
|
|
725
|
+
entry = current_model
|
|
726
|
+
{
|
|
727
|
+
api_key: api_key,
|
|
728
|
+
base_url: base_url,
|
|
729
|
+
model: model_name,
|
|
730
|
+
anthropic_format: anthropic_format?,
|
|
731
|
+
api_format: api_format,
|
|
732
|
+
provider_id: provider_id_for(entry),
|
|
733
|
+
capabilities: entry && entry["capabilities"]
|
|
734
|
+
}
|
|
735
|
+
end
|
|
736
|
+
|
|
718
737
|
# Resolve the provider id for a model entry. An explicit `provider_id`
|
|
719
738
|
# naming a known preset wins over base_url/api_key heuristics — that lets
|
|
720
739
|
# a custom base_url inherit a preset's lineup (media sidecars, OCR models,
|
|
@@ -731,6 +750,65 @@ module Clacky
|
|
|
731
750
|
)
|
|
732
751
|
end
|
|
733
752
|
|
|
753
|
+
# Find the API key to use for DeepSeek web search / page fetch.
|
|
754
|
+
# Resolution order:
|
|
755
|
+
# 1. Explicit key stored in ~/.clacky/search.yml, when DeepSeek is selected
|
|
756
|
+
# 2. Key from the model in use, when its provider declares deepseek_web_tools
|
|
757
|
+
# 3. Key from any other configured model declaring the same capability
|
|
758
|
+
# 4. nil if none is configured
|
|
759
|
+
def deepseek_web_key
|
|
760
|
+
if Clacky::SearchConfig.deepseek_web?
|
|
761
|
+
explicit = Clacky::SearchConfig.key.to_s.strip
|
|
762
|
+
return explicit unless explicit.empty?
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
entry = web_tool_candidates.find do |m|
|
|
766
|
+
next false unless m.is_a?(Hash)
|
|
767
|
+
next false if m["api_key"].to_s.strip.empty?
|
|
768
|
+
Clacky::Providers.deepseek_web_tools?(provider_id_for(m))
|
|
769
|
+
end
|
|
770
|
+
entry ? entry["api_key"].to_s.strip : nil
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# True when the paid DeepSeek web tools are usable: they are the selected
|
|
774
|
+
# search provider and a key could be resolved.
|
|
775
|
+
def deepseek_web_ready?
|
|
776
|
+
return false unless Clacky::SearchConfig.deepseek_web?
|
|
777
|
+
|
|
778
|
+
!deepseek_web_key.to_s.strip.empty?
|
|
779
|
+
rescue StandardError
|
|
780
|
+
false
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
# Endpoint base URL for DeepSeek web tools given the resolved key.
|
|
784
|
+
# If the key belongs to an OpenClacky model entry (or starts with clacky-),
|
|
785
|
+
# route to that model's base_url so the platform gateway can proxy and bill
|
|
786
|
+
# through the unified balance. Otherwise direct to DeepSeek official.
|
|
787
|
+
def deepseek_web_base_url(resolved_key = nil)
|
|
788
|
+
key = resolved_key || deepseek_web_key
|
|
789
|
+
default_url = Clacky::DeepSeekWebTools::DEFAULT_BASE_URL
|
|
790
|
+
return default_url if key.nil? || key.empty?
|
|
791
|
+
|
|
792
|
+
matching = web_tool_candidates.find { |m| m.is_a?(Hash) && m["api_key"].to_s.strip == key }
|
|
793
|
+
if matching
|
|
794
|
+
pid = provider_id_for(matching)
|
|
795
|
+
if pid == Clacky::Providers::OPENCLACKY_ID && matching["base_url"].to_s.strip != ""
|
|
796
|
+
return matching["base_url"].to_s.chomp("/")
|
|
797
|
+
end
|
|
798
|
+
elsif key.start_with?("clacky-")
|
|
799
|
+
return Clacky::Providers::PRESETS.fetch(Clacky::Providers::OPENCLACKY_ID)["base_url"].chomp("/")
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
default_url
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
# Current model first (so a session-level switch or overlay wins), then the
|
|
806
|
+
# rest of the configured models as fallback.
|
|
807
|
+
private def web_tool_candidates
|
|
808
|
+
current = current_model
|
|
809
|
+
current ? [current] + @models : @models
|
|
810
|
+
end
|
|
811
|
+
|
|
734
812
|
private def derive_media_model(kind, model_override: nil)
|
|
735
813
|
anchor = current_model || find_model_by_type("default")
|
|
736
814
|
return nil unless anchor
|
|
@@ -42,15 +42,20 @@ module Clacky
|
|
|
42
42
|
"abs-claude-sonnet-5" => "global.anthropic.claude-sonnet-5",
|
|
43
43
|
"abs-claude-sonnet-4-6" => "global.anthropic.claude-sonnet-4-6",
|
|
44
44
|
"abs-claude-sonnet-4-5" => "global.anthropic.claude-sonnet-4-5",
|
|
45
|
+
"abs-claude-haiku-5-5" => "global.anthropic.claude-haiku-5-5",
|
|
45
46
|
"abs-claude-haiku-4-5" => "global.anthropic.claude-haiku-4-5",
|
|
46
|
-
# GLM / Kimi /
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# unknown.
|
|
47
|
+
# GLM / Kimi / Grok / GPT via TokHub (oc-). The gateway books these under the
|
|
48
|
+
# alias itself, so the mapping is an identity — it exists so the billing UI
|
|
49
|
+
# can resolve the alias instead of dropping the record as unknown.
|
|
50
50
|
"oc-glm-5.3" => "oc-glm-5.3",
|
|
51
51
|
"oc-kimi-k3" => "oc-kimi-k3",
|
|
52
|
-
"oc-
|
|
52
|
+
"oc-gpt-6.1-sol" => "oc-gpt-6.1-sol",
|
|
53
|
+
"oc-gpt-6-astra" => "oc-gpt-6-astra",
|
|
54
|
+
"oc-gpt-6-sol" => "oc-gpt-6-sol",
|
|
55
|
+
"oc-gpt-6-luna" => "oc-gpt-6-luna",
|
|
56
|
+
"oc-grok-4.7" => "oc-grok-4.7",
|
|
53
57
|
# gemini chat (or-)
|
|
58
|
+
"or-gemini-4-argon" => "gemini-4-argon",
|
|
54
59
|
"or-gemini-3-1-pro" => "gemini-3.1-pro-preview",
|
|
55
60
|
"or-gemini-3-8-flash" => "gemini-3.8-flash",
|
|
56
61
|
"or-gemini-3-7-flash" => "gemini-3.7-flash",
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -683,7 +683,7 @@ module Clacky
|
|
|
683
683
|
already_exists = body["code"].to_s.include?("taken") ||
|
|
684
684
|
body["code"].to_s.include?("already") ||
|
|
685
685
|
result[:error].to_s.include?("HTTP 409")
|
|
686
|
-
{ success: false, error: msg, already_exists: already_exists }
|
|
686
|
+
{ success: false, error: msg, already_exists: already_exists, error_code: code }
|
|
687
687
|
end
|
|
688
688
|
rescue StandardError => e
|
|
689
689
|
{ success: false, error: "Network error: #{e.message}" }
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -346,8 +346,9 @@ module Clacky
|
|
|
346
346
|
end
|
|
347
347
|
|
|
348
348
|
# Handle the `/think` slash command — pick the reasoning effort level
|
|
349
|
-
# for the current session. "off"
|
|
350
|
-
#
|
|
349
|
+
# for the current session. "off" is kept as an explicit choice so model
|
|
350
|
+
# adapters can resolve it per family (MiniMax Flash and GLM-5.3 turn it
|
|
351
|
+
# into their lightest effort instead of the provider's deepest default).
|
|
351
352
|
private def handle_think_command(ui_controller, agent, session_manager = nil)
|
|
352
353
|
choice = ui_controller.show_reasoning_effort_menu(agent.reasoning_effort)
|
|
353
354
|
return if choice.nil?
|
|
@@ -481,8 +482,8 @@ module Clacky
|
|
|
481
482
|
private def auto_name_session(agent, input)
|
|
482
483
|
return unless agent.history.empty?
|
|
483
484
|
|
|
484
|
-
auto_name = input.to_s.gsub(/\s+/, " ").strip[0,
|
|
485
|
-
auto_name += "…" if input.to_s.strip.length >
|
|
485
|
+
auto_name = input.to_s.gsub(/\s+/, " ").strip[0, 80]
|
|
486
|
+
auto_name += "…" if input.to_s.strip.length > 80
|
|
486
487
|
agent.rename(auto_name)
|
|
487
488
|
end
|
|
488
489
|
|
data/lib/clacky/client.rb
CHANGED
|
@@ -56,6 +56,13 @@ module Clacky
|
|
|
56
56
|
@read_timeout = read_timeout
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Console URL where the user can inspect credit / API key state. nil when
|
|
60
|
+
# the provider has no preset (custom base_url).
|
|
61
|
+
def account_url
|
|
62
|
+
preset = Providers.get(@provider_id)
|
|
63
|
+
preset && preset["website_url"]
|
|
64
|
+
end
|
|
65
|
+
|
|
59
66
|
# Returns true when the client is using the AWS Bedrock Converse API.
|
|
60
67
|
def bedrock?
|
|
61
68
|
@use_bedrock
|
|
@@ -122,7 +129,7 @@ module Clacky
|
|
|
122
129
|
response = bedrock_connection.post(bedrock_endpoint(api_model)) { |r| r.body = body.to_json }
|
|
123
130
|
parse_simple_bedrock_response(response)
|
|
124
131
|
elsif anthropic_format?
|
|
125
|
-
body = MessageFormat::Anthropic.build_request_body(messages, api_model, [], max_tokens, false)
|
|
132
|
+
body = MessageFormat::Anthropic.build_request_body(messages, api_model, [], max_tokens, false, reasoning_effort: reasoning_effort)
|
|
126
133
|
response = anthropic_connection.post(anthropic_messages_path) { |r| r.body = body.to_json }
|
|
127
134
|
parse_simple_anthropic_response(response)
|
|
128
135
|
elsif responses_format?
|
|
@@ -349,10 +356,10 @@ module Clacky
|
|
|
349
356
|
end
|
|
350
357
|
|
|
351
358
|
unless response.status == 200
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
raise_error(
|
|
359
|
+
# Recover the streamed error body on the original response so headers
|
|
360
|
+
# remain available to error classification and routed-tier attribution.
|
|
361
|
+
response.env.body = sse_buf if response.body.to_s.empty?
|
|
362
|
+
raise_error(response)
|
|
356
363
|
end
|
|
357
364
|
|
|
358
365
|
result = aggregator.to_h
|