htmlgraph 0.25.0__py3-none-any.whl → 0.26.2__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/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/main.py +252 -47
- htmlgraph/api/templates/dashboard.html +11 -0
- htmlgraph/api/templates/partials/activity-feed.html +517 -8
- htmlgraph/cli.py +1 -1
- htmlgraph/config.py +173 -96
- htmlgraph/dashboard.html +632 -7237
- htmlgraph/db/schema.py +258 -9
- 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 +88 -10
- htmlgraph/hooks/drift_handler.py +24 -20
- htmlgraph/hooks/event_tracker.py +264 -189
- htmlgraph/hooks/orchestrator.py +6 -4
- htmlgraph/hooks/orchestrator_reflector.py +4 -4
- htmlgraph/hooks/pretooluse.py +63 -36
- htmlgraph/hooks/prompt_analyzer.py +14 -25
- htmlgraph/hooks/session_handler.py +123 -69
- htmlgraph/hooks/state_manager.py +7 -4
- htmlgraph/hooks/subagent_stop.py +3 -2
- htmlgraph/hooks/validator.py +15 -11
- htmlgraph/operations/fastapi_server.py +2 -2
- htmlgraph/orchestration/headless_spawner.py +489 -16
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/server.py +100 -203
- htmlgraph-0.26.2.data/data/htmlgraph/dashboard.html +812 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/METADATA +1 -1
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/RECORD +40 -32
- htmlgraph-0.25.0.data/data/htmlgraph/dashboard.html +0 -7417
- {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.25.0.data → htmlgraph-0.26.2.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/WHEEL +0 -0
- {htmlgraph-0.25.0.dist-info → htmlgraph-0.26.2.dist-info}/entry_points.txt +0 -0
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
6
8
|
from dataclasses import dataclass
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
12
|
+
from htmlgraph.orchestration.live_events import LiveEventPublisher
|
|
10
13
|
from htmlgraph.sdk import SDK
|
|
11
14
|
|
|
12
15
|
|
|
@@ -63,7 +66,92 @@ class HeadlessSpawner:
|
|
|
63
66
|
|
|
64
67
|
def __init__(self) -> None:
|
|
65
68
|
"""Initialize spawner."""
|
|
66
|
-
|
|
69
|
+
self._live_publisher: LiveEventPublisher | None = None
|
|
70
|
+
|
|
71
|
+
def _get_live_publisher(self) -> "LiveEventPublisher | None":
|
|
72
|
+
"""
|
|
73
|
+
Get LiveEventPublisher instance for real-time WebSocket streaming.
|
|
74
|
+
|
|
75
|
+
Returns None if publisher unavailable (optional dependency).
|
|
76
|
+
"""
|
|
77
|
+
if self._live_publisher is None:
|
|
78
|
+
try:
|
|
79
|
+
from htmlgraph.orchestration.live_events import LiveEventPublisher
|
|
80
|
+
|
|
81
|
+
self._live_publisher = LiveEventPublisher()
|
|
82
|
+
except Exception:
|
|
83
|
+
# Live events are optional
|
|
84
|
+
pass
|
|
85
|
+
return self._live_publisher
|
|
86
|
+
|
|
87
|
+
def _publish_live_event(
|
|
88
|
+
self,
|
|
89
|
+
event_type: str,
|
|
90
|
+
spawner_type: str,
|
|
91
|
+
**kwargs: str | int | float | bool | None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Publish a live event for WebSocket streaming.
|
|
95
|
+
|
|
96
|
+
Silently fails if publisher unavailable (optional feature).
|
|
97
|
+
"""
|
|
98
|
+
publisher = self._get_live_publisher()
|
|
99
|
+
if publisher is None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
parent_event_id = os.getenv("HTMLGRAPH_PARENT_EVENT")
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
if event_type == "spawner_start":
|
|
106
|
+
publisher.spawner_start(
|
|
107
|
+
spawner_type=spawner_type,
|
|
108
|
+
prompt=str(kwargs.get("prompt", "")),
|
|
109
|
+
parent_event_id=parent_event_id,
|
|
110
|
+
model=str(kwargs.get("model", "")) if kwargs.get("model") else None,
|
|
111
|
+
)
|
|
112
|
+
elif event_type == "spawner_phase":
|
|
113
|
+
progress_val = kwargs.get("progress")
|
|
114
|
+
publisher.spawner_phase(
|
|
115
|
+
spawner_type=spawner_type,
|
|
116
|
+
phase=str(kwargs.get("phase", "executing")),
|
|
117
|
+
progress=int(progress_val) if progress_val is not None else None,
|
|
118
|
+
details=str(kwargs.get("details", ""))
|
|
119
|
+
if kwargs.get("details")
|
|
120
|
+
else None,
|
|
121
|
+
parent_event_id=parent_event_id,
|
|
122
|
+
)
|
|
123
|
+
elif event_type == "spawner_complete":
|
|
124
|
+
duration_val = kwargs.get("duration")
|
|
125
|
+
tokens_val = kwargs.get("tokens")
|
|
126
|
+
publisher.spawner_complete(
|
|
127
|
+
spawner_type=spawner_type,
|
|
128
|
+
success=bool(kwargs.get("success", False)),
|
|
129
|
+
duration_seconds=float(duration_val)
|
|
130
|
+
if duration_val is not None
|
|
131
|
+
else None,
|
|
132
|
+
response_preview=str(kwargs.get("response", ""))[:200]
|
|
133
|
+
if kwargs.get("response")
|
|
134
|
+
else None,
|
|
135
|
+
tokens_used=int(tokens_val) if tokens_val is not None else None,
|
|
136
|
+
error=str(kwargs.get("error", "")) if kwargs.get("error") else None,
|
|
137
|
+
parent_event_id=parent_event_id,
|
|
138
|
+
)
|
|
139
|
+
elif event_type == "spawner_tool_use":
|
|
140
|
+
publisher.spawner_tool_use(
|
|
141
|
+
spawner_type=spawner_type,
|
|
142
|
+
tool_name=str(kwargs.get("tool_name", "unknown")),
|
|
143
|
+
parent_event_id=parent_event_id,
|
|
144
|
+
)
|
|
145
|
+
elif event_type == "spawner_message":
|
|
146
|
+
publisher.spawner_message(
|
|
147
|
+
spawner_type=spawner_type,
|
|
148
|
+
message=str(kwargs.get("message", "")),
|
|
149
|
+
role=str(kwargs.get("role", "assistant")),
|
|
150
|
+
parent_event_id=parent_event_id,
|
|
151
|
+
)
|
|
152
|
+
except Exception:
|
|
153
|
+
# Live events should never break spawner execution
|
|
154
|
+
pass
|
|
67
155
|
|
|
68
156
|
def _get_sdk(self) -> "SDK | None":
|
|
69
157
|
"""
|
|
@@ -376,6 +464,8 @@ class HeadlessSpawner:
|
|
|
376
464
|
include_directories: list[str] | None = None,
|
|
377
465
|
track_in_htmlgraph: bool = True,
|
|
378
466
|
timeout: int = 120,
|
|
467
|
+
tracker: Any = None,
|
|
468
|
+
parent_event_id: str | None = None,
|
|
379
469
|
) -> AIResult:
|
|
380
470
|
"""
|
|
381
471
|
Spawn Gemini in headless mode.
|
|
@@ -387,6 +477,8 @@ class HeadlessSpawner:
|
|
|
387
477
|
include_directories: Directories to include for context. Default: None
|
|
388
478
|
track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
|
|
389
479
|
timeout: Max seconds to wait
|
|
480
|
+
tracker: Optional SpawnerEventTracker for recording subprocess invocation
|
|
481
|
+
parent_event_id: Optional parent event ID for event hierarchy
|
|
390
482
|
|
|
391
483
|
Returns:
|
|
392
484
|
AIResult with response, error, and tracked events if tracking enabled
|
|
@@ -397,6 +489,15 @@ class HeadlessSpawner:
|
|
|
397
489
|
if track_in_htmlgraph:
|
|
398
490
|
sdk = self._get_sdk()
|
|
399
491
|
|
|
492
|
+
# Publish live event: spawner starting
|
|
493
|
+
self._publish_live_event(
|
|
494
|
+
"spawner_start",
|
|
495
|
+
"gemini",
|
|
496
|
+
prompt=prompt,
|
|
497
|
+
model=model,
|
|
498
|
+
)
|
|
499
|
+
start_time = time.time()
|
|
500
|
+
|
|
400
501
|
try:
|
|
401
502
|
# Build command based on tested pattern from spike spk-4029eef3
|
|
402
503
|
cmd = ["gemini", "-p", prompt, "--output-format", output_format]
|
|
@@ -425,6 +526,53 @@ class HeadlessSpawner:
|
|
|
425
526
|
# Tracking failure should not break execution
|
|
426
527
|
pass
|
|
427
528
|
|
|
529
|
+
# Publish live event: executing
|
|
530
|
+
self._publish_live_event(
|
|
531
|
+
"spawner_phase",
|
|
532
|
+
"gemini",
|
|
533
|
+
phase="executing",
|
|
534
|
+
details="Running Gemini CLI",
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Record subprocess invocation if tracker is available
|
|
538
|
+
subprocess_event_id = None
|
|
539
|
+
print(
|
|
540
|
+
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
|
|
541
|
+
file=sys.stderr,
|
|
542
|
+
)
|
|
543
|
+
if tracker and parent_event_id:
|
|
544
|
+
print(
|
|
545
|
+
"DEBUG: Recording subprocess invocation for Gemini...",
|
|
546
|
+
file=sys.stderr,
|
|
547
|
+
)
|
|
548
|
+
try:
|
|
549
|
+
subprocess_event = tracker.record_tool_call(
|
|
550
|
+
tool_name="subprocess.gemini",
|
|
551
|
+
tool_input={"cmd": cmd},
|
|
552
|
+
phase_event_id=parent_event_id,
|
|
553
|
+
spawned_agent="gemini-2.0-flash",
|
|
554
|
+
)
|
|
555
|
+
if subprocess_event:
|
|
556
|
+
subprocess_event_id = subprocess_event.get("event_id")
|
|
557
|
+
print(
|
|
558
|
+
f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}",
|
|
559
|
+
file=sys.stderr,
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
print("DEBUG: subprocess_event was None", file=sys.stderr)
|
|
563
|
+
except Exception as e:
|
|
564
|
+
# Tracking failure should not break execution
|
|
565
|
+
print(
|
|
566
|
+
f"DEBUG: Exception recording Gemini subprocess: {e}",
|
|
567
|
+
file=sys.stderr,
|
|
568
|
+
)
|
|
569
|
+
pass
|
|
570
|
+
else:
|
|
571
|
+
print(
|
|
572
|
+
f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
|
|
573
|
+
file=sys.stderr,
|
|
574
|
+
)
|
|
575
|
+
|
|
428
576
|
# Execute with timeout and stderr redirection
|
|
429
577
|
# Note: Cannot use capture_output with stderr parameter
|
|
430
578
|
result = subprocess.run(
|
|
@@ -435,8 +583,36 @@ class HeadlessSpawner:
|
|
|
435
583
|
timeout=timeout,
|
|
436
584
|
)
|
|
437
585
|
|
|
586
|
+
# Complete subprocess invocation tracking
|
|
587
|
+
if tracker and subprocess_event_id:
|
|
588
|
+
try:
|
|
589
|
+
tracker.complete_tool_call(
|
|
590
|
+
event_id=subprocess_event_id,
|
|
591
|
+
output_summary=result.stdout[:500] if result.stdout else "",
|
|
592
|
+
success=result.returncode == 0,
|
|
593
|
+
)
|
|
594
|
+
except Exception:
|
|
595
|
+
# Tracking failure should not break execution
|
|
596
|
+
pass
|
|
597
|
+
|
|
598
|
+
# Publish live event: processing response
|
|
599
|
+
self._publish_live_event(
|
|
600
|
+
"spawner_phase",
|
|
601
|
+
"gemini",
|
|
602
|
+
phase="processing",
|
|
603
|
+
details="Parsing Gemini response",
|
|
604
|
+
)
|
|
605
|
+
|
|
438
606
|
# Check for command execution errors
|
|
439
607
|
if result.returncode != 0:
|
|
608
|
+
duration = time.time() - start_time
|
|
609
|
+
self._publish_live_event(
|
|
610
|
+
"spawner_complete",
|
|
611
|
+
"gemini",
|
|
612
|
+
success=False,
|
|
613
|
+
duration=duration,
|
|
614
|
+
error=f"CLI failed with exit code {result.returncode}",
|
|
615
|
+
)
|
|
440
616
|
return AIResult(
|
|
441
617
|
success=False,
|
|
442
618
|
response="",
|
|
@@ -455,16 +631,20 @@ class HeadlessSpawner:
|
|
|
455
631
|
# Only use stream-json parsing if we got valid events
|
|
456
632
|
if tracked_events:
|
|
457
633
|
# For stream-json, we need to extract response differently
|
|
458
|
-
#
|
|
634
|
+
# Collect all assistant message content, then check result
|
|
459
635
|
response_text = ""
|
|
460
636
|
for event in tracked_events:
|
|
461
|
-
if event.get("type") == "
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
637
|
+
if event.get("type") == "message":
|
|
638
|
+
# Only collect assistant messages
|
|
639
|
+
if event.get("role") == "assistant":
|
|
640
|
+
content = event.get("content", "")
|
|
641
|
+
if content:
|
|
642
|
+
response_text += content
|
|
643
|
+
elif event.get("type") == "result":
|
|
644
|
+
# Result event may have response field (override if present)
|
|
645
|
+
if "response" in event and event["response"]:
|
|
646
|
+
response_text = event["response"]
|
|
647
|
+
# Don't break - we've already collected messages
|
|
468
648
|
|
|
469
649
|
# Token usage from stats in result event
|
|
470
650
|
tokens = None
|
|
@@ -481,6 +661,16 @@ class HeadlessSpawner:
|
|
|
481
661
|
tokens = total_tokens if total_tokens > 0 else None
|
|
482
662
|
break
|
|
483
663
|
|
|
664
|
+
# Publish live event: complete
|
|
665
|
+
duration = time.time() - start_time
|
|
666
|
+
self._publish_live_event(
|
|
667
|
+
"spawner_complete",
|
|
668
|
+
"gemini",
|
|
669
|
+
success=True,
|
|
670
|
+
duration=duration,
|
|
671
|
+
response=response_text,
|
|
672
|
+
tokens=tokens,
|
|
673
|
+
)
|
|
484
674
|
return AIResult(
|
|
485
675
|
success=True,
|
|
486
676
|
response=response_text,
|
|
@@ -498,6 +688,14 @@ class HeadlessSpawner:
|
|
|
498
688
|
try:
|
|
499
689
|
output = json.loads(result.stdout)
|
|
500
690
|
except json.JSONDecodeError as e:
|
|
691
|
+
duration = time.time() - start_time
|
|
692
|
+
self._publish_live_event(
|
|
693
|
+
"spawner_complete",
|
|
694
|
+
"gemini",
|
|
695
|
+
success=False,
|
|
696
|
+
duration=duration,
|
|
697
|
+
error=f"Failed to parse JSON: {e}",
|
|
698
|
+
)
|
|
501
699
|
return AIResult(
|
|
502
700
|
success=False,
|
|
503
701
|
response="",
|
|
@@ -521,6 +719,16 @@ class HeadlessSpawner:
|
|
|
521
719
|
total_tokens += model_tokens
|
|
522
720
|
tokens = total_tokens if total_tokens > 0 else None
|
|
523
721
|
|
|
722
|
+
# Publish live event: complete
|
|
723
|
+
duration = time.time() - start_time
|
|
724
|
+
self._publish_live_event(
|
|
725
|
+
"spawner_complete",
|
|
726
|
+
"gemini",
|
|
727
|
+
success=True,
|
|
728
|
+
duration=duration,
|
|
729
|
+
response=response_text,
|
|
730
|
+
tokens=tokens,
|
|
731
|
+
)
|
|
524
732
|
return AIResult(
|
|
525
733
|
success=True,
|
|
526
734
|
response=response_text,
|
|
@@ -531,6 +739,14 @@ class HeadlessSpawner:
|
|
|
531
739
|
)
|
|
532
740
|
|
|
533
741
|
except subprocess.TimeoutExpired as e:
|
|
742
|
+
duration = time.time() - start_time
|
|
743
|
+
self._publish_live_event(
|
|
744
|
+
"spawner_complete",
|
|
745
|
+
"gemini",
|
|
746
|
+
success=False,
|
|
747
|
+
duration=duration,
|
|
748
|
+
error=f"Timed out after {timeout} seconds",
|
|
749
|
+
)
|
|
534
750
|
return AIResult(
|
|
535
751
|
success=False,
|
|
536
752
|
response="",
|
|
@@ -545,6 +761,14 @@ class HeadlessSpawner:
|
|
|
545
761
|
tracked_events=tracked_events,
|
|
546
762
|
)
|
|
547
763
|
except FileNotFoundError:
|
|
764
|
+
duration = time.time() - start_time
|
|
765
|
+
self._publish_live_event(
|
|
766
|
+
"spawner_complete",
|
|
767
|
+
"gemini",
|
|
768
|
+
success=False,
|
|
769
|
+
duration=duration,
|
|
770
|
+
error="CLI not found",
|
|
771
|
+
)
|
|
548
772
|
return AIResult(
|
|
549
773
|
success=False,
|
|
550
774
|
response="",
|
|
@@ -554,6 +778,14 @@ class HeadlessSpawner:
|
|
|
554
778
|
tracked_events=tracked_events,
|
|
555
779
|
)
|
|
556
780
|
except Exception as e:
|
|
781
|
+
duration = time.time() - start_time
|
|
782
|
+
self._publish_live_event(
|
|
783
|
+
"spawner_complete",
|
|
784
|
+
"gemini",
|
|
785
|
+
success=False,
|
|
786
|
+
duration=duration,
|
|
787
|
+
error=str(e),
|
|
788
|
+
)
|
|
557
789
|
return AIResult(
|
|
558
790
|
success=False,
|
|
559
791
|
response="",
|
|
@@ -579,6 +811,8 @@ class HeadlessSpawner:
|
|
|
579
811
|
bypass_approvals: bool = False,
|
|
580
812
|
track_in_htmlgraph: bool = True,
|
|
581
813
|
timeout: int = 120,
|
|
814
|
+
tracker: Any = None,
|
|
815
|
+
parent_event_id: str | None = None,
|
|
582
816
|
) -> AIResult:
|
|
583
817
|
"""
|
|
584
818
|
Spawn Codex in headless mode.
|
|
@@ -598,6 +832,8 @@ class HeadlessSpawner:
|
|
|
598
832
|
bypass_approvals: Bypass approval checks. Default: False
|
|
599
833
|
track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
|
|
600
834
|
timeout: Max seconds to wait
|
|
835
|
+
tracker: Optional SpawnerEventTracker for recording subprocess invocation
|
|
836
|
+
parent_event_id: Optional parent event ID for event hierarchy
|
|
601
837
|
|
|
602
838
|
Returns:
|
|
603
839
|
AIResult with response, error, and tracked events if tracking enabled
|
|
@@ -608,6 +844,15 @@ class HeadlessSpawner:
|
|
|
608
844
|
if track_in_htmlgraph and output_json:
|
|
609
845
|
sdk = self._get_sdk()
|
|
610
846
|
|
|
847
|
+
# Publish live event: spawner starting
|
|
848
|
+
self._publish_live_event(
|
|
849
|
+
"spawner_start",
|
|
850
|
+
"codex",
|
|
851
|
+
prompt=prompt,
|
|
852
|
+
model=model,
|
|
853
|
+
)
|
|
854
|
+
start_time = time.time()
|
|
855
|
+
|
|
611
856
|
cmd = ["codex", "exec"]
|
|
612
857
|
|
|
613
858
|
if output_json:
|
|
@@ -674,6 +919,53 @@ class HeadlessSpawner:
|
|
|
674
919
|
pass
|
|
675
920
|
|
|
676
921
|
try:
|
|
922
|
+
# Publish live event: executing
|
|
923
|
+
self._publish_live_event(
|
|
924
|
+
"spawner_phase",
|
|
925
|
+
"codex",
|
|
926
|
+
phase="executing",
|
|
927
|
+
details="Running Codex CLI",
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
# Record subprocess invocation if tracker is available
|
|
931
|
+
subprocess_event_id = None
|
|
932
|
+
print(
|
|
933
|
+
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
|
|
934
|
+
file=sys.stderr,
|
|
935
|
+
)
|
|
936
|
+
if tracker and parent_event_id:
|
|
937
|
+
print(
|
|
938
|
+
"DEBUG: Recording subprocess invocation for Codex...",
|
|
939
|
+
file=sys.stderr,
|
|
940
|
+
)
|
|
941
|
+
try:
|
|
942
|
+
subprocess_event = tracker.record_tool_call(
|
|
943
|
+
tool_name="subprocess.codex",
|
|
944
|
+
tool_input={"cmd": cmd},
|
|
945
|
+
phase_event_id=parent_event_id,
|
|
946
|
+
spawned_agent="gpt-4",
|
|
947
|
+
)
|
|
948
|
+
if subprocess_event:
|
|
949
|
+
subprocess_event_id = subprocess_event.get("event_id")
|
|
950
|
+
print(
|
|
951
|
+
f"DEBUG: Subprocess event created for Codex: {subprocess_event_id}",
|
|
952
|
+
file=sys.stderr,
|
|
953
|
+
)
|
|
954
|
+
else:
|
|
955
|
+
print("DEBUG: subprocess_event was None", file=sys.stderr)
|
|
956
|
+
except Exception as e:
|
|
957
|
+
# Tracking failure should not break execution
|
|
958
|
+
print(
|
|
959
|
+
f"DEBUG: Exception recording Codex subprocess: {e}",
|
|
960
|
+
file=sys.stderr,
|
|
961
|
+
)
|
|
962
|
+
pass
|
|
963
|
+
else:
|
|
964
|
+
print(
|
|
965
|
+
f"DEBUG: Skipping Codex subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
|
|
966
|
+
file=sys.stderr,
|
|
967
|
+
)
|
|
968
|
+
|
|
677
969
|
result = subprocess.run(
|
|
678
970
|
cmd,
|
|
679
971
|
stdout=subprocess.PIPE,
|
|
@@ -682,13 +974,43 @@ class HeadlessSpawner:
|
|
|
682
974
|
timeout=timeout,
|
|
683
975
|
)
|
|
684
976
|
|
|
977
|
+
# Complete subprocess invocation tracking
|
|
978
|
+
if tracker and subprocess_event_id:
|
|
979
|
+
try:
|
|
980
|
+
tracker.complete_tool_call(
|
|
981
|
+
event_id=subprocess_event_id,
|
|
982
|
+
output_summary=result.stdout[:500] if result.stdout else "",
|
|
983
|
+
success=result.returncode == 0,
|
|
984
|
+
)
|
|
985
|
+
except Exception:
|
|
986
|
+
# Tracking failure should not break execution
|
|
987
|
+
pass
|
|
988
|
+
|
|
989
|
+
# Publish live event: processing
|
|
990
|
+
self._publish_live_event(
|
|
991
|
+
"spawner_phase",
|
|
992
|
+
"codex",
|
|
993
|
+
phase="processing",
|
|
994
|
+
details="Parsing Codex response",
|
|
995
|
+
)
|
|
996
|
+
|
|
685
997
|
if not output_json:
|
|
686
998
|
# Plain text mode - return as-is
|
|
999
|
+
duration = time.time() - start_time
|
|
1000
|
+
success = result.returncode == 0
|
|
1001
|
+
self._publish_live_event(
|
|
1002
|
+
"spawner_complete",
|
|
1003
|
+
"codex",
|
|
1004
|
+
success=success,
|
|
1005
|
+
duration=duration,
|
|
1006
|
+
response=result.stdout.strip()[:200] if success else None,
|
|
1007
|
+
error="Command failed" if not success else None,
|
|
1008
|
+
)
|
|
687
1009
|
return AIResult(
|
|
688
|
-
success=
|
|
1010
|
+
success=success,
|
|
689
1011
|
response=result.stdout.strip(),
|
|
690
1012
|
tokens_used=None,
|
|
691
|
-
error=None if
|
|
1013
|
+
error=None if success else "Command failed",
|
|
692
1014
|
raw_output=result.stdout,
|
|
693
1015
|
tracked_events=tracked_events,
|
|
694
1016
|
)
|
|
@@ -735,11 +1057,23 @@ class HeadlessSpawner:
|
|
|
735
1057
|
# Sum all token types
|
|
736
1058
|
tokens = sum(usage.values())
|
|
737
1059
|
|
|
1060
|
+
# Publish live event: complete
|
|
1061
|
+
duration = time.time() - start_time
|
|
1062
|
+
success = result.returncode == 0
|
|
1063
|
+
self._publish_live_event(
|
|
1064
|
+
"spawner_complete",
|
|
1065
|
+
"codex",
|
|
1066
|
+
success=success,
|
|
1067
|
+
duration=duration,
|
|
1068
|
+
response=response[:200] if response else None,
|
|
1069
|
+
tokens=tokens,
|
|
1070
|
+
error="Command failed" if not success else None,
|
|
1071
|
+
)
|
|
738
1072
|
return AIResult(
|
|
739
|
-
success=
|
|
1073
|
+
success=success,
|
|
740
1074
|
response=response or "",
|
|
741
1075
|
tokens_used=tokens,
|
|
742
|
-
error=None if
|
|
1076
|
+
error=None if success else "Command failed",
|
|
743
1077
|
raw_output={
|
|
744
1078
|
"events": events,
|
|
745
1079
|
"parse_errors": parse_errors if parse_errors else None,
|
|
@@ -748,6 +1082,14 @@ class HeadlessSpawner:
|
|
|
748
1082
|
)
|
|
749
1083
|
|
|
750
1084
|
except FileNotFoundError:
|
|
1085
|
+
duration = time.time() - start_time
|
|
1086
|
+
self._publish_live_event(
|
|
1087
|
+
"spawner_complete",
|
|
1088
|
+
"codex",
|
|
1089
|
+
success=False,
|
|
1090
|
+
duration=duration,
|
|
1091
|
+
error="CLI not found",
|
|
1092
|
+
)
|
|
751
1093
|
return AIResult(
|
|
752
1094
|
success=False,
|
|
753
1095
|
response="",
|
|
@@ -757,6 +1099,14 @@ class HeadlessSpawner:
|
|
|
757
1099
|
tracked_events=tracked_events,
|
|
758
1100
|
)
|
|
759
1101
|
except subprocess.TimeoutExpired as e:
|
|
1102
|
+
duration = time.time() - start_time
|
|
1103
|
+
self._publish_live_event(
|
|
1104
|
+
"spawner_complete",
|
|
1105
|
+
"codex",
|
|
1106
|
+
success=False,
|
|
1107
|
+
duration=duration,
|
|
1108
|
+
error=f"Timed out after {timeout} seconds",
|
|
1109
|
+
)
|
|
760
1110
|
return AIResult(
|
|
761
1111
|
success=False,
|
|
762
1112
|
response="",
|
|
@@ -771,6 +1121,14 @@ class HeadlessSpawner:
|
|
|
771
1121
|
tracked_events=tracked_events,
|
|
772
1122
|
)
|
|
773
1123
|
except Exception as e:
|
|
1124
|
+
duration = time.time() - start_time
|
|
1125
|
+
self._publish_live_event(
|
|
1126
|
+
"spawner_complete",
|
|
1127
|
+
"codex",
|
|
1128
|
+
success=False,
|
|
1129
|
+
duration=duration,
|
|
1130
|
+
error=str(e),
|
|
1131
|
+
)
|
|
774
1132
|
return AIResult(
|
|
775
1133
|
success=False,
|
|
776
1134
|
response="",
|
|
@@ -788,6 +1146,8 @@ class HeadlessSpawner:
|
|
|
788
1146
|
deny_tools: list[str] | None = None,
|
|
789
1147
|
track_in_htmlgraph: bool = True,
|
|
790
1148
|
timeout: int = 120,
|
|
1149
|
+
tracker: Any = None,
|
|
1150
|
+
parent_event_id: str | None = None,
|
|
791
1151
|
) -> AIResult:
|
|
792
1152
|
"""
|
|
793
1153
|
Spawn GitHub Copilot in headless mode.
|
|
@@ -799,6 +1159,8 @@ class HeadlessSpawner:
|
|
|
799
1159
|
deny_tools: Tools to deny (--deny-tool). Default: None
|
|
800
1160
|
track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
|
|
801
1161
|
timeout: Max seconds to wait
|
|
1162
|
+
tracker: Optional SpawnerEventTracker for recording subprocess invocation
|
|
1163
|
+
parent_event_id: Optional parent event ID for event hierarchy
|
|
802
1164
|
|
|
803
1165
|
Returns:
|
|
804
1166
|
AIResult with response, error, and tracked events if tracking enabled
|
|
@@ -809,6 +1171,14 @@ class HeadlessSpawner:
|
|
|
809
1171
|
if track_in_htmlgraph:
|
|
810
1172
|
sdk = self._get_sdk()
|
|
811
1173
|
|
|
1174
|
+
# Publish live event: spawner starting
|
|
1175
|
+
self._publish_live_event(
|
|
1176
|
+
"spawner_start",
|
|
1177
|
+
"copilot",
|
|
1178
|
+
prompt=prompt,
|
|
1179
|
+
)
|
|
1180
|
+
start_time = time.time()
|
|
1181
|
+
|
|
812
1182
|
cmd = ["copilot", "-p", prompt]
|
|
813
1183
|
|
|
814
1184
|
# Add allow all tools flag
|
|
@@ -838,6 +1208,53 @@ class HeadlessSpawner:
|
|
|
838
1208
|
pass
|
|
839
1209
|
|
|
840
1210
|
try:
|
|
1211
|
+
# Publish live event: executing
|
|
1212
|
+
self._publish_live_event(
|
|
1213
|
+
"spawner_phase",
|
|
1214
|
+
"copilot",
|
|
1215
|
+
phase="executing",
|
|
1216
|
+
details="Running Copilot CLI",
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
# Record subprocess invocation if tracker is available
|
|
1220
|
+
subprocess_event_id = None
|
|
1221
|
+
print(
|
|
1222
|
+
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}",
|
|
1223
|
+
file=sys.stderr,
|
|
1224
|
+
)
|
|
1225
|
+
if tracker and parent_event_id:
|
|
1226
|
+
print(
|
|
1227
|
+
"DEBUG: Recording subprocess invocation for Copilot...",
|
|
1228
|
+
file=sys.stderr,
|
|
1229
|
+
)
|
|
1230
|
+
try:
|
|
1231
|
+
subprocess_event = tracker.record_tool_call(
|
|
1232
|
+
tool_name="subprocess.copilot",
|
|
1233
|
+
tool_input={"cmd": cmd},
|
|
1234
|
+
phase_event_id=parent_event_id,
|
|
1235
|
+
spawned_agent="github-copilot",
|
|
1236
|
+
)
|
|
1237
|
+
if subprocess_event:
|
|
1238
|
+
subprocess_event_id = subprocess_event.get("event_id")
|
|
1239
|
+
print(
|
|
1240
|
+
f"DEBUG: Subprocess event created for Copilot: {subprocess_event_id}",
|
|
1241
|
+
file=sys.stderr,
|
|
1242
|
+
)
|
|
1243
|
+
else:
|
|
1244
|
+
print("DEBUG: subprocess_event was None", file=sys.stderr)
|
|
1245
|
+
except Exception as e:
|
|
1246
|
+
# Tracking failure should not break execution
|
|
1247
|
+
print(
|
|
1248
|
+
f"DEBUG: Exception recording Copilot subprocess: {e}",
|
|
1249
|
+
file=sys.stderr,
|
|
1250
|
+
)
|
|
1251
|
+
pass
|
|
1252
|
+
else:
|
|
1253
|
+
print(
|
|
1254
|
+
f"DEBUG: Skipping Copilot subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}",
|
|
1255
|
+
file=sys.stderr,
|
|
1256
|
+
)
|
|
1257
|
+
|
|
841
1258
|
result = subprocess.run(
|
|
842
1259
|
cmd,
|
|
843
1260
|
capture_output=True,
|
|
@@ -845,6 +1262,26 @@ class HeadlessSpawner:
|
|
|
845
1262
|
timeout=timeout,
|
|
846
1263
|
)
|
|
847
1264
|
|
|
1265
|
+
# Complete subprocess invocation tracking
|
|
1266
|
+
if tracker and subprocess_event_id:
|
|
1267
|
+
try:
|
|
1268
|
+
tracker.complete_tool_call(
|
|
1269
|
+
event_id=subprocess_event_id,
|
|
1270
|
+
output_summary=result.stdout[:500] if result.stdout else "",
|
|
1271
|
+
success=result.returncode == 0,
|
|
1272
|
+
)
|
|
1273
|
+
except Exception:
|
|
1274
|
+
# Tracking failure should not break execution
|
|
1275
|
+
pass
|
|
1276
|
+
|
|
1277
|
+
# Publish live event: processing
|
|
1278
|
+
self._publish_live_event(
|
|
1279
|
+
"spawner_phase",
|
|
1280
|
+
"copilot",
|
|
1281
|
+
phase="processing",
|
|
1282
|
+
details="Parsing Copilot response",
|
|
1283
|
+
)
|
|
1284
|
+
|
|
848
1285
|
# Parse output: response is before stats block
|
|
849
1286
|
lines = result.stdout.split("\n")
|
|
850
1287
|
|
|
@@ -874,16 +1311,36 @@ class HeadlessSpawner:
|
|
|
874
1311
|
prompt, response, sdk
|
|
875
1312
|
)
|
|
876
1313
|
|
|
1314
|
+
# Publish live event: complete
|
|
1315
|
+
duration = time.time() - start_time
|
|
1316
|
+
success = result.returncode == 0
|
|
1317
|
+
self._publish_live_event(
|
|
1318
|
+
"spawner_complete",
|
|
1319
|
+
"copilot",
|
|
1320
|
+
success=success,
|
|
1321
|
+
duration=duration,
|
|
1322
|
+
response=response[:200] if response else None,
|
|
1323
|
+
tokens=tokens,
|
|
1324
|
+
error=result.stderr if not success else None,
|
|
1325
|
+
)
|
|
877
1326
|
return AIResult(
|
|
878
|
-
success=
|
|
1327
|
+
success=success,
|
|
879
1328
|
response=response,
|
|
880
1329
|
tokens_used=tokens,
|
|
881
|
-
error=None if
|
|
1330
|
+
error=None if success else result.stderr,
|
|
882
1331
|
raw_output=result.stdout,
|
|
883
1332
|
tracked_events=tracked_events,
|
|
884
1333
|
)
|
|
885
1334
|
|
|
886
1335
|
except FileNotFoundError:
|
|
1336
|
+
duration = time.time() - start_time
|
|
1337
|
+
self._publish_live_event(
|
|
1338
|
+
"spawner_complete",
|
|
1339
|
+
"copilot",
|
|
1340
|
+
success=False,
|
|
1341
|
+
duration=duration,
|
|
1342
|
+
error="CLI not found",
|
|
1343
|
+
)
|
|
887
1344
|
return AIResult(
|
|
888
1345
|
success=False,
|
|
889
1346
|
response="",
|
|
@@ -893,6 +1350,14 @@ class HeadlessSpawner:
|
|
|
893
1350
|
tracked_events=tracked_events,
|
|
894
1351
|
)
|
|
895
1352
|
except subprocess.TimeoutExpired as e:
|
|
1353
|
+
duration = time.time() - start_time
|
|
1354
|
+
self._publish_live_event(
|
|
1355
|
+
"spawner_complete",
|
|
1356
|
+
"copilot",
|
|
1357
|
+
success=False,
|
|
1358
|
+
duration=duration,
|
|
1359
|
+
error=f"Timed out after {timeout} seconds",
|
|
1360
|
+
)
|
|
896
1361
|
return AIResult(
|
|
897
1362
|
success=False,
|
|
898
1363
|
response="",
|
|
@@ -907,6 +1372,14 @@ class HeadlessSpawner:
|
|
|
907
1372
|
tracked_events=tracked_events,
|
|
908
1373
|
)
|
|
909
1374
|
except Exception as e:
|
|
1375
|
+
duration = time.time() - start_time
|
|
1376
|
+
self._publish_live_event(
|
|
1377
|
+
"spawner_complete",
|
|
1378
|
+
"copilot",
|
|
1379
|
+
success=False,
|
|
1380
|
+
duration=duration,
|
|
1381
|
+
error=str(e),
|
|
1382
|
+
)
|
|
910
1383
|
return AIResult(
|
|
911
1384
|
success=False,
|
|
912
1385
|
response="",
|