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,544 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hybrid Error Handling System - Core error handler module.
|
|
3
|
+
|
|
4
|
+
Provides structured exception capture, formatting, and storage with three-tier
|
|
5
|
+
display levels (minimal, verbose, debug) for token-efficient error reporting.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- ErrorRecord: Structured exception representation
|
|
9
|
+
- LocalsSanitizer: Safely extract locals (exclude secrets)
|
|
10
|
+
- MinimalFormatter: 163 tokens - error type + message only
|
|
11
|
+
- VerboseFormatter: 300 tokens - stack trace without locals
|
|
12
|
+
- DebugFormatter: 794 tokens - full Rich traceback with sanitized locals
|
|
13
|
+
- ErrorHandler: Main class for capturing and formatting exceptions
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import traceback
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from typing import Any, Literal
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ErrorRecord:
|
|
27
|
+
"""
|
|
28
|
+
Structured representation of an exception with full context.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
exception: The exception object
|
|
32
|
+
exception_type: Class name of the exception
|
|
33
|
+
message: Exception message
|
|
34
|
+
traceback_str: Full traceback as string
|
|
35
|
+
locals_dict: Local variables at point of exception
|
|
36
|
+
stack_frames: List of stack frame information
|
|
37
|
+
captured_at: Timestamp when error was captured
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
exception: BaseException
|
|
41
|
+
exception_type: str
|
|
42
|
+
message: str
|
|
43
|
+
traceback_str: str
|
|
44
|
+
locals_dict: dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
stack_frames: list[dict[str, Any]] = field(default_factory=list)
|
|
46
|
+
captured_at: datetime = field(default_factory=datetime.now)
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict[str, Any]:
|
|
49
|
+
"""Convert to JSON-serializable dict (excluding exception object)."""
|
|
50
|
+
return {
|
|
51
|
+
"exception_type": self.exception_type,
|
|
52
|
+
"message": self.message,
|
|
53
|
+
"traceback_str": self.traceback_str,
|
|
54
|
+
"locals_dict": self.locals_dict,
|
|
55
|
+
"stack_frames": self.stack_frames,
|
|
56
|
+
"captured_at": self.captured_at.isoformat(),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def to_json(self) -> str:
|
|
60
|
+
"""Serialize to JSON string."""
|
|
61
|
+
try:
|
|
62
|
+
return json.dumps(self.to_dict())
|
|
63
|
+
except (TypeError, ValueError):
|
|
64
|
+
# Fallback if serialization fails
|
|
65
|
+
return json.dumps(
|
|
66
|
+
{
|
|
67
|
+
"exception_type": self.exception_type,
|
|
68
|
+
"message": self.message,
|
|
69
|
+
"traceback_str": self.traceback_str,
|
|
70
|
+
"captured_at": self.captured_at.isoformat(),
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LocalsSanitizer:
|
|
76
|
+
"""
|
|
77
|
+
Safely sanitize local variables for error logging.
|
|
78
|
+
|
|
79
|
+
Excludes sensitive variables (passwords, tokens, secrets, api_keys),
|
|
80
|
+
truncates large values, and limits container sizes to prevent
|
|
81
|
+
exposing secrets or consuming excessive storage.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# Patterns for sensitive variable names
|
|
85
|
+
SECRET_PATTERNS = {
|
|
86
|
+
r".*password.*",
|
|
87
|
+
r".*token.*",
|
|
88
|
+
r".*secret.*",
|
|
89
|
+
r".*api_key.*",
|
|
90
|
+
r".*api.*",
|
|
91
|
+
r".*credential.*",
|
|
92
|
+
r".*auth.*",
|
|
93
|
+
r".*oauth.*",
|
|
94
|
+
r".*bearer.*",
|
|
95
|
+
r".*key.*",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Max sizes for truncation
|
|
99
|
+
MAX_STRING_LENGTH = 500
|
|
100
|
+
MAX_DICT_ITEMS = 10
|
|
101
|
+
MAX_LIST_ITEMS = 10
|
|
102
|
+
MAX_TOTAL_LOCALS = 5000
|
|
103
|
+
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
"""Initialize sanitizer with compiled regex patterns."""
|
|
106
|
+
self.secret_patterns = [
|
|
107
|
+
re.compile(p, re.IGNORECASE) for p in self.SECRET_PATTERNS
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
def is_secret_pattern(self, key: str) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Check if variable name matches sensitive patterns.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
key: Variable name to check
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
True if matches secret pattern, False otherwise
|
|
119
|
+
"""
|
|
120
|
+
return any(pattern.match(key) for pattern in self.secret_patterns)
|
|
121
|
+
|
|
122
|
+
def truncate_if_needed(self, value: Any, depth: int = 0) -> Any:
|
|
123
|
+
"""
|
|
124
|
+
Recursively truncate large values.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
value: Value to potentially truncate
|
|
128
|
+
depth: Current recursion depth (prevents infinite recursion)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Truncated value or original if within limits
|
|
132
|
+
"""
|
|
133
|
+
# Prevent deep recursion
|
|
134
|
+
if depth > 5:
|
|
135
|
+
return "[truncated: max depth exceeded]"
|
|
136
|
+
|
|
137
|
+
if isinstance(value, str):
|
|
138
|
+
if len(value) > self.MAX_STRING_LENGTH:
|
|
139
|
+
return value[: self.MAX_STRING_LENGTH] + "..."
|
|
140
|
+
return value
|
|
141
|
+
|
|
142
|
+
if isinstance(value, dict):
|
|
143
|
+
if len(value) > self.MAX_DICT_ITEMS:
|
|
144
|
+
return {
|
|
145
|
+
k: self.truncate_if_needed(v, depth + 1)
|
|
146
|
+
for k, v in list(value.items())[: self.MAX_DICT_ITEMS]
|
|
147
|
+
} | {
|
|
148
|
+
"[...]": f"(truncated: {len(value) - self.MAX_DICT_ITEMS} more items)"
|
|
149
|
+
}
|
|
150
|
+
return {k: self.truncate_if_needed(v, depth + 1) for k, v in value.items()}
|
|
151
|
+
|
|
152
|
+
if isinstance(value, (list, tuple)):
|
|
153
|
+
if len(value) > self.MAX_LIST_ITEMS:
|
|
154
|
+
truncated = [
|
|
155
|
+
self.truncate_if_needed(v, depth + 1)
|
|
156
|
+
for v in list(value)[: self.MAX_LIST_ITEMS]
|
|
157
|
+
]
|
|
158
|
+
truncated.append(
|
|
159
|
+
f"[...truncated: {len(value) - self.MAX_LIST_ITEMS} more items]"
|
|
160
|
+
)
|
|
161
|
+
return truncated if isinstance(value, list) else tuple(truncated)
|
|
162
|
+
return [self.truncate_if_needed(v, depth + 1) for v in value]
|
|
163
|
+
|
|
164
|
+
return value
|
|
165
|
+
|
|
166
|
+
def sanitize(self, locals_dict: dict[str, Any]) -> dict[str, Any]:
|
|
167
|
+
"""
|
|
168
|
+
Sanitize local variables, removing secrets and limiting sizes.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
locals_dict: Dictionary of local variables
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Sanitized dictionary safe for logging
|
|
175
|
+
"""
|
|
176
|
+
sanitized: dict[str, Any] = {}
|
|
177
|
+
total_size = 0
|
|
178
|
+
|
|
179
|
+
for key, value in locals_dict.items():
|
|
180
|
+
# Skip secret patterns
|
|
181
|
+
if self.is_secret_pattern(key):
|
|
182
|
+
sanitized[key] = "[REDACTED]"
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
# Skip common Python internals
|
|
186
|
+
if key.startswith("__") or key in ("self", "cls"):
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
# Truncate large values
|
|
191
|
+
truncated = self.truncate_if_needed(value)
|
|
192
|
+
|
|
193
|
+
# Convert to JSON-serializable form and measure size
|
|
194
|
+
try:
|
|
195
|
+
json_str = json.dumps(truncated, default=str)
|
|
196
|
+
size = len(json_str)
|
|
197
|
+
except (TypeError, ValueError):
|
|
198
|
+
# If not JSON-serializable, convert to string
|
|
199
|
+
json_str = json.dumps(str(truncated))
|
|
200
|
+
size = len(json_str)
|
|
201
|
+
|
|
202
|
+
# Stop adding if we exceed max total size
|
|
203
|
+
if total_size + size > self.MAX_TOTAL_LOCALS:
|
|
204
|
+
sanitized["[truncated]"] = (
|
|
205
|
+
f"(locals exceeded {self.MAX_TOTAL_LOCALS} chars)"
|
|
206
|
+
)
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
sanitized[key] = truncated
|
|
210
|
+
total_size += size
|
|
211
|
+
|
|
212
|
+
except Exception:
|
|
213
|
+
# If sanitization fails, skip this variable
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
return sanitized
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class MinimalFormatter:
|
|
220
|
+
"""
|
|
221
|
+
Minimal error format (163 tokens).
|
|
222
|
+
|
|
223
|
+
Displays only error type, message, and hint to use --debug flag.
|
|
224
|
+
Used for normal operation to minimize token usage.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def format(record: ErrorRecord) -> str:
|
|
229
|
+
"""
|
|
230
|
+
Format error in minimal style.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
record: ErrorRecord to format
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Formatted error string
|
|
237
|
+
"""
|
|
238
|
+
lines = [
|
|
239
|
+
f"ERROR {record.exception_type}: {record.message}",
|
|
240
|
+
"",
|
|
241
|
+
"Run with --debug for full traceback and context",
|
|
242
|
+
]
|
|
243
|
+
return "\n".join(lines)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class VerboseFormatter:
|
|
247
|
+
"""
|
|
248
|
+
Verbose error format (300 tokens).
|
|
249
|
+
|
|
250
|
+
Displays error type, message, and stack trace without local variables.
|
|
251
|
+
Used with --verbose flag for intermediate detail level.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def format(record: ErrorRecord) -> str:
|
|
256
|
+
"""
|
|
257
|
+
Format error in verbose style.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
record: ErrorRecord to format
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Formatted error string
|
|
264
|
+
"""
|
|
265
|
+
lines = [
|
|
266
|
+
f"ERROR {record.exception_type}: {record.message}",
|
|
267
|
+
"",
|
|
268
|
+
"Stack trace:",
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
# Add stack frames
|
|
272
|
+
for frame in record.stack_frames:
|
|
273
|
+
filename = frame.get("filename", "unknown")
|
|
274
|
+
lineno = frame.get("lineno", "?")
|
|
275
|
+
function = frame.get("function", "?")
|
|
276
|
+
code_line = frame.get("code_line", "")
|
|
277
|
+
|
|
278
|
+
lines.append(f' File "{filename}", line {lineno}, in {function}')
|
|
279
|
+
if code_line:
|
|
280
|
+
lines.append(f" {code_line.strip()}")
|
|
281
|
+
|
|
282
|
+
lines.append("")
|
|
283
|
+
lines.append("Run with --debug for full local variable context")
|
|
284
|
+
|
|
285
|
+
return "\n".join(lines)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class DebugFormatter:
|
|
289
|
+
"""
|
|
290
|
+
Debug error format (794 tokens).
|
|
291
|
+
|
|
292
|
+
Displays full Rich-formatted traceback with sanitized local variables.
|
|
293
|
+
Used with --debug flag for complete debugging information.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def format(record: ErrorRecord) -> str:
|
|
298
|
+
"""
|
|
299
|
+
Format error in debug style.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
record: ErrorRecord to format
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Formatted error string with full context
|
|
306
|
+
"""
|
|
307
|
+
# Try to use Rich for fancy formatting
|
|
308
|
+
try:
|
|
309
|
+
# Rich is available but we format manually
|
|
310
|
+
# Convert traceback string to Traceback object
|
|
311
|
+
tb_str = record.traceback_str
|
|
312
|
+
|
|
313
|
+
# Format as code block with traceback
|
|
314
|
+
lines = [
|
|
315
|
+
"═" * 60,
|
|
316
|
+
"FULL TRACEBACK (--debug mode)",
|
|
317
|
+
"═" * 60,
|
|
318
|
+
"",
|
|
319
|
+
tb_str,
|
|
320
|
+
"",
|
|
321
|
+
]
|
|
322
|
+
|
|
323
|
+
# Add locals if available
|
|
324
|
+
if record.locals_dict:
|
|
325
|
+
lines.append("─" * 60)
|
|
326
|
+
lines.append("LOCAL VARIABLES")
|
|
327
|
+
lines.append("─" * 60)
|
|
328
|
+
for key, value in record.locals_dict.items():
|
|
329
|
+
try:
|
|
330
|
+
val_str = json.dumps(value, default=str, indent=2)
|
|
331
|
+
if len(val_str) > 100:
|
|
332
|
+
val_str = val_str[:100] + "..."
|
|
333
|
+
except (TypeError, ValueError):
|
|
334
|
+
val_str = str(value)
|
|
335
|
+
|
|
336
|
+
lines.append(f"{key} = {val_str}")
|
|
337
|
+
|
|
338
|
+
lines.append("")
|
|
339
|
+
|
|
340
|
+
return "\n".join(lines)
|
|
341
|
+
|
|
342
|
+
except ImportError:
|
|
343
|
+
# Fallback if Rich not available
|
|
344
|
+
lines = [
|
|
345
|
+
"FULL TRACEBACK",
|
|
346
|
+
"═" * 60,
|
|
347
|
+
record.traceback_str,
|
|
348
|
+
"",
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
if record.locals_dict:
|
|
352
|
+
lines.append("LOCAL VARIABLES")
|
|
353
|
+
lines.append("─" * 60)
|
|
354
|
+
for key, value in record.locals_dict.items():
|
|
355
|
+
try:
|
|
356
|
+
val_str = json.dumps(value, default=str)
|
|
357
|
+
except (TypeError, ValueError):
|
|
358
|
+
val_str = str(value)
|
|
359
|
+
lines.append(f"{key} = {val_str}")
|
|
360
|
+
|
|
361
|
+
return "\n".join(lines)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class ErrorHandler:
|
|
365
|
+
"""
|
|
366
|
+
Main error handler for capturing and formatting exceptions.
|
|
367
|
+
|
|
368
|
+
Provides methods to:
|
|
369
|
+
- Capture exceptions with full context
|
|
370
|
+
- Extract stack frames and locals
|
|
371
|
+
- Format errors at different verbosity levels
|
|
372
|
+
- Serialize for storage
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
def __init__(self, debug: bool = False, verbose: bool = False) -> None:
|
|
376
|
+
"""
|
|
377
|
+
Initialize ErrorHandler.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
debug: Whether to show debug output
|
|
381
|
+
verbose: Whether to show verbose output
|
|
382
|
+
"""
|
|
383
|
+
self.debug = debug
|
|
384
|
+
self.verbose = verbose
|
|
385
|
+
self.sanitizer = LocalsSanitizer()
|
|
386
|
+
|
|
387
|
+
def capture_exception(self, exception: BaseException | None = None) -> ErrorRecord:
|
|
388
|
+
"""
|
|
389
|
+
Capture exception with full context including stack frames and locals.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
exception: Exception to capture (uses sys.exc_info() if None)
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
ErrorRecord with captured exception details
|
|
396
|
+
"""
|
|
397
|
+
exc_traceback = None
|
|
398
|
+
if exception is None:
|
|
399
|
+
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
400
|
+
if exc_value is None:
|
|
401
|
+
# No active exception
|
|
402
|
+
raise RuntimeError("No active exception to capture")
|
|
403
|
+
exception = exc_value
|
|
404
|
+
|
|
405
|
+
# Get exception info
|
|
406
|
+
exception_type = exception.__class__.__name__
|
|
407
|
+
message = str(exception)
|
|
408
|
+
|
|
409
|
+
# Capture traceback
|
|
410
|
+
traceback_str = "".join(
|
|
411
|
+
traceback.format_exception(type(exception), exception, exc_traceback)
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Extract stack frames
|
|
415
|
+
stack_frames = self._extract_stack_frames(exc_traceback)
|
|
416
|
+
|
|
417
|
+
# Extract locals from each frame
|
|
418
|
+
locals_dict = self._extract_locals(exc_traceback)
|
|
419
|
+
|
|
420
|
+
return ErrorRecord(
|
|
421
|
+
exception=exception,
|
|
422
|
+
exception_type=exception_type,
|
|
423
|
+
message=message,
|
|
424
|
+
traceback_str=traceback_str,
|
|
425
|
+
locals_dict=locals_dict,
|
|
426
|
+
stack_frames=stack_frames,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def _extract_stack_frames(self, tb: Any) -> list[dict[str, Any]]:
|
|
430
|
+
"""
|
|
431
|
+
Extract stack frame information from traceback.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
tb: Traceback object
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
List of frame dictionaries
|
|
438
|
+
"""
|
|
439
|
+
frames: list[dict[str, Any]] = []
|
|
440
|
+
|
|
441
|
+
while tb is not None:
|
|
442
|
+
frame = tb.tb_frame
|
|
443
|
+
frames.append(
|
|
444
|
+
{
|
|
445
|
+
"filename": frame.f_code.co_filename,
|
|
446
|
+
"lineno": tb.tb_lineno,
|
|
447
|
+
"function": frame.f_code.co_name,
|
|
448
|
+
"code_line": self._get_code_line(
|
|
449
|
+
frame.f_code.co_filename, tb.tb_lineno
|
|
450
|
+
),
|
|
451
|
+
}
|
|
452
|
+
)
|
|
453
|
+
tb = tb.tb_next
|
|
454
|
+
|
|
455
|
+
return frames
|
|
456
|
+
|
|
457
|
+
def _get_code_line(self, filename: str, lineno: int) -> str:
|
|
458
|
+
"""
|
|
459
|
+
Get source code line at specified location.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
filename: Source file path
|
|
463
|
+
lineno: Line number
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Source code line or empty string if not found
|
|
467
|
+
"""
|
|
468
|
+
try:
|
|
469
|
+
with open(filename, encoding="utf-8") as f:
|
|
470
|
+
lines = f.readlines()
|
|
471
|
+
if 0 < lineno <= len(lines):
|
|
472
|
+
return lines[lineno - 1].rstrip()
|
|
473
|
+
except OSError:
|
|
474
|
+
pass
|
|
475
|
+
return ""
|
|
476
|
+
|
|
477
|
+
def _extract_locals(self, tb: Any) -> dict[str, Any]:
|
|
478
|
+
"""
|
|
479
|
+
Extract local variables from traceback frames.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
tb: Traceback object
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Sanitized locals dictionary from innermost frame
|
|
486
|
+
"""
|
|
487
|
+
locals_dict: dict[str, Any] = {}
|
|
488
|
+
|
|
489
|
+
# Get locals from innermost frame (where exception occurred)
|
|
490
|
+
while tb is not None:
|
|
491
|
+
locals_dict = tb.tb_frame.f_locals.copy()
|
|
492
|
+
tb = tb.tb_next
|
|
493
|
+
|
|
494
|
+
# Sanitize before returning
|
|
495
|
+
return self.sanitizer.sanitize(locals_dict)
|
|
496
|
+
|
|
497
|
+
def format_error(
|
|
498
|
+
self,
|
|
499
|
+
record: ErrorRecord,
|
|
500
|
+
level: Literal["minimal", "verbose", "debug"] | None = None,
|
|
501
|
+
) -> str:
|
|
502
|
+
"""
|
|
503
|
+
Format error record at specified verbosity level.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
record: ErrorRecord to format
|
|
507
|
+
level: Display level (minimal/verbose/debug). If None, infers from flags.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Formatted error string
|
|
511
|
+
"""
|
|
512
|
+
if level is None:
|
|
513
|
+
if self.debug:
|
|
514
|
+
level = "debug"
|
|
515
|
+
elif self.verbose:
|
|
516
|
+
level = "verbose"
|
|
517
|
+
else:
|
|
518
|
+
level = "minimal"
|
|
519
|
+
|
|
520
|
+
if level == "debug":
|
|
521
|
+
return DebugFormatter.format(record)
|
|
522
|
+
elif level == "verbose":
|
|
523
|
+
return VerboseFormatter.format(record)
|
|
524
|
+
else: # minimal
|
|
525
|
+
return MinimalFormatter.format(record)
|
|
526
|
+
|
|
527
|
+
def serialize_for_storage(self, record: ErrorRecord) -> dict[str, Any]:
|
|
528
|
+
"""
|
|
529
|
+
Serialize ErrorRecord for storage in ErrorEntry.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
record: ErrorRecord to serialize
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Dictionary suitable for JSON storage
|
|
536
|
+
"""
|
|
537
|
+
return {
|
|
538
|
+
"exception_type": record.exception_type,
|
|
539
|
+
"message": record.message,
|
|
540
|
+
"traceback": record.traceback_str,
|
|
541
|
+
"locals_dump": json.dumps(record.locals_dict, default=str),
|
|
542
|
+
"stack_frames": record.stack_frames,
|
|
543
|
+
"captured_at": record.captured_at.isoformat(),
|
|
544
|
+
}
|
htmlgraph/event_log.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Event logging for HtmlGraph.
|
|
3
5
|
|
|
@@ -9,54 +11,98 @@ Design goals:
|
|
|
9
11
|
- Deterministic serialization for rebuildable analytics indexes
|
|
10
12
|
"""
|
|
11
13
|
|
|
12
|
-
from __future__ import annotations
|
|
13
14
|
|
|
14
15
|
import json
|
|
15
|
-
from dataclasses import dataclass
|
|
16
16
|
from datetime import datetime
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import TYPE_CHECKING, Any
|
|
19
19
|
|
|
20
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator
|
|
21
|
+
|
|
20
22
|
if TYPE_CHECKING:
|
|
21
23
|
pass
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
class EventRecord(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Event record for HtmlGraph tracking.
|
|
29
|
+
|
|
30
|
+
Uses Pydantic for automatic validation and serialization.
|
|
31
|
+
Immutable via ConfigDict(frozen=True).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(frozen=True)
|
|
35
|
+
|
|
36
|
+
event_id: str = Field(..., min_length=1, description="Unique event identifier")
|
|
37
|
+
timestamp: datetime = Field(..., description="Event timestamp")
|
|
38
|
+
session_id: str = Field(..., min_length=1, description="Session identifier")
|
|
39
|
+
agent: str = Field(..., description="Agent name (e.g., 'claude', 'gemini')")
|
|
40
|
+
tool: str = Field(..., description="Tool used (e.g., 'Bash', 'Edit', 'Read')")
|
|
41
|
+
summary: str = Field(..., description="Human-readable event summary")
|
|
42
|
+
success: bool = Field(..., description="Whether the operation succeeded")
|
|
43
|
+
feature_id: str | None = Field(None, description="Associated feature ID")
|
|
44
|
+
drift_score: float | None = Field(None, description="Context drift score")
|
|
45
|
+
start_commit: str | None = Field(None, description="Starting git commit hash")
|
|
46
|
+
continued_from: str | None = Field(
|
|
47
|
+
None, description="Previous session ID if continued"
|
|
48
|
+
)
|
|
49
|
+
work_type: str | None = Field(None, description="WorkType enum value")
|
|
50
|
+
session_status: str | None = Field(None, description="Session status")
|
|
51
|
+
file_paths: list[str] | None = Field(None, description="Files involved in event")
|
|
52
|
+
payload: dict[str, Any] | None = Field(None, description="Additional event data")
|
|
53
|
+
parent_session_id: str | None = Field(
|
|
54
|
+
None, description="Parent session ID for subagents"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Phase 1: Enhanced Event Data Schema for multi-AI delegation tracking
|
|
58
|
+
delegated_to_ai: str | None = Field(
|
|
59
|
+
None, description="AI delegate: 'gemini', 'codex', 'copilot', 'claude', or None"
|
|
60
|
+
)
|
|
61
|
+
task_id: str | None = Field(
|
|
62
|
+
None, description="Unique task ID for parallel tracking"
|
|
63
|
+
)
|
|
64
|
+
task_status: str | None = Field(
|
|
65
|
+
None,
|
|
66
|
+
description="Task status: 'pending', 'running', 'completed', 'failed', 'timeout'",
|
|
67
|
+
)
|
|
68
|
+
model_selected: str | None = Field(
|
|
69
|
+
None, description="Specific model (e.g., 'gemini-2.0-flash')"
|
|
70
|
+
)
|
|
71
|
+
complexity_level: str | None = Field(
|
|
72
|
+
None, description="Complexity: 'low', 'medium', 'high', 'very-high'"
|
|
73
|
+
)
|
|
74
|
+
budget_mode: str | None = Field(
|
|
75
|
+
None, description="Budget mode: 'free', 'balanced', 'performance'"
|
|
76
|
+
)
|
|
77
|
+
execution_duration_seconds: float | None = Field(
|
|
78
|
+
None, description="Delegation execution time"
|
|
79
|
+
)
|
|
80
|
+
tokens_estimated: int | None = Field(None, description="Estimated token usage")
|
|
81
|
+
tokens_actual: int | None = Field(None, description="Actual token usage")
|
|
82
|
+
cost_usd: float | None = Field(None, description="Calculated cost in USD")
|
|
83
|
+
task_findings: str | None = Field(None, description="Results from delegated task")
|
|
84
|
+
|
|
85
|
+
@field_validator("event_id", "session_id")
|
|
86
|
+
@classmethod
|
|
87
|
+
def validate_non_empty_string(cls, v: str) -> str:
|
|
88
|
+
"""Ensure event_id and session_id are non-empty."""
|
|
89
|
+
if not v or not v.strip():
|
|
90
|
+
raise ValueError("Field must be a non-empty string")
|
|
91
|
+
return v
|
|
92
|
+
|
|
93
|
+
@field_serializer("timestamp")
|
|
94
|
+
def serialize_timestamp(self, timestamp: datetime) -> str:
|
|
95
|
+
"""Serialize timestamp to ISO format string."""
|
|
96
|
+
return timestamp.isoformat()
|
|
97
|
+
|
|
98
|
+
@field_serializer("file_paths")
|
|
99
|
+
def serialize_file_paths(self, file_paths: list[str] | None) -> list[str]:
|
|
100
|
+
"""Ensure file_paths is always a list (never None) in JSON output."""
|
|
101
|
+
return file_paths or []
|
|
41
102
|
|
|
42
103
|
def to_json(self) -> dict[str, Any]:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"timestamp": self.timestamp.isoformat(),
|
|
46
|
-
"session_id": self.session_id,
|
|
47
|
-
"agent": self.agent,
|
|
48
|
-
"tool": self.tool,
|
|
49
|
-
"summary": self.summary,
|
|
50
|
-
"success": self.success,
|
|
51
|
-
"feature_id": self.feature_id,
|
|
52
|
-
"work_type": self.work_type,
|
|
53
|
-
"drift_score": self.drift_score,
|
|
54
|
-
"start_commit": self.start_commit,
|
|
55
|
-
"continued_from": self.continued_from,
|
|
56
|
-
"session_status": self.session_status,
|
|
57
|
-
"file_paths": self.file_paths or [],
|
|
58
|
-
"payload": self.payload,
|
|
59
|
-
}
|
|
104
|
+
"""Convert EventRecord to JSON-serializable dictionary."""
|
|
105
|
+
return self.model_dump(mode="json")
|
|
60
106
|
|
|
61
107
|
|
|
62
108
|
class JsonlEventLog:
|
|
@@ -74,7 +120,10 @@ class JsonlEventLog:
|
|
|
74
120
|
|
|
75
121
|
def append(self, record: EventRecord) -> Path:
|
|
76
122
|
path = self.path_for_session(record.session_id)
|
|
77
|
-
line =
|
|
123
|
+
line = (
|
|
124
|
+
json.dumps(record.model_dump(mode="json"), ensure_ascii=False, default=str)
|
|
125
|
+
+ "\n"
|
|
126
|
+
)
|
|
78
127
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
128
|
|
|
80
129
|
# Best-effort dedupe: some producers (e.g. git hooks) may retry or be chained.
|