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.
- mcp_bridge/__init__.py +1 -1
- mcp_bridge/auth/cli.py +84 -46
- mcp_bridge/auth/oauth.py +88 -63
- mcp_bridge/hooks/__init__.py +29 -8
- mcp_bridge/hooks/agent_reminder.py +61 -0
- mcp_bridge/hooks/auto_slash_command.py +186 -0
- mcp_bridge/hooks/comment_checker.py +136 -0
- mcp_bridge/hooks/context_monitor.py +58 -0
- mcp_bridge/hooks/empty_message_sanitizer.py +240 -0
- mcp_bridge/hooks/keyword_detector.py +122 -0
- mcp_bridge/hooks/manager.py +27 -8
- mcp_bridge/hooks/preemptive_compaction.py +157 -0
- mcp_bridge/hooks/session_recovery.py +186 -0
- mcp_bridge/hooks/todo_enforcer.py +75 -0
- mcp_bridge/hooks/truncator.py +1 -1
- mcp_bridge/native_hooks/stravinsky_mode.py +109 -0
- mcp_bridge/native_hooks/truncator.py +1 -1
- mcp_bridge/prompts/delphi.py +3 -2
- mcp_bridge/prompts/dewey.py +105 -21
- mcp_bridge/prompts/stravinsky.py +451 -127
- mcp_bridge/server.py +304 -38
- mcp_bridge/server_tools.py +21 -3
- mcp_bridge/tools/__init__.py +2 -1
- mcp_bridge/tools/agent_manager.py +313 -236
- mcp_bridge/tools/init.py +1 -1
- mcp_bridge/tools/model_invoke.py +534 -52
- mcp_bridge/tools/skill_loader.py +51 -47
- mcp_bridge/tools/task_runner.py +74 -30
- mcp_bridge/tools/templates.py +101 -12
- {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/METADATA +6 -12
- stravinsky-0.2.40.dist-info/RECORD +57 -0
- stravinsky-0.2.7.dist-info/RECORD +0 -47
- {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/WHEEL +0 -0
- {stravinsky-0.2.7.dist-info → stravinsky-0.2.40.dist-info}/entry_points.txt +0 -0
mcp_bridge/hooks/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Hooks initialization.
|
|
3
|
-
Registers all Tier 1-
|
|
3
|
+
Registers all Tier 1-4 hooks into the HookManager.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from .manager import get_hook_manager
|
|
@@ -9,20 +9,41 @@ from .edit_recovery import edit_error_recovery_hook
|
|
|
9
9
|
from .directory_context import directory_context_hook
|
|
10
10
|
from .compaction import context_compaction_hook
|
|
11
11
|
from .budget_optimizer import budget_optimizer_hook
|
|
12
|
+
from .todo_enforcer import todo_continuation_hook
|
|
13
|
+
from .keyword_detector import keyword_detector_hook
|
|
14
|
+
from .comment_checker import comment_checker_hook
|
|
15
|
+
from .context_monitor import context_monitor_hook
|
|
16
|
+
from .agent_reminder import agent_reminder_hook
|
|
17
|
+
from .preemptive_compaction import preemptive_compaction_hook
|
|
18
|
+
from .auto_slash_command import auto_slash_command_hook
|
|
19
|
+
from .session_recovery import session_recovery_hook
|
|
20
|
+
from .empty_message_sanitizer import empty_message_sanitizer_hook
|
|
21
|
+
|
|
12
22
|
|
|
13
23
|
def initialize_hooks():
|
|
14
24
|
"""Register all available hooks."""
|
|
15
25
|
manager = get_hook_manager()
|
|
16
|
-
|
|
17
|
-
# Tier 1
|
|
26
|
+
|
|
27
|
+
# Tier 1: Post-tool-call (immediate response modification)
|
|
18
28
|
manager.register_post_tool_call(output_truncator_hook)
|
|
19
29
|
manager.register_post_tool_call(edit_error_recovery_hook)
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
manager.register_post_tool_call(comment_checker_hook)
|
|
31
|
+
manager.register_post_tool_call(agent_reminder_hook)
|
|
32
|
+
manager.register_post_tool_call(session_recovery_hook)
|
|
33
|
+
|
|
34
|
+
# Tier 2: Pre-model-invoke (context management)
|
|
22
35
|
manager.register_pre_model_invoke(directory_context_hook)
|
|
23
36
|
manager.register_pre_model_invoke(context_compaction_hook)
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
manager.register_pre_model_invoke(context_monitor_hook)
|
|
38
|
+
manager.register_pre_model_invoke(preemptive_compaction_hook)
|
|
39
|
+
manager.register_pre_model_invoke(empty_message_sanitizer_hook)
|
|
40
|
+
|
|
41
|
+
# Tier 3: Pre-model-invoke (performance optimization)
|
|
26
42
|
manager.register_pre_model_invoke(budget_optimizer_hook)
|
|
27
43
|
|
|
28
|
-
#
|
|
44
|
+
# Tier 4: Pre-model-invoke (behavior enforcement)
|
|
45
|
+
manager.register_pre_model_invoke(keyword_detector_hook)
|
|
46
|
+
manager.register_pre_model_invoke(todo_continuation_hook)
|
|
47
|
+
manager.register_pre_model_invoke(auto_slash_command_hook)
|
|
48
|
+
|
|
49
|
+
return manager
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent Usage Reminder Hook.
|
|
3
|
+
|
|
4
|
+
When direct search tools (grep, glob, find) are used,
|
|
5
|
+
suggests using background agents for more comprehensive results.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
AGENT_SUGGESTION = """
|
|
14
|
+
[AGENT SUGGESTION]
|
|
15
|
+
You used `{tool_name}` directly. For more comprehensive results, consider:
|
|
16
|
+
|
|
17
|
+
**Background Agents (parallel, more thorough):**
|
|
18
|
+
```
|
|
19
|
+
background_task(agent="explore", prompt="Search for {search_context}...")
|
|
20
|
+
background_task(agent="dewey", prompt="Find documentation for {search_context}...")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Background agents can search multiple patterns simultaneously and provide richer context.
|
|
24
|
+
Use direct tools for quick, targeted lookups. Use agents for exploratory research.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
SEARCH_TOOLS = {"grep", "glob", "rg", "find", "Grep", "Glob", "grep_search", "glob_files"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def agent_reminder_hook(
|
|
31
|
+
tool_name: str, arguments: Dict[str, Any], output: str
|
|
32
|
+
) -> Optional[str]:
|
|
33
|
+
"""
|
|
34
|
+
Post-tool call hook that suggests background agents after direct search tool usage.
|
|
35
|
+
"""
|
|
36
|
+
if tool_name not in SEARCH_TOOLS:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
search_context = _extract_search_context(arguments)
|
|
40
|
+
|
|
41
|
+
if not search_context:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
if len(output) < 100:
|
|
45
|
+
logger.info(
|
|
46
|
+
f"[AgentReminder] Direct search '{tool_name}' returned limited results, suggesting agents"
|
|
47
|
+
)
|
|
48
|
+
suggestion = AGENT_SUGGESTION.format(tool_name=tool_name, search_context=search_context)
|
|
49
|
+
return output + "\n" + suggestion
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _extract_search_context(arguments: Dict[str, Any]) -> str:
|
|
55
|
+
"""Extract search context from tool arguments."""
|
|
56
|
+
for key in ("pattern", "query", "search", "name", "path"):
|
|
57
|
+
if key in arguments:
|
|
58
|
+
value = arguments[key]
|
|
59
|
+
if isinstance(value, str) and len(value) < 100:
|
|
60
|
+
return value
|
|
61
|
+
return "related patterns"
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto Slash Command Hook.
|
|
3
|
+
|
|
4
|
+
Detects and auto-processes slash commands in user input:
|
|
5
|
+
- Parses `/command` patterns in user input
|
|
6
|
+
- Looks up matching skill via skill_loader
|
|
7
|
+
- Injects skill content into prompt
|
|
8
|
+
- Registered as pre_model_invoke hook
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Pattern to match slash commands at the beginning of a line or after whitespace
|
|
19
|
+
SLASH_COMMAND_PATTERN = re.compile(r'(?:^|(?<=\s))\/([a-zA-Z][a-zA-Z0-9_-]*)\b', re.MULTILINE)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def extract_slash_commands(text: str) -> List[str]:
|
|
23
|
+
"""
|
|
24
|
+
Extract all slash command names from text.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
text: Input text to scan for slash commands
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
List of command names (without the slash)
|
|
31
|
+
"""
|
|
32
|
+
matches = SLASH_COMMAND_PATTERN.findall(text)
|
|
33
|
+
# Deduplicate while preserving order
|
|
34
|
+
seen = set()
|
|
35
|
+
unique = []
|
|
36
|
+
for match in matches:
|
|
37
|
+
if match.lower() not in seen:
|
|
38
|
+
seen.add(match.lower())
|
|
39
|
+
unique.append(match)
|
|
40
|
+
return unique
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_skill_content(command_name: str, project_path: Optional[str] = None) -> Optional[Tuple[str, str]]:
|
|
44
|
+
"""
|
|
45
|
+
Load skill content by command name.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
command_name: The command name to look up
|
|
49
|
+
project_path: Optional project path for local skills
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tuple of (skill_name, skill_content) or None if not found
|
|
53
|
+
"""
|
|
54
|
+
from ..tools.skill_loader import discover_skills, parse_frontmatter
|
|
55
|
+
|
|
56
|
+
skills = discover_skills(project_path)
|
|
57
|
+
|
|
58
|
+
# Find matching skill (case-insensitive)
|
|
59
|
+
skill = next(
|
|
60
|
+
(s for s in skills if s["name"].lower() == command_name.lower()),
|
|
61
|
+
None
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not skill:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
content = Path(skill["path"]).read_text()
|
|
69
|
+
metadata, body = parse_frontmatter(content)
|
|
70
|
+
|
|
71
|
+
# Build the skill injection block
|
|
72
|
+
skill_block = f"""
|
|
73
|
+
---
|
|
74
|
+
## Skill: /{skill['name']}
|
|
75
|
+
**Source**: {skill['path']}
|
|
76
|
+
"""
|
|
77
|
+
if metadata.get("description"):
|
|
78
|
+
skill_block += f"**Description**: {metadata['description']}\n"
|
|
79
|
+
|
|
80
|
+
if metadata.get("allowed-tools"):
|
|
81
|
+
skill_block += f"**Allowed Tools**: {metadata['allowed-tools']}\n"
|
|
82
|
+
|
|
83
|
+
skill_block += f"""
|
|
84
|
+
### Instructions:
|
|
85
|
+
{body}
|
|
86
|
+
---
|
|
87
|
+
"""
|
|
88
|
+
return skill["name"], skill_block
|
|
89
|
+
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.error(f"[AutoSlashCommand] Error loading skill '{command_name}': {e}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_project_path_from_prompt(prompt: str) -> Optional[str]:
|
|
96
|
+
"""
|
|
97
|
+
Try to extract project path from prompt context.
|
|
98
|
+
Looks for common patterns that indicate the working directory.
|
|
99
|
+
"""
|
|
100
|
+
# Look for CWD markers
|
|
101
|
+
cwd_patterns = [
|
|
102
|
+
r'CWD:\s*([^\n]+)',
|
|
103
|
+
r'Working directory:\s*([^\n]+)',
|
|
104
|
+
r'project_path["\']?\s*[:=]\s*["\']?([^"\'}\n]+)',
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for pattern in cwd_patterns:
|
|
108
|
+
match = re.search(pattern, prompt)
|
|
109
|
+
if match:
|
|
110
|
+
path = match.group(1).strip()
|
|
111
|
+
if Path(path).exists():
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
SKILL_INJECTION_HEADER = """
|
|
118
|
+
> **[AUTO-SKILL INJECTION]**
|
|
119
|
+
> The following skill(s) have been automatically loaded based on slash commands detected in your input:
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
SKILL_NOT_FOUND_WARNING = """
|
|
123
|
+
> **[SKILL NOT FOUND]**
|
|
124
|
+
> The slash command `/{command}` was detected but no matching skill was found.
|
|
125
|
+
> Available skills can be listed with the `skill_list` tool.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def auto_slash_command_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
130
|
+
"""
|
|
131
|
+
Pre-model invoke hook that detects slash commands and injects skill content.
|
|
132
|
+
|
|
133
|
+
Scans the prompt for /command patterns and loads corresponding skill files
|
|
134
|
+
from .claude/commands/ directories.
|
|
135
|
+
"""
|
|
136
|
+
prompt = params.get("prompt", "")
|
|
137
|
+
|
|
138
|
+
# Skip if already processed
|
|
139
|
+
if "[AUTO-SKILL INJECTION]" in prompt:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
# Extract slash commands
|
|
143
|
+
commands = extract_slash_commands(prompt)
|
|
144
|
+
|
|
145
|
+
if not commands:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
logger.info(f"[AutoSlashCommand] Detected slash commands: {commands}")
|
|
149
|
+
|
|
150
|
+
# Try to get project path from prompt context
|
|
151
|
+
project_path = get_project_path_from_prompt(prompt)
|
|
152
|
+
|
|
153
|
+
# Load skills for each command
|
|
154
|
+
injections = []
|
|
155
|
+
warnings = []
|
|
156
|
+
loaded_skills = []
|
|
157
|
+
|
|
158
|
+
for command in commands:
|
|
159
|
+
result = load_skill_content(command, project_path)
|
|
160
|
+
if result:
|
|
161
|
+
skill_name, skill_content = result
|
|
162
|
+
injections.append(skill_content)
|
|
163
|
+
loaded_skills.append(skill_name)
|
|
164
|
+
logger.info(f"[AutoSlashCommand] Loaded skill: {skill_name}")
|
|
165
|
+
else:
|
|
166
|
+
warnings.append(SKILL_NOT_FOUND_WARNING.format(command=command))
|
|
167
|
+
logger.warning(f"[AutoSlashCommand] Skill not found: {command}")
|
|
168
|
+
|
|
169
|
+
if not injections and not warnings:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Build the injection block
|
|
173
|
+
injection_block = ""
|
|
174
|
+
|
|
175
|
+
if injections:
|
|
176
|
+
injection_block = SKILL_INJECTION_HEADER
|
|
177
|
+
injection_block += f"> Skills loaded: {', '.join(loaded_skills)}\n"
|
|
178
|
+
injection_block += "\n".join(injections)
|
|
179
|
+
|
|
180
|
+
if warnings:
|
|
181
|
+
injection_block += "\n".join(warnings)
|
|
182
|
+
|
|
183
|
+
# Prepend the injection to the prompt
|
|
184
|
+
params["prompt"] = injection_block + "\n\n" + prompt
|
|
185
|
+
|
|
186
|
+
return params
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comment Checker Hook.
|
|
3
|
+
|
|
4
|
+
Warns when generated code contains excessive comments.
|
|
5
|
+
Code should be self-documenting; excessive comments indicate AI slop.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
COMMENT_WARNING = """
|
|
15
|
+
[WARNING - EXCESSIVE COMMENTS DETECTED]
|
|
16
|
+
|
|
17
|
+
Your recent code edit contains a high ratio of comments ({ratio:.0%}).
|
|
18
|
+
Code generated by stravinsky should be indistinguishable from human-written code.
|
|
19
|
+
|
|
20
|
+
**Guidelines:**
|
|
21
|
+
- Comments should explain WHY, not WHAT
|
|
22
|
+
- Self-documenting code > commented code
|
|
23
|
+
- Remove obvious comments like "// Initialize the variable"
|
|
24
|
+
- Keep only: complex algorithms, security notes, performance optimizations, regex explanations
|
|
25
|
+
|
|
26
|
+
**Action Required:**
|
|
27
|
+
Review and remove unnecessary comments from your recent edit.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
COMMENT_PATTERNS = {
|
|
31
|
+
"python": [
|
|
32
|
+
r"^\s*#(?!\!)",
|
|
33
|
+
r'^\s*"""[\s\S]*?"""',
|
|
34
|
+
r"^\s*'''[\s\S]*?'''",
|
|
35
|
+
],
|
|
36
|
+
"javascript": [
|
|
37
|
+
r"^\s*//",
|
|
38
|
+
r"/\*[\s\S]*?\*/",
|
|
39
|
+
],
|
|
40
|
+
"typescript": [
|
|
41
|
+
r"^\s*//",
|
|
42
|
+
r"/\*[\s\S]*?\*/",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
CODE_EXTENSIONS = {
|
|
47
|
+
".py": "python",
|
|
48
|
+
".js": "javascript",
|
|
49
|
+
".ts": "typescript",
|
|
50
|
+
".tsx": "typescript",
|
|
51
|
+
".jsx": "javascript",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def comment_checker_hook(
|
|
56
|
+
tool_name: str, arguments: Dict[str, Any], output: str
|
|
57
|
+
) -> Optional[str]:
|
|
58
|
+
"""
|
|
59
|
+
Post-tool call hook that checks for excessive comments in code edits.
|
|
60
|
+
"""
|
|
61
|
+
if tool_name not in ("Write", "Edit", "write", "edit"):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
file_path = arguments.get("filePath", arguments.get("file_path", ""))
|
|
65
|
+
if not file_path:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
ext = None
|
|
69
|
+
for extension in CODE_EXTENSIONS:
|
|
70
|
+
if file_path.endswith(extension):
|
|
71
|
+
ext = extension
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if not ext:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
lang = CODE_EXTENSIONS[ext]
|
|
78
|
+
content = arguments.get("content", arguments.get("newString", ""))
|
|
79
|
+
|
|
80
|
+
if not content or len(content) < 50:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
comment_ratio = _calculate_comment_ratio(content, lang)
|
|
84
|
+
|
|
85
|
+
if comment_ratio > 0.30:
|
|
86
|
+
logger.warning(
|
|
87
|
+
f"[CommentChecker] High comment ratio ({comment_ratio:.0%}) detected in {file_path}"
|
|
88
|
+
)
|
|
89
|
+
warning = COMMENT_WARNING.format(ratio=comment_ratio)
|
|
90
|
+
return output + "\n" + warning
|
|
91
|
+
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _calculate_comment_ratio(content: str, lang: str) -> float:
|
|
96
|
+
"""Calculate the ratio of comment lines to total lines."""
|
|
97
|
+
lines = content.split("\n")
|
|
98
|
+
total_lines = len([l for l in lines if l.strip()])
|
|
99
|
+
|
|
100
|
+
if total_lines == 0:
|
|
101
|
+
return 0.0
|
|
102
|
+
|
|
103
|
+
comment_lines = 0
|
|
104
|
+
patterns = COMMENT_PATTERNS.get(lang, [])
|
|
105
|
+
|
|
106
|
+
in_multiline = False
|
|
107
|
+
for line in lines:
|
|
108
|
+
stripped = line.strip()
|
|
109
|
+
if not stripped:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
if lang == "python":
|
|
113
|
+
if stripped.startswith("#") and not stripped.startswith("#!"):
|
|
114
|
+
comment_lines += 1
|
|
115
|
+
elif stripped.startswith('"""') or stripped.startswith("'''"):
|
|
116
|
+
if stripped.count('"""') >= 2 or stripped.count("'''") >= 2:
|
|
117
|
+
comment_lines += 1
|
|
118
|
+
else:
|
|
119
|
+
in_multiline = True
|
|
120
|
+
comment_lines += 1
|
|
121
|
+
elif in_multiline:
|
|
122
|
+
comment_lines += 1
|
|
123
|
+
if '"""' in stripped or "'''" in stripped:
|
|
124
|
+
in_multiline = False
|
|
125
|
+
else:
|
|
126
|
+
if stripped.startswith("//"):
|
|
127
|
+
comment_lines += 1
|
|
128
|
+
elif stripped.startswith("/*"):
|
|
129
|
+
in_multiline = True
|
|
130
|
+
comment_lines += 1
|
|
131
|
+
elif in_multiline:
|
|
132
|
+
comment_lines += 1
|
|
133
|
+
if "*/" in stripped:
|
|
134
|
+
in_multiline = False
|
|
135
|
+
|
|
136
|
+
return comment_lines / total_lines
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context Window Monitor Hook.
|
|
3
|
+
|
|
4
|
+
Monitors context window usage and provides headroom reminders.
|
|
5
|
+
At 70% usage, reminds the agent there's still capacity.
|
|
6
|
+
At 85%, suggests compaction before hitting hard limits.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
HEADROOM_REMINDER = """
|
|
15
|
+
[CONTEXT AWARENESS]
|
|
16
|
+
Current context usage: ~{usage:.0%}
|
|
17
|
+
You have approximately {remaining:.0%} headroom remaining.
|
|
18
|
+
Continue working - no compaction needed yet.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
COMPACTION_WARNING = """
|
|
22
|
+
[CONTEXT WARNING - PREEMPTIVE COMPACTION RECOMMENDED]
|
|
23
|
+
Current context usage: ~{usage:.0%}
|
|
24
|
+
Approaching context limit. Consider:
|
|
25
|
+
1. Completing current task quickly
|
|
26
|
+
2. Using `background_cancel(all=true)` to clean up agents
|
|
27
|
+
3. Summarizing findings before context overflow
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
CONTEXT_THRESHOLD_REMINDER = 0.70
|
|
31
|
+
CONTEXT_THRESHOLD_WARNING = 0.85
|
|
32
|
+
ESTIMATED_MAX_TOKENS = 200000
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def context_monitor_hook(params: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
36
|
+
"""
|
|
37
|
+
Pre-model invoke hook that monitors context window usage.
|
|
38
|
+
"""
|
|
39
|
+
prompt = params.get("prompt", "")
|
|
40
|
+
estimated_tokens = len(prompt) / 4
|
|
41
|
+
|
|
42
|
+
usage_ratio = estimated_tokens / ESTIMATED_MAX_TOKENS
|
|
43
|
+
|
|
44
|
+
if usage_ratio >= CONTEXT_THRESHOLD_WARNING:
|
|
45
|
+
remaining = 1.0 - usage_ratio
|
|
46
|
+
logger.warning(f"[ContextMonitor] High context usage: {usage_ratio:.0%}")
|
|
47
|
+
warning = COMPACTION_WARNING.format(usage=usage_ratio, remaining=remaining)
|
|
48
|
+
params["prompt"] = prompt + "\n\n" + warning
|
|
49
|
+
return params
|
|
50
|
+
|
|
51
|
+
elif usage_ratio >= CONTEXT_THRESHOLD_REMINDER:
|
|
52
|
+
remaining = 1.0 - usage_ratio
|
|
53
|
+
logger.info(f"[ContextMonitor] Context usage: {usage_ratio:.0%}, reminding of headroom")
|
|
54
|
+
reminder = HEADROOM_REMINDER.format(usage=usage_ratio, remaining=remaining)
|
|
55
|
+
params["prompt"] = prompt + "\n\n" + reminder
|
|
56
|
+
return params
|
|
57
|
+
|
|
58
|
+
return None
|