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
emdash_core/agent/__init__.py
CHANGED
|
@@ -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
|
]
|
emdash_core/agent/agents.py
CHANGED
|
@@ -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
|
|
209
|
+
"""Parse YAML frontmatter.
|
|
136
210
|
|
|
137
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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]:
|
emdash_core/agent/events.py
CHANGED
|
@@ -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(
|
|
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
|
|
254
|
+
def emit_plan_mode_requested(
|
|
231
255
|
self,
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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:
|