htmlgraph 0.26.24__py3-none-any.whl → 0.27.0__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 +23 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/cli.py +3 -3
- htmlgraph/analytics/cost_analyzer.py +5 -1
- htmlgraph/analytics/cross_session.py +13 -9
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/work_type.py +15 -11
- htmlgraph/analytics_index.py +2 -1
- htmlgraph/api/main.py +114 -51
- htmlgraph/api/templates/dashboard-redesign.html +3 -3
- htmlgraph/api/templates/dashboard.html +3 -3
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/builders/base.py +2 -1
- htmlgraph/builders/bug.py +2 -1
- htmlgraph/builders/chore.py +2 -1
- htmlgraph/builders/epic.py +2 -1
- htmlgraph/builders/feature.py +2 -1
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +2 -1
- htmlgraph/builders/spike.py +2 -1
- htmlgraph/builders/track.py +28 -1
- htmlgraph/cli/analytics.py +2 -1
- htmlgraph/cli/base.py +33 -8
- htmlgraph/cli/core.py +2 -1
- htmlgraph/cli/main.py +2 -1
- htmlgraph/cli/models.py +2 -1
- htmlgraph/cli/templates/cost_dashboard.py +2 -1
- htmlgraph/cli/work/__init__.py +76 -1
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +2 -1
- htmlgraph/cli/work/orchestration.py +2 -1
- htmlgraph/cli/work/report.py +2 -1
- htmlgraph/cli/work/sessions.py +2 -1
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +2 -1
- htmlgraph/collections/base.py +43 -4
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +12 -7
- htmlgraph/collections/spike.py +6 -1
- htmlgraph/collections/task_delegation.py +7 -2
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +15 -10
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +11 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/edge_index.py +2 -1
- htmlgraph/event_log.py +81 -66
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +6 -2
- htmlgraph/hooks/cigs_pretool_enforcer.py +5 -1
- htmlgraph/hooks/drift_handler.py +3 -3
- htmlgraph/hooks/event_tracker.py +40 -61
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +92 -14
- htmlgraph/hooks/orchestrator_reflector.py +4 -0
- htmlgraph/hooks/post_tool_use_failure.py +7 -3
- htmlgraph/hooks/posttooluse.py +4 -0
- htmlgraph/hooks/prompt_analyzer.py +5 -5
- htmlgraph/hooks/session_handler.py +5 -2
- htmlgraph/hooks/session_summary.py +6 -2
- htmlgraph/hooks/validator.py +8 -4
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +2 -1
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +18 -1
- htmlgraph/operations/analytics.py +2 -1
- htmlgraph/operations/bootstrap.py +2 -1
- htmlgraph/operations/events.py +2 -1
- htmlgraph/operations/fastapi_server.py +2 -1
- htmlgraph/operations/hooks.py +2 -1
- htmlgraph/operations/initialization.py +2 -1
- htmlgraph/operations/server.py +2 -1
- htmlgraph/orchestration/__init__.py +4 -0
- htmlgraph/orchestration/claude_launcher.py +23 -20
- htmlgraph/orchestration/command_builder.py +2 -1
- htmlgraph/orchestration/headless_spawner.py +6 -2
- htmlgraph/orchestration/model_selection.py +7 -3
- htmlgraph/orchestration/plugin_manager.py +25 -21
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/claude.py +5 -2
- htmlgraph/orchestration/spawners/codex.py +12 -19
- htmlgraph/orchestration/spawners/copilot.py +13 -18
- htmlgraph/orchestration/spawners/gemini.py +12 -19
- htmlgraph/orchestration/subprocess_runner.py +6 -3
- htmlgraph/orchestration/task_coordination.py +16 -8
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/parallel.py +2 -1
- htmlgraph/query_builder.py +2 -1
- htmlgraph/reflection.py +2 -1
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +2 -1
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/server.py +21 -17
- htmlgraph/session_manager.py +1 -7
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/handoff.py +10 -3
- htmlgraph/system_prompts.py +2 -1
- htmlgraph/track_builder.py +14 -1
- htmlgraph/transcript.py +2 -1
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/METADATA +15 -1
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/RECORD +154 -117
- htmlgraph/sdk.py +0 -3430
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/dashboard.html +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.26.24.data → htmlgraph-0.27.0.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/WHEEL +0 -0
- {htmlgraph-0.26.24.dist-info → htmlgraph-0.27.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Spawner event tracking helper for internal activity tracking in spawned sessions.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for tracking internal activities within spawner agents
|
|
4
|
+
and linking them to parent delegation events for observability in HtmlGraph.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from htmlgraph.orchestration.spawner_event_tracker import SpawnerEventTracker
|
|
8
|
+
|
|
9
|
+
tracker = SpawnerEventTracker(
|
|
10
|
+
delegation_event_id="event-abc123",
|
|
11
|
+
parent_agent="orchestrator",
|
|
12
|
+
spawner_type="gemini"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Track initialization phase
|
|
16
|
+
init_event = tracker.record_phase("Initializing Spawner", spawned_agent="gemini-2.0-flash")
|
|
17
|
+
|
|
18
|
+
# Track execution phase
|
|
19
|
+
exec_event = tracker.record_phase("Executing Gemini", tool_name="gemini-cli")
|
|
20
|
+
tracker.complete_phase(exec_event["event_id"], output_summary="Generated output...")
|
|
21
|
+
|
|
22
|
+
# Track completion
|
|
23
|
+
tracker.record_completion(success=True, response="Result here")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import time
|
|
28
|
+
import uuid
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SpawnerEventTracker:
|
|
33
|
+
"""Track internal activities in spawner agents with parent-child linking."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
delegation_event_id: str | None = None,
|
|
38
|
+
parent_agent: str = "orchestrator",
|
|
39
|
+
spawner_type: str = "generic",
|
|
40
|
+
session_id: str | None = None,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize spawner event tracker.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
delegation_event_id: Parent delegation event ID to link to
|
|
47
|
+
parent_agent: Agent that initiated the spawning
|
|
48
|
+
spawner_type: Type of spawner (gemini, codex, copilot)
|
|
49
|
+
session_id: Session ID for events
|
|
50
|
+
"""
|
|
51
|
+
self.delegation_event_id = delegation_event_id
|
|
52
|
+
self.parent_agent = parent_agent
|
|
53
|
+
self.spawner_type = spawner_type
|
|
54
|
+
self.session_id = session_id or f"session-{uuid.uuid4().hex[:8]}"
|
|
55
|
+
self.db = None
|
|
56
|
+
self.phase_events: dict[str, dict[str, Any]] = {}
|
|
57
|
+
self.start_time = time.time()
|
|
58
|
+
|
|
59
|
+
# Try to initialize database for event tracking
|
|
60
|
+
try:
|
|
61
|
+
from htmlgraph.config import get_database_path
|
|
62
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
63
|
+
|
|
64
|
+
# Get correct database path from environment or project root
|
|
65
|
+
db_path = get_database_path()
|
|
66
|
+
|
|
67
|
+
if db_path.exists():
|
|
68
|
+
self.db = HtmlGraphDB(str(db_path))
|
|
69
|
+
except Exception:
|
|
70
|
+
# Tracking is optional, continue without it
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
def record_phase(
|
|
74
|
+
self,
|
|
75
|
+
phase_name: str,
|
|
76
|
+
spawned_agent: str | None = None,
|
|
77
|
+
tool_name: str | None = None,
|
|
78
|
+
input_summary: str | None = None,
|
|
79
|
+
status: str = "running",
|
|
80
|
+
parent_phase_event_id: str | None = None,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""
|
|
83
|
+
Record the start of a phase in spawner execution.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
phase_name: Human-readable phase name (e.g., "Initializing Spawner")
|
|
87
|
+
spawned_agent: Agent being spawned (optional)
|
|
88
|
+
tool_name: Tool being executed (optional)
|
|
89
|
+
input_summary: Summary of input (optional)
|
|
90
|
+
status: Current status (running, completed, failed)
|
|
91
|
+
parent_phase_event_id: Parent phase event ID for proper nesting (optional)
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Event dictionary with event_id and metadata
|
|
95
|
+
"""
|
|
96
|
+
if not self.db:
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
event_id = f"event-{uuid.uuid4().hex[:8]}"
|
|
100
|
+
event_type = "tool_call"
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
context = {
|
|
104
|
+
"phase_name": phase_name,
|
|
105
|
+
"spawner_type": self.spawner_type,
|
|
106
|
+
"parent_delegation_event": self.delegation_event_id,
|
|
107
|
+
}
|
|
108
|
+
if spawned_agent:
|
|
109
|
+
context["spawned_agent"] = spawned_agent
|
|
110
|
+
if tool_name:
|
|
111
|
+
context["tool"] = tool_name
|
|
112
|
+
|
|
113
|
+
# Use parent_phase_event_id if provided, otherwise use delegation_event_id
|
|
114
|
+
actual_parent_event_id = parent_phase_event_id or self.delegation_event_id
|
|
115
|
+
|
|
116
|
+
self.db.insert_event(
|
|
117
|
+
event_id=event_id,
|
|
118
|
+
agent_id=spawned_agent or self.parent_agent,
|
|
119
|
+
event_type=event_type,
|
|
120
|
+
session_id=self.session_id,
|
|
121
|
+
tool_name=tool_name
|
|
122
|
+
or f"HeadlessSpawner.{phase_name.replace(' ', '_').lower()}",
|
|
123
|
+
input_summary=input_summary or phase_name,
|
|
124
|
+
context=context,
|
|
125
|
+
parent_event_id=actual_parent_event_id,
|
|
126
|
+
subagent_type=self.spawner_type,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
event = {
|
|
130
|
+
"event_id": event_id,
|
|
131
|
+
"phase_name": phase_name,
|
|
132
|
+
"spawned_agent": spawned_agent,
|
|
133
|
+
"tool_name": tool_name,
|
|
134
|
+
"status": status,
|
|
135
|
+
"start_time": time.time(),
|
|
136
|
+
}
|
|
137
|
+
self.phase_events[event_id] = event
|
|
138
|
+
return event
|
|
139
|
+
|
|
140
|
+
except Exception:
|
|
141
|
+
# Non-fatal - tracking is best-effort
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
def complete_phase(
|
|
145
|
+
self,
|
|
146
|
+
event_id: str,
|
|
147
|
+
output_summary: str | None = None,
|
|
148
|
+
status: str = "completed",
|
|
149
|
+
execution_duration: float | None = None,
|
|
150
|
+
) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Mark a phase as completed with results.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
event_id: Event ID from record_phase
|
|
156
|
+
output_summary: Summary of output/result
|
|
157
|
+
status: Final status (completed, failed)
|
|
158
|
+
execution_duration: Execution time in seconds (auto-calculated if not provided)
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
True if update successful, False otherwise
|
|
162
|
+
"""
|
|
163
|
+
if not self.db or not event_id:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
if execution_duration is None and event_id in self.phase_events:
|
|
168
|
+
execution_duration = (
|
|
169
|
+
time.time() - self.phase_events[event_id]["start_time"]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
if self.db.connection is None:
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
cursor = self.db.connection.cursor()
|
|
176
|
+
cursor.execute(
|
|
177
|
+
"""
|
|
178
|
+
UPDATE agent_events
|
|
179
|
+
SET output_summary = ?, status = ?, execution_duration_seconds = ?,
|
|
180
|
+
updated_at = CURRENT_TIMESTAMP
|
|
181
|
+
WHERE event_id = ?
|
|
182
|
+
""",
|
|
183
|
+
(output_summary, status, execution_duration or 0.0, event_id),
|
|
184
|
+
)
|
|
185
|
+
self.db.connection.commit()
|
|
186
|
+
|
|
187
|
+
if event_id in self.phase_events:
|
|
188
|
+
self.phase_events[event_id]["status"] = status
|
|
189
|
+
self.phase_events[event_id]["output_summary"] = output_summary
|
|
190
|
+
|
|
191
|
+
return True
|
|
192
|
+
except Exception:
|
|
193
|
+
# Non-fatal
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def record_completion(
|
|
197
|
+
self,
|
|
198
|
+
success: bool,
|
|
199
|
+
response: str | None = None,
|
|
200
|
+
error: str | None = None,
|
|
201
|
+
tokens_used: int = 0,
|
|
202
|
+
cost: float = 0.0,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Record final completion with overall results.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
success: Whether execution succeeded
|
|
209
|
+
response: Successful response/output
|
|
210
|
+
error: Error message if failed
|
|
211
|
+
tokens_used: Tokens consumed
|
|
212
|
+
cost: Execution cost
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Completion event dictionary
|
|
216
|
+
"""
|
|
217
|
+
total_duration = time.time() - self.start_time
|
|
218
|
+
|
|
219
|
+
completion_event: dict[str, Any] = {
|
|
220
|
+
"success": success,
|
|
221
|
+
"duration": total_duration,
|
|
222
|
+
"tokens": tokens_used,
|
|
223
|
+
"cost": cost,
|
|
224
|
+
"phase_count": len(self.phase_events),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if success:
|
|
228
|
+
completion_event["response"] = response
|
|
229
|
+
else:
|
|
230
|
+
completion_event["error"] = error
|
|
231
|
+
|
|
232
|
+
return completion_event
|
|
233
|
+
|
|
234
|
+
def get_phase_events(self) -> dict[str, dict[str, Any]]:
|
|
235
|
+
"""Get all recorded phase events."""
|
|
236
|
+
return self.phase_events
|
|
237
|
+
|
|
238
|
+
def record_tool_call(
|
|
239
|
+
self,
|
|
240
|
+
tool_name: str,
|
|
241
|
+
tool_input: dict | None,
|
|
242
|
+
phase_event_id: str,
|
|
243
|
+
spawned_agent: str | None = None,
|
|
244
|
+
) -> dict[str, Any]:
|
|
245
|
+
"""
|
|
246
|
+
Record a tool call within a spawned execution phase.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
tool_name: Name of the tool (bash, read_file, write_file, etc.)
|
|
250
|
+
tool_input: Input parameters to the tool
|
|
251
|
+
phase_event_id: Parent phase event ID to link to
|
|
252
|
+
spawned_agent: Agent making the tool call (optional)
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Event dictionary with event_id and metadata
|
|
256
|
+
"""
|
|
257
|
+
if not self.db:
|
|
258
|
+
return {}
|
|
259
|
+
|
|
260
|
+
event_id = f"event-{uuid.uuid4().hex[:8]}"
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
context = {
|
|
264
|
+
"tool_name": tool_name,
|
|
265
|
+
"spawner_type": self.spawner_type,
|
|
266
|
+
"parent_phase_event": phase_event_id,
|
|
267
|
+
}
|
|
268
|
+
if spawned_agent:
|
|
269
|
+
context["spawned_agent"] = spawned_agent
|
|
270
|
+
|
|
271
|
+
self.db.insert_event(
|
|
272
|
+
event_id=event_id,
|
|
273
|
+
agent_id=spawned_agent or self.parent_agent,
|
|
274
|
+
event_type="tool_call",
|
|
275
|
+
session_id=self.session_id,
|
|
276
|
+
tool_name=tool_name,
|
|
277
|
+
input_summary=(
|
|
278
|
+
str(tool_input)[:200] if tool_input else f"Call to {tool_name}"
|
|
279
|
+
),
|
|
280
|
+
context=context,
|
|
281
|
+
parent_event_id=phase_event_id,
|
|
282
|
+
subagent_type=self.spawner_type,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
tool_event = {
|
|
286
|
+
"event_id": event_id,
|
|
287
|
+
"tool_name": tool_name,
|
|
288
|
+
"tool_input": tool_input,
|
|
289
|
+
"phase_event_id": phase_event_id,
|
|
290
|
+
"spawned_agent": spawned_agent,
|
|
291
|
+
"status": "running",
|
|
292
|
+
"start_time": time.time(),
|
|
293
|
+
}
|
|
294
|
+
return tool_event
|
|
295
|
+
|
|
296
|
+
except Exception:
|
|
297
|
+
# Non-fatal - tracking is best-effort
|
|
298
|
+
return {}
|
|
299
|
+
|
|
300
|
+
def complete_tool_call(
|
|
301
|
+
self,
|
|
302
|
+
event_id: str,
|
|
303
|
+
output_summary: str | None = None,
|
|
304
|
+
success: bool = True,
|
|
305
|
+
) -> bool:
|
|
306
|
+
"""
|
|
307
|
+
Mark a tool call as complete with results.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
event_id: Event ID from record_tool_call
|
|
311
|
+
output_summary: Summary of tool output/result
|
|
312
|
+
success: Whether the tool call succeeded
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
True if update successful, False otherwise
|
|
316
|
+
"""
|
|
317
|
+
if not self.db or not event_id:
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
if self.db.connection is None:
|
|
322
|
+
return False
|
|
323
|
+
|
|
324
|
+
cursor = self.db.connection.cursor()
|
|
325
|
+
cursor.execute(
|
|
326
|
+
"""
|
|
327
|
+
UPDATE agent_events
|
|
328
|
+
SET output_summary = ?, status = ?, updated_at = CURRENT_TIMESTAMP
|
|
329
|
+
WHERE event_id = ?
|
|
330
|
+
""",
|
|
331
|
+
(
|
|
332
|
+
output_summary,
|
|
333
|
+
"completed" if success else "failed",
|
|
334
|
+
event_id,
|
|
335
|
+
),
|
|
336
|
+
)
|
|
337
|
+
self.db.connection.commit()
|
|
338
|
+
return True
|
|
339
|
+
except Exception:
|
|
340
|
+
# Non-fatal
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
def get_event_hierarchy(self) -> dict[str, Any]:
|
|
344
|
+
"""
|
|
345
|
+
Get the event hierarchy for this spawner execution.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Dictionary with delegation event as root and phases as children
|
|
349
|
+
"""
|
|
350
|
+
return {
|
|
351
|
+
"delegation_event_id": self.delegation_event_id,
|
|
352
|
+
"spawner_type": self.spawner_type,
|
|
353
|
+
"session_id": self.session_id,
|
|
354
|
+
"total_duration": time.time() - self.start_time,
|
|
355
|
+
"phase_events": self.phase_events,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def create_tracker_from_env(
|
|
360
|
+
spawner_type: str = "generic",
|
|
361
|
+
) -> SpawnerEventTracker:
|
|
362
|
+
"""
|
|
363
|
+
Create a SpawnerEventTracker from environment variables.
|
|
364
|
+
|
|
365
|
+
Reads HTMLGRAPH_PARENT_EVENT, HTMLGRAPH_PARENT_AGENT, HTMLGRAPH_PARENT_SESSION
|
|
366
|
+
from environment to link to parent context.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
spawner_type: Type of spawner (gemini, codex, copilot)
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Initialized SpawnerEventTracker
|
|
373
|
+
"""
|
|
374
|
+
delegation_event_id = os.getenv("HTMLGRAPH_PARENT_EVENT")
|
|
375
|
+
parent_agent = os.getenv("HTMLGRAPH_PARENT_AGENT", "orchestrator")
|
|
376
|
+
session_id = os.getenv("HTMLGRAPH_PARENT_SESSION")
|
|
377
|
+
|
|
378
|
+
return SpawnerEventTracker(
|
|
379
|
+
delegation_event_id=delegation_event_id,
|
|
380
|
+
parent_agent=parent_agent,
|
|
381
|
+
spawner_type=spawner_type,
|
|
382
|
+
session_id=session_id,
|
|
383
|
+
)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""Claude spawner implementation."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import subprocess
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from .base import AIResult, BaseSpawner
|
|
8
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
9
12
|
if TYPE_CHECKING:
|
|
10
13
|
pass
|
|
11
14
|
|
|
@@ -64,8 +67,8 @@ class ClaudeSpawner(BaseSpawner):
|
|
|
64
67
|
>>> spawner = ClaudeSpawner()
|
|
65
68
|
>>> result = spawner.spawn("What is 2+2?")
|
|
66
69
|
>>> if result.success:
|
|
67
|
-
...
|
|
68
|
-
...
|
|
70
|
+
... logger.info("%s", result.response) # "4"
|
|
71
|
+
... logger.info(f"Cost: ${result.raw_output['total_cost_usd']}")
|
|
69
72
|
"""
|
|
70
73
|
cmd = ["claude", "-p"]
|
|
71
74
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""Codex spawner implementation."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import subprocess
|
|
5
|
-
import sys
|
|
6
6
|
import time
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from .base import AIResult, BaseSpawner
|
|
10
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
from htmlgraph.sdk import SDK
|
|
13
15
|
|
|
@@ -232,15 +234,11 @@ class CodexSpawner(BaseSpawner):
|
|
|
232
234
|
|
|
233
235
|
# Record subprocess invocation if tracker is available
|
|
234
236
|
subprocess_event_id = None
|
|
235
|
-
|
|
236
|
-
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
237
|
-
file=sys.stderr,
|
|
237
|
+
logger.warning(
|
|
238
|
+
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
238
239
|
)
|
|
239
240
|
if tracker and parent_event_id:
|
|
240
|
-
|
|
241
|
-
"DEBUG: Recording subprocess invocation for Codex...",
|
|
242
|
-
file=sys.stderr,
|
|
243
|
-
)
|
|
241
|
+
logger.debug("Recording subprocess invocation for Codex...")
|
|
244
242
|
try:
|
|
245
243
|
subprocess_event = tracker.record_tool_call(
|
|
246
244
|
tool_name="subprocess.codex",
|
|
@@ -250,23 +248,18 @@ class CodexSpawner(BaseSpawner):
|
|
|
250
248
|
)
|
|
251
249
|
if subprocess_event:
|
|
252
250
|
subprocess_event_id = subprocess_event.get("event_id")
|
|
253
|
-
|
|
254
|
-
f"DEBUG: Subprocess event created for Codex: {subprocess_event_id}"
|
|
255
|
-
file=sys.stderr,
|
|
251
|
+
logger.warning(
|
|
252
|
+
f"DEBUG: Subprocess event created for Codex: {subprocess_event_id}"
|
|
256
253
|
)
|
|
257
254
|
else:
|
|
258
|
-
|
|
255
|
+
logger.debug("subprocess_event was None")
|
|
259
256
|
except Exception as e:
|
|
260
257
|
# Tracking failure should not break execution
|
|
261
|
-
|
|
262
|
-
f"DEBUG: Exception recording Codex subprocess: {e}",
|
|
263
|
-
file=sys.stderr,
|
|
264
|
-
)
|
|
258
|
+
logger.warning(f"DEBUG: Exception recording Codex subprocess: {e}")
|
|
265
259
|
pass
|
|
266
260
|
else:
|
|
267
|
-
|
|
268
|
-
f"DEBUG: Skipping Codex subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
269
|
-
file=sys.stderr,
|
|
261
|
+
logger.warning(
|
|
262
|
+
f"DEBUG: Skipping Codex subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
270
263
|
)
|
|
271
264
|
|
|
272
265
|
result = subprocess.run(
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"""Copilot spawner implementation."""
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
import subprocess
|
|
4
|
-
import sys
|
|
5
5
|
import time
|
|
6
6
|
from typing import TYPE_CHECKING, Any
|
|
7
7
|
|
|
8
8
|
from .base import AIResult, BaseSpawner
|
|
9
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
10
12
|
if TYPE_CHECKING:
|
|
11
13
|
from htmlgraph.sdk import SDK
|
|
12
14
|
|
|
@@ -129,15 +131,11 @@ class CopilotSpawner(BaseSpawner):
|
|
|
129
131
|
|
|
130
132
|
# Record subprocess invocation if tracker is available
|
|
131
133
|
subprocess_event_id = None
|
|
132
|
-
|
|
133
|
-
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
134
|
-
file=sys.stderr,
|
|
134
|
+
logger.warning(
|
|
135
|
+
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
135
136
|
)
|
|
136
137
|
if tracker and parent_event_id:
|
|
137
|
-
|
|
138
|
-
"DEBUG: Recording subprocess invocation for Copilot...",
|
|
139
|
-
file=sys.stderr,
|
|
140
|
-
)
|
|
138
|
+
logger.debug("Recording subprocess invocation for Copilot...")
|
|
141
139
|
try:
|
|
142
140
|
subprocess_event = tracker.record_tool_call(
|
|
143
141
|
tool_name="subprocess.copilot",
|
|
@@ -147,23 +145,20 @@ class CopilotSpawner(BaseSpawner):
|
|
|
147
145
|
)
|
|
148
146
|
if subprocess_event:
|
|
149
147
|
subprocess_event_id = subprocess_event.get("event_id")
|
|
150
|
-
|
|
151
|
-
f"DEBUG: Subprocess event created for Copilot: {subprocess_event_id}"
|
|
152
|
-
file=sys.stderr,
|
|
148
|
+
logger.warning(
|
|
149
|
+
f"DEBUG: Subprocess event created for Copilot: {subprocess_event_id}"
|
|
153
150
|
)
|
|
154
151
|
else:
|
|
155
|
-
|
|
152
|
+
logger.debug("subprocess_event was None")
|
|
156
153
|
except Exception as e:
|
|
157
154
|
# Tracking failure should not break execution
|
|
158
|
-
|
|
159
|
-
f"DEBUG: Exception recording Copilot subprocess: {e}"
|
|
160
|
-
file=sys.stderr,
|
|
155
|
+
logger.warning(
|
|
156
|
+
f"DEBUG: Exception recording Copilot subprocess: {e}"
|
|
161
157
|
)
|
|
162
158
|
pass
|
|
163
159
|
else:
|
|
164
|
-
|
|
165
|
-
f"DEBUG: Skipping Copilot subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
166
|
-
file=sys.stderr,
|
|
160
|
+
logger.warning(
|
|
161
|
+
f"DEBUG: Skipping Copilot subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
167
162
|
)
|
|
168
163
|
|
|
169
164
|
result = subprocess.run(
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
"""Gemini spawner implementation."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import subprocess
|
|
5
|
-
import sys
|
|
6
6
|
import time
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
9
|
from .base import AIResult, BaseSpawner
|
|
10
10
|
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
11
13
|
if TYPE_CHECKING:
|
|
12
14
|
from htmlgraph.sdk import SDK
|
|
13
15
|
|
|
@@ -220,15 +222,11 @@ class GeminiSpawner(BaseSpawner):
|
|
|
220
222
|
|
|
221
223
|
# Record subprocess invocation if tracker is available
|
|
222
224
|
subprocess_event_id = None
|
|
223
|
-
|
|
224
|
-
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
225
|
-
file=sys.stderr,
|
|
225
|
+
logger.warning(
|
|
226
|
+
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
226
227
|
)
|
|
227
228
|
if tracker and parent_event_id:
|
|
228
|
-
|
|
229
|
-
"DEBUG: Recording subprocess invocation for Gemini...",
|
|
230
|
-
file=sys.stderr,
|
|
231
|
-
)
|
|
229
|
+
logger.debug("Recording subprocess invocation for Gemini...")
|
|
232
230
|
try:
|
|
233
231
|
subprocess_event = tracker.record_tool_call(
|
|
234
232
|
tool_name="subprocess.gemini",
|
|
@@ -238,23 +236,18 @@ class GeminiSpawner(BaseSpawner):
|
|
|
238
236
|
)
|
|
239
237
|
if subprocess_event:
|
|
240
238
|
subprocess_event_id = subprocess_event.get("event_id")
|
|
241
|
-
|
|
242
|
-
f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}"
|
|
243
|
-
file=sys.stderr,
|
|
239
|
+
logger.warning(
|
|
240
|
+
f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}"
|
|
244
241
|
)
|
|
245
242
|
else:
|
|
246
|
-
|
|
243
|
+
logger.debug("subprocess_event was None")
|
|
247
244
|
except Exception as e:
|
|
248
245
|
# Tracking failure should not break execution
|
|
249
|
-
|
|
250
|
-
f"DEBUG: Exception recording Gemini subprocess: {e}",
|
|
251
|
-
file=sys.stderr,
|
|
252
|
-
)
|
|
246
|
+
logger.warning(f"DEBUG: Exception recording Gemini subprocess: {e}")
|
|
253
247
|
pass
|
|
254
248
|
else:
|
|
255
|
-
|
|
256
|
-
f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
257
|
-
file=sys.stderr,
|
|
249
|
+
logger.warning(
|
|
250
|
+
f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
258
251
|
)
|
|
259
252
|
|
|
260
253
|
# Execute with timeout and stderr redirection
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""Subprocess execution with standardized error handling.
|
|
2
4
|
|
|
3
5
|
Provides consistent error handling for Claude Code CLI invocations.
|
|
4
6
|
"""
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
import logging
|
|
8
9
|
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
class SubprocessRunner:
|
|
13
16
|
"""Execute subprocess commands with error handling."""
|
|
@@ -25,7 +28,7 @@ class SubprocessRunner:
|
|
|
25
28
|
try:
|
|
26
29
|
subprocess.run(cmd, check=False)
|
|
27
30
|
except FileNotFoundError:
|
|
28
|
-
|
|
31
|
+
logger.warning("Error: 'claude' command not found.")
|
|
29
32
|
print(
|
|
30
33
|
"Please install Claude Code CLI: https://code.claude.com",
|
|
31
34
|
file=sys.stderr,
|