htmlgraph 0.24.2__py3-none-any.whl → 0.26.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htmlgraph/__init__.py +20 -1
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/analytics/cross_session.py +4 -3
- htmlgraph/analytics/work_type.py +52 -16
- htmlgraph/analytics_index.py +51 -19
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/main.py +2263 -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 +812 -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 +1020 -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 +509 -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 +163 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/builders/base.py +55 -1
- htmlgraph/builders/bug.py +17 -2
- htmlgraph/builders/chore.py +17 -2
- htmlgraph/builders/epic.py +17 -2
- htmlgraph/builders/feature.py +25 -2
- htmlgraph/builders/phase.py +17 -2
- htmlgraph/builders/spike.py +27 -2
- htmlgraph/builders/track.py +14 -0
- htmlgraph/cigs/__init__.py +4 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cli.py +1427 -401
- 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 +21 -0
- htmlgraph/collections/session.py +189 -0
- htmlgraph/collections/spike.py +7 -1
- htmlgraph/collections/task_delegation.py +236 -0
- htmlgraph/collections/traces.py +482 -0
- htmlgraph/config.py +113 -0
- htmlgraph/converter.py +41 -0
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +3356 -492
- htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
- 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 +1584 -0
- 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 +710 -0
- htmlgraph/docs/README.md +533 -0
- htmlgraph/docs/version_check.py +3 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +2 -0
- 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 +2 -2
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +318 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +496 -79
- htmlgraph/hooks/orchestrator.py +6 -4
- htmlgraph/hooks/orchestrator_reflector.py +4 -4
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/pretooluse.py +473 -6
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +637 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_stop.py +309 -0
- htmlgraph/hooks/task_enforcer.py +39 -0
- htmlgraph/hooks/validator.py +15 -11
- htmlgraph/models.py +111 -15
- htmlgraph/operations/fastapi_server.py +230 -0
- htmlgraph/orchestration/headless_spawner.py +344 -29
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/repo_hash.py +511 -0
- htmlgraph/sdk.py +348 -10
- htmlgraph/server.py +194 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +131 -1
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/system_prompts.py +449 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +19 -0
- htmlgraph/validation.py +115 -0
- htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command implementations."""
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.prompt import Prompt
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from htmlgraph.cli_framework import BaseCommand, CommandError, CommandResult
|
|
11
|
+
|
|
12
|
+
_console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FeatureCreateCommand(BaseCommand):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
collection: str,
|
|
20
|
+
title: str,
|
|
21
|
+
description: str,
|
|
22
|
+
priority: str,
|
|
23
|
+
steps: Iterable[str] | None,
|
|
24
|
+
track_id: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
self.collection = collection
|
|
28
|
+
self.title = title
|
|
29
|
+
self.description = description
|
|
30
|
+
self.priority = priority
|
|
31
|
+
self.steps = list(steps) if steps else []
|
|
32
|
+
self.track_id = track_id
|
|
33
|
+
|
|
34
|
+
def execute(self) -> CommandResult:
|
|
35
|
+
sdk = self.get_sdk()
|
|
36
|
+
|
|
37
|
+
# Determine track_id for feature creation
|
|
38
|
+
track_id = self.track_id
|
|
39
|
+
|
|
40
|
+
# Only enforce track selection for main features collection
|
|
41
|
+
if self.collection == "features":
|
|
42
|
+
if not track_id:
|
|
43
|
+
# Get available tracks
|
|
44
|
+
try:
|
|
45
|
+
tracks = sdk.tracks.all()
|
|
46
|
+
if not tracks:
|
|
47
|
+
raise CommandError(
|
|
48
|
+
"No tracks found. Create a track first:\n"
|
|
49
|
+
" uv run htmlgraph track new 'Track Title'"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
if len(tracks) == 1:
|
|
53
|
+
# Auto-select if only one track exists
|
|
54
|
+
track_id = tracks[0].id
|
|
55
|
+
_console.print(
|
|
56
|
+
f"[dim]Auto-selected track: {tracks[0].title}[/dim]"
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
# Interactive selection
|
|
60
|
+
_console.print("[bold]Available Tracks:[/bold]")
|
|
61
|
+
for i, track in enumerate(tracks, 1):
|
|
62
|
+
_console.print(f" {i}. {track.title} ({track.id})")
|
|
63
|
+
|
|
64
|
+
selection = Prompt.ask(
|
|
65
|
+
"Select track",
|
|
66
|
+
choices=[str(i) for i in range(1, len(tracks) + 1)],
|
|
67
|
+
)
|
|
68
|
+
track_id = tracks[int(selection) - 1].id
|
|
69
|
+
except Exception as e:
|
|
70
|
+
raise CommandError(f"Failed to get available tracks: {e}")
|
|
71
|
+
|
|
72
|
+
builder = sdk.features.create(
|
|
73
|
+
title=self.title,
|
|
74
|
+
description=self.description,
|
|
75
|
+
priority=self.priority,
|
|
76
|
+
)
|
|
77
|
+
if self.steps:
|
|
78
|
+
builder.add_steps(self.steps)
|
|
79
|
+
if track_id:
|
|
80
|
+
builder.set_track(track_id)
|
|
81
|
+
node = builder.save()
|
|
82
|
+
else:
|
|
83
|
+
node = sdk.session_manager.create_feature(
|
|
84
|
+
title=self.title,
|
|
85
|
+
collection=self.collection,
|
|
86
|
+
description=self.description,
|
|
87
|
+
priority=self.priority,
|
|
88
|
+
steps=self.steps,
|
|
89
|
+
agent=self.agent,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Format output with Rich
|
|
93
|
+
table = Table(show_header=False, box=None)
|
|
94
|
+
table.add_column(style="bold cyan")
|
|
95
|
+
table.add_column()
|
|
96
|
+
|
|
97
|
+
table.add_row("Created:", f"[green]{node.id}[/green]")
|
|
98
|
+
table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
|
|
99
|
+
table.add_row("Status:", f"[blue]{node.status}[/blue]")
|
|
100
|
+
if node.track_id:
|
|
101
|
+
table.add_row("Track:", f"[cyan]{node.track_id}[/cyan]")
|
|
102
|
+
table.add_row(
|
|
103
|
+
"Path:", f"[dim]{self.graph_dir}/{self.collection}/{node.id}.html[/dim]"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Format as Rich panel for text output
|
|
107
|
+
text = [
|
|
108
|
+
f"Created: {node.id}",
|
|
109
|
+
f" Title: {node.title}",
|
|
110
|
+
f" Status: {node.status}",
|
|
111
|
+
]
|
|
112
|
+
if node.track_id:
|
|
113
|
+
text.append(f" Track: {node.track_id}")
|
|
114
|
+
text.append(f" Path: {self.graph_dir}/{self.collection}/{node.id}.html")
|
|
115
|
+
|
|
116
|
+
return CommandResult(data=node, text=text)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class FeatureStartCommand(BaseCommand):
|
|
120
|
+
def __init__(self, *, collection: str, feature_id: str) -> None:
|
|
121
|
+
super().__init__()
|
|
122
|
+
self.collection = collection
|
|
123
|
+
self.feature_id = feature_id
|
|
124
|
+
|
|
125
|
+
def execute(self) -> CommandResult:
|
|
126
|
+
sdk = self.get_sdk()
|
|
127
|
+
collection = getattr(sdk, self.collection, None)
|
|
128
|
+
|
|
129
|
+
if not collection:
|
|
130
|
+
raise CommandError(f"Collection '{self.collection}' not found in SDK.")
|
|
131
|
+
|
|
132
|
+
node = collection.start(self.feature_id)
|
|
133
|
+
if node is None:
|
|
134
|
+
raise CommandError(
|
|
135
|
+
f"Feature '{self.feature_id}' not found in {self.collection}."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
status = sdk.session_manager.get_status()
|
|
139
|
+
|
|
140
|
+
# Format output with Rich
|
|
141
|
+
table = Table(show_header=False, box=None)
|
|
142
|
+
table.add_column(style="bold cyan")
|
|
143
|
+
table.add_column()
|
|
144
|
+
|
|
145
|
+
table.add_row("Started:", f"[green]{node.id}[/green]")
|
|
146
|
+
table.add_row("Title:", f"[yellow]{node.title}[/yellow]")
|
|
147
|
+
table.add_row("Status:", f"[blue]{node.status}[/blue]")
|
|
148
|
+
wip_color = "red" if status["wip_count"] >= status["wip_limit"] else "green"
|
|
149
|
+
table.add_row(
|
|
150
|
+
"WIP:",
|
|
151
|
+
f"[{wip_color}]{status['wip_count']}/{status['wip_limit']}[/{wip_color}]",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
text = [
|
|
155
|
+
f"Started: {node.id}",
|
|
156
|
+
f" Title: {node.title}",
|
|
157
|
+
f" Status: {node.status}",
|
|
158
|
+
f" WIP: {status['wip_count']}/{status['wip_limit']}",
|
|
159
|
+
]
|
|
160
|
+
return CommandResult(data=node, text=text)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class FeatureCompleteCommand(BaseCommand):
|
|
164
|
+
def __init__(self, *, collection: str, feature_id: str) -> None:
|
|
165
|
+
super().__init__()
|
|
166
|
+
self.collection = collection
|
|
167
|
+
self.feature_id = feature_id
|
|
168
|
+
|
|
169
|
+
def execute(self) -> CommandResult:
|
|
170
|
+
sdk = self.get_sdk()
|
|
171
|
+
collection = getattr(sdk, self.collection, None)
|
|
172
|
+
|
|
173
|
+
if not collection:
|
|
174
|
+
raise CommandError(f"Collection '{self.collection}' not found in SDK.")
|
|
175
|
+
|
|
176
|
+
node = collection.complete(self.feature_id)
|
|
177
|
+
if node is None:
|
|
178
|
+
raise CommandError(
|
|
179
|
+
f"Feature '{self.feature_id}' not found in {self.collection}."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Format output with Rich
|
|
183
|
+
panel = Panel(
|
|
184
|
+
f"[bold green]✓ Completed[/bold green]\n"
|
|
185
|
+
f"[cyan]{node.id}[/cyan]\n"
|
|
186
|
+
f"[yellow]{node.title}[/yellow]",
|
|
187
|
+
border_style="green",
|
|
188
|
+
)
|
|
189
|
+
_console.print(panel)
|
|
190
|
+
|
|
191
|
+
text = [
|
|
192
|
+
f"Completed: {node.id}",
|
|
193
|
+
f" Title: {node.title}",
|
|
194
|
+
]
|
|
195
|
+
return CommandResult(data=node, text=text)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
9
|
+
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from htmlgraph.sdk import SDK
|
|
14
|
+
|
|
15
|
+
_console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CommandError(Exception):
|
|
19
|
+
"""User-facing CLI error with an exit code."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, message: str, exit_code: int = 1) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.exit_code = exit_code
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CommandResult:
|
|
28
|
+
data: Any = None
|
|
29
|
+
text: str | Iterable[str] | None = None
|
|
30
|
+
json_data: Any | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Formatter(Protocol):
|
|
34
|
+
def output(self, result: CommandResult) -> None: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _serialize_json(value: Any) -> Any:
|
|
38
|
+
if value is None:
|
|
39
|
+
return None
|
|
40
|
+
if isinstance(value, (datetime, date)):
|
|
41
|
+
return value.isoformat()
|
|
42
|
+
if hasattr(value, "model_dump") and callable(getattr(value, "model_dump")):
|
|
43
|
+
return _serialize_json(value.model_dump())
|
|
44
|
+
if hasattr(value, "to_dict") and callable(getattr(value, "to_dict")):
|
|
45
|
+
return _serialize_json(value.to_dict())
|
|
46
|
+
if isinstance(value, dict):
|
|
47
|
+
return {key: _serialize_json(val) for key, val in value.items()}
|
|
48
|
+
if isinstance(value, (list, tuple, set)):
|
|
49
|
+
return [_serialize_json(item) for item in value]
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class JsonFormatter:
|
|
54
|
+
def output(self, result: CommandResult) -> None:
|
|
55
|
+
payload = result.json_data if result.json_data is not None else result.data
|
|
56
|
+
_console.print(json.dumps(_serialize_json(payload), indent=2))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TextFormatter:
|
|
60
|
+
def output(self, result: CommandResult) -> None:
|
|
61
|
+
if result.text is None:
|
|
62
|
+
if result.data is not None:
|
|
63
|
+
_console.print(result.data)
|
|
64
|
+
return
|
|
65
|
+
if isinstance(result.text, str):
|
|
66
|
+
_console.print(result.text)
|
|
67
|
+
return
|
|
68
|
+
_console.print("\n".join(str(line) for line in result.text))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_formatter(format_name: str) -> Formatter:
|
|
72
|
+
if format_name == "json":
|
|
73
|
+
return JsonFormatter()
|
|
74
|
+
if format_name in ("text", "plain", ""):
|
|
75
|
+
return TextFormatter()
|
|
76
|
+
raise CommandError(f"Unknown output format '{format_name}'")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BaseCommand:
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
self.graph_dir: str | None = None
|
|
82
|
+
self.agent: str | None = None
|
|
83
|
+
self._sdk: SDK | None = None
|
|
84
|
+
|
|
85
|
+
def validate(self) -> None:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
def execute(self) -> CommandResult:
|
|
89
|
+
raise NotImplementedError
|
|
90
|
+
|
|
91
|
+
def get_sdk(self) -> Any:
|
|
92
|
+
if self.graph_dir is None:
|
|
93
|
+
raise CommandError("Missing graph directory for command execution.")
|
|
94
|
+
if self._sdk is None:
|
|
95
|
+
from htmlgraph.sdk import SDK
|
|
96
|
+
|
|
97
|
+
self._sdk = SDK(directory=self.graph_dir, agent=self.agent)
|
|
98
|
+
return self._sdk
|
|
99
|
+
|
|
100
|
+
def run(self, *, graph_dir: str, agent: str | None, output_format: str) -> None:
|
|
101
|
+
self.graph_dir = graph_dir
|
|
102
|
+
self.agent = agent
|
|
103
|
+
try:
|
|
104
|
+
self.validate()
|
|
105
|
+
result = self.execute()
|
|
106
|
+
formatter = get_formatter(output_format)
|
|
107
|
+
formatter.output(result)
|
|
108
|
+
except CommandError as exc:
|
|
109
|
+
error_console = Console(file=sys.stderr)
|
|
110
|
+
error_console.print(f"[red]Error: {exc}[/red]")
|
|
111
|
+
sys.exit(exc.exit_code)
|
|
112
|
+
except ValueError as exc:
|
|
113
|
+
error_console = Console(file=sys.stderr)
|
|
114
|
+
error_console.print(f"[red]Error: {exc}[/red]")
|
|
115
|
+
sys.exit(1)
|
|
@@ -15,6 +15,7 @@ from htmlgraph.collections.metric import MetricCollection
|
|
|
15
15
|
from htmlgraph.collections.pattern import PatternCollection
|
|
16
16
|
from htmlgraph.collections.phase import PhaseCollection
|
|
17
17
|
from htmlgraph.collections.spike import SpikeCollection
|
|
18
|
+
from htmlgraph.collections.task_delegation import TaskDelegationCollection
|
|
18
19
|
from htmlgraph.collections.todo import TodoCollection
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
@@ -29,4 +30,5 @@ __all__ = [
|
|
|
29
30
|
"InsightCollection",
|
|
30
31
|
"MetricCollection",
|
|
31
32
|
"TodoCollection",
|
|
33
|
+
"TaskDelegationCollection",
|
|
32
34
|
]
|
htmlgraph/collections/base.py
CHANGED
|
@@ -525,6 +525,27 @@ class BaseCollection(Generic[CollectionT]):
|
|
|
525
525
|
node.updated = datetime.now()
|
|
526
526
|
graph.update(node)
|
|
527
527
|
results["success_count"] += 1
|
|
528
|
+
|
|
529
|
+
# Log completion event to SQLite
|
|
530
|
+
try:
|
|
531
|
+
self._sdk._log_event(
|
|
532
|
+
event_type="tool_call",
|
|
533
|
+
tool_name="SDK.mark_done",
|
|
534
|
+
input_summary=f"Mark {self._node_type} done: {node_id}",
|
|
535
|
+
output_summary=f"Marked {node_id} as done",
|
|
536
|
+
context={
|
|
537
|
+
"collection": self._collection_name,
|
|
538
|
+
"node_id": node_id,
|
|
539
|
+
"node_type": self._node_type,
|
|
540
|
+
"title": node.title,
|
|
541
|
+
},
|
|
542
|
+
cost_tokens=25,
|
|
543
|
+
)
|
|
544
|
+
except Exception as e:
|
|
545
|
+
import logging
|
|
546
|
+
|
|
547
|
+
logging.debug(f"Event logging failed for mark_done: {e}")
|
|
548
|
+
|
|
528
549
|
except Exception as e:
|
|
529
550
|
results["failed_ids"].append(node_id)
|
|
530
551
|
results["warnings"].append(f"Failed to mark {node_id}: {str(e)}")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SessionCollection - Session state and management interface.
|
|
3
|
+
|
|
4
|
+
Provides methods to:
|
|
5
|
+
- Get current session state automatically
|
|
6
|
+
- Set up environment variables automatically
|
|
7
|
+
- Track session metadata
|
|
8
|
+
- Detect post-compact sessions
|
|
9
|
+
|
|
10
|
+
Integration with SessionStart hook:
|
|
11
|
+
sdk = SDK()
|
|
12
|
+
state = sdk.sessions.get_current_state()
|
|
13
|
+
sdk.sessions.setup_environment_variables(state)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from htmlgraph.collections.base import BaseCollection
|
|
21
|
+
from htmlgraph.session_state import SessionState, SessionStateManager
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from htmlgraph.sdk import SDK
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SessionCollection(BaseCollection):
|
|
28
|
+
"""
|
|
29
|
+
Collection interface for session state management.
|
|
30
|
+
|
|
31
|
+
Extends BaseCollection with session-specific state management operations.
|
|
32
|
+
|
|
33
|
+
Provides:
|
|
34
|
+
- Automatic session state detection (post-compact, delegation status)
|
|
35
|
+
- Environment variable setup (CLAUDE_SESSION_ID, CLAUDE_DELEGATION_ENABLED, etc.)
|
|
36
|
+
- Session metadata recording and retrieval
|
|
37
|
+
- Compact detection
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> sdk = SDK(agent="claude")
|
|
41
|
+
>>> state = sdk.sessions.get_current_state()
|
|
42
|
+
>>> sdk.sessions.setup_environment_variables(state)
|
|
43
|
+
# All environment variables automatically set
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
_collection_name = "sessions"
|
|
47
|
+
_node_type = "session"
|
|
48
|
+
|
|
49
|
+
def __init__(self, sdk: SDK):
|
|
50
|
+
"""
|
|
51
|
+
Initialize SessionCollection.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
sdk: Parent SDK instance
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(sdk, "sessions", "session")
|
|
57
|
+
self._state_manager = SessionStateManager(sdk._directory)
|
|
58
|
+
|
|
59
|
+
def get_current_state(self) -> SessionState:
|
|
60
|
+
"""
|
|
61
|
+
Get current session state with automatic detection.
|
|
62
|
+
|
|
63
|
+
Automatically detects:
|
|
64
|
+
- Current session ID
|
|
65
|
+
- Session source (startup, resume, compact, clear)
|
|
66
|
+
- Post-compact status
|
|
67
|
+
- Delegation enable/disable
|
|
68
|
+
- Session validity
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
SessionState dict with:
|
|
72
|
+
- session_id: Current session identifier
|
|
73
|
+
- session_source: "startup", "resume", "compact", "clear"
|
|
74
|
+
- is_post_compact: True if this is post-compact session
|
|
75
|
+
- previous_session_id: Previous session ID if available
|
|
76
|
+
- delegation_enabled: Should delegation be active
|
|
77
|
+
- prompt_injected: Was orchestrator prompt injected
|
|
78
|
+
- session_valid: Is session valid for tracking
|
|
79
|
+
- timestamp: Current UTC timestamp
|
|
80
|
+
- compact_metadata: Compact detection details
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> sdk = SDK()
|
|
84
|
+
>>> state = sdk.sessions.get_current_state()
|
|
85
|
+
>>> print(f"Session: {state['session_id']}")
|
|
86
|
+
>>> print(f"Post-compact: {state['is_post_compact']}")
|
|
87
|
+
>>> print(f"Delegation enabled: {state['delegation_enabled']}")
|
|
88
|
+
"""
|
|
89
|
+
return self._state_manager.get_current_state()
|
|
90
|
+
|
|
91
|
+
def setup_environment_variables(
|
|
92
|
+
self,
|
|
93
|
+
session_state: SessionState | None = None,
|
|
94
|
+
auto_detect_compact: bool = True,
|
|
95
|
+
) -> dict[str, str]:
|
|
96
|
+
"""
|
|
97
|
+
Automatically set up environment variables for session state.
|
|
98
|
+
|
|
99
|
+
Sets up environment variables that persist across context boundaries:
|
|
100
|
+
- CLAUDE_SESSION_ID: Current session identifier
|
|
101
|
+
- CLAUDE_SESSION_SOURCE: "startup|resume|compact|clear"
|
|
102
|
+
- CLAUDE_SESSION_COMPACTED: "true|false"
|
|
103
|
+
- CLAUDE_DELEGATION_ENABLED: "true|false"
|
|
104
|
+
- CLAUDE_PREVIOUS_SESSION_ID: Previous session ID
|
|
105
|
+
- CLAUDE_ORCHESTRATOR_ACTIVE: "true|false"
|
|
106
|
+
- CLAUDE_PROMPT_PERSISTENCE_VERSION: "1.0"
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
session_state: Session state dict (auto-detected if not provided)
|
|
110
|
+
auto_detect_compact: Whether to auto-detect post-compact state
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dict of environment variables that were set
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> sdk = SDK()
|
|
117
|
+
>>> state = sdk.sessions.get_current_state()
|
|
118
|
+
>>> env_vars = sdk.sessions.setup_environment_variables(state)
|
|
119
|
+
>>> print(f"CLAUDE_SESSION_ID: {env_vars['CLAUDE_SESSION_ID']}")
|
|
120
|
+
>>> print(f"CLAUDE_DELEGATION_ENABLED: {env_vars['CLAUDE_DELEGATION_ENABLED']}")
|
|
121
|
+
"""
|
|
122
|
+
return self._state_manager.setup_environment_variables(
|
|
123
|
+
session_state=session_state, auto_detect_compact=auto_detect_compact
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def record_state(
|
|
127
|
+
self,
|
|
128
|
+
session_id: str,
|
|
129
|
+
source: str,
|
|
130
|
+
is_post_compact: bool,
|
|
131
|
+
delegation_enabled: bool,
|
|
132
|
+
environment_vars: dict[str, str] | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Store session state metadata for future reference.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
session_id: Current session ID
|
|
139
|
+
source: Session source ("startup", "resume", "compact", "clear")
|
|
140
|
+
is_post_compact: Whether this is post-compact
|
|
141
|
+
delegation_enabled: Whether delegation is enabled
|
|
142
|
+
environment_vars: Environment variables that were set
|
|
143
|
+
|
|
144
|
+
Example:
|
|
145
|
+
>>> sdk = SDK()
|
|
146
|
+
>>> sdk.sessions.record_state(
|
|
147
|
+
... session_id="sess-123",
|
|
148
|
+
... source="compact",
|
|
149
|
+
... is_post_compact=True,
|
|
150
|
+
... delegation_enabled=True
|
|
151
|
+
... )
|
|
152
|
+
"""
|
|
153
|
+
self._state_manager.record_state(
|
|
154
|
+
session_id=session_id,
|
|
155
|
+
source=source,
|
|
156
|
+
is_post_compact=is_post_compact,
|
|
157
|
+
delegation_enabled=delegation_enabled,
|
|
158
|
+
environment_vars=environment_vars,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def detect_compact_automatically(self) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Auto-detect if this is post-compact by comparing session IDs.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if this is a post-compact session
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
>>> sdk = SDK()
|
|
170
|
+
>>> if sdk.sessions.detect_compact_automatically():
|
|
171
|
+
... print("This is a post-compact session")
|
|
172
|
+
"""
|
|
173
|
+
return self._state_manager.detect_compact_automatically()
|
|
174
|
+
|
|
175
|
+
def get_state_manager(self) -> SessionStateManager:
|
|
176
|
+
"""
|
|
177
|
+
Get the underlying SessionStateManager.
|
|
178
|
+
|
|
179
|
+
Use this for advanced session state operations.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
SessionStateManager instance
|
|
183
|
+
|
|
184
|
+
Example:
|
|
185
|
+
>>> sdk = SDK()
|
|
186
|
+
>>> manager = sdk.sessions.get_state_manager()
|
|
187
|
+
>>> state = manager.get_current_state()
|
|
188
|
+
"""
|
|
189
|
+
return self._state_manager
|
htmlgraph/collections/spike.py
CHANGED
|
@@ -82,8 +82,14 @@ class SpikeCollection(BaseCollection["SpikeCollection"]):
|
|
|
82
82
|
all_spikes = self.all()
|
|
83
83
|
|
|
84
84
|
# Filter by agent if specified
|
|
85
|
+
# Check both agent_assigned and model_name fields
|
|
85
86
|
if agent:
|
|
86
|
-
all_spikes = [
|
|
87
|
+
all_spikes = [
|
|
88
|
+
s
|
|
89
|
+
for s in all_spikes
|
|
90
|
+
if (s.agent_assigned and agent.lower() in s.agent_assigned.lower())
|
|
91
|
+
or (s.model_name and agent.lower() in s.model_name.lower())
|
|
92
|
+
]
|
|
87
93
|
|
|
88
94
|
# Normalize to UTC for comparison
|
|
89
95
|
def to_comparable(dt: datetime) -> datetime:
|