omni-cortex 1.13.0__tar.gz → 1.15.0__tar.gz

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 (71) hide show
  1. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/PKG-INFO +1 -1
  2. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/chat_service.py +61 -2
  3. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/database.py +120 -0
  4. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/main.py +177 -0
  5. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/models.py +72 -0
  6. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/__init__.py +1 -1
  7. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/pyproject.toml +1 -1
  8. omni_cortex-1.15.0/scripts/generate_storage_architecture_pdf.py +572 -0
  9. omni_cortex-1.15.0/scripts/update_docs_pdfs.py +1263 -0
  10. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/.gitignore +0 -0
  11. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/LICENSE +0 -0
  12. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/README.md +0 -0
  13. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/.env.example +0 -0
  14. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/backfill_summaries.py +0 -0
  15. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/image_service.py +0 -0
  16. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/logging_config.py +0 -0
  17. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/project_config.py +0 -0
  18. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/project_scanner.py +0 -0
  19. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/prompt_security.py +0 -0
  20. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/pyproject.toml +0 -0
  21. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/security.py +0 -0
  22. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/uv.lock +0 -0
  23. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/dashboard/backend/websocket_manager.py +0 -0
  24. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/hooks/post_tool_use.py +0 -0
  25. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/hooks/pre_tool_use.py +0 -0
  26. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/hooks/session_utils.py +0 -0
  27. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/hooks/stop.py +0 -0
  28. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/hooks/subagent_stop.py +0 -0
  29. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/hooks/user_prompt.py +0 -0
  30. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/categorization/__init__.py +0 -0
  31. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/categorization/auto_tags.py +0 -0
  32. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/categorization/auto_type.py +0 -0
  33. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/config.py +0 -0
  34. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/dashboard.py +0 -0
  35. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/database/__init__.py +0 -0
  36. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/database/connection.py +0 -0
  37. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/database/migrations.py +0 -0
  38. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/database/schema.py +0 -0
  39. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/database/sync.py +0 -0
  40. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/decay/__init__.py +0 -0
  41. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/decay/importance.py +0 -0
  42. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/embeddings/__init__.py +0 -0
  43. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/embeddings/local.py +0 -0
  44. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/models/__init__.py +0 -0
  45. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/models/activity.py +0 -0
  46. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/models/agent.py +0 -0
  47. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/models/memory.py +0 -0
  48. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/models/relationship.py +0 -0
  49. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/models/session.py +0 -0
  50. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/resources/__init__.py +0 -0
  51. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/search/__init__.py +0 -0
  52. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/search/hybrid.py +0 -0
  53. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/search/keyword.py +0 -0
  54. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/search/ranking.py +0 -0
  55. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/search/semantic.py +0 -0
  56. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/server.py +0 -0
  57. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/setup.py +0 -0
  58. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/tools/__init__.py +0 -0
  59. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/tools/activities.py +0 -0
  60. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/tools/memories.py +0 -0
  61. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/tools/sessions.py +0 -0
  62. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/tools/utilities.py +0 -0
  63. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/utils/__init__.py +0 -0
  64. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/utils/formatting.py +0 -0
  65. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/utils/ids.py +0 -0
  66. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/utils/timestamps.py +0 -0
  67. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/omni_cortex/utils/truncation.py +0 -0
  68. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/scripts/check-venv.py +0 -0
  69. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/scripts/import_ken_memories.py +0 -0
  70. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/scripts/populate_session_data.py +0 -0
  71. {omni_cortex-1.13.0 → omni_cortex-1.15.0}/scripts/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omni-cortex
3
- Version: 1.13.0
3
+ Version: 1.15.0
4
4
  Summary: Give Claude Code a perfect memory - auto-logs everything, searches smartly, and gets smarter over time
5
5
  Project-URL: Homepage, https://github.com/AllCytes/Omni-Cortex
6
6
  Project-URL: Repository, https://github.com/AllCytes/Omni-Cortex
@@ -42,9 +42,13 @@ def is_available() -> bool:
42
42
  return False
43
43
 
44
44
 
45
- def build_style_context_prompt(style_profile: dict) -> str:
45
+ def build_style_context_prompt(style_profile: dict | None) -> str:
46
46
  """Build a prompt section describing user's communication style."""
47
47
 
48
+ # Return empty string if no style profile provided
49
+ if not style_profile:
50
+ return ""
51
+
48
52
  # Handle both camelCase (new format) and snake_case (old format)
