codeframe-ai 0.9.0__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 (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1026 @@
1
+ """Core data models for CodeFRAME."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from enum import Enum
6
+ from typing import List, Optional, Dict, Any, Literal, Union
7
+ from pydantic import BaseModel, Field, ConfigDict, computed_field, field_validator
8
+
9
+
10
+ class TaskStatus(Enum):
11
+ """Task execution status."""
12
+
13
+ PENDING = "pending"
14
+ ASSIGNED = "assigned"
15
+ IN_PROGRESS = "in_progress"
16
+ BLOCKED = "blocked"
17
+ COMPLETED = "completed"
18
+ FAILED = "failed"
19
+
20
+
21
+ # Valid task status values for API validation and database constraints
22
+ VALID_TASK_STATUSES: frozenset[str] = frozenset(s.value for s in TaskStatus)
23
+
24
+
25
+ class AgentMaturity(Enum):
26
+ """Agent maturity levels based on Situational Leadership II."""
27
+
28
+ D1 = "directive" # Low skill, needs step-by-step
29
+ D2 = "coaching" # Some skill, needs guidance
30
+ D3 = "supporting" # High skill, needs autonomy
31
+ D4 = "delegating" # Expert, full ownership
32
+
33
+
34
+ class ProjectStatus(Enum):
35
+ """Project lifecycle status."""
36
+
37
+ INIT = "init"
38
+ PLANNING = "planning"
39
+ RUNNING = "running" # cf-10: Agent actively working on project
40
+ ACTIVE = "active"
41
+ PAUSED = "paused"
42
+ STOPPED = "stopped" # Agent terminated, project not active
43
+ COMPLETED = "completed"
44
+
45
+
46
+ class ProjectPhase(Enum):
47
+ """Project workflow phase."""
48
+
49
+ DISCOVERY = "discovery"
50
+ PLANNING = "planning"
51
+ ACTIVE = "active"
52
+ REVIEW = "review"
53
+ COMPLETE = "complete"
54
+
55
+
56
+ class SourceType(Enum):
57
+ """Project source type."""
58
+
59
+ GIT_REMOTE = "git_remote"
60
+ LOCAL_PATH = "local_path"
61
+ UPLOAD = "upload"
62
+ EMPTY = "empty"
63
+
64
+
65
+ @dataclass
66
+ class Project:
67
+ """Represents a CodeFRAME project.
68
+
69
+ Projects are the top-level container for issues and tasks. Each project
70
+ has a managed workspace, optional source tracking, and workflow state.
71
+ """
72
+
73
+ id: Optional[int] = None
74
+ name: str = ""
75
+ description: str = ""
76
+ source_type: SourceType = SourceType.EMPTY
77
+ source_location: Optional[str] = None
78
+ source_branch: str = "main"
79
+ workspace_path: str = ""
80
+ git_initialized: bool = False
81
+ current_commit: Optional[str] = None
82
+ status: ProjectStatus = ProjectStatus.INIT
83
+ phase: ProjectPhase = ProjectPhase.DISCOVERY
84
+ created_at: datetime = field(default_factory=datetime.now)
85
+ paused_at: Optional[datetime] = None
86
+ config: Optional[Dict[str, Any]] = None
87
+
88
+ def to_dict(self) -> Dict[str, Any]:
89
+ """Convert Project to dictionary for JSON serialization."""
90
+ return {
91
+ "id": self.id,
92
+ "name": self.name,
93
+ "description": self.description,
94
+ "source_type": self.source_type.value,
95
+ "source_location": self.source_location,
96
+ "source_branch": self.source_branch,
97
+ "workspace_path": self.workspace_path,
98
+ "git_initialized": self.git_initialized,
99
+ "current_commit": self.current_commit,
100
+ "status": self.status.value,
101
+ "phase": self.phase.value,
102
+ "created_at": self.created_at.isoformat() if self.created_at else None,
103
+ "paused_at": self.paused_at.isoformat() if self.paused_at else None,
104
+ "config": self.config,
105
+ }
106
+
107
+
108
+ class BlockerSeverity(Enum):
109
+ """Blocker severity for escalation (deprecated, use BlockerType)."""
110
+
111
+ SYNC = "sync" # Urgent, needs immediate response
112
+ ASYNC = "async" # Can wait, stack for later
113
+
114
+
115
+ class BlockerType(str, Enum):
116
+ """Type of blocker requiring human intervention."""
117
+
118
+ SYNC = "SYNC" # Critical blocker - agent pauses immediately
119
+ ASYNC = "ASYNC" # Clarification request - agent continues work
120
+
121
+
122
+ class BlockerStatus(str, Enum):
123
+ """Current status of a blocker."""
124
+
125
+ PENDING = "PENDING" # Awaiting user response
126
+ RESOLVED = "RESOLVED" # User has provided answer
127
+ EXPIRED = "EXPIRED" # Blocker timed out (24h default)
128
+
129
+
130
+ class Severity(str, Enum):
131
+ """Code review finding severity levels (Sprint 10)."""
132
+
133
+ CRITICAL = "critical" # Must fix before completion
134
+ HIGH = "high" # Should fix before completion
135
+ MEDIUM = "medium" # Should fix eventually
136
+ LOW = "low" # Nice to fix
137
+ INFO = "info" # Informational only
138
+
139
+
140
+ class ReviewCategory(str, Enum):
141
+ """Code review finding categories (Sprint 10)."""
142
+
143
+ SECURITY = "security" # Security vulnerabilities
144
+ PERFORMANCE = "performance" # Performance issues
145
+ QUALITY = "quality" # Code quality problems
146
+ MAINTAINABILITY = "maintainability" # Hard to maintain code
147
+ STYLE = "style" # Style/formatting issues
148
+
149
+
150
+ class QualityGateType(str, Enum):
151
+ """Types of quality gates (Sprint 10)."""
152
+
153
+ BUILD = "build"
154
+ TESTS = "tests"
155
+ TYPE_CHECK = "type_check"
156
+ COVERAGE = "coverage"
157
+ CODE_REVIEW = "code_review"
158
+ LINTING = "linting"
159
+ SKIP_DETECTION = "skip_detection"
160
+
161
+
162
+ class CallType(str, Enum):
163
+ """Type of LLM call for categorization (Sprint 10)."""
164
+
165
+ TASK_EXECUTION = "task_execution"
166
+ CODE_REVIEW = "code_review"
167
+ COORDINATION = "coordination"
168
+ OTHER = "other"
169
+
170
+
171
+ @dataclass
172
+ class Issue:
173
+ """Represents a high-level work item that contains multiple tasks.
174
+
175
+ Issues are numbered hierarchically (e.g., "1.5", "2.3") and can parallelize
176
+ with other issues at the same level. Each issue contains sequential tasks.
177
+ """
178
+
179
+ id: Optional[int] = None
180
+ project_id: Optional[int] = None
181
+ issue_number: str = "" # e.g., "1.5" or "2.1"
182
+ title: str = ""
183
+ description: str = ""
184
+ status: TaskStatus = TaskStatus.PENDING
185
+ priority: int = 2 # 0-4, 0 = highest
186
+ workflow_step: int = 1
187
+ created_at: datetime = field(default_factory=datetime.now)
188
+ completed_at: Optional[datetime] = None
189
+
190
+ def to_dict(self) -> Dict[str, Any]:
191
+ """Convert Issue to dictionary for JSON serialization."""
192
+ return {
193
+ "id": self.id,
194
+ "project_id": self.project_id,
195
+ "issue_number": self.issue_number,
196
+ "title": self.title,
197
+ "description": self.description,
198
+ "status": self.status.value,
199
+ "priority": self.priority,
200
+ "workflow_step": self.workflow_step,
201
+ "created_at": self.created_at.isoformat() if self.created_at else None,
202
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None,
203
+ }
204
+
205
+
206
+ @dataclass
207
+ class IssueWithTaskCount:
208
+ """Issue with associated task count for summary views.
209
+
210
+ Uses composition to wrap an Issue and add task_count, reducing
211
+ field duplication and ensuring changes to Issue are automatically
212
+ reflected here.
213
+
214
+ Used by get_issue_with_task_counts() to return typed results
215
+ instead of raw dictionaries.
216
+ """
217
+
218
+ issue: Issue
219
+ task_count: int = 0
220
+
221
+ # Convenience accessors for common Issue fields
222
+ @property
223
+ def id(self) -> Optional[int]:
224
+ return self.issue.id
225
+
226
+ @property
227
+ def project_id(self) -> Optional[int]:
228
+ return self.issue.project_id
229
+
230
+ @property
231
+ def issue_number(self) -> str:
232
+ return self.issue.issue_number
233
+
234
+ @property
235
+ def title(self) -> str:
236
+ return self.issue.title
237
+
238
+ @property
239
+ def status(self) -> TaskStatus:
240
+ return self.issue.status
241
+
242
+ def to_dict(self) -> Dict[str, Any]:
243
+ """Convert to dictionary for JSON serialization."""
244
+ result = self.issue.to_dict()
245
+ result["task_count"] = self.task_count
246
+ return result
247
+
248
+
249
+ @dataclass
250
+ class Task:
251
+ """Represents an atomic development task within an issue.
252
+
253
+ Tasks are numbered hierarchically (e.g., "1.5.1", "1.5.2") and are
254
+ always sequential within their parent issue (cannot parallelize).
255
+ Each task depends on the previous task in the sequence.
256
+
257
+ Effort estimation fields support project planning and scheduling:
258
+ - estimated_hours: Time estimate in hours for task completion
259
+ - complexity_score: Task complexity rating (1-5 scale)
260
+ - uncertainty_level: Confidence in estimate ("low", "medium", "high")
261
+ - resource_requirements: JSON string of required skills/tools
262
+
263
+ Supervisor intervention context (set when retrying after tactical pattern match):
264
+ - intervention_context: Dict with structure:
265
+ {
266
+ "intervention_applied": bool, # True if supervisor applied intervention
267
+ "pattern_matched": str, # ID of matched tactical pattern
268
+ "existing_files": List[str], # Files that already exist
269
+ "instruction": str, # Guidance for agent on retry
270
+ "strategy": str, # InterventionStrategy value
271
+ }
272
+ """
273
+
274
+ id: Optional[int] = None
275
+ project_id: Optional[int] = None
276
+ issue_id: Optional[int] = None # Foreign key to parent issue
277
+ task_number: str = "" # e.g., "1.5.3"
278
+ parent_issue_number: str = "" # e.g., "1.5"
279
+ title: str = ""
280
+ description: str = ""
281
+ status: TaskStatus = TaskStatus.PENDING
282
+ assigned_to: Optional[str] = None
283
+ depends_on: str = "" # Previous task number (e.g., "1.5.2")
284
+ can_parallelize: bool = False # Always FALSE within issue
285
+ priority: int = 2 # 0-4, 0 = highest (inherited from issue)
286
+ workflow_step: int = 1 # Maps to 15-step workflow
287
+ requires_mcp: bool = False
288
+ estimated_tokens: int = 0
289
+ actual_tokens: Optional[int] = None
290
+ # Effort estimation fields
291
+ estimated_hours: Optional[float] = None # Time estimate in hours
292
+ complexity_score: Optional[int] = None # Complexity rating (1-5)
293
+ uncertainty_level: Optional[str] = None # "low", "medium", "high"
294
+ resource_requirements: Optional[str] = None # JSON string of required skills/tools
295
+ # Supervisor intervention context (populated on retry after tactical pattern match)
296
+ intervention_context: Optional[Dict[str, Any]] = None
297
+ created_at: datetime = field(default_factory=datetime.now)
298
+ completed_at: Optional[datetime] = None
299
+
300
+ def to_dict(self) -> Dict[str, Any]:
301
+ """Convert Task to dictionary for JSON serialization."""
302
+ return {
303
+ "id": self.id,
304
+ "project_id": self.project_id,
305
+ "issue_id": self.issue_id,
306
+ "task_number": self.task_number,
307
+ "parent_issue_number": self.parent_issue_number,
308
+ "title": self.title,
309
+ "description": self.description,
310
+ "status": self.status.value,
311
+ "assigned_to": self.assigned_to,
312
+ "depends_on": self.depends_on,
313
+ "can_parallelize": self.can_parallelize,
314
+ "priority": self.priority,
315
+ "workflow_step": self.workflow_step,
316
+ "requires_mcp": self.requires_mcp,
317
+ "estimated_tokens": self.estimated_tokens,
318
+ "actual_tokens": self.actual_tokens,
319
+ "estimated_hours": self.estimated_hours,
320
+ "complexity_score": self.complexity_score,
321
+ "uncertainty_level": self.uncertainty_level,
322
+ "resource_requirements": self.resource_requirements,
323
+ "intervention_context": self.intervention_context,
324
+ "created_at": self.created_at.isoformat() if self.created_at else None,
325
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None,
326
+ }
327
+
328
+
329
+ @dataclass
330
+ class Blocker:
331
+ """Represents a task blocker requiring human input."""
332
+
333
+ id: int
334
+ task_id: int
335
+ severity: BlockerSeverity
336
+ reason: str
337
+ question: str
338
+ resolution: Optional[str] = None
339
+ created_at: datetime = field(default_factory=datetime.now)
340
+ resolved_at: Optional[datetime] = None
341
+
342
+
343
+ @dataclass
344
+ class AgentMetrics:
345
+ """Performance metrics for agent maturity assessment."""
346
+
347
+ task_success_rate: float
348
+ blocker_frequency: float
349
+ test_pass_rate: float
350
+ rework_rate: float
351
+ context_efficiency: float # tokens per task
352
+
353
+
354
+ @dataclass
355
+ class Notification:
356
+ """Multi-channel notification."""
357
+
358
+ id: str
359
+ project_id: int
360
+ severity: BlockerSeverity
361
+ title: str
362
+ message: str
363
+ blocker_id: Optional[int] = None
364
+ action_required: str = ""
365
+ channels: List[str] = field(default_factory=list)
366
+ sent_at: Optional[datetime] = None
367
+ acknowledged_at: Optional[datetime] = None
368
+
369
+
370
+ # Pydantic models for API validation and serialization
371
+
372
+
373
+ class BlockerModel(BaseModel):
374
+ """Pydantic model for blocker database records."""
375
+
376
+ model_config = ConfigDict(from_attributes=True, use_enum_values=True)
377
+
378
+ id: int
379
+ agent_id: str
380
+ task_id: Optional[int] = None
381
+ blocker_type: BlockerType
382
+ question: str = Field(..., max_length=2000)
383
+ answer: Optional[str] = Field(None, max_length=5000)
384
+ status: BlockerStatus
385
+ created_at: datetime
386
+ resolved_at: Optional[datetime] = None
387
+
388
+
389
+ class BlockerCreate(BaseModel):
390
+ """Request model for creating a blocker."""
391
+
392
+ agent_id: str
393
+ task_id: Optional[int] = None
394
+ blocker_type: BlockerType = BlockerType.ASYNC
395
+ question: str = Field(..., min_length=1, max_length=2000)
396
+
397
+
398
+ class BlockerResolve(BaseModel):
399
+ """Request model for resolving a blocker."""
400
+
401
+ answer: str = Field(..., min_length=1, max_length=5000)
402
+
403
+ @field_validator("answer")
404
+ @classmethod
405
+ def validate_answer_not_whitespace(cls, v: str) -> str:
406
+ """Validate that answer is not empty or whitespace-only."""
407
+ if not v.strip():
408
+ raise ValueError("Answer cannot be empty or whitespace-only")
409
+ return v
410
+
411
+
412
+ class BlockerListResponse(BaseModel):
413
+ """Response model for listing blockers."""
414
+
415
+ blockers: List[BlockerModel]
416
+ total: int
417
+ pending_count: int
418
+ sync_count: int
419
+ async_count: int = 0
420
+
421
+
422
+ # Sprint 9: MVP Completion Models
423
+
424
+
425
+ class LintResult(BaseModel):
426
+ """Lint execution result.
427
+
428
+ Stores results from linting tools (ruff, eslint) for tracking quality trends
429
+ over time and enforcing quality gates.
430
+
431
+ Attributes:
432
+ id: Database ID (auto-generated)
433
+ task_id: Task that was linted
434
+ linter: Tool used (ruff, eslint, other)
435
+ error_count: Number of critical errors (block task completion)
436
+ warning_count: Number of non-blocking warnings
437
+ files_linted: Number of files checked
438
+ output: Full lint output (JSON or text)
439
+ created_at: When lint was executed
440
+ """
441
+
442
+ id: Optional[int] = None
443
+ task_id: Optional[int] = Field(None, description="Task ID (optional for in-memory results)")
444
+ linter: Literal["ruff", "eslint", "other"] = Field(..., description="Linter tool")
445
+ error_count: int = Field(0, ge=0, description="Number of errors")
446
+ warning_count: int = Field(0, ge=0, description="Number of warnings")
447
+ files_linted: int = Field(0, ge=0, description="Number of files checked")
448
+ output: Optional[str] = Field(None, description="Full lint output (JSON or text)")
449
+ created_at: Optional[datetime] = None
450
+
451
+ @property
452
+ def has_critical_errors(self) -> bool:
453
+ """Check if lint found critical errors that block task."""
454
+ return self.error_count > 0
455
+
456
+ @property
457
+ def total_issues(self) -> int:
458
+ """Total issues (errors + warnings)."""
459
+ return self.error_count + self.warning_count
460
+
461
+ def to_dict(self) -> Dict[str, Any]:
462
+ """Convert to dictionary for database storage."""
463
+ return {
464
+ "task_id": self.task_id,
465
+ "linter": self.linter,
466
+ "error_count": self.error_count,
467
+ "warning_count": self.warning_count,
468
+ "files_linted": self.files_linted,
469
+ "output": self.output,
470
+ }
471
+
472
+
473
+ class ReviewFinding(BaseModel):
474
+ """Individual review finding.
475
+
476
+ Represents a single code quality issue found during code review
477
+ (complexity, security, style, or duplication).
478
+
479
+ Attributes:
480
+ category: Type of issue (complexity, security, style, duplication)
481
+ severity: Criticality level (critical, high, medium, low)
482
+ file_path: File containing the issue
483
+ line_number: Line number (optional)
484
+ message: Human-readable description
485
+ suggestion: Recommended fix (optional)
486
+ tool: Tool that detected issue (radon, bandit, etc.)
487
+ """
488
+
489
+ category: Literal["complexity", "security", "style", "duplication"] = Field(
490
+ ..., description="Finding category"
491
+ )
492
+ severity: Literal["critical", "high", "medium", "low"] = Field(
493
+ ..., description="Severity level"
494
+ )
495
+ file_path: str = Field(..., description="File with issue")
496
+ line_number: Optional[int] = Field(None, description="Line number (if applicable)")
497
+ message: str = Field(..., description="Human-readable description")
498
+ suggestion: Optional[str] = Field(None, description="Recommended fix")
499
+ tool: str = Field(..., description="Tool that detected issue (radon, bandit, etc.)")
500
+
501
+ def to_markdown(self) -> str:
502
+ """Format finding as markdown for blocker display."""
503
+ severity_emoji = {
504
+ "critical": "🔴",
505
+ "high": "🟠",
506
+ "medium": "🟡",
507
+ "low": "⚪",
508
+ }
509
+
510
+ location = f"{self.file_path}:{self.line_number}" if self.line_number else self.file_path
511
+
512
+ md = f"{severity_emoji[self.severity]} **{self.severity.upper()}** [{self.category}] {location}\n"
513
+ md += f" {self.message}\n"
514
+
515
+ if self.suggestion:
516
+ md += f" 💡 Suggestion: {self.suggestion}\n"
517
+
518
+ return md
519
+
520
+
521
+ class ReviewReport(BaseModel):
522
+ """Complete review report for a task.
523
+
524
+ Aggregates all review findings into a comprehensive quality assessment
525
+ with scoring and approve/reject decision.
526
+
527
+ Attributes:
528
+ task_id: Task that was reviewed
529
+ reviewer_agent_id: Agent that performed review
530
+ overall_score: Overall quality score (0-100)
531
+ complexity_score: Complexity subscore (0-100)
532
+ security_score: Security subscore (0-100)
533
+ style_score: Style subscore (0-100)
534
+ status: Review decision (approved, changes_requested, rejected)
535
+ findings: List of individual findings
536
+ summary: Human-readable summary
537
+ """
538
+
539
+ task_id: int
540
+ reviewer_agent_id: str
541
+ overall_score: float = Field(..., ge=0, le=100, description="Overall quality score (0-100)")
542
+ complexity_score: float = Field(..., ge=0, le=100)
543
+ security_score: float = Field(..., ge=0, le=100)
544
+ style_score: float = Field(..., ge=0, le=100)
545
+ status: Literal["approved", "changes_requested", "rejected"]
546
+ findings: List[ReviewFinding] = Field(default_factory=list)
547
+ summary: str = Field(..., description="Human-readable summary")
548
+
549
+ @property
550
+ def has_critical_findings(self) -> bool:
551
+ """Check if any findings are critical severity."""
552
+ return any(f.severity == "critical" for f in self.findings)
553
+
554
+ @property
555
+ def critical_count(self) -> int:
556
+ """Count critical findings."""
557
+ return sum(1 for f in self.findings if f.severity == "critical")
558
+
559
+ @property
560
+ def high_count(self) -> int:
561
+ """Count high severity findings."""
562
+ return sum(1 for f in self.findings if f.severity == "high")
563
+
564
+ def to_blocker_message(self) -> str:
565
+ """Format review report as blocker message."""
566
+ msg = f"## Code Review: {self.status.replace('_', ' ').title()}\n\n"
567
+ msg += f"**Overall Score**: {self.overall_score:.1f}/100\n\n"
568
+
569
+ if self.findings:
570
+ msg += f"**Findings**: {len(self.findings)} issues ({self.critical_count} critical, {self.high_count} high)\n\n"
571
+
572
+ # Group by severity
573
+ for severity in ["critical", "high", "medium", "low"]:
574
+ severity_findings = [f for f in self.findings if f.severity == severity]
575
+ if severity_findings:
576
+ msg += f"### {severity.upper()} Issues\n\n"
577
+ for finding in severity_findings:
578
+ msg += finding.to_markdown() + "\n"
579
+
580
+ msg += f"\n---\n\n{self.summary}"
581
+
582
+ return msg
583
+
584
+
585
+ # ============================================================================
586
+ # Discovery Answer UI Models (Feature: 012-discovery-answer-ui)
587
+ # ============================================================================
588
+
589
+
590
+ class DiscoveryAnswer(BaseModel):
591
+ """Request model for discovery answer submission."""
592
+
593
+ model_config = ConfigDict(str_strip_whitespace=True)
594
+
595
+ answer: str = Field(
596
+ ...,
597
+ min_length=1,
598
+ max_length=5000,
599
+ description="User's answer to the current discovery question",
600
+ )
601
+
602
+ @field_validator("answer")
603
+ @classmethod
604
+ def validate_answer(cls, v: str) -> str:
605
+ """Ensure answer is not empty after trimming."""
606
+ trimmed = v.strip()
607
+ if not trimmed:
608
+ raise ValueError("Answer cannot be empty or whitespace only")
609
+ if len(trimmed) > 5000:
610
+ raise ValueError("Answer cannot exceed 5000 characters")
611
+ return trimmed
612
+
613
+
614
+ class DiscoveryAnswerResponse(BaseModel):
615
+ """Response model for discovery answer submission."""
616
+
617
+ success: bool = Field(..., description="Whether the answer was successfully processed")
618
+
619
+ next_question: Optional[str] = Field(
620
+ None, description="Next discovery question text (null if discovery complete)"
621
+ )
622
+
623
+ is_complete: bool = Field(..., description="Whether the discovery phase is complete")
624
+
625
+ current_index: int = Field(..., ge=0, description="Current question index (0-based)")
626
+
627
+ total_questions: int = Field(..., gt=0, description="Total number of discovery questions")
628
+
629
+ progress_percentage: float = Field(
630
+ ..., ge=0.0, le=100.0, description="Discovery completion percentage (0.0 - 100.0)"
631
+ )
632
+
633
+ model_config = ConfigDict(
634
+ json_schema_extra={
635
+ "example": {
636
+ "success": True,
637
+ "next_question": "What tech stack are you planning to use?",
638
+ "is_complete": False,
639
+ "current_index": 3,
640
+ "total_questions": 20,
641
+ "progress_percentage": 15.0,
642
+ }
643
+ }
644
+ )
645
+
646
+
647
+ # ============================================================================
648
+ # Sprint 10 Models (Feature: 015-review-polish)
649
+ # ============================================================================
650
+
651
+
652
+ class CodeReview(BaseModel):
653
+ """Code review finding from Review Agent (Sprint 10)."""
654
+
655
+ id: Optional[int] = None
656
+ task_id: int
657
+ agent_id: str
658
+ project_id: int
659
+ file_path: str = Field(..., description="Relative path from project root")
660
+ line_number: Optional[int] = Field(None, description="Line number, None for file-level")
661
+ severity: Severity
662
+ category: ReviewCategory
663
+ message: str = Field(..., min_length=10, description="Description of the issue")
664
+ recommendation: Optional[str] = Field(None, description="How to fix it")
665
+ code_snippet: Optional[str] = Field(None, description="Offending code for context")
666
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
667
+
668
+ model_config = ConfigDict(use_enum_values=True)
669
+
670
+ @property
671
+ def is_blocking(self) -> bool:
672
+ """Whether this finding should block task completion."""
673
+ # Handle both enum and string values (use_enum_values=True converts to strings)
674
+ blocking_severities = [Severity.CRITICAL.value, Severity.HIGH.value]
675
+ severity_value = self.severity if isinstance(self.severity, str) else self.severity.value
676
+ return severity_value in blocking_severities
677
+
678
+
679
+ class TokenUsage(BaseModel):
680
+ """Token usage record for a single LLM call (Sprint 10)."""
681
+
682
+ id: Optional[int] = None
683
+ # Tasks use integer PKs in the v1 schema and UUID strings in v2 workspaces;
684
+ # SQLite is type-flexible, so we accept either at the model boundary.
685
+ task_id: Optional[Union[int, str]] = None # None for non-task calls
686
+ agent_id: str
687
+ project_id: int
688
+ model_name: str = Field(..., description="e.g., claude-sonnet-4-5")
689
+ input_tokens: int = Field(..., ge=0)
690
+ output_tokens: int = Field(..., ge=0)
691
+ estimated_cost_usd: float = Field(..., ge=0.0)
692
+ actual_cost_usd: Optional[float] = Field(None, ge=0.0)
693
+ call_type: CallType = CallType.OTHER
694
+ session_id: Optional[str] = Field(None, description="SDK session ID for conversation tracking")
695
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
696
+
697
+ model_config = ConfigDict(use_enum_values=True)
698
+
699
+ @property
700
+ def total_tokens(self) -> int:
701
+ """Total tokens (input + output)."""
702
+ return self.input_tokens + self.output_tokens
703
+
704
+ @staticmethod
705
+ def calculate_cost(model_name: str, input_tokens: int, output_tokens: int) -> float:
706
+ """Calculate estimated cost in USD.
707
+
708
+ Pricing as of 2025-11:
709
+ - Claude Sonnet 4.5: $3.00 input / $15.00 output per MTok
710
+ - Claude Opus 4: $15.00 input / $75.00 output per MTok
711
+ - Claude Haiku 4: $0.80 input / $4.00 output per MTok
712
+ """
713
+ pricing = {
714
+ "claude-sonnet-4-5": {"input": 3.00, "output": 15.00},
715
+ "claude-opus-4": {"input": 15.00, "output": 75.00},
716
+ "claude-haiku-4": {"input": 0.80, "output": 4.00},
717
+ }
718
+
719
+ if model_name not in pricing:
720
+ raise ValueError(f"Unknown model: {model_name}")
721
+
722
+ prices = pricing[model_name]
723
+ cost = (input_tokens * prices["input"] / 1_000_000) + (
724
+ output_tokens * prices["output"] / 1_000_000
725
+ )
726
+ return round(cost, 6) # 6 decimal places for precision
727
+
728
+
729
+ class QualityGateFailure(BaseModel):
730
+ """Individual quality gate failure (Sprint 10)."""
731
+
732
+ gate: QualityGateType
733
+ reason: str = Field(..., min_length=5)
734
+ details: Optional[str] = None # Full error output
735
+ severity: Severity = Severity.HIGH
736
+
737
+
738
+ class QualityGateResult(BaseModel):
739
+ """Result of running quality gates for a task (Sprint 10).
740
+
741
+ Includes information about which gates were run based on task classification,
742
+ and which gates were skipped as not applicable.
743
+ """
744
+
745
+ task_id: int
746
+ status: str = Field(..., description="passed or failed")
747
+ failures: List[QualityGateFailure] = Field(default_factory=list)
748
+ execution_time_seconds: float = Field(..., ge=0.0)
749
+ timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
750
+ skipped_gates: List[str] = Field(
751
+ default_factory=list,
752
+ description="Gates that were skipped based on task classification"
753
+ )
754
+ task_category: Optional[str] = Field(
755
+ default=None,
756
+ description="Task category used for gate selection (e.g., 'design', 'code_implementation')"
757
+ )
758
+
759
+ @property
760
+ def passed(self) -> bool:
761
+ """Whether all gates passed."""
762
+ return self.status == "passed" and len(self.failures) == 0
763
+
764
+ @property
765
+ def has_critical_failures(self) -> bool:
766
+ """Whether any failures are critical."""
767
+ return any(f.severity == Severity.CRITICAL for f in self.failures)
768
+
769
+ @property
770
+ def gates_skipped_count(self) -> int:
771
+ """Number of gates that were skipped."""
772
+ return len(self.skipped_gates)
773
+
774
+
775
+ class CheckpointMetadata(BaseModel):
776
+ """Metadata stored in checkpoint for quick inspection (Sprint 10)."""
777
+
778
+ project_id: int
779
+ phase: str # discovery, planning, active, review, complete
780
+ tasks_completed: int
781
+ tasks_total: int
782
+ agents_active: List[str]
783
+ last_task_completed: Optional[str] = None
784
+ context_items_count: int
785
+ total_cost_usd: float
786
+ # Quality tracking fields (optional for backward compatibility)
787
+ quality_stats: Optional[Dict[str, Any]] = None # Current quality metrics from QualityTracker
788
+ quality_trend: Optional[str] = None # "improving", "stable", or "declining"
789
+
790
+
791
+ class Checkpoint(BaseModel):
792
+ """Project checkpoint for restore operations (Sprint 10)."""
793
+
794
+ id: Optional[int] = None
795
+ project_id: int
796
+ name: str = Field(..., min_length=1, max_length=100)
797
+ description: Optional[str] = Field(None, max_length=500)
798
+ trigger: str = Field(..., description="manual, auto, phase_transition, pause")
799
+ git_commit: str = Field(..., min_length=7, max_length=40, description="Git commit SHA")
800
+ database_backup_path: str = Field(..., description="Path to .sqlite backup")
801
+ context_snapshot_path: str = Field(..., description="Path to context JSON")
802
+ metadata: CheckpointMetadata
803
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
804
+
805
+ def validate_files_exist(self) -> bool:
806
+ """Check if all checkpoint files exist."""
807
+ from pathlib import Path
808
+
809
+ db_path = Path(self.database_backup_path)
810
+ context_path = Path(self.context_snapshot_path)
811
+ return db_path.exists() and context_path.exists()
812
+
813
+
814
+ # =============================================================================
815
+ # Execution Event Models for SSE/WebSocket Streaming
816
+ # =============================================================================
817
+
818
+ ExecutionEventType = Literal[
819
+ "progress", "output", "blocker", "completion", "error", "heartbeat"
820
+ ]
821
+
822
+
823
+ class BaseExecutionEvent(BaseModel):
824
+ """Base class for all execution events.
825
+
826
+ These events are used for real-time streaming via SSE/WebSocket.
827
+ Each event type has specific data fields in a nested 'data' dict.
828
+ """
829
+
830
+ event_type: ExecutionEventType
831
+ task_id: str
832
+ timestamp: str = Field(
833
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
834
+ )
835
+
836
+ model_config = ConfigDict(extra="forbid")
837
+
838
+
839
+ class AgentPhase:
840
+ """Phase constants for ReactAgent progress reporting.
841
+
842
+ Each phase maps to specific tool calls during agent execution.
843
+ """
844
+
845
+ EXPLORING = "exploring" # read_file, list_files, search_codebase
846
+ PLANNING = "planning" # context loading, system prompt construction
847
+ CREATING = "creating" # create_file
848
+ EDITING = "editing" # edit_file
849
+ TESTING = "testing" # run_tests, run_command
850
+ FIXING = "fixing" # edit_file after verification failure
851
+ VERIFYING = "verifying" # gates.run() / final verification
852
+
853
+
854
+ class ProgressEvent(BaseExecutionEvent):
855
+ """Event indicating task execution progress.
856
+
857
+ Emitted when the agent transitions between phases or steps.
858
+ """
859
+
860
+ event_type: Literal["progress"] = "progress"
861
+ phase: str # planning, execution, verification, self_correction
862
+ step: int
863
+ total_steps: int
864
+ message: Optional[str] = None
865
+ tool_name: Optional[str] = None
866
+ file_path: Optional[str] = None
867
+ iteration: Optional[int] = None
868
+
869
+ @computed_field # type: ignore[prop-decorator]
870
+ @property
871
+ def data(self) -> Dict[str, Any]:
872
+ """Nested data for SSE format compatibility."""
873
+ return {
874
+ "phase": self.phase,
875
+ "step": self.step,
876
+ "total_steps": self.total_steps,
877
+ "message": self.message,
878
+ "tool_name": self.tool_name,
879
+ "file_path": self.file_path,
880
+ "iteration": self.iteration,
881
+ }
882
+
883
+
884
+ class OutputEvent(BaseExecutionEvent):
885
+ """Event containing stdout/stderr output.
886
+
887
+ Emitted when the agent produces command output.
888
+ """
889
+
890
+ event_type: Literal["output"] = "output"
891
+ stream: Literal["stdout", "stderr"]
892
+ line: str
893
+
894
+ @computed_field # type: ignore[prop-decorator]
895
+ @property
896
+ def data(self) -> Dict[str, str]:
897
+ """Nested data for SSE format compatibility."""
898
+ return {
899
+ "stream": self.stream,
900
+ "line": self.line,
901
+ }
902
+
903
+
904
+ class BlockerEvent(BaseExecutionEvent):
905
+ """Event indicating a blocker was created.
906
+
907
+ Emitted when the agent needs human input to proceed.
908
+ """
909
+
910
+ event_type: Literal["blocker"] = "blocker"
911
+ blocker_id: int
912
+ question: str
913
+ context: Optional[str] = None
914
+
915
+ @computed_field # type: ignore[prop-decorator]
916
+ @property
917
+ def data(self) -> Dict[str, Any]:
918
+ """Nested data for SSE format compatibility."""
919
+ return {
920
+ "blocker_id": self.blocker_id,
921
+ "question": self.question,
922
+ "context": self.context,
923
+ }
924
+
925
+
926
+ class CompletionEvent(BaseExecutionEvent):
927
+ """Event indicating task execution completed.
928
+
929
+ Emitted when the agent finishes (successfully or with failure).
930
+ """
931
+
932
+ event_type: Literal["completion"] = "completion"
933
+ status: Literal["completed", "failed", "blocked"]
934
+ duration_seconds: float
935
+ files_modified: Optional[List[str]] = None
936
+
937
+ @computed_field # type: ignore[prop-decorator]
938
+ @property
939
+ def data(self) -> Dict[str, Any]:
940
+ """Nested data for SSE format compatibility."""
941
+ return {
942
+ "status": self.status,
943
+ "duration_seconds": self.duration_seconds,
944
+ "files_modified": self.files_modified,
945
+ }
946
+
947
+
948
+ class ErrorEvent(BaseExecutionEvent):
949
+ """Event indicating an error occurred.
950
+
951
+ Emitted when the agent encounters an error during execution.
952
+ """
953
+
954
+ event_type: Literal["error"] = "error"
955
+ error: str
956
+ error_type: str
957
+ traceback: Optional[str] = None
958
+
959
+ @computed_field # type: ignore[prop-decorator]
960
+ @property
961
+ def data(self) -> Dict[str, Any]:
962
+ """Nested data for SSE format compatibility."""
963
+ return {
964
+ "error": self.error,
965
+ "error_type": self.error_type,
966
+ "traceback": self.traceback,
967
+ }
968
+
969
+
970
+ class HeartbeatEvent(BaseExecutionEvent):
971
+ """Keep-alive event for long-running streams.
972
+
973
+ Emitted periodically to prevent connection timeouts.
974
+ """
975
+
976
+ event_type: Literal["heartbeat"] = "heartbeat"
977
+
978
+ @computed_field # type: ignore[prop-decorator]
979
+ @property
980
+ def data(self) -> Dict[str, Any]:
981
+ """Nested data for SSE format compatibility."""
982
+ return {}
983
+
984
+
985
+ # Union type for all execution events
986
+ ExecutionEvent = Union[
987
+ ProgressEvent,
988
+ OutputEvent,
989
+ BlockerEvent,
990
+ CompletionEvent,
991
+ ErrorEvent,
992
+ HeartbeatEvent,
993
+ ]
994
+
995
+
996
+ def create_execution_event(
997
+ event_type: ExecutionEventType,
998
+ task_id: str,
999
+ **kwargs: Any,
1000
+ ) -> ExecutionEvent:
1001
+ """Factory function to create the appropriate event type.
1002
+
1003
+ Args:
1004
+ event_type: Type of event to create
1005
+ task_id: Task ID for the event
1006
+ **kwargs: Event-specific fields
1007
+
1008
+ Returns:
1009
+ The appropriate ExecutionEvent subclass instance
1010
+
1011
+ Raises:
1012
+ ValueError: If event_type is not recognized
1013
+ """
1014
+ event_classes: Dict[str, type] = {
1015
+ "progress": ProgressEvent,
1016
+ "output": OutputEvent,
1017
+ "blocker": BlockerEvent,
1018
+ "completion": CompletionEvent,
1019
+ "error": ErrorEvent,
1020
+ "heartbeat": HeartbeatEvent,
1021
+ }
1022
+
1023
+ if event_type not in event_classes:
1024
+ raise ValueError(f"Unknown event type: {event_type}")
1025
+
1026
+ return event_classes[event_type](task_id=task_id, **kwargs)