emdash-core 0.1.7__py3-none-any.whl → 0.1.25__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 +6 -1
- emdash_core/agent/events.py +29 -0
- emdash_core/agent/prompts/__init__.py +5 -0
- emdash_core/agent/prompts/main_agent.py +22 -2
- emdash_core/agent/prompts/plan_mode.py +126 -0
- emdash_core/agent/prompts/subagents.py +11 -7
- emdash_core/agent/prompts/workflow.py +138 -43
- emdash_core/agent/providers/base.py +4 -0
- emdash_core/agent/providers/models.py +7 -0
- emdash_core/agent/providers/openai_provider.py +74 -2
- emdash_core/agent/runner.py +556 -34
- emdash_core/agent/skills.py +319 -0
- emdash_core/agent/toolkit.py +48 -0
- emdash_core/agent/tools/__init__.py +3 -2
- emdash_core/agent/tools/modes.py +197 -53
- emdash_core/agent/tools/search.py +4 -0
- emdash_core/agent/tools/skill.py +193 -0
- emdash_core/agent/tools/spec.py +61 -94
- emdash_core/agent/tools/tasks.py +15 -78
- emdash_core/api/agent.py +7 -7
- emdash_core/api/index.py +1 -1
- emdash_core/api/projectmd.py +4 -2
- emdash_core/api/router.py +2 -0
- emdash_core/api/skills.py +241 -0
- emdash_core/checkpoint/__init__.py +40 -0
- emdash_core/checkpoint/cli.py +175 -0
- emdash_core/checkpoint/git_operations.py +250 -0
- emdash_core/checkpoint/manager.py +231 -0
- emdash_core/checkpoint/models.py +107 -0
- emdash_core/checkpoint/storage.py +201 -0
- emdash_core/config.py +1 -1
- emdash_core/core/config.py +18 -2
- emdash_core/graph/schema.py +5 -5
- emdash_core/ingestion/orchestrator.py +19 -10
- emdash_core/models/agent.py +1 -1
- emdash_core/server.py +42 -0
- emdash_core/sse/stream.py +1 -0
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/METADATA +1 -2
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/RECORD +41 -31
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/entry_points.txt +1 -0
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/WHEEL +0 -0
emdash_core/agent/tools/modes.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Agent mode management tools.
|
|
2
2
|
|
|
3
|
-
Provides tools for
|
|
4
|
-
|
|
3
|
+
Provides tools for entering and exiting modes, following
|
|
4
|
+
Claude Code's approach of explicit mode transitions.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from dataclasses import dataclass
|
|
@@ -13,27 +13,23 @@ from .base import BaseTool, ToolResult, ToolCategory
|
|
|
13
13
|
|
|
14
14
|
class AgentMode(Enum):
|
|
15
15
|
"""Available agent modes."""
|
|
16
|
+
PLAN = "plan"
|
|
17
|
+
CODE = "code"
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
REVIEW = "review"
|
|
21
|
-
DEBUG = "debug"
|
|
19
|
+
|
|
20
|
+
# Modes that can be entered via enter_mode tool
|
|
21
|
+
SUPPORTED_MODES = ["plan"] # Extensible list
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
@dataclass
|
|
25
25
|
class ModeState:
|
|
26
26
|
"""Singleton state for agent mode."""
|
|
27
27
|
|
|
28
|
-
current_mode: AgentMode = AgentMode.
|
|
29
|
-
|
|
28
|
+
current_mode: AgentMode = AgentMode.CODE
|
|
29
|
+
plan_content: Optional[str] = None # Stores the current plan
|
|
30
30
|
|
|
31
31
|
_instance: Optional["ModeState"] = None
|
|
32
32
|
|
|
33
|
-
def __post_init__(self):
|
|
34
|
-
if self.mode_context is None:
|
|
35
|
-
self.mode_context = {}
|
|
36
|
-
|
|
37
33
|
@classmethod
|
|
38
34
|
def get_instance(cls) -> "ModeState":
|
|
39
35
|
"""Get the singleton instance."""
|
|
@@ -47,18 +43,16 @@ class ModeState:
|
|
|
47
43
|
cls._instance = None
|
|
48
44
|
|
|
49
45
|
|
|
50
|
-
class
|
|
51
|
-
"""Tool for
|
|
46
|
+
class EnterModeTool(BaseTool):
|
|
47
|
+
"""Tool for entering a different mode from code mode."""
|
|
52
48
|
|
|
53
|
-
name = "
|
|
54
|
-
description = """
|
|
49
|
+
name = "enter_mode"
|
|
50
|
+
description = """Enter a different operating mode.
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- review: Code review and feedback
|
|
61
|
-
- debug: Debugging and issue investigation"""
|
|
52
|
+
Currently supported modes:
|
|
53
|
+
- plan: Enter plan mode to explore the codebase and design an implementation plan.
|
|
54
|
+
In plan mode you can ONLY use read-only tools (no file modifications).
|
|
55
|
+
Use exit_plan when your plan is ready for user approval."""
|
|
62
56
|
category = ToolCategory.PLANNING
|
|
63
57
|
|
|
64
58
|
def __init__(self, connection=None):
|
|
@@ -68,40 +62,49 @@ Modes:
|
|
|
68
62
|
def execute(
|
|
69
63
|
self,
|
|
70
64
|
mode: str,
|
|
71
|
-
|
|
65
|
+
reason: str = "",
|
|
66
|
+
**kwargs,
|
|
72
67
|
) -> ToolResult:
|
|
73
|
-
"""
|
|
68
|
+
"""Enter a new mode.
|
|
74
69
|
|
|
75
70
|
Args:
|
|
76
|
-
mode:
|
|
77
|
-
|
|
71
|
+
mode: Mode to enter (currently only "plan" supported)
|
|
72
|
+
reason: Why you're entering this mode (helps context)
|
|
78
73
|
|
|
79
74
|
Returns:
|
|
80
|
-
ToolResult indicating
|
|
75
|
+
ToolResult indicating mode switch
|
|
81
76
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
valid_modes = [m.value for m in AgentMode]
|
|
77
|
+
mode_lower = mode.lower()
|
|
78
|
+
|
|
79
|
+
if mode_lower not in SUPPORTED_MODES:
|
|
86
80
|
return ToolResult.error_result(
|
|
87
|
-
f"
|
|
88
|
-
suggestions=[f"
|
|
81
|
+
f"Unsupported mode: {mode}",
|
|
82
|
+
suggestions=[f"Supported modes: {', '.join(SUPPORTED_MODES)}"],
|
|
89
83
|
)
|
|
90
84
|
|
|
91
85
|
state = ModeState.get_instance()
|
|
92
|
-
old_mode = state.current_mode
|
|
93
|
-
state.current_mode = new_mode
|
|
94
86
|
|
|
95
|
-
if
|
|
96
|
-
state.
|
|
87
|
+
if mode_lower == "plan":
|
|
88
|
+
if state.current_mode == AgentMode.PLAN:
|
|
89
|
+
return ToolResult.error_result(
|
|
90
|
+
"Already in plan mode",
|
|
91
|
+
suggestions=["Use exit_plan to submit your plan for approval"],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
state.current_mode = AgentMode.PLAN
|
|
95
|
+
state.plan_content = None # Reset plan content
|
|
96
|
+
|
|
97
|
+
return ToolResult.success_result(
|
|
98
|
+
data={
|
|
99
|
+
"status": "entered_plan_mode",
|
|
100
|
+
"mode": "plan",
|
|
101
|
+
"reason": reason,
|
|
102
|
+
"message": "You are now in plan mode. Explore the codebase and design your plan. Use exit_plan when ready.",
|
|
103
|
+
},
|
|
104
|
+
)
|
|
97
105
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"previous_mode": old_mode.value,
|
|
101
|
-
"current_mode": new_mode.value,
|
|
102
|
-
"context": context,
|
|
103
|
-
},
|
|
104
|
-
)
|
|
106
|
+
# Placeholder for future modes
|
|
107
|
+
return ToolResult.error_result(f"Mode '{mode}' not yet implemented")
|
|
105
108
|
|
|
106
109
|
def get_schema(self) -> dict:
|
|
107
110
|
"""Get OpenAI function schema."""
|
|
@@ -109,30 +112,171 @@ Modes:
|
|
|
109
112
|
properties={
|
|
110
113
|
"mode": {
|
|
111
114
|
"type": "string",
|
|
112
|
-
"enum":
|
|
113
|
-
"description": "
|
|
115
|
+
"enum": SUPPORTED_MODES,
|
|
116
|
+
"description": "Mode to enter",
|
|
114
117
|
},
|
|
115
|
-
"
|
|
118
|
+
"reason": {
|
|
116
119
|
"type": "string",
|
|
117
|
-
"description": "
|
|
120
|
+
"description": "Brief reason for entering this mode",
|
|
118
121
|
},
|
|
119
122
|
},
|
|
120
123
|
required=["mode"],
|
|
121
124
|
)
|
|
122
125
|
|
|
123
126
|
|
|
127
|
+
class ExitPlanModeTool(BaseTool):
|
|
128
|
+
"""Tool for exiting plan mode and submitting plan for approval."""
|
|
129
|
+
|
|
130
|
+
name = "exit_plan"
|
|
131
|
+
description = """Exit plan mode and submit your plan for user approval.
|
|
132
|
+
|
|
133
|
+
Scale detail based on task complexity:
|
|
134
|
+
- Simple task: title, summary, files_to_modify (steps optional)
|
|
135
|
+
- Complex task: all fields including phases, risks, open questions
|
|
136
|
+
|
|
137
|
+
Required fields:
|
|
138
|
+
- title: Clear, concise plan title
|
|
139
|
+
- summary: What will be implemented and why
|
|
140
|
+
- files_to_modify: List of file changes with paths, line numbers, and descriptions
|
|
141
|
+
|
|
142
|
+
Optional fields (include only if needed - each must "earn its place"):
|
|
143
|
+
- implementation_steps: For complex tasks needing ordered steps
|
|
144
|
+
- risks: Non-trivial risks only
|
|
145
|
+
- testing_strategy: Beyond obvious test cases
|
|
146
|
+
|
|
147
|
+
The user will either:
|
|
148
|
+
- Approve: You'll return to code mode to implement the plan
|
|
149
|
+
- Reject: You'll receive feedback and can revise in plan mode"""
|
|
150
|
+
category = ToolCategory.PLANNING
|
|
151
|
+
|
|
152
|
+
def __init__(self, connection=None):
|
|
153
|
+
"""Initialize without requiring connection."""
|
|
154
|
+
self.connection = connection
|
|
155
|
+
|
|
156
|
+
def execute(
|
|
157
|
+
self,
|
|
158
|
+
title: str,
|
|
159
|
+
summary: str,
|
|
160
|
+
files_to_modify: list[dict] = None,
|
|
161
|
+
implementation_steps: list[str] = None,
|
|
162
|
+
risks: list[str] = None,
|
|
163
|
+
testing_strategy: str = None,
|
|
164
|
+
**kwargs,
|
|
165
|
+
) -> ToolResult:
|
|
166
|
+
"""Exit plan mode and submit plan for approval.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
title: Title of the plan
|
|
170
|
+
summary: Summary of what will be implemented and why
|
|
171
|
+
files_to_modify: List of file changes, each with:
|
|
172
|
+
- path: File path (e.g., "src/auth.py")
|
|
173
|
+
- lines: Line range (e.g., "45-60" or "new file")
|
|
174
|
+
- changes: Description of what changes
|
|
175
|
+
implementation_steps: Ordered list of detailed implementation steps
|
|
176
|
+
risks: List of potential risks or considerations
|
|
177
|
+
testing_strategy: Description of how changes will be tested
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
ToolResult triggering user approval flow
|
|
181
|
+
"""
|
|
182
|
+
state = ModeState.get_instance()
|
|
183
|
+
|
|
184
|
+
if state.current_mode != AgentMode.PLAN:
|
|
185
|
+
return ToolResult.error_result(
|
|
186
|
+
"Not in plan mode",
|
|
187
|
+
suggestions=["Use enter_mode with mode='plan' to enter plan mode first"],
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
if not title or not title.strip():
|
|
191
|
+
return ToolResult.error_result("Title is required")
|
|
192
|
+
if not summary or not summary.strip():
|
|
193
|
+
return ToolResult.error_result("Summary is required")
|
|
194
|
+
if not files_to_modify:
|
|
195
|
+
return ToolResult.error_result(
|
|
196
|
+
"files_to_modify is required",
|
|
197
|
+
suggestions=["Include at least one file with path, lines, and changes"],
|
|
198
|
+
)
|
|
199
|
+
# implementation_steps optional for simple tasks where files_to_modify is self-explanatory
|
|
200
|
+
|
|
201
|
+
# Store plan content for reference
|
|
202
|
+
state.plan_content = summary
|
|
203
|
+
|
|
204
|
+
return ToolResult.success_result({
|
|
205
|
+
"status": "plan_submitted",
|
|
206
|
+
"title": title.strip(),
|
|
207
|
+
"summary": summary.strip(),
|
|
208
|
+
"files_to_modify": files_to_modify,
|
|
209
|
+
"implementation_steps": implementation_steps or [],
|
|
210
|
+
"risks": risks or [],
|
|
211
|
+
"testing_strategy": testing_strategy or "",
|
|
212
|
+
"message": "Plan submitted for user approval. Waiting for user response.",
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
def get_schema(self) -> dict:
|
|
216
|
+
"""Get OpenAI function schema."""
|
|
217
|
+
return self._make_schema(
|
|
218
|
+
properties={
|
|
219
|
+
"title": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"description": "Clear, concise title of the plan",
|
|
222
|
+
},
|
|
223
|
+
"summary": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"description": "Detailed summary of what will be implemented and why",
|
|
226
|
+
},
|
|
227
|
+
"files_to_modify": {
|
|
228
|
+
"type": "array",
|
|
229
|
+
"items": {
|
|
230
|
+
"type": "object",
|
|
231
|
+
"properties": {
|
|
232
|
+
"path": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"description": "File path (e.g., 'src/auth.py')",
|
|
235
|
+
},
|
|
236
|
+
"lines": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "Line range (e.g., '45-60') or 'new file'",
|
|
239
|
+
},
|
|
240
|
+
"changes": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "Description of what changes in this file",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
"required": ["path", "changes"],
|
|
246
|
+
},
|
|
247
|
+
"description": "List of files to modify with line numbers and change descriptions",
|
|
248
|
+
},
|
|
249
|
+
"implementation_steps": {
|
|
250
|
+
"type": "array",
|
|
251
|
+
"items": {"type": "string"},
|
|
252
|
+
"description": "Detailed ordered list of implementation steps with sub-tasks",
|
|
253
|
+
},
|
|
254
|
+
"risks": {
|
|
255
|
+
"type": "array",
|
|
256
|
+
"items": {"type": "string"},
|
|
257
|
+
"description": "Potential risks, breaking changes, or considerations",
|
|
258
|
+
},
|
|
259
|
+
"testing_strategy": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"description": "How the changes will be tested (unit tests, integration tests, etc.)",
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
required=["title", "summary", "files_to_modify"],
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
124
268
|
class GetModeTool(BaseTool):
|
|
125
269
|
"""Tool for getting current agent mode."""
|
|
126
270
|
|
|
127
271
|
name = "get_mode"
|
|
128
|
-
description = "Get the current agent operating mode
|
|
272
|
+
description = "Get the current agent operating mode (plan or code)."
|
|
129
273
|
category = ToolCategory.PLANNING
|
|
130
274
|
|
|
131
275
|
def __init__(self, connection=None):
|
|
132
276
|
"""Initialize without requiring connection."""
|
|
133
277
|
self.connection = connection
|
|
134
278
|
|
|
135
|
-
def execute(self) -> ToolResult:
|
|
279
|
+
def execute(self, **kwargs) -> ToolResult:
|
|
136
280
|
"""Get current mode.
|
|
137
281
|
|
|
138
282
|
Returns:
|
|
@@ -143,8 +287,8 @@ class GetModeTool(BaseTool):
|
|
|
143
287
|
return ToolResult.success_result(
|
|
144
288
|
data={
|
|
145
289
|
"current_mode": state.current_mode.value,
|
|
146
|
-
"
|
|
147
|
-
"available_modes":
|
|
290
|
+
"has_plan": state.plan_content is not None,
|
|
291
|
+
"available_modes": SUPPORTED_MODES,
|
|
148
292
|
},
|
|
149
293
|
)
|
|
150
294
|
|
|
@@ -22,6 +22,7 @@ Useful for finding code related to concepts like "authentication", "database que
|
|
|
22
22
|
entity_types: Optional[list[str]] = None,
|
|
23
23
|
limit: int = 10,
|
|
24
24
|
min_score: float = 0.5,
|
|
25
|
+
**kwargs, # Ignore unexpected params from LLM
|
|
25
26
|
) -> ToolResult:
|
|
26
27
|
"""Execute semantic search.
|
|
27
28
|
|
|
@@ -121,6 +122,7 @@ More precise than semantic search when you know part of the name."""
|
|
|
121
122
|
query: str,
|
|
122
123
|
entity_types: Optional[list[str]] = None,
|
|
123
124
|
limit: int = 10,
|
|
125
|
+
**kwargs, # Ignore unexpected params from LLM
|
|
124
126
|
) -> ToolResult:
|
|
125
127
|
"""Execute text search.
|
|
126
128
|
|
|
@@ -215,6 +217,7 @@ Useful for finding code patterns, string literals, or specific implementations."
|
|
|
215
217
|
file_pattern: Optional[str] = None,
|
|
216
218
|
max_results: int = 50,
|
|
217
219
|
context_lines: int = 2,
|
|
220
|
+
**kwargs, # Ignore unexpected params from LLM
|
|
218
221
|
) -> ToolResult:
|
|
219
222
|
"""Execute grep search.
|
|
220
223
|
|
|
@@ -324,6 +327,7 @@ Common patterns:
|
|
|
324
327
|
self,
|
|
325
328
|
pattern: str,
|
|
326
329
|
max_results: int = 100,
|
|
330
|
+
**kwargs, # Ignore unexpected params from LLM
|
|
327
331
|
) -> ToolResult:
|
|
328
332
|
"""Execute glob search for files.
|
|
329
333
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Skill invocation tool.
|
|
2
|
+
|
|
3
|
+
Allows the agent to activate and invoke skills during task execution.
|
|
4
|
+
Skills provide specialized instructions for repeatable tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .base import BaseTool, ToolResult, ToolCategory
|
|
10
|
+
from ..skills import SkillRegistry, Skill
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SkillTool(BaseTool):
|
|
14
|
+
"""Tool for invoking skills.
|
|
15
|
+
|
|
16
|
+
Skills are markdown-based instruction files that teach the agent
|
|
17
|
+
how to perform specific, repeatable tasks. This tool allows
|
|
18
|
+
explicit skill invocation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "skill"
|
|
22
|
+
description = """Invoke a skill for specialized task execution.
|
|
23
|
+
|
|
24
|
+
Skills provide focused instructions for common tasks like:
|
|
25
|
+
- commit: Generate commit messages following conventions
|
|
26
|
+
- review-pr: Review PRs with code standards
|
|
27
|
+
- security-review: Security-focused code review
|
|
28
|
+
|
|
29
|
+
When you invoke a skill, you receive its instructions which you should follow.
|
|
30
|
+
Use list_skills to see available skills."""
|
|
31
|
+
category = ToolCategory.PLANNING
|
|
32
|
+
|
|
33
|
+
def __init__(self, connection=None):
|
|
34
|
+
"""Initialize without requiring connection."""
|
|
35
|
+
self.connection = connection
|
|
36
|
+
|
|
37
|
+
def execute(
|
|
38
|
+
self,
|
|
39
|
+
skill: str,
|
|
40
|
+
args: str = "",
|
|
41
|
+
**kwargs,
|
|
42
|
+
) -> ToolResult:
|
|
43
|
+
"""Invoke a skill.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
skill: Name of the skill to invoke
|
|
47
|
+
args: Optional arguments to pass to the skill
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
ToolResult with skill instructions
|
|
51
|
+
"""
|
|
52
|
+
registry = SkillRegistry.get_instance()
|
|
53
|
+
skill_obj = registry.get_skill(skill)
|
|
54
|
+
|
|
55
|
+
if skill_obj is None:
|
|
56
|
+
available = registry.list_skills()
|
|
57
|
+
if available:
|
|
58
|
+
return ToolResult.error_result(
|
|
59
|
+
f"Skill '{skill}' not found",
|
|
60
|
+
suggestions=[f"Available skills: {', '.join(available)}"],
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
return ToolResult.error_result(
|
|
64
|
+
f"Skill '{skill}' not found. No skills are currently loaded.",
|
|
65
|
+
suggestions=[
|
|
66
|
+
"Create skills in .emdash/skills/<skill-name>/SKILL.md",
|
|
67
|
+
"Skills are loaded from the .emdash/skills/ directory",
|
|
68
|
+
],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Build the skill activation response
|
|
72
|
+
response_parts = [
|
|
73
|
+
f"# Skill Activated: {skill_obj.name}",
|
|
74
|
+
"",
|
|
75
|
+
f"**Description**: {skill_obj.description}",
|
|
76
|
+
"",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
if args:
|
|
80
|
+
response_parts.extend([
|
|
81
|
+
f"**Arguments**: {args}",
|
|
82
|
+
"",
|
|
83
|
+
])
|
|
84
|
+
|
|
85
|
+
if skill_obj.tools:
|
|
86
|
+
response_parts.extend([
|
|
87
|
+
f"**Required tools**: {', '.join(skill_obj.tools)}",
|
|
88
|
+
"",
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
response_parts.extend([
|
|
92
|
+
"---",
|
|
93
|
+
"",
|
|
94
|
+
skill_obj.instructions,
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
return ToolResult.success_result(
|
|
98
|
+
data={
|
|
99
|
+
"skill_name": skill_obj.name,
|
|
100
|
+
"description": skill_obj.description,
|
|
101
|
+
"instructions": skill_obj.instructions,
|
|
102
|
+
"tools": skill_obj.tools,
|
|
103
|
+
"args": args,
|
|
104
|
+
"message": "\n".join(response_parts),
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def get_schema(self) -> dict:
|
|
109
|
+
"""Get OpenAI function schema."""
|
|
110
|
+
registry = SkillRegistry.get_instance()
|
|
111
|
+
available_skills = registry.list_skills()
|
|
112
|
+
|
|
113
|
+
description = self.description
|
|
114
|
+
if available_skills:
|
|
115
|
+
description += f"\n\nCurrently available skills: {', '.join(available_skills)}"
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"type": "function",
|
|
119
|
+
"function": {
|
|
120
|
+
"name": self.name,
|
|
121
|
+
"description": description,
|
|
122
|
+
"parameters": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"skill": {
|
|
126
|
+
"type": "string",
|
|
127
|
+
"description": "Name of the skill to invoke",
|
|
128
|
+
},
|
|
129
|
+
"args": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"description": "Optional arguments for the skill (e.g., PR number, file path)",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
"required": ["skill"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class ListSkillsTool(BaseTool):
|
|
141
|
+
"""Tool for listing available skills."""
|
|
142
|
+
|
|
143
|
+
name = "list_skills"
|
|
144
|
+
description = "List all available skills and their descriptions."
|
|
145
|
+
category = ToolCategory.PLANNING
|
|
146
|
+
|
|
147
|
+
def __init__(self, connection=None):
|
|
148
|
+
"""Initialize without requiring connection."""
|
|
149
|
+
self.connection = connection
|
|
150
|
+
|
|
151
|
+
def execute(self, **kwargs) -> ToolResult:
|
|
152
|
+
"""List available skills.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
ToolResult with list of skills
|
|
156
|
+
"""
|
|
157
|
+
registry = SkillRegistry.get_instance()
|
|
158
|
+
skills = registry.get_all_skills()
|
|
159
|
+
|
|
160
|
+
if not skills:
|
|
161
|
+
return ToolResult.success_result(
|
|
162
|
+
data={
|
|
163
|
+
"skills": [],
|
|
164
|
+
"message": "No skills loaded. Create skills in .emdash/skills/<skill-name>/SKILL.md",
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
skills_list = []
|
|
169
|
+
for skill in skills.values():
|
|
170
|
+
skills_list.append({
|
|
171
|
+
"name": skill.name,
|
|
172
|
+
"description": skill.description,
|
|
173
|
+
"user_invocable": skill.user_invocable,
|
|
174
|
+
"tools": skill.tools,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
# Build human-readable message
|
|
178
|
+
lines = ["# Available Skills", ""]
|
|
179
|
+
for s in skills_list:
|
|
180
|
+
invocable = f" (invoke with /{s['name']})" if s["user_invocable"] else ""
|
|
181
|
+
lines.append(f"- **{s['name']}**: {s['description']}{invocable}")
|
|
182
|
+
|
|
183
|
+
return ToolResult.success_result(
|
|
184
|
+
data={
|
|
185
|
+
"skills": skills_list,
|
|
186
|
+
"count": len(skills_list),
|
|
187
|
+
"message": "\n".join(lines),
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def get_schema(self) -> dict:
|
|
192
|
+
"""Get OpenAI function schema."""
|
|
193
|
+
return self._make_schema(properties={}, required=[])
|