49
53
  tone_dist = style_profile.get("toneDistribution") or style_profile.get("tone_distribution", {})
50
54
  tone_list = ", ".join(tone_dist.keys()) if tone_dist else "neutral"
@@ -416,6 +420,8 @@ def build_compose_prompt(
416
420
  template: Optional[str],
417
421
  tone_level: int,
418
422
  memory_context: str,
423
+ custom_instructions: Optional[str] = None,
424
+ include_explanation: bool = False,
419
425
  ) -> str:
420
426
  """Build the prompt for composing a response in user's style.
421
427
 
@@ -426,6 +432,8 @@ def build_compose_prompt(
426
432
  template: Optional response template (answer, guide, redirect, acknowledge)
427
433
  tone_level: Tone formality level (0-100)
428
434
  memory_context: Relevant memories formatted as context
435
+ custom_instructions: Optional specific instructions from the user
436
+ include_explanation: Whether to explain the incoming message first
429
437
 
430
438
  Returns:
431
439
  Complete prompt for response generation
@@ -480,7 +488,37 @@ Use this information naturally in your response if relevant. Don't explicitly ci
480
488
 
481
489
  """
482
490
 
483
- prompt += """
491
+ # Add custom instructions if provided
492
+ if custom_instructions:
493
+ prompt += f"""
494
+ ## CUSTOM INSTRUCTIONS FROM USER
495
+
496
+ The user has provided these specific instructions for the response:
497
+
498
+ <custom_instructions>
499
+ {xml_escape(custom_instructions)}
500
+ </custom_instructions>
501
+
502
+ Please incorporate these requirements while maintaining the user's voice.
503
+
504
+ """
505
+
506
+ # Build task instructions based on explanation mode
507
+ if include_explanation:
508
+ prompt += """
509
+ **Your Task:**
510
+ 1. FIRST, provide a clear explanation of what the incoming message means or is asking
511
+ Format: "**Understanding:** [your explanation in user's voice]"
512
+ 2. THEN, write a response to the incoming message in YOUR voice
513
+ Format: "**Response:** [your response]"
514
+ 3. Use the knowledge from your memories naturally if relevant
515
+ 4. Match the tone level specified above
516
+ 5. Follow the platform context guidelines
517
+ 6. Sound exactly like something you would write yourself
518
+
519
+ Write the explanation and response now:"""
520
+ else:
521
+ prompt += """
484
522
  **Your Task:**
