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.
- emdash_core/agent/__init__.py +4 -0
- emdash_core/agent/events.py +42 -20
- emdash_core/agent/inprocess_subagent.py +123 -10
- emdash_core/agent/prompts/__init__.py +4 -3
- emdash_core/agent/prompts/main_agent.py +32 -2
- emdash_core/agent/prompts/plan_mode.py +236 -107
- emdash_core/agent/prompts/subagents.py +79 -15
- emdash_core/agent/prompts/workflow.py +145 -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 +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 +47 -8
- emdash_core/agent/toolkit.py +46 -14
- emdash_core/agent/toolkits/plan.py +9 -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 +41 -2
- emdash_core/api/agent.py +555 -1
- 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.33.dist-info}/METADATA +2 -1
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/RECORD +31 -24
- emdash_core/agent/runner.py +0 -1123
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/WHEEL +0 -0
- {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/entry_points.txt +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,7 +140,32 @@ 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,
|
|
@@ -78,6 +177,7 @@ def _run_agent_sync(
|
|
|
78
177
|
"runner": runner,
|
|
79
178
|
"message_count": 1,
|
|
80
179
|
"model": model,
|
|
180
|
+
"plan_mode": plan_mode,
|
|
81
181
|
}
|
|
82
182
|
|
|
83
183
|
# Convert image data if provided
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: frontend-design
|
|
3
|
+
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
|
4
|
+
user_invocable: true
|
|
5
|
+
tools: []
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Frontend Design
|
|
9
|
+
|
|
10
|
+
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
|
11
|
+
|
|
12
|
+
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
|
13
|
+
|
|
14
|
+
## Design Thinking
|
|
15
|
+
|
|
16
|
+
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
|
17
|
+
|
|
18
|
+
- **Purpose**: What problem does this interface solve? Who uses it?
|
|
19
|
+
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
|
20
|
+
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
|
21
|
+
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
|
22
|
+
|
|
23
|
+
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
|
24
|
+
|
|
25
|
+
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
|
26
|
+
|
|
27
|
+
- Production-grade and functional
|
|
28
|
+
- Visually striking and memorable
|
|
29
|
+
- Cohesive with a clear aesthetic point-of-view
|
|
30
|
+
- Meticulously refined in every detail
|
|
31
|
+
|
|
32
|
+
## Frontend Aesthetics Guidelines
|
|
33
|
+
|
|
34
|
+
Focus on:
|
|
35
|
+
|
|
36
|
+
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
|
37
|
+
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
|
38
|
+
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
|
39
|
+
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
|
40
|
+
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
|
41
|
+
|
|
42
|
+
## What to Avoid
|
|
43
|
+
|
|
44
|
+
NEVER use generic AI-generated aesthetics like:
|
|
45
|
+
- Overused font families (Inter, Roboto, Arial, system fonts)
|
|
46
|
+
- Cliched color schemes (particularly purple gradients on white backgrounds)
|
|
47
|
+
- Predictable layouts and component patterns
|
|
48
|
+
- Cookie-cutter design that lacks context-specific character
|
|
49
|
+
|
|
50
|
+
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
|
51
|
+
|
|
52
|
+
## Implementation Complexity
|
|
53
|
+
|
|
54
|
+
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
|
55
|
+
|
|
56
|
+
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
emdash_core/sse/stream.py
CHANGED
|
@@ -16,6 +16,10 @@ class EventType(str, Enum):
|
|
|
16
16
|
TOOL_START = "tool_start"
|
|
17
17
|
TOOL_RESULT = "tool_result"
|
|
18
18
|
|
|
19
|
+
# Sub-agent lifecycle
|
|
20
|
+
SUBAGENT_START = "subagent_start"
|
|
21
|
+
SUBAGENT_END = "subagent_end"
|
|
22
|
+
|
|
19
23
|
# Agent thinking/progress
|
|
20
24
|
THINKING = "thinking"
|
|
21
25
|
PROGRESS = "progress"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: emdash-core
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.33
|
|
4
4
|
Summary: EmDash Core - FastAPI server for code intelligence
|
|
5
5
|
Author: Em Dash Team
|
|
6
6
|
Requires-Python: >=3.10,<4.0
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.14
|
|
13
13
|
Requires-Dist: astroid (>=3.0.1,<4.0.0)
|
|
14
14
|
Requires-Dist: beautifulsoup4 (>=4.12.0)
|
|
15
|
+
Requires-Dist: claude-agent-sdk (>=0.1.19)
|
|
15
16
|
Requires-Dist: duckduckgo-search (>=6.0.0)
|
|
16
17
|
Requires-Dist: fastapi (>=0.109.0)
|
|
17
18
|
Requires-Dist: gitpython (>=3.1.40,<4.0.0)
|