emdash-core 0.1.25__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/agent/__init__.py +4 -0
- emdash_core/agent/events.py +42 -20
- emdash_core/agent/inprocess_subagent.py +123 -10
- emdash_core/agent/prompts/__init__.py +4 -3
- emdash_core/agent/prompts/main_agent.py +32 -2
- emdash_core/agent/prompts/plan_mode.py +236 -107
- emdash_core/agent/prompts/subagents.py +79 -15
- emdash_core/agent/prompts/workflow.py +145 -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 +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 +47 -8
- emdash_core/agent/toolkit.py +46 -14
- emdash_core/agent/toolkits/plan.py +9 -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 +41 -2
- emdash_core/api/agent.py +555 -1
- 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.33.dist-info}/METADATA +2 -1
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/RECORD +31 -24
- emdash_core/agent/runner.py +0 -1123
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/WHEEL +0 -0
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/entry_points.txt +0 -0
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
|
|
@@ -92,6 +94,11 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
92
94
|
suggestions=[f"Available types: {available_types}"],
|
|
93
95
|
)
|
|
94
96
|
|
|
97
|
+
# Log current mode for debugging
|
|
98
|
+
from .modes import ModeState
|
|
99
|
+
mode_state = ModeState.get_instance()
|
|
100
|
+
log.info(f"TaskTool: current_mode={mode_state.current_mode}, subagent_type={subagent_type}")
|
|
101
|
+
|
|
95
102
|
log.info(
|
|
96
103
|
"Spawning sub-agent type={} model={} prompt={}",
|
|
97
104
|
subagent_type,
|
|
@@ -99,16 +106,26 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
99
106
|
prompt[:50] + "..." if len(prompt) > 50 else prompt,
|
|
100
107
|
)
|
|
101
108
|
|
|
109
|
+
# Emit subagent start event for UI visibility
|
|
110
|
+
if self.emitter:
|
|
111
|
+
from ..events import EventType
|
|
112
|
+
self.emitter.emit(EventType.SUBAGENT_START, {
|
|
113
|
+
"agent_type": subagent_type,
|
|
114
|
+
"prompt": prompt[:100] + "..." if len(prompt) > 100 else prompt,
|
|
115
|
+
"description": description,
|
|
116
|
+
})
|
|
117
|
+
|
|
102
118
|
if run_in_background:
|
|
103
|
-
return self._run_background(subagent_type, prompt, max_turns)
|
|
119
|
+
return self._run_background(subagent_type, prompt, max_turns, thoroughness)
|
|
104
120
|
else:
|
|
105
|
-
return self._run_sync(subagent_type, prompt, max_turns)
|
|
121
|
+
return self._run_sync(subagent_type, prompt, max_turns, thoroughness)
|
|
106
122
|
|
|
107
123
|
def _run_sync(
|
|
108
124
|
self,
|
|
109
125
|
subagent_type: str,
|
|
110
126
|
prompt: str,
|
|
111
127
|
max_turns: int,
|
|
128
|
+
thoroughness: str = "medium",
|
|
112
129
|
) -> ToolResult:
|
|
113
130
|
"""Run sub-agent synchronously in the same process.
|
|
114
131
|
|
|
@@ -116,6 +133,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
116
133
|
subagent_type: Agent type
|
|
117
134
|
prompt: Task prompt
|
|
118
135
|
max_turns: Maximum API round-trips
|
|
136
|
+
thoroughness: Search thoroughness level
|
|
119
137
|
|
|
120
138
|
Returns:
|
|
121
139
|
ToolResult with agent results
|
|
@@ -127,8 +145,20 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
127
145
|
repo_root=self.repo_root,
|
|
128
146
|
emitter=self.emitter,
|
|
129
147
|
max_turns=max_turns,
|
|
148
|
+
thoroughness=thoroughness,
|
|
130
149
|
)
|
|
131
150
|
|
|
151
|
+
# Emit subagent end event
|
|
152
|
+
if self.emitter:
|
|
153
|
+
from ..events import EventType
|
|
154
|
+
self.emitter.emit(EventType.SUBAGENT_END, {
|
|
155
|
+
"agent_type": subagent_type,
|
|
156
|
+
"success": result.success,
|
|
157
|
+
"iterations": result.iterations,
|
|
158
|
+
"files_explored": len(result.files_explored),
|
|
159
|
+
"execution_time": result.execution_time,
|
|
160
|
+
})
|
|
161
|
+
|
|
132
162
|
if result.success:
|
|
133
163
|
return ToolResult.success_result(
|
|
134
164
|
data=result.to_dict(),
|
|
@@ -149,6 +179,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
149
179
|
subagent_type: str,
|
|
150
180
|
prompt: str,
|
|
151
181
|
max_turns: int,
|
|
182
|
+
thoroughness: str = "medium",
|
|
152
183
|
) -> ToolResult:
|
|
153
184
|
"""Run sub-agent in background using a thread.
|
|
154
185
|
|
|
@@ -156,6 +187,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
156
187
|
subagent_type: Agent type
|
|
157
188
|
prompt: Task prompt
|
|
158
189
|
max_turns: Maximum API round-trips
|
|
190
|
+
thoroughness: Search thoroughness level
|
|
159
191
|
|
|
160
192
|
Returns:
|
|
161
193
|
ToolResult with task info
|
|
@@ -175,6 +207,7 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
175
207
|
repo_root=self.repo_root,
|
|
176
208
|
emitter=self.emitter,
|
|
177
209
|
max_turns=max_turns,
|
|
210
|
+
thoroughness=thoroughness,
|
|
178
211
|
)
|
|
179
212
|
|
|
180
213
|
# Store future for later retrieval (attach to class for now)
|
|
@@ -257,6 +290,12 @@ Multiple sub-agents can be launched in parallel."""
|
|
|
257
290
|
"type": "string",
|
|
258
291
|
"description": "Agent ID to resume from previous execution",
|
|
259
292
|
},
|
|
293
|
+
"thoroughness": {
|
|
294
|
+
"type": "string",
|
|
295
|
+
"enum": ["quick", "medium", "thorough"],
|
|
296
|
+
"description": "Search thoroughness: quick (basic searches), medium (moderate exploration), thorough (comprehensive analysis)",
|
|
297
|
+
"default": "medium",
|
|
298
|
+
},
|
|
260
299
|
},
|
|
261
300
|
required=["prompt"],
|
|
262
301
|
)
|