485
523
  1. Write a response to the incoming message in YOUR voice (the user's voice)
486
524
  2. Use the knowledge from your memories naturally if relevant
@@ -501,6 +539,8 @@ async def compose_response(
501
539
  tone_level: int = 50,
502
540
  include_memories: bool = True,
503
541
  style_profile: Optional[dict] = None,
542
+ custom_instructions: Optional[str] = None,
543
+ include_explanation: bool = False,
504
544
  ) -> dict:
505
545
  """Compose a response to an incoming message in the user's style.
506
546
 
@@ -512,6 +552,8 @@ async def compose_response(
512
552
  tone_level: Tone formality level (0-100)
513
553
  include_memories: Whether to include relevant memories
514
554
  style_profile: User's style profile dictionary
555
+ custom_instructions: Optional specific instructions from the user
556
+ include_explanation: Whether to explain the incoming message first
515
557
 
516
558
  Returns:
517
559
  Dict with response, sources, and metadata
@@ -550,6 +592,8 @@ async def compose_response(
550
592
  template=template,
551
593
  tone_level=tone_level,
552
594
  memory_context=memory_context,
595
+ custom_instructions=custom_instructions,
596
+ include_explanation=include_explanation,
553
597
  )
554
598
 
555
599
  try:
@@ -563,10 +607,25 @@ async def compose_response(
563
607
  "response": f"Failed to generate response: {str(e)}",
564
608
  "sources": sources,
565
609
  "error": "generation_failed",
610
+ "explanation": None,
566
611
  }
567
612
 
613
+ # Parse explanation if requested
614
+ explanation = None
615
+ if include_explanation:
616
+ # Try to extract explanation and response parts
617
+ import re
618
+ understanding_match = re.search(r'\*\*Understanding:\*\*\s*(.+?)(?=\*\*Response:\*\*)', composed_response, re.DOTALL)
619
+ response_match = re.search(r'\*\*Response:\*\*\s*(.+)', composed_response, re.DOTALL)
620
+
621
+ if understanding_match and response_match:
622
+ explanation = understanding_match.group(1).strip()
623
+ composed_response = response_match.group(1).strip()
624
+ # If parsing fails, leave explanation as None and return full response
625
+
568
626
  return {
569
627
  "response": composed_response,
570
628
  "sources": sources,
571
629
  "error": None,
630
+ "explanation": explanation,
572
631
  }
@@ -1651,3 +1651,123 @@ def compute_style_profile_from_messages(db_path: str) -> Optional[dict]:
1651
1651
  "styleMarkers": style_markers,
1652
1652
  "sampleMessages": sample_messages,
1653
1653
  }
1654
+
1655
+
1656
+ # --- Agent Query Functions ---
1657
+
1658
+
1659
+ def get_agents(
1660
+ db_path: str,
1661
+ agent_type: Optional[str] = None,
1662
+ limit: int = 50,
1663
+ active_only: bool = False
1664
+ ) -> list[dict]:
1665
+ """Get agents with recent activity counts."""
1666
+ conn = get_connection(db_path)
1667
+
1668
+ query = """
1669
+ SELECT
1670
+ a.*,
1671
+ COALESCE(recent.count, 0) as recent_activity_count,
1672
+ CASE WHEN a.last_seen > datetime('now', '-5 minutes') THEN 1 ELSE 0 END as is_active
1673
+ FROM agents a
1674
+ LEFT JOIN (
1675
+ SELECT agent_id, COUNT(*) as count
1676
+ FROM activities
1677
+ WHERE timestamp > datetime('now', '-1 hour')
1678
+ GROUP BY agent_id
1679
+ ) recent ON recent.agent_id = a.id
1680
+ WHERE 1=1
1681
+ """
1682
+ params = []
1683
+
1684
+ if agent_type:
1685
+ query += " AND a.type = ?"
1686
+ params.append(agent_type)
1687
+
1688
+ if active_only:
1689
+ query += " AND a.last_seen > datetime('now', '-5 minutes')"
1690
+
1691
+ query += " ORDER BY a.last_seen DESC LIMIT ?"
1692
+ params.append(limit)
1693
+
1694
+ cursor = conn.execute(query, params)
1695
+ results = [dict(row) for row in cursor.fetchall()]
1696
+ conn.close()
1697
+ return results
1698
+
1699
+
1700
+ def get_agent_by_id(db_path: str, agent_id: str) -> Optional[dict]:
1701
+ """Get single agent by ID."""
1702
+ conn = get_connection(db_path)
1703
+
1704
+ cursor = conn.execute("""
1705
+ SELECT
1706
+ a.*,
1707
+ CASE WHEN a.last_seen > datetime('now', '-5 minutes') THEN 1 ELSE 0 END as is_active
1708
+ FROM agents a
1709
+ WHERE a.id = ?
1710
+ """, (agent_id,))
1711
+
1712
+ row = cursor.fetchone()
1713
+ conn.close()
1714
+ return dict(row) if row else None
1715
+
1716
+
1717
+ def get_agent_tool_breakdown(db_path: str, agent_id: str) -> list[dict]:
1718
+ """Get tool usage breakdown for an agent."""
1719
+ conn = get_connection(db_path)
1720
+
1721
+ cursor = conn.execute("""
1722
+ SELECT
1723
+ tool_name,
1724
+ COUNT(*) as count,
1725
+ AVG(duration_ms) as avg_duration_ms,
1726
+ SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as success_rate
1727
+ FROM activities
1728
+ WHERE agent_id = ? AND tool_name IS NOT NULL
1729
+ GROUP BY tool_name
1730
+ ORDER BY count DESC
1731
+ """, (agent_id,))
1732
+
1733
+ results = [dict(row) for row in cursor.fetchall()]
1734
+ conn.close()
1735
+ return results
1736
+
1737
+
1738
+ def get_agent_files_touched(db_path: str, agent_id: str, limit: int = 50) -> list[str]:
1739
+ """Get list of files an agent has touched."""
1740
+ conn = get_connection(db_path)
1741
+
1742
+ # Files from file_path column
1743
+ cursor = conn.execute("""
1744
+ SELECT DISTINCT file_path
1745
+ FROM activities
1746
+ WHERE agent_id = ?
1747
+ AND file_path IS NOT NULL
1748
+ AND file_path != ''
1749
+ LIMIT ?
1750
+ """, (agent_id, limit))
1751
+
1752
+ files = [row[0] for row in cursor.fetchall()]
1753
+ conn.close()
1754
+ return files
1755
+
1756
+
1757
+ def get_agent_parent(db_path: str, agent_id: str) -> Optional[str]:
1758
+ """Find which agent spawned this subagent (via Task tool)."""
1759
+ conn = get_connection(db_path)
1760
+
1761
+ # Look for Task tool call that created this agent
1762
+ cursor = conn.execute("""
1763
+ SELECT agent_id
1764
+ FROM activities
1765
+ WHERE tool_name = 'Task'
1766
+ AND tool_output LIKE ?
1767
+ ORDER BY timestamp DESC
1768
+ LIMIT 1
1769
+ """, (f'%{agent_id}%',))
1770
+
1771
+ row = cursor.fetchone()
1772
+ conn.close()
1773
+ return row[0] if row else None
@@ -39,6 +39,11 @@ from database import (
39
39
  get_activities,
40
40
  get_activity_detail,
41
41
  get_activity_heatmap,
42
+ get_agents,
43
+ get_agent_by_id,
44
+ get_agent_files_touched,
45
+ get_agent_parent,
46
+ get_agent_tool_breakdown,
42
47
  get_all_tags,
43
48
  get_command_usage,
44
49
  get_mcp_usage,
@@ -1146,6 +1151,8 @@ async def compose_response_endpoint(
1146
1151
  tone_level=request.tone_level,
1147
1152
  include_memories=request.include_memories,
1148
1153
  style_profile=style_profile,
1154
+ custom_instructions=request.custom_instructions,
1155
+ include_explanation=request.include_explanation,
1149
1156
  )
1150
1157
 
1151
1158
  if result.get("error"):
@@ -1165,6 +1172,8 @@ async def compose_response_endpoint(
1165
1172
  incoming_message=request.incoming_message,
1166
1173
  context_type=request.context_type,
1167
1174
  created_at=datetime.now().isoformat(),
1175
+ custom_instructions=request.custom_instructions,
1176
+ explanation=result.get("explanation"),
1168
1177
  )
1169
1178
 
1170
1179
  log_success("/api/compose-response", context=request.context_type, tone=request.tone_level)
@@ -1621,6 +1630,174 @@ async def export_memories(
1621
1630
  raise HTTPException(status_code=400, detail=f"Unsupported format: {format}. Use json, markdown, or csv.")
1622
1631
 
1623
1632
 
1633
+ # --- Agent Endpoints ---
1634
+
1635
+
1636
+ @app.get("/api/agents")
1637
+ async def list_agents(
1638
+ project: str = Query(..., description="Path to the database file"),
1639
+ type: Optional[str] = Query(None, description="Filter by agent type: main, subagent, tool"),
1640
+ active_only: bool = Query(False, description="Show only active agents (last 5 minutes)"),
1641
+ limit: int = Query(50, ge=1, le=200),
1642
+ ):
1643
+ """List all agents with filtering."""
1644
+ if not Path(project).exists():
1645
+ raise HTTPException(status_code=404, detail="Database not found")
1646
+
1647
+ agents = get_agents(project, agent_type=type, limit=limit, active_only=active_only)
1648
+ return {"agents": agents, "count": len(agents)}
1649
+
1650
+
1651
+ @app.get("/api/agents/{agent_id}")
1652
+ async def get_agent(
1653
+ agent_id: str,
1654
+ project: str = Query(..., description="Path to the database file"),
1655
+ ):
1656
+ """Get single agent details."""
1657
+ if not Path(project).exists():
1658
+ raise HTTPException(status_code=404, detail="Database not found")
1659
+
1660
+ agent = get_agent_by_id(project, agent_id)
1661
+ if not agent:
1662
+ raise HTTPException(status_code=404, detail="Agent not found")
1663
+
1664
+ return agent
1665
+
1666
+
1667
+ @app.get("/api/agents/{agent_id}/stats")
1668
+ async def get_agent_stats_endpoint(
1669
+ agent_id: str,
1670
+ project: str = Query(..., description="Path to the database file"),
1671
+ ):
1672
+ """Get detailed stats for an agent."""
1673
+ if not Path(project).exists():
1674
+ raise HTTPException(status_code=404, detail="Database not found")
1675
+
1676
+ agent = get_agent_by_id(project, agent_id)
1677
+ if not agent:
1678
+ raise HTTPException(status_code=404, detail="Agent not found")
1679
+
1680
+ tool_breakdown = get_agent_tool_breakdown(project, agent_id)
1681
+ files_touched = get_agent_files_touched(project, agent_id)
1682
+ parent_agent = get_agent_parent(project, agent_id) if agent.get('type') == 'subagent' else None
1683
+
1684
+ return {
1685
+ "agent": agent,
1686
+ "tool_breakdown": tool_breakdown,
1687
+ "files_touched": files_touched,
1688
+ "parent_agent_id": parent_agent,
1689
+ "adw_phase": None # Will be populated in Part 2 with ADW integration
1690
+ }
1691
+
1692
+
1693
+ # --- ADW Endpoints ---
1694
+
1695
+
1696
+ def scan_adw_folder() -> list[dict]:
1697
+ """Scan agents/ folder for ADW runs."""
1698
+ agents_dir = Path("agents")
1699
+ if not agents_dir.exists():
1700
+ return []
1701
+
1702
+ adw_runs = []
1703
+ for adw_dir in agents_dir.iterdir():
1704
+ if adw_dir.is_dir() and adw_dir.name.startswith("adw_"):
1705
+ state_file = adw_dir / "adw_state.json"
1706
+ if state_file.exists():
1707
+ try:
1708
+ state = json.loads(state_file.read_text())
1709
+ adw_runs.append({
1710
+ "adw_id": state.get("adw_id", adw_dir.name),
1711
+ "created_at": state.get("created_at"),
1712
+ "status": state.get("status", "unknown"),
1713
+ "current_phase": state.get("current_phase", "unknown"),
1714
+ "phases_completed": len(state.get("completed_phases", [])),
1715
+ "phases_total": 4 # plan, build, validate, release
1716
+ })
1717
+ except json.JSONDecodeError:
1718
+ pass
1719
+
1720
+ # Sort by created_at descending
1721
+ adw_runs.sort(key=lambda x: x.get("created_at", ""), reverse=True)
1722
+ return adw_runs
1723
+
1724
+
1725
+ def get_adw_state_with_agents(adw_id: str, db_path: str) -> Optional[dict]:
1726
+ """Get ADW state with correlated agent activity."""
1727
+ adw_dir = Path(f"agents/{adw_id}")
1728
+ state_file = adw_dir / "adw_state.json"
1729
+
1730
+ if not state_file.exists():
1731
+ return None
1732
+
1733
+ state = json.loads(state_file.read_text())
1734
+
1735
+ # Build phase info with agent correlation
1736
+ phases = []
1737
+ all_phases = ["plan", "build", "validate", "release"]
1738
+ completed = state.get("completed_phases", [])
1739
+ current = state.get("current_phase")
1740
+
1741
+ for phase_name in all_phases:
1742
+ phase_dir = adw_dir / phase_name
1743
+
1744
+ # Determine status
1745
+ if phase_name in completed:
1746
+ status = "completed"
1747
+ elif phase_name == current:
1748
+ status = "running"
1749
+ else:
1750
+ status = "pending"
1751
+
1752
+ # Find agents that ran in this phase (from output files)
1753
+ agent_ids = []
1754
+ if phase_dir.exists():
1755
+ for output_file in phase_dir.glob("*_output.jsonl"):
1756
+ agent_name = output_file.stem.replace("_output", "")
1757
+ agent_ids.append(agent_name)
1758
+
1759
+ phases.append({
1760
+ "name": phase_name,
1761
+ "status": status,
1762
+ "agent_ids": agent_ids,
1763
+ "duration_seconds": None # Could be computed from timestamps if needed
1764
+ })
1765
+
1766
+ return {
1767
+ "adw_id": state.get("adw_id", adw_id),
1768
+ "task_description": state.get("task_description", ""),
1769
+ "created_at": state.get("created_at"),
1770
+ "current_phase": current,
1771
+ "completed_phases": completed,
1772
+ "status": state.get("status", "unknown"),
1773
+ "phases": phases,
1774
+ "project_path": state.get("project_path", "")
1775
+ }
1776
+
1777
+
1778
+ @app.get("/api/adw/list")
1779
+ async def list_adw_runs(limit: int = Query(20, ge=1, le=100)):
1780
+ """List all ADW runs from agents/ folder."""
1781
+ adw_runs = scan_adw_folder()[:limit]
1782
+ return {"adw_runs": adw_runs, "count": len(adw_runs)}
1783
+
1784
+
1785
+ @app.get("/api/adw/{adw_id}")
1786
+ async def get_adw_details(
1787
+ adw_id: str,
1788
+ project: str = Query(..., description="Path to the database file"),
1789
+ ):
1790
+ """Get ADW state with agent correlation."""
1791
+ if not Path(project).exists():
1792
+ raise HTTPException(status_code=404, detail="Database not found")
1793
+
1794
+ state = get_adw_state_with_agents(adw_id, project)
1795
+ if not state:
1796
+ raise HTTPException(status_code=404, detail="ADW not found")
1797
+
1798
+ return state
1799
+
1800
+
1624
1801
  # --- Health Check ---
1625
1802
 
1626
1803
 
@@ -384,6 +384,8 @@ class ComposeRequest(BaseModel):
384
384
  template: Optional[str] = None # answer, guide, redirect, acknowledge
385
385
  tone_level: int = Field(default=50, ge=0, le=100) # 0=casual, 100=professional
386
386
  include_memories: bool = Field(default=True)
387
+ custom_instructions: Optional[str] = Field(default=None, max_length=2000)
388
+ include_explanation: bool = Field(default=False)
387
389
 
388
390
 
389
391
  class ComposeResponse(BaseModel):
@@ -398,3 +400,73 @@ class ComposeResponse(BaseModel):
398
400
  incoming_message: str
399
401
  context_type: str
400
402
  created_at: str
403
+ custom_instructions: Optional[str] = None
404
+ explanation: Optional[str] = None
405
+
406
+
407
+ # --- Agent & ADW Models ---
408
+
409
+
410
+ class Agent(BaseModel):
411
+ """Agent from the agents table."""
412
+
413
+ id: str
414
+ name: Optional[str] = None
415
+ type: str # 'main', 'subagent', 'tool'
416
+ first_seen: datetime
417
+ last_seen: datetime
418
+ total_activities: int
419
+ recent_activity_count: int = 0 # Activities in last hour
420
+ is_active: bool = False # Has activity in last 5 minutes
421
+
422
+
423
+ class AgentToolStats(BaseModel):
424
+ """Tool usage breakdown for an agent."""
425
+
426
+ tool_name: str
427
+ count: int
428
+ avg_duration_ms: float
429
+ success_rate: float
430
+
431
+
432
+ class AgentStats(BaseModel):
433
+ """Detailed stats for a single agent."""
434
+
435
+ agent: Agent
436
+ tool_breakdown: list[AgentToolStats]
437
+ files_touched: list[str]
438
+ parent_agent_id: Optional[str] = None # If subagent, who spawned it
439
+ adw_phase: Optional[str] = None # Which ADW phase this agent ran in
440
+
441
+
442
+ class ADWPhaseInfo(BaseModel):
443
+ """Info about a single ADW phase."""
444
+
445
+ name: str # 'plan', 'build', 'validate', 'release'
446
+ status: str # 'pending', 'running', 'completed', 'failed', 'skipped'
447
+ duration_seconds: Optional[float] = None
448
+ agent_ids: list[str] = [] # Agents that ran in this phase
449
+
450
+
451
+ class ADWState(BaseModel):
452
+ """Full ADW state with agent correlation."""
453
+
454
+ adw_id: str
455
+ task_description: str
456
+ created_at: datetime
457
+ current_phase: str
458
+ completed_phases: list[str]
459
+ status: str # 'running', 'completed', 'failed'
460
+ phases: list[ADWPhaseInfo]
461
+ project_path: str
462
+
463
+
464
+ class ADWListItem(BaseModel):
465
+ """Summary for ADW list."""
466
+
467
+ adw_id: str
468
+ created_at: datetime
469
+ status: str
470
+ current_phase: str
471
+ phases_completed: int
472
+ phases_total: int
@@ -1,3 +1,3 @@
1
1
  """Omni Cortex MCP - Universal Memory System for Claude Code."""
2
2
 
3
- __version__ = "1.13.0"
3
+ __version__ = "1.15.0"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "omni-cortex"
7
- version = "1.13.0"
7
+ version = "1.15.0"
8
8
  description = "Give Claude Code a perfect memory - auto-logs everything, searches smartly, and gets smarter over time"
9
9
  readme = "README.md"
10
10
  license = "MIT"