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.
- emdash_core/__init__.py +6 -1
- emdash_core/agent/__init__.py +4 -0
- emdash_core/agent/events.py +52 -1
- emdash_core/agent/inprocess_subagent.py +123 -10
- emdash_core/agent/prompts/__init__.py +6 -0
- emdash_core/agent/prompts/main_agent.py +53 -3
- emdash_core/agent/prompts/plan_mode.py +255 -0
- emdash_core/agent/prompts/subagents.py +84 -16
- emdash_core/agent/prompts/workflow.py +270 -56
- emdash_core/agent/providers/base.py +4 -0
- emdash_core/agent/providers/factory.py +2 -2
- emdash_core/agent/providers/models.py +7 -0
- emdash_core/agent/providers/openai_provider.py +137 -13
- 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 +358 -0
- emdash_core/agent/toolkit.py +85 -5
- emdash_core/agent/toolkits/plan.py +9 -11
- emdash_core/agent/tools/__init__.py +3 -2
- emdash_core/agent/tools/coding.py +48 -4
- emdash_core/agent/tools/modes.py +207 -55
- emdash_core/agent/tools/search.py +4 -0
- emdash_core/agent/tools/skill.py +193 -0
- emdash_core/agent/tools/spec.py +61 -94
- emdash_core/agent/tools/task.py +41 -2
- emdash_core/agent/tools/tasks.py +15 -78
- emdash_core/api/agent.py +562 -8
- emdash_core/api/index.py +1 -1
- emdash_core/api/projectmd.py +4 -2
- emdash_core/api/router.py +2 -0
- emdash_core/api/skills.py +241 -0
- emdash_core/checkpoint/__init__.py +40 -0
- emdash_core/checkpoint/cli.py +175 -0
- emdash_core/checkpoint/git_operations.py +250 -0
- emdash_core/checkpoint/manager.py +231 -0
- emdash_core/checkpoint/models.py +107 -0
- emdash_core/checkpoint/storage.py +201 -0
- emdash_core/config.py +1 -1
- emdash_core/core/config.py +18 -2
- emdash_core/graph/schema.py +5 -5
- emdash_core/ingestion/orchestrator.py +19 -10
- emdash_core/models/agent.py +1 -1
- emdash_core/server.py +42 -0
- emdash_core/skills/frontend-design/SKILL.md +56 -0
- emdash_core/sse/stream.py +5 -0
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/METADATA +2 -2
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/RECORD +54 -37
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/entry_points.txt +1 -0
- emdash_core/agent/runner.py +0 -601
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/WHEEL +0 -0
emdash_core/api/agent.py
CHANGED
|
@@ -31,6 +31,61 @@ def _ensure_emdash_importable():
|
|
|
31
31
|
pass # emdash_core is already in the package
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _run_sdk_agent(
|
|
35
|
+
message: str,
|
|
36
|
+
model: str,
|
|
37
|
+
sse_handler: SSEHandler,
|
|
38
|
+
session_id: str,
|
|
39
|
+
emitter,
|
|
40
|
+
plan_mode: bool = False,
|
|
41
|
+
):
|
|
42
|
+
"""Run the agent using Anthropic Agent SDK.
|
|
43
|
+
|
|
44
|
+
This function is called when use_sdk=True and model is a Claude model.
|
|
45
|
+
It uses the SDKAgentRunner which provides native support for
|
|
46
|
+
Skills, MCP, and extended thinking.
|
|
47
|
+
"""
|
|
48
|
+
import asyncio
|
|
49
|
+
from ..agent.runner import SDKAgentRunner
|
|
50
|
+
|
|
51
|
+
# Get working directory from config
|
|
52
|
+
config = get_config()
|
|
53
|
+
cwd = str(config.repo_root) if config.repo_root else str(Path.cwd())
|
|
54
|
+
|
|
55
|
+
# Create SDK runner
|
|
56
|
+
runner = SDKAgentRunner(
|
|
57
|
+
model=model,
|
|
58
|
+
cwd=cwd,
|
|
59
|
+
emitter=emitter,
|
|
60
|
+
plan_mode=plan_mode,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Store session state
|
|
64
|
+
_sessions[session_id] = {
|
|
65
|
+
"runner": runner,
|
|
66
|
+
"message_count": 1,
|
|
67
|
+
"model": model,
|
|
68
|
+
"plan_mode": plan_mode,
|
|
69
|
+
"is_sdk": True,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Run async agent in sync context
|
|
73
|
+
async def run_async():
|
|
74
|
+
response_text = ""
|
|
75
|
+
async for event in runner.run(message):
|
|
76
|
+
if event.get("type") == "text":
|
|
77
|
+
response_text += event.get("content", "")
|
|
78
|
+
return response_text
|
|
79
|
+
|
|
80
|
+
# Run the async code
|
|
81
|
+
try:
|
|
82
|
+
response = asyncio.run(run_async())
|
|
83
|
+
return response
|
|
84
|
+
except Exception as e:
|
|
85
|
+
sse_handler.emit(EventType.ERROR, {"error": str(e)})
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
|
|
34
89
|
def _run_agent_sync(
|
|
35
90
|
message: str,
|
|
36
91
|
model: str,
|
|
@@ -38,19 +93,38 @@ def _run_agent_sync(
|
|
|
38
93
|
sse_handler: SSEHandler,
|
|
39
94
|
session_id: str,
|
|
40
95
|
images: list = None,
|
|
96
|
+
plan_mode: bool = False,
|
|
97
|
+
use_sdk: bool = None,
|
|
41
98
|
):
|
|
42
99
|
"""Run the agent synchronously (in thread pool).
|
|
43
100
|
|
|
44
101
|
This function runs in a background thread and emits events
|
|
45
102
|
to the SSE handler for streaming to the client.
|
|
103
|
+
|
|
104
|
+
For Claude models with use_sdk=True, uses the Anthropic Agent SDK.
|
|
105
|
+
For other models, uses the standard AgentRunner with OpenAI-compatible API.
|
|
46
106
|
"""
|
|
47
107
|
try:
|
|
48
108
|
_ensure_emdash_importable()
|
|
49
109
|
|
|
50
110
|
# Import agent components from emdash_core
|
|
51
|
-
from ..agent.runner import AgentRunner
|
|
111
|
+
from ..agent.runner import AgentRunner, is_claude_model
|
|
112
|
+
from ..agent.toolkit import AgentToolkit
|
|
52
113
|
from ..agent.events import AgentEventEmitter
|
|
53
114
|
|
|
115
|
+
# Determine if we should use SDK
|
|
116
|
+
# Auto-detect based on model if not explicitly set
|
|
117
|
+
if use_sdk is None:
|
|
118
|
+
import os
|
|
119
|
+
# Check env var for SDK preference
|
|
120
|
+
sdk_enabled = os.environ.get("EMDASH_USE_SDK", "auto").lower()
|
|
121
|
+
if sdk_enabled == "true":
|
|
122
|
+
use_sdk = True
|
|
123
|
+
elif sdk_enabled == "false":
|
|
124
|
+
use_sdk = False
|
|
125
|
+
else: # "auto"
|
|
126
|
+
use_sdk = is_claude_model(model)
|
|
127
|
+
|
|
54
128
|
# Create an emitter that forwards to SSE handler
|
|
55
129
|
class SSEBridgeHandler:
|
|
56
130
|
"""Bridges AgentEventEmitter to SSEHandler."""
|
|
@@ -66,13 +140,46 @@ def _run_agent_sync(
|
|
|
66
140
|
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
67
141
|
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
68
142
|
|
|
143
|
+
# Use SDK for Claude models if enabled
|
|
144
|
+
if use_sdk and is_claude_model(model):
|
|
145
|
+
return _run_sdk_agent(
|
|
146
|
+
message=message,
|
|
147
|
+
model=model,
|
|
148
|
+
sse_handler=sse_handler,
|
|
149
|
+
session_id=session_id,
|
|
150
|
+
emitter=emitter,
|
|
151
|
+
plan_mode=plan_mode,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Standard path: use AgentRunner with OpenAI-compatible API
|
|
155
|
+
# Create toolkit with plan_mode if requested
|
|
156
|
+
# When in plan mode, generate a plan file path so write_to_file is available
|
|
157
|
+
plan_file_path = None
|
|
158
|
+
if plan_mode:
|
|
159
|
+
from pathlib import Path
|
|
160
|
+
repo_root = Path.cwd()
|
|
161
|
+
plan_file_path = str(repo_root / ".emdash" / "plan.md")
|
|
162
|
+
# Ensure .emdash directory exists
|
|
163
|
+
(repo_root / ".emdash").mkdir(exist_ok=True)
|
|
164
|
+
|
|
165
|
+
toolkit = AgentToolkit(plan_mode=plan_mode, plan_file_path=plan_file_path)
|
|
166
|
+
|
|
69
167
|
runner = AgentRunner(
|
|
168
|
+
toolkit=toolkit,
|
|
70
169
|
model=model,
|
|
71
170
|
verbose=True,
|
|
72
171
|
max_iterations=max_iterations,
|
|
73
172
|
emitter=emitter,
|
|
74
173
|
)
|
|
75
174
|
|
|
175
|
+
# Store session state BEFORE running (so it exists even if interrupted)
|
|
176
|
+
_sessions[session_id] = {
|
|
177
|
+
"runner": runner,
|
|
178
|
+
"message_count": 1,
|
|
179
|
+
"model": model,
|
|
180
|
+
"plan_mode": plan_mode,
|
|
181
|
+
}
|
|
182
|
+
|
|
76
183
|
# Convert image data if provided
|
|
77
184
|
agent_images = None
|
|
78
185
|
if images:
|
|
@@ -85,13 +192,6 @@ def _run_agent_sync(
|
|
|
85
192
|
# Run the agent
|
|
86
193
|
response = runner.run(message, images=agent_images)
|
|
87
194
|
|
|
88
|
-
# Store session state
|
|
89
|
-
_sessions[session_id] = {
|
|
90
|
-
"runner": runner,
|
|
91
|
-
"message_count": 1,
|
|
92
|
-
"model": model,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
195
|
return response
|
|
96
196
|
|
|
97
197
|
except Exception as e:
|
|
@@ -114,6 +214,7 @@ async def _run_agent_async(
|
|
|
114
214
|
# Get model from request or config
|
|
115
215
|
model = request.model or config.default_model
|
|
116
216
|
max_iterations = request.options.max_iterations
|
|
217
|
+
plan_mode = request.options.mode == AgentMode.PLAN
|
|
117
218
|
|
|
118
219
|
# Emit session start
|
|
119
220
|
sse_handler.emit(EventType.SESSION_START, {
|
|
@@ -121,6 +222,7 @@ async def _run_agent_async(
|
|
|
121
222
|
"model": model,
|
|
122
223
|
"session_id": session_id,
|
|
123
224
|
"query": request.message,
|
|
225
|
+
"mode": request.options.mode.value,
|
|
124
226
|
})
|
|
125
227
|
|
|
126
228
|
loop = asyncio.get_event_loop()
|
|
@@ -136,6 +238,7 @@ async def _run_agent_async(
|
|
|
136
238
|
sse_handler,
|
|
137
239
|
session_id,
|
|
138
240
|
request.images,
|
|
241
|
+
plan_mode,
|
|
139
242
|
)
|
|
140
243
|
|
|
141
244
|
# Emit session end
|
|
@@ -306,3 +409,454 @@ async def delete_session(session_id: str):
|
|
|
306
409
|
del _sessions[session_id]
|
|
307
410
|
return {"deleted": True}
|
|
308
411
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@router.get("/chat/{session_id}/plan")
|
|
415
|
+
async def get_pending_plan(session_id: str):
|
|
416
|
+
"""Get the pending plan for a session, if any.
|
|
417
|
+
|
|
418
|
+
Returns 404 if session not found, 204 if no pending plan.
|
|
419
|
+
"""
|
|
420
|
+
if session_id not in _sessions:
|
|
421
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
422
|
+
|
|
423
|
+
session = _sessions[session_id]
|
|
424
|
+
runner = session.get("runner")
|
|
425
|
+
|
|
426
|
+
if not runner:
|
|
427
|
+
raise HTTPException(status_code=400, detail="Session has no active runner")
|
|
428
|
+
|
|
429
|
+
pending_plan = runner.get_pending_plan()
|
|
430
|
+
if not pending_plan:
|
|
431
|
+
return {"has_plan": False, "plan": None}
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
"has_plan": True,
|
|
435
|
+
"session_id": session_id,
|
|
436
|
+
"plan": pending_plan,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@router.post("/chat/{session_id}/plan/approve")
|
|
441
|
+
async def approve_plan(session_id: str):
|
|
442
|
+
"""Approve the pending plan and transition to code mode.
|
|
443
|
+
|
|
444
|
+
Returns SSE stream for the implementation phase.
|
|
445
|
+
"""
|
|
446
|
+
if session_id not in _sessions:
|
|
447
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
448
|
+
|
|
449
|
+
session = _sessions[session_id]
|
|
450
|
+
runner = session.get("runner")
|
|
451
|
+
|
|
452
|
+
if not runner:
|
|
453
|
+
raise HTTPException(status_code=400, detail="Session has no active runner")
|
|
454
|
+
|
|
455
|
+
if not runner.has_pending_plan():
|
|
456
|
+
raise HTTPException(status_code=400, detail="No pending plan to approve")
|
|
457
|
+
|
|
458
|
+
# Create SSE handler for streaming the implementation
|
|
459
|
+
sse_handler = SSEHandler(agent_name="Emdash Code")
|
|
460
|
+
|
|
461
|
+
async def _run_approval():
|
|
462
|
+
loop = asyncio.get_event_loop()
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
# Wire up SSE handler
|
|
466
|
+
from ..agent.events import AgentEventEmitter
|
|
467
|
+
|
|
468
|
+
class SSEBridgeHandler:
|
|
469
|
+
def __init__(self, sse_handler: SSEHandler):
|
|
470
|
+
self._sse = sse_handler
|
|
471
|
+
|
|
472
|
+
def handle(self, event):
|
|
473
|
+
self._sse.handle(event)
|
|
474
|
+
|
|
475
|
+
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
476
|
+
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
477
|
+
runner.emitter = emitter
|
|
478
|
+
|
|
479
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
480
|
+
"agent_name": "Emdash Code",
|
|
481
|
+
"model": session.get("model", "unknown"),
|
|
482
|
+
"session_id": session_id,
|
|
483
|
+
"query": "Plan approved - implementing...",
|
|
484
|
+
"plan_approved": True,
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
# Reset cycle state for new mode
|
|
488
|
+
from ..agent.tools.modes import ModeState
|
|
489
|
+
ModeState.get_instance().reset_cycle()
|
|
490
|
+
|
|
491
|
+
# Approve and run implementation
|
|
492
|
+
await loop.run_in_executor(
|
|
493
|
+
_executor,
|
|
494
|
+
runner.approve_plan,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
session["plan_mode"] = False # Now in code mode
|
|
498
|
+
|
|
499
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
500
|
+
"success": True,
|
|
501
|
+
"session_id": session_id,
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
except Exception as e:
|
|
505
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
506
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
507
|
+
"success": False,
|
|
508
|
+
"error": str(e),
|
|
509
|
+
"session_id": session_id,
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
finally:
|
|
513
|
+
sse_handler.close()
|
|
514
|
+
|
|
515
|
+
asyncio.create_task(_run_approval())
|
|
516
|
+
|
|
517
|
+
return StreamingResponse(
|
|
518
|
+
sse_handler,
|
|
519
|
+
media_type="text/event-stream",
|
|
520
|
+
headers={
|
|
521
|
+
"Cache-Control": "no-cache",
|
|
522
|
+
"Connection": "keep-alive",
|
|
523
|
+
"X-Session-ID": session_id,
|
|
524
|
+
},
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@router.post("/chat/{session_id}/plan/reject")
|
|
529
|
+
async def reject_plan(session_id: str, feedback: str = ""):
|
|
530
|
+
"""Reject the pending plan with feedback.
|
|
531
|
+
|
|
532
|
+
Returns SSE stream for the revised planning phase.
|
|
533
|
+
"""
|
|
534
|
+
if session_id not in _sessions:
|
|
535
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
536
|
+
|
|
537
|
+
session = _sessions[session_id]
|
|
538
|
+
runner = session.get("runner")
|
|
539
|
+
|
|
540
|
+
if not runner:
|
|
541
|
+
raise HTTPException(status_code=400, detail="Session has no active runner")
|
|
542
|
+
|
|
543
|
+
if not runner.has_pending_plan():
|
|
544
|
+
raise HTTPException(status_code=400, detail="No pending plan to reject")
|
|
545
|
+
|
|
546
|
+
sse_handler = SSEHandler(agent_name="Emdash Code")
|
|
547
|
+
|
|
548
|
+
async def _run_rejection():
|
|
549
|
+
loop = asyncio.get_event_loop()
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
from ..agent.events import AgentEventEmitter
|
|
553
|
+
|
|
554
|
+
class SSEBridgeHandler:
|
|
555
|
+
def __init__(self, sse_handler: SSEHandler):
|
|
556
|
+
self._sse = sse_handler
|
|
557
|
+
|
|
558
|
+
def handle(self, event):
|
|
559
|
+
self._sse.handle(event)
|
|
560
|
+
|
|
561
|
+
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
562
|
+
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
563
|
+
runner.emitter = emitter
|
|
564
|
+
|
|
565
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
566
|
+
"agent_name": "Emdash Code",
|
|
567
|
+
"model": session.get("model", "unknown"),
|
|
568
|
+
"session_id": session_id,
|
|
569
|
+
"query": f"Plan rejected - revising... {feedback}",
|
|
570
|
+
"plan_rejected": True,
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
# Reset cycle state for revision
|
|
574
|
+
from ..agent.tools.modes import ModeState
|
|
575
|
+
ModeState.get_instance().reset_cycle()
|
|
576
|
+
|
|
577
|
+
# Reject and continue planning
|
|
578
|
+
await loop.run_in_executor(
|
|
579
|
+
_executor,
|
|
580
|
+
lambda: runner.reject_plan(feedback),
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
584
|
+
"success": True,
|
|
585
|
+
"session_id": session_id,
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
except Exception as e:
|
|
589
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
590
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
591
|
+
"success": False,
|
|
592
|
+
"error": str(e),
|
|
593
|
+
"session_id": session_id,
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
finally:
|
|
597
|
+
sse_handler.close()
|
|
598
|
+
|
|
599
|
+
asyncio.create_task(_run_rejection())
|
|
600
|
+
|
|
601
|
+
return StreamingResponse(
|
|
602
|
+
sse_handler,
|
|
603
|
+
media_type="text/event-stream",
|
|
604
|
+
headers={
|
|
605
|
+
"Cache-Control": "no-cache",
|
|
606
|
+
"Connection": "keep-alive",
|
|
607
|
+
"X-Session-ID": session_id,
|
|
608
|
+
},
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@router.post("/chat/{session_id}/planmode/approve")
|
|
613
|
+
async def approve_plan_mode(session_id: str):
|
|
614
|
+
"""Approve entering plan mode.
|
|
615
|
+
|
|
616
|
+
Called when user approves the agent's request to enter plan mode.
|
|
617
|
+
Returns SSE stream for the planning phase.
|
|
618
|
+
"""
|
|
619
|
+
if session_id not in _sessions:
|
|
620
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
621
|
+
|
|
622
|
+
session = _sessions[session_id]
|
|
623
|
+
runner = session.get("runner")
|
|
624
|
+
|
|
625
|
+
if not runner:
|
|
626
|
+
raise HTTPException(status_code=400, detail="Session has no active runner")
|
|
627
|
+
|
|
628
|
+
# Check if plan mode was actually requested
|
|
629
|
+
from ..agent.tools.modes import ModeState
|
|
630
|
+
state = ModeState.get_instance()
|
|
631
|
+
if not state.plan_mode_requested:
|
|
632
|
+
raise HTTPException(status_code=400, detail="No pending plan mode request")
|
|
633
|
+
|
|
634
|
+
sse_handler = SSEHandler(agent_name="Emdash Code")
|
|
635
|
+
|
|
636
|
+
async def _run_approval():
|
|
637
|
+
loop = asyncio.get_event_loop()
|
|
638
|
+
|
|
639
|
+
try:
|
|
640
|
+
from ..agent.events import AgentEventEmitter
|
|
641
|
+
|
|
642
|
+
class SSEBridgeHandler:
|
|
643
|
+
def __init__(self, sse_handler: SSEHandler):
|
|
644
|
+
self._sse = sse_handler
|
|
645
|
+
|
|
646
|
+
def handle(self, event):
|
|
647
|
+
self._sse.handle(event)
|
|
648
|
+
|
|
649
|
+
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
650
|
+
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
651
|
+
runner.emitter = emitter
|
|
652
|
+
|
|
653
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
654
|
+
"agent_name": "Emdash Code",
|
|
655
|
+
"model": session.get("model", "unknown"),
|
|
656
|
+
"session_id": session_id,
|
|
657
|
+
"query": "Plan mode approved - entering plan mode...",
|
|
658
|
+
"plan_mode_approved": True,
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
# Approve and enter plan mode
|
|
662
|
+
await loop.run_in_executor(
|
|
663
|
+
_executor,
|
|
664
|
+
runner.approve_plan_mode,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
668
|
+
"success": True,
|
|
669
|
+
"session_id": session_id,
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
except Exception as e:
|
|
673
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
674
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
675
|
+
"success": False,
|
|
676
|
+
"error": str(e),
|
|
677
|
+
"session_id": session_id,
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
finally:
|
|
681
|
+
sse_handler.close()
|
|
682
|
+
|
|
683
|
+
asyncio.create_task(_run_approval())
|
|
684
|
+
|
|
685
|
+
return StreamingResponse(
|
|
686
|
+
sse_handler,
|
|
687
|
+
media_type="text/event-stream",
|
|
688
|
+
headers={
|
|
689
|
+
"Cache-Control": "no-cache",
|
|
690
|
+
"Connection": "keep-alive",
|
|
691
|
+
"X-Session-ID": session_id,
|
|
692
|
+
},
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
@router.post("/chat/{session_id}/clarification/answer")
|
|
697
|
+
async def answer_clarification(session_id: str, answer: str):
|
|
698
|
+
"""Answer a pending clarification question.
|
|
699
|
+
|
|
700
|
+
Called when the user responds to a clarification question asked by the agent
|
|
701
|
+
via ask_followup_question tool. This resumes the agent with the user's answer.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
session_id: The session ID
|
|
705
|
+
answer: The user's answer to the clarification question
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
SSE stream for the agent's continued execution
|
|
709
|
+
"""
|
|
710
|
+
if session_id not in _sessions:
|
|
711
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
712
|
+
|
|
713
|
+
session = _sessions[session_id]
|
|
714
|
+
runner = session.get("runner")
|
|
715
|
+
|
|
716
|
+
if not runner:
|
|
717
|
+
raise HTTPException(status_code=400, detail="Session has no active runner")
|
|
718
|
+
|
|
719
|
+
sse_handler = SSEHandler(agent_name="Emdash Code")
|
|
720
|
+
|
|
721
|
+
async def _run_answer():
|
|
722
|
+
loop = asyncio.get_event_loop()
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
from ..agent.events import AgentEventEmitter
|
|
726
|
+
|
|
727
|
+
class SSEBridgeHandler:
|
|
728
|
+
def __init__(self, sse_handler: SSEHandler):
|
|
729
|
+
self._sse = sse_handler
|
|
730
|
+
|
|
731
|
+
def handle(self, event):
|
|
732
|
+
self._sse.handle(event)
|
|
733
|
+
|
|
734
|
+
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
735
|
+
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
736
|
+
runner.emitter = emitter
|
|
737
|
+
|
|
738
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
739
|
+
"agent_name": "Emdash Code",
|
|
740
|
+
"model": session.get("model", "unknown"),
|
|
741
|
+
"session_id": session_id,
|
|
742
|
+
"query": f"Clarification answered: {answer[:100]}...",
|
|
743
|
+
"clarification_answered": True,
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
# Answer the clarification and resume the agent
|
|
747
|
+
await loop.run_in_executor(
|
|
748
|
+
_executor,
|
|
749
|
+
lambda: runner.answer_clarification(answer),
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
753
|
+
"success": True,
|
|
754
|
+
"session_id": session_id,
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
except Exception as e:
|
|
758
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
759
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
760
|
+
"success": False,
|
|
761
|
+
"error": str(e),
|
|
762
|
+
"session_id": session_id,
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
finally:
|
|
766
|
+
sse_handler.close()
|
|
767
|
+
|
|
768
|
+
asyncio.create_task(_run_answer())
|
|
769
|
+
|
|
770
|
+
return StreamingResponse(
|
|
771
|
+
sse_handler,
|
|
772
|
+
media_type="text/event-stream",
|
|
773
|
+
headers={
|
|
774
|
+
"Cache-Control": "no-cache",
|
|
775
|
+
"Connection": "keep-alive",
|
|
776
|
+
"X-Session-ID": session_id,
|
|
777
|
+
},
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
@router.post("/chat/{session_id}/planmode/reject")
|
|
782
|
+
async def reject_plan_mode(session_id: str, feedback: str = ""):
|
|
783
|
+
"""Reject entering plan mode.
|
|
784
|
+
|
|
785
|
+
Called when user rejects the agent's request to enter plan mode.
|
|
786
|
+
Returns SSE stream for continued code mode execution.
|
|
787
|
+
"""
|
|
788
|
+
if session_id not in _sessions:
|
|
789
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
790
|
+
|
|
791
|
+
session = _sessions[session_id]
|
|
792
|
+
runner = session.get("runner")
|
|
793
|
+
|
|
794
|
+
if not runner:
|
|
795
|
+
raise HTTPException(status_code=400, detail="Session has no active runner")
|
|
796
|
+
|
|
797
|
+
# Check if plan mode was actually requested
|
|
798
|
+
from ..agent.tools.modes import ModeState
|
|
799
|
+
state = ModeState.get_instance()
|
|
800
|
+
if not state.plan_mode_requested:
|
|
801
|
+
raise HTTPException(status_code=400, detail="No pending plan mode request")
|
|
802
|
+
|
|
803
|
+
sse_handler = SSEHandler(agent_name="Emdash Code")
|
|
804
|
+
|
|
805
|
+
async def _run_rejection():
|
|
806
|
+
loop = asyncio.get_event_loop()
|
|
807
|
+
|
|
808
|
+
try:
|
|
809
|
+
from ..agent.events import AgentEventEmitter
|
|
810
|
+
|
|
811
|
+
class SSEBridgeHandler:
|
|
812
|
+
def __init__(self, sse_handler: SSEHandler):
|
|
813
|
+
self._sse = sse_handler
|
|
814
|
+
|
|
815
|
+
def handle(self, event):
|
|
816
|
+
self._sse.handle(event)
|
|
817
|
+
|
|
818
|
+
emitter = AgentEventEmitter(agent_name="Emdash Code")
|
|
819
|
+
emitter.add_handler(SSEBridgeHandler(sse_handler))
|
|
820
|
+
runner.emitter = emitter
|
|
821
|
+
|
|
822
|
+
sse_handler.emit(EventType.SESSION_START, {
|
|
823
|
+
"agent_name": "Emdash Code",
|
|
824
|
+
"model": session.get("model", "unknown"),
|
|
825
|
+
"session_id": session_id,
|
|
826
|
+
"query": f"Plan mode rejected - continuing in code mode... {feedback}",
|
|
827
|
+
"plan_mode_rejected": True,
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
# Reject and stay in code mode
|
|
831
|
+
await loop.run_in_executor(
|
|
832
|
+
_executor,
|
|
833
|
+
lambda: runner.reject_plan_mode(feedback),
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
837
|
+
"success": True,
|
|
838
|
+
"session_id": session_id,
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
except Exception as e:
|
|
842
|
+
sse_handler.emit(EventType.ERROR, {"message": str(e)})
|
|
843
|
+
sse_handler.emit(EventType.SESSION_END, {
|
|
844
|
+
"success": False,
|
|
845
|
+
"error": str(e),
|
|
846
|
+
"session_id": session_id,
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
finally:
|
|
850
|
+
sse_handler.close()
|
|
851
|
+
|
|
852
|
+
asyncio.create_task(_run_rejection())
|
|
853
|
+
|
|
854
|
+
return StreamingResponse(
|
|
855
|
+
sse_handler,
|
|
856
|
+
media_type="text/event-stream",
|
|
857
|
+
headers={
|
|
858
|
+
"Cache-Control": "no-cache",
|
|
859
|
+
"Connection": "keep-alive",
|
|
860
|
+
"X-Session-ID": session_id,
|
|
861
|
+
},
|
|
862
|
+
)
|
emdash_core/api/index.py
CHANGED
|
@@ -64,7 +64,7 @@ def _run_index_sync(
|
|
|
64
64
|
# Create orchestrator (uses configured connection)
|
|
65
65
|
orchestrator = IngestionOrchestrator()
|
|
66
66
|
|
|
67
|
-
sse_handler.emit(EventType.PROGRESS, {"step": "
|
|
67
|
+
sse_handler.emit(EventType.PROGRESS, {"step": "Indexing codebase", "percent": 10})
|
|
68
68
|
|
|
69
69
|
# Progress callback to emit SSE events during parsing
|
|
70
70
|
def progress_callback(step: str, percent: float):
|
emdash_core/api/projectmd.py
CHANGED
|
@@ -105,7 +105,7 @@ def _generate_projectmd_sync(
|
|
|
105
105
|
runner = AgentRunner(
|
|
106
106
|
model=model,
|
|
107
107
|
verbose=True,
|
|
108
|
-
max_iterations=
|
|
108
|
+
max_iterations=100,
|
|
109
109
|
emitter=emitter,
|
|
110
110
|
)
|
|
111
111
|
|
|
@@ -116,7 +116,9 @@ def _generate_projectmd_sync(
|
|
|
116
116
|
3. Key files and their purposes
|
|
117
117
|
4. How to get started
|
|
118
118
|
|
|
119
|
-
Use the available tools to explore the codebase
|
|
119
|
+
Use the available tools to explore the codebase structure and key files.
|
|
120
|
+
After exploration, write a comprehensive PROJECT.md document.
|
|
121
|
+
IMPORTANT: After exploring, output the complete PROJECT.md content as your final response."""
|
|
120
122
|
|
|
121
123
|
content = runner.run(prompt)
|
|
122
124
|
|
emdash_core/api/router.py
CHANGED
|
@@ -24,6 +24,7 @@ from . import (
|
|
|
24
24
|
context,
|
|
25
25
|
feature,
|
|
26
26
|
projectmd,
|
|
27
|
+
skills,
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
api_router = APIRouter(prefix="/api")
|
|
@@ -37,6 +38,7 @@ api_router.include_router(auth.router)
|
|
|
37
38
|
# Agent operations
|
|
38
39
|
api_router.include_router(agent.router)
|
|
39
40
|
api_router.include_router(agents.router)
|
|
41
|
+
api_router.include_router(skills.router)
|
|
40
42
|
|
|
41
43
|
# Database management
|
|
42
44
|
api_router.include_router(db.router)
|