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.
Files changed (39) hide show
  1. emdash_core/agent/__init__.py +4 -0
  2. emdash_core/agent/agents.py +84 -23
  3. emdash_core/agent/events.py +42 -20
  4. emdash_core/agent/hooks.py +419 -0
  5. emdash_core/agent/inprocess_subagent.py +166 -18
  6. emdash_core/agent/prompts/__init__.py +4 -3
  7. emdash_core/agent/prompts/main_agent.py +67 -2
  8. emdash_core/agent/prompts/plan_mode.py +236 -107
  9. emdash_core/agent/prompts/subagents.py +103 -23
  10. emdash_core/agent/prompts/workflow.py +159 -26
  11. emdash_core/agent/providers/factory.py +2 -2
  12. emdash_core/agent/providers/openai_provider.py +67 -15
  13. emdash_core/agent/runner/__init__.py +49 -0
  14. emdash_core/agent/runner/agent_runner.py +765 -0
  15. emdash_core/agent/runner/context.py +470 -0
  16. emdash_core/agent/runner/factory.py +108 -0
  17. emdash_core/agent/runner/plan.py +217 -0
  18. emdash_core/agent/runner/sdk_runner.py +324 -0
  19. emdash_core/agent/runner/utils.py +67 -0
  20. emdash_core/agent/skills.py +47 -8
  21. emdash_core/agent/toolkit.py +46 -14
  22. emdash_core/agent/toolkits/__init__.py +117 -18
  23. emdash_core/agent/toolkits/base.py +87 -2
  24. emdash_core/agent/toolkits/explore.py +18 -0
  25. emdash_core/agent/toolkits/plan.py +27 -11
  26. emdash_core/agent/tools/__init__.py +2 -2
  27. emdash_core/agent/tools/coding.py +48 -4
  28. emdash_core/agent/tools/modes.py +151 -143
  29. emdash_core/agent/tools/task.py +52 -6
  30. emdash_core/api/agent.py +706 -1
  31. emdash_core/ingestion/repository.py +17 -198
  32. emdash_core/models/agent.py +4 -0
  33. emdash_core/skills/frontend-design/SKILL.md +56 -0
  34. emdash_core/sse/stream.py +4 -0
  35. {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/METADATA +4 -1
  36. {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/RECORD +38 -30
  37. emdash_core/agent/runner.py +0 -1123
  38. {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.dist-info}/WHEEL +0 -0
  39. {emdash_core-0.1.25.dist-info → emdash_core-0.1.37.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
  ]
@@ -2,16 +2,74 @@
2
2
 
3
3
  Allows users to define custom agent configurations with
4
4
  specialized system prompts and tool selections.
5
+
6
+ Example agent file:
7
+ ```markdown
8
+ ---
9
+ description: GitHub integration agent
10
+ model: claude-sonnet-4-20250514
11
+ tools: [grep, glob, read_file]
12
+ mcp_servers:
13
+ github:
14
+ command: github-mcp-server
15
+ args: []
16
+ env:
17
+ GITHUB_TOKEN: ${GITHUB_TOKEN}
18
+ enabled: true
19
+ filesystem:
20
+ command: npx
21
+ args: [-y, "@anthropic/mcp-server-filesystem", "/tmp"]
22
+ enabled: false # Disabled - won't be started
23
+ ---
24
+
25
+ # System Prompt
26
+
27
+ You are a GitHub integration specialist...
28
+ ```
5
29
  """
6
30
 
7
31
  from dataclasses import dataclass, field
8
32
  from pathlib import Path
9
- from typing import Optional
33
+ from typing import Any, Optional
10
34
  import re
11
35
 
36
+ import yaml
37
+
12
38
  from ..utils.logger import log
13
39
 
14
40
 
41
+ @dataclass
42
+ class AgentMCPServerConfig:
43
+ """MCP server configuration for a custom agent.
44
+
45
+ Attributes:
46
+ name: Server name (key in mcp_servers dict)
47
+ command: Command to run the server
48
+ args: Arguments to pass to the command
49
+ env: Environment variables (supports ${VAR} syntax)
50
+ enabled: Whether this server is enabled (default: True)
51
+ timeout: Timeout in seconds for tool calls
52
+ """
53
+ name: str
54
+ command: str
55
+ args: list[str] = field(default_factory=list)
56
+ env: dict[str, str] = field(default_factory=dict)
57
+ enabled: bool = True
58
+ timeout: int = 30
59
+
60
+ @classmethod
61
+ def from_dict(cls, name: str, data: dict[str, Any]) -> "AgentMCPServerConfig":
62
+ """Create from dictionary parsed from YAML."""
63
+ return cls(
64
+ name=name,
65
+ command=data.get("command", ""),
66
+ args=data.get("args", []),
67
+ env=data.get("env", {}),
68
+ enabled=data.get("enabled", True),
69
+ timeout=data.get("timeout", 30),
70
+ )
71
+
72
+
15
73
  @dataclass
16
74
  class CustomAgent:
17
75
  """A custom agent configuration loaded from markdown.
@@ -19,16 +77,20 @@ class CustomAgent:
19
77
  Attributes:
20
78
  name: Agent name (from filename)
21
79
  description: Brief description
80
+ model: Model to use for this agent (optional, uses default if not set)
22
81
  system_prompt: Custom system prompt
23
82
  tools: List of tools to enable
83
+ mcp_servers: MCP server configurations for this agent
24
84
  examples: Example interactions
25
85
  file_path: Source file path
26
86
  """
27
87
 
28
88
  name: str
29
89
  description: str = ""
90
+ model: Optional[str] = None
30
91
  system_prompt: str = ""
31
92
  tools: list[str] = field(default_factory=list)
93
+ mcp_servers: list[AgentMCPServerConfig] = field(default_factory=list)
32
94
  examples: list[dict] = field(default_factory=list)
33
95
  file_path: Optional[Path] = None
34
96
 
@@ -121,46 +183,45 @@ def _parse_agent_file(file_path: Path) -> Optional[CustomAgent]:
121
183
  if system_prompt.startswith("# System Prompt"):
122
184
  system_prompt = system_prompt[len("# System Prompt") :].strip()
123
185
 
186
+ # Parse MCP servers from frontmatter
187
+ mcp_servers = []
188
+ mcp_servers_data = frontmatter.get("mcp_servers", {})
189
+ if isinstance(mcp_servers_data, dict):
190
+ for server_name, server_config in mcp_servers_data.items():
191
+ if isinstance(server_config, dict):
192
+ mcp_servers.append(
193
+ AgentMCPServerConfig.from_dict(server_name, server_config)
194
+ )
195
+
124
196
  return CustomAgent(
125
197
  name=file_path.stem,
126
198
  description=frontmatter.get("description", ""),
199
+ model=frontmatter.get("model"),
127
200
  system_prompt=system_prompt,
128
201
  tools=frontmatter.get("tools", []),
202
+ mcp_servers=mcp_servers,
129
203
  examples=examples,
130
204
  file_path=file_path,
131
205
  )
132
206
 
133
207
 
134
208
  def _parse_frontmatter(frontmatter_str: str) -> dict:
135
- """Parse YAML-like frontmatter.
209
+ """Parse YAML frontmatter.
136
210
 
137
- Simple parser for key: value pairs.
211
+ Uses PyYAML for proper nested structure parsing.
138
212
 
139
213
  Args:
140
- frontmatter_str: Frontmatter string
214
+ frontmatter_str: Frontmatter string (YAML format)
141
215
 
142
216
  Returns:
143
217
  Dict of parsed values
144
218
  """
145
- result = {}
146
-
147
- for line in frontmatter_str.strip().split("\n"):
148
- if ":" not in line:
149
- continue
150
-
151
- key, value = line.split(":", 1)
152
- key = key.strip()
153
- value = value.strip()
154
-
155
- # Parse list values
156
- if value.startswith("[") and value.endswith("]"):
157
- # Simple list parsing
158
- items = value[1:-1].split(",")
159
- result[key] = [item.strip().strip("'\"") for item in items if item.strip()]
160
- else:
161
- result[key] = value.strip("'\"")
162
-
163
- return result
219
+ try:
220
+ result = yaml.safe_load(frontmatter_str)
221
+ return result if isinstance(result, dict) else {}
222
+ except yaml.YAMLError as e:
223
+ log.warning(f"Failed to parse frontmatter as YAML: {e}")
224
+ return {}
164
225
 
165
226
 
166
227
  def _parse_examples(examples_str: str) -> list[dict]:
@@ -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: