emdash-core 0.1.25__py3-none-any.whl → 0.1.37__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/agent/__init__.py +4 -0
- emdash_core/agent/agents.py +84 -23
- emdash_core/agent/events.py +42 -20
- emdash_core/agent/hooks.py +419 -0
- emdash_core/agent/inprocess_subagent.py +166 -18
- emdash_core/agent/prompts/__init__.py +4 -3
- emdash_core/agent/prompts/main_agent.py +67 -2
- emdash_core/agent/prompts/plan_mode.py +236 -107
- emdash_core/agent/prompts/subagents.py +103 -23
- emdash_core/agent/prompts/workflow.py +159 -26
- emdash_core/agent/providers/factory.py +2 -2
- emdash_core/agent/providers/openai_provider.py +67 -15
- emdash_core/agent/runner/__init__.py +49 -0
- emdash_core/agent/runner/agent_runner.py +765 -0
- emdash_core/agent/runner/context.py +470 -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 +47 -8
- emdash_core/agent/toolkit.py +46 -14
- emdash_core/agent/toolkits/__init__.py +117 -18
- emdash_core/agent/toolkits/base.py +87 -2
- emdash_core/agent/toolkits/explore.py +18 -0
- emdash_core/agent/toolkits/plan.py +27 -11
- emdash_core/agent/tools/__init__.py +2 -2
- emdash_core/agent/tools/coding.py +48 -4
- emdash_core/agent/tools/modes.py +151 -143
- emdash_core/agent/tools/task.py +52 -6
- emdash_core/api/agent.py +706 -1
- emdash_core/ingestion/repository.py +17 -198
- emdash_core/models/agent.py +4 -0
- emdash_core/skills/frontend-design/SKILL.md +56 -0
- emdash_core/sse/stream.py +4 -0
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/METADATA +4 -1
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/RECORD +38 -30
- emdash_core/agent/runner.py +0 -1123
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/WHEEL +0 -0
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/entry_points.txt +0 -0
|
@@ -64,6 +64,8 @@ Returns the file content as text."""
|
|
|
64
64
|
path: str,
|
|
65
65
|
start_line: Optional[int] = None,
|
|
66
66
|
end_line: Optional[int] = None,
|
|
67
|
+
offset: Optional[int] = None,
|
|
68
|
+
limit: Optional[int] = None,
|
|
67
69
|
) -> ToolResult:
|
|
68
70
|
"""Read a file.
|
|
69
71
|
|
|
@@ -71,6 +73,8 @@ Returns the file content as text."""
|
|
|
71
73
|
path: Path to the file
|
|
72
74
|
start_line: Optional starting line (1-indexed)
|
|
73
75
|
end_line: Optional ending line (1-indexed)
|
|
76
|
+
offset: Alternative to start_line - line number to start from (1-indexed)
|
|
77
|
+
limit: Alternative to end_line - number of lines to read
|
|
74
78
|
|
|
75
79
|
Returns:
|
|
76
80
|
ToolResult with file content
|
|
@@ -89,8 +93,14 @@ Returns the file content as text."""
|
|
|
89
93
|
content = full_path.read_text()
|
|
90
94
|
lines = content.split("\n")
|
|
91
95
|
|
|
92
|
-
# Handle line ranges
|
|
93
|
-
|
|
96
|
+
# Handle line ranges - support both start_line/end_line and offset/limit
|
|
97
|
+
# offset/limit take precedence if provided
|
|
98
|
+
if offset is not None or limit is not None:
|
|
99
|
+
start_idx = (offset - 1) if offset else 0
|
|
100
|
+
end_idx = start_idx + limit if limit else len(lines)
|
|
101
|
+
lines = lines[start_idx:end_idx]
|
|
102
|
+
content = "\n".join(lines)
|
|
103
|
+
elif start_line or end_line:
|
|
94
104
|
start_idx = (start_line - 1) if start_line else 0
|
|
95
105
|
end_idx = end_line if end_line else len(lines)
|
|
96
106
|
lines = lines[start_idx:end_idx]
|
|
@@ -123,6 +133,14 @@ Returns the file content as text."""
|
|
|
123
133
|
"type": "integer",
|
|
124
134
|
"description": "Ending line number (1-indexed)",
|
|
125
135
|
},
|
|
136
|
+
"offset": {
|
|
137
|
+
"type": "integer",
|
|
138
|
+
"description": "Line number to start reading from (1-indexed). Alternative to start_line.",
|
|
139
|
+
},
|
|
140
|
+
"limit": {
|
|
141
|
+
"type": "integer",
|
|
142
|
+
"description": "Number of lines to read. Alternative to end_line.",
|
|
143
|
+
},
|
|
126
144
|
},
|
|
127
145
|
required=["path"],
|
|
128
146
|
)
|
|
@@ -135,6 +153,18 @@ class WriteToFileTool(CodingTool):
|
|
|
135
153
|
description = """Write content to a file.
|
|
136
154
|
Creates the file if it doesn't exist, or overwrites if it does."""
|
|
137
155
|
|
|
156
|
+
def __init__(self, repo_root: Path, connection=None, allowed_paths: list[str] | None = None):
|
|
157
|
+
"""Initialize with optional path restrictions.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
repo_root: Root directory of the repository
|
|
161
|
+
connection: Optional connection (not used for file ops)
|
|
162
|
+
allowed_paths: If provided, only these paths can be written to.
|
|
163
|
+
Used in plan mode to restrict writes to the plan file.
|
|
164
|
+
"""
|
|
165
|
+
super().__init__(repo_root, connection)
|
|
166
|
+
self.allowed_paths = allowed_paths
|
|
167
|
+
|
|
138
168
|
def execute(
|
|
139
169
|
self,
|
|
140
170
|
path: str,
|
|
@@ -153,6 +183,19 @@ Creates the file if it doesn't exist, or overwrites if it does."""
|
|
|
153
183
|
if not valid:
|
|
154
184
|
return ToolResult.error_result(error)
|
|
155
185
|
|
|
186
|
+
# Check allowed paths restriction (used in plan mode)
|
|
187
|
+
if self.allowed_paths is not None:
|
|
188
|
+
path_str = str(full_path)
|
|
189
|
+
is_allowed = any(
|
|
190
|
+
path_str == allowed or path_str.endswith(allowed.lstrip("/"))
|
|
191
|
+
for allowed in self.allowed_paths
|
|
192
|
+
)
|
|
193
|
+
if not is_allowed:
|
|
194
|
+
return ToolResult.error_result(
|
|
195
|
+
f"In plan mode, you can only write to: {', '.join(self.allowed_paths)}",
|
|
196
|
+
suggestions=["Write your plan to the designated plan file"],
|
|
197
|
+
)
|
|
198
|
+
|
|
156
199
|
try:
|
|
157
200
|
# Create parent directories
|
|
158
201
|
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -218,8 +261,9 @@ The diff should be in standard unified diff format."""
|
|
|
218
261
|
|
|
219
262
|
try:
|
|
220
263
|
# Try to apply with patch command
|
|
264
|
+
# --batch: suppress questions, --forward: skip already-applied patches
|
|
221
265
|
result = subprocess.run(
|
|
222
|
-
["patch", "-p0", "--forward"],
|
|
266
|
+
["patch", "-p0", "--forward", "--batch"],
|
|
223
267
|
input=diff,
|
|
224
268
|
capture_output=True,
|
|
225
269
|
text=True,
|
|
@@ -230,7 +274,7 @@ The diff should be in standard unified diff format."""
|
|
|
230
274
|
if result.returncode != 0:
|
|
231
275
|
# Try with -p1
|
|
232
276
|
result = subprocess.run(
|
|
233
|
-
["patch", "-p1", "--forward"],
|
|
277
|
+
["patch", "-p1", "--forward", "--batch"],
|
|
234
278
|
input=diff,
|
|
235
279
|
capture_output=True,
|
|
236
280
|
text=True,
|
emdash_core/agent/tools/modes.py
CHANGED
|
@@ -4,7 +4,6 @@ Provides tools for entering and exiting modes, following
|
|
|
4
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
|
|
|
@@ -21,15 +20,19 @@ class AgentMode(Enum):
|
|
|
21
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.CODE
|
|
29
|
-
plan_content: Optional[str] = None # Stores the current plan
|
|
30
|
-
|
|
31
26
|
_instance: Optional["ModeState"] = None
|
|
32
27
|
|
|
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)
|
|
35
|
+
|
|
33
36
|
@classmethod
|
|
34
37
|
def get_instance(cls) -> "ModeState":
|
|
35
38
|
"""Get the singleton instance."""
|
|
@@ -42,17 +45,63 @@ class ModeState:
|
|
|
42
45
|
"""Reset the singleton instance."""
|
|
43
46
|
cls._instance = None
|
|
44
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
|
+
|
|
76
|
+
|
|
77
|
+
class EnterPlanModeTool(BaseTool):
|
|
78
|
+
"""Tool for requesting to enter plan mode - REQUIRES USER CONSENT.
|
|
79
|
+
|
|
80
|
+
This follows Claude Code's pattern where entering plan mode is a proposal
|
|
81
|
+
that requires user approval, not an automatic switch.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
name = "enter_plan_mode"
|
|
85
|
+
description = """Request to enter plan mode for implementation planning.
|
|
45
86
|
|
|
46
|
-
|
|
47
|
-
"""Tool for entering a different mode from code mode."""
|
|
87
|
+
This tool REQUIRES USER APPROVAL before plan mode is activated.
|
|
48
88
|
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
51
91
|
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
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."""
|
|
56
105
|
category = ToolCategory.PLANNING
|
|
57
106
|
|
|
58
107
|
def __init__(self, connection=None):
|
|
@@ -61,92 +110,78 @@ Currently supported modes:
|
|
|
61
110
|
|
|
62
111
|
def execute(
|
|
63
112
|
self,
|
|
64
|
-
mode: str,
|
|
65
113
|
reason: str = "",
|
|
66
114
|
**kwargs,
|
|
67
115
|
) -> ToolResult:
|
|
68
|
-
"""
|
|
116
|
+
"""Request to enter plan mode (requires user approval).
|
|
69
117
|
|
|
70
118
|
Args:
|
|
71
|
-
|
|
72
|
-
reason: Why you're entering this mode (helps context)
|
|
119
|
+
reason: Why you want to enter plan mode (shown to user)
|
|
73
120
|
|
|
74
121
|
Returns:
|
|
75
|
-
ToolResult
|
|
122
|
+
ToolResult requesting user approval
|
|
76
123
|
"""
|
|
77
|
-
|
|
124
|
+
state = ModeState.get_instance()
|
|
78
125
|
|
|
79
|
-
if
|
|
126
|
+
if state.current_mode == AgentMode.PLAN:
|
|
80
127
|
return ToolResult.error_result(
|
|
81
|
-
|
|
82
|
-
suggestions=[
|
|
128
|
+
"Already in plan mode",
|
|
129
|
+
suggestions=["Use exit_plan to submit your plan for approval"],
|
|
83
130
|
)
|
|
84
131
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
+
)
|
|
96
138
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
"reason": reason,
|
|
102
|
-
"message": "You are now in plan mode. Explore the codebase and design your plan. Use exit_plan when ready.",
|
|
103
|
-
},
|
|
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)"],
|
|
104
143
|
)
|
|
105
144
|
|
|
106
|
-
#
|
|
107
|
-
|
|
145
|
+
# Mark as requested (not entered - user must approve)
|
|
146
|
+
state.plan_mode_requested = True
|
|
147
|
+
state.plan_mode_reason = reason.strip()
|
|
148
|
+
|
|
149
|
+
return ToolResult.success_result(
|
|
150
|
+
data={
|
|
151
|
+
"status": "plan_mode_requested",
|
|
152
|
+
"reason": reason.strip(),
|
|
153
|
+
"message": "Plan mode requested. Waiting for user approval.",
|
|
154
|
+
},
|
|
155
|
+
)
|
|
108
156
|
|
|
109
157
|
def get_schema(self) -> dict:
|
|
110
158
|
"""Get OpenAI function schema."""
|
|
111
159
|
return self._make_schema(
|
|
112
160
|
properties={
|
|
113
|
-
"mode": {
|
|
114
|
-
"type": "string",
|
|
115
|
-
"enum": SUPPORTED_MODES,
|
|
116
|
-
"description": "Mode to enter",
|
|
117
|
-
},
|
|
118
161
|
"reason": {
|
|
119
162
|
"type": "string",
|
|
120
|
-
"description": "
|
|
163
|
+
"description": "Why you want to enter plan mode (explain the task complexity)",
|
|
121
164
|
},
|
|
122
165
|
},
|
|
123
|
-
required=["
|
|
166
|
+
required=["reason"],
|
|
124
167
|
)
|
|
125
168
|
|
|
126
169
|
|
|
127
170
|
class ExitPlanModeTool(BaseTool):
|
|
128
|
-
"""Tool for
|
|
171
|
+
"""Tool for submitting a plan for user approval."""
|
|
129
172
|
|
|
130
173
|
name = "exit_plan"
|
|
131
|
-
description = """
|
|
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
|
|
174
|
+
description = """Submit an implementation plan for user approval.
|
|
136
175
|
|
|
137
|
-
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
- files_to_modify: List of file changes with paths, line numbers, and descriptions
|
|
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)
|
|
141
179
|
|
|
142
|
-
|
|
143
|
-
- implementation_steps: For complex tasks needing ordered steps
|
|
144
|
-
- risks: Non-trivial risks only
|
|
145
|
-
- testing_strategy: Beyond obvious test cases
|
|
180
|
+
Pass the plan content as the 'plan' parameter.
|
|
146
181
|
|
|
147
182
|
The user will either:
|
|
148
|
-
- Approve: You
|
|
149
|
-
- Reject: You'll receive feedback and can revise
|
|
183
|
+
- Approve: You can proceed with implementation
|
|
184
|
+
- Reject: You'll receive feedback and can revise"""
|
|
150
185
|
category = ToolCategory.PLANNING
|
|
151
186
|
|
|
152
187
|
def __init__(self, connection=None):
|
|
@@ -155,60 +190,73 @@ The user will either:
|
|
|
155
190
|
|
|
156
191
|
def execute(
|
|
157
192
|
self,
|
|
158
|
-
|
|
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,
|
|
193
|
+
plan: Optional[str] = None,
|
|
164
194
|
**kwargs,
|
|
165
195
|
) -> ToolResult:
|
|
166
|
-
"""
|
|
196
|
+
"""Submit plan for user approval.
|
|
167
197
|
|
|
168
198
|
Args:
|
|
169
|
-
|
|
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
|
|
199
|
+
plan: The plan content (required in code mode, optional in plan mode).
|
|
178
200
|
|
|
179
201
|
Returns:
|
|
180
202
|
ToolResult triggering user approval flow
|
|
181
203
|
"""
|
|
182
204
|
state = ModeState.get_instance()
|
|
183
205
|
|
|
184
|
-
|
|
206
|
+
# Prevent multiple exit_plan calls per cycle
|
|
207
|
+
if state.plan_submitted:
|
|
185
208
|
return ToolResult.error_result(
|
|
186
|
-
"
|
|
187
|
-
suggestions=["
|
|
209
|
+
"Plan already submitted. Wait for user approval.",
|
|
210
|
+
suggestions=["Do not call exit_plan again until user responds."],
|
|
188
211
|
)
|
|
189
212
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if
|
|
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"
|
|
195
245
|
return ToolResult.error_result(
|
|
196
|
-
"
|
|
197
|
-
suggestions=[
|
|
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
|
+
],
|
|
198
251
|
)
|
|
199
|
-
# implementation_steps optional for simple tasks where files_to_modify is self-explanatory
|
|
200
252
|
|
|
201
|
-
# Store plan content for reference
|
|
202
|
-
state.plan_content =
|
|
253
|
+
# Store plan content for reference and mark as submitted
|
|
254
|
+
state.plan_content = plan_content.strip()
|
|
255
|
+
state.plan_submitted = True
|
|
203
256
|
|
|
204
257
|
return ToolResult.success_result({
|
|
205
258
|
"status": "plan_submitted",
|
|
206
|
-
"
|
|
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 "",
|
|
259
|
+
"plan": plan_content.strip(),
|
|
212
260
|
"message": "Plan submitted for user approval. Waiting for user response.",
|
|
213
261
|
})
|
|
214
262
|
|
|
@@ -216,52 +264,12 @@ The user will either:
|
|
|
216
264
|
"""Get OpenAI function schema."""
|
|
217
265
|
return self._make_schema(
|
|
218
266
|
properties={
|
|
219
|
-
"
|
|
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": {
|
|
267
|
+
"plan": {
|
|
260
268
|
"type": "string",
|
|
261
|
-
"description": "
|
|
269
|
+
"description": "Optional: The implementation plan as markdown. If not provided, reads from the plan file.",
|
|
262
270
|
},
|
|
263
271
|
},
|
|
264
|
-
required=[
|
|
272
|
+
required=[],
|
|
265
273
|
)
|
|
266
274
|
|
|
267
275
|
|
emdash_core/agent/tools/task.py
CHANGED
|
@@ -62,6 +62,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
62
62
|
max_turns: int = 10,
|
|
63
63
|
run_in_background: bool = False,
|
|
64
64
|
resume: Optional[str] = None,
|
|
65
|
+
thoroughness: str = "medium",
|
|
65
66
|
**kwargs,
|
|
66
67
|
) -> ToolResult:
|
|
67
68
|
"""Spawn a sub-agent to perform a task.
|
|
@@ -74,6 +75,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
74
75
|
max_turns: Maximum API round-trips
|
|
75
76
|
run_in_background: Run asynchronously
|
|
76
77
|
resume: Agent ID to resume from
|
|
78
|
+
thoroughness: Search thoroughness level (quick, medium, thorough)
|
|
77
79
|
|
|
78
80
|
Returns:
|
|
79
81
|
ToolResult with agent results or background task info
|
|
@@ -85,13 +87,22 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
85
87
|
suggestions=["Provide a clear task description in 'prompt'"],
|
|
86
88
|
)
|
|
87
89
|
|
|
88
|
-
available_types = list_agent_types()
|
|
90
|
+
available_types = list_agent_types(self.repo_root)
|
|
91
|
+
log.info(f"TaskTool: repo_root={self.repo_root}, available_types={available_types}")
|
|
89
92
|
if subagent_type not in available_types:
|
|
90
93
|
return ToolResult.error_result(
|
|
91
94
|
f"Unknown agent type: {subagent_type}",
|
|
92
|
-
suggestions=[
|
|
95
|
+
suggestions=[
|
|
96
|
+
f"Available types: {available_types}",
|
|
97
|
+
f"Searched in: {self.repo_root / '.emdash' / 'agents'}",
|
|
98
|
+
],
|
|
93
99
|
)
|
|
94
100
|
|
|
101
|
+
# Log current mode for debugging
|
|
102
|
+
from .modes import ModeState
|
|
103
|
+
mode_state = ModeState.get_instance()
|
|
104
|
+
log.info(f"TaskTool: current_mode={mode_state.current_mode}, subagent_type={subagent_type}")
|
|
105
|
+
|
|
95
106
|
log.info(
|
|
96
107
|
"Spawning sub-agent type={} model={} prompt={}",
|
|
97
108
|
subagent_type,
|
|
@@ -99,16 +110,26 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
99
110
|
prompt[:50] + "..." if len(prompt) > 50 else prompt,
|
|
100
111
|
)
|
|
101
112
|
|
|
113
|
+
# Emit subagent start event for UI visibility
|
|
114
|
+
if self.emitter:
|
|
115
|
+
from ..events import EventType
|
|
116
|
+
self.emitter.emit(EventType.SUBAGENT_START, {
|
|
117
|
+
"agent_type": subagent_type,
|
|
118
|
+
"prompt": prompt[:100] + "..." if len(prompt) > 100 else prompt,
|
|
119
|
+
"description": description,
|
|
120
|
+
})
|
|
121
|
+
|
|
102
122
|
if run_in_background:
|
|
103
|
-
return self._run_background(subagent_type, prompt, max_turns)
|
|
123
|
+
return self._run_background(subagent_type, prompt, max_turns, thoroughness)
|
|
104
124
|
else:
|
|
105
|
-
return self._run_sync(subagent_type, prompt, max_turns)
|
|
125
|
+
return self._run_sync(subagent_type, prompt, max_turns, thoroughness)
|
|
106
126
|
|
|
107
127
|
def _run_sync(
|
|
108
128
|
self,
|
|
109
129
|
subagent_type: str,
|
|
110
130
|
prompt: str,
|
|
111
131
|
max_turns: int,
|
|
132
|
+
thoroughness: str = "medium",
|
|
112
133
|
) -> ToolResult:
|
|
113
134
|
"""Run sub-agent synchronously in the same process.
|
|
114
135
|
|
|
@@ -116,6 +137,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
116
137
|
subagent_type: Agent type
|
|
117
138
|
prompt: Task prompt
|
|
118
139
|
max_turns: Maximum API round-trips
|
|
140
|
+
thoroughness: Search thoroughness level
|
|
119
141
|
|
|
120
142
|
Returns:
|
|
121
143
|
ToolResult with agent results
|
|
@@ -127,8 +149,20 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
127
149
|
repo_root=self.repo_root,
|
|
128
150
|
emitter=self.emitter,
|
|
129
151
|
max_turns=max_turns,
|
|
152
|
+
thoroughness=thoroughness,
|
|
130
153
|
)
|
|
131
154
|
|
|
155
|
+
# Emit subagent end event
|
|
156
|
+
if self.emitter:
|
|
157
|
+
from ..events import EventType
|
|
158
|
+
self.emitter.emit(EventType.SUBAGENT_END, {
|
|
159
|
+
"agent_type": subagent_type,
|
|
160
|
+
"success": result.success,
|
|
161
|
+
"iterations": result.iterations,
|
|
162
|
+
"files_explored": len(result.files_explored),
|
|
163
|
+
"execution_time": result.execution_time,
|
|
164
|
+
})
|
|
165
|
+
|
|
132
166
|
if result.success:
|
|
133
167
|
return ToolResult.success_result(
|
|
134
168
|
data=result.to_dict(),
|
|
@@ -149,6 +183,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
149
183
|
subagent_type: str,
|
|
150
184
|
prompt: str,
|
|
151
185
|
max_turns: int,
|
|
186
|
+
thoroughness: str = "medium",
|
|
152
187
|
) -> ToolResult:
|
|
153
188
|
"""Run sub-agent in background using a thread.
|
|
154
189
|
|
|
@@ -156,6 +191,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
156
191
|
subagent_type: Agent type
|
|
157
192
|
prompt: Task prompt
|
|
158
193
|
max_turns: Maximum API round-trips
|
|
194
|
+
thoroughness: Search thoroughness level
|
|
159
195
|
|
|
160
196
|
Returns:
|
|
161
197
|
ToolResult with task info
|
|
@@ -175,6 +211,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
175
211
|
repo_root=self.repo_root,
|
|
176
212
|
emitter=self.emitter,
|
|
177
213
|
max_turns=max_turns,
|
|
214
|
+
thoroughness=thoroughness,
|
|
178
215
|
)
|
|
179
216
|
|
|
180
217
|
# Store future for later retrieval (attach to class for now)
|
|
@@ -221,6 +258,9 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
221
258
|
|
|
222
259
|
def get_schema(self) -> dict:
|
|
223
260
|
"""Get OpenAI function schema."""
|
|
261
|
+
# Get available agent types dynamically (includes custom agents)
|
|
262
|
+
available_types = list_agent_types(self.repo_root)
|
|
263
|
+
|
|
224
264
|
return self._make_schema(
|
|
225
265
|
properties={
|
|
226
266
|
"description": {
|
|
@@ -233,8 +273,8 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
233
273
|
},
|
|
234
274
|
"subagent_type": {
|
|
235
275
|
"type": "string",
|
|
236
|
-
"enum":
|
|
237
|
-
"description": "Type of specialized agent",
|
|
276
|
+
"enum": available_types,
|
|
277
|
+
"description": f"Type of specialized agent. Available: {', '.join(available_types)}",
|
|
238
278
|
"default": "Explore",
|
|
239
279
|
},
|
|
240
280
|
"model_tier": {
|
|
@@ -257,6 +297,12 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
257
297
|
"type": "string",
|
|
258
298
|
"description": "Agent ID to resume from previous execution",
|
|
259
299
|
},
|
|
300
|
+
"thoroughness": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"enum": ["quick", "medium", "thorough"],
|
|
303
|
+
"description": "Search thoroughness: quick (basic searches), medium (moderate exploration), thorough (comprehensive analysis)",
|
|
304
|
+
"default": "medium",
|
|
305
|
+
},
|
|
260
306
|
},
|
|
261
307
|
required=["prompt"],
|
|
262
308
|
)
|