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,153 @@
|
|
|
1
|
+
"""Agent mode management tools.
|
|
2
|
+
|
|
3
|
+
Provides tools for switching between different agent modes
|
|
4
|
+
(exploration, planning, coding, etc).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from .base import BaseTool, ToolResult, ToolCategory
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentMode(Enum):
|
|
15
|
+
"""Available agent modes."""
|
|
16
|
+
|
|
17
|
+
EXPLORATION = "exploration"
|
|
18
|
+
PLANNING = "planning"
|
|
19
|
+
CODING = "coding"
|
|
20
|
+
REVIEW = "review"
|
|
21
|
+
DEBUG = "debug"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ModeState:
|
|
26
|
+
"""Singleton state for agent mode."""
|
|
27
|
+
|
|
28
|
+
current_mode: AgentMode = AgentMode.EXPLORATION
|
|
29
|
+
mode_context: dict = None
|
|
30
|
+
|
|
31
|
+
_instance: Optional["ModeState"] = None
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
if self.mode_context is None:
|
|
35
|
+
self.mode_context = {}
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def get_instance(cls) -> "ModeState":
|
|
39
|
+
"""Get the singleton instance."""
|
|
40
|
+
if cls._instance is None:
|
|
41
|
+
cls._instance = cls()
|
|
42
|
+
return cls._instance
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def reset(cls) -> None:
|
|
46
|
+
"""Reset the singleton instance."""
|
|
47
|
+
cls._instance = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SwitchModeTool(BaseTool):
|
|
51
|
+
"""Tool for switching between agent modes."""
|
|
52
|
+
|
|
53
|
+
name = "switch_mode"
|
|
54
|
+
description = """Switch the agent to a different operating mode.
|
|
55
|
+
|
|
56
|
+
Modes:
|
|
57
|
+
- exploration: General codebase exploration and understanding
|
|
58
|
+
- planning: Feature planning and specification writing
|
|
59
|
+
- coding: Active code editing and implementation
|
|
60
|
+
- review: Code review and feedback
|
|
61
|
+
- debug: Debugging and issue investigation"""
|
|
62
|
+
category = ToolCategory.PLANNING
|
|
63
|
+
|
|
64
|
+
def __init__(self, connection=None):
|
|
65
|
+
"""Initialize without requiring connection."""
|
|
66
|
+
self.connection = connection
|
|
67
|
+
|
|
68
|
+
def execute(
|
|
69
|
+
self,
|
|
70
|
+
mode: str,
|
|
71
|
+
context: Optional[str] = None,
|
|
72
|
+
) -> ToolResult:
|
|
73
|
+
"""Switch to a new mode.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
mode: Target mode name
|
|
77
|
+
context: Optional context for the new mode
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
ToolResult indicating success
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
new_mode = AgentMode(mode.lower())
|
|
84
|
+
except ValueError:
|
|
85
|
+
valid_modes = [m.value for m in AgentMode]
|
|
86
|
+
return ToolResult.error_result(
|
|
87
|
+
f"Invalid mode: {mode}",
|
|
88
|
+
suggestions=[f"Valid modes: {', '.join(valid_modes)}"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
state = ModeState.get_instance()
|
|
92
|
+
old_mode = state.current_mode
|
|
93
|
+
state.current_mode = new_mode
|
|
94
|
+
|
|
95
|
+
if context:
|
|
96
|
+
state.mode_context[new_mode.value] = context
|
|
97
|
+
|
|
98
|
+
return ToolResult.success_result(
|
|
99
|
+
data={
|
|
100
|
+
"previous_mode": old_mode.value,
|
|
101
|
+
"current_mode": new_mode.value,
|
|
102
|
+
"context": context,
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def get_schema(self) -> dict:
|
|
107
|
+
"""Get OpenAI function schema."""
|
|
108
|
+
return self._make_schema(
|
|
109
|
+
properties={
|
|
110
|
+
"mode": {
|
|
111
|
+
"type": "string",
|
|
112
|
+
"enum": [m.value for m in AgentMode],
|
|
113
|
+
"description": "Target mode to switch to",
|
|
114
|
+
},
|
|
115
|
+
"context": {
|
|
116
|
+
"type": "string",
|
|
117
|
+
"description": "Optional context for the new mode",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
required=["mode"],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class GetModeTool(BaseTool):
|
|
125
|
+
"""Tool for getting current agent mode."""
|
|
126
|
+
|
|
127
|
+
name = "get_mode"
|
|
128
|
+
description = "Get the current agent operating mode and its context."
|
|
129
|
+
category = ToolCategory.PLANNING
|
|
130
|
+
|
|
131
|
+
def __init__(self, connection=None):
|
|
132
|
+
"""Initialize without requiring connection."""
|
|
133
|
+
self.connection = connection
|
|
134
|
+
|
|
135
|
+
def execute(self) -> ToolResult:
|
|
136
|
+
"""Get current mode.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
ToolResult with current mode info
|
|
140
|
+
"""
|
|
141
|
+
state = ModeState.get_instance()
|
|
142
|
+
|
|
143
|
+
return ToolResult.success_result(
|
|
144
|
+
data={
|
|
145
|
+
"current_mode": state.current_mode.value,
|
|
146
|
+
"context": state.mode_context.get(state.current_mode.value),
|
|
147
|
+
"available_modes": [m.value for m in AgentMode],
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def get_schema(self) -> dict:
|
|
152
|
+
"""Get OpenAI function schema."""
|
|
153
|
+
return self._make_schema(properties={}, required=[])
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Planning tool for exploration strategy."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .base import BaseTool, ToolResult, ToolCategory
|
|
6
|
+
from ...utils.logger import log
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Exploration strategies
|
|
10
|
+
STRATEGIES = {
|
|
11
|
+
"understand_feature": {
|
|
12
|
+
"description": "Understand how a feature works",
|
|
13
|
+
"steps": [
|
|
14
|
+
{"tool": "semantic_search", "purpose": "Find entry points"},
|
|
15
|
+
{"tool": "expand_node", "purpose": "Understand main components"},
|
|
16
|
+
{"tool": "get_callers", "purpose": "See usage patterns"},
|
|
17
|
+
{"tool": "get_file_dependencies", "purpose": "Map module structure"},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
"debug_issue": {
|
|
21
|
+
"description": "Debug a bug or issue",
|
|
22
|
+
"steps": [
|
|
23
|
+
{"tool": "grep", "purpose": "Find error messages or symptoms"},
|
|
24
|
+
{"tool": "semantic_search", "purpose": "Find related code"},
|
|
25
|
+
{"tool": "get_callees", "purpose": "Trace execution path"},
|
|
26
|
+
{"tool": "expand_node", "purpose": "Examine suspicious code"},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
"assess_change_impact": {
|
|
30
|
+
"description": "Assess impact of a proposed change",
|
|
31
|
+
"steps": [
|
|
32
|
+
{"tool": "get_callers", "purpose": "Find all usages"},
|
|
33
|
+
{"tool": "get_impact_analysis", "purpose": "Assess blast radius"},
|
|
34
|
+
{"tool": "get_file_dependencies", "purpose": "Find affected modules"},
|
|
35
|
+
{"tool": "get_class_hierarchy", "purpose": "Check inheritance impact"},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
"onboard_codebase": {
|
|
39
|
+
"description": "Get oriented in a new codebase",
|
|
40
|
+
"steps": [
|
|
41
|
+
{"tool": "get_communities", "purpose": "Understand major areas"},
|
|
42
|
+
{"tool": "get_top_pagerank", "purpose": "Find key components"},
|
|
43
|
+
{"tool": "get_area_importance", "purpose": "Map directory structure"},
|
|
44
|
+
{"tool": "semantic_search", "purpose": "Explore specific topics"},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
"find_similar_code": {
|
|
48
|
+
"description": "Find code similar to a reference",
|
|
49
|
+
"steps": [
|
|
50
|
+
{"tool": "expand_node", "purpose": "Understand the reference"},
|
|
51
|
+
{"tool": "semantic_search", "purpose": "Find similar patterns"},
|
|
52
|
+
{"tool": "get_neighbors", "purpose": "Explore related code"},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PlanExplorationTool(BaseTool):
|
|
59
|
+
"""Create an exploration plan for a goal."""
|
|
60
|
+
|
|
61
|
+
name = "plan_exploration"
|
|
62
|
+
description = """Create a structured exploration plan for understanding code.
|
|
63
|
+
|
|
64
|
+
Available strategies:
|
|
65
|
+
- understand_feature: Learn how a feature works
|
|
66
|
+
- debug_issue: Track down a bug
|
|
67
|
+
- assess_change_impact: Evaluate change risk
|
|
68
|
+
- onboard_codebase: Get oriented in new code
|
|
69
|
+
- find_similar_code: Find similar patterns
|
|
70
|
+
|
|
71
|
+
Or provide a custom goal and get a tailored plan."""
|
|
72
|
+
category = ToolCategory.PLANNING
|
|
73
|
+
|
|
74
|
+
def execute(
|
|
75
|
+
self,
|
|
76
|
+
goal: str,
|
|
77
|
+
strategy: Optional[str] = None,
|
|
78
|
+
context: Optional[str] = None,
|
|
79
|
+
constraints: Optional[list[str]] = None,
|
|
80
|
+
use_case: Optional[str] = None,
|
|
81
|
+
) -> ToolResult:
|
|
82
|
+
"""Create an exploration plan.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
goal: What you want to understand or accomplish
|
|
86
|
+
strategy: Optional predefined strategy to use
|
|
87
|
+
context: Additional context about the goal
|
|
88
|
+
constraints: Constraints to consider
|
|
89
|
+
use_case: Optional use case hint (e.g., "spec", "debug", "review")
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
ToolResult with exploration plan
|
|
93
|
+
"""
|
|
94
|
+
# Map use_case to strategy if strategy not provided
|
|
95
|
+
if use_case and not strategy:
|
|
96
|
+
use_case_mapping = {
|
|
97
|
+
"spec": "understand_feature",
|
|
98
|
+
"debug": "debug_issue",
|
|
99
|
+
"review": "assess_change_impact",
|
|
100
|
+
"onboard": "onboard_codebase",
|
|
101
|
+
}
|
|
102
|
+
strategy = use_case_mapping.get(use_case)
|
|
103
|
+
try:
|
|
104
|
+
# Use predefined strategy if specified
|
|
105
|
+
if strategy and strategy in STRATEGIES:
|
|
106
|
+
strat = STRATEGIES[strategy]
|
|
107
|
+
return ToolResult.success_result(
|
|
108
|
+
data={
|
|
109
|
+
"goal": goal,
|
|
110
|
+
"strategy": strategy,
|
|
111
|
+
"description": strat["description"],
|
|
112
|
+
"steps": strat["steps"],
|
|
113
|
+
"context": context,
|
|
114
|
+
"constraints": constraints,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Infer strategy from goal
|
|
119
|
+
inferred = self._infer_strategy(goal)
|
|
120
|
+
|
|
121
|
+
if inferred and inferred in STRATEGIES:
|
|
122
|
+
strat = STRATEGIES[inferred]
|
|
123
|
+
return ToolResult.success_result(
|
|
124
|
+
data={
|
|
125
|
+
"goal": goal,
|
|
126
|
+
"strategy": inferred,
|
|
127
|
+
"description": strat["description"],
|
|
128
|
+
"steps": strat["steps"],
|
|
129
|
+
"context": context,
|
|
130
|
+
"constraints": constraints,
|
|
131
|
+
"inferred": True,
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Generic exploration plan
|
|
136
|
+
return ToolResult.success_result(
|
|
137
|
+
data={
|
|
138
|
+
"goal": goal,
|
|
139
|
+
"strategy": "custom",
|
|
140
|
+
"description": "Custom exploration plan",
|
|
141
|
+
"steps": [
|
|
142
|
+
{"tool": "semantic_search", "purpose": f"Search for '{goal}'"},
|
|
143
|
+
{"tool": "expand_node", "purpose": "Examine top results"},
|
|
144
|
+
{"tool": "get_neighbors", "purpose": "Explore connections"},
|
|
145
|
+
],
|
|
146
|
+
"context": context,
|
|
147
|
+
"constraints": constraints,
|
|
148
|
+
"available_strategies": list(STRATEGIES.keys()),
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
except Exception as e:
|
|
153
|
+
log.exception("Plan exploration failed")
|
|
154
|
+
return ToolResult.error_result(f"Planning failed: {str(e)}")
|
|
155
|
+
|
|
156
|
+
def _infer_strategy(self, goal: str) -> Optional[str]:
|
|
157
|
+
"""Infer strategy from goal text."""
|
|
158
|
+
goal_lower = goal.lower()
|
|
159
|
+
|
|
160
|
+
if any(word in goal_lower for word in ["bug", "error", "issue", "fix", "debug"]):
|
|
161
|
+
return "debug_issue"
|
|
162
|
+
|
|
163
|
+
if any(word in goal_lower for word in ["how", "understand", "learn", "feature"]):
|
|
164
|
+
return "understand_feature"
|
|
165
|
+
|
|
166
|
+
if any(word in goal_lower for word in ["change", "modify", "refactor", "impact"]):
|
|
167
|
+
return "assess_change_impact"
|
|
168
|
+
|
|
169
|
+
if any(word in goal_lower for word in ["new", "onboard", "overview", "structure"]):
|
|
170
|
+
return "onboard_codebase"
|
|
171
|
+
|
|
172
|
+
if any(word in goal_lower for word in ["similar", "like", "pattern", "example"]):
|
|
173
|
+
return "find_similar_code"
|
|
174
|
+
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def get_schema(self) -> dict:
|
|
178
|
+
"""Get OpenAI function schema."""
|
|
179
|
+
return self._make_schema(
|
|
180
|
+
properties={
|
|
181
|
+
"goal": {
|
|
182
|
+
"type": "string",
|
|
183
|
+
"description": "What you want to understand or accomplish",
|
|
184
|
+
},
|
|
185
|
+
"strategy": {
|
|
186
|
+
"type": "string",
|
|
187
|
+
"enum": list(STRATEGIES.keys()),
|
|
188
|
+
"description": "Optional predefined strategy to use",
|
|
189
|
+
},
|
|
190
|
+
"context": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"description": "Additional context about the goal",
|
|
193
|
+
},
|
|
194
|
+
"constraints": {
|
|
195
|
+
"type": "array",
|
|
196
|
+
"items": {"type": "string"},
|
|
197
|
+
"description": "Constraints to consider",
|
|
198
|
+
},
|
|
199
|
+
"use_case": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"enum": ["spec", "debug", "review", "onboard"],
|
|
202
|
+
"description": "Use case hint to guide strategy selection",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
required=["goal"],
|
|
206
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""WritePlan tool for Plan sub-agents.
|
|
2
|
+
|
|
3
|
+
Allows writing implementation plans to a restricted directory (.emdash/plans/).
|
|
4
|
+
This is the only write operation available to Plan agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from .base import BaseTool, ToolResult, ToolCategory
|
|
11
|
+
from ...utils.logger import log
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WritePlanTool(BaseTool):
|
|
15
|
+
"""Write implementation plan to .emdash/plans/ directory only.
|
|
16
|
+
|
|
17
|
+
This tool is restricted to writing markdown files only to the
|
|
18
|
+
.emdash/plans/ directory. It cannot write to any other location.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "write_plan"
|
|
22
|
+
description = """Write or update an implementation plan markdown file.
|
|
23
|
+
|
|
24
|
+
Plans are saved to .emdash/plans/<filename>.md in the repository.
|
|
25
|
+
Use this to document implementation strategies, architectural decisions,
|
|
26
|
+
and step-by-step plans.
|
|
27
|
+
|
|
28
|
+
Example filenames: "auth-refactor.md", "api-redesign.md", "feature-plan.md"
|
|
29
|
+
"""
|
|
30
|
+
category = ToolCategory.PLANNING
|
|
31
|
+
|
|
32
|
+
def __init__(self, repo_root: Path, connection=None):
|
|
33
|
+
"""Initialize with repo root.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
repo_root: Root directory of the repository
|
|
37
|
+
connection: Optional connection (not used)
|
|
38
|
+
"""
|
|
39
|
+
self.repo_root = repo_root.resolve()
|
|
40
|
+
self.plans_dir = self.repo_root / ".emdash" / "plans"
|
|
41
|
+
self.connection = connection
|
|
42
|
+
|
|
43
|
+
def execute(
|
|
44
|
+
self,
|
|
45
|
+
filename: str = "",
|
|
46
|
+
content: str = "",
|
|
47
|
+
**kwargs,
|
|
48
|
+
) -> ToolResult:
|
|
49
|
+
"""Write a plan file.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
filename: Plan filename (e.g., "auth-refactor.md")
|
|
53
|
+
content: Markdown content for the plan
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
ToolResult indicating success or error
|
|
57
|
+
"""
|
|
58
|
+
# Validate filename
|
|
59
|
+
if not filename:
|
|
60
|
+
return ToolResult.error_result("Filename is required")
|
|
61
|
+
|
|
62
|
+
# Ensure .md extension
|
|
63
|
+
if not filename.endswith(".md"):
|
|
64
|
+
filename = f"{filename}.md"
|
|
65
|
+
|
|
66
|
+
# Sanitize filename - remove path components and invalid chars
|
|
67
|
+
safe_filename = Path(filename).name
|
|
68
|
+
safe_filename = re.sub(r'[<>:"/\\|?*]', '-', safe_filename)
|
|
69
|
+
|
|
70
|
+
if not safe_filename or safe_filename.startswith('.'):
|
|
71
|
+
return ToolResult.error_result(
|
|
72
|
+
f"Invalid filename: {filename}",
|
|
73
|
+
suggestions=["Use alphanumeric characters, hyphens, underscores"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Build full path
|
|
77
|
+
plan_path = self.plans_dir / safe_filename
|
|
78
|
+
|
|
79
|
+
# Validate path is within plans directory (prevent traversal)
|
|
80
|
+
try:
|
|
81
|
+
plan_path.resolve().relative_to(self.plans_dir.resolve())
|
|
82
|
+
except ValueError:
|
|
83
|
+
return ToolResult.error_result(
|
|
84
|
+
"Path traversal not allowed",
|
|
85
|
+
suggestions=["Provide a simple filename without directory paths"],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Create plans directory if needed
|
|
90
|
+
self.plans_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
# Check if updating existing file
|
|
93
|
+
is_update = plan_path.exists()
|
|
94
|
+
|
|
95
|
+
# Write the plan
|
|
96
|
+
plan_path.write_text(content)
|
|
97
|
+
|
|
98
|
+
log.info(
|
|
99
|
+
"Plan {} written: {}",
|
|
100
|
+
"updated" if is_update else "created",
|
|
101
|
+
plan_path,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return ToolResult.success_result(
|
|
105
|
+
data={
|
|
106
|
+
"path": str(plan_path.relative_to(self.repo_root)),
|
|
107
|
+
"filename": safe_filename,
|
|
108
|
+
"bytes_written": len(content),
|
|
109
|
+
"is_update": is_update,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except PermissionError:
|
|
114
|
+
return ToolResult.error_result(
|
|
115
|
+
f"Permission denied writing to {plan_path}",
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
log.exception("Failed to write plan")
|
|
119
|
+
return ToolResult.error_result(f"Failed to write plan: {e}")
|
|
120
|
+
|
|
121
|
+
def get_schema(self) -> dict:
|
|
122
|
+
"""Get OpenAI function schema."""
|
|
123
|
+
return self._make_schema(
|
|
124
|
+
properties={
|
|
125
|
+
"filename": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Plan filename (e.g., 'auth-refactor.md', 'feature-plan')",
|
|
128
|
+
},
|
|
129
|
+
"content": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Markdown content for the implementation plan",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required=["filename", "content"],
|
|
135
|
+
)
|