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.
Files changed (55) hide show
  1. emdash_core/__init__.py +6 -1
  2. emdash_core/agent/__init__.py +4 -0
  3. emdash_core/agent/events.py +52 -1
  4. emdash_core/agent/inprocess_subagent.py +123 -10
  5. emdash_core/agent/prompts/__init__.py +6 -0
  6. emdash_core/agent/prompts/main_agent.py +53 -3
  7. emdash_core/agent/prompts/plan_mode.py +255 -0
  8. emdash_core/agent/prompts/subagents.py +84 -16
  9. emdash_core/agent/prompts/workflow.py +270 -56
  10. emdash_core/agent/providers/base.py +4 -0
  11. emdash_core/agent/providers/factory.py +2 -2
  12. emdash_core/agent/providers/models.py +7 -0
  13. emdash_core/agent/providers/openai_provider.py +137 -13
  14. emdash_core/agent/runner/__init__.py +49 -0
  15. emdash_core/agent/runner/agent_runner.py +753 -0
  16. emdash_core/agent/runner/context.py +451 -0
  17. emdash_core/agent/runner/factory.py +108 -0
  18. emdash_core/agent/runner/plan.py +217 -0
  19. emdash_core/agent/runner/sdk_runner.py +324 -0
  20. emdash_core/agent/runner/utils.py +67 -0
  21. emdash_core/agent/skills.py +358 -0
  22. emdash_core/agent/toolkit.py +85 -5
  23. emdash_core/agent/toolkits/plan.py +9 -11
  24. emdash_core/agent/tools/__init__.py +3 -2
  25. emdash_core/agent/tools/coding.py +48 -4
  26. emdash_core/agent/tools/modes.py +207 -55
  27. emdash_core/agent/tools/search.py +4 -0
  28. emdash_core/agent/tools/skill.py +193 -0
  29. emdash_core/agent/tools/spec.py +61 -94
  30. emdash_core/agent/tools/task.py +41 -2
  31. emdash_core/agent/tools/tasks.py +15 -78
  32. emdash_core/api/agent.py +562 -8
  33. emdash_core/api/index.py +1 -1
  34. emdash_core/api/projectmd.py +4 -2
  35. emdash_core/api/router.py +2 -0
  36. emdash_core/api/skills.py +241 -0
  37. emdash_core/checkpoint/__init__.py +40 -0
  38. emdash_core/checkpoint/cli.py +175 -0
  39. emdash_core/checkpoint/git_operations.py +250 -0
  40. emdash_core/checkpoint/manager.py +231 -0
  41. emdash_core/checkpoint/models.py +107 -0
  42. emdash_core/checkpoint/storage.py +201 -0
  43. emdash_core/config.py +1 -1
  44. emdash_core/core/config.py +18 -2
  45. emdash_core/graph/schema.py +5 -5
  46. emdash_core/ingestion/orchestrator.py +19 -10
  47. emdash_core/models/agent.py +1 -1
  48. emdash_core/server.py +42 -0
  49. emdash_core/skills/frontend-design/SKILL.md +56 -0
  50. emdash_core/sse/stream.py +5 -0
  51. {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/METADATA +2 -2
  52. {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/RECORD +54 -37
  53. {emdash_core-0.1.7.dist-info → emdash_core-0.1.33.dist-info}/entry_points.txt +1 -0
  54. emdash_core/agent/runner.py +0 -601
  55. {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": "Parsing codebase", "percent": 10})
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):
@@ -105,7 +105,7 @@ def _generate_projectmd_sync(
105
105
  runner = AgentRunner(
106
106
  model=model,
107
107
  verbose=True,
108
- max_iterations=30,
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, then write a comprehensive PROJECT.md."""
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)