omni-cortex 1.17.2__py3-none-any.whl → 1.17.4__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 (93) hide show
  1. omni_cortex/_bundled/dashboard/backend/.env.example +12 -0
  2. omni_cortex/_bundled/dashboard/backend/backfill_summaries.py +280 -0
  3. omni_cortex/_bundled/dashboard/backend/chat_service.py +631 -0
  4. omni_cortex/_bundled/dashboard/backend/database.py +1773 -0
  5. omni_cortex/_bundled/dashboard/backend/image_service.py +552 -0
  6. omni_cortex/_bundled/dashboard/backend/logging_config.py +122 -0
  7. omni_cortex/_bundled/dashboard/backend/main.py +1888 -0
  8. omni_cortex/_bundled/dashboard/backend/models.py +472 -0
  9. omni_cortex/_bundled/dashboard/backend/project_config.py +170 -0
  10. omni_cortex/_bundled/dashboard/backend/project_scanner.py +164 -0
  11. omni_cortex/_bundled/dashboard/backend/prompt_security.py +111 -0
  12. omni_cortex/_bundled/dashboard/backend/pyproject.toml +23 -0
  13. omni_cortex/_bundled/dashboard/backend/security.py +104 -0
  14. omni_cortex/_bundled/dashboard/backend/test_database.py +301 -0
  15. omni_cortex/_bundled/dashboard/backend/tmpclaude-2dfa-cwd +1 -0
  16. omni_cortex/_bundled/dashboard/backend/tmpclaude-c460-cwd +1 -0
  17. omni_cortex/_bundled/dashboard/backend/uv.lock +1110 -0
  18. omni_cortex/_bundled/dashboard/backend/websocket_manager.py +104 -0
  19. omni_cortex/_bundled/dashboard/frontend/dist/assets/index-CQlQK3nE.js +551 -0
  20. omni_cortex/_bundled/dashboard/frontend/dist/assets/index-CmUNNfe4.css +1 -0
  21. omni_cortex/_bundled/dashboard/frontend/dist/index.html +14 -0
  22. omni_cortex/_bundled/hooks/post_tool_use.py +497 -0
  23. omni_cortex/_bundled/hooks/pre_tool_use.py +277 -0
  24. omni_cortex/_bundled/hooks/session_utils.py +186 -0
  25. omni_cortex/_bundled/hooks/stop.py +219 -0
  26. omni_cortex/_bundled/hooks/subagent_stop.py +120 -0
  27. omni_cortex/_bundled/hooks/user_prompt.py +331 -0
  28. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/main.py +2 -2
  29. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/user_prompt.py +113 -2
  30. {omni_cortex-1.17.2.dist-info → omni_cortex-1.17.4.dist-info}/METADATA +6 -1
  31. omni_cortex-1.17.4.dist-info/RECORD +53 -0
  32. omni_cortex/__init__.py +0 -3
  33. omni_cortex/categorization/__init__.py +0 -9
  34. omni_cortex/categorization/auto_tags.py +0 -166
  35. omni_cortex/categorization/auto_type.py +0 -165
  36. omni_cortex/config.py +0 -141
  37. omni_cortex/dashboard.py +0 -232
  38. omni_cortex/database/__init__.py +0 -24
  39. omni_cortex/database/connection.py +0 -137
  40. omni_cortex/database/migrations.py +0 -210
  41. omni_cortex/database/schema.py +0 -212
  42. omni_cortex/database/sync.py +0 -421
  43. omni_cortex/decay/__init__.py +0 -7
  44. omni_cortex/decay/importance.py +0 -147
  45. omni_cortex/embeddings/__init__.py +0 -35
  46. omni_cortex/embeddings/local.py +0 -442
  47. omni_cortex/models/__init__.py +0 -20
  48. omni_cortex/models/activity.py +0 -265
  49. omni_cortex/models/agent.py +0 -144
  50. omni_cortex/models/memory.py +0 -395
  51. omni_cortex/models/relationship.py +0 -206
  52. omni_cortex/models/session.py +0 -290
  53. omni_cortex/resources/__init__.py +0 -1
  54. omni_cortex/search/__init__.py +0 -22
  55. omni_cortex/search/hybrid.py +0 -197
  56. omni_cortex/search/keyword.py +0 -204
  57. omni_cortex/search/ranking.py +0 -127
  58. omni_cortex/search/semantic.py +0 -232
  59. omni_cortex/server.py +0 -360
  60. omni_cortex/setup.py +0 -278
  61. omni_cortex/tools/__init__.py +0 -13
  62. omni_cortex/tools/activities.py +0 -453
  63. omni_cortex/tools/memories.py +0 -536
  64. omni_cortex/tools/sessions.py +0 -311
  65. omni_cortex/tools/utilities.py +0 -477
  66. omni_cortex/utils/__init__.py +0 -13
  67. omni_cortex/utils/formatting.py +0 -282
  68. omni_cortex/utils/ids.py +0 -72
  69. omni_cortex/utils/timestamps.py +0 -129
  70. omni_cortex/utils/truncation.py +0 -111
  71. omni_cortex-1.17.2.dist-info/RECORD +0 -65
  72. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/.env.example +0 -0
  73. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +0 -0
  74. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +0 -0
  75. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/database.py +0 -0
  76. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/image_service.py +0 -0
  77. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +0 -0
  78. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/models.py +0 -0
  79. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
  80. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
  81. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/prompt_security.py +0 -0
  82. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
  83. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/security.py +0 -0
  84. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/uv.lock +0 -0
  85. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
  86. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/post_tool_use.py +0 -0
  87. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/pre_tool_use.py +0 -0
  88. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/session_utils.py +0 -0
  89. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/stop.py +0 -0
  90. {omni_cortex-1.17.2.data → omni_cortex-1.17.4.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
  91. {omni_cortex-1.17.2.dist-info → omni_cortex-1.17.4.dist-info}/WHEEL +0 -0
  92. {omni_cortex-1.17.2.dist-info → omni_cortex-1.17.4.dist-info}/entry_points.txt +0 -0
  93. {omni_cortex-1.17.2.dist-info → omni_cortex-1.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,453 +0,0 @@
1
- """Activity logging tools for Omni Cortex MCP."""
2
-
3
- import json
4
- from datetime import datetime, timezone, timedelta
5
- from typing import Optional
6
- from pydantic import BaseModel, Field, ConfigDict
7
-
8
- from mcp.server.fastmcp import FastMCP
9
-
10
- from ..database.connection import init_database
11
- from ..config import get_project_path, get_session_id
12
- from ..models.activity import Activity, ActivityCreate, create_activity, get_activities
13
- from ..models.memory import list_memories
14
- from ..utils.formatting import format_activity_markdown, format_timeline_markdown
15
- from ..utils.timestamps import now_iso, parse_iso
16
- from pathlib import Path
17
-
18
-
19
- # === Input Models ===
20
-
21
- class LogActivityInput(BaseModel):
22
- """Input for logging an activity."""
23
-
24
- model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
25
-
26
- event_type: str = Field(
27
- ..., description="Event type: pre_tool_use, post_tool_use, decision, observation"
28
- )
29
- tool_name: Optional[str] = Field(None, description="Tool name if applicable")
30
- tool_input: Optional[str] = Field(None, description="Tool input (JSON string)")
31
- tool_output: Optional[str] = Field(None, description="Tool output (JSON string)")
32
- duration_ms: Optional[int] = Field(None, description="Duration in milliseconds", ge=0)
33
- success: bool = Field(True, description="Whether the operation succeeded")
34
- error_message: Optional[str] = Field(None, description="Error message if failed")
35
- file_path: Optional[str] = Field(None, description="Relevant file path")
36
- agent_id: Optional[str] = Field(None, description="Agent ID")
37
- # Command analytics fields
38
- command_name: Optional[str] = Field(None, description="Slash command name")
39
- command_scope: Optional[str] = Field(None, description="'universal' or 'project'")
40
- mcp_server: Optional[str] = Field(None, description="MCP server name")
41
- skill_name: Optional[str] = Field(None, description="Skill name")
42
-
43
-
44
- class GetActivitiesInput(BaseModel):
45
- """Input for getting activities."""
46
-
47
- model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
48
-
49
- session_id: Optional[str] = Field(None, description="Filter by session ID")
50
- agent_id: Optional[str] = Field(None, description="Filter by agent ID")
51
- event_type: Optional[str] = Field(None, description="Filter by event type")
52
- tool_name: Optional[str] = Field(None, description="Filter by tool name")
53
- since: Optional[str] = Field(None, description="Start time (ISO 8601)")
54
- until: Optional[str] = Field(None, description="End time (ISO 8601)")
55
- limit: int = Field(50, description="Maximum results", ge=1, le=200)
56
- offset: int = Field(0, description="Pagination offset", ge=0)
57
-
58
-
59
- class TimelineInput(BaseModel):
60
- """Input for getting timeline."""
61
-
62
- model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
63
-
64
- hours: int = Field(24, description="Hours to look back", ge=1, le=168)
65
- include_activities: bool = Field(True, description="Include activities")
66
- include_memories: bool = Field(True, description="Include memories")
67
- group_by: str = Field("hour", description="Group by: hour, day, or session")
68
-
69
-
70
- def register_activity_tools(mcp: FastMCP) -> None:
71
- """Register all activity tools with the MCP server."""
72
-
73
- @mcp.tool(
74
- name="cortex_log_activity",
75
- annotations={
76
- "title": "Log Activity",
77
- "readOnlyHint": False,
78
- "destructiveHint": False,
79
- "idempotentHint": False,
80
- "openWorldHint": False,
81
- },
82
- )
83
- async def cortex_log_activity(params: LogActivityInput) -> str:
84
- """Log a tool call, decision, or observation.
85
-
86
- This tool records activities in the audit trail. Most activity logging
87
- is done automatically by hooks, but this tool allows manual logging.
88
-
89
- Args:
90
- params: LogActivityInput with event details
91
-
92
- Returns:
93
- Confirmation with activity ID
94
- """
95
- try:
96
- conn = init_database()
97
- project_path = str(get_project_path())
98
- session_id = get_session_id()
99
-
100
- # Auto-detect command analytics if not provided
101
- command_name = params.command_name
102
- command_scope = params.command_scope
103
- mcp_server = params.mcp_server
104
- skill_name = params.skill_name
105
-
106
- # Extract skill info from Skill tool calls
107
- if params.tool_name == "Skill" and params.tool_input:
108
- extracted_skill, extracted_scope = _extract_skill_info(
109
- params.tool_input, project_path
110
- )
111
- if extracted_skill:
112
- skill_name = skill_name or extracted_skill
113
- command_scope = command_scope or extracted_scope
114
-
115
- # Extract MCP server from tool name (mcp__servername__toolname pattern)
116
- if params.tool_name and params.tool_name.startswith("mcp__"):
117
- extracted_mcp = _extract_mcp_server(params.tool_name)
118
- mcp_server = mcp_server or extracted_mcp
119
-
120
- activity_data = ActivityCreate(
121
- event_type=params.event_type,
122
- tool_name=params.tool_name,
123
- tool_input=params.tool_input,
124
- tool_output=params.tool_output,
125
- duration_ms=params.duration_ms,
126
- success=params.success,
127
- error_message=params.error_message,
128
- file_path=params.file_path,
129
- agent_id=params.agent_id,
130
- command_name=command_name,
131
- command_scope=command_scope,
132
- mcp_server=mcp_server,
133
- skill_name=skill_name,
134
- )
135
-
136
- activity = create_activity(
137
- conn,
138
- activity_data,
139
- session_id=session_id,
140
- project_path=project_path,
141
- )
142
-
143
- return (
144
- f"Logged: {activity.id}\n"
145
- f"Type: {activity.event_type}\n"
146
- f"Tool: {activity.tool_name or 'N/A'}\n"
147
- f"Success: {'Yes' if activity.success else 'No'}"
148
- )
149
-
150
- except Exception as e:
151
- return f"Error logging activity: {e}"
152
-
153
- @mcp.tool(
154
- name="cortex_get_activities",
155
- annotations={
156
- "title": "Get Activities",
157
- "readOnlyHint": True,
158
- "destructiveHint": False,
159
- "idempotentHint": True,
160
- "openWorldHint": False,
161
- },
162
- )
163
- async def cortex_get_activities(params: GetActivitiesInput) -> str:
164
- """Query the activity log with filters.
165
-
166
- Args:
167
- params: GetActivitiesInput with filters and pagination
168
-
169
- Returns:
170
- Activities formatted as markdown
171
- """
172
- try:
173
- conn = init_database()
174
-
175
- activities, total = get_activities(
176
- conn,
177
- session_id=params.session_id,
178
- agent_id=params.agent_id,
179
- event_type=params.event_type,
180
- tool_name=params.tool_name,
181
- since=params.since,
182
- until=params.until,
183
- limit=params.limit,
184
- offset=params.offset,
185
- )
186
-
187
- if not activities:
188
- return "No activities found."
189
-
190
- lines = [f"# Activities ({len(activities)} of {total})", ""]
191
-
192
- for activity in activities:
193
- lines.append(format_activity_markdown(activity.model_dump()))
194
- lines.append("")
195
-
196
- return "\n".join(lines)
197
-
198
- except Exception as e:
199
- return f"Error getting activities: {e}"
200
-
201
- @mcp.tool(
202
- name="cortex_get_timeline",
203
- annotations={
204
- "title": "Get Timeline",
205
- "readOnlyHint": True,
206
- "destructiveHint": False,
207
- "idempotentHint": True,
208
- "openWorldHint": False,
209
- },
210
- )
211
- async def cortex_get_timeline(params: TimelineInput) -> str:
212
- """Get a timeline of activities and memories.
213
-
214
- This provides a chronological view of what happened in the project
215
- over the specified time period.
216
-
217
- Args:
218
- params: TimelineInput with time range and inclusion options
219
-
220
- Returns:
221
- Timeline formatted as markdown
222
- """
223
- try:
224
- conn = init_database()
225
-
226
- # Calculate time range
227
- now = datetime.now(timezone.utc)
228
- since = (now - timedelta(hours=params.hours)).isoformat()
229
-
230
- activities_list = []
231
- memories_list = []
232
-
233
- if params.include_activities:
234
- activities_result, _ = get_activities(
235
- conn,
236
- since=since,
237
- limit=100,
238
- )
239
- activities_list = [a.model_dump() for a in activities_result]
240
-
241
- if params.include_memories:
242
- memories_result, _ = list_memories(
243
- conn,
244
- sort_by="created_at",
245
- sort_order="desc",
246
- limit=50,
247
- )
248
- # Filter to time range
249
- memories_list = [
250
- m.model_dump()
251
- for m in memories_result
252
- if parse_iso(m.created_at) >= parse_iso(since)
253
- ]
254
-
255
- return format_timeline_markdown(
256
- activities_list,
257
- memories_list,
258
- group_by=params.group_by,
259
- )
260
-
261
- except Exception as e:
262
- return f"Error getting timeline: {e}"
263
-
264
-
265
- # === Helper Functions for Command Analytics ===
266
-
267
-
268
- def _extract_skill_info(tool_input: str, project_path: str) -> tuple[Optional[str], Optional[str]]:
269
- """Extract skill name and scope from Skill tool input.
270
-
271
- Args:
272
- tool_input: JSON string of tool input
273
- project_path: Current project path for scope detection
274
-
275
- Returns:
276
- Tuple of (skill_name, scope) where scope is 'universal' or 'project'
277
- """
278
- try:
279
- input_data = json.loads(tool_input)
280
- skill_name = input_data.get("skill", "")
281
- if not skill_name:
282
- return None, None
283
-
284
- # Determine scope by checking file locations
285
- from pathlib import Path
286
-
287
- # Check project-specific commands first
288
- project_cmd = Path(project_path) / ".claude" / "commands" / f"{skill_name}.md"
289
- if project_cmd.exists():
290
- return skill_name, "project"
291
-
292
- # Check universal commands
293
- universal_cmd = Path.home() / ".claude" / "commands" / f"{skill_name}.md"
294
- if universal_cmd.exists():
295
- return skill_name, "universal"
296
-
297
- # Default to unknown scope if skill exists but location is unclear
298
- return skill_name, "unknown"
299
-
300
- except (json.JSONDecodeError, TypeError, KeyError):
301
- return None, None
302
-
303
-
304
- def _extract_mcp_server(tool_name: str) -> Optional[str]:
305
- """Extract MCP server name from tool name.
306
-
307
- Tool names follow the pattern: mcp__servername__toolname
308
-
309
- Args:
310
- tool_name: Full tool name
311
-
312
- Returns:
313
- MCP server name or None
314
- """
315
- if not tool_name or not tool_name.startswith("mcp__"):
316
- return None
317
-
318
- parts = tool_name.split("__")
319
- if len(parts) >= 3:
320
- return parts[1] # Server name is the second part
321
-
322
- return None
323
-
324
-
325
- # === Natural Language Summary Generation ===
326
-
327
-
328
- def generate_activity_summary(
329
- tool_name: Optional[str],
330
- tool_input: Optional[str],
331
- success: bool,
332
- file_path: Optional[str],
333
- event_type: str,
334
- ) -> tuple[str, str]:
335
- """Generate natural language summary for an activity.
336
-
337
- Returns:
338
- tuple of (short_summary, detailed_summary)
339
- - short_summary: 12-20 words, shown in collapsed view
340
- - detailed_summary: Expanded description with more context
341
- """
342
- short = ""
343
- detail = ""
344
-
345
- # Parse tool input if available
346
- input_data = {}
347
- if tool_input:
348
- try:
349
- input_data = json.loads(tool_input)
350
- except (json.JSONDecodeError, TypeError):
351
- pass
352
-
353
- # Generate summaries based on tool type
354
- if tool_name == "Read":
355
- path = input_data.get("file_path", file_path or "unknown file")
356
- filename = Path(path).name if path else "file"
357
- short = f"Read file: {filename}"
358
- detail = f"Reading contents of {path}"
359
-
360
- elif tool_name == "Write":
361
- path = input_data.get("file_path", file_path or "unknown file")
362
- filename = Path(path).name if path else "file"
363
- short = f"Write file: {filename}"
364
- detail = f"Writing/creating file at {path}"
365
-
366
- elif tool_name == "Edit":
367
- path = input_data.get("file_path", file_path or "unknown file")
368
- filename = Path(path).name if path else "file"
369
- short = f"Edit file: {filename}"
370
- detail = f"Editing {path} - replacing text content"
371
-
372
- elif tool_name == "Bash":
373
- cmd = input_data.get("command", "")[:50]
374
- short = f"Run command: {cmd}..."
375
- detail = f"Executing bash command: {input_data.get('command', 'unknown')}"
376
-
377
- elif tool_name == "Grep":
378
- pattern = input_data.get("pattern", "")
379
- short = f"Search for: {pattern[:30]}"
380
- detail = f"Searching codebase for pattern: {pattern}"
381
-
382
- elif tool_name == "Glob":
383
- pattern = input_data.get("pattern", "")
384
- short = f"Find files: {pattern[:30]}"
385
- detail = f"Finding files matching pattern: {pattern}"
386
-
387
- elif tool_name == "Skill":
388
- skill = input_data.get("skill", "unknown")
389
- short = f"Run skill: /{skill}"
390
- detail = f"Executing slash command /{skill}"
391
-
392
- elif tool_name == "Task":
393
- desc = input_data.get("description", "task")
394
- short = f"Spawn agent: {desc[:30]}"
395
- detail = f"Launching sub-agent for: {input_data.get('prompt', desc)[:100]}"
396
-
397
- elif tool_name == "WebSearch":
398
- query = input_data.get("query", "")
399
- short = f"Web search: {query[:30]}"
400
- detail = f"Searching the web for: {query}"
401
-
402
- elif tool_name == "WebFetch":
403
- url = input_data.get("url", "")
404
- short = f"Fetch URL: {url[:40]}"
405
- detail = f"Fetching content from: {url}"
406
-
407
- elif tool_name == "TodoWrite":
408
- todos = input_data.get("todos", [])
409
- count = len(todos) if isinstance(todos, list) else 0
410
- short = f"Update todo list: {count} items"
411
- detail = f"Managing task list with {count} items"
412
-
413
- elif tool_name == "AskUserQuestion":
414
- questions = input_data.get("questions", [])
415
- count = len(questions) if isinstance(questions, list) else 1
416
- short = f"Ask user: {count} question(s)"
417
- detail = f"Prompting user for input with {count} question(s)"
418
-
419
- elif tool_name and tool_name.startswith("mcp__"):
420
- parts = tool_name.split("__")
421
- server = parts[1] if len(parts) > 1 else "unknown"
422
- tool = parts[2] if len(parts) > 2 else tool_name
423
- short = f"MCP call: {server}/{tool}"
424
- detail = f"Calling {tool} tool from MCP server {server}"
425
-
426
- elif tool_name == "cortex_remember" or (tool_name and "remember" in tool_name.lower()):
427
- params = input_data.get("params", {})
428
- content = params.get("content", "") if isinstance(params, dict) else ""
429
- short = f"Store memory: {content[:30]}..." if content else "Store memory"
430
- detail = f"Saving to memory system: {content[:100]}" if content else "Saving to memory system"
431
-
432
- elif tool_name == "cortex_recall" or (tool_name and "recall" in tool_name.lower()):
433
- params = input_data.get("params", {})
434
- query = params.get("query", "") if isinstance(params, dict) else ""
435
- short = f"Recall: {query[:30]}" if query else "Recall memories"
436
- detail = f"Searching memories for: {query}" if query else "Retrieving memories"
437
-
438
- elif tool_name == "NotebookEdit":
439
- path = input_data.get("notebook_path", "")
440
- filename = Path(path).name if path else "notebook"
441
- short = f"Edit notebook: {filename}"
442
- detail = f"Editing Jupyter notebook {path}"
443
-
444
- else:
445
- short = f"{event_type}: {tool_name or 'unknown'}"
446
- detail = f"Activity type {event_type} with tool {tool_name}"
447
-
448
- # Add status suffix for failures
449
- if not success:
450
- short = f"[FAILED] {short}"
451
- detail = f"[FAILED] {detail}"
452
-
453
- return short, detail