htmlgraph 0.9.3__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 +173 -17
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +127 -0
- htmlgraph/agent_registry.py +45 -30
- htmlgraph/agents.py +160 -107
- htmlgraph/analytics/__init__.py +9 -2
- htmlgraph/analytics/cli.py +190 -51
- 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 +192 -100
- 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 +190 -14
- htmlgraph/analytics_index.py +135 -51
- 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 +208 -0
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/__init__.py +14 -0
- htmlgraph/builders/base.py +118 -29
- htmlgraph/builders/bug.py +150 -0
- htmlgraph/builders/chore.py +119 -0
- htmlgraph/builders/epic.py +150 -0
- htmlgraph/builders/feature.py +31 -6
- htmlgraph/builders/insight.py +195 -0
- htmlgraph/builders/metric.py +217 -0
- htmlgraph/builders/pattern.py +202 -0
- htmlgraph/builders/phase.py +162 -0
- htmlgraph/builders/spike.py +52 -19
- htmlgraph/builders/track.py +148 -72
- 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 +18 -0
- htmlgraph/collections/base.py +415 -98
- htmlgraph/collections/bug.py +53 -0
- htmlgraph/collections/chore.py +53 -0
- htmlgraph/collections/epic.py +53 -0
- htmlgraph/collections/feature.py +12 -26
- htmlgraph/collections/insight.py +100 -0
- htmlgraph/collections/metric.py +92 -0
- htmlgraph/collections/pattern.py +97 -0
- htmlgraph/collections/phase.py +53 -0
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +56 -16
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +511 -0
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +344 -0
- htmlgraph/converter.py +216 -28
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2406 -307
- 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 +19 -2
- htmlgraph/deploy.py +142 -125
- htmlgraph/deployment_models.py +474 -0
- 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 +182 -27
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +100 -52
- htmlgraph/event_migration.py +13 -4
- htmlgraph/exceptions.py +49 -0
- htmlgraph/file_watcher.py +101 -28
- htmlgraph/find_api.py +75 -63
- htmlgraph/git_events.py +145 -63
- htmlgraph/graph.py +1122 -106
- 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 +45 -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 +1314 -0
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/hooks-config.example.json +12 -0
- htmlgraph/hooks/installer.py +343 -0
- htmlgraph/hooks/orchestrator.py +674 -0
- htmlgraph/hooks/orchestrator_reflector.py +223 -0
- htmlgraph/hooks/post-checkout.sh +28 -0
- htmlgraph/hooks/post-commit.sh +24 -0
- htmlgraph/hooks/post-merge.sh +26 -0
- htmlgraph/hooks/post_tool_use_failure.py +273 -0
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +408 -0
- htmlgraph/hooks/pre-commit.sh +94 -0
- htmlgraph/hooks/pre-push.sh +28 -0
- htmlgraph/hooks/pretooluse.py +819 -0
- 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 +255 -0
- htmlgraph/hooks/task_validator.py +177 -0
- htmlgraph/hooks/validator.py +628 -0
- htmlgraph/ids.py +41 -27
- htmlgraph/index.d.ts +286 -0
- htmlgraph/learning.py +767 -0
- htmlgraph/mcp_server.py +69 -23
- htmlgraph/models.py +1586 -87
- 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/task_coordination.py +343 -0
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +669 -0
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +328 -0
- htmlgraph/orchestrator_validator.py +133 -0
- htmlgraph/parallel.py +646 -0
- htmlgraph/parser.py +160 -35
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/planning.py +147 -52
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +109 -72
- 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/routing.py +8 -19
- htmlgraph/scripts/deploy.py +1 -2
- 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 +685 -180
- htmlgraph/services/__init__.py +10 -0
- htmlgraph/services/claiming.py +199 -0
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +1392 -175
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +201 -0
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/setup.py +34 -17
- htmlgraph/spike_index.py +143 -0
- htmlgraph/sync_docs.py +12 -15
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph/templates/GEMINI.md.template +87 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +146 -15
- htmlgraph/track_manager.py +69 -21
- htmlgraph/transcript.py +890 -0
- htmlgraph/transcript_analytics.py +699 -0
- htmlgraph/types.py +323 -0
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +8 -5
- htmlgraph/work_type_utils.py +3 -2
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
- htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
- htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -2688
- htmlgraph/sdk.py +0 -709
- htmlgraph-0.9.3.dist-info/RECORD +0 -61
- {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task attribution mixin for SDK - subagent work tracking.
|
|
3
|
+
|
|
4
|
+
Provides methods for tracking which subagent did what work.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TaskAttributionMixin:
|
|
13
|
+
"""
|
|
14
|
+
Mixin providing task attribution capabilities to SDK.
|
|
15
|
+
|
|
16
|
+
Tracks which subagent executed what work for observability.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def get_task_attribution(self, task_id: str) -> dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Get attribution - which subagent did what work in this task?
|
|
22
|
+
|
|
23
|
+
Queries the database to find all events associated with a Claude Code task,
|
|
24
|
+
showing which subagent executed each tool call.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
task_id: Claude Code's internal task ID (available from Task() response)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Dictionary with task_id, by_subagent mapping, and total_events count
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> sdk = SDK(agent="claude")
|
|
34
|
+
>>> result = sdk.get_task_attribution("task-abc123-xyz789")
|
|
35
|
+
>>> for subagent, events in result['by_subagent'].items():
|
|
36
|
+
... logger.info(f"{subagent}:")
|
|
37
|
+
... for event in events:
|
|
38
|
+
... logger.info(f" - {event['tool']}: {event['summary']}")
|
|
39
|
+
>>> logger.info(f"Total events: {result['total_events']}")
|
|
40
|
+
|
|
41
|
+
See also:
|
|
42
|
+
get_subagent_work: Get all work grouped by subagent in a session
|
|
43
|
+
"""
|
|
44
|
+
from htmlgraph.config import get_database_path
|
|
45
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
db = HtmlGraphDB(str(get_database_path()))
|
|
49
|
+
events = db.get_events_for_task(task_id)
|
|
50
|
+
|
|
51
|
+
# Group by subagent_type
|
|
52
|
+
by_subagent: dict[str, list[dict[str, Any]]] = {}
|
|
53
|
+
for event in events:
|
|
54
|
+
agent = event.get("subagent_type", "orchestrator")
|
|
55
|
+
if agent not in by_subagent:
|
|
56
|
+
by_subagent[agent] = []
|
|
57
|
+
by_subagent[agent].append(
|
|
58
|
+
{
|
|
59
|
+
"tool": event.get("tool_name"),
|
|
60
|
+
"summary": event.get("input_summary"),
|
|
61
|
+
"timestamp": event.get("created_at"),
|
|
62
|
+
"event_id": event.get("event_id"),
|
|
63
|
+
"success": not event.get("is_error", False),
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"task_id": task_id,
|
|
69
|
+
"by_subagent": by_subagent,
|
|
70
|
+
"total_events": len(events),
|
|
71
|
+
}
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return {
|
|
74
|
+
"task_id": task_id,
|
|
75
|
+
"by_subagent": {},
|
|
76
|
+
"total_events": 0,
|
|
77
|
+
"error": str(e),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def get_subagent_work(self, session_id: str) -> dict[str, list[dict[str, Any]]]:
|
|
81
|
+
"""
|
|
82
|
+
Get all work grouped by which subagent did it in a session.
|
|
83
|
+
|
|
84
|
+
Shows which subagent (researcher, general-purpose, etc.) executed each
|
|
85
|
+
tool call within a session.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
session_id: Session ID to analyze
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary mapping subagent_type to list of events they executed.
|
|
92
|
+
Each event includes: tool_name, input_summary, output_summary, created_at, event_id
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> sdk = SDK(agent="claude")
|
|
96
|
+
>>> work = sdk.get_subagent_work("sess-123")
|
|
97
|
+
>>> for subagent, events in work.items():
|
|
98
|
+
... logger.info(f"{subagent} ({len(events)} events):")
|
|
99
|
+
... for event in events:
|
|
100
|
+
... logger.info(f" - {event['tool_name']}: {event['input_summary']}")
|
|
101
|
+
|
|
102
|
+
See also:
|
|
103
|
+
get_task_attribution: Get work for a specific Claude Code task
|
|
104
|
+
analyze_session: Get session metrics and analytics
|
|
105
|
+
"""
|
|
106
|
+
from htmlgraph.config import get_database_path
|
|
107
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
db = HtmlGraphDB(str(get_database_path()))
|
|
111
|
+
return db.get_subagent_work(session_id)
|
|
112
|
+
except Exception:
|
|
113
|
+
return {}
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core mixin for SDK - database, refs, export, and utility methods.
|
|
3
|
+
|
|
4
|
+
Provides essential SDK functionality that doesn't belong in specialized mixins.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from htmlgraph.agents import AgentInterface
|
|
17
|
+
from htmlgraph.db.schema import HtmlGraphDB
|
|
18
|
+
from htmlgraph.models import Node
|
|
19
|
+
from htmlgraph.refs import RefManager
|
|
20
|
+
from htmlgraph.session_manager import SessionManager
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CoreMixin:
|
|
26
|
+
"""
|
|
27
|
+
Mixin providing core SDK functionality.
|
|
28
|
+
|
|
29
|
+
Includes:
|
|
30
|
+
- Database access (db, query, execute_query_builder)
|
|
31
|
+
- Ref resolution (ref)
|
|
32
|
+
- Export functionality (export_to_html)
|
|
33
|
+
- Event logging (_log_event)
|
|
34
|
+
- Utility methods (reload, summary, my_work, next_task, get_status, dedupe_sessions)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_directory: Path
|
|
38
|
+
_db: HtmlGraphDB
|
|
39
|
+
_agent_id: str | None
|
|
40
|
+
_parent_session: str | None
|
|
41
|
+
_agent_interface: AgentInterface
|
|
42
|
+
_graph: Any
|
|
43
|
+
session_manager: SessionManager
|
|
44
|
+
refs: RefManager
|
|
45
|
+
|
|
46
|
+
# Collection references (for ref resolution)
|
|
47
|
+
features: Any
|
|
48
|
+
tracks: Any
|
|
49
|
+
bugs: Any
|
|
50
|
+
spikes: Any
|
|
51
|
+
chores: Any
|
|
52
|
+
epics: Any
|
|
53
|
+
todos: Any
|
|
54
|
+
phases: Any
|
|
55
|
+
|
|
56
|
+
def ref(self, short_ref: str) -> Node | None:
|
|
57
|
+
"""
|
|
58
|
+
Resolve a short ref to a Node object.
|
|
59
|
+
|
|
60
|
+
Short refs are stable identifiers like @f1, @t2, @b5 that map to
|
|
61
|
+
full node IDs. This method resolves the short ref and fetches the
|
|
62
|
+
corresponding node from the appropriate collection.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
short_ref: Short ref like "@f1", "@t2", "@b5", etc.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Node object or None if not found
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> sdk = SDK(agent="claude")
|
|
72
|
+
>>> feature = sdk.ref("@f1")
|
|
73
|
+
>>> if feature:
|
|
74
|
+
... logger.info("%s", feature.title)
|
|
75
|
+
... feature.status = "done"
|
|
76
|
+
... sdk.features.update(feature)
|
|
77
|
+
"""
|
|
78
|
+
# Resolve short ref to full ID
|
|
79
|
+
full_id = self.refs.resolve_ref(short_ref)
|
|
80
|
+
if not full_id:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
# Determine type from ref prefix and fetch from appropriate collection
|
|
84
|
+
if len(short_ref) < 2:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
prefix = short_ref[1] # Get letter after @
|
|
88
|
+
|
|
89
|
+
# Map prefix to collection
|
|
90
|
+
collection_map = {
|
|
91
|
+
"f": self.features,
|
|
92
|
+
"t": self.tracks,
|
|
93
|
+
"b": self.bugs,
|
|
94
|
+
"s": self.spikes,
|
|
95
|
+
"c": self.chores,
|
|
96
|
+
"e": self.epics,
|
|
97
|
+
"d": self.todos,
|
|
98
|
+
"p": self.phases,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
collection = collection_map.get(prefix)
|
|
102
|
+
if not collection:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
# Get node from collection
|
|
106
|
+
if hasattr(collection, "get"):
|
|
107
|
+
return cast("Node | None", collection.get(full_id))
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# =========================================================================
|
|
112
|
+
# SQLite Database Integration
|
|
113
|
+
# =========================================================================
|
|
114
|
+
|
|
115
|
+
def db(self) -> HtmlGraphDB:
|
|
116
|
+
"""
|
|
117
|
+
Get the SQLite database instance.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
HtmlGraphDB instance for executing queries
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
>>> sdk = SDK(agent="claude")
|
|
124
|
+
>>> db = sdk.db()
|
|
125
|
+
>>> events = db.get_session_events("sess-123")
|
|
126
|
+
>>> features = db.get_features_by_status("todo")
|
|
127
|
+
"""
|
|
128
|
+
return self._db
|
|
129
|
+
|
|
130
|
+
def query(self, sql: str, params: tuple[Any, ...] = ()) -> list[dict[str, Any]]:
|
|
131
|
+
"""
|
|
132
|
+
Execute a raw SQL query on the SQLite database.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
sql: SQL query string
|
|
136
|
+
params: Query parameters (for safe parameterized queries)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of result dictionaries
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> sdk = SDK(agent="claude")
|
|
143
|
+
>>> results = sdk.query(
|
|
144
|
+
... "SELECT * FROM features WHERE status = ? AND priority = ?",
|
|
145
|
+
... ("todo", "high")
|
|
146
|
+
... )
|
|
147
|
+
>>> for row in results:
|
|
148
|
+
... print(row["title"])
|
|
149
|
+
"""
|
|
150
|
+
if not self._db.connection:
|
|
151
|
+
self._db.connect()
|
|
152
|
+
|
|
153
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
154
|
+
cursor.execute(sql, params)
|
|
155
|
+
rows = cursor.fetchall()
|
|
156
|
+
return [dict(row) for row in rows]
|
|
157
|
+
|
|
158
|
+
def execute_query_builder(
|
|
159
|
+
self, sql: str, params: tuple[Any, ...] = ()
|
|
160
|
+
) -> list[dict[str, Any]]:
|
|
161
|
+
"""
|
|
162
|
+
Execute a query using the Queries builder.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
sql: SQL query from Queries builder
|
|
166
|
+
params: Parameters from Queries builder
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of result dictionaries
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> sdk = SDK(agent="claude")
|
|
173
|
+
>>> sql, params = Queries.get_features_by_status("todo", limit=5)
|
|
174
|
+
>>> results = sdk.execute_query_builder(sql, params)
|
|
175
|
+
"""
|
|
176
|
+
return self.query(sql, params)
|
|
177
|
+
|
|
178
|
+
def export_to_html(
|
|
179
|
+
self,
|
|
180
|
+
output_dir: str | None = None,
|
|
181
|
+
include_features: bool = True,
|
|
182
|
+
include_sessions: bool = True,
|
|
183
|
+
include_events: bool = False,
|
|
184
|
+
) -> dict[str, int]:
|
|
185
|
+
"""
|
|
186
|
+
Export SQLite data to HTML files for backward compatibility.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
output_dir: Directory to export to (defaults to .htmlgraph)
|
|
190
|
+
include_features: Export features
|
|
191
|
+
include_sessions: Export sessions
|
|
192
|
+
include_events: Export events (detailed, use with care)
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Dict with export counts: {"features": int, "sessions": int, "events": int}
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> sdk = SDK(agent="claude")
|
|
199
|
+
>>> result = sdk.export_to_html()
|
|
200
|
+
>>> logger.info(f"Exported {result['features']} features")
|
|
201
|
+
"""
|
|
202
|
+
from pathlib import Path
|
|
203
|
+
|
|
204
|
+
if output_dir is None:
|
|
205
|
+
output_dir = str(self._directory)
|
|
206
|
+
|
|
207
|
+
output_path = Path(output_dir)
|
|
208
|
+
counts: dict[str, int] = {"features": 0, "sessions": 0, "events": 0}
|
|
209
|
+
|
|
210
|
+
if include_features:
|
|
211
|
+
# Export all features from SQLite to HTML
|
|
212
|
+
features_dir = output_path / "features"
|
|
213
|
+
features_dir.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
217
|
+
cursor.execute("SELECT * FROM features")
|
|
218
|
+
rows = cursor.fetchall()
|
|
219
|
+
|
|
220
|
+
for row in rows:
|
|
221
|
+
feature_dict = dict(row)
|
|
222
|
+
feature_id = feature_dict["id"]
|
|
223
|
+
# Write HTML file (simplified export)
|
|
224
|
+
html_file = features_dir / f"{feature_id}.html"
|
|
225
|
+
html_file.write_text(
|
|
226
|
+
f"<h1>{feature_dict['title']}</h1>"
|
|
227
|
+
f"<p>Status: {feature_dict['status']}</p>"
|
|
228
|
+
f"<p>Type: {feature_dict['type']}</p>"
|
|
229
|
+
)
|
|
230
|
+
counts["features"] += 1
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Error exporting features: {e}")
|
|
233
|
+
|
|
234
|
+
if include_sessions:
|
|
235
|
+
# Export all sessions from SQLite to HTML
|
|
236
|
+
sessions_dir = output_path / "sessions"
|
|
237
|
+
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
cursor = self._db.connection.cursor() # type: ignore[union-attr]
|
|
241
|
+
cursor.execute("SELECT * FROM sessions")
|
|
242
|
+
rows = cursor.fetchall()
|
|
243
|
+
|
|
244
|
+
for row in rows:
|
|
245
|
+
session_dict = dict(row)
|
|
246
|
+
session_id = session_dict["session_id"]
|
|
247
|
+
# Write HTML file (simplified export)
|
|
248
|
+
html_file = sessions_dir / f"{session_id}.html"
|
|
249
|
+
html_file.write_text(
|
|
250
|
+
f"<h1>Session {session_id}</h1>"
|
|
251
|
+
f"<p>Agent: {session_dict['agent_assigned']}</p>"
|
|
252
|
+
f"<p>Status: {session_dict['status']}</p>"
|
|
253
|
+
)
|
|
254
|
+
counts["sessions"] += 1
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"Error exporting sessions: {e}")
|
|
257
|
+
|
|
258
|
+
return counts
|
|
259
|
+
|
|
260
|
+
def _log_event(
|
|
261
|
+
self,
|
|
262
|
+
event_type: str,
|
|
263
|
+
tool_name: str | None = None,
|
|
264
|
+
input_summary: str | None = None,
|
|
265
|
+
output_summary: str | None = None,
|
|
266
|
+
context: dict[str, Any] | None = None,
|
|
267
|
+
cost_tokens: int = 0,
|
|
268
|
+
) -> bool:
|
|
269
|
+
"""
|
|
270
|
+
Log an event to the SQLite database with parent-child linking.
|
|
271
|
+
|
|
272
|
+
Internal method used by collections to track operations.
|
|
273
|
+
Automatically creates a session if one doesn't exist.
|
|
274
|
+
Reads parent event ID from HTMLGRAPH_PARENT_ACTIVITY env var for hierarchical tracking.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
event_type: Type of event (tool_call, completion, error, etc.)
|
|
278
|
+
tool_name: Tool that was called
|
|
279
|
+
input_summary: Summary of input
|
|
280
|
+
output_summary: Summary of output
|
|
281
|
+
context: Additional context metadata
|
|
282
|
+
cost_tokens: Token cost estimate
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
True if logged successfully, False otherwise
|
|
286
|
+
|
|
287
|
+
Example (internal use):
|
|
288
|
+
>>> sdk._log_event(
|
|
289
|
+
... event_type="tool_call",
|
|
290
|
+
... tool_name="Edit",
|
|
291
|
+
... input_summary="Edit file.py",
|
|
292
|
+
... cost_tokens=100
|
|
293
|
+
... )
|
|
294
|
+
"""
|
|
295
|
+
from uuid import uuid4
|
|
296
|
+
|
|
297
|
+
event_id = f"evt-{uuid4().hex[:12]}"
|
|
298
|
+
session_id = self._parent_session or "cli-session"
|
|
299
|
+
|
|
300
|
+
# Read parent event ID from environment variable for hierarchical linking
|
|
301
|
+
parent_event_id = os.getenv("HTMLGRAPH_PARENT_ACTIVITY")
|
|
302
|
+
|
|
303
|
+
# Ensure session exists before logging event
|
|
304
|
+
try:
|
|
305
|
+
self._ensure_session_exists(session_id, parent_event_id=parent_event_id) # type: ignore[attr-defined]
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.debug(f"Failed to ensure session exists: {e}")
|
|
308
|
+
# Continue anyway - session creation failure shouldn't block event logging
|
|
309
|
+
|
|
310
|
+
# Ensure agent_id is set for event logging
|
|
311
|
+
agent_id = self._agent_id or "unknown"
|
|
312
|
+
|
|
313
|
+
return self._db.insert_event(
|
|
314
|
+
event_id=event_id,
|
|
315
|
+
agent_id=agent_id,
|
|
316
|
+
event_type=event_type,
|
|
317
|
+
session_id=session_id,
|
|
318
|
+
tool_name=tool_name,
|
|
319
|
+
input_summary=input_summary,
|
|
320
|
+
output_summary=output_summary,
|
|
321
|
+
context=context,
|
|
322
|
+
parent_event_id=parent_event_id,
|
|
323
|
+
cost_tokens=cost_tokens,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def reload(self) -> None:
|
|
327
|
+
"""Reload all data from disk."""
|
|
328
|
+
self._graph.reload()
|
|
329
|
+
self._agent_interface.reload()
|
|
330
|
+
# SessionManager reloads implicitly on access via its converters/graphs
|
|
331
|
+
|
|
332
|
+
def summary(self, max_items: int = 10) -> str:
|
|
333
|
+
"""
|
|
334
|
+
Get project summary.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Compact overview for AI agent orientation
|
|
338
|
+
"""
|
|
339
|
+
return self._agent_interface.get_summary(max_items)
|
|
340
|
+
|
|
341
|
+
def my_work(self) -> dict[str, Any]:
|
|
342
|
+
"""
|
|
343
|
+
Get current agent's workload.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Dict with in_progress, completed counts
|
|
347
|
+
"""
|
|
348
|
+
if not self._agent_id:
|
|
349
|
+
raise ValueError("No agent ID set")
|
|
350
|
+
return self._agent_interface.get_workload(self._agent_id)
|
|
351
|
+
|
|
352
|
+
def next_task(
|
|
353
|
+
self, priority: str | None = None, auto_claim: bool = True
|
|
354
|
+
) -> Node | None:
|
|
355
|
+
"""
|
|
356
|
+
Get next available task for this agent.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
priority: Optional priority filter
|
|
360
|
+
auto_claim: Automatically claim the task
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Next available Node or None
|
|
364
|
+
"""
|
|
365
|
+
return self._agent_interface.get_next_task(
|
|
366
|
+
agent_id=self._agent_id,
|
|
367
|
+
priority=priority,
|
|
368
|
+
node_type="feature",
|
|
369
|
+
auto_claim=auto_claim,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def get_status(self) -> dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Get project status.
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
Dict with status metrics (WIP, counts, etc.)
|
|
378
|
+
"""
|
|
379
|
+
return self.session_manager.get_status()
|
|
380
|
+
|
|
381
|
+
def dedupe_sessions(
|
|
382
|
+
self,
|
|
383
|
+
max_events: int = 1,
|
|
384
|
+
move_dir_name: str = "_orphans",
|
|
385
|
+
dry_run: bool = False,
|
|
386
|
+
stale_extra_active: bool = True,
|
|
387
|
+
) -> dict[str, int]:
|
|
388
|
+
"""
|
|
389
|
+
Move low-signal sessions (e.g. SessionStart-only) out of the main sessions dir.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
max_events: Maximum events threshold (sessions with <= this many events are moved)
|
|
393
|
+
move_dir_name: Directory name to move orphaned sessions to
|
|
394
|
+
dry_run: If True, only report what would be done without actually moving files
|
|
395
|
+
stale_extra_active: If True, also mark extra active sessions as stale
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Dict with counts: {"scanned": int, "moved": int, "missing": int, "staled_active": int, "kept_active": int}
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> sdk = SDK(agent="claude")
|
|
402
|
+
>>> result = sdk.dedupe_sessions(max_events=1, dry_run=False)
|
|
403
|
+
>>> logger.info(f"Scanned: {result['scanned']}, Moved: {result['moved']}")
|
|
404
|
+
"""
|
|
405
|
+
return self.session_manager.dedupe_orphan_sessions(
|
|
406
|
+
max_events=max_events,
|
|
407
|
+
move_dir_name=move_dir_name,
|
|
408
|
+
dry_run=dry_run,
|
|
409
|
+
stale_extra_active=stale_extra_active,
|
|
410
|
+
)
|