openclacky 0.5.1 ā 0.5.2
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/.clackyrules +2 -0
- data/README.md +1 -0
- data/lib/clacky/agent.rb +100 -6
- data/lib/clacky/agent_config.rb +4 -1
- data/lib/clacky/cli.rb +104 -88
- data/lib/clacky/client.rb +56 -7
- data/lib/clacky/hook_manager.rb +2 -1
- data/lib/clacky/progress_indicator.rb +12 -10
- data/lib/clacky/tools/safe_shell.rb +7 -2
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/ui/banner.rb +144 -0
- data/lib/clacky/ui/formatter.rb +209 -0
- data/lib/clacky/ui/prompt.rb +70 -0
- data/lib/clacky/ui/statusbar.rb +98 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +6 -0
- metadata +33 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8c1d065edf333a21a538aff43effdcefbc1cb9fa9a3c816d2da3943418f502fb
|
|
4
|
+
data.tar.gz: d891f1b03ff43bcd237eb510990b74145522404dd4fa3b2bc4b3741f3e495a32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5190473333b6c0259ee7ec76bf3127986b03fcbbaeba8face1e0f5bd5ebc60d40b9937df7ef129737eb6364789a214a515970728cfd71b80770cc671e5440004
|
|
7
|
+
data.tar.gz: 0a7858fe351e34810bca0b28ab91b3974041758608fe8c446e6f0c555402c990418bb6d3bc4ac071fe61fe3d881a5c190f191258287541b57508a940c2643752
|
data/.clackyrules
CHANGED
|
@@ -22,7 +22,9 @@ It provides chat functionality and autonomous AI agent capabilities with tool us
|
|
|
22
22
|
- Use frozen string literals: `# frozen_string_literal: true`
|
|
23
23
|
- Keep classes focused and single-responsibility
|
|
24
24
|
- Use meaningful variable and method names
|
|
25
|
+
- **IMPORTANT**: All code comments must be written in English
|
|
25
26
|
- Add descriptive comments for complex logic
|
|
27
|
+
- Use clear, self-documenting code with English naming
|
|
26
28
|
|
|
27
29
|
### Architecture Patterns
|
|
28
30
|
- Tools inherit from `Clacky::Tools::Base`
|
data/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A command-line interface for interacting with AI models. OpenClacky supports Ope
|
|
|
12
12
|
- šØ Colorful terminal output
|
|
13
13
|
- š OpenAI-compatible API support (OpenAI, Gitee AI, DeepSeek, etc.)
|
|
14
14
|
- š ļø Rich built-in tools: file operations, web search, code execution, and more
|
|
15
|
+
- ā” Prompt caching support for Claude models (reduces costs up to 90%)
|
|
15
16
|
|
|
16
17
|
## Installation
|
|
17
18
|
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -8,7 +8,8 @@ require_relative "utils/arguments_parser"
|
|
|
8
8
|
|
|
9
9
|
module Clacky
|
|
10
10
|
class Agent
|
|
11
|
-
attr_reader :session_id, :messages, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos
|
|
11
|
+
attr_reader :session_id, :messages, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
|
|
12
|
+
:cache_stats
|
|
12
13
|
|
|
13
14
|
# Pricing per 1M tokens (approximate - adjust based on actual model)
|
|
14
15
|
PRICING = {
|
|
@@ -65,6 +66,12 @@ module Clacky
|
|
|
65
66
|
@todos = [] # Store todos in memory
|
|
66
67
|
@iterations = 0
|
|
67
68
|
@total_cost = 0.0
|
|
69
|
+
@cache_stats = {
|
|
70
|
+
cache_creation_input_tokens: 0,
|
|
71
|
+
cache_read_input_tokens: 0,
|
|
72
|
+
total_requests: 0,
|
|
73
|
+
cache_hit_requests: 0
|
|
74
|
+
}
|
|
68
75
|
@start_time = nil
|
|
69
76
|
@working_dir = working_dir || Dir.pwd
|
|
70
77
|
@created_at = Time.now.iso8601
|
|
@@ -90,6 +97,14 @@ module Clacky
|
|
|
90
97
|
@working_dir = session_data[:working_dir]
|
|
91
98
|
@created_at = session_data[:created_at]
|
|
92
99
|
@total_tasks = session_data.dig(:stats, :total_tasks) || 0
|
|
100
|
+
|
|
101
|
+
# Restore cache statistics if available
|
|
102
|
+
@cache_stats = session_data.dig(:stats, :cache_stats) || {
|
|
103
|
+
cache_creation_input_tokens: 0,
|
|
104
|
+
cache_read_input_tokens: 0,
|
|
105
|
+
total_requests: 0,
|
|
106
|
+
cache_hit_requests: 0
|
|
107
|
+
}
|
|
93
108
|
|
|
94
109
|
# Check if the session ended with an error
|
|
95
110
|
last_status = session_data.dig(:stats, :last_status)
|
|
@@ -103,7 +118,7 @@ module Clacky
|
|
|
103
118
|
@messages = @messages[0...last_user_index]
|
|
104
119
|
|
|
105
120
|
# Trigger a hook to notify about the rollback
|
|
106
|
-
|
|
121
|
+
@hooks.trigger(:session_rollback, {
|
|
107
122
|
reason: "Previous session ended with error",
|
|
108
123
|
error_message: last_error,
|
|
109
124
|
rolled_back_message_index: last_user_index
|
|
@@ -122,7 +137,14 @@ module Clacky
|
|
|
122
137
|
# Add system prompt as the first message if this is the first run
|
|
123
138
|
if @messages.empty?
|
|
124
139
|
system_prompt = build_system_prompt
|
|
125
|
-
|
|
140
|
+
system_message = { role: "system", content: system_prompt }
|
|
141
|
+
|
|
142
|
+
# Enable caching for system prompt if configured and model supports it
|
|
143
|
+
if @config.enable_prompt_caching
|
|
144
|
+
system_message[:cache_control] = { type: "ephemeral" }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@messages << system_message
|
|
126
148
|
end
|
|
127
149
|
|
|
128
150
|
@messages << { role: "user", content: user_input }
|
|
@@ -222,7 +244,8 @@ module Clacky
|
|
|
222
244
|
total_iterations: @iterations,
|
|
223
245
|
total_cost_usd: @total_cost.round(4),
|
|
224
246
|
duration_seconds: @start_time ? (Time.now - @start_time).round(2) : 0,
|
|
225
|
-
last_status: status.to_s
|
|
247
|
+
last_status: status.to_s,
|
|
248
|
+
cache_stats: @cache_stats
|
|
226
249
|
}
|
|
227
250
|
|
|
228
251
|
# Add error message if status is error
|
|
@@ -240,6 +263,7 @@ module Clacky
|
|
|
240
263
|
max_iterations: @config.max_iterations,
|
|
241
264
|
max_cost_usd: @config.max_cost_usd,
|
|
242
265
|
enable_compression: @config.enable_compression,
|
|
266
|
+
enable_prompt_caching: @config.enable_prompt_caching,
|
|
243
267
|
keep_recent_messages: @config.keep_recent_messages,
|
|
244
268
|
max_tokens: @config.max_tokens,
|
|
245
269
|
verbose: @config.verbose
|
|
@@ -366,7 +390,8 @@ module Clacky
|
|
|
366
390
|
model: @config.model,
|
|
367
391
|
tools: tools_to_send,
|
|
368
392
|
max_tokens: @config.max_tokens,
|
|
369
|
-
verbose: @config.verbose
|
|
393
|
+
verbose: @config.verbose,
|
|
394
|
+
enable_caching: @config.enable_prompt_caching
|
|
370
395
|
)
|
|
371
396
|
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
|
372
397
|
retries += 1
|
|
@@ -386,6 +411,54 @@ module Clacky
|
|
|
386
411
|
|
|
387
412
|
track_cost(response[:usage])
|
|
388
413
|
|
|
414
|
+
# Handle truncated responses (when max_tokens limit is reached)
|
|
415
|
+
if response[:finish_reason] == "length"
|
|
416
|
+
# Count recent truncations to prevent infinite loops
|
|
417
|
+
recent_truncations = @messages.last(5).count { |m|
|
|
418
|
+
m[:role] == "user" && m[:content]&.include?("[SYSTEM] Your response was truncated")
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if recent_truncations >= 2
|
|
422
|
+
# Too many truncations - task is too complex
|
|
423
|
+
progress.finish
|
|
424
|
+
puts "\nā ļø Response truncated multiple times. Task is too complex for a single response." if @config.verbose
|
|
425
|
+
|
|
426
|
+
# Create a response that tells the user to break down the task
|
|
427
|
+
error_response = {
|
|
428
|
+
content: "I apologize, but this task is too complex to complete in a single response. " \
|
|
429
|
+
"Please break it down into smaller steps, or reduce the amount of content to generate at once.\n\n" \
|
|
430
|
+
"For example, when creating a long document:\n" \
|
|
431
|
+
"1. First create the file with a basic structure\n" \
|
|
432
|
+
"2. Then use edit() to add content section by section",
|
|
433
|
+
finish_reason: "stop",
|
|
434
|
+
tool_calls: nil
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# Add this as an assistant message so it appears in conversation
|
|
438
|
+
@messages << {
|
|
439
|
+
role: "assistant",
|
|
440
|
+
content: error_response[:content]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return error_response
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Insert system message to guide LLM to retry with smaller steps
|
|
447
|
+
@messages << {
|
|
448
|
+
role: "user",
|
|
449
|
+
content: "[SYSTEM] Your response was truncated due to length limit. Please retry with a different approach:\n" \
|
|
450
|
+
"- For long file content: create the file with structure first, then use edit() to add content section by section\n" \
|
|
451
|
+
"- Break down large tasks into multiple smaller steps\n" \
|
|
452
|
+
"- Avoid putting more than 2000 characters in a single tool call argument\n" \
|
|
453
|
+
"- Use multiple tool calls instead of one large call"
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
puts "ā ļø Response truncated due to length limit. Retrying with smaller steps..." if @config.verbose
|
|
457
|
+
|
|
458
|
+
# Recursively retry
|
|
459
|
+
return think(&block)
|
|
460
|
+
end
|
|
461
|
+
|
|
389
462
|
# Add assistant response to messages
|
|
390
463
|
msg = { role: "assistant" }
|
|
391
464
|
# Always include content field (some APIs require it even with tool_calls)
|
|
@@ -543,6 +616,18 @@ module Clacky
|
|
|
543
616
|
input_cost = (usage[:prompt_tokens] / 1_000_000.0) * PRICING[:input]
|
|
544
617
|
output_cost = (usage[:completion_tokens] / 1_000_000.0) * PRICING[:output]
|
|
545
618
|
@total_cost += input_cost + output_cost
|
|
619
|
+
|
|
620
|
+
# Track cache usage statistics
|
|
621
|
+
@cache_stats[:total_requests] += 1
|
|
622
|
+
|
|
623
|
+
if usage[:cache_creation_input_tokens]
|
|
624
|
+
@cache_stats[:cache_creation_input_tokens] += usage[:cache_creation_input_tokens]
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
if usage[:cache_read_input_tokens]
|
|
628
|
+
@cache_stats[:cache_read_input_tokens] += usage[:cache_read_input_tokens]
|
|
629
|
+
@cache_stats[:cache_hit_requests] += 1
|
|
630
|
+
end
|
|
546
631
|
end
|
|
547
632
|
|
|
548
633
|
def compress_messages_if_needed
|
|
@@ -582,7 +667,15 @@ module Clacky
|
|
|
582
667
|
summary = summarize_messages(messages_to_compress)
|
|
583
668
|
|
|
584
669
|
# Rebuild messages array: [system, summary, recent_messages]
|
|
585
|
-
|
|
670
|
+
# Preserve cache_control on system message if it exists
|
|
671
|
+
rebuilt_messages = [system_msg, summary, *recent_messages].compact
|
|
672
|
+
|
|
673
|
+
# Re-apply cache control to system message if caching is enabled
|
|
674
|
+
if @config.enable_prompt_caching && rebuilt_messages.first&.dig(:role) == "system"
|
|
675
|
+
rebuilt_messages.first[:cache_control] = { type: "ephemeral" }
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
@messages = rebuilt_messages
|
|
586
679
|
|
|
587
680
|
final_size = @messages.size
|
|
588
681
|
|
|
@@ -978,6 +1071,7 @@ module Clacky
|
|
|
978
1071
|
iterations: @iterations,
|
|
979
1072
|
duration_seconds: Time.now - @start_time,
|
|
980
1073
|
total_cost_usd: @total_cost.round(4),
|
|
1074
|
+
cache_stats: @cache_stats,
|
|
981
1075
|
messages: @messages,
|
|
982
1076
|
error: error
|
|
983
1077
|
}
|
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -7,7 +7,8 @@ module Clacky
|
|
|
7
7
|
|
|
8
8
|
attr_accessor :model, :max_iterations, :max_cost_usd, :timeout_seconds,
|
|
9
9
|
:permission_mode, :allowed_tools, :disallowed_tools,
|
|
10
|
-
:max_tokens, :verbose, :enable_compression, :keep_recent_messages
|
|
10
|
+
:max_tokens, :verbose, :enable_compression, :keep_recent_messages,
|
|
11
|
+
:enable_prompt_caching
|
|
11
12
|
|
|
12
13
|
def initialize(options = {})
|
|
13
14
|
@model = options[:model] || "gpt-3.5-turbo"
|
|
@@ -21,6 +22,8 @@ module Clacky
|
|
|
21
22
|
@verbose = options[:verbose] || false
|
|
22
23
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
23
24
|
@keep_recent_messages = options[:keep_recent_messages] || 20
|
|
25
|
+
# Enable prompt caching by default for cost savings
|
|
26
|
+
@enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -4,6 +4,10 @@ require "thor"
|
|
|
4
4
|
require "tty-prompt"
|
|
5
5
|
require "tty-spinner"
|
|
6
6
|
require "readline"
|
|
7
|
+
require_relative "ui/banner"
|
|
8
|
+
require_relative "ui/prompt"
|
|
9
|
+
require_relative "ui/statusbar"
|
|
10
|
+
require_relative "ui/formatter"
|
|
7
11
|
|
|
8
12
|
module Clacky
|
|
9
13
|
class CLI < Thor
|
|
@@ -140,7 +144,7 @@ module Clacky
|
|
|
140
144
|
# Report the error
|
|
141
145
|
say "\nā Error: #{e.message}", :red
|
|
142
146
|
say e.backtrace.first(5).join("\n"), :red if options[:verbose]
|
|
143
|
-
|
|
147
|
+
|
|
144
148
|
# Show session saved message
|
|
145
149
|
if session_manager&.last_saved_path
|
|
146
150
|
say "\nš Session saved: #{session_manager.last_saved_path}", :yellow
|
|
@@ -149,7 +153,7 @@ module Clacky
|
|
|
149
153
|
# Guide user to recover
|
|
150
154
|
say "\nš” To recover and retry, run:", :yellow
|
|
151
155
|
say " clacky agent -c", :cyan
|
|
152
|
-
|
|
156
|
+
|
|
153
157
|
exit 1
|
|
154
158
|
ensure
|
|
155
159
|
Dir.chdir(original_dir)
|
|
@@ -214,12 +218,14 @@ module Clacky
|
|
|
214
218
|
end
|
|
215
219
|
|
|
216
220
|
def display_agent_event(event)
|
|
221
|
+
formatter = ui_formatter
|
|
222
|
+
|
|
217
223
|
case event[:type]
|
|
218
224
|
when :thinking
|
|
219
|
-
|
|
225
|
+
formatter.thinking
|
|
220
226
|
when :assistant_message
|
|
221
227
|
# Display assistant's thinking/explanation before tool calls
|
|
222
|
-
|
|
228
|
+
formatter.assistant_message(event[:data][:content])
|
|
223
229
|
when :tool_call
|
|
224
230
|
display_tool_call(event[:data])
|
|
225
231
|
when :observation
|
|
@@ -227,15 +233,15 @@ module Clacky
|
|
|
227
233
|
# Auto-display TODO status if exists
|
|
228
234
|
display_todo_status_if_exists
|
|
229
235
|
when :answer
|
|
230
|
-
|
|
236
|
+
formatter.assistant_message(event[:data][:content])
|
|
231
237
|
when :tool_denied
|
|
232
|
-
|
|
238
|
+
formatter.tool_denied(event[:data][:name])
|
|
233
239
|
when :tool_planned
|
|
234
|
-
|
|
240
|
+
formatter.tool_planned(event[:data][:name])
|
|
235
241
|
when :tool_error
|
|
236
|
-
|
|
242
|
+
formatter.tool_error(event[:data][:error].message)
|
|
237
243
|
when :on_iteration
|
|
238
|
-
|
|
244
|
+
formatter.iteration(event[:data][:iteration]) if options[:verbose]
|
|
239
245
|
end
|
|
240
246
|
end
|
|
241
247
|
|
|
@@ -249,14 +255,14 @@ module Clacky
|
|
|
249
255
|
begin
|
|
250
256
|
args = JSON.parse(args_json, symbolize_names: true)
|
|
251
257
|
formatted = tool.format_call(args)
|
|
252
|
-
|
|
258
|
+
ui_formatter.tool_call(formatted)
|
|
253
259
|
rescue JSON::ParserError, StandardError => e
|
|
254
260
|
say "ā ļø Warning: Failed to format tool call: #{e.message}", :yellow
|
|
255
|
-
|
|
261
|
+
ui_formatter.tool_call("#{tool_name}(...)")
|
|
256
262
|
end
|
|
257
263
|
else
|
|
258
264
|
say "ā ļø Warning: Tool instance not found for '#{tool_name}'", :yellow
|
|
259
|
-
|
|
265
|
+
ui_formatter.tool_call("#{tool_name}(...)")
|
|
260
266
|
end
|
|
261
267
|
|
|
262
268
|
# Show verbose details if requested
|
|
@@ -274,15 +280,15 @@ module Clacky
|
|
|
274
280
|
if tool
|
|
275
281
|
begin
|
|
276
282
|
summary = tool.format_result(result)
|
|
277
|
-
|
|
283
|
+
ui_formatter.tool_result(summary)
|
|
278
284
|
rescue StandardError => e
|
|
279
|
-
|
|
285
|
+
ui_formatter.tool_result("Done")
|
|
280
286
|
end
|
|
281
287
|
else
|
|
282
288
|
# Fallback for unknown tools
|
|
283
289
|
result_str = result.to_s
|
|
284
290
|
summary = result_str.length > 100 ? "#{result_str[0..100]}..." : result_str
|
|
285
|
-
|
|
291
|
+
ui_formatter.tool_result(summary)
|
|
286
292
|
end
|
|
287
293
|
|
|
288
294
|
# Show verbose details if requested
|
|
@@ -313,42 +319,7 @@ module Clacky
|
|
|
313
319
|
todos = @current_agent.todos
|
|
314
320
|
return if todos.empty?
|
|
315
321
|
|
|
316
|
-
|
|
317
|
-
completed = todos.count { |t| t[:status] == "completed" }
|
|
318
|
-
total = todos.size
|
|
319
|
-
|
|
320
|
-
# Build progress bar
|
|
321
|
-
progress_bar = todos.map { |t| t[:status] == "completed" ? "ā" : "ā" }.join
|
|
322
|
-
|
|
323
|
-
# Check if all completed
|
|
324
|
-
if completed == total
|
|
325
|
-
say "\nš Tasks [#{completed}/#{total}]: #{progress_bar} š All completed!", :green
|
|
326
|
-
return
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
# Find current and next tasks
|
|
330
|
-
current_task = todos.find { |t| t[:status] == "pending" }
|
|
331
|
-
next_task_index = todos.index(current_task)
|
|
332
|
-
next_task = next_task_index && todos[next_task_index + 1]
|
|
333
|
-
|
|
334
|
-
say "\nš Tasks [#{completed}/#{total}]: #{progress_bar}", :yellow
|
|
335
|
-
if current_task
|
|
336
|
-
say " ā Next: ##{current_task[:id]} - #{current_task[:task]}", :white
|
|
337
|
-
end
|
|
338
|
-
if next_task && next_task[:status] == "pending"
|
|
339
|
-
say " ⢠After that: ##{next_task[:id]} - #{next_task[:task]}", :white
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def display_agent_result(result)
|
|
344
|
-
say "\n" + ("=" * 60), :cyan
|
|
345
|
-
say "Agent Session Complete", :green
|
|
346
|
-
say "=" * 60, :cyan
|
|
347
|
-
say "Status: #{result[:status]}", :green
|
|
348
|
-
say "Iterations: #{result[:iterations]}", :yellow
|
|
349
|
-
say "Duration: #{result[:duration_seconds].round(2)}s", :yellow
|
|
350
|
-
say "Total Cost: $#{result[:total_cost_usd]}", :yellow
|
|
351
|
-
say "=" * 60, :cyan
|
|
322
|
+
ui_formatter.todo_status(todos)
|
|
352
323
|
end
|
|
353
324
|
|
|
354
325
|
def validate_working_directory(path)
|
|
@@ -387,26 +358,37 @@ module Clacky
|
|
|
387
358
|
# Store agent as instance variable for access in display methods
|
|
388
359
|
@current_agent = agent
|
|
389
360
|
|
|
361
|
+
# Initialize UI components
|
|
362
|
+
banner = ui_banner
|
|
363
|
+
prompt = ui_prompt
|
|
364
|
+
statusbar = ui_statusbar
|
|
365
|
+
|
|
366
|
+
# Show startup banner for new session
|
|
367
|
+
if agent.total_tasks == 0
|
|
368
|
+
banner.display_startup
|
|
369
|
+
end
|
|
370
|
+
|
|
390
371
|
# Show session info if continuing
|
|
391
372
|
if agent.total_tasks > 0
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
373
|
+
banner.display_session_continue(
|
|
374
|
+
session_id: agent.session_id[0..7],
|
|
375
|
+
created_at: Time.parse(agent.created_at).strftime('%Y-%m-%d %H:%M'),
|
|
376
|
+
tasks: agent.total_tasks,
|
|
377
|
+
cost: agent.total_cost.round(4)
|
|
378
|
+
)
|
|
397
379
|
|
|
398
380
|
# Show recent conversation history
|
|
399
381
|
display_recent_messages(agent.messages, limit: 5)
|
|
382
|
+
else
|
|
383
|
+
# Show welcome info for new session
|
|
384
|
+
banner.display_agent_welcome(
|
|
385
|
+
working_dir: working_dir,
|
|
386
|
+
mode: agent_config.permission_mode,
|
|
387
|
+
max_iterations: agent_config.max_iterations,
|
|
388
|
+
max_cost: agent_config.max_cost_usd
|
|
389
|
+
)
|
|
400
390
|
end
|
|
401
391
|
|
|
402
|
-
say "š¤ Starting interactive agent mode...", :green
|
|
403
|
-
say "Working directory: #{working_dir}", :cyan
|
|
404
|
-
say "Mode: #{agent_config.permission_mode}", :yellow
|
|
405
|
-
say "Max iterations: #{agent_config.max_iterations} per task", :yellow
|
|
406
|
-
say "Max cost: $#{agent_config.max_cost_usd} per task", :yellow
|
|
407
|
-
say "\nType 'exit' or 'quit' to end the session.\n", :yellow
|
|
408
|
-
|
|
409
|
-
prompt = TTY::Prompt.new
|
|
410
392
|
total_tasks = agent.total_tasks
|
|
411
393
|
total_cost = agent.total_cost
|
|
412
394
|
|
|
@@ -418,15 +400,26 @@ module Clacky
|
|
|
418
400
|
unless current_message && !current_message.strip.empty?
|
|
419
401
|
say "\n" if total_tasks > 0
|
|
420
402
|
|
|
421
|
-
#
|
|
422
|
-
|
|
403
|
+
# Show status bar before input
|
|
404
|
+
statusbar.display(
|
|
405
|
+
working_dir: working_dir,
|
|
406
|
+
mode: agent_config.permission_mode.to_s,
|
|
407
|
+
model: agent_config.model,
|
|
408
|
+
tasks: total_tasks,
|
|
409
|
+
cost: total_cost
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Use enhanced prompt with "You:" prefix
|
|
413
|
+
current_message = prompt.read_input(prefix: "You:")
|
|
423
414
|
|
|
424
415
|
break if current_message.nil? || %w[exit quit].include?(current_message&.downcase&.strip)
|
|
425
416
|
next if current_message.strip.empty?
|
|
417
|
+
|
|
418
|
+
# Display user's message after input
|
|
419
|
+
ui_formatter.user_message(current_message)
|
|
426
420
|
end
|
|
427
421
|
|
|
428
422
|
total_tasks += 1
|
|
429
|
-
say "\n"
|
|
430
423
|
|
|
431
424
|
begin
|
|
432
425
|
result = agent.run(current_message) do |event|
|
|
@@ -441,17 +434,18 @@ module Clacky
|
|
|
441
434
|
end
|
|
442
435
|
|
|
443
436
|
# Show brief task completion
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
437
|
+
banner.display_task_complete(
|
|
438
|
+
iterations: result[:iterations],
|
|
439
|
+
cost: result[:total_cost_usd].round(4),
|
|
440
|
+
total_tasks: total_tasks,
|
|
441
|
+
total_cost: total_cost.round(4),
|
|
442
|
+
cache_stats: result[:cache_stats]
|
|
443
|
+
)
|
|
450
444
|
rescue Clacky::AgentInterrupted
|
|
451
445
|
# Save session on interruption
|
|
452
446
|
if session_manager
|
|
453
447
|
session_manager.save(agent.to_session_data(status: :interrupted))
|
|
454
|
-
|
|
448
|
+
ui_formatter.warning("Task interrupted by user (Ctrl+C)")
|
|
455
449
|
say "You can start a new task or type 'exit' to quit.\n", :yellow
|
|
456
450
|
end
|
|
457
451
|
rescue StandardError => e
|
|
@@ -461,17 +455,15 @@ module Clacky
|
|
|
461
455
|
end
|
|
462
456
|
|
|
463
457
|
# Report the error
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
458
|
+
banner.display_error(e.message, details: options[:verbose] ? e.backtrace.first(3).join("\n") : nil)
|
|
459
|
+
|
|
467
460
|
# Show session saved message
|
|
468
461
|
if session_manager&.last_saved_path
|
|
469
|
-
|
|
462
|
+
ui_formatter.info("Session saved: #{session_manager.last_saved_path}")
|
|
470
463
|
end
|
|
471
464
|
|
|
472
465
|
# Guide user to recover
|
|
473
|
-
|
|
474
|
-
say " clacky agent -c", :cyan
|
|
466
|
+
ui_formatter.info("To recover and retry, run: clacky agent -c")
|
|
475
467
|
say "\nOr you can continue with a new task or type 'exit' to quit.", :yellow
|
|
476
468
|
end
|
|
477
469
|
|
|
@@ -484,9 +476,10 @@ module Clacky
|
|
|
484
476
|
session_manager.save(agent.to_session_data)
|
|
485
477
|
end
|
|
486
478
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
479
|
+
banner.display_goodbye(
|
|
480
|
+
total_tasks: total_tasks,
|
|
481
|
+
total_cost: total_cost.round(4)
|
|
482
|
+
)
|
|
490
483
|
end
|
|
491
484
|
|
|
492
485
|
def list_sessions
|
|
@@ -582,21 +575,23 @@ module Clacky
|
|
|
582
575
|
return
|
|
583
576
|
end
|
|
584
577
|
|
|
585
|
-
|
|
586
|
-
|
|
578
|
+
formatter = ui_formatter
|
|
579
|
+
formatter.separator("ā")
|
|
580
|
+
say pastel.dim("Recent conversation history:"), :yellow
|
|
581
|
+
formatter.separator("ā")
|
|
587
582
|
|
|
588
583
|
recent.each do |msg|
|
|
589
584
|
case msg[:role]
|
|
590
585
|
when "user"
|
|
591
586
|
content = truncate_message(msg[:content], 150)
|
|
592
|
-
say "
|
|
587
|
+
say " #{pastel.blue('[>>]')} You: #{content}"
|
|
593
588
|
when "assistant"
|
|
594
589
|
content = truncate_message(msg[:content], 200)
|
|
595
|
-
say "
|
|
590
|
+
say " #{pastel.green('[<<]')} Assistant: #{content}"
|
|
596
591
|
end
|
|
597
592
|
end
|
|
598
593
|
|
|
599
|
-
|
|
594
|
+
formatter.separator("ā")
|
|
600
595
|
say ""
|
|
601
596
|
end
|
|
602
597
|
|
|
@@ -612,6 +607,27 @@ module Clacky
|
|
|
612
607
|
cleaned
|
|
613
608
|
end
|
|
614
609
|
end
|
|
610
|
+
|
|
611
|
+
# UI component accessors
|
|
612
|
+
def ui_banner
|
|
613
|
+
@ui_banner ||= UI::Banner.new
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def ui_prompt
|
|
617
|
+
@ui_prompt ||= UI::Prompt.new
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def ui_statusbar
|
|
621
|
+
@ui_statusbar ||= UI::StatusBar.new
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def ui_formatter
|
|
625
|
+
@ui_formatter ||= UI::Formatter.new
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def pastel
|
|
629
|
+
@pastel ||= Pastel.new
|
|
630
|
+
end
|
|
615
631
|
end
|
|
616
632
|
|
|
617
633
|
private
|
data/lib/clacky/client.rb
CHANGED
|
@@ -43,7 +43,9 @@ module Clacky
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Send messages with function calling (tools) support
|
|
46
|
-
|
|
46
|
+
# Options:
|
|
47
|
+
# - enable_caching: Enable prompt caching for system prompt and tools (default: false)
|
|
48
|
+
def send_messages_with_tools(messages, model:, tools:, max_tokens:, verbose: false, enable_caching: false)
|
|
47
49
|
body = {
|
|
48
50
|
model: model,
|
|
49
51
|
max_tokens: max_tokens,
|
|
@@ -51,7 +53,18 @@ module Clacky
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
# Add tools if provided
|
|
54
|
-
|
|
56
|
+
# For Claude API with caching: mark the last tool definition with cache_control
|
|
57
|
+
if tools&.any?
|
|
58
|
+
if enable_caching && supports_prompt_caching?(model)
|
|
59
|
+
# Deep clone tools to avoid modifying original
|
|
60
|
+
cached_tools = tools.map { |tool| deep_clone(tool) }
|
|
61
|
+
# Mark the last tool for caching (Claude caches from cache breakpoint to end)
|
|
62
|
+
cached_tools.last[:cache_control] = { type: "ephemeral" }
|
|
63
|
+
body[:tools] = cached_tools
|
|
64
|
+
else
|
|
65
|
+
body[:tools] = tools
|
|
66
|
+
end
|
|
67
|
+
end
|
|
55
68
|
|
|
56
69
|
# Debug output
|
|
57
70
|
if verbose || ENV["CLACKY_DEBUG"]
|
|
@@ -77,6 +90,31 @@ module Clacky
|
|
|
77
90
|
|
|
78
91
|
private
|
|
79
92
|
|
|
93
|
+
# Check if the model supports prompt caching
|
|
94
|
+
# Currently only Claude 3.5 Sonnet and newer Claude models support this
|
|
95
|
+
def supports_prompt_caching?(model)
|
|
96
|
+
model_str = model.to_s.downcase
|
|
97
|
+
# Claude 3.5 Sonnet (20241022 and newer) supports prompt caching
|
|
98
|
+
# Also Claude 3.7 Sonnet and Opus models when they're released
|
|
99
|
+
model_str.include?("claude-3.5-sonnet") ||
|
|
100
|
+
model_str.include?("claude-3-7") ||
|
|
101
|
+
model_str.include?("claude-4")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Deep clone a hash/array structure (for tool definitions)
|
|
105
|
+
def deep_clone(obj)
|
|
106
|
+
case obj
|
|
107
|
+
when Hash
|
|
108
|
+
obj.each_with_object({}) { |(k, v), h| h[k] = deep_clone(v) }
|
|
109
|
+
when Array
|
|
110
|
+
obj.map { |item| deep_clone(item) }
|
|
111
|
+
when String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass
|
|
112
|
+
obj
|
|
113
|
+
else
|
|
114
|
+
obj.dup rescue obj
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
80
118
|
def connection
|
|
81
119
|
@connection ||= Faraday.new(url: @base_url) do |conn|
|
|
82
120
|
conn.headers["Content-Type"] = "application/json"
|
|
@@ -117,15 +155,26 @@ module Clacky
|
|
|
117
155
|
puts " content length: #{message["content"]&.length || 0}"
|
|
118
156
|
end
|
|
119
157
|
|
|
158
|
+
# Parse usage with cache information
|
|
159
|
+
usage_data = {
|
|
160
|
+
prompt_tokens: usage["prompt_tokens"],
|
|
161
|
+
completion_tokens: usage["completion_tokens"],
|
|
162
|
+
total_tokens: usage["total_tokens"]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Add cache metrics if present (Claude API with prompt caching)
|
|
166
|
+
if usage["cache_creation_input_tokens"]
|
|
167
|
+
usage_data[:cache_creation_input_tokens] = usage["cache_creation_input_tokens"]
|
|
168
|
+
end
|
|
169
|
+
if usage["cache_read_input_tokens"]
|
|
170
|
+
usage_data[:cache_read_input_tokens] = usage["cache_read_input_tokens"]
|
|
171
|
+
end
|
|
172
|
+
|
|
120
173
|
{
|
|
121
174
|
content: message["content"],
|
|
122
175
|
tool_calls: parse_tool_calls(message["tool_calls"]),
|
|
123
176
|
finish_reason: data["choices"].first["finish_reason"],
|
|
124
|
-
usage:
|
|
125
|
-
prompt_tokens: usage["prompt_tokens"],
|
|
126
|
-
completion_tokens: usage["completion_tokens"],
|
|
127
|
-
total_tokens: usage["total_tokens"]
|
|
128
|
-
}
|
|
177
|
+
usage: usage_data
|
|
129
178
|
}
|
|
130
179
|
when 401
|
|
131
180
|
raise Error, "Invalid API key"
|