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.
Files changed (32) hide show
  1. emdash_core/agent/__init__.py +4 -0
  2. emdash_core/agent/events.py +42 -20
  3. emdash_core/agent/inprocess_subagent.py +123 -10
  4. emdash_core/agent/prompts/__init__.py +4 -3
  5. emdash_core/agent/prompts/main_agent.py +32 -2
  6. emdash_core/agent/prompts/plan_mode.py +236 -107
  7. emdash_core/agent/prompts/subagents.py +79 -15
  8. emdash_core/agent/prompts/workflow.py +145 -26
  9. emdash_core/agent/providers/factory.py +2 -2
  10. emdash_core/agent/providers/openai_provider.py +67 -15
  11. emdash_core/agent/runner/__init__.py +49 -0
  12. emdash_core/agent/runner/agent_runner.py +753 -0
  13. emdash_core/agent/runner/context.py +451 -0
  14. emdash_core/agent/runner/factory.py +108 -0
  15. emdash_core/agent/runner/plan.py +217 -0
  16. emdash_core/agent/runner/sdk_runner.py +324 -0
  17. emdash_core/agent/runner/utils.py +67 -0
  18. emdash_core/agent/skills.py +47 -8
  19. emdash_core/agent/toolkit.py +46 -14
  20. emdash_core/agent/toolkits/plan.py +9 -11
  21. emdash_core/agent/tools/__init__.py +2 -2
  22. emdash_core/agent/tools/coding.py +48 -4
  23. emdash_core/agent/tools/modes.py +151 -143
  24. emdash_core/agent/tools/task.py +41 -2
  25. emdash_core/api/agent.py +555 -1
  26. emdash_core/skills/frontend-design/SKILL.md +56 -0
  27. emdash_core/sse/stream.py +4 -0
  28. {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/METADATA +2 -1
  29. {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/RECORD +31 -24
  30. emdash_core/agent/runner.py +0 -1123
  31. {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/WHEEL +0 -0
  32. {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/entry_points.txt +0 -0
@@ -19,6 +19,9 @@ def __getattr__(name: str):
19
19
  elif name == "AgentRunner":
20
20
  from .runner import AgentRunner
21
21
  return AgentRunner
22
+ elif name == "SafeJSONEncoder":
23
+ from .runner import SafeJSONEncoder
24
+ return SafeJSONEncoder
22
25
  elif name == "ToolResult":
23
26
  from .tools.base import ToolResult
24
27
  return ToolResult
@@ -32,6 +35,7 @@ __all__ = [
32
35
  "AgentToolkit",
33
36
  "AgentSession",
34
37
  "AgentRunner",
38
+ "SafeJSONEncoder",
35
39
  "ToolResult",
36
40
  "ToolCategory",
37
41
  ]
@@ -17,6 +17,10 @@ class EventType(Enum):
17
17
  TOOL_START = "tool_start"
18
18
  TOOL_RESULT = "tool_result"
19
19
 
20
+ # Sub-agent lifecycle
21
+ SUBAGENT_START = "subagent_start"
22
+ SUBAGENT_END = "subagent_end"
23
+
20
24
  # Agent thinking/progress
21
25
  THINKING = "thinking"
22
26
  PROGRESS = "progress"
@@ -24,10 +28,12 @@ class EventType(Enum):
24
28
  # Output
25
29
  RESPONSE = "response"
26
30
  PARTIAL_RESPONSE = "partial_response"
31
+ ASSISTANT_TEXT = "assistant_text" # Intermediate text between tool calls
27
32
 
28
33
  # Interaction
29
34
  CLARIFICATION = "clarification"
30
35
  CLARIFICATION_RESPONSE = "clarification_response"
36
+ PLAN_MODE_REQUESTED = "plan_mode_requested"
31
37
  PLAN_SUBMITTED = "plan_submitted"
32
38
 
33
39
  # Errors
@@ -144,16 +150,23 @@ class AgentEventEmitter:
144
150
 
145
151
  return event
146
152
 
147
- def emit_tool_start(self, name: str, args: dict[str, Any] | None = None) -> AgentEvent:
153
+ def emit_tool_start(
154
+ self,
155
+ name: str,
156
+ args: dict[str, Any] | None = None,
157
+ tool_id: str | None = None,
158
+ ) -> AgentEvent:
148
159
  """Convenience method to emit a tool start event.
149
160
 
150
161
  Args:
151
162
  name: Tool name
152
163
  args: Tool arguments
164
+ tool_id: Unique ID for this tool call (for matching with result)
153
165
  """
154
166
  return self.emit(EventType.TOOL_START, {
155
167
  "name": name,
156
168
  "args": args or {},
169
+ "tool_id": tool_id,
157
170
  })
158
171
 
159
172
  def emit_tool_result(
@@ -162,6 +175,7 @@ class AgentEventEmitter:
162
175
  success: bool,
163
176
  summary: str | None = None,
164
177
  data: dict[str, Any] | None = None,
178
+ tool_id: str | None = None,
165
179
  ) -> AgentEvent:
166
180
  """Convenience method to emit a tool result event.
167
181
 
@@ -170,12 +184,14 @@ class AgentEventEmitter:
170
184
  success: Whether the tool succeeded
171
185
  summary: Brief summary of the result
172
186
  data: Full result data (may be truncated by handlers)
187
+ tool_id: Unique ID for this tool call (for matching with start)
173
188
  """
174
189
  return self.emit(EventType.TOOL_RESULT, {
175
190
  "name": name,
176
191
  "success": success,
177
192
  "summary": summary,
178
193
  "data": data,
194
+ "tool_id": tool_id,
179
195
  })
180
196
 
181
197
  def emit_thinking(self, message: str) -> AgentEvent:
@@ -208,6 +224,14 @@ class AgentEventEmitter:
208
224
  event_type = EventType.RESPONSE if is_final else EventType.PARTIAL_RESPONSE
209
225
  return self.emit(event_type, {"content": content})
210
226
 
227
+ def emit_assistant_text(self, content: str) -> AgentEvent:
228
+ """Emit intermediate assistant text (shown between tool calls).
229
+
230
+ Args:
231
+ content: Text content from assistant (e.g., "Let me read the file...")
232
+ """
233
+ return self.emit(EventType.ASSISTANT_TEXT, {"content": content})
234
+
211
235
  def emit_clarification(
212
236
  self,
213
237
  question: str,
@@ -227,32 +251,30 @@ class AgentEventEmitter:
227
251
  "options": options,
228
252
  })
229
253
 
230
- def emit_plan_submitted(
254
+ def emit_plan_mode_requested(
231
255
  self,
232
- title: str,
233
- summary: str,
234
- files_to_modify: list[dict] | None = None,
235
- implementation_steps: list[str] | None = None,
236
- risks: list[str] | None = None,
237
- testing_strategy: str | None = None,
256
+ reason: str,
238
257
  ) -> AgentEvent:
258
+ """Convenience method to emit a plan mode request event.
259
+
260
+ This is emitted when the agent calls enter_plan_mode tool,
261
+ requesting user consent to enter plan mode.
262
+
263
+ Args:
264
+ reason: Why the agent wants to enter plan mode
265
+ """
266
+ return self.emit(EventType.PLAN_MODE_REQUESTED, {
267
+ "reason": reason,
268
+ })
269
+
270
+ def emit_plan_submitted(self, plan: str) -> AgentEvent:
239
271
  """Convenience method to emit a plan submission event.
240
272
 
241
273
  Args:
242
- title: Plan title
243
- summary: Plan summary
244
- files_to_modify: List of files with path, lines, changes
245
- implementation_steps: Ordered implementation steps
246
- risks: Potential risks or considerations
247
- testing_strategy: How changes will be tested
274
+ plan: The implementation plan as markdown
248
275
  """
249
276
  return self.emit(EventType.PLAN_SUBMITTED, {
250
- "title": title,
251
- "summary": summary,
252
- "files_to_modify": files_to_modify or [],
253
- "implementation_steps": implementation_steps or [],
254
- "risks": risks or [],
255
- "testing_strategy": testing_strategy or "",
277
+ "plan": plan,
256
278
  })
257
279
 
258
280
  def emit_error(self, message: str, details: str | None = None) -> AgentEvent:
@@ -62,6 +62,7 @@ class InProcessSubAgent:
62
62
  model: Optional[str] = None,
63
63
  max_turns: int = 10,
64
64
  agent_id: Optional[str] = None,
65
+ thoroughness: str = "medium",
65
66
  ):
66
67
  """Initialize in-process sub-agent.
67
68
 
@@ -72,12 +73,14 @@ class InProcessSubAgent:
72
73
  model: Model to use (defaults to fast model)
73
74
  max_turns: Maximum iterations
74
75
  agent_id: Optional agent ID (generated if not provided)
76
+ thoroughness: Search thoroughness level (quick, medium, thorough)
75
77
  """
76
78
  self.subagent_type = subagent_type
77
79
  self.repo_root = repo_root.resolve()
78
80
  self.emitter = emitter
79
81
  self.max_turns = max_turns
80
82
  self.agent_id = agent_id or str(uuid.uuid4())[:8]
83
+ self.thoroughness = thoroughness
81
84
 
82
85
  # Get toolkit for this agent type
83
86
  self.toolkit = get_toolkit(subagent_type, repo_root)
@@ -86,13 +89,40 @@ class InProcessSubAgent:
86
89
  model_name = model or DEFAULT_MODEL
87
90
  self.provider = get_provider(model_name)
88
91
 
89
- # Get system prompt
90
- self.system_prompt = get_subagent_prompt(subagent_type)
92
+ # Get system prompt and inject thoroughness level
93
+ base_prompt = get_subagent_prompt(subagent_type)
94
+ self.system_prompt = self._inject_thoroughness(base_prompt)
91
95
 
92
96
  # Tracking
93
97
  self.files_explored: set[str] = set()
94
98
  self.tools_used: list[str] = []
95
99
 
100
+ def _inject_thoroughness(self, prompt: str) -> str:
101
+ """Inject thoroughness level into the system prompt."""
102
+ thoroughness_guidance = {
103
+ "quick": """
104
+ ## Thoroughness Level: QUICK
105
+ - Do basic searches only - find the most obvious matches first
106
+ - Stop after finding 2-3 relevant files
107
+ - Don't explore deeply - just locate the key files
108
+ - Prioritize speed over completeness""",
109
+ "medium": """
110
+ ## Thoroughness Level: MEDIUM
111
+ - Do moderate exploration - check multiple locations
112
+ - Follow 1-2 levels of imports/references
113
+ - Balance speed with coverage
114
+ - Stop when you have reasonable confidence""",
115
+ "thorough": """
116
+ ## Thoroughness Level: THOROUGH
117
+ - Do comprehensive analysis across the codebase
118
+ - Check multiple naming conventions and locations
119
+ - Follow import chains and cross-references deeply
120
+ - Explore edge cases and alternative implementations
121
+ - Only stop when you've exhausted relevant areas""",
122
+ }
123
+ guidance = thoroughness_guidance.get(self.thoroughness, thoroughness_guidance["medium"])
124
+ return prompt + "\n" + guidance
125
+
96
126
  def _emit(self, event_type: str, **data) -> None:
97
127
  """Emit event with agent tagging.
98
128
 
@@ -115,6 +145,50 @@ class InProcessSubAgent:
115
145
  if event_type in event_map:
116
146
  self.emitter.emit(event_map[event_type], data)
117
147
 
148
+ def _get_project_context(self) -> str:
149
+ """Get PROJECT.md and directory structure for context."""
150
+ context_parts = []
151
+
152
+ # Try to read PROJECT.md
153
+ project_md = self.repo_root / "PROJECT.md"
154
+ if project_md.exists():
155
+ try:
156
+ content = project_md.read_text()
157
+ # Truncate if too long
158
+ if len(content) > 8000:
159
+ content = content[:8000] + "\n...[truncated]"
160
+ context_parts.append(f"## PROJECT.md\n\n{content}")
161
+ except Exception as e:
162
+ log.debug(f"Could not read PROJECT.md: {e}")
163
+
164
+ # Get directory structure (top 2 levels)
165
+ try:
166
+ structure_lines = ["## Project Structure\n"]
167
+ for item in sorted(self.repo_root.iterdir()):
168
+ if item.name.startswith(".") and item.name not in (".emdash",):
169
+ continue
170
+ if item.name in ("node_modules", "__pycache__", ".git", "dist", "build", ".venv", "venv"):
171
+ continue
172
+ if item.is_dir():
173
+ structure_lines.append(f" {item.name}/")
174
+ # Show first level contents
175
+ try:
176
+ for subitem in sorted(item.iterdir())[:10]:
177
+ if not subitem.name.startswith("."):
178
+ suffix = "/" if subitem.is_dir() else ""
179
+ structure_lines.append(f" {subitem.name}{suffix}")
180
+ if len(list(item.iterdir())) > 10:
181
+ structure_lines.append(f" ...")
182
+ except PermissionError:
183
+ pass
184
+ else:
185
+ structure_lines.append(f" {item.name}")
186
+ context_parts.append("\n".join(structure_lines))
187
+ except Exception as e:
188
+ log.debug(f"Could not get directory structure: {e}")
189
+
190
+ return "\n\n".join(context_parts) if context_parts else ""
191
+
118
192
  def run(self, prompt: str) -> SubAgentResult:
119
193
  """Execute the task and return results.
120
194
 
@@ -130,6 +204,19 @@ class InProcessSubAgent:
130
204
  last_content = ""
131
205
  error = None
132
206
 
207
+ # For Plan agents, inject project context
208
+ if self.subagent_type == "Plan":
209
+ context = self._get_project_context()
210
+ if context:
211
+ prompt = f"""Here is context about the project:
212
+
213
+ {context}
214
+
215
+ ---
216
+
217
+ Now, your task:
218
+ {prompt}"""
219
+
133
220
  # Add user message
134
221
  messages.append({"role": "user", "content": prompt})
135
222
 
@@ -265,6 +352,7 @@ def run_subagent(
265
352
  emitter=None,
266
353
  model: Optional[str] = None,
267
354
  max_turns: int = 10,
355
+ thoroughness: str = "medium",
268
356
  ) -> SubAgentResult:
269
357
  """Run a sub-agent synchronously.
270
358
 
@@ -275,18 +363,38 @@ def run_subagent(
275
363
  emitter: Event emitter
276
364
  model: Model to use
277
365
  max_turns: Max iterations
366
+ thoroughness: Search thoroughness level (quick, medium, thorough)
278
367
 
279
368
  Returns:
280
369
  SubAgentResult
281
370
  """
282
- agent = InProcessSubAgent(
283
- subagent_type=subagent_type,
284
- repo_root=repo_root,
285
- emitter=emitter,
286
- model=model,
287
- max_turns=max_turns,
288
- )
289
- return agent.run(prompt)
371
+ try:
372
+ agent = InProcessSubAgent(
373
+ subagent_type=subagent_type,
374
+ repo_root=repo_root,
375
+ emitter=emitter,
376
+ model=model,
377
+ max_turns=max_turns,
378
+ thoroughness=thoroughness,
379
+ )
380
+ return agent.run(prompt)
381
+ except Exception as e:
382
+ # Return a proper error result instead of letting the exception propagate
383
+ # This prevents 0.0s "silent" failures and gives clear error messages
384
+ log.error(f"Failed to create sub-agent: {e}")
385
+ return SubAgentResult(
386
+ success=False,
387
+ agent_type=subagent_type,
388
+ agent_id="init-failed",
389
+ task=prompt,
390
+ summary="",
391
+ files_explored=[],
392
+ findings=[],
393
+ iterations=0,
394
+ tools_used=[],
395
+ execution_time=0.0,
396
+ error=f"Sub-agent initialization failed: {e}",
397
+ )
290
398
 
291
399
 
292
400
  def run_subagent_async(
@@ -296,6 +404,7 @@ def run_subagent_async(
296
404
  emitter=None,
297
405
  model: Optional[str] = None,
298
406
  max_turns: int = 10,
407
+ thoroughness: str = "medium",
299
408
  ) -> Future[SubAgentResult]:
300
409
  """Run a sub-agent asynchronously (returns Future).
301
410
 
@@ -306,6 +415,7 @@ def run_subagent_async(
306
415
  emitter: Event emitter
307
416
  model: Model to use
308
417
  max_turns: Max iterations
418
+ thoroughness: Search thoroughness level (quick, medium, thorough)
309
419
 
310
420
  Returns:
311
421
  Future[SubAgentResult] - call .result() to get result
@@ -319,6 +429,7 @@ def run_subagent_async(
319
429
  emitter=emitter,
320
430
  model=model,
321
431
  max_turns=max_turns,
432
+ thoroughness=thoroughness,
322
433
  )
323
434
 
324
435
 
@@ -335,6 +446,7 @@ def run_subagents_parallel(
335
446
  - prompt: str
336
447
  - model: str (optional)
337
448
  - max_turns: int (optional)
449
+ - thoroughness: str (optional, default "medium")
338
450
  repo_root: Repository root
339
451
  emitter: Shared event emitter
340
452
 
@@ -350,6 +462,7 @@ def run_subagents_parallel(
350
462
  emitter=emitter,
351
463
  model=task.get("model"),
352
464
  max_turns=task.get("max_turns", 10),
465
+ thoroughness=task.get("thoroughness", "medium"),
353
466
  )
354
467
  futures.append(future)
355
468
 
@@ -15,11 +15,12 @@ from .workflow import (
15
15
  )
16
16
  from .main_agent import (
17
17
  BASE_SYSTEM_PROMPT,
18
+ CODE_MODE_PROMPT,
19
+ PLAN_MODE_PROMPT,
18
20
  build_system_prompt,
19
21
  build_tools_section,
20
22
  )
21
23
  from .subagents import SUBAGENT_PROMPTS, get_subagent_prompt
22
- from .plan_mode import PLAN_MODE_PROMPT
23
24
 
24
25
  __all__ = [
25
26
  # Workflow patterns
@@ -33,11 +34,11 @@ __all__ = [
33
34
  "PARALLEL_EXECUTION",
34
35
  # Main agent
35
36
  "BASE_SYSTEM_PROMPT",
37
+ "CODE_MODE_PROMPT",
38
+ "PLAN_MODE_PROMPT",
36
39
  "build_system_prompt",
37
40
  "build_tools_section",
38
41
  # Sub-agents
39
42
  "SUBAGENT_PROMPTS",
40
43
  "get_subagent_prompt",
41
- # Plan mode
42
- "PLAN_MODE_PROMPT",
43
44
  ]
@@ -9,13 +9,22 @@ from .workflow import (
9
9
  EXPLORATION_STRATEGY,
10
10
  OUTPUT_GUIDELINES,
11
11
  PARALLEL_EXECUTION,
12
+ TODO_LIST_GUIDANCE,
12
13
  )
13
14
 
14
15
  # Base system prompt template with placeholder for tools
15
- BASE_SYSTEM_PROMPT = """You are a code exploration and implementation assistant. You orchestrate focused sub-agents for exploration while maintaining the high-level view.
16
+ _BASE_PROMPT = """You are a code exploration and implementation assistant. You orchestrate focused sub-agents for exploration while maintaining the high-level view.
16
17
 
17
18
  {tools_section}
18
- """ + WORKFLOW_PATTERNS + PARALLEL_EXECUTION + EXPLORATION_STRATEGY + OUTPUT_GUIDELINES
19
+ """
20
+
21
+ # Main agent system prompt - same for both code and plan modes
22
+ # Main agent is always an orchestrator that delegates to subagents
23
+ BASE_SYSTEM_PROMPT = _BASE_PROMPT + WORKFLOW_PATTERNS + PARALLEL_EXECUTION + EXPLORATION_STRATEGY + TODO_LIST_GUIDANCE + OUTPUT_GUIDELINES
24
+
25
+ # Legacy aliases
26
+ CODE_MODE_PROMPT = BASE_SYSTEM_PROMPT
27
+ PLAN_MODE_PROMPT = BASE_SYSTEM_PROMPT
19
28
 
20
29
 
21
30
  def build_system_prompt(toolkit) -> str:
@@ -29,8 +38,15 @@ def build_system_prompt(toolkit) -> str:
29
38
  """
30
39
  tools_section = build_tools_section(toolkit)
31
40
  skills_section = build_skills_section()
41
+ rules_section = build_rules_section()
42
+
43
+ # Main agent always uses the same prompt - it orchestrates and delegates
32
44
  prompt = BASE_SYSTEM_PROMPT.format(tools_section=tools_section)
33
45
 
46
+ # Add rules section if there are rules defined
47
+ if rules_section:
48
+ prompt += "\n" + rules_section
49
+
34
50
  # Add skills section if there are skills available
35
51
  if skills_section:
36
52
  prompt += "\n" + skills_section
@@ -38,6 +54,20 @@ def build_system_prompt(toolkit) -> str:
38
54
  return prompt
39
55
 
40
56
 
57
+ def build_rules_section() -> str:
58
+ """Build the rules section of the system prompt.
59
+
60
+ Loads rules from .emdash/rules/*.md files.
61
+
62
+ Returns:
63
+ Formatted string with project rules, or empty string if none
64
+ """
65
+ from ..rules import load_rules, format_rules_for_prompt
66
+
67
+ rules = load_rules()
68
+ return format_rules_for_prompt(rules)
69
+
70
+
41
71
  def build_skills_section() -> str:
42
72
  """Build the skills section of the system prompt.
43
73