openclacky 0.7.0 → 0.7.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/commit/SKILL.md +29 -4
  3. data/.clackyrules +3 -1
  4. data/CHANGELOG.md +103 -2
  5. data/README.md +70 -161
  6. data/bin/clarky +11 -0
  7. data/docs/HOW-TO-USE-CN.md +96 -0
  8. data/docs/HOW-TO-USE.md +94 -0
  9. data/docs/config.example.yml +27 -0
  10. data/docs/deploy_subagent_design.md +540 -0
  11. data/docs/time_machine_design.md +247 -0
  12. data/docs/why-openclacky.md +0 -1
  13. data/lib/clacky/agent/cost_tracker.rb +180 -0
  14. data/lib/clacky/agent/llm_caller.rb +54 -0
  15. data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
  16. data/lib/clacky/agent/message_compressor_helper.rb +534 -0
  17. data/lib/clacky/agent/session_serializer.rb +152 -0
  18. data/lib/clacky/agent/skill_manager.rb +138 -0
  19. data/lib/clacky/agent/system_prompt_builder.rb +96 -0
  20. data/lib/clacky/agent/time_machine.rb +199 -0
  21. data/lib/clacky/agent/tool_executor.rb +434 -0
  22. data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
  23. data/lib/clacky/agent.rb +260 -1370
  24. data/lib/clacky/agent_config.rb +447 -10
  25. data/lib/clacky/cli.rb +275 -98
  26. data/lib/clacky/client.rb +12 -2
  27. data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
  28. data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
  29. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
  30. data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
  31. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
  32. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
  33. data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
  34. data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
  35. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
  36. data/lib/clacky/default_skills/new/SKILL.md +2 -2
  37. data/lib/clacky/json_ui_controller.rb +195 -0
  38. data/lib/clacky/providers.rb +107 -0
  39. data/lib/clacky/skill.rb +48 -7
  40. data/lib/clacky/skill_loader.rb +7 -0
  41. data/lib/clacky/tools/edit.rb +105 -48
  42. data/lib/clacky/tools/file_reader.rb +44 -73
  43. data/lib/clacky/tools/invoke_skill.rb +89 -0
  44. data/lib/clacky/tools/list_tasks.rb +54 -0
  45. data/lib/clacky/tools/redo_task.rb +41 -0
  46. data/lib/clacky/tools/safe_shell.rb +1 -1
  47. data/lib/clacky/tools/shell.rb +74 -62
  48. data/lib/clacky/tools/trash_manager.rb +1 -1
  49. data/lib/clacky/tools/undo_task.rb +32 -0
  50. data/lib/clacky/tools/web_fetch.rb +2 -1
  51. data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
  52. data/lib/clacky/ui2/components/inline_input.rb +23 -2
  53. data/lib/clacky/ui2/components/input_area.rb +65 -21
  54. data/lib/clacky/ui2/components/modal_component.rb +199 -62
  55. data/lib/clacky/ui2/layout_manager.rb +75 -25
  56. data/lib/clacky/ui2/line_editor.rb +23 -2
  57. data/lib/clacky/ui2/markdown_renderer.rb +31 -10
  58. data/lib/clacky/ui2/screen_buffer.rb +2 -0
  59. data/lib/clacky/ui2/ui_controller.rb +316 -37
  60. data/lib/clacky/ui2.rb +2 -0
  61. data/lib/clacky/ui_interface.rb +50 -0
  62. data/lib/clacky/utils/arguments_parser.rb +31 -3
  63. data/lib/clacky/utils/file_processor.rb +13 -18
  64. data/lib/clacky/version.rb +1 -1
  65. data/lib/clacky.rb +19 -9
  66. data/scripts/install.sh +274 -97
  67. data/scripts/uninstall.sh +12 -12
  68. metadata +40 -13
  69. data/.clacky/skills/test-skill/SKILL.md +0 -15
  70. data/lib/clacky/compression/base.rb +0 -231
  71. data/lib/clacky/compression/standard.rb +0 -339
  72. data/lib/clacky/config.rb +0 -117
  73. /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
  74. /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
  75. /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
  76. /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
  77. /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
  78. /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
