htmlgraph 0.25.0__py3-none-any.whl → 0.26.1__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.
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +193 -45
- htmlgraph/api/templates/dashboard.html +11 -0
- htmlgraph/api/templates/partials/activity-feed.html +458 -8
- htmlgraph/dashboard.html +41 -0
- htmlgraph/db/schema.py +254 -4
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +57 -10
- htmlgraph/hooks/drift_handler.py +24 -20
- htmlgraph/hooks/event_tracker.py +204 -177
- htmlgraph/hooks/orchestrator.py +6 -4
- htmlgraph/hooks/orchestrator_reflector.py +4 -4
- htmlgraph/hooks/pretooluse.py +3 -6
- htmlgraph/hooks/prompt_analyzer.py +14 -25
- htmlgraph/hooks/session_handler.py +123 -69
- htmlgraph/hooks/state_manager.py +7 -4
- htmlgraph/hooks/validator.py +15 -11
- htmlgraph/orchestration/headless_spawner.py +322 -15
- htmlgraph/orchestration/live_events.py +377 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/dashboard.html +41 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +1 -1
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +32 -27
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import subprocess
|
|
6
|
+
import time
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
11
|
+
from htmlgraph.orchestration.live_events import LiveEventPublisher
|
|
10
12
|
from htmlgraph.sdk import SDK
|
|
11
13
|
|
|
12
14
|
|
|
@@ -63,7 +65,92 @@ class HeadlessSpawner:
|
|
|
63
65
|
|
|
64
66
|
def __init__(self) -> None:
|
|
65
67
|
"""Initialize spawner."""
|
|
66
|
-
|
|
68
|
+
self._live_publisher: LiveEventPublisher | None = None
|
|
69
|
+
|
|
70
|
+
def _get_live_publisher(self) -> "LiveEventPublisher | None":
|
|
71
|
+
"""
|
|
72
|
+
Get LiveEventPublisher instance for real-time WebSocket streaming.
|
|
73
|
+
|
|
74
|
+
Returns None if publisher unavailable (optional dependency).
|
|
75
|
+
"""
|
|
76
|
+
if self._live_publisher is None:
|
|
77
|
+
try:
|
|
78
|
+
from htmlgraph.orchestration.live_events import LiveEventPublisher
|
|
79
|
+
|
|
80
|
+
self._live_publisher = LiveEventPublisher()
|
|
81
|
+
except Exception:
|
|
82
|
+
# Live events are optional
|
|
83
|
+
pass
|
|
84
|
+
return self._live_publisher
|
|
85
|
+
|
|
86
|
+
def _publish_live_event(
|
|
87
|
+
self,
|
|
88
|
+
event_type: str,
|
|
89
|
+
spawner_type: str,
|
|
90
|
+
**kwargs: str | int | float | bool | None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Publish a live event for WebSocket streaming.
|
|
94
|
+
|
|
95
|
+
Silently fails if publisher unavailable (optional feature).
|
|
96
|
+
"""
|
|
97
|
+
publisher = self._get_live_publisher()
|
|
98
|
+
if publisher is None:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
parent_event_id = os.getenv("HTMLGRAPH_PARENT_EVENT")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
if event_type == "spawner_start":
|
|
105
|
+
publisher.spawner_start(
|
|
106
|
+
spawner_type=spawner_type,
|
|
107
|
+
prompt=str(kwargs.get("prompt", "")),
|
|
108
|
+
parent_event_id=parent_event_id,
|
|
109
|
+
model=str(kwargs.get("model", "")) if kwargs.get("model") else None,
|
|
110
|
+
)
|
|
111
|
+
elif event_type == "spawner_phase":
|
|
112
|
+
progress_val = kwargs.get("progress")
|
|
113
|
+
publisher.spawner_phase(
|
|
114
|
+
spawner_type=spawner_type,
|
|
115
|
+
phase=str(kwargs.get("phase", "executing")),
|
|
116
|
+
progress=int(progress_val) if progress_val is not None else None,
|
|
117
|
+
details=str(kwargs.get("details", ""))
|
|
118
|
+
if kwargs.get("details")
|
|
119
|
+
else None,
|
|
120
|
+
parent_event_id=parent_event_id,
|
|
121
|
+
)
|
|
122
|
+
elif event_type == "spawner_complete":
|
|
123
|
+
duration_val = kwargs.get("duration")
|
|
124
|
+
tokens_val = kwargs.get("tokens")
|
|
125
|
+
publisher.spawner_complete(
|
|
126
|
+
spawner_type=spawner_type,
|
|
127
|
+
success=bool(kwargs.get("success", False)),
|
|
128
|
+
duration_seconds=float(duration_val)
|
|
129
|
+
if duration_val is not None
|
|
130
|
+
else None,
|
|
131
|
+
response_preview=str(kwargs.get("response", ""))[:200]
|
|
132
|
+
if kwargs.get("response")
|
|
133
|
+
else None,
|
|
134
|
+
tokens_used=int(tokens_val) if tokens_val is not None else None,
|
|
135
|
+
error=str(kwargs.get("error", "")) if kwargs.get("error") else None,
|
|
136
|
+
parent_event_id=parent_event_id,
|
|
137
|
+
)
|
|
138
|
+
elif event_type == "spawner_tool_use":
|
|
139
|
+
publisher.spawner_tool_use(
|
|
140
|
+
spawner_type=spawner_type,
|
|
141
|
+
tool_name=str(kwargs.get("tool_name", "unknown")),
|
|
142
|
+
parent_event_id=parent_event_id,
|
|
143
|
+
)
|
|
144
|
+
elif event_type == "spawner_message":
|
|
145
|
+
publisher.spawner_message(
|
|
146
|
+
spawner_type=spawner_type,
|
|
147
|
+
message=str(kwargs.get("message", "")),
|
|
148
|
+
role=str(kwargs.get("role", "assistant")),
|
|
149
|
+
parent_event_id=parent_event_id,
|
|
150
|
+
)
|
|
151
|
+
except Exception:
|
|
152
|
+
# Live events should never break spawner execution
|
|
153
|
+
pass
|
|
67
154
|
|
|
68
155
|
def _get_sdk(self) -> "SDK | None":
|
|
69
156
|
"""
|
|
@@ -397,6 +484,15 @@ class HeadlessSpawner:
|
|
|
397
484
|
if track_in_htmlgraph:
|
|
398
485
|
sdk = self._get_sdk()
|
|
399
486
|
|
|
487
|
+
# Publish live event: spawner starting
|
|
488
|
+
self._publish_live_event(
|
|
489
|
+
"spawner_start",
|
|
490
|
+
"gemini",
|
|
491
|
+
prompt=prompt,
|
|
492
|
+
model=model,
|
|
493
|
+
)
|
|
494
|
+
start_time = time.time()
|
|
495
|
+
|
|
400
496
|
try:
|
|
401
497
|
# Build command based on tested pattern from spike spk-4029eef3
|
|
402
498
|
cmd = ["gemini", "-p", prompt, "--output-format", output_format]
|
|
@@ -425,6 +521,14 @@ class HeadlessSpawner:
|
|
|
425
521
|
# Tracking failure should not break execution
|
|
426
522
|
pass
|
|
427
523
|
|
|
524
|
+
# Publish live event: executing
|
|
525
|
+
self._publish_live_event(
|
|
526
|
+
"spawner_phase",
|
|
527
|
+
"gemini",
|
|
528
|
+
phase="executing",
|
|
529
|
+
details="Running Gemini CLI",
|
|
530
|
+
)
|
|
531
|
+
|
|
428
532
|
# Execute with timeout and stderr redirection
|
|
429
533
|
# Note: Cannot use capture_output with stderr parameter
|
|
430
534
|
result = subprocess.run(
|
|
@@ -435,8 +539,24 @@ class HeadlessSpawner:
|
|
|
435
539
|
timeout=timeout,
|
|
436
540
|
)
|
|
437
541
|
|
|
542
|
+
# Publish live event: processing response
|
|
543
|
+
self._publish_live_event(
|
|
544
|
+
"spawner_phase",
|
|
545
|
+
"gemini",
|
|
546
|
+
phase="processing",
|
|
547
|
+
details="Parsing Gemini response",
|
|
548
|
+
)
|
|
549
|
+
|
|
438
550
|
# Check for command execution errors
|
|
439
551
|
if result.returncode != 0:
|
|
552
|
+
duration = time.time() - start_time
|
|
553
|
+
self._publish_live_event(
|
|
554
|
+
"spawner_complete",
|
|
555
|
+
"gemini",
|
|
556
|
+
success=False,
|
|
557
|
+
duration=duration,
|
|
558
|
+
error=f"CLI failed with exit code {result.returncode}",
|
|
559
|
+
)
|
|
440
560
|
return AIResult(
|
|
441
561
|
success=False,
|
|
442
562
|
response="",
|
|
@@ -455,16 +575,20 @@ class HeadlessSpawner:
|
|
|
455
575
|
# Only use stream-json parsing if we got valid events
|
|
456
576
|
if tracked_events:
|
|
457
577
|
# For stream-json, we need to extract response differently
|
|
458
|
-
#
|
|
578
|
+
# Collect all assistant message content, then check result
|
|
459
579
|
response_text = ""
|
|
460
580
|
for event in tracked_events:
|
|
461
|
-
if event.get("type") == "
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
581
|
+
if event.get("type") == "message":
|
|
582
|
+
# Only collect assistant messages
|
|
583
|
+
if event.get("role") == "assistant":
|
|
584
|
+
content = event.get("content", "")
|
|
585
|
+
if content:
|
|
586
|
+
response_text += content
|
|
587
|
+
elif event.get("type") == "result":
|
|
588
|
+
# Result event may have response field (override if present)
|
|
589
|
+
if "response" in event and event["response"]:
|
|
590
|
+
response_text = event["response"]
|
|
591
|
+
# Don't break - we've already collected messages
|
|
468
592
|
|
|
469
593
|
# Token usage from stats in result event
|
|
470
594
|
tokens = None
|
|
@@ -481,6 +605,16 @@ class HeadlessSpawner:
|
|
|
481
605
|
tokens = total_tokens if total_tokens > 0 else None
|
|
482
606
|
break
|
|
483
607
|
|
|
608
|
+
# Publish live event: complete
|
|
609
|
+
duration = time.time() - start_time
|
|
610
|
+
self._publish_live_event(
|
|
611
|
+
"spawner_complete",
|
|
612
|
+
"gemini",
|
|
613
|
+
success=True,
|
|
614
|
+
duration=duration,
|
|
615
|
+
response=response_text,
|
|
616
|
+
tokens=tokens,
|
|
617
|
+
)
|
|
484
618
|
return AIResult(
|
|
485
619
|
success=True,
|
|
486
620
|
response=response_text,
|
|
@@ -498,6 +632,14 @@ class HeadlessSpawner:
|
|
|
498
632
|
try:
|
|
499
633
|
output = json.loads(result.stdout)
|
|
500
634
|
except json.JSONDecodeError as e:
|
|
635
|
+
duration = time.time() - start_time
|
|
636
|
+
self._publish_live_event(
|
|
637
|
+
"spawner_complete",
|
|
638
|
+
"gemini",
|
|
639
|
+
success=False,
|
|
640
|
+
duration=duration,
|
|
641
|
+
error=f"Failed to parse JSON: {e}",
|
|
642
|
+
)
|
|
501
643
|
return AIResult(
|
|
502
644
|
success=False,
|
|
503
645
|
response="",
|
|
@@ -521,6 +663,16 @@ class HeadlessSpawner:
|
|
|
521
663
|
total_tokens += model_tokens
|
|
522
664
|
tokens = total_tokens if total_tokens > 0 else None
|
|
523
665
|
|
|
666
|
+
# Publish live event: complete
|
|
667
|
+
duration = time.time() - start_time
|
|
668
|
+
self._publish_live_event(
|
|
669
|
+
"spawner_complete",
|
|
670
|
+
"gemini",
|
|
671
|
+
success=True,
|
|
672
|
+
duration=duration,
|
|
673
|
+
response=response_text,
|
|
674
|
+
tokens=tokens,
|
|
675
|
+
)
|
|
524
676
|
return AIResult(
|
|
525
677
|
success=True,
|
|
526
678
|
response=response_text,
|
|
@@ -531,6 +683,14 @@ class HeadlessSpawner:
|
|
|
531
683
|
)
|
|
532
684
|
|
|
533
685
|
except subprocess.TimeoutExpired as e:
|
|
686
|
+
duration = time.time() - start_time
|
|
687
|
+
self._publish_live_event(
|
|
688
|
+
"spawner_complete",
|
|
689
|
+
"gemini",
|
|
690
|
+
success=False,
|
|
691
|
+
duration=duration,
|
|
692
|
+
error=f"Timed out after {timeout} seconds",
|
|
693
|
+
)
|
|
534
694
|
return AIResult(
|
|
535
695
|
success=False,
|
|
536
696
|
response="",
|
|
@@ -545,6 +705,14 @@ class HeadlessSpawner:
|
|
|
545
705
|
tracked_events=tracked_events,
|
|
546
706
|
)
|
|
547
707
|
except FileNotFoundError:
|
|
708
|
+
duration = time.time() - start_time
|
|
709
|
+
self._publish_live_event(
|
|
710
|
+
"spawner_complete",
|
|
711
|
+
"gemini",
|
|
712
|
+
success=False,
|
|
713
|
+
duration=duration,
|
|
714
|
+
error="CLI not found",
|
|
715
|
+
)
|
|
548
716
|
return AIResult(
|
|
549
717
|
success=False,
|
|
550
718
|
response="",
|
|
@@ -554,6 +722,14 @@ class HeadlessSpawner:
|
|
|
554
722
|
tracked_events=tracked_events,
|
|
555
723
|
)
|
|
556
724
|
except Exception as e:
|
|
725
|
+
duration = time.time() - start_time
|
|
726
|
+
self._publish_live_event(
|
|
727
|
+
"spawner_complete",
|
|
728
|
+
"gemini",
|
|
729
|
+
success=False,
|
|
730
|
+
duration=duration,
|
|
731
|
+
error=str(e),
|
|
732
|
+
)
|
|
557
733
|
return AIResult(
|
|
558
734
|
success=False,
|
|
559
735
|
response="",
|
|
@@ -608,6 +784,15 @@ class HeadlessSpawner:
|
|
|
608
784
|
if track_in_htmlgraph and output_json:
|
|
609
785
|
sdk = self._get_sdk()
|
|
610
786
|
|
|
787
|
+
# Publish live event: spawner starting
|
|
788
|
+
self._publish_live_event(
|
|
789
|
+
"spawner_start",
|
|
790
|
+
"codex",
|
|
791
|
+
prompt=prompt,
|
|
792
|
+
model=model,
|
|
793
|
+
)
|
|
794
|
+
start_time = time.time()
|
|
795
|
+
|
|
611
796
|
cmd = ["codex", "exec"]
|
|
612
797
|
|
|
613
798
|
if output_json:
|
|
@@ -674,6 +859,14 @@ class HeadlessSpawner:
|
|
|
674
859
|
pass
|
|
675
860
|
|
|
676
861
|
try:
|
|
862
|
+
# Publish live event: executing
|
|
863
|
+
self._publish_live_event(
|
|
864
|
+
"spawner_phase",
|
|
865
|
+
"codex",
|
|
866
|
+
phase="executing",
|
|
867
|
+
details="Running Codex CLI",
|
|
868
|
+
)
|
|
869
|
+
|
|
677
870
|
result = subprocess.run(
|
|
678
871
|
cmd,
|
|
679
872
|
stdout=subprocess.PIPE,
|
|
@@ -682,13 +875,31 @@ class HeadlessSpawner:
|
|
|
682
875
|
timeout=timeout,
|
|
683
876
|
)
|
|
684
877
|
|
|
878
|
+
# Publish live event: processing
|
|
879
|
+
self._publish_live_event(
|
|
880
|
+
"spawner_phase",
|
|
881
|
+
"codex",
|
|
882
|
+
phase="processing",
|
|
883
|
+
details="Parsing Codex response",
|
|
884
|
+
)
|
|
885
|
+
|
|
685
886
|
if not output_json:
|
|
686
887
|
# Plain text mode - return as-is
|
|
888
|
+
duration = time.time() - start_time
|
|
889
|
+
success = result.returncode == 0
|
|
890
|
+
self._publish_live_event(
|
|
891
|
+
"spawner_complete",
|
|
892
|
+
"codex",
|
|
893
|
+
success=success,
|
|
894
|
+
duration=duration,
|
|
895
|
+
response=result.stdout.strip()[:200] if success else None,
|
|
896
|
+
error="Command failed" if not success else None,
|
|
897
|
+
)
|
|
687
898
|
return AIResult(
|
|
688
|
-
success=
|
|
899
|
+
success=success,
|
|
689
900
|
response=result.stdout.strip(),
|
|
690
901
|
tokens_used=None,
|
|
691
|
-
error=None if
|
|
902
|
+
error=None if success else "Command failed",
|
|
692
903
|
raw_output=result.stdout,
|
|
693
904
|
tracked_events=tracked_events,
|
|
694
905
|
)
|
|
@@ -735,11 +946,23 @@ class HeadlessSpawner:
|
|
|
735
946
|
# Sum all token types
|
|
736
947
|
tokens = sum(usage.values())
|
|
737
948
|
|
|
949
|
+
# Publish live event: complete
|
|
950
|
+
duration = time.time() - start_time
|
|
951
|
+
success = result.returncode == 0
|
|
952
|
+
self._publish_live_event(
|
|
953
|
+
"spawner_complete",
|
|
954
|
+
"codex",
|
|
955
|
+
success=success,
|
|
956
|
+
duration=duration,
|
|
957
|
+
response=response[:200] if response else None,
|
|
958
|
+
tokens=tokens,
|
|
959
|
+
error="Command failed" if not success else None,
|
|
960
|
+
)
|
|
738
961
|
return AIResult(
|
|
739
|
-
success=
|
|
962
|
+
success=success,
|
|
740
963
|
response=response or "",
|
|
741
964
|
tokens_used=tokens,
|
|
742
|
-
error=None if
|
|
965
|
+
error=None if success else "Command failed",
|
|
743
966
|
raw_output={
|
|
744
967
|
"events": events,
|
|
745
968
|
"parse_errors": parse_errors if parse_errors else None,
|
|
@@ -748,6 +971,14 @@ class HeadlessSpawner:
|
|
|
748
971
|
)
|
|
749
972
|
|
|
750
973
|
except FileNotFoundError:
|
|
974
|
+
duration = time.time() - start_time
|
|
975
|
+
self._publish_live_event(
|
|
976
|
+
"spawner_complete",
|
|
977
|
+
"codex",
|
|
978
|
+
success=False,
|
|
979
|
+
duration=duration,
|
|
980
|
+
error="CLI not found",
|
|
981
|
+
)
|
|
751
982
|
return AIResult(
|
|
752
983
|
success=False,
|
|
753
984
|
response="",
|
|
@@ -757,6 +988,14 @@ class HeadlessSpawner:
|
|
|
757
988
|
tracked_events=tracked_events,
|
|
758
989
|
)
|
|
759
990
|
except subprocess.TimeoutExpired as e:
|
|
991
|
+
duration = time.time() - start_time
|
|
992
|
+
self._publish_live_event(
|
|
993
|
+
"spawner_complete",
|
|
994
|
+
"codex",
|
|
995
|
+
success=False,
|
|
996
|
+
duration=duration,
|
|
997
|
+
error=f"Timed out after {timeout} seconds",
|
|
998
|
+
)
|
|
760
999
|
return AIResult(
|
|
761
1000
|
success=False,
|
|
762
1001
|
response="",
|
|
@@ -771,6 +1010,14 @@ class HeadlessSpawner:
|
|
|
771
1010
|
tracked_events=tracked_events,
|
|
772
1011
|
)
|
|
773
1012
|
except Exception as e:
|
|
1013
|
+
duration = time.time() - start_time
|
|
1014
|
+
self._publish_live_event(
|
|
1015
|
+
"spawner_complete",
|
|
1016
|
+
"codex",
|
|
1017
|
+
success=False,
|
|
1018
|
+
duration=duration,
|
|
1019
|
+
error=str(e),
|
|
1020
|
+
)
|
|
774
1021
|
return AIResult(
|
|
775
1022
|
success=False,
|
|
776
1023
|
response="",
|
|
@@ -809,6 +1056,14 @@ class HeadlessSpawner:
|
|
|
809
1056
|
if track_in_htmlgraph:
|
|
810
1057
|
sdk = self._get_sdk()
|
|
811
1058
|
|
|
1059
|
+
# Publish live event: spawner starting
|
|
1060
|
+
self._publish_live_event(
|
|
1061
|
+
"spawner_start",
|
|
1062
|
+
"copilot",
|
|
1063
|
+
prompt=prompt,
|
|
1064
|
+
)
|
|
1065
|
+
start_time = time.time()
|
|
1066
|
+
|
|
812
1067
|
cmd = ["copilot", "-p", prompt]
|
|
813
1068
|
|
|
814
1069
|
# Add allow all tools flag
|
|
@@ -838,6 +1093,14 @@ class HeadlessSpawner:
|
|
|
838
1093
|
pass
|
|
839
1094
|
|
|
840
1095
|
try:
|
|
1096
|
+
# Publish live event: executing
|
|
1097
|
+
self._publish_live_event(
|
|
1098
|
+
"spawner_phase",
|
|
1099
|
+
"copilot",
|
|
1100
|
+
phase="executing",
|
|
1101
|
+
details="Running Copilot CLI",
|
|
1102
|
+
)
|
|
1103
|
+
|
|
841
1104
|
result = subprocess.run(
|
|
842
1105
|
cmd,
|
|
843
1106
|
capture_output=True,
|
|
@@ -845,6 +1108,14 @@ class HeadlessSpawner:
|
|
|
845
1108
|
timeout=timeout,
|
|
846
1109
|
)
|
|
847
1110
|
|
|
1111
|
+
# Publish live event: processing
|
|
1112
|
+
self._publish_live_event(
|
|
1113
|
+
"spawner_phase",
|
|
1114
|
+
"copilot",
|
|
1115
|
+
phase="processing",
|
|
1116
|
+
details="Parsing Copilot response",
|
|
1117
|
+
)
|
|
1118
|
+
|
|
848
1119
|
# Parse output: response is before stats block
|
|
849
1120
|
lines = result.stdout.split("\n")
|
|
850
1121
|
|
|
@@ -874,16 +1145,36 @@ class HeadlessSpawner:
|
|
|
874
1145
|
prompt, response, sdk
|
|
875
1146
|
)
|
|
876
1147
|
|
|
1148
|
+
# Publish live event: complete
|
|
1149
|
+
duration = time.time() - start_time
|
|
1150
|
+
success = result.returncode == 0
|
|
1151
|
+
self._publish_live_event(
|
|
1152
|
+
"spawner_complete",
|
|
1153
|
+
"copilot",
|
|
1154
|
+
success=success,
|
|
1155
|
+
duration=duration,
|
|
1156
|
+
response=response[:200] if response else None,
|
|
1157
|
+
tokens=tokens,
|
|
1158
|
+
error=result.stderr if not success else None,
|
|
1159
|
+
)
|
|
877
1160
|
return AIResult(
|
|
878
|
-
success=
|
|
1161
|
+
success=success,
|
|
879
1162
|
response=response,
|
|
880
1163
|
tokens_used=tokens,
|
|
881
|
-
error=None if
|
|
1164
|
+
error=None if success else result.stderr,
|
|
882
1165
|
raw_output=result.stdout,
|
|
883
1166
|
tracked_events=tracked_events,
|
|
884
1167
|
)
|
|
885
1168
|
|
|
886
1169
|
except FileNotFoundError:
|
|
1170
|
+
duration = time.time() - start_time
|
|
1171
|
+
self._publish_live_event(
|
|
1172
|
+
"spawner_complete",
|
|
1173
|
+
"copilot",
|
|
1174
|
+
success=False,
|
|
1175
|
+
duration=duration,
|
|
1176
|
+
error="CLI not found",
|
|
1177
|
+
)
|
|
887
1178
|
return AIResult(
|
|
888
1179
|
success=False,
|
|
889
1180
|
response="",
|
|
@@ -893,6 +1184,14 @@ class HeadlessSpawner:
|
|
|
893
1184
|
tracked_events=tracked_events,
|
|
894
1185
|
)
|
|
895
1186
|
except subprocess.TimeoutExpired as e:
|
|
1187
|
+
duration = time.time() - start_time
|
|
1188
|
+
self._publish_live_event(
|
|
1189
|
+
"spawner_complete",
|
|
1190
|
+
"copilot",
|
|
1191
|
+
success=False,
|
|
1192
|
+
duration=duration,
|
|
1193
|
+
error=f"Timed out after {timeout} seconds",
|
|
1194
|
+
)
|
|
896
1195
|
return AIResult(
|
|
897
1196
|
success=False,
|
|
898
1197
|
response="",
|
|
@@ -907,6 +1206,14 @@ class HeadlessSpawner:
|
|
|
907
1206
|
tracked_events=tracked_events,
|
|
908
1207
|
)
|
|
909
1208
|
except Exception as e:
|
|
1209
|
+
duration = time.time() - start_time
|
|
1210
|
+
self._publish_live_event(
|
|
1211
|
+
"spawner_complete",
|
|
1212
|
+
"copilot",
|
|
1213
|
+
success=False,
|
|
1214
|
+
duration=duration,
|
|
1215
|
+
error=str(e),
|
|
1216
|
+
)
|
|
910
1217
|
return AIResult(
|
|
911
1218
|
success=False,
|
|
912
1219
|
response="",
|