stravinsky 0.2.7__py3-none-any.whl → 0.2.40__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.
Files changed (34) hide show
  1. mcp_bridge/__init__.py +1 -1
  2. mcp_bridge/auth/cli.py +84 -46
  3. mcp_bridge/auth/oauth.py +88 -63
  4. mcp_bridge/hooks/__init__.py +29 -8
  5. mcp_bridge/hooks/agent_reminder.py +61 -0
  6. mcp_bridge/hooks/auto_slash_command.py +186 -0
  7. mcp_bridge/hooks/comment_checker.py +136 -0
  8. mcp_bridge/hooks/context_monitor.py +58 -0
  9. mcp_bridge/hooks/empty_message_sanitizer.py +240 -0
  10. mcp_bridge/hooks/keyword_detector.py +122 -0
  11. mcp_bridge/hooks/manager.py +27 -8
  12. mcp_bridge/hooks/preemptive_compaction.py +157 -0
  13. mcp_bridge/hooks/session_recovery.py +186 -0
  14. mcp_bridge/hooks/todo_enforcer.py +75 -0
  15. mcp_bridge/hooks/truncator.py +1 -1
  16. mcp_bridge/native_hooks/stravinsky_mode.py +109 -0
  17. mcp_bridge/native_hooks/truncator.py +1 -1
  18. mcp_bridge/prompts/delphi.py +3 -2
  19. mcp_bridge/prompts/dewey.py +105 -21
  20. mcp_bridge/prompts/stravinsky.py +451 -127
  21. mcp_bridge/server.py +304 -38
  22. mcp_bridge/server_tools.py +21 -3
  23. mcp_bridge/tools/__init__.py +2 -1
  24. mcp_bridge/tools/agent_manager.py +313 -236
  25. mcp_bridge/tools/init.py +1 -1
  26. mcp_bridge/tools/model_invoke.py +534 -52
  27. mcp_bridge/tools/skill_loader.py +51 -47
  28. mcp_bridge/tools/task_runner.py +74 -30
  29. mcp_bridge/tools/templates.py +101 -12
  30. {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/METADATA +6 -12
  31. stravinsky-0.2.40.dist-info/RECORD +57 -0
  32. stravinsky-0.2.7.dist-info/RECORD +0 -47
  33. {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/WHEEL +0 -0
  34. {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,240 @@
1
+ """
2
+ Empty Message Sanitizer Hook.
3
+
4
+ Cleans up empty/malformed messages:
5
+ - Detects empty content in messages
6
+ - Replaces with placeholder or removes
7
+ - Prevents API errors from empty content
8
+ - Registered as pre_model_invoke hook
9
+ """
10
+
11
+ import logging
12
+ import re
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Patterns that indicate effectively empty content
18
+ EMPTY_PATTERNS = [
19
+ r'^\s*$', # Whitespace only
20
+ r'^[\n\r]+$', # Newlines only
21
+ r'^[\t ]+$', # Tabs/spaces only
22
+ r'^\s*null\s*$', # Null string
23
+ r'^\s*undefined\s*$', # Undefined string
24
+ r'^\s*None\s*$', # Python None as string
25
+ ]
26
+
27
+ # Patterns for malformed JSON-like content
28
+ MALFORMED_PATTERNS = [
29
+ r'^\s*\{\s*\}\s*$', # Empty JSON object
30
+ r'^\s*\[\s*\]\s*$', # Empty JSON array
31
+ r'^\s*""\s*$', # Empty quoted string
32
+ r"^\s*''\s*$", # Empty single-quoted string
33
+ ]
34
+
35
+ # Characters that might corrupt the prompt
36
+ DANGEROUS_PATTERNS = [
37
+ r'\x00', # Null byte
38
+ r'[\x01-\x08]', # Control characters
39
+ r'[\x0b\x0c]', # Vertical tab, form feed
40
+ r'[\x0e-\x1f]', # More control characters
41
+ r'\x7f', # DEL character
42
+ ]
43
+
44
+ PLACEHOLDER_MESSAGE = "[Content sanitized - empty or malformed input detected]"
45
+
46
+ SANITIZATION_NOTICE = """
47
+ > **[MESSAGE SANITIZATION]**
48
+ > {count} empty or malformed message segment(s) were detected and sanitized.
49
+ > This prevents API errors and ensures proper message processing.
50
+ """
51
+
52
+
53
+ def is_empty_or_malformed(content: str) -> bool:
54
+ """
55
+ Check if content is empty or malformed.
56
+
57
+ Args:
58
+ content: The content string to check
59
+
60
+ Returns:
61
+ True if content is empty or malformed
62
+ """
63
+ if content is None:
64
+ return True
65
+
66
+ if not isinstance(content, str):
67
+ # Try to convert to string
68
+ try:
69
+ content = str(content)
70
+ except:
71
+ return True
72
+
73
+ # Check empty patterns
74
+ for pattern in EMPTY_PATTERNS:
75
+ if re.match(pattern, content, re.IGNORECASE):
76
+ return True
77
+
78
+ # Check malformed patterns
79
+ for pattern in MALFORMED_PATTERNS:
80
+ if re.match(pattern, content, re.IGNORECASE):
81
+ return True
82
+
83
+ return False
84
+
85
+
86
+ def contains_dangerous_characters(content: str) -> bool:
87
+ """
88
+ Check if content contains potentially dangerous control characters.
89
+
90
+ Args:
91
+ content: The content string to check
92
+
93
+ Returns:
94
+ True if dangerous characters are found
95
+ """
96
+ if not isinstance(content, str):
97
+ return False
98
+
99
+ for pattern in DANGEROUS_PATTERNS:
100
+ if re.search(pattern, content):
101
+ return True
102
+
103
+ return False
104
+
105
+
106
+ def sanitize_content(content: str) -> str:
107
+ """
108
+ Sanitize content by removing dangerous characters.
109
+
110
+ Args:
111
+ content: The content to sanitize
112
+
113
+ Returns:
114
+ Sanitized content
115
+ """
116
+ if not isinstance(content, str):
117
+ try:
118
+ content = str(content)
119
+ except:
120
+ return PLACEHOLDER_MESSAGE
121
+
122
+ # Remove dangerous characters
123
+ sanitized = content
124
+ for pattern in DANGEROUS_PATTERNS:
125
+ sanitized = re.sub(pattern, '', sanitized)
126
+
127
+ # If result is empty after sanitization, use placeholder
128
+ if is_empty_or_malformed(sanitized):
129
+ return PLACEHOLDER_MESSAGE
130
+
131
+ return sanitized
132
+
133
+
134
+ def sanitize_message_blocks(prompt: str) -> tuple[str, int]:
135
+ """
136
+ Scan prompt for message blocks and sanitize empty ones.
137
+
138
+ This handles common message formats in prompts:
139
+ - user: content
140
+ - assistant: content
141
+ - system: content
142
+ - <role>content</role>
143
+
144
+ Args:
145
+ prompt: The full prompt text
146
+
147
+ Returns:
148
+ Tuple of (sanitized prompt, count of sanitized blocks)
149
+ """
150
+ sanitized_count = 0
151
+
152
+ # Pattern for role-prefixed messages (user:, assistant:, system:)
153
+ role_pattern = re.compile(
154
+ r'((?:user|assistant|system|human|ai):\s*)([\n\r]+|$)',
155
+ re.IGNORECASE | re.MULTILINE
156
+ )
157
+
158
+ def replace_empty_role(match):
159
+ nonlocal sanitized_count
160
+ sanitized_count += 1
161
+ role = match.group(1)
162
+ return f"{role}{PLACEHOLDER_MESSAGE}\n"
163
+
164
+ prompt = role_pattern.sub(replace_empty_role, prompt)
165
+
166
+ # Pattern for XML-style message tags
167
+ xml_pattern = re.compile(
168
+ r'(<(?:user|assistant|system|human|ai)>)\s*(</\1>)',
169
+ re.IGNORECASE
170
+ )
171
+
172
+ def replace_empty_xml(match):
173
+ nonlocal sanitized_count
174
+ sanitized_count += 1
175
+ open_tag = match.group(1)
176
+ close_tag = match.group(2)
177
+ return f"{open_tag}{PLACEHOLDER_MESSAGE}{close_tag}"
178
+
179
+ prompt = xml_pattern.sub(replace_empty_xml, prompt)
180
+
181
+ return prompt, sanitized_count
182
+
183
+
184
+ async def empty_message_sanitizer_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
185
+ """
186
+ Pre-model invoke hook that sanitizes empty and malformed message content.
187
+
188
+ Scans the prompt for:
189
+ - Empty message blocks
190
+ - Malformed content patterns
191
+ - Dangerous control characters
192
+
193
+ And sanitizes them to prevent API errors.
194
+ """
195
+ prompt = params.get("prompt", "")
196
+
197
+ # Skip if already sanitized
198
+ if "[MESSAGE SANITIZATION]" in prompt:
199
+ return None
200
+
201
+ # Skip if prompt is valid and not empty
202
+ if not prompt or not isinstance(prompt, str):
203
+ logger.warning("[EmptyMessageSanitizer] Empty or invalid prompt detected")
204
+ params["prompt"] = PLACEHOLDER_MESSAGE
205
+ return params
206
+
207
+ modifications_made = False
208
+ sanitized_count = 0
209
+
210
+ # Check for dangerous characters in the entire prompt
211
+ if contains_dangerous_characters(prompt):
212
+ prompt = sanitize_content(prompt)
213
+ modifications_made = True
214
+ sanitized_count += 1
215
+ logger.info("[EmptyMessageSanitizer] Removed dangerous control characters")
216
+
217
+ # Sanitize empty message blocks
218
+ prompt, block_count = sanitize_message_blocks(prompt)
219
+ if block_count > 0:
220
+ sanitized_count += block_count
221
+ modifications_made = True
222
+ logger.info(f"[EmptyMessageSanitizer] Sanitized {block_count} empty message blocks")
223
+
224
+ # Check if the entire prompt is effectively empty
225
+ if is_empty_or_malformed(prompt):
226
+ prompt = PLACEHOLDER_MESSAGE
227
+ sanitized_count += 1
228
+ modifications_made = True
229
+ logger.warning("[EmptyMessageSanitizer] Entire prompt was empty/malformed")
230
+
231
+ if not modifications_made:
232
+ return None
233
+
234
+ # Add sanitization notice if modifications were made
235
+ notice = SANITIZATION_NOTICE.format(count=sanitized_count)
236
+ params["prompt"] = notice + prompt
237
+
238
+ logger.info(f"[EmptyMessageSanitizer] Applied {sanitized_count} sanitization(s)")
239
+
240
+ return params
@@ -0,0 +1,122 @@
1
+ """
2
+ Keyword Detector Hook.
3
+
4
+ Detects trigger keywords (ironstar, search, analyze) in user prompts
5
+ and injects corresponding mode activation tags.
6
+ """
7
+
8
+ import logging
9
+ import re
10
+ from typing import Any, Dict, Optional
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ IRONSTAR_MODE = """<ironstar-mode>
15
+ [CODE RED] Maximum precision required. Ultrathink before acting.
16
+
17
+ YOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.
18
+ TELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.
19
+
20
+ ## AGENT UTILIZATION PRINCIPLES (by capability, not by name)
21
+ - **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS for file patterns, internal implementations, project structure
22
+ - **Documentation & References**: Use dewey agents via BACKGROUND TASKS for API references, examples, external library docs
23
+ - **Planning & Strategy**: NEVER plan yourself - ALWAYS spawn a dedicated planning agent for work breakdown
24
+ - **High-IQ Reasoning**: Leverage delphi for architecture decisions, code review, strategic planning
25
+ - **Frontend/UI Tasks**: Delegate to frontend-ui-ux-engineer for design and implementation
26
+
27
+ ## EXECUTION RULES
28
+ - **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each.
29
+ - **PARALLEL**: Fire independent agent calls simultaneously via background_task - NEVER wait sequentially.
30
+ - **BACKGROUND FIRST**: Use background_task for exploration/research agents (10+ concurrent if needed).
31
+ - **VERIFY**: Re-read request after completion. Check ALL requirements met before reporting done.
32
+ - **DELEGATE**: Don't do everything yourself - orchestrate specialized agents for their strengths.
33
+
34
+ ## WORKFLOW
35
+ 1. Analyze the request and identify required capabilities
36
+ 2. Spawn exploration/dewey agents via background_task in PARALLEL (10+ if needed)
37
+ 3. Always Use Plan agent with gathered context to create detailed work breakdown
38
+ 4. Execute with continuous verification against original requirements
39
+
40
+ ## TDD (if test infrastructure exists)
41
+
42
+ 1. Write spec (requirements)
43
+ 2. Write tests (failing)
44
+ 3. RED: tests fail
45
+ 4. Implement minimal code
46
+ 5. GREEN: tests pass
47
+ 6. Refactor if needed (must stay green)
48
+ 7. Next feature, repeat
49
+
50
+ ## ZERO TOLERANCE FAILURES
51
+ - **NO Scope Reduction**: Never make "demo", "skeleton", "simplified", "basic" versions - deliver FULL implementation
52
+ - **NO MockUp Work**: When user asked you to do "port A", you must "port A", fully, 100%. No Extra feature, No reduced feature, no mock data, fully working 100% port.
53
+ - **NO Partial Completion**: Never stop at 60-80% saying "you can extend this..." - finish 100%
54
+ - **NO Assumed Shortcuts**: Never skip requirements you deem "optional" or "can be added later"
55
+ - **NO Premature Stopping**: Never declare done until ALL TODOs are completed and verified
56
+ - **NO TEST DELETION**: Never delete or skip failing tests to make the build pass. Fix the code, not the tests.
57
+
58
+ THE USER ASKED FOR X. DELIVER EXACTLY X. NOT A SUBSET. NOT A DEMO. NOT A STARTING POINT.
59
+
60
+ </ironstar-mode>
61
+
62
+ ---
63
+ """
64
+
65
+ SEARCH_MODE = """[search-mode]
66
+ MAXIMIZE SEARCH EFFORT. Launch multiple background agents IN PARALLEL:
67
+ - explore agents (codebase patterns, file structures, ast-grep)
68
+ - dewey agents (remote repos, official docs, GitHub examples)
69
+ Plus direct tools: Grep, ripgrep (rg), ast-grep (sg)
70
+ NEVER stop at first result - be exhaustive.
71
+ """
72
+
73
+ ANALYZE_MODE = """[analyze-mode]
74
+ ANALYSIS MODE. Gather context before diving deep:
75
+
76
+ CONTEXT GATHERING (parallel):
77
+ - 1-2 explore agents (codebase patterns, implementations)
78
+ - 1-2 dewey agents (if external library involved)
79
+ - Direct tools: Grep, AST-grep, LSP for targeted searches
80
+
81
+ IF COMPLEX (architecture, multi-system, debugging after 2+ failures):
82
+ - Consult delphi for strategic guidance
83
+
84
+ SYNTHESIZE findings before proceeding.
85
+ """
86
+
87
+ KEYWORD_PATTERNS = {
88
+ r"\bironstar\b": IRONSTAR_MODE,
89
+ r"\birs\b": IRONSTAR_MODE,
90
+ r"\bultrawork\b": IRONSTAR_MODE,
91
+ r"\bulw\b": IRONSTAR_MODE,
92
+ r"\bsearch\b": SEARCH_MODE,
93
+ r"\banalyze\b": ANALYZE_MODE,
94
+ r"\banalysis\b": ANALYZE_MODE,
95
+ }
96
+
97
+
98
+ async def keyword_detector_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
99
+ """
100
+ Pre-model invoke hook that detects keywords and injects mode tags.
101
+ """
102
+ prompt = params.get("prompt", "")
103
+ prompt_lower = prompt.lower()
104
+
105
+ injections = []
106
+ matched_modes = set()
107
+
108
+ for pattern, mode_tag in KEYWORD_PATTERNS.items():
109
+ if re.search(pattern, prompt_lower):
110
+ mode_id = id(mode_tag)
111
+ if mode_id not in matched_modes:
112
+ matched_modes.add(mode_id)
113
+ injections.append(mode_tag)
114
+ logger.info(f"[KeywordDetector] Matched pattern '{pattern}', injecting mode tag")
115
+
116
+ if injections:
117
+ injection_block = "\n".join(injections)
118
+ modified_prompt = prompt + "\n\n" + injection_block
119
+ params["prompt"] = modified_prompt
120
+ return params
121
+
122
+ return None
@@ -8,16 +8,24 @@ from typing import Any, Callable, Dict, List, Optional, Awaitable
8
8
 
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
+
11
12
  class HookManager:
12
13
  """
13
14
  Manages the registration and execution of hooks.
14
15
  """
16
+
15
17
  _instance = None
16
18
 
17
19
  def __init__(self):
18
- self.pre_tool_call_hooks: List[Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]] = []
19
- self.post_tool_call_hooks: List[Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]] = []
20
- self.pre_model_invoke_hooks: List[Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]] = []
20
+ self.pre_tool_call_hooks: List[
21
+ Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
22
+ ] = []
23
+ self.post_tool_call_hooks: List[
24
+ Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]
25
+ ] = []
26
+ self.pre_model_invoke_hooks: List[
27
+ Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
28
+ ] = []
21
29
 
22
30
  @classmethod
23
31
  def get_instance(cls):
@@ -25,19 +33,27 @@ class HookManager:
25
33
  cls._instance = cls()
26
34
  return cls._instance
27
35
 
28
- def register_pre_tool_call(self, hook: Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]):
36
+ def register_pre_tool_call(
37
+ self, hook: Callable[[str, Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
38
+ ):
29
39
  """Run before a tool is called. Can modify arguments or return early result."""
30
40
  self.pre_tool_call_hooks.append(hook)
31
41
 
32
- def register_post_tool_call(self, hook: Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]):
42
+ def register_post_tool_call(
43
+ self, hook: Callable[[str, Dict[str, Any], str], Awaitable[Optional[str]]]
44
+ ):
33
45
  """Run after a tool call. Can modify or recover from tool output/error."""
34
46
  self.post_tool_call_hooks.append(hook)
35
47
 
36
- def register_pre_model_invoke(self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]):
48
+ def register_pre_model_invoke(
49
+ self, hook: Callable[[Dict[str, Any]], Awaitable[Optional[Dict[str, Any]]]]
50
+ ):
37
51
  """Run before model invocation. Can modify prompt or parameters."""
38
52
  self.pre_model_invoke_hooks.append(hook)
39
53
 
40
- async def execute_pre_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> Optional[Dict[str, Any]]:
54
+ async def execute_pre_tool_call(
55
+ self, tool_name: str, arguments: Dict[str, Any]
56
+ ) -> Dict[str, Any]:
41
57
  """Executes all pre-tool call hooks."""
