tapps-agents 3.5.40__py3-none-any.whl → 3.6.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.
- tapps_agents/__init__.py +2 -2
- tapps_agents/agents/__init__.py +22 -22
- tapps_agents/agents/analyst/__init__.py +5 -5
- tapps_agents/agents/architect/__init__.py +5 -5
- tapps_agents/agents/architect/agent.py +1033 -1033
- tapps_agents/agents/architect/pattern_detector.py +75 -75
- tapps_agents/agents/cleanup/__init__.py +7 -7
- tapps_agents/agents/cleanup/agent.py +445 -445
- tapps_agents/agents/debugger/__init__.py +7 -7
- tapps_agents/agents/debugger/agent.py +310 -310
- tapps_agents/agents/debugger/error_analyzer.py +437 -437
- tapps_agents/agents/designer/__init__.py +5 -5
- tapps_agents/agents/designer/agent.py +786 -786
- tapps_agents/agents/designer/visual_designer.py +638 -638
- tapps_agents/agents/documenter/__init__.py +7 -7
- tapps_agents/agents/documenter/agent.py +531 -531
- tapps_agents/agents/documenter/doc_generator.py +472 -472
- tapps_agents/agents/documenter/doc_validator.py +393 -393
- tapps_agents/agents/documenter/framework_doc_updater.py +493 -493
- tapps_agents/agents/enhancer/__init__.py +7 -7
- tapps_agents/agents/evaluator/__init__.py +7 -7
- tapps_agents/agents/evaluator/agent.py +443 -443
- tapps_agents/agents/evaluator/priority_evaluator.py +641 -641
- tapps_agents/agents/evaluator/quality_analyzer.py +147 -147
- tapps_agents/agents/evaluator/report_generator.py +344 -344
- tapps_agents/agents/evaluator/usage_analyzer.py +192 -192
- tapps_agents/agents/evaluator/workflow_analyzer.py +189 -189
- tapps_agents/agents/implementer/__init__.py +7 -7
- tapps_agents/agents/implementer/agent.py +798 -798
- tapps_agents/agents/implementer/auto_fix.py +1119 -1119
- tapps_agents/agents/implementer/code_generator.py +73 -73
- tapps_agents/agents/improver/__init__.py +1 -1
- tapps_agents/agents/improver/agent.py +753 -753
- tapps_agents/agents/ops/__init__.py +1 -1
- tapps_agents/agents/ops/agent.py +619 -619
- tapps_agents/agents/ops/dependency_analyzer.py +600 -600
- tapps_agents/agents/orchestrator/__init__.py +5 -5
- tapps_agents/agents/orchestrator/agent.py +522 -522
- tapps_agents/agents/planner/__init__.py +7 -7
- tapps_agents/agents/planner/agent.py +1127 -1127
- tapps_agents/agents/reviewer/__init__.py +24 -24
- tapps_agents/agents/reviewer/agent.py +3513 -3513
- tapps_agents/agents/reviewer/aggregator.py +213 -213
- tapps_agents/agents/reviewer/batch_review.py +448 -448
- tapps_agents/agents/reviewer/cache.py +443 -443
- tapps_agents/agents/reviewer/context7_enhancer.py +630 -630
- tapps_agents/agents/reviewer/context_detector.py +203 -203
- tapps_agents/agents/reviewer/docker_compose_validator.py +158 -158
- tapps_agents/agents/reviewer/dockerfile_validator.py +176 -176
- tapps_agents/agents/reviewer/error_handling.py +126 -126
- tapps_agents/agents/reviewer/feedback_generator.py +490 -490
- tapps_agents/agents/reviewer/influxdb_validator.py +316 -316
- tapps_agents/agents/reviewer/issue_tracking.py +169 -169
- tapps_agents/agents/reviewer/library_detector.py +295 -295
- tapps_agents/agents/reviewer/library_patterns.py +268 -268
- tapps_agents/agents/reviewer/maintainability_scorer.py +593 -593
- tapps_agents/agents/reviewer/metric_strategies.py +276 -276
- tapps_agents/agents/reviewer/mqtt_validator.py +160 -160
- tapps_agents/agents/reviewer/output_enhancer.py +105 -105
- tapps_agents/agents/reviewer/pattern_detector.py +241 -241
- tapps_agents/agents/reviewer/performance_scorer.py +357 -357
- tapps_agents/agents/reviewer/phased_review.py +516 -516
- tapps_agents/agents/reviewer/progressive_review.py +435 -435
- tapps_agents/agents/reviewer/react_scorer.py +331 -331
- tapps_agents/agents/reviewer/score_constants.py +228 -228
- tapps_agents/agents/reviewer/score_validator.py +507 -507
- tapps_agents/agents/reviewer/scorer_registry.py +373 -373
- tapps_agents/agents/reviewer/scoring.py +1566 -1566
- tapps_agents/agents/reviewer/service_discovery.py +534 -534
- tapps_agents/agents/reviewer/tools/__init__.py +41 -41
- tapps_agents/agents/reviewer/tools/parallel_executor.py +581 -581
- tapps_agents/agents/reviewer/tools/ruff_grouping.py +250 -250
- tapps_agents/agents/reviewer/tools/scoped_mypy.py +284 -284
- tapps_agents/agents/reviewer/typescript_scorer.py +1142 -1142
- tapps_agents/agents/reviewer/validation.py +208 -208
- tapps_agents/agents/reviewer/websocket_validator.py +132 -132
- tapps_agents/agents/tester/__init__.py +7 -7
- tapps_agents/agents/tester/accessibility_auditor.py +309 -309
- tapps_agents/agents/tester/agent.py +1080 -1080
- tapps_agents/agents/tester/batch_generator.py +54 -54
- tapps_agents/agents/tester/context_learner.py +51 -51
- tapps_agents/agents/tester/coverage_analyzer.py +386 -386
- tapps_agents/agents/tester/coverage_test_generator.py +290 -290
- tapps_agents/agents/tester/debug_enhancer.py +238 -238
- tapps_agents/agents/tester/device_emulator.py +241 -241
- tapps_agents/agents/tester/integration_generator.py +62 -62
- tapps_agents/agents/tester/network_recorder.py +300 -300
- tapps_agents/agents/tester/performance_monitor.py +320 -320
- tapps_agents/agents/tester/test_fixer.py +316 -316
- tapps_agents/agents/tester/test_generator.py +632 -632
- tapps_agents/agents/tester/trace_manager.py +234 -234
- tapps_agents/agents/tester/visual_regression.py +291 -291
- tapps_agents/analysis/pattern_detector.py +36 -36
- tapps_agents/beads/hydration.py +213 -213
- tapps_agents/beads/parse.py +32 -32
- tapps_agents/beads/specs.py +206 -206
- tapps_agents/cli/__init__.py +9 -9
- tapps_agents/cli/__main__.py +8 -8
- tapps_agents/cli/base.py +478 -478
- tapps_agents/cli/command_classifier.py +72 -72
- tapps_agents/cli/commands/__init__.py +2 -2
- tapps_agents/cli/commands/analyst.py +173 -173
- tapps_agents/cli/commands/architect.py +109 -109
- tapps_agents/cli/commands/cleanup_agent.py +92 -92
- tapps_agents/cli/commands/common.py +126 -126
- tapps_agents/cli/commands/debugger.py +90 -90
- tapps_agents/cli/commands/designer.py +112 -112
- tapps_agents/cli/commands/documenter.py +136 -136
- tapps_agents/cli/commands/enhancer.py +110 -110
- tapps_agents/cli/commands/evaluator.py +255 -255
- tapps_agents/cli/commands/health.py +665 -665
- tapps_agents/cli/commands/implementer.py +301 -301
- tapps_agents/cli/commands/improver.py +91 -91
- tapps_agents/cli/commands/knowledge.py +111 -111
- tapps_agents/cli/commands/learning.py +172 -172
- tapps_agents/cli/commands/observability.py +283 -283
- tapps_agents/cli/commands/ops.py +135 -135
- tapps_agents/cli/commands/orchestrator.py +116 -116
- tapps_agents/cli/commands/planner.py +237 -237
- tapps_agents/cli/commands/reviewer.py +1872 -1872
- tapps_agents/cli/commands/status.py +285 -285
- tapps_agents/cli/commands/task.py +227 -219
- tapps_agents/cli/commands/tester.py +191 -191
- tapps_agents/cli/commands/top_level.py +3586 -3586
- tapps_agents/cli/feedback.py +936 -936
- tapps_agents/cli/formatters.py +608 -608
- tapps_agents/cli/help/__init__.py +7 -7
- tapps_agents/cli/help/static_help.py +425 -425
- tapps_agents/cli/network_detection.py +110 -110
- tapps_agents/cli/output_compactor.py +274 -274
- tapps_agents/cli/parsers/__init__.py +2 -2
- tapps_agents/cli/parsers/analyst.py +186 -186
- tapps_agents/cli/parsers/architect.py +167 -167
- tapps_agents/cli/parsers/cleanup_agent.py +228 -228
- tapps_agents/cli/parsers/debugger.py +116 -116
- tapps_agents/cli/parsers/designer.py +182 -182
- tapps_agents/cli/parsers/documenter.py +134 -134
- tapps_agents/cli/parsers/enhancer.py +113 -113
- tapps_agents/cli/parsers/evaluator.py +213 -213
- tapps_agents/cli/parsers/implementer.py +168 -168
- tapps_agents/cli/parsers/improver.py +132 -132
- tapps_agents/cli/parsers/ops.py +159 -159
- tapps_agents/cli/parsers/orchestrator.py +98 -98
- tapps_agents/cli/parsers/planner.py +145 -145
- tapps_agents/cli/parsers/reviewer.py +462 -462
- tapps_agents/cli/parsers/tester.py +124 -124
- tapps_agents/cli/progress_heartbeat.py +254 -254
- tapps_agents/cli/streaming_progress.py +336 -336
- tapps_agents/cli/utils/__init__.py +6 -6
- tapps_agents/cli/utils/agent_lifecycle.py +48 -48
- tapps_agents/cli/utils/error_formatter.py +82 -82
- tapps_agents/cli/utils/error_recovery.py +188 -188
- tapps_agents/cli/utils/output_handler.py +59 -59
- tapps_agents/cli/utils/prompt_enhancer.py +319 -319
- tapps_agents/cli/validators/__init__.py +9 -9
- tapps_agents/cli/validators/command_validator.py +81 -81
- tapps_agents/context7/__init__.py +112 -112
- tapps_agents/context7/agent_integration.py +869 -869
- tapps_agents/context7/analytics.py +382 -382
- tapps_agents/context7/analytics_dashboard.py +299 -299
- tapps_agents/context7/async_cache.py +681 -681
- tapps_agents/context7/backup_client.py +958 -958
- tapps_agents/context7/cache_locking.py +194 -194
- tapps_agents/context7/cache_metadata.py +214 -214
- tapps_agents/context7/cache_prewarm.py +488 -488
- tapps_agents/context7/cache_structure.py +168 -168
- tapps_agents/context7/cache_warming.py +604 -604
- tapps_agents/context7/circuit_breaker.py +376 -376
- tapps_agents/context7/cleanup.py +461 -461
- tapps_agents/context7/commands.py +858 -858
- tapps_agents/context7/credential_validation.py +276 -276
- tapps_agents/context7/cross_reference_resolver.py +168 -168
- tapps_agents/context7/cross_references.py +424 -424
- tapps_agents/context7/doc_manager.py +225 -225
- tapps_agents/context7/fuzzy_matcher.py +369 -369
- tapps_agents/context7/kb_cache.py +404 -404
- tapps_agents/context7/language_detector.py +219 -219
- tapps_agents/context7/library_detector.py +725 -725
- tapps_agents/context7/lookup.py +738 -738
- tapps_agents/context7/metadata.py +258 -258
- tapps_agents/context7/refresh_queue.py +300 -300
- tapps_agents/context7/security.py +373 -373
- tapps_agents/context7/staleness_policies.py +278 -278
- tapps_agents/context7/tiles_integration.py +47 -47
- tapps_agents/continuous_bug_fix/__init__.py +20 -20
- tapps_agents/continuous_bug_fix/bug_finder.py +306 -306
- tapps_agents/continuous_bug_fix/bug_fix_coordinator.py +177 -177
- tapps_agents/continuous_bug_fix/commit_manager.py +178 -178
- tapps_agents/continuous_bug_fix/continuous_bug_fixer.py +322 -322
- tapps_agents/continuous_bug_fix/proactive_bug_finder.py +285 -285
- tapps_agents/core/__init__.py +298 -298
- tapps_agents/core/adaptive_cache_config.py +432 -432
- tapps_agents/core/agent_base.py +647 -647
- tapps_agents/core/agent_cache.py +466 -466
- tapps_agents/core/agent_learning.py +1865 -1865
- tapps_agents/core/analytics_dashboard.py +563 -563
- tapps_agents/core/analytics_enhancements.py +597 -597
- tapps_agents/core/anonymization.py +274 -274
- tapps_agents/core/artifact_context_builder.py +293 -0
- tapps_agents/core/ast_parser.py +228 -228
- tapps_agents/core/async_file_ops.py +402 -402
- tapps_agents/core/best_practice_consultant.py +299 -299
- tapps_agents/core/brownfield_analyzer.py +299 -299
- tapps_agents/core/brownfield_review.py +541 -541
- tapps_agents/core/browser_controller.py +513 -513
- tapps_agents/core/capability_registry.py +418 -418
- tapps_agents/core/change_impact_analyzer.py +190 -190
- tapps_agents/core/checkpoint_manager.py +377 -377
- tapps_agents/core/code_generator.py +329 -329
- tapps_agents/core/code_validator.py +276 -276
- tapps_agents/core/command_registry.py +327 -327
- tapps_agents/core/config.py +33 -0
- tapps_agents/core/context_gathering/__init__.py +2 -2
- tapps_agents/core/context_gathering/repository_explorer.py +28 -28
- tapps_agents/core/context_intelligence/__init__.py +2 -2
- tapps_agents/core/context_intelligence/relevance_scorer.py +24 -24
- tapps_agents/core/context_intelligence/token_budget_manager.py +27 -27
- tapps_agents/core/context_manager.py +240 -240
- tapps_agents/core/cursor_feedback_monitor.py +146 -146
- tapps_agents/core/cursor_verification.py +290 -290
- tapps_agents/core/customization_loader.py +280 -280
- tapps_agents/core/customization_schema.py +260 -260
- tapps_agents/core/customization_template.py +238 -238
- tapps_agents/core/debug_logger.py +124 -124
- tapps_agents/core/design_validator.py +298 -298
- tapps_agents/core/diagram_generator.py +226 -226
- tapps_agents/core/docker_utils.py +232 -232
- tapps_agents/core/document_generator.py +617 -617
- tapps_agents/core/domain_detector.py +30 -30
- tapps_agents/core/error_envelope.py +454 -454
- tapps_agents/core/error_handler.py +270 -270
- tapps_agents/core/estimation_tracker.py +189 -189
- tapps_agents/core/eval_prompt_engine.py +116 -116
- tapps_agents/core/evaluation_base.py +119 -119
- tapps_agents/core/evaluation_models.py +320 -320
- tapps_agents/core/evaluation_orchestrator.py +225 -225
- tapps_agents/core/evaluators/__init__.py +7 -7
- tapps_agents/core/evaluators/architectural_evaluator.py +205 -205
- tapps_agents/core/evaluators/behavioral_evaluator.py +160 -160
- tapps_agents/core/evaluators/performance_profile_evaluator.py +160 -160
- tapps_agents/core/evaluators/security_posture_evaluator.py +148 -148
- tapps_agents/core/evaluators/spec_compliance_evaluator.py +181 -181
- tapps_agents/core/exceptions.py +107 -107
- tapps_agents/core/expert_config_generator.py +293 -293
- tapps_agents/core/export_schema.py +202 -202
- tapps_agents/core/external_feedback_models.py +102 -102
- tapps_agents/core/external_feedback_storage.py +213 -213
- tapps_agents/core/fallback_strategy.py +314 -314
- tapps_agents/core/feedback_analyzer.py +162 -162
- tapps_agents/core/feedback_collector.py +178 -178
- tapps_agents/core/git_operations.py +445 -445
- tapps_agents/core/hardware_profiler.py +151 -151
- tapps_agents/core/instructions.py +324 -324
- tapps_agents/core/io_guardrails.py +69 -69
- tapps_agents/core/issue_manifest.py +249 -249
- tapps_agents/core/issue_schema.py +139 -139
- tapps_agents/core/json_utils.py +128 -128
- tapps_agents/core/knowledge_graph.py +446 -446
- tapps_agents/core/language_detector.py +296 -296
- tapps_agents/core/learning_confidence.py +242 -242
- tapps_agents/core/learning_dashboard.py +246 -246
- tapps_agents/core/learning_decision.py +384 -384
- tapps_agents/core/learning_explainability.py +578 -578
- tapps_agents/core/learning_export.py +287 -287
- tapps_agents/core/learning_integration.py +228 -228
- tapps_agents/core/llm_behavior.py +232 -232
- tapps_agents/core/long_duration_support.py +786 -786
- tapps_agents/core/mcp_setup.py +106 -106
- tapps_agents/core/memory_integration.py +396 -396
- tapps_agents/core/meta_learning.py +666 -666
- tapps_agents/core/module_path_sanitizer.py +199 -199
- tapps_agents/core/multi_agent_orchestrator.py +382 -382
- tapps_agents/core/network_errors.py +125 -125
- tapps_agents/core/nfr_validator.py +336 -336
- tapps_agents/core/offline_mode.py +158 -158
- tapps_agents/core/output_contracts.py +300 -300
- tapps_agents/core/output_formatter.py +300 -300
- tapps_agents/core/path_normalizer.py +174 -174
- tapps_agents/core/path_validator.py +322 -322
- tapps_agents/core/pattern_library.py +250 -250
- tapps_agents/core/performance_benchmark.py +301 -301
- tapps_agents/core/performance_monitor.py +184 -184
- tapps_agents/core/playwright_mcp_controller.py +771 -771
- tapps_agents/core/policy_loader.py +135 -135
- tapps_agents/core/progress.py +166 -166
- tapps_agents/core/project_profile.py +354 -354
- tapps_agents/core/project_type_detector.py +454 -454
- tapps_agents/core/prompt_base.py +223 -223
- tapps_agents/core/prompt_learning/__init__.py +2 -2
- tapps_agents/core/prompt_learning/learning_loop.py +24 -24
- tapps_agents/core/prompt_learning/project_prompt_store.py +25 -25
- tapps_agents/core/prompt_learning/skills_prompt_analyzer.py +35 -35
- tapps_agents/core/prompt_optimization/__init__.py +6 -6
- tapps_agents/core/prompt_optimization/ab_tester.py +114 -114
- tapps_agents/core/prompt_optimization/correlation_analyzer.py +160 -160
- tapps_agents/core/prompt_optimization/progressive_refiner.py +129 -129
- tapps_agents/core/prompt_optimization/prompt_library.py +37 -37
- tapps_agents/core/requirements_evaluator.py +431 -431
- tapps_agents/core/resource_aware_executor.py +449 -449
- tapps_agents/core/resource_monitor.py +343 -343
- tapps_agents/core/resume_handler.py +298 -298
- tapps_agents/core/retry_handler.py +197 -197
- tapps_agents/core/review_checklists.py +479 -479
- tapps_agents/core/role_loader.py +201 -201
- tapps_agents/core/role_template_loader.py +201 -201
- tapps_agents/core/runtime_mode.py +60 -60
- tapps_agents/core/security_scanner.py +342 -342
- tapps_agents/core/skill_agent_registry.py +194 -194
- tapps_agents/core/skill_integration.py +208 -208
- tapps_agents/core/skill_loader.py +492 -492
- tapps_agents/core/skill_template.py +341 -341
- tapps_agents/core/skill_validator.py +478 -478
- tapps_agents/core/stack_analyzer.py +35 -35
- tapps_agents/core/startup.py +174 -174
- tapps_agents/core/storage_manager.py +397 -397
- tapps_agents/core/storage_models.py +166 -166
- tapps_agents/core/story_evaluator.py +410 -410
- tapps_agents/core/subprocess_utils.py +170 -170
- tapps_agents/core/task_duration.py +296 -296
- tapps_agents/core/task_memory.py +582 -582
- tapps_agents/core/task_state.py +226 -226
- tapps_agents/core/tech_stack_priorities.py +208 -208
- tapps_agents/core/temp_directory.py +194 -194
- tapps_agents/core/template_merger.py +600 -600
- tapps_agents/core/template_selector.py +280 -280
- tapps_agents/core/test_generator.py +286 -286
- tapps_agents/core/tiered_context.py +253 -253
- tapps_agents/core/token_monitor.py +345 -345
- tapps_agents/core/traceability.py +254 -254
- tapps_agents/core/trajectory_tracker.py +50 -50
- tapps_agents/core/unicode_safe.py +143 -143
- tapps_agents/core/unified_cache_config.py +170 -170
- tapps_agents/core/unified_state.py +324 -324
- tapps_agents/core/validate_cursor_setup.py +237 -237
- tapps_agents/core/validation_registry.py +136 -136
- tapps_agents/core/validators/__init__.py +4 -4
- tapps_agents/core/validators/python_validator.py +87 -87
- tapps_agents/core/verification_agent.py +90 -90
- tapps_agents/core/visual_feedback.py +644 -644
- tapps_agents/core/workflow_validator.py +197 -197
- tapps_agents/core/worktree.py +367 -367
- tapps_agents/docker/__init__.py +10 -10
- tapps_agents/docker/analyzer.py +186 -186
- tapps_agents/docker/debugger.py +229 -229
- tapps_agents/docker/error_patterns.py +216 -216
- tapps_agents/epic/__init__.py +22 -22
- tapps_agents/epic/beads_sync.py +115 -115
- tapps_agents/epic/markdown_sync.py +105 -105
- tapps_agents/epic/models.py +96 -96
- tapps_agents/experts/__init__.py +163 -163
- tapps_agents/experts/agent_integration.py +243 -243
- tapps_agents/experts/auto_generator.py +331 -331
- tapps_agents/experts/base_expert.py +536 -536
- tapps_agents/experts/builtin_registry.py +261 -261
- tapps_agents/experts/business_metrics.py +565 -565
- tapps_agents/experts/cache.py +266 -266
- tapps_agents/experts/confidence_breakdown.py +306 -306
- tapps_agents/experts/confidence_calculator.py +336 -336
- tapps_agents/experts/confidence_metrics.py +236 -236
- tapps_agents/experts/domain_config.py +311 -311
- tapps_agents/experts/domain_detector.py +550 -550
- tapps_agents/experts/domain_utils.py +84 -84
- tapps_agents/experts/expert_config.py +113 -113
- tapps_agents/experts/expert_engine.py +465 -465
- tapps_agents/experts/expert_registry.py +744 -744
- tapps_agents/experts/expert_synthesizer.py +70 -70
- tapps_agents/experts/governance.py +197 -197
- tapps_agents/experts/history_logger.py +312 -312
- tapps_agents/experts/knowledge/README.md +180 -180
- tapps_agents/experts/knowledge/accessibility/accessible-forms.md +331 -331
- tapps_agents/experts/knowledge/accessibility/aria-patterns.md +344 -344
- tapps_agents/experts/knowledge/accessibility/color-contrast.md +285 -285
- tapps_agents/experts/knowledge/accessibility/keyboard-navigation.md +332 -332
- tapps_agents/experts/knowledge/accessibility/screen-readers.md +282 -282
- tapps_agents/experts/knowledge/accessibility/semantic-html.md +355 -355
- tapps_agents/experts/knowledge/accessibility/testing-accessibility.md +369 -369
- tapps_agents/experts/knowledge/accessibility/wcag-2.1.md +296 -296
- tapps_agents/experts/knowledge/accessibility/wcag-2.2.md +211 -211
- tapps_agents/experts/knowledge/agent-learning/best-practices.md +715 -715
- tapps_agents/experts/knowledge/agent-learning/pattern-extraction.md +282 -282
- tapps_agents/experts/knowledge/agent-learning/prompt-optimization.md +320 -320
- tapps_agents/experts/knowledge/ai-frameworks/model-optimization.md +90 -90
- tapps_agents/experts/knowledge/ai-frameworks/openvino-patterns.md +260 -260
- tapps_agents/experts/knowledge/api-design-integration/api-gateway-patterns.md +309 -309
- tapps_agents/experts/knowledge/api-design-integration/api-security-patterns.md +521 -521
- tapps_agents/experts/knowledge/api-design-integration/api-versioning.md +421 -421
- tapps_agents/experts/knowledge/api-design-integration/async-protocol-patterns.md +61 -61
- tapps_agents/experts/knowledge/api-design-integration/contract-testing.md +221 -221
- tapps_agents/experts/knowledge/api-design-integration/external-api-integration.md +489 -489
- tapps_agents/experts/knowledge/api-design-integration/fastapi-patterns.md +360 -360
- tapps_agents/experts/knowledge/api-design-integration/fastapi-testing.md +262 -262
- tapps_agents/experts/knowledge/api-design-integration/graphql-patterns.md +582 -582
- tapps_agents/experts/knowledge/api-design-integration/grpc-best-practices.md +499 -499
- tapps_agents/experts/knowledge/api-design-integration/mqtt-patterns.md +455 -455
- tapps_agents/experts/knowledge/api-design-integration/rate-limiting.md +507 -507
- tapps_agents/experts/knowledge/api-design-integration/restful-api-design.md +618 -618
- tapps_agents/experts/knowledge/api-design-integration/websocket-patterns.md +480 -480
- tapps_agents/experts/knowledge/cloud-infrastructure/cloud-native-patterns.md +175 -175
- tapps_agents/experts/knowledge/cloud-infrastructure/container-health-checks.md +261 -261
- tapps_agents/experts/knowledge/cloud-infrastructure/containerization.md +222 -222
- tapps_agents/experts/knowledge/cloud-infrastructure/cost-optimization.md +122 -122
- tapps_agents/experts/knowledge/cloud-infrastructure/disaster-recovery.md +153 -153
- tapps_agents/experts/knowledge/cloud-infrastructure/dockerfile-patterns.md +285 -285
- tapps_agents/experts/knowledge/cloud-infrastructure/infrastructure-as-code.md +187 -187
- tapps_agents/experts/knowledge/cloud-infrastructure/kubernetes-patterns.md +253 -253
- tapps_agents/experts/knowledge/cloud-infrastructure/multi-cloud-strategies.md +155 -155
- tapps_agents/experts/knowledge/cloud-infrastructure/serverless-architecture.md +200 -200
- tapps_agents/experts/knowledge/code-quality-analysis/README.md +16 -16
- tapps_agents/experts/knowledge/code-quality-analysis/code-metrics.md +137 -137
- tapps_agents/experts/knowledge/code-quality-analysis/complexity-analysis.md +181 -181
- tapps_agents/experts/knowledge/code-quality-analysis/technical-debt-patterns.md +191 -191
- tapps_agents/experts/knowledge/data-privacy-compliance/anonymization.md +313 -313
- tapps_agents/experts/knowledge/data-privacy-compliance/ccpa.md +255 -255
- tapps_agents/experts/knowledge/data-privacy-compliance/consent-management.md +282 -282
- tapps_agents/experts/knowledge/data-privacy-compliance/data-minimization.md +275 -275
- tapps_agents/experts/knowledge/data-privacy-compliance/data-retention.md +297 -297
- tapps_agents/experts/knowledge/data-privacy-compliance/data-subject-rights.md +383 -383
- tapps_agents/experts/knowledge/data-privacy-compliance/encryption-privacy.md +285 -285
- tapps_agents/experts/knowledge/data-privacy-compliance/gdpr.md +344 -344
- tapps_agents/experts/knowledge/data-privacy-compliance/hipaa.md +385 -385
- tapps_agents/experts/knowledge/data-privacy-compliance/privacy-by-design.md +280 -280
- tapps_agents/experts/knowledge/database-data-management/acid-vs-cap.md +164 -164
- tapps_agents/experts/knowledge/database-data-management/backup-and-recovery.md +182 -182
- tapps_agents/experts/knowledge/database-data-management/data-modeling.md +172 -172
- tapps_agents/experts/knowledge/database-data-management/database-design.md +187 -187
- tapps_agents/experts/knowledge/database-data-management/flux-query-optimization.md +342 -342
- tapps_agents/experts/knowledge/database-data-management/influxdb-connection-patterns.md +432 -432
- tapps_agents/experts/knowledge/database-data-management/influxdb-patterns.md +442 -442
- tapps_agents/experts/knowledge/database-data-management/migration-strategies.md +216 -216
- tapps_agents/experts/knowledge/database-data-management/nosql-patterns.md +259 -259
- tapps_agents/experts/knowledge/database-data-management/scalability-patterns.md +184 -184
- tapps_agents/experts/knowledge/database-data-management/sql-optimization.md +175 -175
- tapps_agents/experts/knowledge/database-data-management/time-series-modeling.md +444 -444
- tapps_agents/experts/knowledge/development-workflow/README.md +16 -16
- tapps_agents/experts/knowledge/development-workflow/automation-best-practices.md +216 -216
- tapps_agents/experts/knowledge/development-workflow/build-strategies.md +198 -198
- tapps_agents/experts/knowledge/development-workflow/deployment-patterns.md +205 -205
- tapps_agents/experts/knowledge/development-workflow/git-workflows.md +205 -205
- tapps_agents/experts/knowledge/documentation-knowledge-management/README.md +16 -16
- tapps_agents/experts/knowledge/documentation-knowledge-management/api-documentation-patterns.md +231 -231
- tapps_agents/experts/knowledge/documentation-knowledge-management/documentation-standards.md +191 -191
- tapps_agents/experts/knowledge/documentation-knowledge-management/knowledge-management.md +171 -171
- tapps_agents/experts/knowledge/documentation-knowledge-management/technical-writing-guide.md +192 -192
- tapps_agents/experts/knowledge/observability-monitoring/alerting-patterns.md +461 -461
- tapps_agents/experts/knowledge/observability-monitoring/apm-tools.md +459 -459
- tapps_agents/experts/knowledge/observability-monitoring/distributed-tracing.md +367 -367
- tapps_agents/experts/knowledge/observability-monitoring/logging-strategies.md +478 -478
- tapps_agents/experts/knowledge/observability-monitoring/metrics-and-monitoring.md +510 -510
- tapps_agents/experts/knowledge/observability-monitoring/observability-best-practices.md +492 -492
- tapps_agents/experts/knowledge/observability-monitoring/open-telemetry.md +573 -573
- tapps_agents/experts/knowledge/observability-monitoring/slo-sli-sla.md +419 -419
- tapps_agents/experts/knowledge/performance/anti-patterns.md +284 -284
- tapps_agents/experts/knowledge/performance/api-performance.md +256 -256
- tapps_agents/experts/knowledge/performance/caching.md +327 -327
- tapps_agents/experts/knowledge/performance/database-performance.md +252 -252
- tapps_agents/experts/knowledge/performance/optimization-patterns.md +327 -327
- tapps_agents/experts/knowledge/performance/profiling.md +297 -297
- tapps_agents/experts/knowledge/performance/resource-management.md +293 -293
- tapps_agents/experts/knowledge/performance/scalability.md +306 -306
- tapps_agents/experts/knowledge/security/owasp-top10.md +209 -209
- tapps_agents/experts/knowledge/security/secure-coding-practices.md +207 -207
- tapps_agents/experts/knowledge/security/threat-modeling.md +220 -220
- tapps_agents/experts/knowledge/security/vulnerability-patterns.md +342 -342
- tapps_agents/experts/knowledge/software-architecture/docker-compose-patterns.md +314 -314
- tapps_agents/experts/knowledge/software-architecture/microservices-patterns.md +379 -379
- tapps_agents/experts/knowledge/software-architecture/service-communication.md +316 -316
- tapps_agents/experts/knowledge/testing/best-practices.md +310 -310
- tapps_agents/experts/knowledge/testing/coverage-analysis.md +293 -293
- tapps_agents/experts/knowledge/testing/mocking.md +256 -256
- tapps_agents/experts/knowledge/testing/test-automation.md +276 -276
- tapps_agents/experts/knowledge/testing/test-data.md +271 -271
- tapps_agents/experts/knowledge/testing/test-design-patterns.md +280 -280
- tapps_agents/experts/knowledge/testing/test-maintenance.md +236 -236
- tapps_agents/experts/knowledge/testing/test-strategies.md +311 -311
- tapps_agents/experts/knowledge/user-experience/information-architecture.md +325 -325
- tapps_agents/experts/knowledge/user-experience/interaction-design.md +363 -363
- tapps_agents/experts/knowledge/user-experience/prototyping.md +293 -293
- tapps_agents/experts/knowledge/user-experience/usability-heuristics.md +337 -337
- tapps_agents/experts/knowledge/user-experience/usability-testing.md +311 -311
- tapps_agents/experts/knowledge/user-experience/user-journeys.md +296 -296
- tapps_agents/experts/knowledge/user-experience/user-research.md +373 -373
- tapps_agents/experts/knowledge/user-experience/ux-principles.md +340 -340
- tapps_agents/experts/knowledge_freshness.py +321 -321
- tapps_agents/experts/knowledge_ingestion.py +438 -438
- tapps_agents/experts/knowledge_need_detector.py +93 -93
- tapps_agents/experts/knowledge_validator.py +382 -382
- tapps_agents/experts/observability.py +440 -440
- tapps_agents/experts/passive_notifier.py +238 -238
- tapps_agents/experts/proactive_orchestrator.py +32 -32
- tapps_agents/experts/rag_chunker.py +205 -205
- tapps_agents/experts/rag_embedder.py +152 -152
- tapps_agents/experts/rag_evaluation.py +299 -299
- tapps_agents/experts/rag_index.py +303 -303
- tapps_agents/experts/rag_metrics.py +293 -293
- tapps_agents/experts/rag_safety.py +263 -263
- tapps_agents/experts/report_generator.py +296 -296
- tapps_agents/experts/setup_wizard.py +441 -441
- tapps_agents/experts/simple_rag.py +431 -431
- tapps_agents/experts/vector_rag.py +354 -354
- tapps_agents/experts/weight_distributor.py +304 -304
- tapps_agents/health/__init__.py +24 -24
- tapps_agents/health/base.py +75 -75
- tapps_agents/health/checks/__init__.py +22 -22
- tapps_agents/health/checks/automation.py +127 -127
- tapps_agents/health/checks/context7_cache.py +210 -210
- tapps_agents/health/checks/environment.py +116 -116
- tapps_agents/health/checks/execution.py +170 -170
- tapps_agents/health/checks/knowledge_base.py +187 -187
- tapps_agents/health/checks/outcomes.py +324 -324
- tapps_agents/health/collector.py +280 -280
- tapps_agents/health/dashboard.py +137 -137
- tapps_agents/health/metrics.py +151 -151
- tapps_agents/health/orchestrator.py +271 -271
- tapps_agents/health/registry.py +166 -166
- tapps_agents/hooks/__init__.py +33 -33
- tapps_agents/hooks/config.py +140 -140
- tapps_agents/hooks/events.py +135 -135
- tapps_agents/hooks/executor.py +128 -128
- tapps_agents/hooks/manager.py +143 -143
- tapps_agents/integration/__init__.py +8 -8
- tapps_agents/integration/service_integrator.py +121 -121
- tapps_agents/integrations/__init__.py +10 -10
- tapps_agents/integrations/clawdbot.py +525 -525
- tapps_agents/integrations/memory_bridge.py +356 -356
- tapps_agents/mcp/__init__.py +18 -18
- tapps_agents/mcp/gateway.py +112 -112
- tapps_agents/mcp/servers/__init__.py +13 -13
- tapps_agents/mcp/servers/analysis.py +204 -204
- tapps_agents/mcp/servers/context7.py +198 -198
- tapps_agents/mcp/servers/filesystem.py +218 -218
- tapps_agents/mcp/servers/git.py +201 -201
- tapps_agents/mcp/tool_registry.py +115 -115
- tapps_agents/quality/__init__.py +54 -54
- tapps_agents/quality/coverage_analyzer.py +379 -379
- tapps_agents/quality/enforcement.py +82 -82
- tapps_agents/quality/gates/__init__.py +37 -37
- tapps_agents/quality/gates/approval_gate.py +255 -255
- tapps_agents/quality/gates/base.py +84 -84
- tapps_agents/quality/gates/exceptions.py +43 -43
- tapps_agents/quality/gates/policy_gate.py +195 -195
- tapps_agents/quality/gates/registry.py +239 -239
- tapps_agents/quality/gates/security_gate.py +156 -156
- tapps_agents/quality/quality_gates.py +369 -369
- tapps_agents/quality/secret_scanner.py +335 -335
- tapps_agents/session/__init__.py +19 -19
- tapps_agents/session/manager.py +256 -256
- tapps_agents/simple_mode/__init__.py +66 -66
- tapps_agents/simple_mode/agent_contracts.py +357 -357
- tapps_agents/simple_mode/beads_hooks.py +151 -151
- tapps_agents/simple_mode/code_snippet_handler.py +382 -382
- tapps_agents/simple_mode/documentation_manager.py +395 -395
- tapps_agents/simple_mode/documentation_reader.py +187 -187
- tapps_agents/simple_mode/file_inference.py +292 -292
- tapps_agents/simple_mode/framework_change_detector.py +268 -268
- tapps_agents/simple_mode/intent_parser.py +510 -510
- tapps_agents/simple_mode/learning_progression.py +358 -358
- tapps_agents/simple_mode/nl_handler.py +700 -700
- tapps_agents/simple_mode/onboarding.py +253 -253
- tapps_agents/simple_mode/orchestrators/__init__.py +38 -38
- tapps_agents/simple_mode/orchestrators/base.py +185 -185
- tapps_agents/simple_mode/orchestrators/breakdown_orchestrator.py +49 -49
- tapps_agents/simple_mode/orchestrators/brownfield_orchestrator.py +135 -135
- tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2700 -2667
- tapps_agents/simple_mode/orchestrators/deliverable_checklist.py +349 -349
- tapps_agents/simple_mode/orchestrators/enhance_orchestrator.py +53 -53
- tapps_agents/simple_mode/orchestrators/epic_orchestrator.py +122 -122
- tapps_agents/simple_mode/orchestrators/explore_orchestrator.py +184 -184
- tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +723 -723
- tapps_agents/simple_mode/orchestrators/plan_analysis_orchestrator.py +206 -206
- tapps_agents/simple_mode/orchestrators/pr_orchestrator.py +237 -237
- tapps_agents/simple_mode/orchestrators/refactor_orchestrator.py +222 -222
- tapps_agents/simple_mode/orchestrators/requirements_tracer.py +262 -262
- tapps_agents/simple_mode/orchestrators/resume_orchestrator.py +210 -210
- tapps_agents/simple_mode/orchestrators/review_orchestrator.py +161 -161
- tapps_agents/simple_mode/orchestrators/test_orchestrator.py +82 -82
- tapps_agents/simple_mode/output_aggregator.py +340 -340
- tapps_agents/simple_mode/result_formatters.py +598 -598
- tapps_agents/simple_mode/step_dependencies.py +382 -382
- tapps_agents/simple_mode/step_results.py +276 -276
- tapps_agents/simple_mode/streaming.py +388 -388
- tapps_agents/simple_mode/variations.py +129 -129
- tapps_agents/simple_mode/visual_feedback.py +238 -238
- tapps_agents/simple_mode/zero_config.py +274 -274
- tapps_agents/suggestions/__init__.py +8 -8
- tapps_agents/suggestions/inline_suggester.py +52 -52
- tapps_agents/templates/__init__.py +8 -8
- tapps_agents/templates/microservice_generator.py +274 -274
- tapps_agents/utils/env_validator.py +291 -291
- tapps_agents/workflow/__init__.py +171 -171
- tapps_agents/workflow/acceptance_verifier.py +132 -132
- tapps_agents/workflow/agent_handlers/__init__.py +41 -41
- tapps_agents/workflow/agent_handlers/analyst_handler.py +75 -75
- tapps_agents/workflow/agent_handlers/architect_handler.py +107 -107
- tapps_agents/workflow/agent_handlers/base.py +84 -84
- tapps_agents/workflow/agent_handlers/debugger_handler.py +100 -100
- tapps_agents/workflow/agent_handlers/designer_handler.py +110 -110
- tapps_agents/workflow/agent_handlers/documenter_handler.py +94 -94
- tapps_agents/workflow/agent_handlers/implementer_handler.py +235 -235
- tapps_agents/workflow/agent_handlers/ops_handler.py +62 -62
- tapps_agents/workflow/agent_handlers/orchestrator_handler.py +43 -43
- tapps_agents/workflow/agent_handlers/planner_handler.py +98 -98
- tapps_agents/workflow/agent_handlers/registry.py +119 -119
- tapps_agents/workflow/agent_handlers/reviewer_handler.py +119 -119
- tapps_agents/workflow/agent_handlers/tester_handler.py +69 -69
- tapps_agents/workflow/analytics_accessor.py +337 -337
- tapps_agents/workflow/analytics_alerts.py +416 -416
- tapps_agents/workflow/analytics_dashboard_cursor.py +281 -281
- tapps_agents/workflow/analytics_dual_write.py +103 -103
- tapps_agents/workflow/analytics_integration.py +119 -119
- tapps_agents/workflow/analytics_query_parser.py +278 -278
- tapps_agents/workflow/analytics_visualizer.py +259 -259
- tapps_agents/workflow/artifact_helper.py +204 -204
- tapps_agents/workflow/audit_logger.py +263 -263
- tapps_agents/workflow/auto_execution_config.py +340 -340
- tapps_agents/workflow/auto_progression.py +586 -586
- tapps_agents/workflow/branch_cleanup.py +349 -349
- tapps_agents/workflow/checkpoint.py +256 -256
- tapps_agents/workflow/checkpoint_manager.py +178 -178
- tapps_agents/workflow/code_artifact.py +179 -179
- tapps_agents/workflow/common_enums.py +96 -96
- tapps_agents/workflow/confirmation_handler.py +130 -130
- tapps_agents/workflow/context_analyzer.py +222 -222
- tapps_agents/workflow/context_artifact.py +230 -230
- tapps_agents/workflow/cursor_chat.py +94 -94
- tapps_agents/workflow/cursor_executor.py +2337 -2196
- tapps_agents/workflow/cursor_skill_helper.py +516 -516
- tapps_agents/workflow/dependency_resolver.py +244 -244
- tapps_agents/workflow/design_artifact.py +156 -156
- tapps_agents/workflow/detector.py +751 -751
- tapps_agents/workflow/direct_execution_fallback.py +301 -301
- tapps_agents/workflow/docs_artifact.py +168 -168
- tapps_agents/workflow/enforcer.py +389 -389
- tapps_agents/workflow/enhancement_artifact.py +142 -142
- tapps_agents/workflow/error_recovery.py +806 -806
- tapps_agents/workflow/event_bus.py +183 -183
- tapps_agents/workflow/event_log.py +612 -612
- tapps_agents/workflow/events.py +63 -63
- tapps_agents/workflow/exceptions.py +43 -43
- tapps_agents/workflow/execution_graph.py +498 -498
- tapps_agents/workflow/execution_plan.py +126 -126
- tapps_agents/workflow/file_utils.py +186 -186
- tapps_agents/workflow/gate_evaluator.py +182 -182
- tapps_agents/workflow/gate_integration.py +200 -200
- tapps_agents/workflow/graph_visualizer.py +130 -130
- tapps_agents/workflow/health_checker.py +206 -206
- tapps_agents/workflow/logging_helper.py +243 -243
- tapps_agents/workflow/manifest.py +582 -582
- tapps_agents/workflow/marker_writer.py +250 -250
- tapps_agents/workflow/message_formatter.py +188 -188
- tapps_agents/workflow/messaging.py +325 -325
- tapps_agents/workflow/metadata_models.py +91 -91
- tapps_agents/workflow/metrics_integration.py +226 -226
- tapps_agents/workflow/migration_utils.py +116 -116
- tapps_agents/workflow/models.py +148 -111
- tapps_agents/workflow/nlp_config.py +198 -198
- tapps_agents/workflow/nlp_error_handler.py +207 -207
- tapps_agents/workflow/nlp_executor.py +163 -163
- tapps_agents/workflow/nlp_parser.py +528 -528
- tapps_agents/workflow/observability_dashboard.py +451 -451
- tapps_agents/workflow/observer.py +170 -170
- tapps_agents/workflow/ops_artifact.py +257 -257
- tapps_agents/workflow/output_passing.py +214 -214
- tapps_agents/workflow/parallel_executor.py +463 -463
- tapps_agents/workflow/planning_artifact.py +179 -179
- tapps_agents/workflow/preset_loader.py +285 -285
- tapps_agents/workflow/preset_recommender.py +270 -270
- tapps_agents/workflow/progress_logger.py +145 -145
- tapps_agents/workflow/progress_manager.py +303 -303
- tapps_agents/workflow/progress_monitor.py +186 -186
- tapps_agents/workflow/progress_updates.py +423 -423
- tapps_agents/workflow/quality_artifact.py +158 -158
- tapps_agents/workflow/quality_loopback.py +101 -101
- tapps_agents/workflow/recommender.py +387 -387
- tapps_agents/workflow/remediation_loop.py +166 -166
- tapps_agents/workflow/result_aggregator.py +300 -300
- tapps_agents/workflow/review_artifact.py +185 -185
- tapps_agents/workflow/schema_validator.py +522 -522
- tapps_agents/workflow/session_handoff.py +178 -178
- tapps_agents/workflow/skill_invoker.py +648 -648
- tapps_agents/workflow/state_manager.py +756 -756
- tapps_agents/workflow/state_persistence_config.py +331 -331
- tapps_agents/workflow/status_monitor.py +449 -449
- tapps_agents/workflow/step_checkpoint.py +314 -314
- tapps_agents/workflow/step_details.py +201 -201
- tapps_agents/workflow/story_models.py +147 -147
- tapps_agents/workflow/streaming.py +416 -416
- tapps_agents/workflow/suggestion_engine.py +552 -552
- tapps_agents/workflow/testing_artifact.py +186 -186
- tapps_agents/workflow/timeline.py +158 -158
- tapps_agents/workflow/token_integration.py +209 -209
- tapps_agents/workflow/validation.py +217 -217
- tapps_agents/workflow/visual_feedback.py +391 -391
- tapps_agents/workflow/workflow_chain.py +95 -95
- tapps_agents/workflow/workflow_summary.py +219 -219
- tapps_agents/workflow/worktree_manager.py +724 -724
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/METADATA +672 -672
- tapps_agents-3.6.0.dist-info/RECORD +758 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/licenses/LICENSE +22 -22
- tapps_agents/health/checks/outcomes.backup_20260204_064058.py +0 -324
- tapps_agents/health/checks/outcomes.backup_20260204_064256.py +0 -324
- tapps_agents/health/checks/outcomes.backup_20260204_064600.py +0 -324
- tapps_agents-3.5.40.dist-info/RECORD +0 -760
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.40.dist-info → tapps_agents-3.6.0.dist-info}/top_level.txt +0 -0
|
@@ -1,612 +1,612 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Append-Only Workflow Event Log
|
|
3
|
-
|
|
4
|
-
Provides durable, append-only event logging for workflow execution.
|
|
5
|
-
Epic 5 / Story 5.4: Workflow State Management
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import json
|
|
9
|
-
import logging
|
|
10
|
-
import os
|
|
11
|
-
import threading
|
|
12
|
-
from dataclasses import asdict, dataclass
|
|
13
|
-
from datetime import UTC, datetime
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import Any, Callable
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclass
|
|
21
|
-
class WorkflowEvent:
|
|
22
|
-
"""A workflow execution event."""
|
|
23
|
-
|
|
24
|
-
event_type: str # workflow_start, workflow_end, step_start, step_finish, step_fail, step_skip
|
|
25
|
-
workflow_id: str
|
|
26
|
-
seq: int # Monotonic sequence number
|
|
27
|
-
timestamp: datetime
|
|
28
|
-
step_id: str | None = None
|
|
29
|
-
agent: str | None = None
|
|
30
|
-
action: str | None = None
|
|
31
|
-
status: str | None = None
|
|
32
|
-
error: str | None = None
|
|
33
|
-
artifacts: dict[str, Any] | None = None
|
|
34
|
-
metadata: dict[str, Any] | None = None
|
|
35
|
-
# Decision logging and trace fields (plan 1.1, 1.4)
|
|
36
|
-
rationale: str | None = None
|
|
37
|
-
input_summary: str | None = None
|
|
38
|
-
criteria: dict[str, Any] | None = None
|
|
39
|
-
skill_name: str | None = None
|
|
40
|
-
model_profile: str | None = None
|
|
41
|
-
artifact_paths: list[str] | None = None
|
|
42
|
-
tool_call_summary: dict[str, Any] | None = None # e.g. command, success, duration_ms
|
|
43
|
-
|
|
44
|
-
def to_dict(self) -> dict[str, Any]:
|
|
45
|
-
"""Convert event to dictionary for JSON serialization."""
|
|
46
|
-
data = asdict(self)
|
|
47
|
-
data["timestamp"] = self.timestamp.isoformat() + "Z"
|
|
48
|
-
return data
|
|
49
|
-
|
|
50
|
-
@classmethod
|
|
51
|
-
def from_dict(cls, data: dict[str, Any]) -> "WorkflowEvent":
|
|
52
|
-
"""Create event from dictionary."""
|
|
53
|
-
data = data.copy()
|
|
54
|
-
if isinstance(data.get("timestamp"), str):
|
|
55
|
-
timestamp_str = data["timestamp"]
|
|
56
|
-
if timestamp_str.endswith("Z"):
|
|
57
|
-
timestamp_str = timestamp_str[:-1]
|
|
58
|
-
data["timestamp"] = datetime.fromisoformat(timestamp_str)
|
|
59
|
-
# Only pass known fields (backward compat with old event format)
|
|
60
|
-
known = {
|
|
61
|
-
"event_type", "workflow_id", "seq", "timestamp", "step_id", "agent",
|
|
62
|
-
"action", "status", "error", "artifacts", "metadata",
|
|
63
|
-
"rationale", "input_summary", "criteria", "skill_name", "model_profile",
|
|
64
|
-
"artifact_paths", "tool_call_summary",
|
|
65
|
-
}
|
|
66
|
-
return cls(**{k: v for k, v in data.items() if k in known})
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
@dataclass
|
|
70
|
-
class EventFilter:
|
|
71
|
-
"""Filter for selecting which events a subscriber should receive."""
|
|
72
|
-
|
|
73
|
-
event_types: list[str] | None = None # None = all types
|
|
74
|
-
step_ids: list[str] | None = None # None = all steps
|
|
75
|
-
agents: list[str] | None = None # None = all agents
|
|
76
|
-
custom_filter: Callable[[WorkflowEvent], bool] | None = None # Custom filter function
|
|
77
|
-
|
|
78
|
-
def matches(self, event: WorkflowEvent) -> bool:
|
|
79
|
-
"""Check if event matches this filter."""
|
|
80
|
-
if self.event_types and event.event_type not in self.event_types:
|
|
81
|
-
return False
|
|
82
|
-
if self.step_ids and event.step_id not in self.step_ids:
|
|
83
|
-
return False
|
|
84
|
-
if self.agents and event.agent not in self.agents:
|
|
85
|
-
return False
|
|
86
|
-
if self.custom_filter and not self.custom_filter(event):
|
|
87
|
-
return False
|
|
88
|
-
return True
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@dataclass
|
|
92
|
-
class Subscription:
|
|
93
|
-
"""Represents an event subscription."""
|
|
94
|
-
|
|
95
|
-
callback: Callable[[WorkflowEvent], None]
|
|
96
|
-
filter: EventFilter | None
|
|
97
|
-
subscription_id: str
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class EventStream:
|
|
101
|
-
"""Real-time event stream for consuming workflow events."""
|
|
102
|
-
|
|
103
|
-
def __init__(self):
|
|
104
|
-
"""Initialize event stream."""
|
|
105
|
-
self._subscribers: list[Subscription] = []
|
|
106
|
-
self._lock = threading.Lock()
|
|
107
|
-
self._event_buffer: dict[str, list[WorkflowEvent]] = {} # workflow_id -> events
|
|
108
|
-
self._buffer_limit = 1000 # Keep last 1000 events per workflow in memory
|
|
109
|
-
|
|
110
|
-
def subscribe(
|
|
111
|
-
self,
|
|
112
|
-
callback: Callable[[WorkflowEvent], None],
|
|
113
|
-
filter: EventFilter | None = None,
|
|
114
|
-
subscription_id: str | None = None,
|
|
115
|
-
) -> Subscription:
|
|
116
|
-
"""
|
|
117
|
-
Subscribe to events with optional filter.
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
callback: Function to call when matching events occur
|
|
121
|
-
filter: Optional filter to limit which events trigger callback
|
|
122
|
-
subscription_id: Optional ID for this subscription
|
|
123
|
-
|
|
124
|
-
Returns:
|
|
125
|
-
Subscription object that can be used to unsubscribe
|
|
126
|
-
"""
|
|
127
|
-
if subscription_id is None:
|
|
128
|
-
import uuid
|
|
129
|
-
|
|
130
|
-
subscription_id = str(uuid.uuid4())
|
|
131
|
-
|
|
132
|
-
subscription = Subscription(
|
|
133
|
-
callback=callback, filter=filter, subscription_id=subscription_id
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
with self._lock:
|
|
137
|
-
self._subscribers.append(subscription)
|
|
138
|
-
|
|
139
|
-
logger.debug(f"Subscribed to event stream: {subscription_id}")
|
|
140
|
-
return subscription
|
|
141
|
-
|
|
142
|
-
def unsubscribe(self, subscription: Subscription) -> None:
|
|
143
|
-
"""
|
|
144
|
-
Unsubscribe from events.
|
|
145
|
-
|
|
146
|
-
Args:
|
|
147
|
-
subscription: Subscription to remove
|
|
148
|
-
"""
|
|
149
|
-
with self._lock:
|
|
150
|
-
self._subscribers = [
|
|
151
|
-
s for s in self._subscribers if s.subscription_id != subscription.subscription_id
|
|
152
|
-
]
|
|
153
|
-
logger.debug(f"Unsubscribed from event stream: {subscription.subscription_id}")
|
|
154
|
-
|
|
155
|
-
def emit(self, event: WorkflowEvent) -> None:
|
|
156
|
-
"""
|
|
157
|
-
Emit an event to all subscribers.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
event: Event to emit
|
|
161
|
-
"""
|
|
162
|
-
# Add to in-memory buffer
|
|
163
|
-
with self._lock:
|
|
164
|
-
if event.workflow_id not in self._event_buffer:
|
|
165
|
-
self._event_buffer[event.workflow_id] = []
|
|
166
|
-
self._event_buffer[event.workflow_id].append(event)
|
|
167
|
-
|
|
168
|
-
# Limit buffer size
|
|
169
|
-
if len(self._event_buffer[event.workflow_id]) > self._buffer_limit:
|
|
170
|
-
self._event_buffer[event.workflow_id] = self._event_buffer[
|
|
171
|
-
event.workflow_id
|
|
172
|
-
][-self._buffer_limit :]
|
|
173
|
-
|
|
174
|
-
# Get subscribers to notify (copy to avoid holding lock during callback)
|
|
175
|
-
subscribers_to_notify = list(self._subscribers)
|
|
176
|
-
|
|
177
|
-
# Notify subscribers (outside lock to avoid deadlocks)
|
|
178
|
-
for subscription in subscribers_to_notify:
|
|
179
|
-
try:
|
|
180
|
-
if subscription.filter is None or subscription.filter.matches(event):
|
|
181
|
-
subscription.callback(event)
|
|
182
|
-
except Exception as e:
|
|
183
|
-
# Don't let one subscriber's exception break others
|
|
184
|
-
logger.error(
|
|
185
|
-
f"Subscriber {subscription.subscription_id} raised exception: {e}",
|
|
186
|
-
exc_info=True,
|
|
187
|
-
extra={"event_type": event.event_type, "workflow_id": event.workflow_id},
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
def get_latest_events(self, workflow_id: str, limit: int = 100) -> list[WorkflowEvent]:
|
|
191
|
-
"""
|
|
192
|
-
Get latest events for a workflow from in-memory buffer.
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
workflow_id: Workflow ID
|
|
196
|
-
limit: Maximum number of events to return
|
|
197
|
-
|
|
198
|
-
Returns:
|
|
199
|
-
List of events, most recent first
|
|
200
|
-
"""
|
|
201
|
-
with self._lock:
|
|
202
|
-
events = self._event_buffer.get(workflow_id, [])
|
|
203
|
-
return list(reversed(events[-limit:]))
|
|
204
|
-
|
|
205
|
-
def clear_buffer(self, workflow_id: str | None = None) -> None:
|
|
206
|
-
"""
|
|
207
|
-
Clear event buffer.
|
|
208
|
-
|
|
209
|
-
Args:
|
|
210
|
-
workflow_id: If provided, clear only this workflow's buffer. Otherwise clear all.
|
|
211
|
-
"""
|
|
212
|
-
with self._lock:
|
|
213
|
-
if workflow_id:
|
|
214
|
-
self._event_buffer.pop(workflow_id, None)
|
|
215
|
-
else:
|
|
216
|
-
self._event_buffer.clear()
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
class WorkflowEventLog:
|
|
220
|
-
"""Manages append-only event log for workflow execution."""
|
|
221
|
-
|
|
222
|
-
def __init__(self, events_dir: Path, enable_streaming: bool = True):
|
|
223
|
-
"""
|
|
224
|
-
Initialize event log.
|
|
225
|
-
|
|
226
|
-
Args:
|
|
227
|
-
events_dir: Directory to store event log files
|
|
228
|
-
enable_streaming: Whether to enable in-memory event streaming
|
|
229
|
-
"""
|
|
230
|
-
self.events_dir = Path(events_dir)
|
|
231
|
-
self.events_dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
-
self._sequence_counter: dict[str, int] = {}
|
|
233
|
-
self._stream: EventStream | None = EventStream() if enable_streaming else None
|
|
234
|
-
|
|
235
|
-
def _get_event_file(self, workflow_id: str) -> Path:
|
|
236
|
-
"""Get event log file path for a workflow."""
|
|
237
|
-
return self.events_dir / f"{workflow_id}.events.jsonl"
|
|
238
|
-
|
|
239
|
-
def _get_next_sequence(self, workflow_id: str) -> int:
|
|
240
|
-
"""Get next sequence number for a workflow."""
|
|
241
|
-
if workflow_id not in self._sequence_counter:
|
|
242
|
-
# Try to read last sequence from file
|
|
243
|
-
event_file = self._get_event_file(workflow_id)
|
|
244
|
-
if event_file.exists():
|
|
245
|
-
try:
|
|
246
|
-
last_seq = 0
|
|
247
|
-
with open(event_file, encoding="utf-8") as f:
|
|
248
|
-
for line in f:
|
|
249
|
-
if line.strip():
|
|
250
|
-
event_data = json.loads(line)
|
|
251
|
-
last_seq = max(last_seq, event_data.get("seq", 0))
|
|
252
|
-
self._sequence_counter[workflow_id] = last_seq
|
|
253
|
-
except Exception as e:
|
|
254
|
-
logger.warning(
|
|
255
|
-
f"Failed to read sequence from {event_file}: {e}",
|
|
256
|
-
exc_info=True,
|
|
257
|
-
)
|
|
258
|
-
self._sequence_counter[workflow_id] = 0
|
|
259
|
-
else:
|
|
260
|
-
self._sequence_counter[workflow_id] = 0
|
|
261
|
-
|
|
262
|
-
self._sequence_counter[workflow_id] += 1
|
|
263
|
-
return self._sequence_counter[workflow_id]
|
|
264
|
-
|
|
265
|
-
def emit_event(
|
|
266
|
-
self,
|
|
267
|
-
event_type: str,
|
|
268
|
-
workflow_id: str,
|
|
269
|
-
step_id: str | None = None,
|
|
270
|
-
agent: str | None = None,
|
|
271
|
-
action: str | None = None,
|
|
272
|
-
status: str | None = None,
|
|
273
|
-
error: str | None = None,
|
|
274
|
-
artifacts: dict[str, Any] | None = None,
|
|
275
|
-
metadata: dict[str, Any] | None = None,
|
|
276
|
-
*,
|
|
277
|
-
rationale: str | None = None,
|
|
278
|
-
input_summary: str | None = None,
|
|
279
|
-
criteria: dict[str, Any] | None = None,
|
|
280
|
-
skill_name: str | None = None,
|
|
281
|
-
model_profile: str | None = None,
|
|
282
|
-
artifact_paths: list[str] | None = None,
|
|
283
|
-
tool_call_summary: dict[str, Any] | None = None,
|
|
284
|
-
) -> WorkflowEvent:
|
|
285
|
-
"""
|
|
286
|
-
Emit a workflow event (append-only write).
|
|
287
|
-
|
|
288
|
-
Args:
|
|
289
|
-
event_type: Type of event (workflow_start, workflow_end, step_start, step_finish, step_fail, step_skip)
|
|
290
|
-
workflow_id: Workflow ID
|
|
291
|
-
step_id: Step ID (if applicable)
|
|
292
|
-
agent: Agent name (if applicable)
|
|
293
|
-
action: Action name (if applicable)
|
|
294
|
-
status: Status (if applicable)
|
|
295
|
-
error: Error message (if applicable)
|
|
296
|
-
artifacts: Artifact summaries (if applicable)
|
|
297
|
-
metadata: Additional metadata
|
|
298
|
-
rationale: Optional decision rationale (decision logging)
|
|
299
|
-
input_summary: Optional input summary (decision logging)
|
|
300
|
-
criteria: Optional criteria dict (decision logging)
|
|
301
|
-
skill_name: Optional skill name (trace)
|
|
302
|
-
model_profile: Optional model profile (trace)
|
|
303
|
-
artifact_paths: Optional list of artifact paths produced (trace)
|
|
304
|
-
tool_call_summary: Optional dict with command, success, duration_ms (trace)
|
|
305
|
-
|
|
306
|
-
Returns:
|
|
307
|
-
Created WorkflowEvent
|
|
308
|
-
"""
|
|
309
|
-
seq = self._get_next_sequence(workflow_id)
|
|
310
|
-
event = WorkflowEvent(
|
|
311
|
-
event_type=event_type,
|
|
312
|
-
workflow_id=workflow_id,
|
|
313
|
-
seq=seq,
|
|
314
|
-
timestamp=datetime.now(UTC),
|
|
315
|
-
step_id=step_id,
|
|
316
|
-
agent=agent,
|
|
317
|
-
action=action,
|
|
318
|
-
status=status,
|
|
319
|
-
error=error,
|
|
320
|
-
artifacts=artifacts,
|
|
321
|
-
metadata=metadata,
|
|
322
|
-
rationale=rationale,
|
|
323
|
-
input_summary=input_summary,
|
|
324
|
-
criteria=criteria,
|
|
325
|
-
skill_name=skill_name,
|
|
326
|
-
model_profile=model_profile,
|
|
327
|
-
artifact_paths=artifact_paths,
|
|
328
|
-
tool_call_summary=tool_call_summary,
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
# Append to event log file (best-effort, non-blocking)
|
|
332
|
-
try:
|
|
333
|
-
event_file = self._get_event_file(workflow_id)
|
|
334
|
-
with open(event_file, "a", encoding="utf-8") as f:
|
|
335
|
-
# Use atomic write: write to temp file, then rename
|
|
336
|
-
# For append-only, we'll use a simpler approach with file locking
|
|
337
|
-
# In practice, JSONL append is atomic on most filesystems
|
|
338
|
-
json_line = json.dumps(event.to_dict(), ensure_ascii=False)
|
|
339
|
-
f.write(json_line + "\n")
|
|
340
|
-
f.flush()
|
|
341
|
-
os.fsync(f.fileno()) # Ensure durability
|
|
342
|
-
except Exception as e:
|
|
343
|
-
# Log error but don't fail workflow execution
|
|
344
|
-
logger.error(
|
|
345
|
-
f"Failed to write event to log: {e}",
|
|
346
|
-
exc_info=True,
|
|
347
|
-
extra={
|
|
348
|
-
"workflow_id": workflow_id,
|
|
349
|
-
"event_type": event_type,
|
|
350
|
-
"step_id": step_id,
|
|
351
|
-
},
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
# Emit to stream subscribers (real-time notification)
|
|
355
|
-
if self._stream:
|
|
356
|
-
self._stream.emit(event)
|
|
357
|
-
|
|
358
|
-
return event
|
|
359
|
-
|
|
360
|
-
def read_events(
|
|
361
|
-
self, workflow_id: str, limit: int | None = None
|
|
362
|
-
) -> list[WorkflowEvent]:
|
|
363
|
-
"""
|
|
364
|
-
Read events for a workflow.
|
|
365
|
-
|
|
366
|
-
Args:
|
|
367
|
-
workflow_id: Workflow ID
|
|
368
|
-
limit: Maximum number of events to read (None = all)
|
|
369
|
-
|
|
370
|
-
Returns:
|
|
371
|
-
List of events, ordered by sequence number
|
|
372
|
-
"""
|
|
373
|
-
event_file = self._get_event_file(workflow_id)
|
|
374
|
-
if not event_file.exists():
|
|
375
|
-
return []
|
|
376
|
-
|
|
377
|
-
events: list[WorkflowEvent] = []
|
|
378
|
-
try:
|
|
379
|
-
with open(event_file, encoding="utf-8") as f:
|
|
380
|
-
for line in f:
|
|
381
|
-
if line.strip():
|
|
382
|
-
try:
|
|
383
|
-
event_data = json.loads(line)
|
|
384
|
-
event = WorkflowEvent.from_dict(event_data)
|
|
385
|
-
events.append(event)
|
|
386
|
-
except Exception as e:
|
|
387
|
-
logger.warning(
|
|
388
|
-
f"Failed to parse event line: {e}",
|
|
389
|
-
extra={"workflow_id": workflow_id, "line": line[:100]},
|
|
390
|
-
)
|
|
391
|
-
continue
|
|
392
|
-
|
|
393
|
-
# Sort by sequence number (should already be sorted, but ensure it)
|
|
394
|
-
events.sort(key=lambda e: e.seq)
|
|
395
|
-
|
|
396
|
-
if limit:
|
|
397
|
-
events = events[-limit:] # Get most recent events
|
|
398
|
-
|
|
399
|
-
except Exception as e:
|
|
400
|
-
logger.error(
|
|
401
|
-
f"Failed to read events from {event_file}: {e}",
|
|
402
|
-
exc_info=True,
|
|
403
|
-
extra={"workflow_id": workflow_id},
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
return events
|
|
407
|
-
|
|
408
|
-
def get_execution_history(
|
|
409
|
-
self, workflow_id: str
|
|
410
|
-
) -> dict[str, Any]:
|
|
411
|
-
"""
|
|
412
|
-
Generate human-readable execution history from event log.
|
|
413
|
-
|
|
414
|
-
Args:
|
|
415
|
-
workflow_id: Workflow ID
|
|
416
|
-
|
|
417
|
-
Returns:
|
|
418
|
-
Dictionary with execution history summary
|
|
419
|
-
"""
|
|
420
|
-
events = self.read_events(workflow_id)
|
|
421
|
-
if not events:
|
|
422
|
-
return {
|
|
423
|
-
"workflow_id": workflow_id,
|
|
424
|
-
"events": [],
|
|
425
|
-
"summary": "No events found",
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
workflow_start = next(
|
|
429
|
-
(e for e in events if e.event_type == "workflow_start"), None
|
|
430
|
-
)
|
|
431
|
-
workflow_end = next(
|
|
432
|
-
(e for e in events if e.event_type == "workflow_end"), None
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
step_events = [
|
|
436
|
-
e
|
|
437
|
-
for e in events
|
|
438
|
-
if e.event_type in ("step_start", "step_finish", "step_fail", "step_skip")
|
|
439
|
-
]
|
|
440
|
-
|
|
441
|
-
# Group step events by step_id
|
|
442
|
-
step_history: dict[str, list[WorkflowEvent]] = {}
|
|
443
|
-
for event in step_events:
|
|
444
|
-
if event.step_id:
|
|
445
|
-
if event.step_id not in step_history:
|
|
446
|
-
step_history[event.step_id] = []
|
|
447
|
-
step_history[event.step_id].append(event)
|
|
448
|
-
|
|
449
|
-
# Calculate duration if both start and end exist
|
|
450
|
-
duration_seconds: float | None = None
|
|
451
|
-
if workflow_start and workflow_end:
|
|
452
|
-
duration_seconds = (
|
|
453
|
-
workflow_end.timestamp - workflow_start.timestamp
|
|
454
|
-
).total_seconds()
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
"workflow_id": workflow_id,
|
|
458
|
-
"started_at": workflow_start.timestamp.isoformat() + "Z"
|
|
459
|
-
if workflow_start
|
|
460
|
-
else None,
|
|
461
|
-
"ended_at": workflow_end.timestamp.isoformat() + "Z" if workflow_end else None,
|
|
462
|
-
"duration_seconds": duration_seconds,
|
|
463
|
-
"status": workflow_end.status if workflow_end else "running",
|
|
464
|
-
"total_events": len(events),
|
|
465
|
-
"step_count": len(step_history),
|
|
466
|
-
"steps": {
|
|
467
|
-
step_id: [
|
|
468
|
-
{
|
|
469
|
-
"event_type": e.event_type,
|
|
470
|
-
"timestamp": e.timestamp.isoformat() + "Z",
|
|
471
|
-
"status": e.status,
|
|
472
|
-
"error": e.error,
|
|
473
|
-
}
|
|
474
|
-
for e in sorted(events, key=lambda e: e.seq)
|
|
475
|
-
]
|
|
476
|
-
for step_id, events in step_history.items()
|
|
477
|
-
},
|
|
478
|
-
"events": [
|
|
479
|
-
{
|
|
480
|
-
"seq": e.seq,
|
|
481
|
-
"event_type": e.event_type,
|
|
482
|
-
"timestamp": e.timestamp.isoformat() + "Z",
|
|
483
|
-
"step_id": e.step_id,
|
|
484
|
-
"agent": e.agent,
|
|
485
|
-
"action": e.action,
|
|
486
|
-
"status": e.status,
|
|
487
|
-
"error": e.error,
|
|
488
|
-
}
|
|
489
|
-
for e in events
|
|
490
|
-
],
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
def get_execution_trace(self, workflow_id: str) -> dict[str, Any]:
|
|
494
|
-
"""
|
|
495
|
-
Derive a structured execution trace from events for metrics/AgentOps.
|
|
496
|
-
|
|
497
|
-
Args:
|
|
498
|
-
workflow_id: Workflow ID
|
|
499
|
-
|
|
500
|
-
Returns:
|
|
501
|
-
Dict with workflow_id, started_at, ended_at, steps (list of
|
|
502
|
-
step_id, agent, skill_name, action, started_at, ended_at,
|
|
503
|
-
duration_ms, status, tool_calls, artifact_paths, error).
|
|
504
|
-
"""
|
|
505
|
-
events = self.read_events(workflow_id)
|
|
506
|
-
if not events:
|
|
507
|
-
return {"workflow_id": workflow_id, "steps": []}
|
|
508
|
-
|
|
509
|
-
workflow_start = next((e for e in events if e.event_type == "workflow_start"), None)
|
|
510
|
-
workflow_end = next((e for e in events if e.event_type == "workflow_end"), None)
|
|
511
|
-
step_evts = [
|
|
512
|
-
e for e in events
|
|
513
|
-
if e.event_type in ("step_start", "step_finish", "step_fail", "step_skip")
|
|
514
|
-
]
|
|
515
|
-
|
|
516
|
-
# Build steps: group by step_id, pair start with finish/fail/skip
|
|
517
|
-
steps_d: dict[str, dict[str, Any]] = {}
|
|
518
|
-
for e in step_evts:
|
|
519
|
-
if not e.step_id:
|
|
520
|
-
continue
|
|
521
|
-
if e.step_id not in steps_d:
|
|
522
|
-
steps_d[e.step_id] = {
|
|
523
|
-
"step_id": e.step_id,
|
|
524
|
-
"agent": e.agent,
|
|
525
|
-
"skill_name": e.skill_name,
|
|
526
|
-
"action": e.action,
|
|
527
|
-
"started_at": None,
|
|
528
|
-
"ended_at": None,
|
|
529
|
-
"duration_ms": None,
|
|
530
|
-
"status": e.status or "unknown",
|
|
531
|
-
"tool_call_summary": e.tool_call_summary,
|
|
532
|
-
"artifact_paths": e.artifact_paths or [],
|
|
533
|
-
"error": e.error,
|
|
534
|
-
}
|
|
535
|
-
s = steps_d[e.step_id]
|
|
536
|
-
if e.event_type == "step_start":
|
|
537
|
-
s["started_at"] = e.timestamp.isoformat() + "Z"
|
|
538
|
-
else:
|
|
539
|
-
s["ended_at"] = e.timestamp.isoformat() + "Z"
|
|
540
|
-
s["status"] = e.status or s["status"]
|
|
541
|
-
s["tool_call_summary"] = e.tool_call_summary or s["tool_call_summary"]
|
|
542
|
-
s["artifact_paths"] = e.artifact_paths or s["artifact_paths"]
|
|
543
|
-
s["error"] = e.error or s["error"]
|
|
544
|
-
|
|
545
|
-
for s in steps_d.values():
|
|
546
|
-
if s["started_at"] and s["ended_at"]:
|
|
547
|
-
try:
|
|
548
|
-
start = datetime.fromisoformat(s["started_at"].replace("Z", "+00:00"))
|
|
549
|
-
end = datetime.fromisoformat(s["ended_at"].replace("Z", "+00:00"))
|
|
550
|
-
s["duration_ms"] = (end - start).total_seconds() * 1000
|
|
551
|
-
except Exception:
|
|
552
|
-
pass
|
|
553
|
-
|
|
554
|
-
return {
|
|
555
|
-
"workflow_id": workflow_id,
|
|
556
|
-
"started_at": workflow_start.timestamp.isoformat() + "Z" if workflow_start else None,
|
|
557
|
-
"ended_at": workflow_end.timestamp.isoformat() + "Z" if workflow_end else None,
|
|
558
|
-
"steps": list(steps_d.values()),
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
def subscribe(
|
|
562
|
-
self,
|
|
563
|
-
callback: Callable[[WorkflowEvent], None],
|
|
564
|
-
filter: EventFilter | None = None,
|
|
565
|
-
subscription_id: str | None = None,
|
|
566
|
-
) -> Subscription | None:
|
|
567
|
-
"""
|
|
568
|
-
Subscribe to real-time workflow events.
|
|
569
|
-
|
|
570
|
-
Args:
|
|
571
|
-
callback: Function to call when matching events occur
|
|
572
|
-
filter: Optional filter to limit which events trigger callback
|
|
573
|
-
subscription_id: Optional ID for this subscription
|
|
574
|
-
|
|
575
|
-
Returns:
|
|
576
|
-
Subscription object that can be used to unsubscribe, or None if streaming disabled
|
|
577
|
-
"""
|
|
578
|
-
if not self._stream:
|
|
579
|
-
logger.warning("Event streaming is disabled")
|
|
580
|
-
return None
|
|
581
|
-
return self._stream.subscribe(callback, filter, subscription_id)
|
|
582
|
-
|
|
583
|
-
def get_latest_events(self, workflow_id: str, limit: int = 100) -> list[WorkflowEvent]:
|
|
584
|
-
"""
|
|
585
|
-
Get latest events for a workflow from in-memory buffer.
|
|
586
|
-
|
|
587
|
-
Args:
|
|
588
|
-
workflow_id: Workflow ID
|
|
589
|
-
limit: Maximum number of events to return
|
|
590
|
-
|
|
591
|
-
Returns:
|
|
592
|
-
List of events, most recent first
|
|
593
|
-
"""
|
|
594
|
-
if not self._stream:
|
|
595
|
-
# Fallback to reading from file
|
|
596
|
-
return list(reversed(self.read_events(workflow_id, limit=limit)))
|
|
597
|
-
return self._stream.get_latest_events(workflow_id, limit)
|
|
598
|
-
|
|
599
|
-
def generate_execution_graph(self, workflow_id: str) -> "ExecutionGraph":
|
|
600
|
-
"""
|
|
601
|
-
Generate execution graph from event log.
|
|
602
|
-
|
|
603
|
-
Args:
|
|
604
|
-
workflow_id: Workflow ID
|
|
605
|
-
|
|
606
|
-
Returns:
|
|
607
|
-
ExecutionGraph instance
|
|
608
|
-
"""
|
|
609
|
-
from .execution_graph import ExecutionGraphGenerator
|
|
610
|
-
|
|
611
|
-
generator = ExecutionGraphGenerator(event_log=self)
|
|
612
|
-
return generator.generate_graph(workflow_id)
|
|
1
|
+
"""
|
|
2
|
+
Append-Only Workflow Event Log
|
|
3
|
+
|
|
4
|
+
Provides durable, append-only event logging for workflow execution.
|
|
5
|
+
Epic 5 / Story 5.4: Workflow State Management
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import threading
|
|
12
|
+
from dataclasses import asdict, dataclass
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class WorkflowEvent:
|
|
22
|
+
"""A workflow execution event."""
|
|
23
|
+
|
|
24
|
+
event_type: str # workflow_start, workflow_end, step_start, step_finish, step_fail, step_skip
|
|
25
|
+
workflow_id: str
|
|
26
|
+
seq: int # Monotonic sequence number
|
|
27
|
+
timestamp: datetime
|
|
28
|
+
step_id: str | None = None
|
|
29
|
+
agent: str | None = None
|
|
30
|
+
action: str | None = None
|
|
31
|
+
status: str | None = None
|
|
32
|
+
error: str | None = None
|
|
33
|
+
artifacts: dict[str, Any] | None = None
|
|
34
|
+
metadata: dict[str, Any] | None = None
|
|
35
|
+
# Decision logging and trace fields (plan 1.1, 1.4)
|
|
36
|
+
rationale: str | None = None
|
|
37
|
+
input_summary: str | None = None
|
|
38
|
+
criteria: dict[str, Any] | None = None
|
|
39
|
+
skill_name: str | None = None
|
|
40
|
+
model_profile: str | None = None
|
|
41
|
+
artifact_paths: list[str] | None = None
|
|
42
|
+
tool_call_summary: dict[str, Any] | None = None # e.g. command, success, duration_ms
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict[str, Any]:
|
|
45
|
+
"""Convert event to dictionary for JSON serialization."""
|
|
46
|
+
data = asdict(self)
|
|
47
|
+
data["timestamp"] = self.timestamp.isoformat() + "Z"
|
|
48
|
+
return data
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_dict(cls, data: dict[str, Any]) -> "WorkflowEvent":
|
|
52
|
+
"""Create event from dictionary."""
|
|
53
|
+
data = data.copy()
|
|
54
|
+
if isinstance(data.get("timestamp"), str):
|
|
55
|
+
timestamp_str = data["timestamp"]
|
|
56
|
+
if timestamp_str.endswith("Z"):
|
|
57
|
+
timestamp_str = timestamp_str[:-1]
|
|
58
|
+
data["timestamp"] = datetime.fromisoformat(timestamp_str)
|
|
59
|
+
# Only pass known fields (backward compat with old event format)
|
|
60
|
+
known = {
|
|
61
|
+
"event_type", "workflow_id", "seq", "timestamp", "step_id", "agent",
|
|
62
|
+
"action", "status", "error", "artifacts", "metadata",
|
|
63
|
+
"rationale", "input_summary", "criteria", "skill_name", "model_profile",
|
|
64
|
+
"artifact_paths", "tool_call_summary",
|
|
65
|
+
}
|
|
66
|
+
return cls(**{k: v for k, v in data.items() if k in known})
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class EventFilter:
|
|
71
|
+
"""Filter for selecting which events a subscriber should receive."""
|
|
72
|
+
|
|
73
|
+
event_types: list[str] | None = None # None = all types
|
|
74
|
+
step_ids: list[str] | None = None # None = all steps
|
|
75
|
+
agents: list[str] | None = None # None = all agents
|
|
76
|
+
custom_filter: Callable[[WorkflowEvent], bool] | None = None # Custom filter function
|
|
77
|
+
|
|
78
|
+
def matches(self, event: WorkflowEvent) -> bool:
|
|
79
|
+
"""Check if event matches this filter."""
|
|
80
|
+
if self.event_types and event.event_type not in self.event_types:
|
|
81
|
+
return False
|
|
82
|
+
if self.step_ids and event.step_id not in self.step_ids:
|
|
83
|
+
return False
|
|
84
|
+
if self.agents and event.agent not in self.agents:
|
|
85
|
+
return False
|
|
86
|
+
if self.custom_filter and not self.custom_filter(event):
|
|
87
|
+
return False
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class Subscription:
|
|
93
|
+
"""Represents an event subscription."""
|
|
94
|
+
|
|
95
|
+
callback: Callable[[WorkflowEvent], None]
|
|
96
|
+
filter: EventFilter | None
|
|
97
|
+
subscription_id: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class EventStream:
|
|
101
|
+
"""Real-time event stream for consuming workflow events."""
|
|
102
|
+
|
|
103
|
+
def __init__(self):
|
|
104
|
+
"""Initialize event stream."""
|
|
105
|
+
self._subscribers: list[Subscription] = []
|
|
106
|
+
self._lock = threading.Lock()
|
|
107
|
+
self._event_buffer: dict[str, list[WorkflowEvent]] = {} # workflow_id -> events
|
|
108
|
+
self._buffer_limit = 1000 # Keep last 1000 events per workflow in memory
|
|
109
|
+
|
|
110
|
+
def subscribe(
|
|
111
|
+
self,
|
|
112
|
+
callback: Callable[[WorkflowEvent], None],
|
|
113
|
+
filter: EventFilter | None = None,
|
|
114
|
+
subscription_id: str | None = None,
|
|
115
|
+
) -> Subscription:
|
|
116
|
+
"""
|
|
117
|
+
Subscribe to events with optional filter.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
callback: Function to call when matching events occur
|
|
121
|
+
filter: Optional filter to limit which events trigger callback
|
|
122
|
+
subscription_id: Optional ID for this subscription
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Subscription object that can be used to unsubscribe
|
|
126
|
+
"""
|
|
127
|
+
if subscription_id is None:
|
|
128
|
+
import uuid
|
|
129
|
+
|
|
130
|
+
subscription_id = str(uuid.uuid4())
|
|
131
|
+
|
|
132
|
+
subscription = Subscription(
|
|
133
|
+
callback=callback, filter=filter, subscription_id=subscription_id
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
with self._lock:
|
|
137
|
+
self._subscribers.append(subscription)
|
|
138
|
+
|
|
139
|
+
logger.debug(f"Subscribed to event stream: {subscription_id}")
|
|
140
|
+
return subscription
|
|
141
|
+
|
|
142
|
+
def unsubscribe(self, subscription: Subscription) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Unsubscribe from events.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
subscription: Subscription to remove
|
|
148
|
+
"""
|
|
149
|
+
with self._lock:
|
|
150
|
+
self._subscribers = [
|
|
151
|
+
s for s in self._subscribers if s.subscription_id != subscription.subscription_id
|
|
152
|
+
]
|
|
153
|
+
logger.debug(f"Unsubscribed from event stream: {subscription.subscription_id}")
|
|
154
|
+
|
|
155
|
+
def emit(self, event: WorkflowEvent) -> None:
|
|
156
|
+
"""
|
|
157
|
+
Emit an event to all subscribers.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
event: Event to emit
|
|
161
|
+
"""
|
|
162
|
+
# Add to in-memory buffer
|
|
163
|
+
with self._lock:
|
|
164
|
+
if event.workflow_id not in self._event_buffer:
|
|
165
|
+
self._event_buffer[event.workflow_id] = []
|
|
166
|
+
self._event_buffer[event.workflow_id].append(event)
|
|
167
|
+
|
|
168
|
+
# Limit buffer size
|
|
169
|
+
if len(self._event_buffer[event.workflow_id]) > self._buffer_limit:
|
|
170
|
+
self._event_buffer[event.workflow_id] = self._event_buffer[
|
|
171
|
+
event.workflow_id
|
|
172
|
+
][-self._buffer_limit :]
|
|
173
|
+
|
|
174
|
+
# Get subscribers to notify (copy to avoid holding lock during callback)
|
|
175
|
+
subscribers_to_notify = list(self._subscribers)
|
|
176
|
+
|
|
177
|
+
# Notify subscribers (outside lock to avoid deadlocks)
|
|
178
|
+
for subscription in subscribers_to_notify:
|
|
179
|
+
try:
|
|
180
|
+
if subscription.filter is None or subscription.filter.matches(event):
|
|
181
|
+
subscription.callback(event)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
# Don't let one subscriber's exception break others
|
|
184
|
+
logger.error(
|
|
185
|
+
f"Subscriber {subscription.subscription_id} raised exception: {e}",
|
|
186
|
+
exc_info=True,
|
|
187
|
+
extra={"event_type": event.event_type, "workflow_id": event.workflow_id},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def get_latest_events(self, workflow_id: str, limit: int = 100) -> list[WorkflowEvent]:
|
|
191
|
+
"""
|
|
192
|
+
Get latest events for a workflow from in-memory buffer.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
workflow_id: Workflow ID
|
|
196
|
+
limit: Maximum number of events to return
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of events, most recent first
|
|
200
|
+
"""
|
|
201
|
+
with self._lock:
|
|
202
|
+
events = self._event_buffer.get(workflow_id, [])
|
|
203
|
+
return list(reversed(events[-limit:]))
|
|
204
|
+
|
|
205
|
+
def clear_buffer(self, workflow_id: str | None = None) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Clear event buffer.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
workflow_id: If provided, clear only this workflow's buffer. Otherwise clear all.
|
|
211
|
+
"""
|
|
212
|
+
with self._lock:
|
|
213
|
+
if workflow_id:
|
|
214
|
+
self._event_buffer.pop(workflow_id, None)
|
|
215
|
+
else:
|
|
216
|
+
self._event_buffer.clear()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class WorkflowEventLog:
|
|
220
|
+
"""Manages append-only event log for workflow execution."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, events_dir: Path, enable_streaming: bool = True):
|
|
223
|
+
"""
|
|
224
|
+
Initialize event log.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
events_dir: Directory to store event log files
|
|
228
|
+
enable_streaming: Whether to enable in-memory event streaming
|
|
229
|
+
"""
|
|
230
|
+
self.events_dir = Path(events_dir)
|
|
231
|
+
self.events_dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
self._sequence_counter: dict[str, int] = {}
|
|
233
|
+
self._stream: EventStream | None = EventStream() if enable_streaming else None
|
|
234
|
+
|
|
235
|
+
def _get_event_file(self, workflow_id: str) -> Path:
|
|
236
|
+
"""Get event log file path for a workflow."""
|
|
237
|
+
return self.events_dir / f"{workflow_id}.events.jsonl"
|
|
238
|
+
|
|
239
|
+
def _get_next_sequence(self, workflow_id: str) -> int:
|
|
240
|
+
"""Get next sequence number for a workflow."""
|
|
241
|
+
if workflow_id not in self._sequence_counter:
|
|
242
|
+
# Try to read last sequence from file
|
|
243
|
+
event_file = self._get_event_file(workflow_id)
|
|
244
|
+
if event_file.exists():
|
|
245
|
+
try:
|
|
246
|
+
last_seq = 0
|
|
247
|
+
with open(event_file, encoding="utf-8") as f:
|
|
248
|
+
for line in f:
|
|
249
|
+
if line.strip():
|
|
250
|
+
event_data = json.loads(line)
|
|
251
|
+
last_seq = max(last_seq, event_data.get("seq", 0))
|
|
252
|
+
self._sequence_counter[workflow_id] = last_seq
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.warning(
|
|
255
|
+
f"Failed to read sequence from {event_file}: {e}",
|
|
256
|
+
exc_info=True,
|
|
257
|
+
)
|
|
258
|
+
self._sequence_counter[workflow_id] = 0
|
|
259
|
+
else:
|
|
260
|
+
self._sequence_counter[workflow_id] = 0
|
|
261
|
+
|
|
262
|
+
self._sequence_counter[workflow_id] += 1
|
|
263
|
+
return self._sequence_counter[workflow_id]
|
|
264
|
+
|
|
265
|
+
def emit_event(
|
|
266
|
+
self,
|
|
267
|
+
event_type: str,
|
|
268
|
+
workflow_id: str,
|
|
269
|
+
step_id: str | None = None,
|
|
270
|
+
agent: str | None = None,
|
|
271
|
+
action: str | None = None,
|
|
272
|
+
status: str | None = None,
|
|
273
|
+
error: str | None = None,
|
|
274
|
+
artifacts: dict[str, Any] | None = None,
|
|
275
|
+
metadata: dict[str, Any] | None = None,
|
|
276
|
+
*,
|
|
277
|
+
rationale: str | None = None,
|
|
278
|
+
input_summary: str | None = None,
|
|
279
|
+
criteria: dict[str, Any] | None = None,
|
|
280
|
+
skill_name: str | None = None,
|
|
281
|
+
model_profile: str | None = None,
|
|
282
|
+
artifact_paths: list[str] | None = None,
|
|
283
|
+
tool_call_summary: dict[str, Any] | None = None,
|
|
284
|
+
) -> WorkflowEvent:
|
|
285
|
+
"""
|
|
286
|
+
Emit a workflow event (append-only write).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
event_type: Type of event (workflow_start, workflow_end, step_start, step_finish, step_fail, step_skip)
|
|
290
|
+
workflow_id: Workflow ID
|
|
291
|
+
step_id: Step ID (if applicable)
|
|
292
|
+
agent: Agent name (if applicable)
|
|
293
|
+
action: Action name (if applicable)
|
|
294
|
+
status: Status (if applicable)
|
|
295
|
+
error: Error message (if applicable)
|
|
296
|
+
artifacts: Artifact summaries (if applicable)
|
|
297
|
+
metadata: Additional metadata
|
|
298
|
+
rationale: Optional decision rationale (decision logging)
|
|
299
|
+
input_summary: Optional input summary (decision logging)
|
|
300
|
+
criteria: Optional criteria dict (decision logging)
|
|
301
|
+
skill_name: Optional skill name (trace)
|
|
302
|
+
model_profile: Optional model profile (trace)
|
|
303
|
+
artifact_paths: Optional list of artifact paths produced (trace)
|
|
304
|
+
tool_call_summary: Optional dict with command, success, duration_ms (trace)
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Created WorkflowEvent
|
|
308
|
+
"""
|
|
309
|
+
seq = self._get_next_sequence(workflow_id)
|
|
310
|
+
event = WorkflowEvent(
|
|
311
|
+
event_type=event_type,
|
|
312
|
+
workflow_id=workflow_id,
|
|
313
|
+
seq=seq,
|
|
314
|
+
timestamp=datetime.now(UTC),
|
|
315
|
+
step_id=step_id,
|
|
316
|
+
agent=agent,
|
|
317
|
+
action=action,
|
|
318
|
+
status=status,
|
|
319
|
+
error=error,
|
|
320
|
+
artifacts=artifacts,
|
|
321
|
+
metadata=metadata,
|
|
322
|
+
rationale=rationale,
|
|
323
|
+
input_summary=input_summary,
|
|
324
|
+
criteria=criteria,
|
|
325
|
+
skill_name=skill_name,
|
|
326
|
+
model_profile=model_profile,
|
|
327
|
+
artifact_paths=artifact_paths,
|
|
328
|
+
tool_call_summary=tool_call_summary,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Append to event log file (best-effort, non-blocking)
|
|
332
|
+
try:
|
|
333
|
+
event_file = self._get_event_file(workflow_id)
|
|
334
|
+
with open(event_file, "a", encoding="utf-8") as f:
|
|
335
|
+
# Use atomic write: write to temp file, then rename
|
|
336
|
+
# For append-only, we'll use a simpler approach with file locking
|
|
337
|
+
# In practice, JSONL append is atomic on most filesystems
|
|
338
|
+
json_line = json.dumps(event.to_dict(), ensure_ascii=False)
|
|
339
|
+
f.write(json_line + "\n")
|
|
340
|
+
f.flush()
|
|
341
|
+
os.fsync(f.fileno()) # Ensure durability
|
|
342
|
+
except Exception as e:
|
|
343
|
+
# Log error but don't fail workflow execution
|
|
344
|
+
logger.error(
|
|
345
|
+
f"Failed to write event to log: {e}",
|
|
346
|
+
exc_info=True,
|
|
347
|
+
extra={
|
|
348
|
+
"workflow_id": workflow_id,
|
|
349
|
+
"event_type": event_type,
|
|
350
|
+
"step_id": step_id,
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Emit to stream subscribers (real-time notification)
|
|
355
|
+
if self._stream:
|
|
356
|
+
self._stream.emit(event)
|
|
357
|
+
|
|
358
|
+
return event
|
|
359
|
+
|
|
360
|
+
def read_events(
|
|
361
|
+
self, workflow_id: str, limit: int | None = None
|
|
362
|
+
) -> list[WorkflowEvent]:
|
|
363
|
+
"""
|
|
364
|
+
Read events for a workflow.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
workflow_id: Workflow ID
|
|
368
|
+
limit: Maximum number of events to read (None = all)
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
List of events, ordered by sequence number
|
|
372
|
+
"""
|
|
373
|
+
event_file = self._get_event_file(workflow_id)
|
|
374
|
+
if not event_file.exists():
|
|
375
|
+
return []
|
|
376
|
+
|
|
377
|
+
events: list[WorkflowEvent] = []
|
|
378
|
+
try:
|
|
379
|
+
with open(event_file, encoding="utf-8") as f:
|
|
380
|
+
for line in f:
|
|
381
|
+
if line.strip():
|
|
382
|
+
try:
|
|
383
|
+
event_data = json.loads(line)
|
|
384
|
+
event = WorkflowEvent.from_dict(event_data)
|
|
385
|
+
events.append(event)
|
|
386
|
+
except Exception as e:
|
|
387
|
+
logger.warning(
|
|
388
|
+
f"Failed to parse event line: {e}",
|
|
389
|
+
extra={"workflow_id": workflow_id, "line": line[:100]},
|
|
390
|
+
)
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
# Sort by sequence number (should already be sorted, but ensure it)
|
|
394
|
+
events.sort(key=lambda e: e.seq)
|
|
395
|
+
|
|
396
|
+
if limit:
|
|
397
|
+
events = events[-limit:] # Get most recent events
|
|
398
|
+
|
|
399
|
+
except Exception as e:
|
|
400
|
+
logger.error(
|
|
401
|
+
f"Failed to read events from {event_file}: {e}",
|
|
402
|
+
exc_info=True,
|
|
403
|
+
extra={"workflow_id": workflow_id},
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
return events
|
|
407
|
+
|
|
408
|
+
def get_execution_history(
|
|
409
|
+
self, workflow_id: str
|
|
410
|
+
) -> dict[str, Any]:
|
|
411
|
+
"""
|
|
412
|
+
Generate human-readable execution history from event log.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
workflow_id: Workflow ID
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Dictionary with execution history summary
|
|
419
|
+
"""
|
|
420
|
+
events = self.read_events(workflow_id)
|
|
421
|
+
if not events:
|
|
422
|
+
return {
|
|
423
|
+
"workflow_id": workflow_id,
|
|
424
|
+
"events": [],
|
|
425
|
+
"summary": "No events found",
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
workflow_start = next(
|
|
429
|
+
(e for e in events if e.event_type == "workflow_start"), None
|
|
430
|
+
)
|
|
431
|
+
workflow_end = next(
|
|
432
|
+
(e for e in events if e.event_type == "workflow_end"), None
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
step_events = [
|
|
436
|
+
e
|
|
437
|
+
for e in events
|
|
438
|
+
if e.event_type in ("step_start", "step_finish", "step_fail", "step_skip")
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
# Group step events by step_id
|
|
442
|
+
step_history: dict[str, list[WorkflowEvent]] = {}
|
|
443
|
+
for event in step_events:
|
|
444
|
+
if event.step_id:
|
|
445
|
+
if event.step_id not in step_history:
|
|
446
|
+
step_history[event.step_id] = []
|
|
447
|
+
step_history[event.step_id].append(event)
|
|
448
|
+
|
|
449
|
+
# Calculate duration if both start and end exist
|
|
450
|
+
duration_seconds: float | None = None
|
|
451
|
+
if workflow_start and workflow_end:
|
|
452
|
+
duration_seconds = (
|
|
453
|
+
workflow_end.timestamp - workflow_start.timestamp
|
|
454
|
+
).total_seconds()
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
"workflow_id": workflow_id,
|
|
458
|
+
"started_at": workflow_start.timestamp.isoformat() + "Z"
|
|
459
|
+
if workflow_start
|
|
460
|
+
else None,
|
|
461
|
+
"ended_at": workflow_end.timestamp.isoformat() + "Z" if workflow_end else None,
|
|
462
|
+
"duration_seconds": duration_seconds,
|
|
463
|
+
"status": workflow_end.status if workflow_end else "running",
|
|
464
|
+
"total_events": len(events),
|
|
465
|
+
"step_count": len(step_history),
|
|
466
|
+
"steps": {
|
|
467
|
+
step_id: [
|
|
468
|
+
{
|
|
469
|
+
"event_type": e.event_type,
|
|
470
|
+
"timestamp": e.timestamp.isoformat() + "Z",
|
|
471
|
+
"status": e.status,
|
|
472
|
+
"error": e.error,
|
|
473
|
+
}
|
|
474
|
+
for e in sorted(events, key=lambda e: e.seq)
|
|
475
|
+
]
|
|
476
|
+
for step_id, events in step_history.items()
|
|
477
|
+
},
|
|
478
|
+
"events": [
|
|
479
|
+
{
|
|
480
|
+
"seq": e.seq,
|
|
481
|
+
"event_type": e.event_type,
|
|
482
|
+
"timestamp": e.timestamp.isoformat() + "Z",
|
|
483
|
+
"step_id": e.step_id,
|
|
484
|
+
"agent": e.agent,
|
|
485
|
+
"action": e.action,
|
|
486
|
+
"status": e.status,
|
|
487
|
+
"error": e.error,
|
|
488
|
+
}
|
|
489
|
+
for e in events
|
|
490
|
+
],
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
def get_execution_trace(self, workflow_id: str) -> dict[str, Any]:
|
|
494
|
+
"""
|
|
495
|
+
Derive a structured execution trace from events for metrics/AgentOps.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
workflow_id: Workflow ID
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Dict with workflow_id, started_at, ended_at, steps (list of
|
|
502
|
+
step_id, agent, skill_name, action, started_at, ended_at,
|
|
503
|
+
duration_ms, status, tool_calls, artifact_paths, error).
|
|
504
|
+
"""
|
|
505
|
+
events = self.read_events(workflow_id)
|
|
506
|
+
if not events:
|
|
507
|
+
return {"workflow_id": workflow_id, "steps": []}
|
|
508
|
+
|
|
509
|
+
workflow_start = next((e for e in events if e.event_type == "workflow_start"), None)
|
|
510
|
+
workflow_end = next((e for e in events if e.event_type == "workflow_end"), None)
|
|
511
|
+
step_evts = [
|
|
512
|
+
e for e in events
|
|
513
|
+
if e.event_type in ("step_start", "step_finish", "step_fail", "step_skip")
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
# Build steps: group by step_id, pair start with finish/fail/skip
|
|
517
|
+
steps_d: dict[str, dict[str, Any]] = {}
|
|
518
|
+
for e in step_evts:
|
|
519
|
+
if not e.step_id:
|
|
520
|
+
continue
|
|
521
|
+
if e.step_id not in steps_d:
|
|
522
|
+
steps_d[e.step_id] = {
|
|
523
|
+
"step_id": e.step_id,
|
|
524
|
+
"agent": e.agent,
|
|
525
|
+
"skill_name": e.skill_name,
|
|
526
|
+
"action": e.action,
|
|
527
|
+
"started_at": None,
|
|
528
|
+
"ended_at": None,
|
|
529
|
+
"duration_ms": None,
|
|
530
|
+
"status": e.status or "unknown",
|
|
531
|
+
"tool_call_summary": e.tool_call_summary,
|
|
532
|
+
"artifact_paths": e.artifact_paths or [],
|
|
533
|
+
"error": e.error,
|
|
534
|
+
}
|
|
535
|
+
s = steps_d[e.step_id]
|
|
536
|
+
if e.event_type == "step_start":
|
|
537
|
+
s["started_at"] = e.timestamp.isoformat() + "Z"
|
|
538
|
+
else:
|
|
539
|
+
s["ended_at"] = e.timestamp.isoformat() + "Z"
|
|
540
|
+
s["status"] = e.status or s["status"]
|
|
541
|
+
s["tool_call_summary"] = e.tool_call_summary or s["tool_call_summary"]
|
|
542
|
+
s["artifact_paths"] = e.artifact_paths or s["artifact_paths"]
|
|
543
|
+
s["error"] = e.error or s["error"]
|
|
544
|
+
|
|
545
|
+
for s in steps_d.values():
|
|
546
|
+
if s["started_at"] and s["ended_at"]:
|
|
547
|
+
try:
|
|
548
|
+
start = datetime.fromisoformat(s["started_at"].replace("Z", "+00:00"))
|
|
549
|
+
end = datetime.fromisoformat(s["ended_at"].replace("Z", "+00:00"))
|
|
550
|
+
s["duration_ms"] = (end - start).total_seconds() * 1000
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
"workflow_id": workflow_id,
|
|
556
|
+
"started_at": workflow_start.timestamp.isoformat() + "Z" if workflow_start else None,
|
|
557
|
+
"ended_at": workflow_end.timestamp.isoformat() + "Z" if workflow_end else None,
|
|
558
|
+
"steps": list(steps_d.values()),
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
def subscribe(
|
|
562
|
+
self,
|
|
563
|
+
callback: Callable[[WorkflowEvent], None],
|
|
564
|
+
filter: EventFilter | None = None,
|
|
565
|
+
subscription_id: str | None = None,
|
|
566
|
+
) -> Subscription | None:
|
|
567
|
+
"""
|
|
568
|
+
Subscribe to real-time workflow events.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
callback: Function to call when matching events occur
|
|
572
|
+
filter: Optional filter to limit which events trigger callback
|
|
573
|
+
subscription_id: Optional ID for this subscription
|
|
574
|
+
|
|
575
|
+
Returns:
|
|
576
|
+
Subscription object that can be used to unsubscribe, or None if streaming disabled
|
|
577
|
+
"""
|
|
578
|
+
if not self._stream:
|
|
579
|
+
logger.warning("Event streaming is disabled")
|
|
580
|
+
return None
|
|
581
|
+
return self._stream.subscribe(callback, filter, subscription_id)
|
|
582
|
+
|
|
583
|
+
def get_latest_events(self, workflow_id: str, limit: int = 100) -> list[WorkflowEvent]:
|
|
584
|
+
"""
|
|
585
|
+
Get latest events for a workflow from in-memory buffer.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
workflow_id: Workflow ID
|
|
589
|
+
limit: Maximum number of events to return
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
List of events, most recent first
|
|
593
|
+
"""
|
|
594
|
+
if not self._stream:
|
|
595
|
+
# Fallback to reading from file
|
|
596
|
+
return list(reversed(self.read_events(workflow_id, limit=limit)))
|
|
597
|
+
return self._stream.get_latest_events(workflow_id, limit)
|
|
598
|
+
|
|
599
|
+
def generate_execution_graph(self, workflow_id: str) -> "ExecutionGraph":
|
|
600
|
+
"""
|
|
601
|
+
Generate execution graph from event log.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
workflow_id: Workflow ID
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
ExecutionGraph instance
|
|
608
|
+
"""
|
|
609
|
+
from .execution_graph import ExecutionGraphGenerator
|
|
610
|
+
|
|
611
|
+
generator = ExecutionGraphGenerator(event_log=self)
|
|
612
|
+
return generator.generate_graph(workflow_id)
|