empathy-framework 3.7.0__py3-none-any.whl → 3.7.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 (267) hide show
  1. coach_wizards/code_reviewer_README.md +60 -0
  2. coach_wizards/code_reviewer_wizard.py +180 -0
  3. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/METADATA +20 -2
  4. empathy_framework-3.7.1.dist-info/RECORD +327 -0
  5. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/top_level.txt +5 -1
  6. empathy_healthcare_plugin/monitors/__init__.py +9 -0
  7. empathy_healthcare_plugin/monitors/clinical_protocol_monitor.py +315 -0
  8. empathy_healthcare_plugin/monitors/monitoring/__init__.py +44 -0
  9. empathy_healthcare_plugin/monitors/monitoring/protocol_checker.py +300 -0
  10. empathy_healthcare_plugin/monitors/monitoring/protocol_loader.py +214 -0
  11. empathy_healthcare_plugin/monitors/monitoring/sensor_parsers.py +306 -0
  12. empathy_healthcare_plugin/monitors/monitoring/trajectory_analyzer.py +389 -0
  13. empathy_llm_toolkit/agent_factory/__init__.py +53 -0
  14. empathy_llm_toolkit/agent_factory/adapters/__init__.py +85 -0
  15. empathy_llm_toolkit/agent_factory/adapters/autogen_adapter.py +312 -0
  16. empathy_llm_toolkit/agent_factory/adapters/crewai_adapter.py +454 -0
  17. empathy_llm_toolkit/agent_factory/adapters/haystack_adapter.py +298 -0
  18. empathy_llm_toolkit/agent_factory/adapters/langchain_adapter.py +362 -0
  19. empathy_llm_toolkit/agent_factory/adapters/langgraph_adapter.py +333 -0
  20. empathy_llm_toolkit/agent_factory/adapters/native.py +228 -0
  21. empathy_llm_toolkit/agent_factory/adapters/wizard_adapter.py +426 -0
  22. empathy_llm_toolkit/agent_factory/base.py +305 -0
  23. empathy_llm_toolkit/agent_factory/crews/__init__.py +67 -0
  24. empathy_llm_toolkit/agent_factory/crews/code_review.py +1113 -0
  25. empathy_llm_toolkit/agent_factory/crews/health_check.py +1246 -0
  26. empathy_llm_toolkit/agent_factory/crews/refactoring.py +1128 -0
  27. empathy_llm_toolkit/agent_factory/crews/security_audit.py +1018 -0
  28. empathy_llm_toolkit/agent_factory/decorators.py +286 -0
  29. empathy_llm_toolkit/agent_factory/factory.py +558 -0
  30. empathy_llm_toolkit/agent_factory/framework.py +192 -0
  31. empathy_llm_toolkit/agent_factory/memory_integration.py +324 -0
  32. empathy_llm_toolkit/agent_factory/resilient.py +320 -0
  33. empathy_llm_toolkit/cli/__init__.py +8 -0
  34. empathy_llm_toolkit/cli/sync_claude.py +487 -0
  35. empathy_llm_toolkit/code_health.py +150 -3
  36. empathy_llm_toolkit/config/__init__.py +29 -0
  37. empathy_llm_toolkit/config/unified.py +295 -0
  38. empathy_llm_toolkit/routing/__init__.py +32 -0
  39. empathy_llm_toolkit/routing/model_router.py +362 -0
  40. empathy_llm_toolkit/security/IMPLEMENTATION_SUMMARY.md +413 -0
  41. empathy_llm_toolkit/security/PHASE2_COMPLETE.md +384 -0
  42. empathy_llm_toolkit/security/PHASE2_SECRETS_DETECTOR_COMPLETE.md +271 -0
  43. empathy_llm_toolkit/security/QUICK_REFERENCE.md +316 -0
  44. empathy_llm_toolkit/security/README.md +262 -0
  45. empathy_llm_toolkit/security/__init__.py +62 -0
  46. empathy_llm_toolkit/security/audit_logger.py +929 -0
  47. empathy_llm_toolkit/security/audit_logger_example.py +152 -0
  48. empathy_llm_toolkit/security/pii_scrubber.py +640 -0
  49. empathy_llm_toolkit/security/secrets_detector.py +678 -0
  50. empathy_llm_toolkit/security/secrets_detector_example.py +304 -0
  51. empathy_llm_toolkit/security/secure_memdocs.py +1192 -0
  52. empathy_llm_toolkit/security/secure_memdocs_example.py +278 -0
  53. empathy_llm_toolkit/wizards/__init__.py +38 -0
  54. empathy_llm_toolkit/wizards/base_wizard.py +364 -0
  55. empathy_llm_toolkit/wizards/customer_support_wizard.py +190 -0
  56. empathy_llm_toolkit/wizards/healthcare_wizard.py +362 -0
  57. empathy_llm_toolkit/wizards/patient_assessment_README.md +64 -0
  58. empathy_llm_toolkit/wizards/patient_assessment_wizard.py +193 -0
  59. empathy_llm_toolkit/wizards/technology_wizard.py +194 -0
  60. empathy_os/__init__.py +52 -52
  61. empathy_os/adaptive/__init__.py +13 -0
  62. empathy_os/adaptive/task_complexity.py +127 -0
  63. empathy_os/cli.py +118 -8
  64. empathy_os/cli_unified.py +121 -1
  65. empathy_os/config/__init__.py +63 -0
  66. empathy_os/config/xml_config.py +239 -0
  67. empathy_os/dashboard/__init__.py +15 -0
  68. empathy_os/dashboard/server.py +743 -0
  69. empathy_os/memory/__init__.py +195 -0
  70. empathy_os/memory/claude_memory.py +466 -0
  71. empathy_os/memory/config.py +224 -0
  72. empathy_os/memory/control_panel.py +1298 -0
  73. empathy_os/memory/edges.py +179 -0
  74. empathy_os/memory/graph.py +567 -0
  75. empathy_os/memory/long_term.py +1193 -0
  76. empathy_os/memory/nodes.py +179 -0
  77. empathy_os/memory/redis_bootstrap.py +540 -0
  78. empathy_os/memory/security/__init__.py +31 -0
  79. empathy_os/memory/security/audit_logger.py +930 -0
  80. empathy_os/memory/security/pii_scrubber.py +640 -0
  81. empathy_os/memory/security/secrets_detector.py +678 -0
  82. empathy_os/memory/short_term.py +2119 -0
  83. empathy_os/memory/storage/__init__.py +15 -0
  84. empathy_os/memory/summary_index.py +583 -0
  85. empathy_os/memory/unified.py +619 -0
  86. empathy_os/metrics/__init__.py +12 -0
  87. empathy_os/metrics/prompt_metrics.py +190 -0
  88. empathy_os/models/__init__.py +136 -0
  89. empathy_os/models/__main__.py +13 -0
  90. empathy_os/models/cli.py +655 -0
  91. empathy_os/models/empathy_executor.py +354 -0
  92. empathy_os/models/executor.py +252 -0
  93. empathy_os/models/fallback.py +671 -0
  94. empathy_os/models/provider_config.py +563 -0
  95. empathy_os/models/registry.py +382 -0
  96. empathy_os/models/tasks.py +302 -0
  97. empathy_os/models/telemetry.py +548 -0
  98. empathy_os/models/token_estimator.py +378 -0
  99. empathy_os/models/validation.py +274 -0
  100. empathy_os/monitoring/__init__.py +52 -0
  101. empathy_os/monitoring/alerts.py +23 -0
  102. empathy_os/monitoring/alerts_cli.py +268 -0
  103. empathy_os/monitoring/multi_backend.py +271 -0
  104. empathy_os/monitoring/otel_backend.py +363 -0
  105. empathy_os/optimization/__init__.py +19 -0
  106. empathy_os/optimization/context_optimizer.py +272 -0
  107. empathy_os/plugins/__init__.py +28 -0
  108. empathy_os/plugins/base.py +361 -0
  109. empathy_os/plugins/registry.py +268 -0
  110. empathy_os/project_index/__init__.py +30 -0
  111. empathy_os/project_index/cli.py +335 -0
  112. empathy_os/project_index/crew_integration.py +430 -0
  113. empathy_os/project_index/index.py +425 -0
  114. empathy_os/project_index/models.py +501 -0
  115. empathy_os/project_index/reports.py +473 -0
  116. empathy_os/project_index/scanner.py +538 -0
  117. empathy_os/prompts/__init__.py +61 -0
  118. empathy_os/prompts/config.py +77 -0
  119. empathy_os/prompts/context.py +177 -0
  120. empathy_os/prompts/parser.py +285 -0
  121. empathy_os/prompts/registry.py +313 -0
  122. empathy_os/prompts/templates.py +208 -0
  123. empathy_os/resilience/__init__.py +56 -0
  124. empathy_os/resilience/circuit_breaker.py +256 -0
  125. empathy_os/resilience/fallback.py +179 -0
  126. empathy_os/resilience/health.py +300 -0
  127. empathy_os/resilience/retry.py +209 -0
  128. empathy_os/resilience/timeout.py +135 -0
  129. empathy_os/routing/__init__.py +43 -0
  130. empathy_os/routing/chain_executor.py +433 -0
  131. empathy_os/routing/classifier.py +217 -0
  132. empathy_os/routing/smart_router.py +234 -0
  133. empathy_os/routing/wizard_registry.py +307 -0
  134. empathy_os/trust/__init__.py +28 -0
  135. empathy_os/trust/circuit_breaker.py +579 -0
  136. empathy_os/validation/__init__.py +19 -0
  137. empathy_os/validation/xml_validator.py +281 -0
  138. empathy_os/wizard_factory_cli.py +170 -0
  139. empathy_os/workflows/__init__.py +360 -0
  140. empathy_os/workflows/base.py +1530 -0
  141. empathy_os/workflows/bug_predict.py +962 -0
  142. empathy_os/workflows/code_review.py +960 -0
  143. empathy_os/workflows/code_review_adapters.py +310 -0
  144. empathy_os/workflows/code_review_pipeline.py +720 -0
  145. empathy_os/workflows/config.py +600 -0
  146. empathy_os/workflows/dependency_check.py +648 -0
  147. empathy_os/workflows/document_gen.py +1069 -0
  148. empathy_os/workflows/documentation_orchestrator.py +1205 -0
  149. empathy_os/workflows/health_check.py +679 -0
  150. empathy_os/workflows/keyboard_shortcuts/__init__.py +39 -0
  151. empathy_os/workflows/keyboard_shortcuts/generators.py +386 -0
  152. empathy_os/workflows/keyboard_shortcuts/parsers.py +414 -0
  153. empathy_os/workflows/keyboard_shortcuts/prompts.py +295 -0
  154. empathy_os/workflows/keyboard_shortcuts/schema.py +193 -0
  155. empathy_os/workflows/keyboard_shortcuts/workflow.py +505 -0
  156. empathy_os/workflows/manage_documentation.py +804 -0
  157. empathy_os/workflows/new_sample_workflow1.py +146 -0
  158. empathy_os/workflows/new_sample_workflow1_README.md +150 -0
  159. empathy_os/workflows/perf_audit.py +687 -0
  160. empathy_os/workflows/pr_review.py +748 -0
  161. empathy_os/workflows/progress.py +445 -0
  162. empathy_os/workflows/progress_server.py +322 -0
  163. empathy_os/workflows/refactor_plan.py +691 -0
  164. empathy_os/workflows/release_prep.py +808 -0
  165. empathy_os/workflows/research_synthesis.py +404 -0
  166. empathy_os/workflows/secure_release.py +585 -0
  167. empathy_os/workflows/security_adapters.py +297 -0
  168. empathy_os/workflows/security_audit.py +1050 -0
  169. empathy_os/workflows/step_config.py +234 -0
  170. empathy_os/workflows/test5.py +125 -0
  171. empathy_os/workflows/test5_README.md +158 -0
  172. empathy_os/workflows/test_gen.py +1855 -0
  173. empathy_os/workflows/test_lifecycle.py +526 -0
  174. empathy_os/workflows/test_maintenance.py +626 -0
  175. empathy_os/workflows/test_maintenance_cli.py +590 -0
  176. empathy_os/workflows/test_maintenance_crew.py +821 -0
  177. empathy_os/workflows/xml_enhanced_crew.py +285 -0
  178. empathy_software_plugin/cli/__init__.py +120 -0
  179. empathy_software_plugin/cli/inspect.py +362 -0
  180. empathy_software_plugin/cli.py +3 -1
  181. empathy_software_plugin/wizards/__init__.py +42 -0
  182. empathy_software_plugin/wizards/advanced_debugging_wizard.py +392 -0
  183. empathy_software_plugin/wizards/agent_orchestration_wizard.py +511 -0
  184. empathy_software_plugin/wizards/ai_collaboration_wizard.py +503 -0
  185. empathy_software_plugin/wizards/ai_context_wizard.py +441 -0
  186. empathy_software_plugin/wizards/ai_documentation_wizard.py +503 -0
  187. empathy_software_plugin/wizards/base_wizard.py +288 -0
  188. empathy_software_plugin/wizards/book_chapter_wizard.py +519 -0
  189. empathy_software_plugin/wizards/code_review_wizard.py +606 -0
  190. empathy_software_plugin/wizards/debugging/__init__.py +50 -0
  191. empathy_software_plugin/wizards/debugging/bug_risk_analyzer.py +414 -0
  192. empathy_software_plugin/wizards/debugging/config_loaders.py +442 -0
  193. empathy_software_plugin/wizards/debugging/fix_applier.py +469 -0
  194. empathy_software_plugin/wizards/debugging/language_patterns.py +383 -0
  195. empathy_software_plugin/wizards/debugging/linter_parsers.py +470 -0
  196. empathy_software_plugin/wizards/debugging/verification.py +369 -0
  197. empathy_software_plugin/wizards/enhanced_testing_wizard.py +537 -0
  198. empathy_software_plugin/wizards/memory_enhanced_debugging_wizard.py +816 -0
  199. empathy_software_plugin/wizards/multi_model_wizard.py +501 -0
  200. empathy_software_plugin/wizards/pattern_extraction_wizard.py +422 -0
  201. empathy_software_plugin/wizards/pattern_retriever_wizard.py +400 -0
  202. empathy_software_plugin/wizards/performance/__init__.py +9 -0
  203. empathy_software_plugin/wizards/performance/bottleneck_detector.py +221 -0
  204. empathy_software_plugin/wizards/performance/profiler_parsers.py +278 -0
  205. empathy_software_plugin/wizards/performance/trajectory_analyzer.py +429 -0
  206. empathy_software_plugin/wizards/performance_profiling_wizard.py +305 -0
  207. empathy_software_plugin/wizards/prompt_engineering_wizard.py +425 -0
  208. empathy_software_plugin/wizards/rag_pattern_wizard.py +461 -0
  209. empathy_software_plugin/wizards/security/__init__.py +32 -0
  210. empathy_software_plugin/wizards/security/exploit_analyzer.py +290 -0
  211. empathy_software_plugin/wizards/security/owasp_patterns.py +241 -0
  212. empathy_software_plugin/wizards/security/vulnerability_scanner.py +604 -0
  213. empathy_software_plugin/wizards/security_analysis_wizard.py +322 -0
  214. empathy_software_plugin/wizards/security_learning_wizard.py +740 -0
  215. empathy_software_plugin/wizards/tech_debt_wizard.py +726 -0
  216. empathy_software_plugin/wizards/testing/__init__.py +27 -0
  217. empathy_software_plugin/wizards/testing/coverage_analyzer.py +459 -0
  218. empathy_software_plugin/wizards/testing/quality_analyzer.py +531 -0
  219. empathy_software_plugin/wizards/testing/test_suggester.py +533 -0
  220. empathy_software_plugin/wizards/testing_wizard.py +274 -0
  221. hot_reload/README.md +473 -0
  222. hot_reload/__init__.py +62 -0
  223. hot_reload/config.py +84 -0
  224. hot_reload/integration.py +228 -0
  225. hot_reload/reloader.py +298 -0
  226. hot_reload/watcher.py +179 -0
  227. hot_reload/websocket.py +176 -0
  228. scaffolding/README.md +589 -0
  229. scaffolding/__init__.py +35 -0
  230. scaffolding/__main__.py +14 -0
  231. scaffolding/cli.py +240 -0
  232. test_generator/__init__.py +38 -0
  233. test_generator/__main__.py +14 -0
  234. test_generator/cli.py +226 -0
  235. test_generator/generator.py +325 -0
  236. test_generator/risk_analyzer.py +216 -0
  237. workflow_patterns/__init__.py +33 -0
  238. workflow_patterns/behavior.py +249 -0
  239. workflow_patterns/core.py +76 -0
  240. workflow_patterns/output.py +99 -0
  241. workflow_patterns/registry.py +255 -0
  242. workflow_patterns/structural.py +288 -0
  243. workflow_scaffolding/__init__.py +11 -0
  244. workflow_scaffolding/__main__.py +12 -0
  245. workflow_scaffolding/cli.py +206 -0
  246. workflow_scaffolding/generator.py +265 -0
  247. agents/code_inspection/patterns/inspection/recurring_B112.json +0 -18
  248. agents/code_inspection/patterns/inspection/recurring_F541.json +0 -16
  249. agents/code_inspection/patterns/inspection/recurring_FORMAT.json +0 -25
  250. agents/code_inspection/patterns/inspection/recurring_bug_20250822_def456.json +0 -16
  251. agents/code_inspection/patterns/inspection/recurring_bug_20250915_abc123.json +0 -16
  252. agents/code_inspection/patterns/inspection/recurring_bug_20251212_3c5b9951.json +0 -16
  253. agents/code_inspection/patterns/inspection/recurring_bug_20251212_97c0f72f.json +0 -16
  254. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a0871d53.json +0 -16
  255. agents/code_inspection/patterns/inspection/recurring_bug_20251212_a9b6ec41.json +0 -16
  256. agents/code_inspection/patterns/inspection/recurring_bug_null_001.json +0 -16
  257. agents/code_inspection/patterns/inspection/recurring_builtin.json +0 -16
  258. agents/compliance_anticipation_agent.py +0 -1422
  259. agents/compliance_db.py +0 -339
  260. agents/epic_integration_wizard.py +0 -530
  261. agents/notifications.py +0 -291
  262. agents/trust_building_behaviors.py +0 -872
  263. empathy_framework-3.7.0.dist-info/RECORD +0 -105
  264. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/WHEEL +0 -0
  265. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/entry_points.txt +0 -0
  266. {empathy_framework-3.7.0.dist-info → empathy_framework-3.7.1.dist-info}/licenses/LICENSE +0 -0
  267. /empathy_os/{monitoring.py → agent_monitoring.py} +0 -0
