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.
Files changed (112) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2263 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +794 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +1020 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3356 -492
  51. htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1584 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  68. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  69. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  70. htmlgraph/hooks/__init__.py +8 -0
  71. htmlgraph/hooks/bootstrap.py +169 -0
  72. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  73. htmlgraph/hooks/concurrent_sessions.py +208 -0
  74. htmlgraph/hooks/context.py +318 -0
  75. htmlgraph/hooks/drift_handler.py +525 -0
  76. htmlgraph/hooks/event_tracker.py +496 -79
  77. htmlgraph/hooks/orchestrator.py +6 -4
  78. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  79. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  80. htmlgraph/hooks/pretooluse.py +473 -6
  81. htmlgraph/hooks/prompt_analyzer.py +637 -0
  82. htmlgraph/hooks/session_handler.py +637 -0
  83. htmlgraph/hooks/state_manager.py +504 -0
  84. htmlgraph/hooks/subagent_stop.py +309 -0
  85. htmlgraph/hooks/task_enforcer.py +39 -0
  86. htmlgraph/hooks/validator.py +15 -11
  87. htmlgraph/models.py +111 -15
  88. htmlgraph/operations/fastapi_server.py +230 -0
  89. htmlgraph/orchestration/headless_spawner.py +344 -29
  90. htmlgraph/orchestration/live_events.py +377 -0
  91. htmlgraph/pydantic_models.py +476 -0
  92. htmlgraph/quality_gates.py +350 -0
  93. htmlgraph/repo_hash.py +511 -0
  94. htmlgraph/sdk.py +348 -10
  95. htmlgraph/server.py +194 -0
  96. htmlgraph/session_hooks.py +300 -0
  97. htmlgraph/session_manager.py +131 -1
  98. htmlgraph/session_registry.py +587 -0
  99. htmlgraph/session_state.py +436 -0
  100. htmlgraph/system_prompts.py +449 -0
  101. htmlgraph/templates/orchestration-view.html +350 -0
  102. htmlgraph/track_builder.py +19 -0
  103. htmlgraph/validation.py +115 -0
  104. htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
  105. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
  106. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
  107. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
  108. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  109. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  110. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  111. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
  112. {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
  ]
@@ -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
@@ -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 = [s for s in all_spikes if s.agent_assigned == agent]
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: