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.
- checksums.yaml +4 -4
- data/.clacky/skills/commit/SKILL.md +29 -4
- data/.clackyrules +3 -1
- data/CHANGELOG.md +103 -2
- data/README.md +70 -161
- data/bin/clarky +11 -0
- data/docs/HOW-TO-USE-CN.md +96 -0
- data/docs/HOW-TO-USE.md +94 -0
- data/docs/config.example.yml +27 -0
- data/docs/deploy_subagent_design.md +540 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/why-openclacky.md +0 -1
- data/lib/clacky/agent/cost_tracker.rb +180 -0
- data/lib/clacky/agent/llm_caller.rb +54 -0
- data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
- data/lib/clacky/agent/message_compressor_helper.rb +534 -0
- data/lib/clacky/agent/session_serializer.rb +152 -0
- data/lib/clacky/agent/skill_manager.rb +138 -0
- data/lib/clacky/agent/system_prompt_builder.rb +96 -0
- data/lib/clacky/agent/time_machine.rb +199 -0
- data/lib/clacky/agent/tool_executor.rb +434 -0
- data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
- data/lib/clacky/agent.rb +260 -1370
- data/lib/clacky/agent_config.rb +447 -10
- data/lib/clacky/cli.rb +275 -98
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +195 -0
- data/lib/clacky/providers.rb +107 -0
- data/lib/clacky/skill.rb +48 -7
- data/lib/clacky/skill_loader.rb +7 -0
- data/lib/clacky/tools/edit.rb +105 -48
- data/lib/clacky/tools/file_reader.rb +44 -73
- data/lib/clacky/tools/invoke_skill.rb +89 -0
- data/lib/clacky/tools/list_tasks.rb +54 -0
- data/lib/clacky/tools/redo_task.rb +41 -0
- data/lib/clacky/tools/safe_shell.rb +1 -1
- data/lib/clacky/tools/shell.rb +74 -62
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/undo_task.rb +32 -0
- data/lib/clacky/tools/web_fetch.rb +2 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
- data/lib/clacky/ui2/components/inline_input.rb +23 -2
- data/lib/clacky/ui2/components/input_area.rb +65 -21
- data/lib/clacky/ui2/components/modal_component.rb +199 -62
- data/lib/clacky/ui2/layout_manager.rb +75 -25
- data/lib/clacky/ui2/line_editor.rb +23 -2
- data/lib/clacky/ui2/markdown_renderer.rb +31 -10
- data/lib/clacky/ui2/screen_buffer.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +316 -37
- data/lib/clacky/ui2.rb +2 -0
- data/lib/clacky/ui_interface.rb +50 -0
- data/lib/clacky/utils/arguments_parser.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -18
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +19 -9
- data/scripts/install.sh +274 -97
- data/scripts/uninstall.sh +12 -12
- metadata +40 -13
- data/.clacky/skills/test-skill/SKILL.md +0 -15
- data/lib/clacky/compression/base.rb +0 -231
- data/lib/clacky/compression/standard.rb +0 -339
- data/lib/clacky/config.rb +0 -117
- /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
- /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
- /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
- /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
- /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
- /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)
|
data/docs/why-openclacky.md
CHANGED
|
@@ -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
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|