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.
Files changed (32) hide show
  1. emdash_core/agent/__init__.py +4 -0
  2. emdash_core/agent/events.py +42 -20
  3. emdash_core/agent/inprocess_subagent.py +123 -10
  4. emdash_core/agent/prompts/__init__.py +4 -3
  5. emdash_core/agent/prompts/main_agent.py +32 -2
  6. emdash_core/agent/prompts/plan_mode.py +236 -107
  7. emdash_core/agent/prompts/subagents.py +79 -15
  8. emdash_core/agent/prompts/workflow.py +145 -26
  9. emdash_core/agent/providers/factory.py +2 -2
  10. emdash_core/agent/providers/openai_provider.py +67 -15
  11. emdash_core/agent/runner/__init__.py +49 -0
  12. emdash_core/agent/runner/agent_runner.py +753 -0
  13. emdash_core/agent/runner/context.py +451 -0
  14. emdash_core/agent/runner/factory.py +108 -0
  15. emdash_core/agent/runner/plan.py +217 -0
  16. emdash_core/agent/runner/sdk_runner.py +324 -0
  17. emdash_core/agent/runner/utils.py +67 -0
  18. emdash_core/agent/skills.py +47 -8
  19. emdash_core/agent/toolkit.py +46 -14
  20. emdash_core/agent/toolkits/plan.py +9 -11
  21. emdash_core/agent/tools/__init__.py +2 -2
  22. emdash_core/agent/tools/coding.py +48 -4
  23. emdash_core/agent/tools/modes.py +151 -143
  24. emdash_core/agent/tools/task.py +41 -2
  25. emdash_core/api/agent.py +555 -1
  26. emdash_core/skills/frontend-design/SKILL.md +56 -0
  27. emdash_core/sse/stream.py +4 -0
  28. {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/METADATA +2 -1
  29. {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/RECORD +31 -24
  30. emdash_core/agent/runner.py +0 -1123
  31. {emdash_core-0.1.25.dist-info → emdash_core-0.1.33.dist-info}/WHEEL +0 -0
  32. {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.25
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)