system_reminders.py
1 """ 2 System Reminders for Ag3ntum. 3 4 Provides contextual reminders that are injected into conversations 5 based on runtime state (file changes, todo updates, etc.). 6 """ 7 import logging 8 from dataclasses import dataclass 9 from enum import Enum 10 from typing import Any, Optional 11 12 from .prompt_manager import get_prompt_manager 13 from .prompt_context import build_prompt_context 14 15 logger = logging.getLogger(__name__) 16 17 18 class ReminderType(Enum): 19 """Types of system reminders (42 total - Claude Code v2.1.39 compatible).""" 20 21 # File Operations (6) 22 FILE_MODIFIED_BY_USER_OR_LINTER = "file-modified-by-user-or-linter" 23 FILE_EXISTS_BUT_EMPTY = "file-exists-but-empty" 24 FILE_TRUNCATED = "file-truncated" 25 FILE_SHORTER_THAN_OFFSET = "file-shorter-than-offset" 26 FILE_OPENED_IN_IDE = "file-opened-in-ide" 27 LINES_SELECTED_IN_IDE = "lines-selected-in-ide" 28 29 # Task/Todo Management (5) 30 TODO_LIST_CHANGED = "todo-list-changed" 31 TODO_LIST_EMPTY = "todo-list-empty" 32 TODOWRITE_REMINDER = "todowrite-reminder" 33 TASK_STATUS = "task-status" 34 TASK_TOOLS_REMINDER = "task-tools-reminder" 35 36 # Plan Mode (7) 37 PLAN_MODE_IS_ACTIVE_5_PHASE = "plan-mode-is-active-5-phase" 38 PLAN_MODE_IS_ACTIVE_ITERATIVE = "plan-mode-is-active-iterative" 39 PLAN_MODE_IS_ACTIVE_SUBAGENT = "plan-mode-is-active-subagent" 40 PLAN_MODE_RE_ENTRY = "plan-mode-re-entry" 41 EXITED_PLAN_MODE = "exited-plan-mode" 42 PLAN_FILE_REFERENCE = "plan-file-reference" 43 VERIFY_PLAN_REMINDER = "verify-plan-reminder" 44 45 # Hooks (5) 46 HOOK_SUCCESS = "hook-success" 47 HOOK_BLOCKING_ERROR = "hook-blocking-error" 48 HOOK_ADDITIONAL_CONTEXT = "hook-additional-context" 49 HOOK_STOPPED_CONTINUATION = "hook-stopped-continuation" 50 HOOK_STOPPED_CONTINUATION_PREFIX = "hook-stopped-continuation-prefix" 51 52 # Token/Resource Limits (3) 53 TOKEN_USAGE = "token-usage" 54 OUTPUT_TOKEN_LIMIT_EXCEEDED = "output-token-limit-exceeded" 55 USD_BUDGET = "usd-budget" 56 57 # Team/Swarm (4) 58 TEAM_COORDINATION = "team-coordination" 59 TEAM_SHUTDOWN = "team-shutdown" 60 DELEGATE_MODE_PROMPT = "delegate-mode-prompt" 61 EXITED_DELEGATE_MODE = "exited-delegate-mode" 62 63 # Session (1) 64 SESSION_CONTINUATION = "session-continuation" 65 66 # MCP Resources (2) 67 MCP_RESOURCE_NO_CONTENT = "mcp-resource-no-content" 68 MCP_RESOURCE_NO_DISPLAYABLE_CONTENT = "mcp-resource-no-displayable-content" 69 70 # Memory (2) 71 MEMORY_FILE_CONTENTS = "memory-file-contents" 72 NESTED_MEMORY_CONTENTS = "nested-memory-contents" 73 74 # Other (7) 75 COMPACT_FILE_REFERENCE = "compact-file-reference" 76 BTW_SIDE_QUESTION = "btw-side-question" 77 AGENT_MENTION = "agent-mention" 78 INVOKED_SKILLS = "invoked-skills" 79 OUTPUT_STYLE_ACTIVE = "output-style-active" 80 NEW_DIAGNOSTICS_DETECTED = "new-diagnostics-detected" 81 MALWARE_ANALYSIS_AFTER_READ = "malware-analysis-after-read-tool-call" 82 83 84 @dataclass 85 class ReminderContext: 86 """Context data for rendering reminders.""" 87 88 # File-related 89 file_path: Optional[str] = None 90 file_snippet: Optional[str] = None 91 truncated_lines: Optional[int] = None 92 selected_lines: Optional[str] = None 93 94 # Todo-related 95 todo_content: Optional[Any] = None 96 97 # Token-related 98 tokens_used: Optional[int] = None 99 tokens_total: Optional[int] = None 100 tokens_remaining: Optional[int] = None 101 usd_used: Optional[float] = None 102 usd_total: Optional[float] = None 103 104 # Hook-related 105 hook_name: Optional[str] = None 106 hook_output: Optional[str] = None 107 hook_error: Optional[str] = None 108 hook_context: Optional[str] = None 109 110 # Plan mode 111 plan_file_path: Optional[str] = None 112 113 # Team/Swarm 114 team_config_path: Optional[str] = None 115 task_list_path: Optional[str] = None 116 restricted_tools: Optional[list[str]] = None 117 118 # MCP Resources 119 resource_uri: Optional[str] = None 120 121 # Memory 122 memory_path: Optional[str] = None 123 memory_content: Optional[str] = None 124 125 # Agent/Skills 126 agent_name: Optional[str] = None 127 skills_list: Optional[list[str]] = None 128 style_name: Optional[str] = None 129 diagnostics: Optional[list[str]] = None 130 131 132 def get_reminder( 133 reminder_type: ReminderType, 134 reminder_context: Optional[ReminderContext] = None, 135 docker_workspace_path: str = "", 136 ) -> Optional[str]: 137 """ 138 Get a rendered system reminder. 139 140 Args: 141 reminder_type: Type of reminder to get 142 reminder_context: Context data for the reminder 143 docker_workspace_path: Docker path for internal translation 144 145 Returns: 146 Rendered reminder string wrapped in <system-reminder> tags, 147 or None if reminder not found 148 """ 149 manager = get_prompt_manager() 150 151 # Build base context 152 context = build_prompt_context(docker_workspace_path=docker_workspace_path) 153 154 # Add reminder-specific context variables 155 if reminder_context: 156 if reminder_context.file_path: 157 context.strings["FILE_PATH"] = reminder_context.file_path 158 if reminder_context.file_snippet: 159 context.strings["FILE_SNIPPET"] = reminder_context.file_snippet 160 if reminder_context.truncated_lines is not None: 161 context.strings["TRUNCATED_LINES"] = str(reminder_context.truncated_lines) 162 if reminder_context.selected_lines: 163 context.strings["SELECTED_LINES"] = reminder_context.selected_lines 164 if reminder_context.tokens_used is not None: 165 context.strings["TOKENS_USED"] = str(reminder_context.tokens_used) 166 if reminder_context.tokens_total is not None: 167 context.strings["TOKENS_TOTAL"] = str(reminder_context.tokens_total) 168 if reminder_context.tokens_remaining is not None: 169 context.strings["TOKENS_REMAINING"] = str(reminder_context.tokens_remaining) 170 if reminder_context.usd_used is not None: 171 context.strings["USD_USED"] = str(reminder_context.usd_used) 172 if reminder_context.usd_total is not None: 173 context.strings["USD_TOTAL"] = str(reminder_context.usd_total) 174 if reminder_context.hook_name: 175 context.strings["HOOK_NAME"] = reminder_context.hook_name 176 if reminder_context.hook_output: 177 context.strings["HOOK_OUTPUT"] = reminder_context.hook_output 178 if reminder_context.hook_error: 179 context.strings["HOOK_ERROR"] = reminder_context.hook_error 180 if reminder_context.hook_context: 181 context.strings["HOOK_CONTEXT"] = reminder_context.hook_context 182 if reminder_context.plan_file_path: 183 context.strings["PLAN_FILE_PATH"] = reminder_context.plan_file_path 184 if reminder_context.team_config_path: 185 context.strings["TEAM_CONFIG_PATH"] = reminder_context.team_config_path 186 if reminder_context.task_list_path: 187 context.strings["TASK_LIST_PATH"] = reminder_context.task_list_path 188 if reminder_context.resource_uri: 189 context.strings["RESOURCE_URI"] = reminder_context.resource_uri 190 if reminder_context.memory_path: 191 context.strings["MEMORY_PATH"] = reminder_context.memory_path 192 if reminder_context.memory_content: 193 context.strings["MEMORY_CONTENT"] = reminder_context.memory_content 194 if reminder_context.agent_name: 195 context.strings["AGENT_NAME"] = reminder_context.agent_name 196 if reminder_context.style_name: 197 context.strings["STYLE_NAME"] = reminder_context.style_name 198 if reminder_context.restricted_tools: 199 context.arrays["RESTRICTED_TOOLS"] = reminder_context.restricted_tools 200 if reminder_context.skills_list: 201 context.arrays["SKILLS_LIST"] = reminder_context.skills_list 202 if reminder_context.diagnostics: 203 context.arrays["DIAGNOSTICS"] = reminder_context.diagnostics 204 205 # Get and render reminder 206 reminder_content = manager.get_system_reminder(reminder_type.value, context) 207 208 if not reminder_content: 209 return None 210 211 # Wrap in system-reminder tags 212 return f"<system-reminder>\n{reminder_content.strip()}\n</system-reminder>"