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,140 @@
1
+ """Context length management for agent conversations."""
2
+
3
+ from typing import Optional
4
+
5
+ from ..core.config import get_config
6
+ from ..utils.logger import log
7
+
8
+
9
+ def estimate_tokens(text: str) -> int:
10
+ """Estimate token count from text.
11
+
12
+ Uses ~4 characters per token as a rough estimate.
13
+ This is conservative for English text and code.
14
+
15
+ Args:
16
+ text: Text to estimate tokens for
17
+
18
+ Returns:
19
+ Estimated token count
20
+ """
21
+ if not text:
22
+ return 0
23
+ return len(text) // 4
24
+
25
+
26
+ def truncate_tool_output(output: str, max_tokens: Optional[int] = None) -> str:
27
+ """Truncate tool output to stay within token limit.
28
+
29
+ Args:
30
+ output: Tool output string (usually JSON)
31
+ max_tokens: Maximum tokens (uses config default if None)
32
+
33
+ Returns:
34
+ Truncated output with indicator if truncated
35
+ """
36
+ config = get_config()
37
+ max_tokens = max_tokens or config.agent.tool_max_output_tokens
38
+ max_chars = max_tokens * 4 # Reverse the ~4 chars/token estimate
39
+
40
+ if len(output) <= max_chars:
41
+ return output
42
+
43
+ # Truncate and add indicator
44
+ truncated = output[: max_chars - 150] # Leave room for truncation message
45
+ chars_removed = len(output) - len(truncated)
46
+ lines_removed = output[max_chars - 150 :].count("\n")
47
+
48
+ truncation_msg = (
49
+ f"\n\n[OUTPUT TRUNCATED: ~{lines_removed} lines omitted, "
50
+ f"{chars_removed} chars removed to fit {max_tokens} token limit]"
51
+ )
52
+
53
+ log.warning(
54
+ "Truncated tool output from {} to {} chars (~{} tokens)",
55
+ len(output),
56
+ len(truncated),
57
+ max_tokens,
58
+ )
59
+
60
+ return truncated + truncation_msg
61
+
62
+
63
+ def reduce_context_for_retry(
64
+ messages: list[dict],
65
+ keep_recent: int = 4,
66
+ ) -> list[dict]:
67
+ """Reduce context by removing old messages.
68
+
69
+ Keeps the first user message and the most recent messages.
70
+
71
+ Args:
72
+ messages: Current messages list
73
+ keep_recent: Number of recent messages to keep
74
+
75
+ Returns:
76
+ Reduced messages list
77
+ """
78
+ if len(messages) <= keep_recent + 1:
79
+ return messages # Can't reduce further
80
+
81
+ # Always keep first message (initial user query)
82
+ first_msg = messages[0]
83
+
84
+ # Keep the most recent messages
85
+ recent_msgs = messages[-keep_recent:]
86
+
87
+ reduced = [first_msg] + recent_msgs
88
+
89
+ log.info(
90
+ "Reduced context from {} to {} messages for retry",
91
+ len(messages),
92
+ len(reduced),
93
+ )
94
+
95
+ return reduced
96
+
97
+
98
+ def is_context_overflow_error(exc: Exception) -> bool:
99
+ """Check if an exception is a context length overflow error.
100
+
101
+ Handles different error formats from various providers:
102
+ - OpenAI: "context_length_exceeded" code
103
+ - Anthropic: "prompt is too long" message
104
+ - Fireworks: Similar to OpenAI format
105
+
106
+ Args:
107
+ exc: Exception to check
108
+
109
+ Returns:
110
+ True if this is a context overflow error
111
+ """
112
+ error_str = str(exc).lower()
113
+
114
+ # Check common error patterns
115
+ overflow_patterns = [
116
+ "context_length_exceeded",
117
+ "context length",
118
+ "maximum context length",
119
+ "prompt is too long",
120
+ "too many tokens",
121
+ "token limit",
122
+ "exceeds the model's maximum",
123
+ "max_tokens",
124
+ ]
125
+
126
+ for pattern in overflow_patterns:
127
+ if pattern in error_str:
128
+ return True
129
+
130
+ # Check for specific error codes
131
+ if hasattr(exc, "code"):
132
+ if exc.code == "context_length_exceeded":
133
+ return True
134
+
135
+ # Check HTTP status (400 often used for this)
136
+ if hasattr(exc, "status_code"):
137
+ if exc.status_code == 400 and any(p in error_str for p in overflow_patterns):
138
+ return True
139
+
140
+ return False
@@ -0,0 +1,338 @@
1
+ """Unified event stream for agent operations.
2
+
3
+ This module provides a centralized event system that both CLI and UI can consume,
4
+ ensuring consistent message handling across interfaces.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from enum import Enum
10
+ from typing import Any, Protocol
11
+
12
+
13
+ class EventType(Enum):
14
+ """Types of events emitted by agents."""
15
+
16
+ # Tool lifecycle
17
+ TOOL_START = "tool_start"
18
+ TOOL_RESULT = "tool_result"
19
+
20
+ # Agent thinking/progress
21
+ THINKING = "thinking"
22
+ PROGRESS = "progress"
23
+
24
+ # Output
25
+ RESPONSE = "response"
26
+ PARTIAL_RESPONSE = "partial_response"
27
+
28
+ # Interaction
29
+ CLARIFICATION = "clarification"
30
+ CLARIFICATION_RESPONSE = "clarification_response"
31
+
32
+ # Errors
33
+ ERROR = "error"
34
+ WARNING = "warning"
35
+
36
+ # Session
37
+ SESSION_START = "session_start"
38
+ SESSION_END = "session_end"
39
+
40
+ # Context
41
+ CONTEXT_FRAME = "context_frame"
42
+
43
+
44
+ @dataclass
45
+ class AgentEvent:
46
+ """A single event emitted by an agent.
47
+
48
+ Attributes:
49
+ type: The type of event
50
+ data: Event-specific data payload
51
+ timestamp: When the event occurred
52
+ agent_name: Optional name of the agent that emitted this event
53
+ """
54
+ type: EventType
55
+ data: dict[str, Any]
56
+ timestamp: datetime = field(default_factory=datetime.now)
57
+ agent_name: str | None = None
58
+
59
+ def to_dict(self) -> dict[str, Any]:
60
+ """Convert to dictionary for JSON serialization."""
61
+ return {
62
+ "type": self.type.value,
63
+ "data": self.data,
64
+ "timestamp": self.timestamp.isoformat(),
65
+ "agent_name": self.agent_name,
66
+ }
67
+
68
+
69
+ class EventHandler(Protocol):
70
+ """Protocol for event handlers."""
71
+
72
+ def handle(self, event: AgentEvent) -> None:
73
+ """Handle an emitted event."""
74
+ ...
75
+
76
+
77
+ class AgentEventEmitter:
78
+ """Emits and stores agent events for consumption by handlers.
79
+
80
+ This is the central hub for the event stream. Agents emit events here,
81
+ and handlers (CLI Rich renderer, JSON streamer, etc.) subscribe to receive them.
82
+
83
+ Example:
84
+ emitter = AgentEventEmitter()
85
+ emitter.add_handler(RichConsoleHandler())
86
+
87
+ # In agent code:
88
+ emitter.emit(EventType.TOOL_START, {"name": "semantic_search", "args": {...}})
89
+ result = execute_tool(...)
90
+ emitter.emit(EventType.TOOL_RESULT, {"name": "semantic_search", "success": True})
91
+ """
92
+
93
+ def __init__(self, agent_name: str | None = None):
94
+ """Initialize the emitter.
95
+
96
+ Args:
97
+ agent_name: Optional name to tag all events from this emitter
98
+ """
99
+ self._handlers: list[EventHandler] = []
100
+ self._events: list[AgentEvent] = []
101
+ self._agent_name = agent_name
102
+
103
+ def add_handler(self, handler: EventHandler) -> None:
104
+ """Add a handler to receive events.
105
+
106
+ Args:
107
+ handler: Handler that implements the EventHandler protocol
108
+ """
109
+ self._handlers.append(handler)
110
+
111
+ def remove_handler(self, handler: EventHandler) -> None:
112
+ """Remove a handler.
113
+
114
+ Args:
115
+ handler: Handler to remove
116
+ """
117
+ if handler in self._handlers:
118
+ self._handlers.remove(handler)
119
+
120
+ def emit(self, event_type: EventType, data: dict[str, Any] | None = None) -> AgentEvent:
121
+ """Emit an event to all handlers.
122
+
123
+ Args:
124
+ event_type: Type of event to emit
125
+ data: Event-specific data payload
126
+
127
+ Returns:
128
+ The created AgentEvent
129
+ """
130
+ event = AgentEvent(
131
+ type=event_type,
132
+ data=data or {},
133
+ agent_name=self._agent_name,
134
+ )
135
+ self._events.append(event)
136
+
137
+ for handler in self._handlers:
138
+ try:
139
+ handler.handle(event)
140
+ except Exception:
141
+ # Don't let handler errors break the agent
142
+ pass
143
+
144
+ return event
145
+
146
+ def emit_tool_start(self, name: str, args: dict[str, Any] | None = None) -> AgentEvent:
147
+ """Convenience method to emit a tool start event.
148
+
149
+ Args:
150
+ name: Tool name
151
+ args: Tool arguments
152
+ """
153
+ return self.emit(EventType.TOOL_START, {
154
+ "name": name,
155
+ "args": args or {},
156
+ })
157
+
158
+ def emit_tool_result(
159
+ self,
160
+ name: str,
161
+ success: bool,
162
+ summary: str | None = None,
163
+ data: dict[str, Any] | None = None,
164
+ ) -> AgentEvent:
165
+ """Convenience method to emit a tool result event.
166
+
167
+ Args:
168
+ name: Tool name
169
+ success: Whether the tool succeeded
170
+ summary: Brief summary of the result
171
+ data: Full result data (may be truncated by handlers)
172
+ """
173
+ return self.emit(EventType.TOOL_RESULT, {
174
+ "name": name,
175
+ "success": success,
176
+ "summary": summary,
177
+ "data": data,
178
+ })
179
+
180
+ def emit_thinking(self, message: str) -> AgentEvent:
181
+ """Convenience method to emit a thinking/progress message.
182
+
183
+ Args:
184
+ message: What the agent is thinking/doing
185
+ """
186
+ return self.emit(EventType.THINKING, {"message": message})
187
+
188
+ def emit_progress(self, message: str, percent: float | None = None) -> AgentEvent:
189
+ """Convenience method to emit a progress update.
190
+
191
+ Args:
192
+ message: Progress message
193
+ percent: Optional completion percentage (0-100)
194
+ """
195
+ return self.emit(EventType.PROGRESS, {
196
+ "message": message,
197
+ "percent": percent,
198
+ })
199
+
200
+ def emit_response(self, content: str, is_final: bool = True) -> AgentEvent:
201
+ """Convenience method to emit a response.
202
+
203
+ Args:
204
+ content: Response content (usually markdown)
205
+ is_final: Whether this is the final response
206
+ """
207
+ event_type = EventType.RESPONSE if is_final else EventType.PARTIAL_RESPONSE
208
+ return self.emit(event_type, {"content": content})
209
+
210
+ def emit_clarification(
211
+ self,
212
+ question: str,
213
+ context: str | None = None,
214
+ options: list[str] | None = None,
215
+ ) -> AgentEvent:
216
+ """Convenience method to emit a clarification request.
217
+
218
+ Args:
219
+ question: The question to ask
220
+ context: Why we're asking
221
+ options: Suggested answers
222
+ """
223
+ return self.emit(EventType.CLARIFICATION, {
224
+ "question": question,
225
+ "context": context,
226
+ "options": options,
227
+ })
228
+
229
+ def emit_error(self, message: str, details: str | None = None) -> AgentEvent:
230
+ """Convenience method to emit an error.
231
+
232
+ Args:
233
+ message: Error message
234
+ details: Additional details (stack trace, etc.)
235
+ """
236
+ return self.emit(EventType.ERROR, {
237
+ "message": message,
238
+ "details": details,
239
+ })
240
+
241
+ def emit_start(self, goal: str, **kwargs) -> AgentEvent:
242
+ """Convenience method to emit a session start event.
243
+
244
+ Args:
245
+ goal: The goal/query for this session
246
+ **kwargs: Additional data to include
247
+ """
248
+ return self.emit(EventType.SESSION_START, {
249
+ "goal": goal,
250
+ **kwargs,
251
+ })
252
+
253
+ def emit_end(self, success: bool = True, **kwargs) -> AgentEvent:
254
+ """Convenience method to emit a session end event.
255
+
256
+ Args:
257
+ success: Whether the session completed successfully
258
+ **kwargs: Additional data to include
259
+ """
260
+ return self.emit(EventType.SESSION_END, {
261
+ "success": success,
262
+ **kwargs,
263
+ })
264
+
265
+ def emit_context_frame(
266
+ self,
267
+ adding: dict[str, Any] | None = None,
268
+ reading: dict[str, Any] | None = None,
269
+ ) -> AgentEvent:
270
+ """Convenience method to emit a context frame update.
271
+
272
+ Args:
273
+ adding: What's being added to context (modified_files, exploration_steps, tokens)
274
+ reading: What's being read from context (items with scores, tokens)
275
+ """
276
+ return self.emit(EventType.CONTEXT_FRAME, {
277
+ "adding": adding or {},
278
+ "reading": reading or {},
279
+ })
280
+
281
+ def emit_message_start(self) -> AgentEvent:
282
+ """Convenience method to emit message start event."""
283
+ self._accumulated_content = ""
284
+ return self.emit(EventType.PARTIAL_RESPONSE, {"status": "start"})
285
+
286
+ def emit_message_delta(self, content: str) -> AgentEvent:
287
+ """Convenience method to emit message delta (streaming content).
288
+
289
+ Args:
290
+ content: The content chunk to stream
291
+ """
292
+ if hasattr(self, '_accumulated_content'):
293
+ self._accumulated_content += content
294
+ return self.emit(EventType.PARTIAL_RESPONSE, {"content": content})
295
+
296
+ def emit_message_end(self) -> AgentEvent:
297
+ """Convenience method to emit message end event with accumulated content."""
298
+ content = getattr(self, '_accumulated_content', "")
299
+ return self.emit(EventType.RESPONSE, {"content": content})
300
+
301
+ def get_events(self) -> list[AgentEvent]:
302
+ """Get all events emitted so far.
303
+
304
+ Returns:
305
+ Copy of the events list
306
+ """
307
+ return self._events.copy()
308
+
309
+ def clear_events(self) -> None:
310
+ """Clear the events history."""
311
+ self._events.clear()
312
+
313
+
314
+ # Default no-op emitter for backwards compatibility
315
+ class NullEmitter(AgentEventEmitter):
316
+ """An emitter that does nothing - for backwards compatibility."""
317
+
318
+ def emit(self, event_type: EventType, data: dict[str, Any] | None = None) -> AgentEvent:
319
+ """Create event but don't store or dispatch it."""
320
+ return AgentEvent(type=event_type, data=data or {}, agent_name=self._agent_name)
321
+
322
+
323
+ # Global default emitter (can be replaced)
324
+ _default_emitter: AgentEventEmitter | None = None
325
+
326
+
327
+ def get_default_emitter() -> AgentEventEmitter:
328
+ """Get the default global emitter."""
329
+ global _default_emitter
330
+ if _default_emitter is None:
331
+ _default_emitter = NullEmitter()
332
+ return _default_emitter
333
+
334
+
335
+ def set_default_emitter(emitter: AgentEventEmitter) -> None:
336
+ """Set the default global emitter."""
337
+ global _default_emitter
338
+ _default_emitter = emitter
@@ -0,0 +1,224 @@
1
+ """Event handlers for agent output.
2
+
3
+ Provides various handler implementations for routing agent events
4
+ to different destinations.
5
+ """
6
+
7
+ import json
8
+ import sys
9
+ from typing import IO, Optional
10
+
11
+ from rich.console import Console
12
+ from rich.live import Live
13
+ from rich.markdown import Markdown
14
+ from rich.panel import Panel
15
+ from rich.spinner import Spinner
16
+ from rich.text import Text
17
+
18
+ from .events import AgentEvent, AgentEventEmitter, EventType
19
+
20
+
21
+ class RichConsoleHandler(AgentEventEmitter):
22
+ """Handler that renders events to the console using Rich.
23
+
24
+ Provides a nice interactive display with spinners, panels,
25
+ and formatted output.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ agent_name: str = "Agent",
31
+ console: Optional[Console] = None,
32
+ show_thinking: bool = True,
33
+ ):
34
+ """Initialize the console handler.
35
+
36
+ Args:
37
+ agent_name: Name to display for the agent
38
+ console: Rich console instance
39
+ show_thinking: Whether to show thinking blocks
40
+ """
41
+ super().__init__(agent_name)
42
+ self.console = console or Console()
43
+ self.show_thinking = show_thinking
44
+ self._current_message = ""
45
+ self._current_thinking = ""
46
+ self._in_message = False
47
+ self._in_thinking = False
48
+
49
+ def _handle(self, event: AgentEvent) -> None:
50
+ """Handle an event by rendering to console."""
51
+ if event.type == EventType.AGENT_START:
52
+ goal = event.data.get("goal", "")
53
+ self.console.print(
54
+ Panel(
55
+ f"[bold cyan]{self.agent_name}[/bold cyan] starting...\n"
56
+ f"[dim]Goal: {goal}[/dim]",
57
+ border_style="cyan",
58
+ )
59
+ )
60
+
61
+ elif event.type == EventType.AGENT_END:
62
+ success = event.data.get("success", True)
63
+ if success:
64
+ self.console.print("[green]Agent completed successfully[/green]")
65
+ else:
66
+ self.console.print("[red]Agent finished with errors[/red]")
67
+
68
+ elif event.type == EventType.AGENT_ERROR:
69
+ error = event.data.get("error", "Unknown error")
70
+ self.console.print(f"[red bold]Error:[/red bold] {error}")
71
+
72
+ elif event.type == EventType.MESSAGE_START:
73
+ self._in_message = True
74
+ self._current_message = ""
75
+
76
+ elif event.type == EventType.MESSAGE_DELTA:
77
+ content = event.data.get("content", "")
78
+ self._current_message += content
79
+ # Print incrementally
80
+ self.console.print(content, end="")
81
+
82
+ elif event.type == EventType.MESSAGE_END:
83
+ self._in_message = False
84
+ if self._current_message:
85
+ self.console.print() # Newline
86
+
87
+ elif event.type == EventType.TOOL_START:
88
+ name = event.data.get("name", "tool")
89
+ args = event.data.get("args", {})
90
+ args_str = ", ".join(f"{k}={v!r}" for k, v in list(args.items())[:3])
91
+ self.console.print(f"[dim]> {name}({args_str})[/dim]")
92
+
93
+ elif event.type == EventType.TOOL_RESULT:
94
+ name = event.data.get("name", "tool")
95
+ success = event.data.get("success", True)
96
+ summary = event.data.get("summary", "")
97
+ if success:
98
+ self.console.print(f"[green] {name}: {summary}[/green]")
99
+ else:
100
+ self.console.print(f"[red] {name} failed: {summary}[/red]")
101
+
102
+ elif event.type == EventType.THINKING_START:
103
+ if self.show_thinking:
104
+ self._in_thinking = True
105
+ self._current_thinking = ""
106
+ self.console.print("[dim italic]Thinking...[/dim italic]")
107
+
108
+ elif event.type == EventType.THINKING_DELTA:
109
+ if self.show_thinking:
110
+ content = event.data.get("content", "")
111
+ self._current_thinking += content
112
+
113
+ elif event.type == EventType.THINKING_END:
114
+ if self.show_thinking and self._current_thinking:
115
+ # Optionally show thinking summary
116
+ self._in_thinking = False
117
+
118
+
119
+ class JSONStreamHandler(AgentEventEmitter):
120
+ """Handler that outputs events as JSON lines.
121
+
122
+ Suitable for piping to other processes or web clients.
123
+ """
124
+
125
+ def __init__(
126
+ self,
127
+ agent_name: str = "Agent",
128
+ output: IO = sys.stdout,
129
+ ):
130
+ """Initialize the JSON stream handler.
131
+
132
+ Args:
133
+ agent_name: Name for the agent
134
+ output: Output stream (default stdout)
135
+ """
136
+ super().__init__(agent_name)
137
+ self.output = output
138
+
139
+ def _handle(self, event: AgentEvent) -> None:
140
+ """Handle an event by writing JSON."""
141
+ try:
142
+ json_line = json.dumps(event.to_dict())
143
+ self.output.write(json_line + "\n")
144
+ self.output.flush()
145
+ except Exception:
146
+ pass # Don't break on serialization errors
147
+
148
+
149
+ class CollectingHandler(AgentEventEmitter):
150
+ """Handler that collects all events for later processing.
151
+
152
+ Useful for testing or batch processing.
153
+ """
154
+
155
+ def __init__(self, agent_name: str = "Agent"):
156
+ """Initialize the collecting handler."""
157
+ super().__init__(agent_name)
158
+ self.events: list[AgentEvent] = []
159
+
160
+ def _handle(self, event: AgentEvent) -> None:
161
+ """Collect the event."""
162
+ self.events.append(event)
163
+
164
+ def clear(self) -> None:
165
+ """Clear collected events."""
166
+ self.events.clear()
167
+
168
+ def get_events(self, event_type: Optional[EventType] = None) -> list[AgentEvent]:
169
+ """Get collected events, optionally filtered by type.
170
+
171
+ Args:
172
+ event_type: Optional type to filter by
173
+
174
+ Returns:
175
+ List of events
176
+ """
177
+ if event_type is None:
178
+ return list(self.events)
179
+ return [e for e in self.events if e.type == event_type]
180
+
181
+
182
+ class CompositeHandler(AgentEventEmitter):
183
+ """Handler that forwards events to multiple handlers.
184
+
185
+ Allows combining multiple output destinations.
186
+ """
187
+
188
+ def __init__(
189
+ self,
190
+ agent_name: str = "Agent",
191
+ handlers: Optional[list[AgentEventEmitter]] = None,
192
+ ):
193
+ """Initialize the composite handler.
194
+
195
+ Args:
196
+ agent_name: Name for the agent
197
+ handlers: List of handlers to forward to
198
+ """
199
+ super().__init__(agent_name)
200
+ self.handlers = handlers or []
201
+
202
+ def add_handler(self, handler: AgentEventEmitter) -> None:
203
+ """Add a handler.
204
+
205
+ Args:
206
+ handler: Handler to add
207
+ """
208
+ self.handlers.append(handler)
209
+
210
+ def remove_handler(self, handler: AgentEventEmitter) -> None:
211
+ """Remove a handler.
212
+
213
+ Args:
214
+ handler: Handler to remove
215
+ """
216
+ self.handlers = [h for h in self.handlers if h != handler]
217
+
218
+ def _handle(self, event: AgentEvent) -> None:
219
+ """Forward event to all handlers."""
220
+ for handler in self.handlers:
221
+ try:
222
+ handler.emit(event)
223
+ except Exception:
224
+ pass # Don't let one handler break others