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.
Files changed (55) hide show
  1. emdash_core/__init__.py +6 -1
  2. emdash_core/agent/__init__.py +4 -0
  3. emdash_core/agent/events.py +52 -1
  4. emdash_core/agent/inprocess_subagent.py +123 -10
  5. emdash_core/agent/prompts/__init__.py +6 -0
  6. emdash_core/agent/prompts/main_agent.py +53 -3
  7. emdash_core/agent/prompts/plan_mode.py +255 -0
  8. emdash_core/agent/prompts/subagents.py +84 -16
  9. emdash_core/agent/prompts/workflow.py +270 -56
  10. emdash_core/agent/providers/base.py +4 -0
  11. emdash_core/agent/providers/factory.py +2 -2
  12. emdash_core/agent/providers/models.py +7 -0
  13. emdash_core/agent/providers/openai_provider.py +137 -13
  14. emdash_core/agent/runner/__init__.py +49 -0
  15. emdash_core/agent/runner/agent_runner.py +753 -0
  16. emdash_core/agent/runner/context.py +451 -0
  17. emdash_core/agent/runner/factory.py +108 -0
  18. emdash_core/agent/runner/plan.py +217 -0
  19. emdash_core/agent/runner/sdk_runner.py +324 -0
  20. emdash_core/agent/runner/utils.py +67 -0
  21. emdash_core/agent/skills.py +358 -0
  22. emdash_core/agent/toolkit.py +85 -5
  23. emdash_core/agent/toolkits/plan.py +9 -11
  24. emdash_core/agent/tools/__init__.py +3 -2
  25. emdash_core/agent/tools/coding.py +48 -4
  26. emdash_core/agent/tools/modes.py +207 -55
  27. emdash_core/agent/tools/search.py +4 -0
  28. emdash_core/agent/tools/skill.py +193 -0
  29. emdash_core/agent/tools/spec.py +61 -94
  30. emdash_core/agent/tools/task.py +41 -2
  31. emdash_core/agent/tools/tasks.py +15 -78
  32. emdash_core/api/agent.py +562 -8
  33. emdash_core/api/index.py +1 -1
  34. emdash_core/api/projectmd.py +4 -2
  35. emdash_core/api/router.py +2 -0
  36. emdash_core/api/skills.py +241 -0
  37. emdash_core/checkpoint/__init__.py +40 -0
  38. emdash_core/checkpoint/cli.py +175 -0
  39. emdash_core/checkpoint/git_operations.py +250 -0
  40. emdash_core/checkpoint/manager.py +231 -0
  41. emdash_core/checkpoint/models.py +107 -0
  42. emdash_core/checkpoint/storage.py +201 -0
  43. emdash_core/config.py +1 -1
  44. emdash_core/core/config.py +18 -2
  45. emdash_core/graph/schema.py +5 -5
  46. emdash_core/ingestion/orchestrator.py +19 -10
  47. emdash_core/models/agent.py +1 -1
  48. emdash_core/server.py +42 -0
  49. emdash_core/skills/frontend-design/SKILL.md +56 -0
  50. emdash_core/sse/stream.py +5 -0
  51. {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/METADATA +2 -2
  52. {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/RECORD +54 -37
  53. {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/entry_points.txt +1 -0
  54. emdash_core/agent/runner.py +0 -601
  55. {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/WHEEL +0 -0
emdash_core/__init__.py CHANGED
@@ -1,3 +1,8 @@
1
1
  """EmDash Core - FastAPI server for code intelligence."""
2
2
 
3
- __version__ = "0.1.0"
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("emdash-core")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0-dev"
@@ -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,13 @@ 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"
37
+ PLAN_SUBMITTED = "plan_submitted"
31
38
 
32
39
  # Errors
33
40
  ERROR = "error"
@@ -143,16 +150,23 @@ class AgentEventEmitter:
143
150
 
144
151
  return event
145
152
 
146
- 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:
147
159
  """Convenience method to emit a tool start event.
148
160
 
149
161
  Args:
150
162
  name: Tool name
151
163
  args: Tool arguments
164
+ tool_id: Unique ID for this tool call (for matching with result)
152
165
  """
153
166
  return self.emit(EventType.TOOL_START, {
154
167
  "name": name,
155
168
  "args": args or {},
169
+ "tool_id": tool_id,
156
170
  })
157
171
 
158
172
  def emit_tool_result(
@@ -161,6 +175,7 @@ class AgentEventEmitter:
161
175
  success: bool,
162
176
  summary: str | None = None,
163
177
  data: dict[str, Any] | None = None,
178
+ tool_id: str | None = None,
164
179
  ) -> AgentEvent:
165
180
  """Convenience method to emit a tool result event.
166
181
 
@@ -169,12 +184,14 @@ class AgentEventEmitter:
169
184
  success: Whether the tool succeeded
170
185
  summary: Brief summary of the result
171
186
  data: Full result data (may be truncated by handlers)
187
+ tool_id: Unique ID for this tool call (for matching with start)
172
188
  """
173
189
  return self.emit(EventType.TOOL_RESULT, {
174
190
  "name": name,
175
191
  "success": success,
176
192
  "summary": summary,
177
193
  "data": data,
194
+ "tool_id": tool_id,
178
195
  })
179
196
 
180
197
  def emit_thinking(self, message: str) -> AgentEvent:
@@ -207,6 +224,14 @@ class AgentEventEmitter:
207
224
  event_type = EventType.RESPONSE if is_final else EventType.PARTIAL_RESPONSE
208
225
  return self.emit(event_type, {"content": content})
209
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
+
210
235
  def emit_clarification(
211
236
  self,
212
237
  question: str,
@@ -226,6 +251,32 @@ class AgentEventEmitter:
226
251
  "options": options,
227
252
  })
228
253
 
254
+ def emit_plan_mode_requested(
255
+ self,
256
+ reason: str,
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:
271
+ """Convenience method to emit a plan submission event.
272
+
273
+ Args:
274
+ plan: The implementation plan as markdown
275
+ """
276
+ return self.emit(EventType.PLAN_SUBMITTED, {
277
+ "plan": plan,
278
+ })
279
+
229
280
  def emit_error(self, message: str, details: str | None = None) -> AgentEvent:
230
281
  """Convenience method to emit an error.
231
282
 
@@ -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
 
@@ -11,9 +11,12 @@ from .workflow import (
11
11
  EXPLORATION_OUTPUT_FORMAT,
12
12
  PLAN_TEMPLATE,
13
13
  SIZING_GUIDELINES,
14
+ PARALLEL_EXECUTION,
14
15
  )
15
16
  from .main_agent import (
16
17
  BASE_SYSTEM_PROMPT,
18
+ CODE_MODE_PROMPT,
19
+ PLAN_MODE_PROMPT,
17
20
  build_system_prompt,
18
21
  build_tools_section,
19
22
  )
@@ -28,8 +31,11 @@ __all__ = [
28
31
  "EXPLORATION_OUTPUT_FORMAT",
29
32
  "PLAN_TEMPLATE",
30
33
  "SIZING_GUIDELINES",
34
+ "PARALLEL_EXECUTION",
31
35
  # Main agent
32
36
  "BASE_SYSTEM_PROMPT",
37
+ "CODE_MODE_PROMPT",
38
+ "PLAN_MODE_PROMPT",
33
39
  "build_system_prompt",
34
40
  "build_tools_section",
35
41
  # Sub-agents
@@ -8,13 +8,23 @@ from .workflow import (
8
8
  WORKFLOW_PATTERNS,
9
9
  EXPLORATION_STRATEGY,
10
10
  OUTPUT_GUIDELINES,
11
+ PARALLEL_EXECUTION,
12
+ TODO_LIST_GUIDANCE,
11
13
  )
12
14
 
13
15
  # Base system prompt template with placeholder for tools
14
- 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.
15
17
 
16
18
  {tools_section}
17
- """ + WORKFLOW_PATTERNS + 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
18
28
 
19
29
 
20
30
  def build_system_prompt(toolkit) -> str:
@@ -27,7 +37,47 @@ def build_system_prompt(toolkit) -> str:
27
37
  Complete system prompt string
28
38
  """
29
39
  tools_section = build_tools_section(toolkit)
30
- return BASE_SYSTEM_PROMPT.format(tools_section=tools_section)
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
44
+ prompt = BASE_SYSTEM_PROMPT.format(tools_section=tools_section)
45
+
46
+ # Add rules section if there are rules defined
47
+ if rules_section:
48
+ prompt += "\n" + rules_section
49
+
50
+ # Add skills section if there are skills available
51
+ if skills_section:
52
+ prompt += "\n" + skills_section
53
+
54
+ return prompt
55
+
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
+
71
+ def build_skills_section() -> str:
72
+ """Build the skills section of the system prompt.
73
+
74
+ Returns:
75
+ Formatted string with available skills, or empty string if none
76
+ """
77
+ from ..skills import SkillRegistry
78
+
79
+ registry = SkillRegistry.get_instance()
80
+ return registry.get_skills_for_prompt()
31
81
 
32
82
 
33
83
  def build_tools_section(toolkit) -> str: