/ src / core / system_reminders.py
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>"