@@ -0,0 +1,247 @@
1
+ # Time Machine Design Documentation
2
+
3
+ ## Overview
4
+
5
+ Time Machine is a feature that allows users to navigate through the agent's task execution history, providing undo/redo capabilities and branch exploration. Users can access it via ESC key or `/undo` command to view an interactive menu of past tasks.
6
+
7
+ ## Core Data Structure Design
8
+
9
+ ### Task History Graph
10
+
11
+ The Time Machine uses a minimal tree-based data structure to track task relationships:
12
+
13
+ **Three Core State Variables:**
14
+ 1. **task_parents** (Hash): Maps each task_id to its parent_id
15
+ - Forms a tree structure where each task points to its predecessor
16
+ - Root tasks have parent_id = 0
17
+ - Enables traversal in both directions (parent→children, child→parent)
18
+
19
+ 2. **current_task_id** (Integer): The latest created task ID
20
+ - Always increments when new tasks are created
21
+ - Never decreases, even during undo operations
22
+ - Represents the "tip" of the execution timeline
23
+
24
+ 3. **active_task_id** (Integer): The current active position in history
25
+ - Can move backward/forward during undo/redo
26
+ - Determines which messages are visible to the LLM
27
+ - When active_task_id < current_task_id, we're viewing "past" state
28
+
29
+ ### Task Metadata Structure
30
+
31
+ Each task in the history contains:
32
+ - **task_id**: Unique identifier (auto-incrementing integer)
33
+ - **summary**: Brief description (first 80 chars of user's message)
34
+ - **status**: One of three states
35
+ - `:past` - Task is before the current active position
36
+ - `:current` - Task is the active position (marked with `→`)
37
+ - `:future` - Task exists but is after active position (marked with `↯`)
38
+ - **has_branches**: Boolean indicating if multiple children exist (marked with `⎇`)
39
+
40
+ ## Snapshot Strategy
41
+
42
+ ### File State Preservation
43
+
44
+ **Complete AFTER-State Snapshots:**
45
+ - After each successful task execution, all modified files are saved
46
+ - Storage location: `~/.clacky/snapshots/{session_id}/task-{id}/`
47
+ - Each file is stored with its full relative path from working directory
48
+ - Only files modified during that task are snapshotted
49
+
50
+ **Why AFTER-state instead of BEFORE-state:**
51
+ - Simpler restoration logic (just copy files back)
52
+ - No need to track "what changed" - the snapshot IS the state
53
+ - Easier to verify correctness (snapshot = expected state)
54
+
55
+ **File Restoration Process:**
56
+ - When switching to a task, iterate through all its snapshotted files
57
+ - Copy each file from snapshot directory to working directory
58
+ - File permissions and timestamps are preserved
59
+
60
+ ### Message Filtering
61
+
62
+ **Active Messages Concept:**
63
+ - Messages array contains ALL messages (past, current, future)
64
+ - `active_messages()` method filters out "future" messages
65
+ - LLM only sees messages with `task_id <= active_task_id`
66
+ - This creates the illusion of time travel without data deletion
67
+
68
+ **Why Keep All Messages:**
69
+ - Enables redo operations (future messages preserved)
70
+ - Allows branch switching (alternative futures available)
71
+ - Simplifies session serialization (single source of truth)
72
+
73
+ ## Session Persistence
74
+
75
+ ### State Serialization
76
+
77
+ Time Machine state is saved under `:time_machine` key in session data:
78
+ - task_parents hash (complete tree structure)
79
+ - current_task_id (latest task number)
80
+ - active_task_id (current viewing position)
81
+
82
+ **Restoration Guarantees:**
83
+ - Complete task tree is rebuilt
84
+ - Active position is restored
85
+ - Snapshot files remain available across sessions
86
+ - User can continue undo/redo from where they left off
87
+
88
+ ## Critical Test Scenarios
89
+
90
+ ### 1. Basic Undo/Redo Flow
91
+
92
+ **Test Focus:**
93
+ - Sequential task creation increments task IDs correctly
94
+ - Undo moves active_task_id backward (current_task_id unchanged)
95
+ - Redo moves active_task_id forward
96
+ - File snapshots are correctly restored at each step
97
+ - Cannot undo beyond root task (task_id = 0)
98
+ - Cannot redo beyond current_task_id
99
+
100
+ **Edge Cases:**
101
+ - Undoing at root task should fail gracefully
102
+ - Redoing when already at tip should fail gracefully
103
+ - Multiple consecutive undos should work correctly
104
+
105
+ ### 2. Branching Scenarios
106
+
107
+ **Test Focus:**
108
+ - After undo, creating new task creates a branch
109
+ - New branch starts from active_task_id, not current_task_id
110
+ - Original future branch is preserved (for potential redo)
111
+ - Parent task is marked with `has_branches: true`
112
+ - Child tasks list should include both branches
113
+
114
+ **Branch Navigation:**
115
+ - Switching between branches restores correct file states
116
+ - Each branch maintains independent history
117
+ - Message filtering correctly shows only relevant messages
118
+
119
+ ### 3. Message Filtering and Task IDs
120
+
121
+ **Test Focus:**
122
+ - Every message is tagged with task_id (user, assistant, tool results)
123
+ - Active messages only include those with task_id <= active_task_id
124
+ - LLM never sees "future" messages during undo state
125
+ - After redo, future messages become visible again
126
+ - New tasks created after undo get fresh task IDs (not reused)
127
+
128
+ **Message Consistency:**
129
+ - Tool results are associated with correct task
130
+ - Multi-turn conversations maintain task association
131
+ - Error messages don't break task ID tagging
132
+
133
+ ### 4. File Snapshot Integrity
134
+
135
+ **Test Focus:**
136
+ - Only modified files are snapshotted (not entire project)
137
+ - File content is exactly preserved (byte-for-byte)
138
+ - Nested directory structures are correctly recreated
139
+ - Multiple files in single task are all snapshotted
140
+ - Snapshot directory naming prevents collisions
141
+
142
+ **Restoration Accuracy:**
143
+ - After undo + file restore, file content matches expected state
144
+ - Subsequent task execution works with restored files
145
+ - Binary files are handled correctly (not corrupted)
146
+
147
+ ### 5. Session Persistence and Recovery
148
+
149
+ **Test Focus:**
150
+ - Save session, restart, restore session preserves Time Machine state
151
+ - Task tree structure is fully rebuilt
152
+ - Active position is correctly restored
153
+ - Snapshot files are accessible after restart
154
+ - Undo/redo operations work identically after restore
155
+
156
+ **Persistence Edge Cases:**
157
+ - Empty task history (new session)
158
+ - Session with complex branching
159
+ - Session saved while in "undo" state (active_task_id < current_task_id)
160
+
161
+ ### 6. AI Tool Integration
162
+
163
+ **Test Focus:**
164
+ - Tools are correctly registered in tool registry
165
+ - AI can invoke undo_task, redo_task, list_tasks
166
+ - Agent parameter is correctly injected (similar to TodoManager pattern)
167
+ - Tool execution returns success/failure messages
168
+ - Tools respect permission modes (confirm_all, auto_approve, etc.)
169
+
170
+ **Tool Interaction:**
171
+ - AI calling undo_task modifies agent state correctly
172
+ - Subsequent AI responses use filtered messages
173
+ - Tool results are included in task history
174
+ - Multiple tool calls in sequence work correctly
175
+
176
+ ### 7. UI and User Interaction
177
+
178
+ **Test Focus:**
179
+ - ESC key triggers time machine menu
180
+ - `/undo` command works identically to ESC
181
+ - Menu displays correct task list with status indicators
182
+ - Visual markers: `→` current, `↯` future, `⎇` branches
183
+ - User selection triggers correct task switch
184
+ - Menu updates after undo/redo operations
185
+
186
+ **User Experience:**
187
+ - Task summaries are readable (truncated to 80 chars)
188
+ - Menu is responsive with large task histories
189
+ - Cancel/exit returns to normal operation
190
+ - Error messages are clear and actionable
191
+
192
+ ### 8. Integration with Existing Features
193
+
194
+ **Test Focus:**
195
+ - Works with message compression (no dependency on tool_calls)
196
+ - Compatible with session serialization
197
+ - Doesn't interfere with cost tracking
198
+ - Works with both UI modes (UI1 and UI2)
199
+ - Subagent forking doesn't inherit Time Machine state
200
+
201
+ **Feature Compatibility:**
202
+ - Todo manager works normally during undo state
203
+ - Web search tools work correctly
204
+ - File tools (write, edit) trigger snapshots
205
+ - Shell commands can be undone via file snapshots
206
+
207
+ ## Design Principles
208
+
209
+ ### Minimal Invasiveness
210
+ - Only 3 new instance variables in Agent class
211
+ - No changes to core message structure (only adds task_id field)
212
+ - Existing tools unaware of Time Machine existence
213
+ - No performance impact when not in use
214
+
215
+ ### Data Integrity
216
+ - Never delete messages or snapshots (immutable history)
217
+ - File restoration is idempotent (can redo multiple times)
218
+ - Task IDs never reused (prevents confusion)
219
+ - Snapshot isolation (each task has independent directory)
220
+
221
+ ### User Control
222
+ - Explicit user action required (ESC or /undo)
223
+ - Clear visual feedback on current position
224
+ - Cannot accidentally lose work (future preserved)
225
+ - Can explore branches without commitment
226
+
227
+ ### Developer Friendly
228
+ - Simple tree data structure (easy to reason about)
229
+ - Comprehensive test coverage (55 test cases)
230
+ - Clear separation of concerns (module-based design)
231
+ - Well-documented edge cases
232
+
233
+ ## Future Enhancement Possibilities
234
+
235
+ ### Potential Improvements
236
+ - Automatic snapshot garbage collection (old sessions)
237
+ - Diff view between task states
238
+ - Named checkpoints (user-defined bookmarks)
239
+ - Merge branches functionality
240
+ - Export task history as replay script
241
+ - Snapshot compression for large files
242
+
243
+ ### Scalability Considerations
244
+ - Large file handling (incremental snapshots)
245
+ - Long session histories (pagination in UI)
246
+ - Multiple simultaneous branches (better visualization)
247
+ - Remote collaboration (shared task history)
@@ -79,7 +79,6 @@ A command-line AI assistant that's approachable for non-technical users but powe
79
79
  |------|----------|----------|
80
80
  | `auto_approve` | Execute all tools automatically | Batch operations |
81
81
  | `confirm_safes` | Auto-approve safe operations | Daily development |
82
- | `confirm_edits` | Confirm file modifications | Careful work |
83
82
  | `plan_only` | Generate plans only | Code review |
84
83
 
85
84
  5. **Session Recovery**
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ class Agent
5
+ # Cost tracking and token usage statistics
6
+ # Manages cost calculation, token estimation, and usage display
7
+ module CostTracker
8
+ # Track cost from API usage
9
+ # Updates total cost and displays iteration statistics
10
+ # @param usage [Hash] Usage data from API response
11
+ # @param raw_api_usage [Hash, nil] Raw API usage data for debugging
12
+ def track_cost(usage, raw_api_usage: nil)
13
+ # Priority 1: Use API-provided cost if available (OpenRouter, LiteLLM, etc.)
14
+ iteration_cost = nil
15
+ if usage[:api_cost]
16
+ @total_cost += usage[:api_cost]
17
+ @cost_source = :api
18
+ @task_cost_source = :api
19
+ iteration_cost = usage[:api_cost]
20
+ @ui&.log("Using API-provided cost: $#{usage[:api_cost]}", level: :debug) if @config.verbose
21
+ else
22
+ # Priority 2: Calculate from tokens using ModelPricing
23
+ result = ModelPricing.calculate_cost(model: current_model, usage: usage)
24
+ cost = result[:cost]
25
+ pricing_source = result[:source]
26
+
27
+ @total_cost += cost
28
+ iteration_cost = cost
29
+ # Map pricing source to cost source: :price or :default
30
+ @cost_source = pricing_source
31
+ @task_cost_source = pricing_source
32
+
33
+ if @config.verbose
34
+ source_label = pricing_source == :price ? "model pricing" : "default pricing"
35
+ @ui&.log("Calculated cost for #{@config.model_name} using #{source_label}: $#{cost.round(6)}", level: :debug)
36
+ @ui&.log("Usage breakdown: prompt=#{usage[:prompt_tokens]}, completion=#{usage[:completion_tokens]}, cache_write=#{usage[:cache_creation_input_tokens] || 0}, cache_read=#{usage[:cache_read_input_tokens] || 0}", level: :debug)
37
+ end
38
+ end
39
+
40
+ # Display token usage statistics for this iteration
41
+ display_iteration_tokens(usage, iteration_cost)
42
+
43
+ # Update session bar cost in real-time (don't wait for agent.run to finish)
44
+ @ui&.update_sessionbar(cost: @total_cost)
45
+
46
+ # Track cache usage statistics (global)
47
+ @cache_stats[:total_requests] += 1
48
+
49
+ if usage[:cache_creation_input_tokens]
50
+ @cache_stats[:cache_creation_input_tokens] += usage[:cache_creation_input_tokens]
51
+ end
52
+
53
+ if usage[:cache_read_input_tokens]
54
+ @cache_stats[:cache_read_input_tokens] += usage[:cache_read_input_tokens]
55
+ @cache_stats[:cache_hit_requests] += 1
56
+ end
57
+
58
+ # Store raw API usage samples (keep last 3 for debugging)
59
+ if raw_api_usage
60
+ @cache_stats[:raw_api_usage_samples] ||= []
61
+ @cache_stats[:raw_api_usage_samples] << raw_api_usage
62
+ @cache_stats[:raw_api_usage_samples] = @cache_stats[:raw_api_usage_samples].last(3)
63
+ end
64
+
65
+ # Track cache usage for current task
66
+ if @task_cache_stats
67
+ @task_cache_stats[:total_requests] += 1
68
+
69
+ if usage[:cache_creation_input_tokens]
70
+ @task_cache_stats[:cache_creation_input_tokens] += usage[:cache_creation_input_tokens]
71
+ end
72
+
73
+ if usage[:cache_read_input_tokens]
74
+ @task_cache_stats[:cache_read_input_tokens] += usage[:cache_read_input_tokens]
75
+ @task_cache_stats[:cache_hit_requests] += 1
76
+ end
77
+ end
78
+ end
79
+
80
+ # Estimate token count for a message content
81
+ # Simple approximation: characters / 4 (English text)
82
+ # For Chinese/other languages, characters / 2 is more accurate
83
+ # This is a rough estimate for compression triggering purposes
84
+ # @param content [String, Array, Object] Message content
85
+ # @return [Integer] Estimated token count
86
+ def estimate_tokens(content)
87
+ return 0 if content.nil?
88
+
89
+ text = if content.is_a?(String)
90
+ content
91
+ elsif content.is_a?(Array)
92
+ # Handle content arrays (e.g., with images)
93
+ # Add safety check to prevent nil.compact error
94
+ mapped = content.map { |c| c[:text] if c.is_a?(Hash) }
95
+ (mapped || []).compact.join
96
+ else
97
+ content.to_s
98
+ end
99
+
100
+ return 0 if text.empty?
101
+
102
+ # Detect language mix - count non-ASCII characters
103
+ ascii_count = text.bytes.count { |b| b < 128 }
104
+ total_bytes = text.bytes.length
105
+
106
+ # Mix ratio (1.0 = all English, 0.5 = all Chinese)
107
+ mix_ratio = total_bytes > 0 ? ascii_count.to_f / total_bytes : 1.0
108
+
109
+ # English: ~4 chars/token, Chinese: ~2 chars/token
110
+ base_chars_per_token = mix_ratio * 4 + (1 - mix_ratio) * 2
111
+
112
+ (text.length / base_chars_per_token).to_i + 50 # Add overhead for message structure
113
+ end
114
+
115
+ # Calculate total token count for all messages
116
+ # Returns estimated tokens and breakdown by category
117
+ # @return [Hash] Token counts by role and total
118
+ def total_message_tokens
119
+ system_tokens = 0
120
+ user_tokens = 0
121
+ assistant_tokens = 0
122
+ tool_tokens = 0
123
+ summary_tokens = 0
124
+
125
+ @messages.each do |msg|
126
+ tokens = estimate_tokens(msg[:content])
127
+ case msg[:role]
128
+ when "system"
129
+ system_tokens += tokens
130
+ when "user"
131
+ user_tokens += tokens
132
+ when "assistant"
133
+ assistant_tokens += tokens
134
+ when "tool"
135
+ tool_tokens += tokens
136
+ end
137
+ end
138
+
139
+ {
140
+ total: system_tokens + user_tokens + assistant_tokens + tool_tokens,
141
+ system: system_tokens,
142
+ user: user_tokens,
143
+ assistant: assistant_tokens,
144
+ tool: tool_tokens
145
+ }
146
+ end
147
+
148
+ private
149
+
150
+ # Display token usage for current iteration
151
+ # @param usage [Hash] Usage data from API
152
+ # @param cost [Float] Cost for this iteration
153
+ def display_iteration_tokens(usage, cost)
154
+ prompt_tokens = usage[:prompt_tokens] || 0
155
+ completion_tokens = usage[:completion_tokens] || 0
156
+ total_tokens = usage[:total_tokens] || (prompt_tokens + completion_tokens)
157
+ cache_write = usage[:cache_creation_input_tokens] || 0
158
+ cache_read = usage[:cache_read_input_tokens] || 0
159
+
160
+ # Calculate token delta from previous iteration
161
+ delta_tokens = total_tokens - @previous_total_tokens
162
+ @previous_total_tokens = total_tokens # Update for next iteration
163
+
164
+ # Prepare data for UI to format and display
165
+ token_data = {
166
+ delta_tokens: delta_tokens,
167
+ prompt_tokens: prompt_tokens,
168
+ completion_tokens: completion_tokens,
169
+ total_tokens: total_tokens,
170
+ cache_write: cache_write,
171
+ cache_read: cache_read,
172
+ cost: cost
173
+ }
174
+
175
+ # Let UI handle formatting and display
176
+ @ui&.show_token_usage(token_data)
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clacky
4
+ class Agent
5
+ # LLM API call management
6
+ # Handles API calls with retry logic and progress indication
7
+ module LlmCaller
8
+ # Execute LLM API call with progress indicator, retry logic, and cost tracking
9
+ # This method is shared by both normal think() and compression flows
10
+ # @return [Hash] API response with :content, :tool_calls, :usage, etc.
11
+ private def call_llm
12
+ @ui&.show_progress
13
+
14
+ tools_to_send = @tool_registry.all_definitions
15
+
16
+ # Retry logic for network failures
17
+ max_retries = 10
18
+ retry_delay = 5
19
+ retries = 0
20
+
21
+ begin
22
+ # Use active_messages to filter out "future" messages after undo
23
+ messages_to_send = respond_to?(:active_messages) ? active_messages : @messages
24
+
25
+ response = @client.send_messages_with_tools(
26
+ messages_to_send,
27
+ model: current_model,
28
+ tools: tools_to_send,
29
+ max_tokens: @config.max_tokens,
30
+ enable_caching: @config.enable_prompt_caching
31
+ )
32
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
33
+ @ui&.clear_progress
34
+ retries += 1
35
+ if retries <= max_retries
36
+ @ui&.show_warning("Network failed: #{e.message}. Retry #{retries}/#{max_retries}...")
37
+ sleep retry_delay
38
+ retry
39
+ else
40
+ @ui&.show_error("Network failed after #{max_retries} retries: #{e.message}")
41
+ raise AgentError, "Network connection failed after #{max_retries} retries: #{e.message}"
42
+ end
43
+ ensure
44
+ @ui&.clear_progress
45
+ end
46
+
47
+ # Track cost for all LLM calls
48
+ track_cost(response[:usage], raw_api_usage: response[:raw_api_usage])
49
+
50
+ response
51
+ end
52
+ end
53
+ end
54
+ end
@@ -57,6 +57,12 @@ module Clacky
57
57
 
58
58
  # Generate compression instruction message to be inserted into conversation
59
59
  # This enables cache reuse by using the same API call with tools
60
+ #
61
+ # SIMPLIFIED APPROACH:
62
+ # - Don't duplicate conversation history in the compression message
63
+ # - LLM can already see all messages, just ask it to compress
64
+ # - Keep the instruction small for better cache efficiency
65
+ #
60
66
  # @param messages [Array<Hash>] Original conversation messages
61
67
  # @param recent_messages [Array<Hash>] Recent messages to keep uncompressed (optional)
62
68
  # @return [Hash] Compression instruction message to insert, or nil if nothing to compress
@@ -67,12 +73,12 @@ module Clacky
67
73
  # If nothing to compress, return nil
68
74
  return nil if messages_to_compress.empty?
69
75
 
70
- # Build compression prompt with instruction and conversation
71
- content = build_compression_content(messages_to_compress)
72
- full_prompt = "#{COMPRESSION_PROMPT}\n\nConversation to compress:\n\n#{content}"
73
-
74
- # Return the compression instruction as a user message with system_injected marker
75
- { role: "user", content: full_prompt, system_injected: true }
76
+ # Simple compression instruction - LLM can see the history already
77
+ {
78
+ role: "user",
79
+ content: COMPRESSION_PROMPT,
80
+ system_injected: true
81
+ }
76
82
  end
77
83
 
78
84
  # Parse LLM response and rebuild message list with compression
@@ -98,36 +104,6 @@ module Clacky
98
104
 
99
105
  private
100
106
 
101
- def build_compression_content(messages)
102
- # Format messages as readable text for compression
103
- messages.map do |msg|
104
- role = msg[:role]
105
- content = format_content(msg[:content])
106
- "[#{role.upcase}] #{content}"
107
- end.join("\n\n")
108
- end
109
-
110
- def format_content(content)
111
- return content if content.is_a?(String)
112
-
113
- if content.is_a?(Array)
114
- content.map do |block|
115
- case block[:type]
116
- when "text"
117
- block[:text]
118
- when "tool_use"
119
- "TOOL: #{block[:name]}(#{block[:input]})"
120
- when "tool_result"
121
- "RESULT: #{block[:content]}"
122
- else
123
- block.to_s
124
- end
125
- end.join("\n")
126
- else
127
- content.to_s
128
- end
129
- end
130
-
131
107
  def parse_compressed_result(result)
132
108
  # Return the compressed result as a single assistant message
133
109
  # Keep the <analysis> or <summary> tags as they provide semantic context