emdash-core 0.1.7__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 (187) hide show
  1. emdash_core/__init__.py +3 -0
  2. emdash_core/agent/__init__.py +37 -0
  3. emdash_core/agent/agents.py +225 -0
  4. emdash_core/agent/code_reviewer.py +476 -0
  5. emdash_core/agent/compaction.py +143 -0
  6. emdash_core/agent/context_manager.py +140 -0
  7. emdash_core/agent/events.py +338 -0
  8. emdash_core/agent/handlers.py +224 -0
  9. emdash_core/agent/inprocess_subagent.py +377 -0
  10. emdash_core/agent/mcp/__init__.py +50 -0
  11. emdash_core/agent/mcp/client.py +346 -0
  12. emdash_core/agent/mcp/config.py +302 -0
  13. emdash_core/agent/mcp/manager.py +496 -0
  14. emdash_core/agent/mcp/tool_factory.py +213 -0
  15. emdash_core/agent/prompts/__init__.py +38 -0
  16. emdash_core/agent/prompts/main_agent.py +104 -0
  17. emdash_core/agent/prompts/subagents.py +131 -0
  18. emdash_core/agent/prompts/workflow.py +136 -0
  19. emdash_core/agent/providers/__init__.py +34 -0
  20. emdash_core/agent/providers/base.py +143 -0
  21. emdash_core/agent/providers/factory.py +80 -0
  22. emdash_core/agent/providers/models.py +220 -0
  23. emdash_core/agent/providers/openai_provider.py +463 -0
  24. emdash_core/agent/providers/transformers_provider.py +217 -0
  25. emdash_core/agent/research/__init__.py +81 -0
  26. emdash_core/agent/research/agent.py +143 -0
  27. emdash_core/agent/research/controller.py +254 -0
  28. emdash_core/agent/research/critic.py +428 -0
  29. emdash_core/agent/research/macros.py +469 -0
  30. emdash_core/agent/research/planner.py +449 -0
  31. emdash_core/agent/research/researcher.py +436 -0
  32. emdash_core/agent/research/state.py +523 -0
  33. emdash_core/agent/research/synthesizer.py +594 -0
  34. emdash_core/agent/reviewer_profile.py +475 -0
  35. emdash_core/agent/rules.py +123 -0
  36. emdash_core/agent/runner.py +601 -0
  37. emdash_core/agent/session.py +262 -0
  38. emdash_core/agent/spec_schema.py +66 -0
  39. emdash_core/agent/specification.py +479 -0
  40. emdash_core/agent/subagent.py +397 -0
  41. emdash_core/agent/subagent_prompts.py +13 -0
  42. emdash_core/agent/toolkit.py +482 -0
  43. emdash_core/agent/toolkits/__init__.py +64 -0
  44. emdash_core/agent/toolkits/base.py +96 -0
  45. emdash_core/agent/toolkits/explore.py +47 -0
  46. emdash_core/agent/toolkits/plan.py +55 -0
  47. emdash_core/agent/tools/__init__.py +141 -0
  48. emdash_core/agent/tools/analytics.py +436 -0
  49. emdash_core/agent/tools/base.py +131 -0
  50. emdash_core/agent/tools/coding.py +484 -0
  51. emdash_core/agent/tools/github_mcp.py +592 -0
  52. emdash_core/agent/tools/history.py +13 -0
  53. emdash_core/agent/tools/modes.py +153 -0
  54. emdash_core/agent/tools/plan.py +206 -0
  55. emdash_core/agent/tools/plan_write.py +135 -0
  56. emdash_core/agent/tools/search.py +412 -0
  57. emdash_core/agent/tools/spec.py +341 -0
  58. emdash_core/agent/tools/task.py +262 -0
  59. emdash_core/agent/tools/task_output.py +204 -0
  60. emdash_core/agent/tools/tasks.py +454 -0
  61. emdash_core/agent/tools/traversal.py +588 -0
  62. emdash_core/agent/tools/web.py +179 -0
  63. emdash_core/analytics/__init__.py +5 -0
  64. emdash_core/analytics/engine.py +1286 -0
  65. emdash_core/api/__init__.py +5 -0
  66. emdash_core/api/agent.py +308 -0
  67. emdash_core/api/agents.py +154 -0
  68. emdash_core/api/analyze.py +264 -0
  69. emdash_core/api/auth.py +173 -0
  70. emdash_core/api/context.py +77 -0
  71. emdash_core/api/db.py +121 -0
  72. emdash_core/api/embed.py +131 -0
  73. emdash_core/api/feature.py +143 -0
  74. emdash_core/api/health.py +93 -0
  75. emdash_core/api/index.py +162 -0
  76. emdash_core/api/plan.py +110 -0
  77. emdash_core/api/projectmd.py +210 -0
  78. emdash_core/api/query.py +320 -0
  79. emdash_core/api/research.py +122 -0
  80. emdash_core/api/review.py +161 -0
  81. emdash_core/api/router.py +76 -0
  82. emdash_core/api/rules.py +116 -0
  83. emdash_core/api/search.py +119 -0
  84. emdash_core/api/spec.py +99 -0
  85. emdash_core/api/swarm.py +223 -0
  86. emdash_core/api/tasks.py +109 -0
  87. emdash_core/api/team.py +120 -0
  88. emdash_core/auth/__init__.py +17 -0
  89. emdash_core/auth/github.py +389 -0
  90. emdash_core/config.py +74 -0
  91. emdash_core/context/__init__.py +52 -0
  92. emdash_core/context/models.py +50 -0
  93. emdash_core/context/providers/__init__.py +11 -0
  94. emdash_core/context/providers/base.py +74 -0
  95. emdash_core/context/providers/explored_areas.py +183 -0
  96. emdash_core/context/providers/touched_areas.py +360 -0
  97. emdash_core/context/registry.py +73 -0
  98. emdash_core/context/reranker.py +199 -0
  99. emdash_core/context/service.py +260 -0
  100. emdash_core/context/session.py +352 -0
  101. emdash_core/core/__init__.py +104 -0
  102. emdash_core/core/config.py +454 -0
  103. emdash_core/core/exceptions.py +55 -0
  104. emdash_core/core/models.py +265 -0
  105. emdash_core/core/review_config.py +57 -0
  106. emdash_core/db/__init__.py +67 -0
  107. emdash_core/db/auth.py +134 -0
  108. emdash_core/db/models.py +91 -0
  109. emdash_core/db/provider.py +222 -0
  110. emdash_core/db/providers/__init__.py +5 -0
  111. emdash_core/db/providers/supabase.py +452 -0
  112. emdash_core/embeddings/__init__.py +24 -0
  113. emdash_core/embeddings/indexer.py +534 -0
  114. emdash_core/embeddings/models.py +192 -0
  115. emdash_core/embeddings/providers/__init__.py +7 -0
  116. emdash_core/embeddings/providers/base.py +112 -0
  117. emdash_core/embeddings/providers/fireworks.py +141 -0
  118. emdash_core/embeddings/providers/openai.py +104 -0
  119. emdash_core/embeddings/registry.py +146 -0
  120. emdash_core/embeddings/service.py +215 -0
  121. emdash_core/graph/__init__.py +26 -0
  122. emdash_core/graph/builder.py +134 -0
  123. emdash_core/graph/connection.py +692 -0
  124. emdash_core/graph/schema.py +416 -0
  125. emdash_core/graph/writer.py +667 -0
  126. emdash_core/ingestion/__init__.py +7 -0
  127. emdash_core/ingestion/change_detector.py +150 -0
  128. emdash_core/ingestion/git/__init__.py +5 -0
  129. emdash_core/ingestion/git/commit_analyzer.py +196 -0
  130. emdash_core/ingestion/github/__init__.py +6 -0
  131. emdash_core/ingestion/github/pr_fetcher.py +296 -0
  132. emdash_core/ingestion/github/task_extractor.py +100 -0
  133. emdash_core/ingestion/orchestrator.py +540 -0
  134. emdash_core/ingestion/parsers/__init__.py +10 -0
  135. emdash_core/ingestion/parsers/base_parser.py +66 -0
  136. emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
  137. emdash_core/ingestion/parsers/class_extractor.py +154 -0
  138. emdash_core/ingestion/parsers/function_extractor.py +202 -0
  139. emdash_core/ingestion/parsers/import_analyzer.py +119 -0
  140. emdash_core/ingestion/parsers/python_parser.py +123 -0
  141. emdash_core/ingestion/parsers/registry.py +72 -0
  142. emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
  143. emdash_core/ingestion/parsers/typescript_parser.py +278 -0
  144. emdash_core/ingestion/repository.py +346 -0
  145. emdash_core/models/__init__.py +38 -0
  146. emdash_core/models/agent.py +68 -0
  147. emdash_core/models/index.py +77 -0
  148. emdash_core/models/query.py +113 -0
  149. emdash_core/planning/__init__.py +7 -0
  150. emdash_core/planning/agent_api.py +413 -0
  151. emdash_core/planning/context_builder.py +265 -0
  152. emdash_core/planning/feature_context.py +232 -0
  153. emdash_core/planning/feature_expander.py +646 -0
  154. emdash_core/planning/llm_explainer.py +198 -0
  155. emdash_core/planning/similarity.py +509 -0
  156. emdash_core/planning/team_focus.py +821 -0
  157. emdash_core/server.py +153 -0
  158. emdash_core/sse/__init__.py +5 -0
  159. emdash_core/sse/stream.py +196 -0
  160. emdash_core/swarm/__init__.py +17 -0
  161. emdash_core/swarm/merge_agent.py +383 -0
  162. emdash_core/swarm/session_manager.py +274 -0
  163. emdash_core/swarm/swarm_runner.py +226 -0
  164. emdash_core/swarm/task_definition.py +137 -0
  165. emdash_core/swarm/worker_spawner.py +319 -0
  166. emdash_core/swarm/worktree_manager.py +278 -0
  167. emdash_core/templates/__init__.py +10 -0
  168. emdash_core/templates/defaults/agent-builder.md.template +82 -0
  169. emdash_core/templates/defaults/focus.md.template +115 -0
  170. emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
  171. emdash_core/templates/defaults/pr-review.md.template +80 -0
  172. emdash_core/templates/defaults/project.md.template +85 -0
  173. emdash_core/templates/defaults/research_critic.md.template +112 -0
  174. emdash_core/templates/defaults/research_planner.md.template +85 -0
  175. emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
  176. emdash_core/templates/defaults/reviewer.md.template +81 -0
  177. emdash_core/templates/defaults/spec.md.template +41 -0
  178. emdash_core/templates/defaults/tasks.md.template +78 -0
  179. emdash_core/templates/loader.py +296 -0
  180. emdash_core/utils/__init__.py +45 -0
  181. emdash_core/utils/git.py +84 -0
  182. emdash_core/utils/image.py +502 -0
  183. emdash_core/utils/logger.py +51 -0
  184. emdash_core-0.1.7.dist-info/METADATA +35 -0
  185. emdash_core-0.1.7.dist-info/RECORD +187 -0
  186. emdash_core-0.1.7.dist-info/WHEEL +4 -0
  187. emdash_core-0.1.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,199 @@
