code-puppy 0.0.165__tar.gz → 0.0.166__tar.gz
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.
- {code_puppy-0.0.165 → code_puppy-0.0.166}/PKG-INFO +1 -1
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/message_history_processor.py +88 -123
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tools/file_operations.py +42 -7
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/custom_widgets.py +7 -2
- {code_puppy-0.0.165 → code_puppy-0.0.166}/pyproject.toml +1 -1
- {code_puppy-0.0.165 → code_puppy-0.0.166}/.gitignore +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/LICENSE +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/README.md +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/__main__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agent.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agents/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agents/agent_code_puppy.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agents/agent_creator_agent.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agents/agent_manager.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agents/agent_orchestrator.json +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agents/base_agent.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agents/json_agent.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/agents/runtime_manager.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/callbacks.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/command_handler.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/file_path_completion.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/load_context_completion.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/add_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/base.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/handler.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/help_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/install_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/list_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/logs_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/remove_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/restart_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/search_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/start_all_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/start_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/status_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/stop_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/test_command.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/utils.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/meta_command_handler.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/model_picker_completion.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/motd.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/prompt_toolkit_completion.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/utils.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/config.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/http_utils.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/main.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/async_lifecycle.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/blocking_startup.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/captured_stdio_server.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/circuit_breaker.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/config_wizard.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/dashboard.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/error_isolation.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/examples/retry_example.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/health_monitor.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/managed_server.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/manager.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/registry.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/retry_manager.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/server_registry_catalog.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/status_tracker.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/mcp/system_tools.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/messaging/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/messaging/message_queue.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/messaging/queue_console.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/messaging/renderers.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/messaging/spinner/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/messaging/spinner/console_spinner.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/messaging/spinner/spinner_base.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/messaging/spinner/textual_spinner.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/model_factory.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/models.json +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/plugins/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/reopenable_async_client.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/round_robin_model.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/state_management.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/status_display.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/summarization_agent.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tools/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tools/agent_tools.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tools/command_runner.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tools/common.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tools/file_modifications.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tools/tools_content.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/app.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/chat_view.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/command_history_modal.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/copy_button.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/human_input_modal.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/input_area.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/sidebar.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/status_bar.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/messages.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/models/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/models/chat_message.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/models/command_history.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/models/enums.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/screens/__init__.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/screens/help.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/screens/mcp_install_wizard.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/screens/settings.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/screens/tools.py +0 -0
- {code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/version_checker.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import queue
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, List, Set, Tuple
|
|
4
4
|
|
|
5
5
|
import pydantic
|
|
6
6
|
from pydantic_ai.messages import ModelMessage, ModelRequest, TextPart, ToolCallPart
|
|
7
7
|
|
|
8
8
|
from code_puppy.config import (
|
|
9
|
-
get_compaction_strategy,
|
|
10
|
-
get_compaction_threshold,
|
|
11
9
|
get_model_name,
|
|
12
10
|
get_protected_token_count,
|
|
11
|
+
get_compaction_threshold,
|
|
12
|
+
get_compaction_strategy,
|
|
13
13
|
)
|
|
14
14
|
from code_puppy.messaging import emit_error, emit_info, emit_warning
|
|
15
15
|
from code_puppy.model_factory import ModelFactory
|
|
@@ -82,9 +82,7 @@ def estimate_tokens_for_message(message: ModelMessage) -> int:
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def filter_huge_messages(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
85
|
-
|
|
86
|
-
deduplicated = deduplicate_tool_returns(messages)
|
|
87
|
-
filtered = [m for m in deduplicated if estimate_tokens_for_message(m) < 50000]
|
|
85
|
+
filtered = [m for m in messages if estimate_tokens_for_message(m) < 50000]
|
|
88
86
|
pruned = prune_interrupted_tool_calls(filtered)
|
|
89
87
|
return pruned
|
|
90
88
|
|
|
@@ -150,6 +148,81 @@ def split_messages_for_protected_summarization(
|
|
|
150
148
|
return messages_to_summarize, protected_messages
|
|
151
149
|
|
|
152
150
|
|
|
151
|
+
def deduplicate_tool_returns(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
152
|
+
"""
|
|
153
|
+
Remove duplicate tool returns while preserving the first occurrence for each tool_call_id.
|
|
154
|
+
|
|
155
|
+
This function identifies tool-return parts that share the same tool_call_id and
|
|
156
|
+
removes duplicates, keeping only the first return for each id. This prevents
|
|
157
|
+
conversation corruption from duplicate tool_result blocks.
|
|
158
|
+
"""
|
|
159
|
+
if not messages:
|
|
160
|
+
return messages
|
|
161
|
+
|
|
162
|
+
seen_tool_returns: Set[str] = set()
|
|
163
|
+
deduplicated: List[ModelMessage] = []
|
|
164
|
+
removed_count = 0
|
|
165
|
+
|
|
166
|
+
for msg in messages:
|
|
167
|
+
# Check if this message has any parts we need to filter
|
|
168
|
+
if not hasattr(msg, "parts") or not msg.parts:
|
|
169
|
+
deduplicated.append(msg)
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Filter parts within this message
|
|
173
|
+
filtered_parts = []
|
|
174
|
+
msg_had_duplicates = False
|
|
175
|
+
|
|
176
|
+
for part in msg.parts:
|
|
177
|
+
tool_call_id = getattr(part, "tool_call_id", None)
|
|
178
|
+
part_kind = getattr(part, "part_kind", None)
|
|
179
|
+
|
|
180
|
+
# Check if this is a tool-return part
|
|
181
|
+
if tool_call_id and part_kind in {
|
|
182
|
+
"tool-return",
|
|
183
|
+
"tool-result",
|
|
184
|
+
"tool_result",
|
|
185
|
+
}:
|
|
186
|
+
if tool_call_id in seen_tool_returns:
|
|
187
|
+
# This is a duplicate return, skip it
|
|
188
|
+
msg_had_duplicates = True
|
|
189
|
+
removed_count += 1
|
|
190
|
+
continue
|
|
191
|
+
else:
|
|
192
|
+
# First occurrence of this return, keep it
|
|
193
|
+
seen_tool_returns.add(tool_call_id)
|
|
194
|
+
filtered_parts.append(part)
|
|
195
|
+
else:
|
|
196
|
+
# Not a tool return, always keep
|
|
197
|
+
filtered_parts.append(part)
|
|
198
|
+
|
|
199
|
+
# If we filtered out parts, create a new message with filtered parts
|
|
200
|
+
if msg_had_duplicates and filtered_parts:
|
|
201
|
+
# Create a new message with the same attributes but filtered parts
|
|
202
|
+
new_msg = type(msg)(parts=filtered_parts)
|
|
203
|
+
# Copy over other attributes if they exist
|
|
204
|
+
for attr_name in dir(msg):
|
|
205
|
+
if (
|
|
206
|
+
not attr_name.startswith("_")
|
|
207
|
+
and attr_name != "parts"
|
|
208
|
+
and hasattr(msg, attr_name)
|
|
209
|
+
):
|
|
210
|
+
try:
|
|
211
|
+
setattr(new_msg, attr_name, getattr(msg, attr_name))
|
|
212
|
+
except (AttributeError, TypeError):
|
|
213
|
+
# Skip attributes that can't be set
|
|
214
|
+
pass
|
|
215
|
+
deduplicated.append(new_msg)
|
|
216
|
+
elif filtered_parts: # No duplicates but has parts
|
|
217
|
+
deduplicated.append(msg)
|
|
218
|
+
# If no parts remain after filtering, drop the entire message
|
|
219
|
+
|
|
220
|
+
if removed_count > 0:
|
|
221
|
+
emit_warning(f"Removed {removed_count} duplicate tool-return part(s)")
|
|
222
|
+
|
|
223
|
+
return deduplicated
|
|
224
|
+
|
|
225
|
+
|
|
153
226
|
def summarize_messages(
|
|
154
227
|
messages: List[ModelMessage], with_protection=True
|
|
155
228
|
) -> Tuple[List[ModelMessage], List[ModelMessage]]:
|
|
@@ -236,100 +309,21 @@ def get_model_context_length() -> int:
|
|
|
236
309
|
return int(context_length)
|
|
237
310
|
|
|
238
311
|
|
|
239
|
-
def deduplicate_tool_returns(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
240
|
-
"""
|
|
241
|
-
Remove duplicate tool returns while preserving the first occurrence for each tool_call_id.
|
|
242
|
-
|
|
243
|
-
This function identifies tool-return parts that share the same tool_call_id and
|
|
244
|
-
removes duplicates, keeping only the first return for each id. This prevents
|
|
245
|
-
conversation corruption from duplicate tool_result blocks.
|
|
246
|
-
"""
|
|
247
|
-
if not messages:
|
|
248
|
-
return messages
|
|
249
|
-
|
|
250
|
-
seen_tool_returns: Set[str] = set()
|
|
251
|
-
deduplicated: List[ModelMessage] = []
|
|
252
|
-
removed_count = 0
|
|
253
|
-
|
|
254
|
-
for msg in messages:
|
|
255
|
-
# Check if this message has any parts we need to filter
|
|
256
|
-
if not hasattr(msg, "parts") or not msg.parts:
|
|
257
|
-
deduplicated.append(msg)
|
|
258
|
-
continue
|
|
259
|
-
|
|
260
|
-
# Filter parts within this message
|
|
261
|
-
filtered_parts = []
|
|
262
|
-
msg_had_duplicates = False
|
|
263
|
-
|
|
264
|
-
for part in msg.parts:
|
|
265
|
-
tool_call_id = getattr(part, "tool_call_id", None)
|
|
266
|
-
part_kind = getattr(part, "part_kind", None)
|
|
267
|
-
|
|
268
|
-
# Check if this is a tool-return part
|
|
269
|
-
if tool_call_id and part_kind in {
|
|
270
|
-
"tool-return",
|
|
271
|
-
"tool-result",
|
|
272
|
-
"tool_result",
|
|
273
|
-
}:
|
|
274
|
-
if tool_call_id in seen_tool_returns:
|
|
275
|
-
# This is a duplicate return, skip it
|
|
276
|
-
msg_had_duplicates = True
|
|
277
|
-
removed_count += 1
|
|
278
|
-
continue
|
|
279
|
-
else:
|
|
280
|
-
# First occurrence of this return, keep it
|
|
281
|
-
seen_tool_returns.add(tool_call_id)
|
|
282
|
-
filtered_parts.append(part)
|
|
283
|
-
else:
|
|
284
|
-
# Not a tool return, always keep
|
|
285
|
-
filtered_parts.append(part)
|
|
286
|
-
|
|
287
|
-
# If we filtered out parts, create a new message with filtered parts
|
|
288
|
-
if msg_had_duplicates and filtered_parts:
|
|
289
|
-
# Create a new message with the same attributes but filtered parts
|
|
290
|
-
new_msg = type(msg)(parts=filtered_parts)
|
|
291
|
-
# Copy over other attributes if they exist
|
|
292
|
-
for attr_name in dir(msg):
|
|
293
|
-
if (
|
|
294
|
-
not attr_name.startswith("_")
|
|
295
|
-
and attr_name != "parts"
|
|
296
|
-
and hasattr(msg, attr_name)
|
|
297
|
-
):
|
|
298
|
-
try:
|
|
299
|
-
setattr(new_msg, attr_name, getattr(msg, attr_name))
|
|
300
|
-
except (AttributeError, TypeError):
|
|
301
|
-
# Skip attributes that can't be set
|
|
302
|
-
pass
|
|
303
|
-
deduplicated.append(new_msg)
|
|
304
|
-
elif filtered_parts: # No duplicates but has parts
|
|
305
|
-
deduplicated.append(msg)
|
|
306
|
-
# If no parts remain after filtering, drop the entire message
|
|
307
|
-
|
|
308
|
-
if removed_count > 0:
|
|
309
|
-
emit_warning(f"Removed {removed_count} duplicate tool-return part(s)")
|
|
310
|
-
|
|
311
|
-
return deduplicated
|
|
312
|
-
|
|
313
|
-
|
|
314
312
|
def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
315
313
|
"""
|
|
316
314
|
Remove any messages that participate in mismatched tool call sequences.
|
|
317
315
|
|
|
318
316
|
A mismatched tool call id is one that appears in a ToolCall (model/tool request)
|
|
319
|
-
without a corresponding tool return, or vice versa. We
|
|
320
|
-
|
|
321
|
-
messages that contain parts referencing mismatched tool_call_ids.
|
|
317
|
+
without a corresponding tool return, or vice versa. We preserve original order
|
|
318
|
+
and only drop messages that contain parts referencing mismatched tool_call_ids.
|
|
322
319
|
"""
|
|
323
320
|
if not messages:
|
|
324
321
|
return messages
|
|
325
322
|
|
|
326
|
-
|
|
327
|
-
|
|
323
|
+
tool_call_ids: Set[str] = set()
|
|
324
|
+
tool_return_ids: Set[str] = set()
|
|
328
325
|
|
|
329
|
-
|
|
330
|
-
tool_return_counts: Dict[str, int] = {}
|
|
331
|
-
|
|
332
|
-
# First pass: count occurrences of each tool_call_id for calls vs returns
|
|
326
|
+
# First pass: collect ids for calls vs returns
|
|
333
327
|
for msg in messages:
|
|
334
328
|
for part in getattr(msg, "parts", []) or []:
|
|
335
329
|
tool_call_id = getattr(part, "tool_call_id", None)
|
|
@@ -338,25 +332,11 @@ def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMess
|
|
|
338
332
|
# Heuristic: if it's an explicit ToolCallPart or has a tool_name/args,
|
|
339
333
|
# consider it a call; otherwise it's a return/result.
|
|
340
334
|
if part.part_kind == "tool-call":
|
|
341
|
-
|
|
342
|
-
tool_call_counts.get(tool_call_id, 0) + 1
|
|
343
|
-
)
|
|
335
|
+
tool_call_ids.add(tool_call_id)
|
|
344
336
|
else:
|
|
345
|
-
|
|
346
|
-
tool_return_counts.get(tool_call_id, 0) + 1
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Find mismatched tool_call_ids (not exactly 1:1 ratio)
|
|
350
|
-
all_tool_ids = set(tool_call_counts.keys()) | set(tool_return_counts.keys())
|
|
351
|
-
mismatched: Set[str] = set()
|
|
352
|
-
|
|
353
|
-
for tool_id in all_tool_ids:
|
|
354
|
-
call_count = tool_call_counts.get(tool_id, 0)
|
|
355
|
-
return_count = tool_return_counts.get(tool_id, 0)
|
|
356
|
-
# Enforce strict 1:1 ratio - both must be exactly 1
|
|
357
|
-
if call_count != 1 or return_count != 1:
|
|
358
|
-
mismatched.add(tool_id)
|
|
337
|
+
tool_return_ids.add(tool_call_id)
|
|
359
338
|
|
|
339
|
+
mismatched: Set[str] = tool_call_ids.symmetric_difference(tool_return_ids)
|
|
360
340
|
if not mismatched:
|
|
361
341
|
return messages
|
|
362
342
|
|
|
@@ -382,10 +362,7 @@ def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMess
|
|
|
382
362
|
|
|
383
363
|
|
|
384
364
|
def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
385
|
-
# First,
|
|
386
|
-
messages = deduplicate_tool_returns(messages)
|
|
387
|
-
|
|
388
|
-
# Then, prune any interrupted/mismatched tool-call conversations
|
|
365
|
+
# First, prune any interrupted/mismatched tool-call conversations
|
|
389
366
|
total_current_tokens = sum(estimate_tokens_for_message(msg) for msg in messages)
|
|
390
367
|
|
|
391
368
|
model_max = get_model_context_length()
|
|
@@ -477,8 +454,6 @@ def truncation(
|
|
|
477
454
|
messages: List[ModelMessage], protected_tokens: int
|
|
478
455
|
) -> List[ModelMessage]:
|
|
479
456
|
emit_info("Truncating message history to manage token usage")
|
|
480
|
-
# First deduplicate tool returns to clean up any duplicates
|
|
481
|
-
messages = deduplicate_tool_returns(messages)
|
|
482
457
|
result = [messages[0]] # Always keep the first message (system prompt)
|
|
483
458
|
num_tokens = 0
|
|
484
459
|
stack = queue.LifoQueue()
|
|
@@ -501,10 +476,6 @@ def truncation(
|
|
|
501
476
|
|
|
502
477
|
def message_history_accumulator(messages: List[Any]):
|
|
503
478
|
_message_history = get_message_history()
|
|
504
|
-
|
|
505
|
-
# Deduplicate tool returns in current history before processing new messages
|
|
506
|
-
_message_history = deduplicate_tool_returns(_message_history)
|
|
507
|
-
|
|
508
479
|
message_history_hashes = set([hash_message(m) for m in _message_history])
|
|
509
480
|
for msg in messages:
|
|
510
481
|
if (
|
|
@@ -513,12 +484,6 @@ def message_history_accumulator(messages: List[Any]):
|
|
|
513
484
|
):
|
|
514
485
|
_message_history.append(msg)
|
|
515
486
|
|
|
516
|
-
# Deduplicate tool returns again after adding new messages to ensure no duplicates
|
|
517
|
-
_message_history = deduplicate_tool_returns(_message_history)
|
|
518
|
-
|
|
519
|
-
# Update the message history with deduplicated messages
|
|
520
|
-
set_message_history(_message_history)
|
|
521
|
-
|
|
522
487
|
# Apply message history trimming using the main processor
|
|
523
488
|
# This ensures we maintain global state while still managing context limits
|
|
524
489
|
message_history_processor(_message_history)
|
|
@@ -208,17 +208,22 @@ def _list_files(
|
|
|
208
208
|
files = result.stdout.strip().split("\n") if result.stdout.strip() else []
|
|
209
209
|
|
|
210
210
|
# Create ListedFile objects with metadata
|
|
211
|
-
for
|
|
212
|
-
if not
|
|
211
|
+
for full_path in files:
|
|
212
|
+
if not full_path: # Skip empty lines
|
|
213
213
|
continue
|
|
214
214
|
|
|
215
|
-
full_path = os.path.join(directory, file_path)
|
|
216
|
-
|
|
217
215
|
# Skip if file doesn't exist (though it should)
|
|
218
216
|
if not os.path.exists(full_path):
|
|
219
217
|
continue
|
|
220
218
|
|
|
219
|
+
# Extract relative path from the full path
|
|
220
|
+
if full_path.startswith(directory):
|
|
221
|
+
file_path = full_path[len(directory):].lstrip(os.sep)
|
|
222
|
+
else:
|
|
223
|
+
file_path = full_path
|
|
224
|
+
|
|
221
225
|
# For non-recursive mode, skip files in subdirectories
|
|
226
|
+
# Only check the relative path, not the full path
|
|
222
227
|
if not recursive and os.sep in file_path:
|
|
223
228
|
continue
|
|
224
229
|
|
|
@@ -242,7 +247,7 @@ def _list_files(
|
|
|
242
247
|
if entry_type == "file":
|
|
243
248
|
size = actual_size
|
|
244
249
|
|
|
245
|
-
# Calculate depth
|
|
250
|
+
# Calculate depth based on the relative path
|
|
246
251
|
depth = file_path.count(os.sep)
|
|
247
252
|
|
|
248
253
|
# Add directory entries if needed for files
|
|
@@ -281,6 +286,33 @@ def _list_files(
|
|
|
281
286
|
except (FileNotFoundError, PermissionError, OSError):
|
|
282
287
|
# Skip files we can't access
|
|
283
288
|
continue
|
|
289
|
+
|
|
290
|
+
# In non-recursive mode, we also need to explicitly list directories in the target directory
|
|
291
|
+
# ripgrep's --files option only returns files, not directories
|
|
292
|
+
if not recursive:
|
|
293
|
+
try:
|
|
294
|
+
entries = os.listdir(directory)
|
|
295
|
+
for entry in entries:
|
|
296
|
+
full_entry_path = os.path.join(directory, entry)
|
|
297
|
+
# Skip if it doesn't exist or if it's a file (since files are already listed by ripgrep)
|
|
298
|
+
if not os.path.exists(full_entry_path) or os.path.isfile(full_entry_path):
|
|
299
|
+
continue
|
|
300
|
+
|
|
301
|
+
# For non-recursive mode, only include directories that are directly in the target directory
|
|
302
|
+
if os.path.isdir(full_entry_path):
|
|
303
|
+
# Create a ListedFile for the directory
|
|
304
|
+
results.append(
|
|
305
|
+
ListedFile(
|
|
306
|
+
path=entry,
|
|
307
|
+
type="directory",
|
|
308
|
+
size=0,
|
|
309
|
+
full_path=full_entry_path,
|
|
310
|
+
depth=0,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
except (FileNotFoundError, PermissionError, OSError):
|
|
314
|
+
# Skip directories we can't access
|
|
315
|
+
pass
|
|
284
316
|
except subprocess.TimeoutExpired:
|
|
285
317
|
error_msg = (
|
|
286
318
|
"[red bold]Error:[/red bold] List files command timed out after 30 seconds"
|
|
@@ -337,9 +369,12 @@ def _list_files(
|
|
|
337
369
|
else:
|
|
338
370
|
return "\U0001f4c4"
|
|
339
371
|
|
|
372
|
+
# Count items in results
|
|
340
373
|
dir_count = sum(1 for item in results if item.type == "directory")
|
|
341
374
|
file_count = sum(1 for item in results if item.type == "file")
|
|
342
375
|
total_size = sum(item.size for item in results if item.type == "file")
|
|
376
|
+
|
|
377
|
+
|
|
343
378
|
|
|
344
379
|
# Build the directory header section
|
|
345
380
|
dir_name = os.path.basename(directory) or directory
|
|
@@ -393,8 +428,8 @@ def _list_files(
|
|
|
393
428
|
final_divider = "[dim]" + "─" * 100 + "\n" + "[/dim]"
|
|
394
429
|
output_lines.append(final_divider)
|
|
395
430
|
|
|
396
|
-
# Return
|
|
397
|
-
return ListFileOutput(content="\n".join(output_lines)
|
|
431
|
+
# Return the content string
|
|
432
|
+
return ListFileOutput(content="\n".join(output_lines))
|
|
398
433
|
|
|
399
434
|
|
|
400
435
|
def _read_file(
|
|
@@ -21,8 +21,13 @@ class CustomTextArea(TextArea):
|
|
|
21
21
|
|
|
22
22
|
def on_key(self, event):
|
|
23
23
|
"""Handle key events before they reach the internal _on_key handler."""
|
|
24
|
-
#
|
|
25
|
-
if event.key == "
|
|
24
|
+
# Let the binding system handle alt+enter
|
|
25
|
+
if event.key == "alt+enter":
|
|
26
|
+
# Don't prevent default - let the binding system handle it
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
# Handle escape+enter manually
|
|
30
|
+
if event.key == "escape+enter":
|
|
26
31
|
self.action_insert_newline()
|
|
27
32
|
event.prevent_default()
|
|
28
33
|
event.stop()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/load_context_completion.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/model_picker_completion.py
RENAMED
|
File without changes
|
|
File without changes
|
{code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/command_line/prompt_toolkit_completion.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code_puppy-0.0.165 → code_puppy-0.0.166}/code_puppy/tui/components/command_history_modal.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|