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.
- emdash_core/__init__.py +3 -0
- emdash_core/agent/__init__.py +37 -0
- emdash_core/agent/agents.py +225 -0
- emdash_core/agent/code_reviewer.py +476 -0
- emdash_core/agent/compaction.py +143 -0
- emdash_core/agent/context_manager.py +140 -0
- emdash_core/agent/events.py +338 -0
- emdash_core/agent/handlers.py +224 -0
- emdash_core/agent/inprocess_subagent.py +377 -0
- emdash_core/agent/mcp/__init__.py +50 -0
- emdash_core/agent/mcp/client.py +346 -0
- emdash_core/agent/mcp/config.py +302 -0
- emdash_core/agent/mcp/manager.py +496 -0
- emdash_core/agent/mcp/tool_factory.py +213 -0
- emdash_core/agent/prompts/__init__.py +38 -0
- emdash_core/agent/prompts/main_agent.py +104 -0
- emdash_core/agent/prompts/subagents.py +131 -0
- emdash_core/agent/prompts/workflow.py +136 -0
- emdash_core/agent/providers/__init__.py +34 -0
- emdash_core/agent/providers/base.py +143 -0
- emdash_core/agent/providers/factory.py +80 -0
- emdash_core/agent/providers/models.py +220 -0
- emdash_core/agent/providers/openai_provider.py +463 -0
- emdash_core/agent/providers/transformers_provider.py +217 -0
- emdash_core/agent/research/__init__.py +81 -0
- emdash_core/agent/research/agent.py +143 -0
- emdash_core/agent/research/controller.py +254 -0
- emdash_core/agent/research/critic.py +428 -0
- emdash_core/agent/research/macros.py +469 -0
- emdash_core/agent/research/planner.py +449 -0
- emdash_core/agent/research/researcher.py +436 -0
- emdash_core/agent/research/state.py +523 -0
- emdash_core/agent/research/synthesizer.py +594 -0
- emdash_core/agent/reviewer_profile.py +475 -0
- emdash_core/agent/rules.py +123 -0
- emdash_core/agent/runner.py +601 -0
- emdash_core/agent/session.py +262 -0
- emdash_core/agent/spec_schema.py +66 -0
- emdash_core/agent/specification.py +479 -0
- emdash_core/agent/subagent.py +397 -0
- emdash_core/agent/subagent_prompts.py +13 -0
- emdash_core/agent/toolkit.py +482 -0
- emdash_core/agent/toolkits/__init__.py +64 -0
- emdash_core/agent/toolkits/base.py +96 -0
- emdash_core/agent/toolkits/explore.py +47 -0
- emdash_core/agent/toolkits/plan.py +55 -0
- emdash_core/agent/tools/__init__.py +141 -0
- emdash_core/agent/tools/analytics.py +436 -0
- emdash_core/agent/tools/base.py +131 -0
- emdash_core/agent/tools/coding.py +484 -0
- emdash_core/agent/tools/github_mcp.py +592 -0
- emdash_core/agent/tools/history.py +13 -0
- emdash_core/agent/tools/modes.py +153 -0
- emdash_core/agent/tools/plan.py +206 -0
- emdash_core/agent/tools/plan_write.py +135 -0
- emdash_core/agent/tools/search.py +412 -0
- emdash_core/agent/tools/spec.py +341 -0
- emdash_core/agent/tools/task.py +262 -0
- emdash_core/agent/tools/task_output.py +204 -0
- emdash_core/agent/tools/tasks.py +454 -0
- emdash_core/agent/tools/traversal.py +588 -0
- emdash_core/agent/tools/web.py +179 -0
- emdash_core/analytics/__init__.py +5 -0
- emdash_core/analytics/engine.py +1286 -0
- emdash_core/api/__init__.py +5 -0
- emdash_core/api/agent.py +308 -0
- emdash_core/api/agents.py +154 -0
- emdash_core/api/analyze.py +264 -0
- emdash_core/api/auth.py +173 -0
- emdash_core/api/context.py +77 -0
- emdash_core/api/db.py +121 -0
- emdash_core/api/embed.py +131 -0
- emdash_core/api/feature.py +143 -0
- emdash_core/api/health.py +93 -0
- emdash_core/api/index.py +162 -0
- emdash_core/api/plan.py +110 -0
- emdash_core/api/projectmd.py +210 -0
- emdash_core/api/query.py +320 -0
- emdash_core/api/research.py +122 -0
- emdash_core/api/review.py +161 -0
- emdash_core/api/router.py +76 -0
- emdash_core/api/rules.py +116 -0
- emdash_core/api/search.py +119 -0
- emdash_core/api/spec.py +99 -0
- emdash_core/api/swarm.py +223 -0
- emdash_core/api/tasks.py +109 -0
- emdash_core/api/team.py +120 -0
- emdash_core/auth/__init__.py +17 -0
- emdash_core/auth/github.py +389 -0
- emdash_core/config.py +74 -0
- emdash_core/context/__init__.py +52 -0
- emdash_core/context/models.py +50 -0
- emdash_core/context/providers/__init__.py +11 -0
- emdash_core/context/providers/base.py +74 -0
- emdash_core/context/providers/explored_areas.py +183 -0
- emdash_core/context/providers/touched_areas.py +360 -0
- emdash_core/context/registry.py +73 -0
- emdash_core/context/reranker.py +199 -0
- emdash_core/context/service.py +260 -0
- emdash_core/context/session.py +352 -0
- emdash_core/core/__init__.py +104 -0
- emdash_core/core/config.py +454 -0
- emdash_core/core/exceptions.py +55 -0
- emdash_core/core/models.py +265 -0
- emdash_core/core/review_config.py +57 -0
- emdash_core/db/__init__.py +67 -0
- emdash_core/db/auth.py +134 -0
- emdash_core/db/models.py +91 -0
- emdash_core/db/provider.py +222 -0
- emdash_core/db/providers/__init__.py +5 -0
- emdash_core/db/providers/supabase.py +452 -0
- emdash_core/embeddings/__init__.py +24 -0
- emdash_core/embeddings/indexer.py +534 -0
- emdash_core/embeddings/models.py +192 -0
- emdash_core/embeddings/providers/__init__.py +7 -0
- emdash_core/embeddings/providers/base.py +112 -0
- emdash_core/embeddings/providers/fireworks.py +141 -0
- emdash_core/embeddings/providers/openai.py +104 -0
- emdash_core/embeddings/registry.py +146 -0
- emdash_core/embeddings/service.py +215 -0
- emdash_core/graph/__init__.py +26 -0
- emdash_core/graph/builder.py +134 -0
- emdash_core/graph/connection.py +692 -0
- emdash_core/graph/schema.py +416 -0
- emdash_core/graph/writer.py +667 -0
- emdash_core/ingestion/__init__.py +7 -0
- emdash_core/ingestion/change_detector.py +150 -0
- emdash_core/ingestion/git/__init__.py +5 -0
- emdash_core/ingestion/git/commit_analyzer.py +196 -0
- emdash_core/ingestion/github/__init__.py +6 -0
- emdash_core/ingestion/github/pr_fetcher.py +296 -0
- emdash_core/ingestion/github/task_extractor.py +100 -0
- emdash_core/ingestion/orchestrator.py +540 -0
- emdash_core/ingestion/parsers/__init__.py +10 -0
- emdash_core/ingestion/parsers/base_parser.py +66 -0
- emdash_core/ingestion/parsers/call_graph_builder.py +121 -0
- emdash_core/ingestion/parsers/class_extractor.py +154 -0
- emdash_core/ingestion/parsers/function_extractor.py +202 -0
- emdash_core/ingestion/parsers/import_analyzer.py +119 -0
- emdash_core/ingestion/parsers/python_parser.py +123 -0
- emdash_core/ingestion/parsers/registry.py +72 -0
- emdash_core/ingestion/parsers/ts_ast_parser.js +313 -0
- emdash_core/ingestion/parsers/typescript_parser.py +278 -0
- emdash_core/ingestion/repository.py +346 -0
- emdash_core/models/__init__.py +38 -0
- emdash_core/models/agent.py +68 -0
- emdash_core/models/index.py +77 -0
- emdash_core/models/query.py +113 -0
- emdash_core/planning/__init__.py +7 -0
- emdash_core/planning/agent_api.py +413 -0
- emdash_core/planning/context_builder.py +265 -0
- emdash_core/planning/feature_context.py +232 -0
- emdash_core/planning/feature_expander.py +646 -0
- emdash_core/planning/llm_explainer.py +198 -0
- emdash_core/planning/similarity.py +509 -0
- emdash_core/planning/team_focus.py +821 -0
- emdash_core/server.py +153 -0
- emdash_core/sse/__init__.py +5 -0
- emdash_core/sse/stream.py +196 -0
- emdash_core/swarm/__init__.py +17 -0
- emdash_core/swarm/merge_agent.py +383 -0
- emdash_core/swarm/session_manager.py +274 -0
- emdash_core/swarm/swarm_runner.py +226 -0
- emdash_core/swarm/task_definition.py +137 -0
- emdash_core/swarm/worker_spawner.py +319 -0
- emdash_core/swarm/worktree_manager.py +278 -0
- emdash_core/templates/__init__.py +10 -0
- emdash_core/templates/defaults/agent-builder.md.template +82 -0
- emdash_core/templates/defaults/focus.md.template +115 -0
- emdash_core/templates/defaults/pr-review-enhanced.md.template +309 -0
- emdash_core/templates/defaults/pr-review.md.template +80 -0
- emdash_core/templates/defaults/project.md.template +85 -0
- emdash_core/templates/defaults/research_critic.md.template +112 -0
- emdash_core/templates/defaults/research_planner.md.template +85 -0
- emdash_core/templates/defaults/research_synthesizer.md.template +128 -0
- emdash_core/templates/defaults/reviewer.md.template +81 -0
- emdash_core/templates/defaults/spec.md.template +41 -0
- emdash_core/templates/defaults/tasks.md.template +78 -0
- emdash_core/templates/loader.py +296 -0
- emdash_core/utils/__init__.py +45 -0
- emdash_core/utils/git.py +84 -0
- emdash_core/utils/image.py +502 -0
- emdash_core/utils/logger.py +51 -0
- emdash_core-0.1.7.dist-info/METADATA +35 -0
- emdash_core-0.1.7.dist-info/RECORD +187 -0
- emdash_core-0.1.7.dist-info/WHEEL +4 -0
- 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
|