@@ -0,0 +1,445 @@
1
+ """Progress Tracking System
2
+
3
+ Real-time progress tracking for workflow execution with WebSocket support.
4
+ Enables live UI updates during workflow runs.
5
+
6
+ Copyright 2025 Smart AI Memory, LLC
7
+ Licensed under Fair Source 0.9
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ from collections.abc import Callable, Coroutine
15
+ from dataclasses import dataclass, field
16
+ from datetime import datetime
17
+ from enum import Enum
18
+ from typing import Any, Protocol
19
+
20
+
21
+ class ProgressStatus(Enum):
22
+ """Status of a workflow or stage."""
23
+
24
+ PENDING = "pending"
25
+ RUNNING = "running"
26
+ COMPLETED = "completed"
27
+ FAILED = "failed"
28
+ SKIPPED = "skipped"
29
+ FALLBACK = "fallback" # Using fallback model
30
+ RETRYING = "retrying" # Retrying after error
31
+
32
+
33
+ @dataclass
34
+ class StageProgress:
35
+ """Progress information for a single stage."""
36
+
37
+ name: str
38
+ status: ProgressStatus
39
+ tier: str = "capable"
40
+ model: str = ""
41
+ started_at: datetime | None = None
42
+ completed_at: datetime | None = None
43
+ duration_ms: int = 0
44
+ cost: float = 0.0
45
+ tokens_in: int = 0
46
+ tokens_out: int = 0
47
+ error: str | None = None
48
+ fallback_info: str | None = None
49
+ retry_count: int = 0
50
+
51
+ def to_dict(self) -> dict[str, Any]:
52
+ """Convert to dictionary for JSON serialization."""
53
+ return {
54
+ "name": self.name,
55
+ "status": self.status.value,
56
+ "tier": self.tier,
57
+ "model": self.model,
58
+ "started_at": self.started_at.isoformat() if self.started_at else None,
59
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None,
60
+ "duration_ms": self.duration_ms,
61
+ "cost": self.cost,
62
+ "tokens_in": self.tokens_in,
63
+ "tokens_out": self.tokens_out,
64
+ "error": self.error,
65
+ "fallback_info": self.fallback_info,
66
+ "retry_count": self.retry_count,
67
+ }
68
+
69
+
70
+ @dataclass
71
+ class ProgressUpdate:
72
+ """A progress update to be broadcast."""
73
+
74
+ workflow: str
75
+ workflow_id: str
76
+ current_stage: str
77
+ stage_index: int
78
+ total_stages: int
79
+ status: ProgressStatus
80
+ message: str
81
+ cost_so_far: float = 0.0
82
+ tokens_so_far: int = 0
83
+ percent_complete: float = 0.0
84
+ estimated_remaining_ms: int | None = None
85
+ stages: list[StageProgress] = field(default_factory=list)
86
+ fallback_info: str | None = None
87
+ error: str | None = None
88
+ timestamp: datetime = field(default_factory=datetime.now)
89
+
90
+ def to_dict(self) -> dict[str, Any]:
91
+ """Convert to dictionary for JSON serialization."""
92
+ return {
93
+ "workflow": self.workflow,
94
+ "workflow_id": self.workflow_id,
95
+ "current_stage": self.current_stage,
96
+ "stage_index": self.stage_index,
97
+ "total_stages": self.total_stages,
98
+ "status": self.status.value,
99
+ "message": self.message,
100
+ "cost_so_far": self.cost_so_far,
101
+ "tokens_so_far": self.tokens_so_far,
102
+ "percent_complete": self.percent_complete,
103
+ "estimated_remaining_ms": self.estimated_remaining_ms,
104
+ "stages": [s.to_dict() for s in self.stages],
105
+ "fallback_info": self.fallback_info,
106
+ "error": self.error,
107
+ "timestamp": self.timestamp.isoformat(),
108
+ }
109
+
110
+ def to_json(self) -> str:
111
+ """Convert to JSON string."""
112
+ return json.dumps(self.to_dict())
113
+
114
+
115
+ # Type for progress callbacks
116
+ ProgressCallback = Callable[[ProgressUpdate], None]
117
+ AsyncProgressCallback = Callable[[ProgressUpdate], Coroutine[Any, Any, None]]
118
+
119
+
120
+ class ProgressTracker:
121
+ """Tracks and broadcasts workflow progress.
122
+
123
+ Maintains state for all stages and emits updates to registered callbacks.
124
+ Supports both sync and async callbacks for flexibility.
125
+ """
126
+
127
+ def __init__(
128
+ self,
129
+ workflow_name: str,
130
+ workflow_id: str,
131
+ stage_names: list[str],
132
+ ):
133
+ self.workflow = workflow_name
134
+ self.workflow_id = workflow_id
135
+ self.stage_names = stage_names
136
+ self.current_index = 0
137
+ self.cost_accumulated = 0.0
138
+ self.tokens_accumulated = 0
139
+ self._started_at = datetime.now()
140
+ self._stage_start_times: dict[str, datetime] = {}
141
+ self._stage_durations: list[int] = []
142
+
143
+ # Initialize stages
144
+ self.stages: list[StageProgress] = [
145
+ StageProgress(name=name, status=ProgressStatus.PENDING) for name in stage_names
146
+ ]
147
+
148
+ # Callbacks
149
+ self._callbacks: list[ProgressCallback] = []
150
+ self._async_callbacks: list[AsyncProgressCallback] = []
151
+
152
+ def add_callback(self, callback: ProgressCallback) -> None:
153
+ """Add a synchronous progress callback."""
154
+ self._callbacks.append(callback)
155
+
156
+ def add_async_callback(self, callback: AsyncProgressCallback) -> None:
157
+ """Add an asynchronous progress callback."""
158
+ self._async_callbacks.append(callback)
159
+
160
+ def remove_callback(self, callback: ProgressCallback) -> None:
161
+ """Remove a synchronous callback."""
162
+ if callback in self._callbacks:
163
+ self._callbacks.remove(callback)
164
+
165
+ def start_workflow(self) -> None:
166
+ """Mark workflow as started."""
167
+ self._started_at = datetime.now()
168
+ self._emit(ProgressStatus.RUNNING, f"Starting {self.workflow}...")
169
+
170
+ def start_stage(self, stage_name: str, tier: str = "capable", model: str = "") -> None:
171
+ """Mark a stage as started."""
172
+ stage = self._get_stage(stage_name)
173
+ if stage:
174
+ stage.status = ProgressStatus.RUNNING
175
+ stage.started_at = datetime.now()
176
+ stage.tier = tier
177
+ stage.model = model
178
+ self._stage_start_times[stage_name] = stage.started_at
179
+ self.current_index = self.stage_names.index(stage_name)
180
+
181
+ self._emit(ProgressStatus.RUNNING, f"Running {stage_name}...")
182
+
183
+ def complete_stage(
184
+ self,
185
+ stage_name: str,
186
+ cost: float = 0.0,
187
+ tokens_in: int = 0,
188
+ tokens_out: int = 0,
189
+ ) -> None:
190
+ """Mark a stage as completed."""
191
+ stage = self._get_stage(stage_name)
192
+ if stage:
193
+ stage.status = ProgressStatus.COMPLETED
194
+ stage.completed_at = datetime.now()
195
+ stage.cost = cost
196
+ stage.tokens_in = tokens_in
197
+ stage.tokens_out = tokens_out
198
+
199
+ if stage.started_at:
200
+ duration_ms = int((stage.completed_at - stage.started_at).total_seconds() * 1000)
201
+ stage.duration_ms = duration_ms
202
+ self._stage_durations.append(duration_ms)
203
+
204
+ self.cost_accumulated += cost
205
+ self.tokens_accumulated += tokens_in + tokens_out
206
+ self.current_index = self.stage_names.index(stage_name) + 1
207
+
208
+ self._emit(ProgressStatus.COMPLETED, f"Completed {stage_name}")
209
+
210
+ def fail_stage(self, stage_name: str, error: str) -> None:
211
+ """Mark a stage as failed."""
212
+ stage = self._get_stage(stage_name)
213
+ if stage:
214
+ stage.status = ProgressStatus.FAILED
215
+ stage.completed_at = datetime.now()
216
+ stage.error = error
217
+
218
+ if stage.started_at:
219
+ stage.duration_ms = int(
220
+ (stage.completed_at - stage.started_at).total_seconds() * 1000,
221
+ )
222
+
223
+ self._emit(ProgressStatus.FAILED, f"Failed: {stage_name}", error=error)
224
+
225
+ def skip_stage(self, stage_name: str, reason: str = "") -> None:
226
+ """Mark a stage as skipped."""
227
+ stage = self._get_stage(stage_name)
228
+ if stage:
229
+ stage.status = ProgressStatus.SKIPPED
230
+
231
+ message = f"Skipped {stage_name}"
232
+ if reason:
233
+ message += f": {reason}"
234
+ self._emit(ProgressStatus.SKIPPED, message)
235
+
236
+ def fallback_occurred(
237
+ self,
238
+ stage_name: str,
239
+ original_model: str,
240
+ fallback_model: str,
241
+ reason: str,
242
+ ) -> None:
243
+ """Record that a fallback occurred."""
244
+ stage = self._get_stage(stage_name)
245
+ fallback_info = f"{original_model} → {fallback_model} ({reason})"
246
+
247
+ if stage:
248
+ stage.status = ProgressStatus.FALLBACK
249
+ stage.fallback_info = fallback_info
250
+
251
+ self._emit(
252
+ ProgressStatus.FALLBACK,
253
+ f"Falling back from {original_model} to {fallback_model}",
254
+ fallback_info=fallback_info,
255
+ )
256
+
257
+ def retry_occurred(self, stage_name: str, attempt: int, max_attempts: int) -> None:
258
+ """Record that a retry is occurring."""
259
+ stage = self._get_stage(stage_name)
260
+ if stage:
261
+ stage.status = ProgressStatus.RETRYING
262
+ stage.retry_count = attempt
263
+
264
+ self._emit(
265
+ ProgressStatus.RETRYING,
266
+ f"Retrying {stage_name} (attempt {attempt}/{max_attempts})",
267
+ )
268
+
269
+ def complete_workflow(self) -> None:
270
+ """Mark workflow as completed."""
271
+ self._emit(
272
+ ProgressStatus.COMPLETED,
273
+ f"Workflow {self.workflow} completed",
274
+ )
275
+
276
+ def fail_workflow(self, error: str) -> None:
277
+ """Mark workflow as failed."""
278
+ self._emit(
279
+ ProgressStatus.FAILED,
280
+ f"Workflow {self.workflow} failed",
281
+ error=error,
282
+ )
283
+
284
+ def _get_stage(self, stage_name: str) -> StageProgress | None:
285
+ """Get stage by name."""
286
+ for stage in self.stages:
287
+ if stage.name == stage_name:
288
+ return stage
289
+ return None
290
+
291
+ def _calculate_percent_complete(self) -> float:
292
+ """Calculate completion percentage."""
293
+ completed = sum(1 for s in self.stages if s.status == ProgressStatus.COMPLETED)
294
+ return (completed / len(self.stages)) * 100 if self.stages else 0.0
295
+
296
+ def _estimate_remaining_ms(self) -> int | None:
297
+ """Estimate remaining time based on average stage duration."""
298
+ if not self._stage_durations:
299
+ return None
300
+
301
+ avg_duration = sum(self._stage_durations) / len(self._stage_durations)
302
+ remaining_stages = len(self.stages) - self.current_index
303
+ return int(avg_duration * remaining_stages)
304
+
305
+ def _emit(
306
+ self,
307
+ status: ProgressStatus,
308
+ message: str,
309
+ fallback_info: str | None = None,
310
+ error: str | None = None,
311
+ ) -> None:
312
+ """Emit a progress update to all callbacks."""
313
+ current_stage = (
314
+ self.stage_names[min(self.current_index, len(self.stage_names) - 1)]
315
+ if self.stage_names
316
+ else ""
317
+ )
318
+
319
+ update = ProgressUpdate(
320
+ workflow=self.workflow,
321
+ workflow_id=self.workflow_id,
322
+ current_stage=current_stage,
323
+ stage_index=self.current_index,
324
+ total_stages=len(self.stages),
325
+ status=status,
326
+ message=message,
327
+ cost_so_far=self.cost_accumulated,
328
+ tokens_so_far=self.tokens_accumulated,
329
+ percent_complete=self._calculate_percent_complete(),
330
+ estimated_remaining_ms=self._estimate_remaining_ms(),
331
+ stages=list(self.stages),
332
+ fallback_info=fallback_info,
333
+ error=error,
334
+ )
335
+
336
+ # Call sync callbacks
337
+ for callback in self._callbacks:
338
+ try:
339
+ callback(update)
340
+ except Exception as e:
341
+ # Log but don't fail on callback errors
342
+ print(f"Progress callback error: {e}")
343
+
344
+ # Call async callbacks
345
+ for async_callback in self._async_callbacks:
346
+ try:
347
+ asyncio.create_task(async_callback(update))
348
+ except RuntimeError:
349
+ # No event loop running, skip async callbacks
350
+ pass
351
+
352
+
353
+ class ProgressReporter(Protocol):
354
+ """Protocol for progress reporting implementations."""
355
+
356
+ def report(self, update: ProgressUpdate) -> None:
357
+ """Report a progress update."""
358
+ ...
359
+
360
+ async def report_async(self, update: ProgressUpdate) -> None:
361
+ """Report a progress update asynchronously."""
362
+ ...
363
+
364
+
365
+ class ConsoleProgressReporter:
366
+ """Simple console-based progress reporter for CLI usage."""
367
+
368
+ def __init__(self, verbose: bool = False):
369
+ self.verbose = verbose
370
+
371
+ def report(self, update: ProgressUpdate) -> None:
372
+ """Print progress to console."""
373
+ percent = f"{update.percent_complete:.0f}%"
374
+ cost = f"${update.cost_so_far:.4f}"
375
+ status_icon = {
376
+ ProgressStatus.PENDING: "○",
377
+ ProgressStatus.RUNNING: "◐",
378
+ ProgressStatus.COMPLETED: "●",
379
+ ProgressStatus.FAILED: "✗",
380
+ ProgressStatus.SKIPPED: "◌",
381
+ ProgressStatus.FALLBACK: "↩",
382
+ ProgressStatus.RETRYING: "↻",
383
+ }.get(update.status, "?")
384
+
385
+ print(f"[{percent}] {status_icon} {update.message} ({cost})")
386
+
387
+ if self.verbose and update.fallback_info:
388
+ print(f" Fallback: {update.fallback_info}")
389
+ if self.verbose and update.error:
390
+ print(f" Error: {update.error}")
391
+
392
+ async def report_async(self, update: ProgressUpdate) -> None:
393
+ """Async version just calls sync."""
394
+ self.report(update)
395
+
396
+
397
+ class JsonLinesProgressReporter:
398
+ """JSON Lines progress reporter for machine parsing."""
399
+
400
+ def __init__(self, output_file: str | None = None):
401
+ self.output_file = output_file
402
+
403
+ def report(self, update: ProgressUpdate) -> None:
404
+ """Output progress as JSON line."""
405
+ json_line = update.to_json()
406
+
407
+ if self.output_file:
408
+ with open(self.output_file, "a") as f:
409
+ f.write(json_line + "\n")
410
+ else:
411
+ print(json_line)
412
+
413
+ async def report_async(self, update: ProgressUpdate) -> None:
414
+ """Async version just calls sync."""
415
+ self.report(update)
416
+
417
+
418
+ def create_progress_tracker(
419
+ workflow_name: str,
420
+ stage_names: list[str],
421
+ reporter: ProgressReporter | None = None,
422
+ ) -> ProgressTracker:
423
+ """Factory function to create a progress tracker with optional reporter.
424
+
425
+ Args:
426
+ workflow_name: Name of the workflow
427
+ stage_names: List of stage names in order
428
+ reporter: Optional progress reporter
429
+
430
+ Returns:
431
+ Configured ProgressTracker instance
432
+
433
+ """
434
+ import uuid
435
+
436
+ tracker = ProgressTracker(
437
+ workflow_name=workflow_name,
438
+ workflow_id=uuid.uuid4().hex[:12],
439
+ stage_names=stage_names,
440
+ )
441
+
442
+ if reporter:
443
+ tracker.add_callback(reporter.report)
444
+
445
+ return tracker