1
+ """Re-ranker for filtering context items by query relevance.
2
+
3
+ Uses a cross-encoder model to score context items against the current query,
4
+ keeping only the most relevant items to save tokens in the LLM context.
5
+ """
6
+
7
+ import os
8
+ from typing import Optional
9
+
10
+ # Disable tokenizers parallelism to avoid fork warnings when running in threads
11
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
12
+
13
+ from .models import ContextItem
14
+ from ..utils.logger import log
15
+
16
+ # Model singleton to avoid reloading
17
+ _reranker_model = None
18
+ _model_load_attempted = False
19
+
20
+
21
+ def get_reranker_model():
22
+ """Get or load the re-ranker model (singleton).
23
+
24
+ Returns:
25
+ CrossEncoder model or None if not available
26
+ """
27
+ global _reranker_model, _model_load_attempted
28
+
29
+ if _model_load_attempted:
30
+ return _reranker_model
31
+
32
+ _model_load_attempted = True
33
+
34
+ # Check if re-ranking is enabled
35
+ if os.getenv("CONTEXT_RERANK_ENABLED", "true").lower() != "true":
36
+ log.debug("Context re-ranking disabled via CONTEXT_RERANK_ENABLED")
37
+ return None
38
+
39
+ try:
40
+ from sentence_transformers import CrossEncoder
41
+
42
+ model_name = os.getenv(
43
+ "CONTEXT_RERANK_MODEL", "mixedbread-ai/mxbai-rerank-xsmall-v1"
44
+ )
45
+ log.info(f"Loading re-ranker model: {model_name}")
46
+ _reranker_model = CrossEncoder(model_name)
47
+ log.info("Re-ranker model loaded successfully")
48
+ return _reranker_model
49
+ except ImportError:
50
+ log.warning("sentence-transformers not installed, re-ranking disabled")
51
+ return None
52
+ except Exception as e:
53
+ log.warning(f"Failed to load re-ranker model: {e}")
54
+ return None
55
+
56
+
57
+ def item_to_text(item: ContextItem) -> str:
58
+ """Convert a ContextItem to text for re-ranking.
59
+
60
+ Args:
61
+ item: Context item to convert
62
+
63
+ Returns:
64
+ Text representation for scoring
65
+ """
66
+ parts = [item.qualified_name]
67
+
68
+ if item.entity_type:
69
+ parts.append(f"({item.entity_type})")
70
+
71
+ if item.description:
72
+ parts.append(f": {item.description[:200]}")
73
+
74
+ if item.file_path:
75
+ # Just include the filename, not full path
76
+ filename = os.path.basename(item.file_path)
77
+ parts.append(f" [file: {filename}]")
78
+
79
+ return " ".join(parts)
80
+
81
+
82
+ def rerank_context_items(
83
+ items: list[ContextItem],
84
+ query: str,
85
+ top_k: Optional[int] = None,
86
+ top_percent: Optional[float] = None,
87
+ ) -> list[ContextItem]:
88
+ """Re-rank context items by relevance to query.
89
+
90
+ Uses a cross-encoder model to score each item against the query,
91
+ then returns the top K or top N% most relevant items.
92
+
93
+ Args:
94
+ items: List of context items to re-rank
95
+ query: The user's query/task description
96
+ top_k: Keep top K items (default from env: CONTEXT_RERANK_TOP_K=20)
97
+ top_percent: Keep top N% items (overrides top_k if set)
98
+
99
+ Returns:
100
+ Filtered and sorted list of context items (most relevant first)
101
+ """
102
+ import time
103
+
104
+ original_count = len(items)
105
+
106
+ if not items:
107
+ return items
108
+
109
+ if not query or not query.strip():
110
+ log.debug("No query provided for re-ranking, returning original items")
111
+ return items
112
+
113
+ model = get_reranker_model()
114
+ if model is None:
115
+ log.debug("Re-ranker model not available, returning original items")
116
+ return items
117
+
118
+ try:
119
+ start_time = time.time()
120
+
121
+ # Convert items to text for scoring
122
+ texts = [item_to_text(item) for item in items]
123
+
124
+ # Create query-document pairs
125
+ pairs = [(query, text) for text in texts]
126
+
127
+ # Score all pairs
128
+ scores = model.predict(pairs)
129
+
130
+ # Combine items with scores
131
+ scored_items = list(zip(items, scores))
132
+
133
+ # Sort by score descending
134
+ scored_items.sort(key=lambda x: x[1], reverse=True)
135
+
136
+ # Determine how many to keep
137
+ if top_percent is not None:
138
+ keep_count = max(1, int(len(items) * top_percent))
139
+ elif top_k is not None:
140
+ keep_count = min(top_k, len(items))
141
+ else:
142
+ # Default from environment
143
+ default_top_k = int(os.getenv("CONTEXT_RERANK_TOP_K", "20"))
144
+ keep_count = min(default_top_k, len(items))
145
+
146
+ duration_ms = (time.time() - start_time) * 1000
147
+
148
+ # Log statistics
149
+ if scored_items:
150
+ max_score = scored_items[0][1]
151
+ min_score = scored_items[-1][1]
152
+ filtered_count = original_count - keep_count
153
+ log.info(
154
+ f"Re-ranked context: {original_count} -> {keep_count} items "
155
+ f"(filtered {filtered_count}) in {duration_ms:.0f}ms | "
156
+ f"scores [{min_score:.3f}-{max_score:.3f}] | "
157
+ f"query: '{query[:40]}...'"
158
+ )
159
+
160
+ # Return top items (without scores)
161
+ return [item for item, score in scored_items[:keep_count]]
162
+
163
+ except Exception as e:
164
+ log.warning(f"Re-ranking failed: {e}, returning original items")
165
+ return items
166
+
167
+
168
+ def get_rerank_scores(
169
+ items: list[ContextItem], query: str
170
+ ) -> list[tuple[ContextItem, float]]:
171
+ """Get re-rank scores for context items without filtering.
172
+
173
+ Useful for debugging and analysis.
174
+
175
+ Args:
176
+ items: List of context items
177
+ query: Query to score against
178
+
179
+ Returns:
180
+ List of (item, score) tuples sorted by score descending
181
+ """
182
+ if not items or not query:
183
+ return [(item, 0.0) for item in items]
184
+
185
+ model = get_reranker_model()
186
+ if model is None:
187
+ return [(item, 0.0) for item in items]
188
+
189
+ try:
190
+ texts = [item_to_text(item) for item in items]
191
+ pairs = [(query, text) for text in texts]
192
+ scores = model.predict(pairs)
193
+
194
+ scored = list(zip(items, scores))
195
+ scored.sort(key=lambda x: x[1], reverse=True)
196
+ return scored
197
+ except Exception as e:
198
+ log.warning(f"Failed to get rerank scores: {e}")
199
+ return [(item, 0.0) for item in items]
@@ -0,0 +1,260 @@
1
+ """Context service - facade over providers and session management."""
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from .models import ContextItem
9
+ from .reranker import rerank_context_items
10
+ from .registry import ContextProviderRegistry
11
+ from .session import SessionContextManager
12
+ from ..graph.connection import KuzuConnection, get_connection
13
+ from ..utils.logger import log
14
+
15
+
16
+ class ContextService:
17
+ """High-level service for managing session context.
18
+
19
+ Provides a unified interface for:
20
+ - Detecting modified files (via git diff)
21
+ - Extracting context from providers
22
+ - Managing session persistence
23
+ - Formatting context for LLM prompts
24
+ """
25
+
26
+ def __init__(self, connection: Optional[KuzuConnection] = None, repo_root: Optional[str] = None):
27
+ """Initialize context service.
28
+
29
+ Args:
30
+ connection: Kuzu database connection (uses global if not provided)
31
+ repo_root: Repository root path for git operations
32
+ """
33
+ self.connection = connection or get_connection()
34
+ self.repo_root = repo_root or os.getcwd()
35
+ self.session_manager = SessionContextManager(self.connection)
36
+ self._providers: Optional[list[str]] = None
37
+ self._min_score = float(os.getenv("CONTEXT_MIN_SCORE", "0.5"))
38
+ self._max_items = int(os.getenv("CONTEXT_MAX_ITEMS", "50"))
39
+
40
+ @property
41
+ def providers(self) -> list[str]:
42
+ """Get list of enabled provider names from config."""
43
+ if self._providers is None:
44
+ env_val = os.getenv("CONTEXT_PROVIDERS", "touched_areas,explored_areas")
45
+ self._providers = [p.strip() for p in env_val.split(",") if p.strip()]
46
+ return self._providers
47
+
48
+ def detect_modified_files(self) -> list[str]:
49
+ """Detect files modified since last commit.
50
+
51
+ Uses git diff to find modified files.
52
+
53
+ Returns:
54
+ List of modified file paths (absolute)
55
+ """
56
+ try:
57
+ # Get unstaged changes
58
+ result = subprocess.run(
59
+ ["git", "diff", "--name-only"],
60
+ capture_output=True,
61
+ text=True,
62
+ cwd=self.repo_root,
63
+ timeout=10,
64
+ )
65
+
66
+ files = []
67
+ if result.returncode == 0 and result.stdout.strip():
68
+ files.extend(result.stdout.strip().split("\n"))
69
+
70
+ # Also get staged changes
71
+ result_staged = subprocess.run(
72
+ ["git", "diff", "--name-only", "--cached"],
73
+ capture_output=True,
74
+ text=True,
75
+ cwd=self.repo_root,
76
+ timeout=10,
77
+ )
78
+
79
+ if result_staged.returncode == 0 and result_staged.stdout.strip():
80
+ files.extend(result_staged.stdout.strip().split("\n"))
81
+
82
+ # Convert to absolute paths and deduplicate
83
+ abs_files = []
84
+ seen = set()
85
+ for f in files:
86
+ if f and f not in seen:
87
+ seen.add(f)
88
+ abs_path = os.path.join(self.repo_root, f)
89
+ if os.path.exists(abs_path):
90
+ abs_files.append(abs_path)
91
+
92
+ return abs_files
93
+
94
+ except subprocess.TimeoutExpired:
95
+ log.warning("Git diff timed out")
96
+ return []
97
+ except FileNotFoundError:
98
+ log.warning("Git not found")
99
+ return []
100
+ except Exception as e:
101
+ log.warning(f"Failed to detect modified files: {e}")
102
+ return []
103
+
104
+ def update_context(
105
+ self,
106
+ terminal_id: str,
107
+ modified_files: Optional[list[str]] = None,
108
+ exploration_steps: Optional[list] = None,
109
+ ):
110
+ """Update session context after changes.
111
+
112
+ Args:
113
+ terminal_id: Terminal session identifier
114
+ modified_files: List of modified files (auto-detected if not provided)
115
+ exploration_steps: List of ExplorationStep objects from AgentSession
116
+ """
117
+ if modified_files is None:
118
+ modified_files = self.detect_modified_files()
119
+
120
+ # Get or create session
121
+ session = self.session_manager.get_or_create_session(terminal_id)
122
+
123
+ # Extract context from all enabled providers
124
+ all_items = []
125
+ for provider_name in self.providers:
126
+ try:
127
+ # Import providers to ensure registration
128
+ from .providers import explored_areas, touched_areas # noqa: F401
129
+
130
+ provider = ContextProviderRegistry.get_provider(provider_name, self.connection)
131
+
132
+ # Different providers need different inputs
133
+ if provider_name == "touched_areas" and modified_files:
134
+ items = provider.extract_context(modified_files)
135
+ elif provider_name == "explored_areas" and exploration_steps:
136
+ items = provider.extract_context(exploration_steps)
137
+ else:
138
+ # Skip provider if no relevant input
139
+ log.debug(f"Skipping provider '{provider_name}' - no relevant input")
140
+ continue
141
+
142
+ all_items.extend(items)
143
+ log.debug(f"Provider '{provider_name}' extracted {len(items)} items")
144
+ except Exception as e:
145
+ log.warning(f"Provider '{provider_name}' failed: {e}")
146
+
147
+ # Add items to session
148
+ if all_items:
149
+ self.session_manager.add_context_items(session.session_id, all_items)
150
+ else:
151
+ log.debug("No context items extracted from any provider")
152
+
153
+ def get_context_prompt(self, terminal_id: str, query: Optional[str] = None) -> str:
154
+ """Get formatted context for LLM system prompt.
155
+
156
+ Args:
157
+ terminal_id: Terminal session identifier
158
+ query: Optional query to re-rank context by relevance
159
+
160
+ Returns:
161
+ Formatted context string for system prompt
162
+ """
163
+ session = self.session_manager.get_or_create_session(terminal_id)
164
+ items = self.session_manager.get_context(session.session_id, self._min_score)
165
+
166
+ if not items:
167
+ return ""
168
+
169
+ # Re-rank by query relevance if query provided
170
+ if query:
171
+ items = rerank_context_items(items, query, top_k=self._max_items)
172
+ else:
173
+ # Limit number of items without re-ranking
174
+ items = items[: self._max_items]
175
+
176
+ # Deduplicate by file_path + qualified_name to avoid repetition
177
+ seen_keys = set()
178
+ unique_items = []
179
+ for item in items:
180
+ # Create unique key from file path and qualified name
181
+ key = (item.file_path or "", item.qualified_name)
182
+ if key not in seen_keys:
183
+ seen_keys.add(key)
184
+ unique_items.append(item)
185
+ items = unique_items
186
+
187
+ # Format as markdown
188
+ lines = [
189
+ "## Session Context",
190
+ "",
191
+ "The following code entities were recently modified or are related to recent changes:",
192
+ "",
193
+ ]
194
+
195
+ for item in items:
196
+ score_indicator = "***" if item.score > 0.8 else "**" if item.score > 0.5 else "*"
197
+ lines.append(f"### {score_indicator}{item.entity_type}: {item.qualified_name}{score_indicator}")
198
+
199
+ if item.file_path:
200
+ # Show relative path if possible
201
+ try:
202
+ rel_path = os.path.relpath(item.file_path, self.repo_root)
203
+ lines.append(f"- File: `{rel_path}`")
204
+ except ValueError:
205
+ lines.append(f"- File: `{item.file_path}`")
206
+
207
+ if item.description:
208
+ # Truncate long descriptions
209
+ desc = item.description.strip()
210
+ if len(desc) > 200:
211
+ desc = desc[:197] + "..."
212
+ lines.append(f"- Description: {desc}")
213
+
214
+ if item.neighbors:
215
+ neighbor_str = ", ".join(f"`{n}`" for n in item.neighbors[:5])
216
+ if len(item.neighbors) > 5:
217
+ neighbor_str += f" (+{len(item.neighbors) - 5} more)"
218
+ lines.append(f"- Related: {neighbor_str}")
219
+
220
+ lines.append("")
221
+
222
+ return "\n".join(lines)
223
+
224
+ def get_context_items(self, terminal_id: str) -> list[ContextItem]:
225
+ """Get raw context items for a session.
226
+
227
+ Args:
228
+ terminal_id: Terminal session identifier
229
+
230
+ Returns:
231
+ List of context items
232
+ """
233
+ session = self.session_manager.get_or_create_session(terminal_id)
234
+ return self.session_manager.get_context(session.session_id, self._min_score)
235
+
236
+ def clear_context(self, terminal_id: str):
237
+ """Clear all context for a session.
238
+
239
+ Args:
240
+ terminal_id: Terminal session identifier
241
+ """
242
+ session = self.session_manager.get_or_create_session(terminal_id)
243
+ self.session_manager.clear_session(session.session_id)
244
+
245
+ @staticmethod
246
+ def get_terminal_id() -> str:
247
+ """Get or generate terminal ID.
248
+
249
+ Uses EMDASH_TERMINAL_ID env var or generates a new one.
250
+
251
+ Returns:
252
+ Terminal ID string
253
+ """
254
+ import uuid
255
+
256
+ terminal_id = os.getenv("EMDASH_TERMINAL_ID")
257
+ if not terminal_id:
258
+ terminal_id = str(uuid.uuid4())
259
+ os.environ["EMDASH_TERMINAL_ID"] = terminal_id
260
+ return terminal_id