emdash-core 0.1.7__py3-none-any.whl → 0.1.33__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/__init__.py +4 -0
- emdash_core/agent/events.py +52 -1
- emdash_core/agent/inprocess_subagent.py +123 -10
- emdash_core/agent/prompts/__init__.py +6 -0
- emdash_core/agent/prompts/main_agent.py +53 -3
- emdash_core/agent/prompts/plan_mode.py +255 -0
- emdash_core/agent/prompts/subagents.py +84 -16
- emdash_core/agent/prompts/workflow.py +270 -56
- emdash_core/agent/providers/base.py +4 -0
- emdash_core/agent/providers/factory.py +2 -2
- emdash_core/agent/providers/models.py +7 -0
- emdash_core/agent/providers/openai_provider.py +137 -13
- emdash_core/agent/runner/__init__.py +49 -0
- emdash_core/agent/runner/agent_runner.py +753 -0
- emdash_core/agent/runner/context.py +451 -0
- emdash_core/agent/runner/factory.py +108 -0
- emdash_core/agent/runner/plan.py +217 -0
- emdash_core/agent/runner/sdk_runner.py +324 -0
- emdash_core/agent/runner/utils.py +67 -0
- emdash_core/agent/skills.py +358 -0
- emdash_core/agent/toolkit.py +85 -5
- emdash_core/agent/toolkits/plan.py +9 -11
- emdash_core/agent/tools/__init__.py +3 -2
- emdash_core/agent/tools/coding.py +48 -4
- emdash_core/agent/tools/modes.py +207 -55
- 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/task.py +41 -2
- emdash_core/agent/tools/tasks.py +15 -78
- emdash_core/api/agent.py +562 -8
- 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/skills/frontend-design/SKILL.md +56 -0
- emdash_core/sse/stream.py +5 -0
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/METADATA +2 -2
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/RECORD +54 -37
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/entry_points.txt +1 -0
- emdash_core/agent/runner.py +0 -601
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/WHEEL +0 -0
emdash_core/agent/tools/modes.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
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
|
-
from dataclasses import dataclass
|
|
8
7
|
from enum import Enum
|
|
9
8
|
from typing import Optional
|
|
10
9
|
|
|
@@ -13,26 +12,26 @@ from .base import BaseTool, ToolResult, ToolCategory
|
|
|
13
12
|
|
|
14
13
|
class AgentMode(Enum):
|
|
15
14
|
"""Available agent modes."""
|
|
15
|
+
PLAN = "plan"
|
|
16
|
+
CODE = "code"
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
REVIEW = "review"
|
|
21
|
-
DEBUG = "debug"
|
|
18
|
+
|
|
19
|
+
# Modes that can be entered via enter_mode tool
|
|
20
|
+
SUPPORTED_MODES = ["plan"] # Extensible list
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
@dataclass
|
|
25
23
|
class ModeState:
|
|
26
24
|
"""Singleton state for agent mode."""
|
|
27
25
|
|
|
28
|
-
current_mode: AgentMode = AgentMode.EXPLORATION
|
|
29
|
-
mode_context: dict = None
|
|
30
|
-
|
|
31
26
|
_instance: Optional["ModeState"] = None
|
|
32
27
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.current_mode: AgentMode = AgentMode.CODE
|
|
30
|
+
self.plan_content: Optional[str] = None # Stores the current plan
|
|
31
|
+
self.plan_submitted: bool = False # Track if exit_plan was called this cycle
|
|
32
|
+
self.plan_mode_requested: bool = False # Track if enter_plan_mode was called
|
|
33
|
+
self.plan_mode_reason: Optional[str] = None # Reason for plan mode request
|
|
34
|
+
self.plan_file_path: Optional[str] = None # Path to the plan file (set when entering plan mode)
|
|
36
35
|
|
|
37
36
|
@classmethod
|
|
38
37
|
def get_instance(cls) -> "ModeState":
|
|
@@ -46,19 +45,63 @@ class ModeState:
|
|
|
46
45
|
"""Reset the singleton instance."""
|
|
47
46
|
cls._instance = None
|
|
48
47
|
|
|
48
|
+
def reset_cycle(self) -> None:
|
|
49
|
+
"""Reset per-cycle state (called on new user message)."""
|
|
50
|
+
self.plan_submitted = False
|
|
51
|
+
self.plan_mode_requested = False
|
|
52
|
+
self.plan_mode_reason = None
|
|
53
|
+
|
|
54
|
+
def approve_plan_mode(self) -> None:
|
|
55
|
+
"""Approve plan mode entry (called when user approves)."""
|
|
56
|
+
self.current_mode = AgentMode.PLAN
|
|
57
|
+
self.plan_content = None
|
|
58
|
+
self.plan_mode_requested = False
|
|
59
|
+
self.plan_mode_reason = None
|
|
60
|
+
# plan_file_path should already be set by the caller
|
|
61
|
+
|
|
62
|
+
def reject_plan_mode(self) -> None:
|
|
63
|
+
"""Reject plan mode entry (called when user rejects)."""
|
|
64
|
+
self.plan_mode_requested = False
|
|
65
|
+
self.plan_mode_reason = None
|
|
66
|
+
self.plan_file_path = None
|
|
67
|
+
|
|
68
|
+
def set_plan_file_path(self, path: str) -> None:
|
|
69
|
+
"""Set the plan file path (called when entering plan mode)."""
|
|
70
|
+
self.plan_file_path = path
|
|
71
|
+
|
|
72
|
+
def get_plan_file_path(self) -> Optional[str]:
|
|
73
|
+
"""Get the current plan file path."""
|
|
74
|
+
return self.plan_file_path
|
|
75
|
+
|
|
49
76
|
|
|
50
|
-
class
|
|
51
|
-
"""Tool for
|
|
77
|
+
class EnterPlanModeTool(BaseTool):
|
|
78
|
+
"""Tool for requesting to enter plan mode - REQUIRES USER CONSENT.
|
|
52
79
|
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
This follows Claude Code's pattern where entering plan mode is a proposal
|
|
81
|
+
that requires user approval, not an automatic switch.
|
|
82
|
+
"""
|
|
55
83
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
name = "enter_plan_mode"
|
|
85
|
+
description = """Request to enter plan mode for implementation planning.
|
|
86
|
+
|
|
87
|
+
This tool REQUIRES USER APPROVAL before plan mode is activated.
|
|
88
|
+
|
|
89
|
+
Use this proactively when you're about to start a non-trivial implementation task.
|
|
90
|
+
Getting user sign-off on your approach before writing code prevents wasted effort.
|
|
91
|
+
|
|
92
|
+
When to use:
|
|
93
|
+
- New feature implementation requiring architectural decisions
|
|
94
|
+
- Multiple valid approaches exist (user should choose)
|
|
95
|
+
- Multi-file changes expected (more than 2-3 files)
|
|
96
|
+
- Unclear requirements that need exploration first
|
|
97
|
+
|
|
98
|
+
When NOT to use:
|
|
99
|
+
- Single-line or few-line fixes
|
|
100
|
+
- Trivial tasks with obvious implementation
|
|
101
|
+
- Pure research/exploration (just explore directly)
|
|
102
|
+
- Tasks with very specific, detailed instructions already provided
|
|
103
|
+
|
|
104
|
+
The user will see your reason and can approve or reject entering plan mode."""
|
|
62
105
|
category = ToolCategory.PLANNING
|
|
63
106
|
|
|
64
107
|
def __init__(self, connection=None):
|
|
@@ -67,39 +110,47 @@ Modes:
|
|
|
67
110
|
|
|
68
111
|
def execute(
|
|
69
112
|
self,
|
|
70
|
-
|
|
71
|
-
|
|
113
|
+
reason: str = "",
|
|
114
|
+
**kwargs,
|
|
72
115
|
) -> ToolResult:
|
|
73
|
-
"""
|
|
116
|
+
"""Request to enter plan mode (requires user approval).
|
|
74
117
|
|
|
75
118
|
Args:
|
|
76
|
-
|
|
77
|
-
context: Optional context for the new mode
|
|
119
|
+
reason: Why you want to enter plan mode (shown to user)
|
|
78
120
|
|
|
79
121
|
Returns:
|
|
80
|
-
ToolResult
|
|
122
|
+
ToolResult requesting user approval
|
|
81
123
|
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
valid_modes = [m.value for m in AgentMode]
|
|
124
|
+
state = ModeState.get_instance()
|
|
125
|
+
|
|
126
|
+
if state.current_mode == AgentMode.PLAN:
|
|
86
127
|
return ToolResult.error_result(
|
|
87
|
-
|
|
88
|
-
suggestions=[
|
|
128
|
+
"Already in plan mode",
|
|
129
|
+
suggestions=["Use exit_plan to submit your plan for approval"],
|
|
89
130
|
)
|
|
90
131
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
132
|
+
# Check if already requested this cycle
|
|
133
|
+
if state.plan_mode_requested:
|
|
134
|
+
return ToolResult.error_result(
|
|
135
|
+
"Plan mode already requested. Wait for user response.",
|
|
136
|
+
suggestions=["Do not call enter_plan_mode again until user responds."],
|
|
137
|
+
)
|
|
94
138
|
|
|
95
|
-
if
|
|
96
|
-
|
|
139
|
+
if not reason or not reason.strip():
|
|
140
|
+
return ToolResult.error_result(
|
|
141
|
+
"Reason is required",
|
|
142
|
+
suggestions=["Explain why you need plan mode (helps user decide)"],
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Mark as requested (not entered - user must approve)
|
|
146
|
+
state.plan_mode_requested = True
|
|
147
|
+
state.plan_mode_reason = reason.strip()
|
|
97
148
|
|
|
98
149
|
return ToolResult.success_result(
|
|
99
150
|
data={
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
151
|
+
"status": "plan_mode_requested",
|
|
152
|
+
"reason": reason.strip(),
|
|
153
|
+
"message": "Plan mode requested. Waiting for user approval.",
|
|
103
154
|
},
|
|
104
155
|
)
|
|
105
156
|
|
|
@@ -107,17 +158,118 @@ Modes:
|
|
|
107
158
|
"""Get OpenAI function schema."""
|
|
108
159
|
return self._make_schema(
|
|
109
160
|
properties={
|
|
110
|
-
"
|
|
161
|
+
"reason": {
|
|
111
162
|
"type": "string",
|
|
112
|
-
"
|
|
113
|
-
"description": "Target mode to switch to",
|
|
163
|
+
"description": "Why you want to enter plan mode (explain the task complexity)",
|
|
114
164
|
},
|
|
115
|
-
|
|
165
|
+
},
|
|
166
|
+
required=["reason"],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ExitPlanModeTool(BaseTool):
|
|
171
|
+
"""Tool for submitting a plan for user approval."""
|
|
172
|
+
|
|
173
|
+
name = "exit_plan"
|
|
174
|
+
description = """Submit an implementation plan for user approval.
|
|
175
|
+
|
|
176
|
+
Use this tool to present a plan to the user for approval. The plan can come from:
|
|
177
|
+
1. A Plan sub-agent you spawned via task(subagent_type="Plan", ...)
|
|
178
|
+
2. Your own planning (if in plan mode)
|
|
179
|
+
|
|
180
|
+
Pass the plan content as the 'plan' parameter.
|
|
181
|
+
|
|
182
|
+
The user will either:
|
|
183
|
+
- Approve: You can proceed with implementation
|
|
184
|
+
- Reject: You'll receive feedback and can revise"""
|
|
185
|
+
category = ToolCategory.PLANNING
|
|
186
|
+
|
|
187
|
+
def __init__(self, connection=None):
|
|
188
|
+
"""Initialize without requiring connection."""
|
|
189
|
+
self.connection = connection
|
|
190
|
+
|
|
191
|
+
def execute(
|
|
192
|
+
self,
|
|
193
|
+
plan: Optional[str] = None,
|
|
194
|
+
**kwargs,
|
|
195
|
+
) -> ToolResult:
|
|
196
|
+
"""Submit plan for user approval.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
plan: The plan content (required in code mode, optional in plan mode).
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
ToolResult triggering user approval flow
|
|
203
|
+
"""
|
|
204
|
+
state = ModeState.get_instance()
|
|
205
|
+
|
|
206
|
+
# Prevent multiple exit_plan calls per cycle
|
|
207
|
+
if state.plan_submitted:
|
|
208
|
+
return ToolResult.error_result(
|
|
209
|
+
"Plan already submitted. Wait for user approval.",
|
|
210
|
+
suggestions=["Do not call exit_plan again until user responds."],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Get plan content from parameter or file
|
|
214
|
+
plan_content = plan
|
|
215
|
+
|
|
216
|
+
# In plan mode, try to read from plan file if not provided
|
|
217
|
+
if state.current_mode == AgentMode.PLAN:
|
|
218
|
+
if not plan_content or not plan_content.strip():
|
|
219
|
+
plan_file_path = state.get_plan_file_path()
|
|
220
|
+
if plan_file_path:
|
|
221
|
+
try:
|
|
222
|
+
from pathlib import Path
|
|
223
|
+
plan_path = Path(plan_file_path)
|
|
224
|
+
if plan_path.exists():
|
|
225
|
+
plan_content = plan_path.read_text()
|
|
226
|
+
except Exception as e:
|
|
227
|
+
return ToolResult.error_result(
|
|
228
|
+
f"Failed to read plan file: {e}",
|
|
229
|
+
suggestions=[f"Write your plan to {plan_file_path} first"],
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# In code mode, plan content is required (from Plan subagent)
|
|
233
|
+
if state.current_mode == AgentMode.CODE:
|
|
234
|
+
if not plan_content or not plan_content.strip():
|
|
235
|
+
return ToolResult.error_result(
|
|
236
|
+
"Plan content is required when submitting from code mode",
|
|
237
|
+
suggestions=[
|
|
238
|
+
"Pass the plan from your Plan sub-agent as the 'plan' parameter",
|
|
239
|
+
"Example: exit_plan(plan=<plan_from_subagent>)",
|
|
240
|
+
],
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if not plan_content or not plan_content.strip():
|
|
244
|
+
plan_file_path = state.get_plan_file_path() or "the plan file"
|
|
245
|
+
return ToolResult.error_result(
|
|
246
|
+
"Plan content is required",
|
|
247
|
+
suggestions=[
|
|
248
|
+
f"Write your plan to {plan_file_path} using write_to_file, then call exit_plan",
|
|
249
|
+
"Or pass the plan directly as a parameter",
|
|
250
|
+
],
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Store plan content for reference and mark as submitted
|
|
254
|
+
state.plan_content = plan_content.strip()
|
|
255
|
+
state.plan_submitted = True
|
|
256
|
+
|
|
257
|
+
return ToolResult.success_result({
|
|
258
|
+
"status": "plan_submitted",
|
|
259
|
+
"plan": plan_content.strip(),
|
|
260
|
+
"message": "Plan submitted for user approval. Waiting for user response.",
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
def get_schema(self) -> dict:
|
|
264
|
+
"""Get OpenAI function schema."""
|
|
265
|
+
return self._make_schema(
|
|
266
|
+
properties={
|
|
267
|
+
"plan": {
|
|
116
268
|
"type": "string",
|
|
117
|
-
"description": "Optional
|
|
269
|
+
"description": "Optional: The implementation plan as markdown. If not provided, reads from the plan file.",
|
|
118
270
|
},
|
|
119
271
|
},
|
|
120
|
-
required=[
|
|
272
|
+
required=[],
|
|
121
273
|
)
|
|
122
274
|
|
|
123
275
|
|
|
@@ -125,14 +277,14 @@ class GetModeTool(BaseTool):
|
|
|
125
277
|
"""Tool for getting current agent mode."""
|
|
126
278
|
|
|
127
279
|
name = "get_mode"
|
|
128
|
-
description = "Get the current agent operating mode
|
|
280
|
+
description = "Get the current agent operating mode (plan or code)."
|
|
129
281
|
category = ToolCategory.PLANNING
|
|
130
282
|
|
|
131
283
|
def __init__(self, connection=None):
|
|
132
284
|
"""Initialize without requiring connection."""
|
|
133
285
|
self.connection = connection
|
|
134
286
|
|
|
135
|
-
def execute(self) -> ToolResult:
|
|
287
|
+
def execute(self, **kwargs) -> ToolResult:
|
|
136
288
|
"""Get current mode.
|
|
137
289
|
|
|
138
290
|
Returns:
|
|
@@ -143,8 +295,8 @@ class GetModeTool(BaseTool):
|
|
|
143
295
|
return ToolResult.success_result(
|
|
144
296
|
data={
|
|
145
297
|
"current_mode": state.current_mode.value,
|
|
146
|
-
"
|
|
147
|
-
"available_modes":
|
|
298
|
+
"has_plan": state.plan_content is not None,
|
|
299
|
+
"available_modes": SUPPORTED_MODES,
|
|
148
300
|
},
|
|
149
301
|
)
|
|
150
302
|
|
|
@@ -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=[])
|