htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__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 +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- 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 +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -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/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- 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/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Gemini spawner implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from .base import AIResult, BaseSpawner
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from htmlgraph.sdk import SDK
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GeminiSpawner(BaseSpawner):
|
|
18
|
+
"""Spawner for Google Gemini CLI.
|
|
19
|
+
|
|
20
|
+
Model Selection:
|
|
21
|
+
The `model` parameter defaults to None, which is the RECOMMENDED approach.
|
|
22
|
+
When model=None, the Gemini CLI automatically selects the best available model
|
|
23
|
+
based on the task and current availability.
|
|
24
|
+
|
|
25
|
+
As of Gemini CLI v0.22+, the default models include:
|
|
26
|
+
- gemini-2.5-flash-lite: Fast, efficient model for most tasks
|
|
27
|
+
- gemini-3-flash-preview: Preview of Gemini 3 with enhanced capabilities
|
|
28
|
+
|
|
29
|
+
Explicitly specifying a model is DISCOURAGED because:
|
|
30
|
+
1. Older models (gemini-2.0-flash, gemini-1.5-flash) may fail due to
|
|
31
|
+
"thinking mode" incompatibility in newer CLI versions
|
|
32
|
+
2. Using None automatically benefits from Google's model updates
|
|
33
|
+
3. The CLI handles model selection and fallback logic
|
|
34
|
+
|
|
35
|
+
Supported models (if you must specify):
|
|
36
|
+
- None (recommended): CLI chooses best available model
|
|
37
|
+
- "gemini-2.5-flash-lite": Fast, efficient
|
|
38
|
+
- "gemini-3-flash-preview": Gemini 3 preview (enhanced capabilities)
|
|
39
|
+
- "gemini-2.5-pro": More capable, slower
|
|
40
|
+
|
|
41
|
+
DEPRECATED models (may cause errors):
|
|
42
|
+
- "gemini-2.0-flash": Deprecated, use None instead
|
|
43
|
+
- "gemini-1.5-flash": Deprecated, use None instead
|
|
44
|
+
- "gemini-1.5-pro": Deprecated, use None instead
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def _parse_and_track_events(self, jsonl_output: str, sdk: "SDK") -> list[dict]:
|
|
48
|
+
"""
|
|
49
|
+
Parse Gemini stream-json events and track in HtmlGraph.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
jsonl_output: JSONL output from Gemini CLI
|
|
53
|
+
sdk: HtmlGraph SDK instance for tracking
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Parsed events list
|
|
57
|
+
"""
|
|
58
|
+
events = []
|
|
59
|
+
|
|
60
|
+
for line in jsonl_output.splitlines():
|
|
61
|
+
if not line.strip():
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
event = json.loads(line)
|
|
66
|
+
events.append(event)
|
|
67
|
+
|
|
68
|
+
# Track based on event type
|
|
69
|
+
event_type = event.get("type")
|
|
70
|
+
|
|
71
|
+
if event_type == "tool_use":
|
|
72
|
+
tool_name = event.get("tool_name", "unknown_tool")
|
|
73
|
+
parameters = event.get("parameters", {})
|
|
74
|
+
self._track_activity(
|
|
75
|
+
sdk,
|
|
76
|
+
tool="gemini_tool_call",
|
|
77
|
+
summary=f"Gemini called {tool_name}",
|
|
78
|
+
payload={
|
|
79
|
+
"tool_name": tool_name,
|
|
80
|
+
"parameters": parameters,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
elif event_type == "tool_result":
|
|
85
|
+
status = event.get("status", "unknown")
|
|
86
|
+
success = status == "success"
|
|
87
|
+
tool_id = event.get("tool_id", "unknown")
|
|
88
|
+
self._track_activity(
|
|
89
|
+
sdk,
|
|
90
|
+
tool="gemini_tool_result",
|
|
91
|
+
summary=f"Gemini tool result: {status}",
|
|
92
|
+
success=success,
|
|
93
|
+
payload={"tool_id": tool_id, "status": status},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
elif event_type == "message":
|
|
97
|
+
role = event.get("role")
|
|
98
|
+
if role == "assistant":
|
|
99
|
+
content = event.get("content", "")
|
|
100
|
+
# Truncate for summary
|
|
101
|
+
summary = (
|
|
102
|
+
content[:100] + "..." if len(content) > 100 else content
|
|
103
|
+
)
|
|
104
|
+
self._track_activity(
|
|
105
|
+
sdk,
|
|
106
|
+
tool="gemini_message",
|
|
107
|
+
summary=f"Gemini: {summary}",
|
|
108
|
+
payload={"role": role, "content_length": len(content)},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
elif event_type == "result":
|
|
112
|
+
stats = event.get("stats", {})
|
|
113
|
+
self._track_activity(
|
|
114
|
+
sdk,
|
|
115
|
+
tool="gemini_completion",
|
|
116
|
+
summary="Gemini task completed",
|
|
117
|
+
payload={"stats": stats},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
# Skip malformed lines
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
return events
|
|
125
|
+
|
|
126
|
+
def spawn(
|
|
127
|
+
self,
|
|
128
|
+
prompt: str,
|
|
129
|
+
output_format: str = "stream-json",
|
|
130
|
+
model: str | None = None,
|
|
131
|
+
include_directories: list[str] | None = None,
|
|
132
|
+
track_in_htmlgraph: bool = True,
|
|
133
|
+
timeout: int = 120,
|
|
134
|
+
tracker: Any = None,
|
|
135
|
+
parent_event_id: str | None = None,
|
|
136
|
+
) -> AIResult:
|
|
137
|
+
"""
|
|
138
|
+
Spawn Gemini in headless mode.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
prompt: Task description for Gemini
|
|
142
|
+
output_format: "json" or "stream-json" (enables real-time tracking)
|
|
143
|
+
model: Model selection. Default: None (RECOMMENDED).
|
|
144
|
+
|
|
145
|
+
When model=None (default), the Gemini CLI automatically selects
|
|
146
|
+
the best available model, which includes:
|
|
147
|
+
- gemini-2.5-flash-lite: Fast, efficient model
|
|
148
|
+
- gemini-3-flash-preview: Gemini 3 with enhanced capabilities
|
|
149
|
+
|
|
150
|
+
Using None is STRONGLY RECOMMENDED because:
|
|
151
|
+
1. Automatically benefits from Google's latest models
|
|
152
|
+
2. Avoids deprecation issues with older model names
|
|
153
|
+
3. CLI handles optimal model selection and fallback
|
|
154
|
+
|
|
155
|
+
DEPRECATED models (may cause errors with CLI v0.22+):
|
|
156
|
+
- gemini-2.0-flash, gemini-1.5-flash, gemini-1.5-pro
|
|
157
|
+
|
|
158
|
+
include_directories: Directories to include for context. Default: None
|
|
159
|
+
track_in_htmlgraph: Enable HtmlGraph activity tracking. Default: True
|
|
160
|
+
timeout: Max seconds to wait
|
|
161
|
+
tracker: Optional SpawnerEventTracker for recording subprocess invocation
|
|
162
|
+
parent_event_id: Optional parent event ID for event hierarchy
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
AIResult with response, error, and tracked events if tracking enabled
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
>>> spawner = GeminiSpawner()
|
|
169
|
+
>>> result = spawner.spawn(
|
|
170
|
+
... prompt="Analyze this codebase",
|
|
171
|
+
... # model=None is the default - uses latest Gemini models
|
|
172
|
+
... track_in_htmlgraph=True
|
|
173
|
+
... )
|
|
174
|
+
"""
|
|
175
|
+
# Initialize tracking if enabled
|
|
176
|
+
sdk: SDK | None = None
|
|
177
|
+
tracked_events: list[dict] = []
|
|
178
|
+
if track_in_htmlgraph:
|
|
179
|
+
sdk = self._get_sdk()
|
|
180
|
+
|
|
181
|
+
# Publish live event: spawner starting
|
|
182
|
+
self._publish_live_event(
|
|
183
|
+
"spawner_start",
|
|
184
|
+
"gemini",
|
|
185
|
+
prompt=prompt,
|
|
186
|
+
model=model,
|
|
187
|
+
)
|
|
188
|
+
start_time = time.time()
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
# Build command based on tested pattern from spike spk-4029eef3
|
|
192
|
+
cmd = ["gemini", "-p", prompt, "--output-format", output_format]
|
|
193
|
+
|
|
194
|
+
# Add model option if specified
|
|
195
|
+
if model:
|
|
196
|
+
cmd.extend(["-m", model])
|
|
197
|
+
|
|
198
|
+
# Add include directories if specified
|
|
199
|
+
if include_directories:
|
|
200
|
+
for directory in include_directories:
|
|
201
|
+
cmd.extend(["--include-directories", directory])
|
|
202
|
+
|
|
203
|
+
# CRITICAL: Add --yolo for headless mode (auto-approve all tools)
|
|
204
|
+
cmd.append("--yolo")
|
|
205
|
+
|
|
206
|
+
# Track spawner start if SDK available
|
|
207
|
+
if sdk:
|
|
208
|
+
self._track_activity(
|
|
209
|
+
sdk,
|
|
210
|
+
tool="gemini_spawn_start",
|
|
211
|
+
summary=f"Spawning Gemini: {prompt[:80]}",
|
|
212
|
+
payload={"prompt_length": len(prompt), "model": model},
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Publish live event: executing
|
|
216
|
+
self._publish_live_event(
|
|
217
|
+
"spawner_phase",
|
|
218
|
+
"gemini",
|
|
219
|
+
phase="executing",
|
|
220
|
+
details="Running Gemini CLI",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Record subprocess invocation if tracker is available
|
|
224
|
+
subprocess_event_id = None
|
|
225
|
+
logger.warning(
|
|
226
|
+
f"DEBUG: tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
227
|
+
)
|
|
228
|
+
if tracker and parent_event_id:
|
|
229
|
+
logger.debug("Recording subprocess invocation for Gemini...")
|
|
230
|
+
try:
|
|
231
|
+
subprocess_event = tracker.record_tool_call(
|
|
232
|
+
tool_name="subprocess.gemini",
|
|
233
|
+
tool_input={"cmd": cmd},
|
|
234
|
+
phase_event_id=parent_event_id,
|
|
235
|
+
spawned_agent=model or "gemini-default",
|
|
236
|
+
)
|
|
237
|
+
if subprocess_event:
|
|
238
|
+
subprocess_event_id = subprocess_event.get("event_id")
|
|
239
|
+
logger.warning(
|
|
240
|
+
f"DEBUG: Subprocess event created for Gemini: {subprocess_event_id}"
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
logger.debug("subprocess_event was None")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
# Tracking failure should not break execution
|
|
246
|
+
logger.warning(f"DEBUG: Exception recording Gemini subprocess: {e}")
|
|
247
|
+
pass
|
|
248
|
+
else:
|
|
249
|
+
logger.warning(
|
|
250
|
+
f"DEBUG: Skipping Gemini subprocess tracking - tracker={tracker is not None}, parent_event_id={parent_event_id}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Execute with timeout and stderr redirection
|
|
254
|
+
# Note: Cannot use capture_output with stderr parameter
|
|
255
|
+
result = subprocess.run(
|
|
256
|
+
cmd,
|
|
257
|
+
stdout=subprocess.PIPE,
|
|
258
|
+
stderr=subprocess.DEVNULL, # Redirect stderr to avoid polluting JSON
|
|
259
|
+
text=True,
|
|
260
|
+
timeout=timeout,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Complete subprocess invocation tracking
|
|
264
|
+
if tracker and subprocess_event_id:
|
|
265
|
+
try:
|
|
266
|
+
tracker.complete_tool_call(
|
|
267
|
+
event_id=subprocess_event_id,
|
|
268
|
+
output_summary=result.stdout[:500] if result.stdout else "",
|
|
269
|
+
success=result.returncode == 0,
|
|
270
|
+
)
|
|
271
|
+
except Exception:
|
|
272
|
+
# Tracking failure should not break execution
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
# Publish live event: processing response
|
|
276
|
+
self._publish_live_event(
|
|
277
|
+
"spawner_phase",
|
|
278
|
+
"gemini",
|
|
279
|
+
phase="processing",
|
|
280
|
+
details="Parsing Gemini response",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Check for command execution errors
|
|
284
|
+
if result.returncode != 0:
|
|
285
|
+
duration = time.time() - start_time
|
|
286
|
+
self._publish_live_event(
|
|
287
|
+
"spawner_complete",
|
|
288
|
+
"gemini",
|
|
289
|
+
success=False,
|
|
290
|
+
duration=duration,
|
|
291
|
+
error=f"CLI failed with exit code {result.returncode}",
|
|
292
|
+
)
|
|
293
|
+
return AIResult(
|
|
294
|
+
success=False,
|
|
295
|
+
response="",
|
|
296
|
+
tokens_used=None,
|
|
297
|
+
error=f"Gemini CLI failed with exit code {result.returncode}",
|
|
298
|
+
raw_output=None,
|
|
299
|
+
tracked_events=tracked_events,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Handle stream-json format with real-time tracking
|
|
303
|
+
if output_format == "stream-json" and sdk:
|
|
304
|
+
try:
|
|
305
|
+
tracked_events = self._parse_and_track_events(result.stdout, sdk)
|
|
306
|
+
# Only use stream-json parsing if we got valid events
|
|
307
|
+
if tracked_events:
|
|
308
|
+
# For stream-json, we need to extract response differently
|
|
309
|
+
# Collect all assistant message content, then check result
|
|
310
|
+
response_text = ""
|
|
311
|
+
for event in tracked_events:
|
|
312
|
+
if event.get("type") == "message":
|
|
313
|
+
# Only collect assistant messages
|
|
314
|
+
if event.get("role") == "assistant":
|
|
315
|
+
content = event.get("content", "")
|
|
316
|
+
if content:
|
|
317
|
+
response_text += content
|
|
318
|
+
elif event.get("type") == "result":
|
|
319
|
+
# Result event may have response field (override if present)
|
|
320
|
+
if "response" in event and event["response"]:
|
|
321
|
+
response_text = event["response"]
|
|
322
|
+
# Don't break - we've already collected messages
|
|
323
|
+
|
|
324
|
+
# Token usage from stats in result event
|
|
325
|
+
tokens = None
|
|
326
|
+
for event in tracked_events:
|
|
327
|
+
if event.get("type") == "result":
|
|
328
|
+
stats = event.get("stats", {})
|
|
329
|
+
if stats and "models" in stats:
|
|
330
|
+
total_tokens = 0
|
|
331
|
+
for model_stats in stats["models"].values():
|
|
332
|
+
model_tokens = model_stats.get(
|
|
333
|
+
"tokens", {}
|
|
334
|
+
).get("total", 0)
|
|
335
|
+
total_tokens += model_tokens
|
|
336
|
+
tokens = total_tokens if total_tokens > 0 else None
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
# Publish live event: complete
|
|
340
|
+
duration = time.time() - start_time
|
|
341
|
+
self._publish_live_event(
|
|
342
|
+
"spawner_complete",
|
|
343
|
+
"gemini",
|
|
344
|
+
success=True,
|
|
345
|
+
duration=duration,
|
|
346
|
+
response=response_text,
|
|
347
|
+
tokens=tokens,
|
|
348
|
+
)
|
|
349
|
+
return AIResult(
|
|
350
|
+
success=True,
|
|
351
|
+
response=response_text,
|
|
352
|
+
tokens_used=tokens,
|
|
353
|
+
error=None,
|
|
354
|
+
raw_output={"events": tracked_events},
|
|
355
|
+
tracked_events=tracked_events,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
except Exception:
|
|
359
|
+
# Fall back to regular JSON parsing if tracking fails
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
# Parse JSON response (for json format or fallback)
|
|
363
|
+
try:
|
|
364
|
+
output = json.loads(result.stdout)
|
|
365
|
+
except json.JSONDecodeError as e:
|
|
366
|
+
duration = time.time() - start_time
|
|
367
|
+
self._publish_live_event(
|
|
368
|
+
"spawner_complete",
|
|
369
|
+
"gemini",
|
|
370
|
+
success=False,
|
|
371
|
+
duration=duration,
|
|
372
|
+
error=f"Failed to parse JSON: {e}",
|
|
373
|
+
)
|
|
374
|
+
return AIResult(
|
|
375
|
+
success=False,
|
|
376
|
+
response="",
|
|
377
|
+
tokens_used=None,
|
|
378
|
+
error=f"Failed to parse JSON output: {e}",
|
|
379
|
+
raw_output={"stdout": result.stdout},
|
|
380
|
+
tracked_events=tracked_events,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Extract response and token usage from parsed output
|
|
384
|
+
# Response is at top level in JSON output
|
|
385
|
+
response_text = output.get("response", "")
|
|
386
|
+
|
|
387
|
+
# Token usage is in stats.models (sum across all models)
|
|
388
|
+
tokens = None
|
|
389
|
+
stats = output.get("stats", {})
|
|
390
|
+
if stats and "models" in stats:
|
|
391
|
+
total_tokens = 0
|
|
392
|
+
for model_stats in stats["models"].values():
|
|
393
|
+
model_tokens = model_stats.get("tokens", {}).get("total", 0)
|
|
394
|
+
total_tokens += model_tokens
|
|
395
|
+
tokens = total_tokens if total_tokens > 0 else None
|
|
396
|
+
|
|
397
|
+
# Publish live event: complete
|
|
398
|
+
duration = time.time() - start_time
|
|
399
|
+
self._publish_live_event(
|
|
400
|
+
"spawner_complete",
|
|
401
|
+
"gemini",
|
|
402
|
+
success=True,
|
|
403
|
+
duration=duration,
|
|
404
|
+
response=response_text,
|
|
405
|
+
tokens=tokens,
|
|
406
|
+
)
|
|
407
|
+
return AIResult(
|
|
408
|
+
success=True,
|
|
409
|
+
response=response_text,
|
|
410
|
+
tokens_used=tokens,
|
|
411
|
+
error=None,
|
|
412
|
+
raw_output=output,
|
|
413
|
+
tracked_events=tracked_events,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
except subprocess.TimeoutExpired as e:
|
|
417
|
+
duration = time.time() - start_time
|
|
418
|
+
self._publish_live_event(
|
|
419
|
+
"spawner_complete",
|
|
420
|
+
"gemini",
|
|
421
|
+
success=False,
|
|
422
|
+
duration=duration,
|
|
423
|
+
error=f"Timed out after {timeout} seconds",
|
|
424
|
+
)
|
|
425
|
+
return AIResult(
|
|
426
|
+
success=False,
|
|
427
|
+
response="",
|
|
428
|
+
tokens_used=None,
|
|
429
|
+
error=f"Gemini CLI timed out after {timeout} seconds",
|
|
430
|
+
raw_output={
|
|
431
|
+
"partial_stdout": e.stdout.decode() if e.stdout else None,
|
|
432
|
+
"partial_stderr": e.stderr.decode() if e.stderr else None,
|
|
433
|
+
}
|
|
434
|
+
if e.stdout or e.stderr
|
|
435
|
+
else None,
|
|
436
|
+
tracked_events=tracked_events,
|
|
437
|
+
)
|
|
438
|
+
except FileNotFoundError:
|
|
439
|
+
duration = time.time() - start_time
|
|
440
|
+
self._publish_live_event(
|
|
441
|
+
"spawner_complete",
|
|
442
|
+
"gemini",
|
|
443
|
+
success=False,
|
|
444
|
+
duration=duration,
|
|
445
|
+
error="CLI not found",
|
|
446
|
+
)
|
|
447
|
+
return AIResult(
|
|
448
|
+
success=False,
|
|
449
|
+
response="",
|
|
450
|
+
tokens_used=None,
|
|
451
|
+
error="Gemini CLI not found. Ensure 'gemini' is installed and in PATH.",
|
|
452
|
+
raw_output=None,
|
|
453
|
+
tracked_events=tracked_events,
|
|
454
|
+
)
|
|
455
|
+
except Exception as e:
|
|
456
|
+
duration = time.time() - start_time
|
|
457
|
+
self._publish_live_event(
|
|
458
|
+
"spawner_complete",
|
|
459
|
+
"gemini",
|
|
460
|
+
success=False,
|
|
461
|
+
duration=duration,
|
|
462
|
+
error=str(e),
|
|
463
|
+
)
|
|
464
|
+
return AIResult(
|
|
465
|
+
success=False,
|
|
466
|
+
response="",
|
|
467
|
+
tokens_used=None,
|
|
468
|
+
error=f"Unexpected error: {type(e).__name__}: {e}",
|
|
469
|
+
raw_output=None,
|
|
470
|
+
tracked_events=tracked_events,
|
|
471
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""Subprocess execution with standardized error handling.
|
|
4
|
+
|
|
5
|
+
Provides consistent error handling for Claude Code CLI invocations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SubprocessRunner:
|
|
16
|
+
"""Execute subprocess commands with error handling."""
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def run_claude_command(cmd: list[str]) -> None:
|
|
20
|
+
"""Execute Claude Code CLI command with error handling.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
cmd: Command list (e.g., ["claude", "--resume"])
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
SystemExit: If 'claude' command not found or other error
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
subprocess.run(cmd, check=False)
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
logger.warning("Error: 'claude' command not found.")
|
|
32
|
+
print(
|
|
33
|
+
"Please install Claude Code CLI: https://code.claude.com",
|
|
34
|
+
file=sys.stderr,
|
|
35
|
+
)
|
|
36
|
+
sys.exit(1)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger(__name__)
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
Orchestration helpers for reliable parallel task coordination.
|
|
3
7
|
|
|
@@ -7,9 +11,13 @@ Provides Task ID pattern for retrieving results from parallel delegations.
|
|
|
7
11
|
import time
|
|
8
12
|
import uuid
|
|
9
13
|
from datetime import datetime, timedelta
|
|
10
|
-
from typing import Any
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from htmlgraph.sdk import SDK
|
|
18
|
+
else:
|
|
19
|
+
# Avoid circular import during module initialization
|
|
20
|
+
SDK = None
|
|
13
21
|
|
|
14
22
|
|
|
15
23
|
def generate_task_id() -> str:
|
|
@@ -72,7 +80,7 @@ Provide detailed findings in your response.
|
|
|
72
80
|
|
|
73
81
|
|
|
74
82
|
def get_results_by_task_id(
|
|
75
|
-
sdk: SDK,
|
|
83
|
+
sdk: "SDK",
|
|
76
84
|
task_id: str,
|
|
77
85
|
timeout: int = 60,
|
|
78
86
|
poll_interval: int = 2,
|
|
@@ -138,7 +146,7 @@ def get_results_by_task_id(
|
|
|
138
146
|
|
|
139
147
|
|
|
140
148
|
def parallel_delegate(
|
|
141
|
-
sdk: SDK,
|
|
149
|
+
sdk: "SDK",
|
|
142
150
|
tasks: list[dict[str, str]],
|
|
143
151
|
timeout: int = 120,
|
|
144
152
|
) -> dict[str, dict[str, Any]]:
|
|
@@ -161,7 +169,7 @@ def parallel_delegate(
|
|
|
161
169
|
])
|
|
162
170
|
|
|
163
171
|
for task_id, result in results.items():
|
|
164
|
-
|
|
172
|
+
logger.info(f"{task_id}: {result['findings']}")
|
|
165
173
|
"""
|
|
166
174
|
# Generate task IDs and enhanced prompts
|
|
167
175
|
task_mapping = {}
|
|
@@ -189,7 +197,7 @@ def parallel_delegate(
|
|
|
189
197
|
|
|
190
198
|
|
|
191
199
|
def save_task_results(
|
|
192
|
-
sdk: SDK,
|
|
200
|
+
sdk: "SDK",
|
|
193
201
|
task_id: str,
|
|
194
202
|
description: str,
|
|
195
203
|
results: str,
|
|
@@ -263,7 +271,7 @@ def save_task_results(
|
|
|
263
271
|
|
|
264
272
|
|
|
265
273
|
def validate_and_save(
|
|
266
|
-
sdk: SDK,
|
|
274
|
+
sdk: "SDK",
|
|
267
275
|
task_id: str,
|
|
268
276
|
description: str,
|
|
269
277
|
results: str,
|
|
@@ -303,7 +311,7 @@ def validate_and_save(
|
|
|
303
311
|
)
|
|
304
312
|
|
|
305
313
|
if outcome["validated"]:
|
|
306
|
-
|
|
314
|
+
logger.info(f"✅ Saved to spike: {outcome['spike_id']}")
|
|
307
315
|
"""
|
|
308
316
|
validated = True
|
|
309
317
|
validation_results = None
|