shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.dev1__py3-none-any.whl
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.
- shotgun/agents/agent_manager.py +382 -60
- shotgun/agents/common.py +15 -9
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +383 -82
- shotgun/agents/config/models.py +122 -18
- shotgun/agents/config/provider.py +81 -15
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +475 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +36 -5
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
- shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
- shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
- shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +2 -2
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +8 -2
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +188 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +154 -0
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +18 -10
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +163 -15
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +357 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +60 -27
- shotgun/main.py +77 -11
- shotgun/posthog_telemetry.py +38 -29
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +243 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/telemetry.py +10 -33
- shotgun/tui/app.py +310 -46
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1531 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +91 -4
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +191 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +14 -7
- shotgun/tui/screens/github_issue.py +111 -0
- shotgun/tui/screens/model_picker.py +77 -32
- shotgun/tui/screens/onboarding.py +580 -0
- shotgun/tui/screens/pipx_migration.py +205 -0
- shotgun/tui/screens/provider_config.py +116 -35
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +112 -18
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +137 -11
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +187 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
|
@@ -13,24 +13,51 @@ logger = get_logger(__name__)
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
async def apply_persistent_compaction(
|
|
16
|
-
messages: list[ModelMessage], deps: AgentDeps
|
|
16
|
+
messages: list[ModelMessage], deps: AgentDeps, force: bool = False
|
|
17
17
|
) -> list[ModelMessage]:
|
|
18
18
|
"""Apply compaction to message history for persistent storage.
|
|
19
19
|
|
|
20
20
|
This ensures that compacted history is actually used as the conversation baseline,
|
|
21
21
|
preventing cascading compaction issues across both CLI and TUI usage patterns.
|
|
22
22
|
|
|
23
|
+
Compaction happens in two phases:
|
|
24
|
+
1. Deterministic pre-compaction: Remove file content (no LLM needed)
|
|
25
|
+
2. LLM-based compaction: Summarize conversation if still over threshold
|
|
26
|
+
|
|
23
27
|
Args:
|
|
24
28
|
messages: Full message history from agent run
|
|
25
29
|
deps: Agent dependencies containing model config
|
|
30
|
+
force: If True, force compaction even if below token threshold
|
|
26
31
|
|
|
27
32
|
Returns:
|
|
28
33
|
Compacted message history that should be stored as conversation state
|
|
29
34
|
"""
|
|
35
|
+
from .file_content_deduplication import deduplicate_file_content
|
|
30
36
|
from .history_processors import token_limit_compactor
|
|
31
37
|
|
|
32
38
|
try:
|
|
33
|
-
#
|
|
39
|
+
# STEP 1: Deterministic pre-compaction (no LLM cost)
|
|
40
|
+
# Remove file content from tool returns - files are still accessible
|
|
41
|
+
# via retrieve_code (codebase) or read_file (.shotgun/ folder)
|
|
42
|
+
messages, tokens_saved = deduplicate_file_content(
|
|
43
|
+
messages,
|
|
44
|
+
retention_window=3, # Keep last 3 messages' file content intact
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if tokens_saved > 0:
|
|
48
|
+
logger.info(
|
|
49
|
+
f"Pre-compaction: removed ~{tokens_saved:,} tokens of file content"
|
|
50
|
+
)
|
|
51
|
+
track_event(
|
|
52
|
+
"file_content_deduplication",
|
|
53
|
+
{
|
|
54
|
+
"tokens_saved_estimate": tokens_saved,
|
|
55
|
+
"retention_window": 3,
|
|
56
|
+
"model_name": deps.llm_model.name.value,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# STEP 2: Count tokens after pre-compaction
|
|
34
61
|
estimated_tokens = await estimate_tokens_from_messages(messages, deps.llm_model)
|
|
35
62
|
|
|
36
63
|
# Create minimal usage info for compaction check
|
|
@@ -46,7 +73,7 @@ async def apply_persistent_compaction(
|
|
|
46
73
|
self.usage = usage
|
|
47
74
|
|
|
48
75
|
ctx = MockContext(deps, usage)
|
|
49
|
-
compacted_messages = await token_limit_compactor(ctx, messages)
|
|
76
|
+
compacted_messages = await token_limit_compactor(ctx, messages, force=force)
|
|
50
77
|
|
|
51
78
|
# Log the result for monitoring
|
|
52
79
|
original_size = len(messages)
|
|
@@ -59,17 +86,21 @@ async def apply_persistent_compaction(
|
|
|
59
86
|
f"({reduction_pct:.1f}% reduction)"
|
|
60
87
|
)
|
|
61
88
|
|
|
62
|
-
# Track persistent compaction event
|
|
89
|
+
# Track persistent compaction event with simple metrics (fast, no token counting)
|
|
63
90
|
track_event(
|
|
64
91
|
"persistent_compaction_applied",
|
|
65
92
|
{
|
|
93
|
+
# Basic compaction metrics
|
|
66
94
|
"messages_before": original_size,
|
|
67
95
|
"messages_after": compacted_size,
|
|
68
|
-
"tokens_before": estimated_tokens,
|
|
69
96
|
"reduction_percentage": round(reduction_pct, 2),
|
|
70
97
|
"agent_mode": deps.agent_mode.value
|
|
71
98
|
if hasattr(deps, "agent_mode") and deps.agent_mode
|
|
72
99
|
else "unknown",
|
|
100
|
+
# Model and provider info (no computation needed)
|
|
101
|
+
"model_name": deps.llm_model.name.value,
|
|
102
|
+
"provider": deps.llm_model.provider.value,
|
|
103
|
+
"key_provider": deps.llm_model.key_provider.value,
|
|
73
104
|
},
|
|
74
105
|
)
|
|
75
106
|
else:
|
|
@@ -10,6 +10,11 @@ INPUT_BUFFER_TOKENS = 500
|
|
|
10
10
|
MIN_SUMMARY_TOKENS = 100
|
|
11
11
|
TOKEN_LIMIT_RATIO = 0.8
|
|
12
12
|
|
|
13
|
+
# Chunked compaction constants
|
|
14
|
+
CHUNK_TARGET_RATIO = 0.60 # Target chunk size as % of max_input_tokens
|
|
15
|
+
CHUNK_SAFE_RATIO = 0.70 # Max safe ratio before triggering chunked compaction
|
|
16
|
+
RETENTION_WINDOW_MESSAGES = 5 # Keep last N message groups outside compaction
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
class SummaryType(Enum):
|
|
15
20
|
"""Types of summarization requests for logging."""
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Pre-compaction file content deduplication for conversation history.
|
|
2
|
+
|
|
3
|
+
This module provides a deterministic pre-pass that removes file content from
|
|
4
|
+
tool returns before LLM-based compaction. Files are still accessible via
|
|
5
|
+
`retrieve_code` (codebase) or `read_file` (.shotgun/ folder).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import copy
|
|
9
|
+
import re
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic_ai.messages import (
|
|
14
|
+
ModelMessage,
|
|
15
|
+
ModelRequest,
|
|
16
|
+
ToolReturnPart,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from shotgun.logging_config import get_logger
|
|
20
|
+
|
|
21
|
+
logger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FileReadTool(StrEnum):
|
|
25
|
+
"""Tool names that read file content."""
|
|
26
|
+
|
|
27
|
+
CODEBASE = "file_read" # Reads from indexed codebase (Kuzu graph)
|
|
28
|
+
SHOTGUN_FOLDER = "read_file" # Reads from .shotgun/ folder
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Minimum content length to bother deduplicating (skip tiny files)
|
|
32
|
+
MIN_CONTENT_LENGTH = 500
|
|
33
|
+
|
|
34
|
+
# Placeholder templates for each type
|
|
35
|
+
CODEBASE_PLACEHOLDER = (
|
|
36
|
+
"**File**: `{file_path}`\n"
|
|
37
|
+
"**Size**: {size_bytes} bytes | **Language**: {language}\n"
|
|
38
|
+
"**Content**: [Removed for compaction - use `retrieve_code` or `file_read` to access]"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
SHOTGUN_PLACEHOLDER = (
|
|
42
|
+
"**File**: `.shotgun/{filename}`\n"
|
|
43
|
+
"**Content**: [Removed for compaction - file persisted in .shotgun/ folder]"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Pattern for parsing file_read output (codebase files)
|
|
47
|
+
# Format: **File**: `path`\n**Size**: N bytes\n[optional encoding]\n\n**Content**:\n```lang\ncontent```
|
|
48
|
+
CODEBASE_FILE_PATTERN = re.compile(
|
|
49
|
+
r"\*\*File\*\*:\s*`([^`]+)`\s*\n" # File path
|
|
50
|
+
r"\*\*Size\*\*:\s*(\d+)\s*bytes\s*\n" # Size in bytes
|
|
51
|
+
r"(?:\*\*Encoding\*\*:.*?\n)?" # Optional encoding line
|
|
52
|
+
r"\n\*\*Content\*\*:\s*\n" # Blank line + Content header
|
|
53
|
+
r"```(\w*)\n" # Language tag
|
|
54
|
+
r"(.*?)```", # Actual content
|
|
55
|
+
re.DOTALL,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_codebase_file_content(
|
|
60
|
+
content: str,
|
|
61
|
+
) -> tuple[str, int, str, str] | None:
|
|
62
|
+
"""Parse file_read tool return content.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
content: The tool return content string
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tuple of (file_path, size_bytes, language, actual_content) or None if not parseable
|
|
69
|
+
"""
|
|
70
|
+
match = CODEBASE_FILE_PATTERN.search(content)
|
|
71
|
+
if not match:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
file_path = match.group(1)
|
|
75
|
+
size_bytes = int(match.group(2))
|
|
76
|
+
language = match.group(3) or ""
|
|
77
|
+
actual_content = match.group(4)
|
|
78
|
+
|
|
79
|
+
return file_path, size_bytes, language, actual_content
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _create_codebase_placeholder(file_path: str, size_bytes: int, language: str) -> str:
|
|
83
|
+
"""Create placeholder for codebase file content."""
|
|
84
|
+
return CODEBASE_PLACEHOLDER.format(
|
|
85
|
+
file_path=file_path,
|
|
86
|
+
size_bytes=size_bytes,
|
|
87
|
+
language=language or "unknown",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _create_shotgun_placeholder(filename: str) -> str:
|
|
92
|
+
"""Create placeholder for .shotgun/ file content."""
|
|
93
|
+
return SHOTGUN_PLACEHOLDER.format(filename=filename)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _estimate_tokens_saved(original: str, replacement: str) -> int:
|
|
97
|
+
"""Rough estimate of tokens saved (~4 chars per token)."""
|
|
98
|
+
original_chars = len(original)
|
|
99
|
+
replacement_chars = len(replacement)
|
|
100
|
+
# Rough token estimate: ~4 characters per token for code
|
|
101
|
+
return max(0, (original_chars - replacement_chars) // 4)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def deduplicate_file_content(
|
|
105
|
+
messages: list[ModelMessage],
|
|
106
|
+
retention_window: int = 3,
|
|
107
|
+
) -> tuple[list[ModelMessage], int]:
|
|
108
|
+
"""Replace file read content with placeholders for indexed/persisted files.
|
|
109
|
+
|
|
110
|
+
This is a deterministic pre-compaction pass that reduces tokens without
|
|
111
|
+
requiring an LLM. Files remain accessible via their respective tools.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
messages: Conversation history
|
|
115
|
+
retention_window: Keep full content in last N messages (for recent context)
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Tuple of (modified_messages, estimated_tokens_saved)
|
|
119
|
+
"""
|
|
120
|
+
if not messages:
|
|
121
|
+
return messages, 0
|
|
122
|
+
|
|
123
|
+
# Deep copy to avoid modifying original
|
|
124
|
+
modified_messages = copy.deepcopy(messages)
|
|
125
|
+
total_tokens_saved = 0
|
|
126
|
+
files_deduplicated = 0
|
|
127
|
+
|
|
128
|
+
# Calculate retention boundary (keep last N messages intact)
|
|
129
|
+
retention_start = max(0, len(modified_messages) - retention_window)
|
|
130
|
+
|
|
131
|
+
for msg_idx, message in enumerate(modified_messages):
|
|
132
|
+
# Skip messages in retention window
|
|
133
|
+
if msg_idx >= retention_start:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Only process ModelRequest (which contains ToolReturnPart)
|
|
137
|
+
if not isinstance(message, ModelRequest):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# Build new parts list, replacing file content where appropriate
|
|
141
|
+
new_parts: list[Any] = []
|
|
142
|
+
message_modified = False
|
|
143
|
+
|
|
144
|
+
for part in message.parts:
|
|
145
|
+
if not isinstance(part, ToolReturnPart):
|
|
146
|
+
new_parts.append(part)
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
tool_name = part.tool_name
|
|
150
|
+
content = part.content
|
|
151
|
+
|
|
152
|
+
# Skip if content is too short to bother
|
|
153
|
+
if not isinstance(content, str) or len(content) < MIN_CONTENT_LENGTH:
|
|
154
|
+
new_parts.append(part)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
replacement = None
|
|
158
|
+
original_content = content
|
|
159
|
+
|
|
160
|
+
# Handle codebase file reads (file_read)
|
|
161
|
+
if tool_name == FileReadTool.CODEBASE:
|
|
162
|
+
parsed = _parse_codebase_file_content(content)
|
|
163
|
+
if parsed:
|
|
164
|
+
file_path, size_bytes, language, actual_content = parsed
|
|
165
|
+
# Only replace if actual content is substantial
|
|
166
|
+
if len(actual_content) >= MIN_CONTENT_LENGTH:
|
|
167
|
+
replacement = _create_codebase_placeholder(
|
|
168
|
+
file_path, size_bytes, language
|
|
169
|
+
)
|
|
170
|
+
logger.debug(
|
|
171
|
+
f"Deduplicating codebase file: {file_path} "
|
|
172
|
+
f"({size_bytes} bytes)"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Handle .shotgun/ file reads (read_file)
|
|
176
|
+
elif tool_name == FileReadTool.SHOTGUN_FOLDER:
|
|
177
|
+
# For read_file, content is raw - we need to figure out filename
|
|
178
|
+
# from the tool call args (but we only have the return here)
|
|
179
|
+
# Use a generic placeholder since we don't have the filename
|
|
180
|
+
if len(content) >= MIN_CONTENT_LENGTH:
|
|
181
|
+
# Try to extract filename from content if it looks like markdown
|
|
182
|
+
# Otherwise use generic placeholder
|
|
183
|
+
replacement = _create_shotgun_placeholder("artifact")
|
|
184
|
+
logger.debug(
|
|
185
|
+
f"Deduplicating .shotgun/ file read ({len(content)} chars)"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Apply replacement if we have one
|
|
189
|
+
if replacement:
|
|
190
|
+
# Create new ToolReturnPart with replaced content
|
|
191
|
+
new_part = ToolReturnPart(
|
|
192
|
+
tool_name=part.tool_name,
|
|
193
|
+
tool_call_id=part.tool_call_id,
|
|
194
|
+
content=replacement,
|
|
195
|
+
timestamp=part.timestamp,
|
|
196
|
+
)
|
|
197
|
+
new_parts.append(new_part)
|
|
198
|
+
message_modified = True
|
|
199
|
+
|
|
200
|
+
tokens_saved = _estimate_tokens_saved(original_content, replacement)
|
|
201
|
+
total_tokens_saved += tokens_saved
|
|
202
|
+
files_deduplicated += 1
|
|
203
|
+
else:
|
|
204
|
+
new_parts.append(part)
|
|
205
|
+
|
|
206
|
+
# Replace message with new parts if modified
|
|
207
|
+
if message_modified:
|
|
208
|
+
modified_messages[msg_idx] = ModelRequest(parts=new_parts)
|
|
209
|
+
|
|
210
|
+
if files_deduplicated > 0:
|
|
211
|
+
logger.info(
|
|
212
|
+
f"File content deduplication: {files_deduplicated} files, "
|
|
213
|
+
f"~{total_tokens_saved:,} tokens saved"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return modified_messages, total_tokens_saved
|