42
58
  current_args = arguments
43
59
  for hook in self.pre_tool_call_hooks:
@@ -49,7 +65,9 @@ class HookManager:
49
65
  logger.error(f"[HookManager] Error in pre_tool_call hook {hook.__name__}: {e}")
50
66
  return current_args
51
67
 
52
- async def execute_post_tool_call(self, tool_name: str, arguments: Dict[str, Any], output: str) -> str:
68
+ async def execute_post_tool_call(
69
+ self, tool_name: str, arguments: Dict[str, Any], output: str
70
+ ) -> str:
53
71
  """Executes all post-tool call hooks."""
54
72
  current_output = output
55
73
  for hook in self.post_tool_call_hooks:
@@ -73,5 +91,6 @@ class HookManager:
73
91
  logger.error(f"[HookManager] Error in pre_model_invoke hook {hook.__name__}: {e}")
74
92
  return current_params
75
93
 
94
+
76
95
  def get_hook_manager() -> HookManager:
77
96
  return HookManager.get_instance()
@@ -0,0 +1,157 @@
1
+ """
2
+ Preemptive Context Compaction Hook.
3
+
4
+ Proactively compresses context BEFORE hitting limits by:
5
+ - Tracking estimated token usage
6
+ - Triggering compaction at 70% capacity (not waiting for errors)
7
+ - Using DCP -> Truncate -> Summarize pipeline
8
+ - Registered as pre_model_invoke hook
9
+ """
10
+
11
+ import logging
12
+ import re
13
+ from typing import Any, Dict, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Token estimation constants
18
+ CHARS_PER_TOKEN = 4 # Rough estimate for English text
19
+ MAX_CONTEXT_TOKENS = 200000 # Claude's context window
20
+ PREEMPTIVE_THRESHOLD = 0.70 # Trigger at 70% capacity
21
+ WARNING_THRESHOLD = 0.85 # Critical warning at 85%
22
+
23
+ # Calculate character thresholds
24
+ PREEMPTIVE_CHAR_THRESHOLD = int(MAX_CONTEXT_TOKENS * CHARS_PER_TOKEN * PREEMPTIVE_THRESHOLD)
25
+ WARNING_CHAR_THRESHOLD = int(MAX_CONTEXT_TOKENS * CHARS_PER_TOKEN * WARNING_THRESHOLD)
26
+
27
+
28
+ def estimate_tokens(text: str) -> int:
29
+ """Estimate token count from character count."""
30
+ return len(text) // CHARS_PER_TOKEN
31
+
32
+
33
+ def calculate_usage_percentage(text: str) -> float:
34
+ """Calculate context window usage as a percentage."""
35
+ estimated_tokens = estimate_tokens(text)
36
+ return (estimated_tokens / MAX_CONTEXT_TOKENS) * 100
37
+
38
+
39
+ def apply_dcp_truncation(text: str, target_reduction: float = 0.3) -> str:
40
+ """
41
+ Apply DCP (Deferred Context Pruning) truncation strategy.
42
+
43
+ Prioritizes keeping:
44
+ 1. System instructions (first ~10%)
45
+ 2. Recent context (last ~30%)
46
+ 3. Key structural elements in middle
47
+
48
+ Args:
49
+ text: The full context text
50
+ target_reduction: How much to reduce (0.3 = reduce by 30%)
51
+
52
+ Returns:
53
+ Truncated text with summary markers
54
+ """
55
+ lines = text.split('\n')
56
+ total_lines = len(lines)
57
+
58
+ if total_lines < 100:
59
+ # Small context, don't truncate
60
+ return text
61
+
62
+ # Calculate segment boundaries
63
+ system_end = int(total_lines * 0.10) # Keep first 10%
64
+ recent_start = int(total_lines * 0.70) # Keep last 30%
65
+
66
+ # Extract segments
67
+ system_segment = lines[:system_end]
68
+ recent_segment = lines[recent_start:]
69
+ middle_segment = lines[system_end:recent_start]
70
+
71
+ # For middle segment, keep key structural elements
72
+ kept_middle = []
73
+ for line in middle_segment:
74
+ # Keep lines with structural importance
75
+ if any([
76
+ line.strip().startswith('##'), # Headers
77
+ line.strip().startswith('def '), # Function definitions
78
+ line.strip().startswith('class '), # Class definitions
79
+ line.strip().startswith('- '), # Bullet points
80
+ 'error' in line.lower(), # Errors
81
+ 'warning' in line.lower(), # Warnings
82
+ 'todo' in line.lower(), # TODOs
83
+ ]):
84
+ kept_middle.append(line)
85
+
86
+ # Limit middle to avoid bloat
87
+ max_middle = int(total_lines * (1 - target_reduction) * 0.3)
88
+ kept_middle = kept_middle[:max_middle]
89
+
90
+ # Compose truncated context
91
+ truncation_marker = f"\n[...{len(middle_segment) - len(kept_middle)} lines truncated for context optimization...]\n"
92
+
93
+ result_lines = system_segment + [truncation_marker] + kept_middle + [truncation_marker] + recent_segment
94
+ return '\n'.join(result_lines)
95
+
96
+
97
+ PREEMPTIVE_COMPACTION_NOTICE = """
98
+ > **[PREEMPTIVE CONTEXT OPTIMIZATION]**
99
+ > Context usage at {usage:.1f}% - proactively optimizing to maintain performance.
100
+ > The context has been structured for efficiency:
101
+ > - System instructions preserved
102
+ > - Recent interactions kept in full
103
+ > - Historical middle sections summarized
104
+ > - Key structural elements retained
105
+ """
106
+
107
+ CRITICAL_WARNING = """
108
+ > **[CRITICAL - CONTEXT WINDOW AT {usage:.1f}%]**
109
+ > Immediate action recommended:
110
+ > 1. Complete current task and document results in TASK_STATE.md
111
+ > 2. Start a new session for fresh context
112
+ > 3. Reference TASK_STATE.md in new session for continuity
113
+ """
114
+
115
+
116
+ async def preemptive_compaction_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
117
+ """
118
+ Pre-model invoke hook that proactively compresses context before hitting limits.
119
+
120
+ Uses a multi-tier strategy:
121
+ - Below 70%: No action
122
+ - 70-85%: Apply DCP truncation with notice
123
+ - Above 85%: Apply aggressive truncation with critical warning
124
+ """
125
+ prompt = params.get("prompt", "")
126
+ prompt_length = len(prompt)
127
+
128
+ # Skip if already optimized recently
129
+ if "[PREEMPTIVE CONTEXT OPTIMIZATION]" in prompt or "[CRITICAL - CONTEXT WINDOW" in prompt:
130
+ return None
131
+
132
+ usage = calculate_usage_percentage(prompt)
133
+
134
+ if prompt_length >= WARNING_CHAR_THRESHOLD:
135
+ # Critical level - aggressive truncation
136
+ logger.warning(f"[PreemptiveCompaction] Critical context usage: {usage:.1f}%")
137
+
138
+ truncated = apply_dcp_truncation(prompt, target_reduction=0.4)
139
+ notice = CRITICAL_WARNING.format(usage=usage)
140
+ params["prompt"] = notice + truncated
141
+
142
+ logger.info(f"[PreemptiveCompaction] Applied aggressive truncation: {len(prompt)} -> {len(truncated)} chars")
143
+ return params
144
+
145
+ elif prompt_length >= PREEMPTIVE_CHAR_THRESHOLD:
146
+ # Preemptive level - moderate truncation
147
+ logger.info(f"[PreemptiveCompaction] Preemptive compaction at {usage:.1f}%")
148
+
149
+ truncated = apply_dcp_truncation(prompt, target_reduction=0.3)
150
+ notice = PREEMPTIVE_COMPACTION_NOTICE.format(usage=usage)
151
+ params["prompt"] = notice + truncated
152
+
153
+ logger.info(f"[PreemptiveCompaction] Applied moderate truncation: {len(prompt)} -> {len(truncated)} chars")
154
+ return params
155
+
156
+ # Below threshold, no action needed
157
+ return None