devsper 2.1.6__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.
- devsper/__init__.py +14 -0
- devsper/agents/a2a/__init__.py +27 -0
- devsper/agents/a2a/client.py +126 -0
- devsper/agents/a2a/discovery.py +24 -0
- devsper/agents/a2a/server.py +128 -0
- devsper/agents/a2a/tool_adapter.py +68 -0
- devsper/agents/a2a/types.py +49 -0
- devsper/agents/agent.py +602 -0
- devsper/agents/critic.py +80 -0
- devsper/agents/message_bus.py +124 -0
- devsper/agents/roles.py +181 -0
- devsper/agents/run_agent.py +78 -0
- devsper/analytics/__init__.py +5 -0
- devsper/analytics/tool_analytics.py +78 -0
- devsper/audit/__init__.py +5 -0
- devsper/audit/logger.py +214 -0
- devsper/bus/__init__.py +29 -0
- devsper/bus/backends/__init__.py +5 -0
- devsper/bus/backends/base.py +38 -0
- devsper/bus/backends/memory.py +55 -0
- devsper/bus/backends/redis.py +146 -0
- devsper/bus/message.py +56 -0
- devsper/bus/schema_version.py +3 -0
- devsper/bus/topics.py +19 -0
- devsper/cache/__init__.py +6 -0
- devsper/cache/embedding_index.py +98 -0
- devsper/cache/hashing.py +24 -0
- devsper/cache/store.py +153 -0
- devsper/cache/task_cache.py +191 -0
- devsper/cli/__init__.py +6 -0
- devsper/cli/commands/reg.py +733 -0
- devsper/cli/github_oauth.py +157 -0
- devsper/cli/init.py +637 -0
- devsper/cli/main.py +2956 -0
- devsper/cli/run_progress.py +103 -0
- devsper/cli/ui/__init__.py +65 -0
- devsper/cli/ui/components.py +94 -0
- devsper/cli/ui/errors.py +104 -0
- devsper/cli/ui/logging.py +120 -0
- devsper/cli/ui/onboarding.py +102 -0
- devsper/cli/ui/progress.py +43 -0
- devsper/cli/ui/run_view.py +308 -0
- devsper/cli/ui/theme.py +40 -0
- devsper/cluster/__init__.py +29 -0
- devsper/cluster/election.py +84 -0
- devsper/cluster/local.py +97 -0
- devsper/cluster/node_info.py +77 -0
- devsper/cluster/registry.py +71 -0
- devsper/cluster/router.py +117 -0
- devsper/cluster/state_backend.py +105 -0
- devsper/compliance/__init__.py +5 -0
- devsper/compliance/pii.py +147 -0
- devsper/config/__init__.py +52 -0
- devsper/config/config_loader.py +121 -0
- devsper/config/defaults.py +77 -0
- devsper/config/resolver.py +342 -0
- devsper/config/schema.py +237 -0
- devsper/credentials/__init__.py +19 -0
- devsper/credentials/cli.py +197 -0
- devsper/credentials/migration.py +124 -0
- devsper/credentials/store.py +142 -0
- devsper/dashboard/__init__.py +9 -0
- devsper/dashboard/dashboard.py +87 -0
- devsper/dev/__init__.py +25 -0
- devsper/dev/builder.py +195 -0
- devsper/dev/debugger.py +95 -0
- devsper/dev/repo_index.py +138 -0
- devsper/dev/sandbox.py +203 -0
- devsper/dev/scaffold.py +122 -0
- devsper/embeddings/__init__.py +5 -0
- devsper/embeddings/service.py +36 -0
- devsper/explainability/__init__.py +14 -0
- devsper/explainability/decision_tree.py +104 -0
- devsper/explainability/rationale.py +38 -0
- devsper/explainability/simulation.py +56 -0
- devsper/hitl/__init__.py +13 -0
- devsper/hitl/approval.py +160 -0
- devsper/hitl/escalation.py +95 -0
- devsper/intelligence/__init__.py +9 -0
- devsper/intelligence/adaptation.py +88 -0
- devsper/intelligence/analysis/__init__.py +19 -0
- devsper/intelligence/analysis/analyzer.py +71 -0
- devsper/intelligence/analysis/cost_estimator.py +66 -0
- devsper/intelligence/analysis/formatter.py +103 -0
- devsper/intelligence/analysis/run_report.py +402 -0
- devsper/intelligence/learning_engine.py +92 -0
- devsper/intelligence/strategies/__init__.py +23 -0
- devsper/intelligence/strategies/base.py +14 -0
- devsper/intelligence/strategies/code_analysis_strategy.py +33 -0
- devsper/intelligence/strategies/data_science_strategy.py +33 -0
- devsper/intelligence/strategies/document_pipeline_strategy.py +33 -0
- devsper/intelligence/strategies/experiment_strategy.py +33 -0
- devsper/intelligence/strategies/research_strategy.py +34 -0
- devsper/intelligence/strategy_selector.py +84 -0
- devsper/intelligence/synthesis.py +132 -0
- devsper/intelligence/task_optimizer.py +92 -0
- devsper/knowledge/__init__.py +5 -0
- devsper/knowledge/extractor.py +204 -0
- devsper/knowledge/knowledge_graph.py +184 -0
- devsper/knowledge/query.py +285 -0
- devsper/memory/__init__.py +35 -0
- devsper/memory/consolidation.py +138 -0
- devsper/memory/embeddings.py +60 -0
- devsper/memory/memory_index.py +97 -0
- devsper/memory/memory_router.py +62 -0
- devsper/memory/memory_store.py +221 -0
- devsper/memory/memory_types.py +54 -0
- devsper/memory/namespaces.py +45 -0
- devsper/memory/scoring.py +77 -0
- devsper/memory/summarizer.py +52 -0
- devsper/nodes/__init__.py +5 -0
- devsper/nodes/controller.py +449 -0
- devsper/nodes/rpc.py +127 -0
- devsper/nodes/single.py +161 -0
- devsper/nodes/worker.py +506 -0
- devsper/orchestration/__init__.py +19 -0
- devsper/orchestration/meta_planner.py +239 -0
- devsper/orchestration/priority_queue.py +61 -0
- devsper/plugins/__init__.py +19 -0
- devsper/plugins/marketplace/__init__.py +0 -0
- devsper/plugins/plugin_loader.py +70 -0
- devsper/plugins/plugin_registry.py +34 -0
- devsper/plugins/registry.py +83 -0
- devsper/protocols/__init__.py +6 -0
- devsper/providers/__init__.py +17 -0
- devsper/providers/anthropic.py +84 -0
- devsper/providers/base.py +75 -0
- devsper/providers/complexity_router.py +94 -0
- devsper/providers/gemini.py +36 -0
- devsper/providers/github.py +180 -0
- devsper/providers/model_router.py +40 -0
- devsper/providers/openai.py +105 -0
- devsper/providers/router/__init__.py +21 -0
- devsper/providers/router/backends/__init__.py +19 -0
- devsper/providers/router/backends/anthropic_backend.py +111 -0
- devsper/providers/router/backends/custom_backend.py +138 -0
- devsper/providers/router/backends/gemini_backend.py +89 -0
- devsper/providers/router/backends/github_backend.py +165 -0
- devsper/providers/router/backends/ollama_backend.py +104 -0
- devsper/providers/router/backends/openai_backend.py +142 -0
- devsper/providers/router/backends/vllm_backend.py +35 -0
- devsper/providers/router/base.py +60 -0
- devsper/providers/router/factory.py +92 -0
- devsper/providers/router/legacy.py +101 -0
- devsper/providers/router/router.py +135 -0
- devsper/reasoning/__init__.py +12 -0
- devsper/reasoning/graph.py +59 -0
- devsper/reasoning/nodes.py +20 -0
- devsper/reasoning/store.py +67 -0
- devsper/runtime/__init__.py +12 -0
- devsper/runtime/health.py +88 -0
- devsper/runtime/replay.py +53 -0
- devsper/runtime/replay_engine.py +142 -0
- devsper/runtime/run_history.py +204 -0
- devsper/runtime/telemetry.py +116 -0
- devsper/runtime/visualize.py +58 -0
- devsper/sandbox/__init__.py +13 -0
- devsper/sandbox/sandbox.py +161 -0
- devsper/swarm/checkpointer.py +65 -0
- devsper/swarm/executor.py +558 -0
- devsper/swarm/map_reduce.py +44 -0
- devsper/swarm/planner.py +197 -0
- devsper/swarm/prefetcher.py +91 -0
- devsper/swarm/scheduler.py +153 -0
- devsper/swarm/speculation.py +47 -0
- devsper/swarm/swarm.py +562 -0
- devsper/tools/__init__.py +33 -0
- devsper/tools/base.py +29 -0
- devsper/tools/code_intelligence/__init__.py +13 -0
- devsper/tools/code_intelligence/api_surface_extractor.py +73 -0
- devsper/tools/code_intelligence/architecture_analyzer.py +65 -0
- devsper/tools/code_intelligence/codebase_indexer.py +71 -0
- devsper/tools/code_intelligence/dependency_graph_builder.py +67 -0
- devsper/tools/code_intelligence/design_pattern_detector.py +62 -0
- devsper/tools/code_intelligence/large_function_detector.py +68 -0
- devsper/tools/code_intelligence/module_responsibility_mapper.py +56 -0
- devsper/tools/code_intelligence/parallel_codebase_analysis.py +44 -0
- devsper/tools/code_intelligence/refactor_candidate_detector.py +81 -0
- devsper/tools/code_intelligence/repository_semantic_index.py +61 -0
- devsper/tools/code_intelligence/test_coverage_estimator.py +62 -0
- devsper/tools/coding/__init__.py +12 -0
- devsper/tools/coding/analyze_code_complexity.py +48 -0
- devsper/tools/coding/dependency_analyzer.py +42 -0
- devsper/tools/coding/extract_functions.py +38 -0
- devsper/tools/coding/format_python.py +50 -0
- devsper/tools/coding/generate_docstrings.py +40 -0
- devsper/tools/coding/generate_unit_tests.py +42 -0
- devsper/tools/coding/lint_python.py +51 -0
- devsper/tools/coding/refactor_function.py +41 -0
- devsper/tools/coding/repo_structure_map.py +54 -0
- devsper/tools/coding/run_python.py +53 -0
- devsper/tools/data/__init__.py +12 -0
- devsper/tools/data/column_type_detection.py +64 -0
- devsper/tools/data/csv_summary.py +52 -0
- devsper/tools/data/dataframe_filter.py +51 -0
- devsper/tools/data/dataframe_groupby.py +47 -0
- devsper/tools/data/dataframe_stats.py +38 -0
- devsper/tools/data/dataset_sampling.py +55 -0
- devsper/tools/data/dataset_schema.py +45 -0
- devsper/tools/data/json_pretty_print.py +37 -0
- devsper/tools/data/json_query.py +46 -0
- devsper/tools/data/missing_value_report.py +47 -0
- devsper/tools/data_science/__init__.py +13 -0
- devsper/tools/data_science/correlation_heatmap.py +72 -0
- devsper/tools/data_science/dataset_bias_detector.py +49 -0
- devsper/tools/data_science/dataset_distribution_report.py +64 -0
- devsper/tools/data_science/dataset_drift_detector.py +64 -0
- devsper/tools/data_science/dataset_outlier_detector.py +65 -0
- devsper/tools/data_science/dataset_profile.py +76 -0
- devsper/tools/data_science/distributed_dataset_processor.py +54 -0
- devsper/tools/data_science/feature_engineering_suggestions.py +69 -0
- devsper/tools/data_science/feature_importance_estimator.py +82 -0
- devsper/tools/data_science/model_input_validator.py +59 -0
- devsper/tools/data_science/time_series_analyzer.py +57 -0
- devsper/tools/documents/__init__.py +11 -0
- devsper/tools/documents/_docproc.py +56 -0
- devsper/tools/documents/document_to_markdown.py +29 -0
- devsper/tools/documents/extract_document_images.py +39 -0
- devsper/tools/documents/extract_document_text.py +29 -0
- devsper/tools/documents/extract_equations.py +36 -0
- devsper/tools/documents/extract_tables.py +47 -0
- devsper/tools/documents/summarize_document.py +42 -0
- devsper/tools/documents/write_latex_document.py +133 -0
- devsper/tools/documents/write_markdown_document.py +89 -0
- devsper/tools/documents/write_word_document.py +149 -0
- devsper/tools/experiments/__init__.py +13 -0
- devsper/tools/experiments/bootstrap_estimator.py +54 -0
- devsper/tools/experiments/experiment_report_generator.py +50 -0
- devsper/tools/experiments/experiment_tracker.py +36 -0
- devsper/tools/experiments/grid_search_runner.py +50 -0
- devsper/tools/experiments/model_benchmark_runner.py +45 -0
- devsper/tools/experiments/monte_carlo_experiment.py +38 -0
- devsper/tools/experiments/parameter_sweep_runner.py +51 -0
- devsper/tools/experiments/result_comparator.py +58 -0
- devsper/tools/experiments/simulation_runner.py +43 -0
- devsper/tools/experiments/statistical_significance_test.py +56 -0
- devsper/tools/experiments/swarm_map_reduce.py +42 -0
- devsper/tools/filesystem/__init__.py +12 -0
- devsper/tools/filesystem/append_file.py +42 -0
- devsper/tools/filesystem/file_hash.py +40 -0
- devsper/tools/filesystem/file_line_count.py +36 -0
- devsper/tools/filesystem/file_metadata.py +38 -0
- devsper/tools/filesystem/file_preview.py +55 -0
- devsper/tools/filesystem/find_large_files.py +50 -0
- devsper/tools/filesystem/list_directory.py +39 -0
- devsper/tools/filesystem/read_file.py +35 -0
- devsper/tools/filesystem/search_files.py +60 -0
- devsper/tools/filesystem/write_file.py +41 -0
- devsper/tools/flagship/__init__.py +15 -0
- devsper/tools/flagship/distributed_document_analysis.py +77 -0
- devsper/tools/flagship/docproc_corpus_pipeline.py +91 -0
- devsper/tools/flagship/repository_semantic_map.py +99 -0
- devsper/tools/flagship/research_graph_builder.py +111 -0
- devsper/tools/flagship/swarm_experiment_runner.py +86 -0
- devsper/tools/knowledge/__init__.py +10 -0
- devsper/tools/knowledge/citation_graph_builder.py +69 -0
- devsper/tools/knowledge/concept_frequency_analyzer.py +74 -0
- devsper/tools/knowledge/corpus_builder.py +66 -0
- devsper/tools/knowledge/cross_document_entity_linker.py +71 -0
- devsper/tools/knowledge/document_corpus_summary.py +68 -0
- devsper/tools/knowledge/document_topic_extractor.py +58 -0
- devsper/tools/knowledge/knowledge_graph_extractor.py +58 -0
- devsper/tools/knowledge/timeline_extractor.py +59 -0
- devsper/tools/math/__init__.py +12 -0
- devsper/tools/math/calculate_expression.py +52 -0
- devsper/tools/math/correlation.py +44 -0
- devsper/tools/math/distribution_summary.py +39 -0
- devsper/tools/math/histogram.py +53 -0
- devsper/tools/math/linear_regression.py +47 -0
- devsper/tools/math/matrix_multiply.py +38 -0
- devsper/tools/math/mean_std.py +35 -0
- devsper/tools/math/monte_carlo_simulation.py +43 -0
- devsper/tools/math/polynomial_fit.py +40 -0
- devsper/tools/math/random_sample.py +36 -0
- devsper/tools/mcp/__init__.py +23 -0
- devsper/tools/mcp/adapter.py +53 -0
- devsper/tools/mcp/client.py +235 -0
- devsper/tools/mcp/discovery.py +53 -0
- devsper/tools/memory/__init__.py +16 -0
- devsper/tools/memory/delete_memory.py +25 -0
- devsper/tools/memory/list_memory.py +34 -0
- devsper/tools/memory/search_memory.py +36 -0
- devsper/tools/memory/store_memory.py +47 -0
- devsper/tools/memory/summarize_memory.py +41 -0
- devsper/tools/memory/tag_memory.py +47 -0
- devsper/tools/pipelines.py +92 -0
- devsper/tools/registry.py +39 -0
- devsper/tools/research/__init__.py +12 -0
- devsper/tools/research/arxiv_download.py +55 -0
- devsper/tools/research/arxiv_search.py +58 -0
- devsper/tools/research/citation_extractor.py +35 -0
- devsper/tools/research/duckduckgo_search.py +42 -0
- devsper/tools/research/paper_metadata_extractor.py +45 -0
- devsper/tools/research/paper_summarizer.py +41 -0
- devsper/tools/research/research_question_generator.py +39 -0
- devsper/tools/research/topic_cluster.py +46 -0
- devsper/tools/research/web_search.py +47 -0
- devsper/tools/research/wikipedia_lookup.py +50 -0
- devsper/tools/research_advanced/__init__.py +14 -0
- devsper/tools/research_advanced/citation_context_extractor.py +60 -0
- devsper/tools/research_advanced/literature_review_generator.py +79 -0
- devsper/tools/research_advanced/methodology_extractor.py +58 -0
- devsper/tools/research_advanced/paper_contribution_extractor.py +50 -0
- devsper/tools/research_advanced/paper_dataset_identifier.py +49 -0
- devsper/tools/research_advanced/paper_method_comparator.py +62 -0
- devsper/tools/research_advanced/paper_similarity_search.py +69 -0
- devsper/tools/research_advanced/paper_trend_analyzer.py +69 -0
- devsper/tools/research_advanced/parallel_document_analyzer.py +56 -0
- devsper/tools/research_advanced/research_gap_finder.py +71 -0
- devsper/tools/research_advanced/research_topic_mapper.py +69 -0
- devsper/tools/research_advanced/swarm_literature_review.py +58 -0
- devsper/tools/scoring/__init__.py +52 -0
- devsper/tools/scoring/report.py +44 -0
- devsper/tools/scoring/scorer.py +39 -0
- devsper/tools/scoring/selector.py +61 -0
- devsper/tools/scoring/store.py +267 -0
- devsper/tools/selector.py +130 -0
- devsper/tools/system/__init__.py +12 -0
- devsper/tools/system/cpu_usage.py +22 -0
- devsper/tools/system/disk_usage.py +35 -0
- devsper/tools/system/environment_variables.py +29 -0
- devsper/tools/system/memory_usage.py +23 -0
- devsper/tools/system/pip_install.py +44 -0
- devsper/tools/system/pip_search.py +29 -0
- devsper/tools/system/process_list.py +34 -0
- devsper/tools/system/python_package_list.py +40 -0
- devsper/tools/system/run_shell_command.py +51 -0
- devsper/tools/system/system_info.py +26 -0
- devsper/tools/tool_runner.py +122 -0
- devsper/tui/__init__.py +5 -0
- devsper/tui/activity_feed_view.py +73 -0
- devsper/tui/adaptive_tasks_view.py +75 -0
- devsper/tui/agent_role_view.py +35 -0
- devsper/tui/app.py +395 -0
- devsper/tui/dashboard_screen.py +290 -0
- devsper/tui/dev_view.py +99 -0
- devsper/tui/inject_screen.py +73 -0
- devsper/tui/knowledge_graph_view.py +46 -0
- devsper/tui/layout.py +43 -0
- devsper/tui/logs_view.py +83 -0
- devsper/tui/memory_view.py +58 -0
- devsper/tui/performance_view.py +33 -0
- devsper/tui/reasoning_graph_view.py +39 -0
- devsper/tui/results_view.py +139 -0
- devsper/tui/swarm_view.py +37 -0
- devsper/tui/task_detail_screen.py +55 -0
- devsper/tui/task_view.py +103 -0
- devsper/types/event.py +97 -0
- devsper/types/exceptions.py +21 -0
- devsper/types/swarm.py +41 -0
- devsper/types/task.py +80 -0
- devsper/upgrade/__init__.py +21 -0
- devsper/upgrade/changelog.py +124 -0
- devsper/upgrade/cli.py +145 -0
- devsper/upgrade/installer.py +103 -0
- devsper/upgrade/notifier.py +52 -0
- devsper/upgrade/version_check.py +121 -0
- devsper/utils/event_logger.py +88 -0
- devsper/utils/http.py +43 -0
- devsper/utils/models.py +54 -0
- devsper/visualization/__init__.py +5 -0
- devsper/visualization/dag_export.py +67 -0
- devsper/workflow/__init__.py +18 -0
- devsper/workflow/conditions.py +157 -0
- devsper/workflow/context.py +108 -0
- devsper/workflow/loader.py +156 -0
- devsper/workflow/resolver.py +109 -0
- devsper/workflow/runner.py +562 -0
- devsper/workflow/schema.py +63 -0
- devsper/workflow/validator.py +128 -0
- devsper-2.1.6.dist-info/METADATA +346 -0
- devsper-2.1.6.dist-info/RECORD +375 -0
- devsper-2.1.6.dist-info/WHEEL +4 -0
- devsper-2.1.6.dist-info/entry_points.txt +3 -0
- devsper-2.1.6.dist-info/licenses/LICENSE +639 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Per-run pub/sub channel for agent-to-agent messaging. v1.7.
|
|
3
|
+
Agents broadcast discoveries; subsequent agents receive them via memory context.
|
|
4
|
+
Not persistent — lives only for the duration of a run.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
|
|
11
|
+
from devsper.types.event import Event, events
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AgentMessage:
|
|
16
|
+
sender_task_id: str
|
|
17
|
+
content: str
|
|
18
|
+
tags: list[str]
|
|
19
|
+
timestamp: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SwarmMessageBus:
|
|
23
|
+
"""
|
|
24
|
+
Per-run pub/sub channel. Agents broadcast discoveries;
|
|
25
|
+
all subsequent agents receive them via memory context.
|
|
26
|
+
Not persistent — lives only for the duration of a run.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, event_log=None):
|
|
30
|
+
self._messages: list[AgentMessage] = []
|
|
31
|
+
self._lock = asyncio.Lock()
|
|
32
|
+
self._event_log = event_log
|
|
33
|
+
|
|
34
|
+
def _emit(self, event_type: events, payload: dict) -> None:
|
|
35
|
+
if self._event_log:
|
|
36
|
+
self._event_log.append_event(
|
|
37
|
+
Event(
|
|
38
|
+
timestamp=datetime.now(timezone.utc),
|
|
39
|
+
type=event_type,
|
|
40
|
+
payload=payload,
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def broadcast(
|
|
45
|
+
self, sender_task_id: str, content: str, tags: list[str] | None = None
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Agent calls this when it discovers something worth sharing."""
|
|
48
|
+
tags = tags or []
|
|
49
|
+
async with self._lock:
|
|
50
|
+
self._messages.append(
|
|
51
|
+
AgentMessage(
|
|
52
|
+
sender_task_id=sender_task_id,
|
|
53
|
+
content=content,
|
|
54
|
+
tags=tags,
|
|
55
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
self._emit(
|
|
59
|
+
events.AGENT_BROADCAST,
|
|
60
|
+
{
|
|
61
|
+
"sender_task_id": sender_task_id,
|
|
62
|
+
"content_preview": (content or "")[:100],
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def broadcast_sync(
|
|
67
|
+
self, sender_task_id: str, content: str, tags: list[str] | None = None
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Synchronous broadcast for use from sync agent.run()."""
|
|
70
|
+
tags = tags or []
|
|
71
|
+
self._messages.append(
|
|
72
|
+
AgentMessage(
|
|
73
|
+
sender_task_id=sender_task_id,
|
|
74
|
+
content=content,
|
|
75
|
+
tags=tags,
|
|
76
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
self._emit(
|
|
80
|
+
events.AGENT_BROADCAST,
|
|
81
|
+
{
|
|
82
|
+
"sender_task_id": sender_task_id,
|
|
83
|
+
"content_preview": (content or "")[:100],
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def get_context(
|
|
88
|
+
self, requesting_task_id: str, max_messages: int = 5
|
|
89
|
+
) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Return formatted string of recent broadcasts for injection into agent prompt.
|
|
92
|
+
Exclude messages from requesting_task_id itself.
|
|
93
|
+
Return most recent max_messages.
|
|
94
|
+
"""
|
|
95
|
+
async with self._lock:
|
|
96
|
+
eligible = [
|
|
97
|
+
m
|
|
98
|
+
for m in self._messages
|
|
99
|
+
if m.sender_task_id != requesting_task_id
|
|
100
|
+
]
|
|
101
|
+
recent = eligible[-max_messages:] if len(eligible) > max_messages else eligible
|
|
102
|
+
if not recent:
|
|
103
|
+
return ""
|
|
104
|
+
lines = ["Shared Discoveries (from other agents in this run):"]
|
|
105
|
+
for m in recent:
|
|
106
|
+
lines.append(f"- [{m.sender_task_id}]: {m.content[:500]}{'...' if len(m.content) > 500 else ''}")
|
|
107
|
+
return "\n".join(lines)
|
|
108
|
+
|
|
109
|
+
def get_context_sync(
|
|
110
|
+
self, requesting_task_id: str, max_messages: int = 5
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Synchronous get_context for use from sync agent.run()."""
|
|
113
|
+
eligible = [
|
|
114
|
+
m
|
|
115
|
+
for m in self._messages
|
|
116
|
+
if m.sender_task_id != requesting_task_id
|
|
117
|
+
]
|
|
118
|
+
recent = eligible[-max_messages:] if len(eligible) > max_messages else eligible
|
|
119
|
+
if not recent:
|
|
120
|
+
return ""
|
|
121
|
+
lines = ["Shared Discoveries (from other agents in this run):"]
|
|
122
|
+
for m in recent:
|
|
123
|
+
lines.append(f"- [{m.sender_task_id}]: {m.content[:500]}{'...' if len(m.content) > 500 else ''}")
|
|
124
|
+
return "\n".join(lines)
|
devsper/agents/roles.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Specialized agent roles: research, code, analysis, critic.
|
|
3
|
+
|
|
4
|
+
Role determines tool access, prompt template, and model selection.
|
|
5
|
+
Planner assigns roles based on task type.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
RESEARCH_AGENT = "research_agent"
|
|
11
|
+
CODE_AGENT = "code_agent"
|
|
12
|
+
ANALYSIS_AGENT = "analysis_agent"
|
|
13
|
+
CRITIC_AGENT = "critic_agent"
|
|
14
|
+
|
|
15
|
+
# Dev / autonomous builder roles
|
|
16
|
+
ARCHITECT_AGENT = "architect_agent"
|
|
17
|
+
BACKEND_AGENT = "backend_agent"
|
|
18
|
+
FRONTEND_AGENT = "frontend_agent"
|
|
19
|
+
TEST_AGENT = "test_agent"
|
|
20
|
+
REVIEW_AGENT = "review_agent"
|
|
21
|
+
|
|
22
|
+
DEFAULT_ROLE = "general"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RoleConfig:
|
|
27
|
+
"""Configuration for an agent role."""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
tool_categories: list[str] # e.g. ["research", "documents"]
|
|
31
|
+
prompt_prefix: str
|
|
32
|
+
model_hint: str # e.g. "analysis", "planning" for model router
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
ROLE_CONFIGS: dict[str, RoleConfig] = {
|
|
36
|
+
RESEARCH_AGENT: RoleConfig(
|
|
37
|
+
name=RESEARCH_AGENT,
|
|
38
|
+
tool_categories=[
|
|
39
|
+
"research",
|
|
40
|
+
"research_advanced",
|
|
41
|
+
"documents",
|
|
42
|
+
"knowledge",
|
|
43
|
+
"memory",
|
|
44
|
+
"mcp",
|
|
45
|
+
],
|
|
46
|
+
prompt_prefix="You are a research specialist. Focus on literature, citations, methodology, and evidence.",
|
|
47
|
+
model_hint="analysis",
|
|
48
|
+
),
|
|
49
|
+
CODE_AGENT: RoleConfig(
|
|
50
|
+
name=CODE_AGENT,
|
|
51
|
+
tool_categories=["coding", "code_intelligence", "filesystem", "system", "mcp"],
|
|
52
|
+
prompt_prefix="You are a code specialist. Focus on implementation, structure, tests, and refactoring.",
|
|
53
|
+
model_hint="analysis",
|
|
54
|
+
),
|
|
55
|
+
ANALYSIS_AGENT: RoleConfig(
|
|
56
|
+
name=ANALYSIS_AGENT,
|
|
57
|
+
tool_categories=["data", "data_science", "math", "experiments", "knowledge", "mcp"],
|
|
58
|
+
prompt_prefix="You are an analysis specialist. Focus on data, metrics, statistics, and interpretation.",
|
|
59
|
+
model_hint="analysis",
|
|
60
|
+
),
|
|
61
|
+
CRITIC_AGENT: RoleConfig(
|
|
62
|
+
name=CRITIC_AGENT,
|
|
63
|
+
tool_categories=["documents", "memory", "knowledge"],
|
|
64
|
+
prompt_prefix="You are a critic/reviewer. Evaluate quality, consistency, and gaps. Be concise and constructive.",
|
|
65
|
+
model_hint="analysis",
|
|
66
|
+
),
|
|
67
|
+
DEFAULT_ROLE: RoleConfig(
|
|
68
|
+
name=DEFAULT_ROLE,
|
|
69
|
+
tool_categories=[], # empty = no filter; all tools (including mcp) eligible
|
|
70
|
+
prompt_prefix="You are an AI worker in a distributed system.",
|
|
71
|
+
model_hint="analysis",
|
|
72
|
+
),
|
|
73
|
+
# Dev / autonomous builder roles
|
|
74
|
+
ARCHITECT_AGENT: RoleConfig(
|
|
75
|
+
name=ARCHITECT_AGENT,
|
|
76
|
+
tool_categories=["code_intelligence", "filesystem"],
|
|
77
|
+
prompt_prefix="You are an architect. Design system structure, APIs, and component layout. Output clear architecture plans (backend/frontend stack, modules, data flow).",
|
|
78
|
+
model_hint="planning",
|
|
79
|
+
),
|
|
80
|
+
BACKEND_AGENT: RoleConfig(
|
|
81
|
+
name=BACKEND_AGENT,
|
|
82
|
+
tool_categories=["coding", "code_intelligence", "filesystem", "system"],
|
|
83
|
+
prompt_prefix="You are a backend specialist. Implement APIs, models, and server logic. Prefer FastAPI/Flask patterns and clear interfaces.",
|
|
84
|
+
model_hint="analysis",
|
|
85
|
+
),
|
|
86
|
+
FRONTEND_AGENT: RoleConfig(
|
|
87
|
+
name=FRONTEND_AGENT,
|
|
88
|
+
tool_categories=["coding", "filesystem", "system"],
|
|
89
|
+
prompt_prefix="You are a frontend specialist. Implement UI components, pages, and client logic. Prefer simple HTML/JS or React patterns.",
|
|
90
|
+
model_hint="analysis",
|
|
91
|
+
),
|
|
92
|
+
TEST_AGENT: RoleConfig(
|
|
93
|
+
name=TEST_AGENT,
|
|
94
|
+
tool_categories=["coding", "code_intelligence", "filesystem", "system"],
|
|
95
|
+
prompt_prefix="You are a test specialist. Write unit and integration tests. Use pytest for Python; ensure coverage of main flows.",
|
|
96
|
+
model_hint="analysis",
|
|
97
|
+
),
|
|
98
|
+
REVIEW_AGENT: RoleConfig(
|
|
99
|
+
name=REVIEW_AGENT,
|
|
100
|
+
tool_categories=["code_intelligence", "coding", "filesystem"],
|
|
101
|
+
prompt_prefix="You are a code reviewer. Check correctness, style, and gaps. Suggest concrete fixes. Be concise.",
|
|
102
|
+
model_hint="analysis",
|
|
103
|
+
),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_role_config(role: str | None) -> RoleConfig:
|
|
108
|
+
"""Return config for the given role, or default if unknown/None."""
|
|
109
|
+
if role and role in ROLE_CONFIGS:
|
|
110
|
+
return ROLE_CONFIGS[role]
|
|
111
|
+
return ROLE_CONFIGS[DEFAULT_ROLE]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Keywords to infer role from task description
|
|
115
|
+
RESEARCH_KEYWORDS = [
|
|
116
|
+
"research",
|
|
117
|
+
"paper",
|
|
118
|
+
"literature",
|
|
119
|
+
"cite",
|
|
120
|
+
"survey",
|
|
121
|
+
"methodology",
|
|
122
|
+
"findings",
|
|
123
|
+
]
|
|
124
|
+
CODE_KEYWORDS = [
|
|
125
|
+
"code",
|
|
126
|
+
"implement",
|
|
127
|
+
"refactor",
|
|
128
|
+
"test",
|
|
129
|
+
"repository",
|
|
130
|
+
"function",
|
|
131
|
+
"api",
|
|
132
|
+
"lint",
|
|
133
|
+
"list",
|
|
134
|
+
"directory",
|
|
135
|
+
"files",
|
|
136
|
+
"contents",
|
|
137
|
+
"path",
|
|
138
|
+
"read file",
|
|
139
|
+
"write file",
|
|
140
|
+
]
|
|
141
|
+
ANALYSIS_KEYWORDS = [
|
|
142
|
+
"analyze",
|
|
143
|
+
"data",
|
|
144
|
+
"metric",
|
|
145
|
+
"statistic",
|
|
146
|
+
"plot",
|
|
147
|
+
"dataset",
|
|
148
|
+
"experiment",
|
|
149
|
+
"evaluate",
|
|
150
|
+
]
|
|
151
|
+
CRITIC_KEYWORDS = [
|
|
152
|
+
"review",
|
|
153
|
+
"critique",
|
|
154
|
+
"evaluate",
|
|
155
|
+
"quality",
|
|
156
|
+
"check",
|
|
157
|
+
"verify",
|
|
158
|
+
"feedback",
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
# Dev builder: explicit roles are set by planner; no inference needed for build tasks
|
|
162
|
+
DEV_ROLES = [
|
|
163
|
+
ARCHITECT_AGENT,
|
|
164
|
+
BACKEND_AGENT,
|
|
165
|
+
FRONTEND_AGENT,
|
|
166
|
+
TEST_AGENT,
|
|
167
|
+
REVIEW_AGENT,
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def infer_role_from_description(description: str) -> str:
|
|
172
|
+
"""Infer agent role from task description. Returns role name or DEFAULT_ROLE."""
|
|
173
|
+
text = (description or "").lower()
|
|
174
|
+
scores = {
|
|
175
|
+
RESEARCH_AGENT: sum(1 for k in RESEARCH_KEYWORDS if k in text),
|
|
176
|
+
CODE_AGENT: sum(1 for k in CODE_KEYWORDS if k in text),
|
|
177
|
+
ANALYSIS_AGENT: sum(1 for k in ANALYSIS_KEYWORDS if k in text),
|
|
178
|
+
CRITIC_AGENT: sum(1 for k in CRITIC_KEYWORDS if k in text),
|
|
179
|
+
}
|
|
180
|
+
best = max(scores, key=scores.get)
|
|
181
|
+
return best if scores[best] > 0 else DEFAULT_ROLE
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for Rust worker subprocess executor.
|
|
3
|
+
Reads AgentRequest JSON from stdin, runs agent, writes AgentResponse JSON to stdout.
|
|
4
|
+
Exit 0 on success, 1 on error (error details in AgentResponse.error on stdout).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from devsper.credentials import inject_into_env
|
|
11
|
+
from devsper.agents.agent import Agent, AgentRequest, AgentResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_agent_sync(request_json: str) -> str:
|
|
15
|
+
"""Run agent from JSON request string; return JSON response string. Used by PyO3 executor."""
|
|
16
|
+
data = json.loads(request_json)
|
|
17
|
+
request = AgentRequest.from_dict(data)
|
|
18
|
+
agent = Agent()
|
|
19
|
+
response = agent.run(request) # sync, not async
|
|
20
|
+
return json.dumps(response.to_dict())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
# So the subprocess can use GitHub/OpenAI etc.: inject keychain credentials into env
|
|
25
|
+
# (same as config resolver does for the Python worker).
|
|
26
|
+
inject_into_env()
|
|
27
|
+
try:
|
|
28
|
+
raw = sys.stdin.read()
|
|
29
|
+
if not raw.strip():
|
|
30
|
+
out = AgentResponse(
|
|
31
|
+
task_id="",
|
|
32
|
+
result="",
|
|
33
|
+
tools_called=[],
|
|
34
|
+
broadcasts=[],
|
|
35
|
+
tokens_used=None,
|
|
36
|
+
duration_seconds=0.0,
|
|
37
|
+
error="Empty stdin",
|
|
38
|
+
success=False,
|
|
39
|
+
)
|
|
40
|
+
print(json.dumps(out.to_dict()), flush=True)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
request = AgentRequest.from_dict(json.loads(raw))
|
|
43
|
+
except Exception as e:
|
|
44
|
+
out = AgentResponse(
|
|
45
|
+
task_id="",
|
|
46
|
+
result="",
|
|
47
|
+
tools_called=[],
|
|
48
|
+
broadcasts=[],
|
|
49
|
+
tokens_used=None,
|
|
50
|
+
duration_seconds=0.0,
|
|
51
|
+
error=str(e),
|
|
52
|
+
success=False,
|
|
53
|
+
)
|
|
54
|
+
print(json.dumps(out.to_dict()), flush=True)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
agent = Agent()
|
|
59
|
+
response = agent.run(request) # sync, not async
|
|
60
|
+
print(json.dumps(response.to_dict()), flush=True)
|
|
61
|
+
sys.exit(0 if response.success else 1)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
out = AgentResponse(
|
|
64
|
+
task_id=request.task.id,
|
|
65
|
+
result="",
|
|
66
|
+
tools_called=[],
|
|
67
|
+
broadcasts=[],
|
|
68
|
+
tokens_used=None,
|
|
69
|
+
duration_seconds=0.0,
|
|
70
|
+
error=str(e),
|
|
71
|
+
success=False,
|
|
72
|
+
)
|
|
73
|
+
print(json.dumps(out.to_dict()), flush=True)
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Track tool usage: count, success rate, latency. Persist in SQLite.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sqlite3
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ToolAnalytics:
|
|
11
|
+
"""SQLite-backed analytics for tool usage: count, success rate, latency."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, db_path: str | Path | None = None) -> None:
|
|
14
|
+
if db_path is None:
|
|
15
|
+
db_path = Path(".devsper") / "tool_analytics.db"
|
|
16
|
+
self.db_path = Path(db_path)
|
|
17
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
self._init_schema()
|
|
19
|
+
|
|
20
|
+
def _conn(self) -> sqlite3.Connection:
|
|
21
|
+
return sqlite3.connect(str(self.db_path))
|
|
22
|
+
|
|
23
|
+
def _init_schema(self) -> None:
|
|
24
|
+
with self._conn() as c:
|
|
25
|
+
c.execute("""
|
|
26
|
+
CREATE TABLE IF NOT EXISTS tool_usage (
|
|
27
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
28
|
+
tool_name TEXT NOT NULL,
|
|
29
|
+
success INTEGER NOT NULL,
|
|
30
|
+
latency_ms REAL NOT NULL,
|
|
31
|
+
created_at REAL NOT NULL
|
|
32
|
+
)
|
|
33
|
+
""")
|
|
34
|
+
c.execute(
|
|
35
|
+
"CREATE INDEX IF NOT EXISTS idx_tool_usage_name ON tool_usage(tool_name)"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def record(self, tool_name: str, success: bool, latency_ms: float) -> None:
|
|
39
|
+
"""Record one tool invocation."""
|
|
40
|
+
with self._conn() as c:
|
|
41
|
+
c.execute(
|
|
42
|
+
"INSERT INTO tool_usage (tool_name, success, latency_ms, created_at) VALUES (?, ?, ?, ?)",
|
|
43
|
+
(tool_name, 1 if success else 0, latency_ms, time.time()),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def get_stats(self) -> list[dict]:
|
|
47
|
+
"""Return per-tool stats: name, count, success_rate, avg_latency_ms."""
|
|
48
|
+
with self._conn() as c:
|
|
49
|
+
rows = c.execute("""
|
|
50
|
+
SELECT tool_name,
|
|
51
|
+
COUNT(*) AS cnt,
|
|
52
|
+
SUM(success) AS ok,
|
|
53
|
+
AVG(latency_ms) AS avg_ms
|
|
54
|
+
FROM tool_usage
|
|
55
|
+
GROUP BY tool_name
|
|
56
|
+
ORDER BY cnt DESC
|
|
57
|
+
""").fetchall()
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
"tool_name": r[0],
|
|
61
|
+
"count": r[1],
|
|
62
|
+
"success_count": r[2],
|
|
63
|
+
"success_rate": (r[2] / r[1] * 100.0) if r[1] else 0.0,
|
|
64
|
+
"avg_latency_ms": round(r[3], 2) if r[3] is not None else 0.0,
|
|
65
|
+
}
|
|
66
|
+
for r in rows
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_default: ToolAnalytics | None = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_default_analytics() -> ToolAnalytics:
|
|
74
|
+
"""Return the default analytics instance (singleton)."""
|
|
75
|
+
global _default
|
|
76
|
+
if _default is None:
|
|
77
|
+
_default = ToolAnalytics()
|
|
78
|
+
return _default
|
devsper/audit/logger.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Append-only audit log: JSONL file per run with optional chain integrity.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AuditRecord:
|
|
16
|
+
"""Single append-only audit entry."""
|
|
17
|
+
record_id: str
|
|
18
|
+
timestamp: str
|
|
19
|
+
run_id: str
|
|
20
|
+
task_id: str
|
|
21
|
+
agent_id: str
|
|
22
|
+
event_type: str
|
|
23
|
+
actor: str
|
|
24
|
+
resource: str
|
|
25
|
+
input_hash: str
|
|
26
|
+
output_hash: str
|
|
27
|
+
decision_rationale: str | None = None
|
|
28
|
+
quota_usage: dict | None = None
|
|
29
|
+
pii_detected: bool = False
|
|
30
|
+
pii_redacted: bool = False
|
|
31
|
+
duration_ms: int = 0
|
|
32
|
+
success: bool = True
|
|
33
|
+
prev_record_hash: str = ""
|
|
34
|
+
|
|
35
|
+
def to_json_line(self) -> str:
|
|
36
|
+
d = {
|
|
37
|
+
"record_id": self.record_id,
|
|
38
|
+
"timestamp": self.timestamp,
|
|
39
|
+
"run_id": self.run_id,
|
|
40
|
+
"task_id": self.task_id,
|
|
41
|
+
"agent_id": self.agent_id,
|
|
42
|
+
"event_type": self.event_type,
|
|
43
|
+
"actor": self.actor,
|
|
44
|
+
"resource": self.resource,
|
|
45
|
+
"input_hash": self.input_hash,
|
|
46
|
+
"output_hash": self.output_hash,
|
|
47
|
+
"decision_rationale": self.decision_rationale,
|
|
48
|
+
"quota_usage": self.quota_usage,
|
|
49
|
+
"pii_detected": self.pii_detected,
|
|
50
|
+
"pii_redacted": self.pii_redacted,
|
|
51
|
+
"duration_ms": self.duration_ms,
|
|
52
|
+
"success": self.success,
|
|
53
|
+
"prev_record_hash": self.prev_record_hash,
|
|
54
|
+
}
|
|
55
|
+
return json.dumps(d, sort_keys=True)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_json_line(cls, line: str) -> "AuditRecord":
|
|
59
|
+
d = json.loads(line)
|
|
60
|
+
return cls(
|
|
61
|
+
record_id=d.get("record_id", ""),
|
|
62
|
+
timestamp=d.get("timestamp", ""),
|
|
63
|
+
run_id=d.get("run_id", ""),
|
|
64
|
+
task_id=d.get("task_id", ""),
|
|
65
|
+
agent_id=d.get("agent_id", ""),
|
|
66
|
+
event_type=d.get("event_type", ""),
|
|
67
|
+
actor=d.get("actor", ""),
|
|
68
|
+
resource=d.get("resource", ""),
|
|
69
|
+
input_hash=d.get("input_hash", ""),
|
|
70
|
+
output_hash=d.get("output_hash", ""),
|
|
71
|
+
decision_rationale=d.get("decision_rationale"),
|
|
72
|
+
quota_usage=d.get("quota_usage"),
|
|
73
|
+
pii_detected=bool(d.get("pii_detected", False)),
|
|
74
|
+
pii_redacted=bool(d.get("pii_redacted", False)),
|
|
75
|
+
duration_ms=int(d.get("duration_ms", 0)),
|
|
76
|
+
success=bool(d.get("success", True)),
|
|
77
|
+
prev_record_hash=d.get("prev_record_hash", ""),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _sha256(s: str) -> str:
|
|
82
|
+
return hashlib.sha256(s.encode("utf-8")).hexdigest()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def make_audit_record(
|
|
86
|
+
run_id: str,
|
|
87
|
+
task_id: str,
|
|
88
|
+
event_type: str,
|
|
89
|
+
actor: str = "",
|
|
90
|
+
resource: str = "",
|
|
91
|
+
input_text: str = "",
|
|
92
|
+
output_text: str = "",
|
|
93
|
+
duration_ms: int = 0,
|
|
94
|
+
success: bool = True,
|
|
95
|
+
pii_detected: bool = False,
|
|
96
|
+
pii_redacted: bool = False,
|
|
97
|
+
agent_id: str = "",
|
|
98
|
+
) -> AuditRecord:
|
|
99
|
+
"""Build an AuditRecord with hashes; agent_id = node_id + task_id or task_id."""
|
|
100
|
+
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
101
|
+
return AuditRecord(
|
|
102
|
+
record_id=str(uuid.uuid4()),
|
|
103
|
+
timestamp=ts,
|
|
104
|
+
run_id=run_id,
|
|
105
|
+
task_id=task_id,
|
|
106
|
+
agent_id=agent_id or task_id,
|
|
107
|
+
event_type=event_type,
|
|
108
|
+
actor=actor,
|
|
109
|
+
resource=resource,
|
|
110
|
+
input_hash=_sha256(input_text),
|
|
111
|
+
output_hash=_sha256(output_text),
|
|
112
|
+
pii_detected=pii_detected,
|
|
113
|
+
pii_redacted=pii_redacted,
|
|
114
|
+
duration_ms=duration_ms,
|
|
115
|
+
success=success,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AuditLogger:
|
|
120
|
+
"""Write-once append-only audit log. Backend: JSONL file under data_dir/audit/{run_id}.audit.jsonl."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, data_dir: str, run_id: str = "") -> None:
|
|
123
|
+
self.data_dir = data_dir
|
|
124
|
+
self.run_id = run_id
|
|
125
|
+
self._audit_dir = os.path.join(data_dir, "audit")
|
|
126
|
+
self._last_hash: str = ""
|
|
127
|
+
self._file_path: str | None = None
|
|
128
|
+
|
|
129
|
+
def _path(self, run_id: str) -> str:
|
|
130
|
+
return os.path.join(self._audit_dir, f"{run_id or self.run_id}.audit.jsonl")
|
|
131
|
+
|
|
132
|
+
def _ensure_chain(self, path: str) -> None:
|
|
133
|
+
"""Load last line hash from file so chain continues."""
|
|
134
|
+
if self._file_path == path:
|
|
135
|
+
return
|
|
136
|
+
self._file_path = path
|
|
137
|
+
self._last_hash = ""
|
|
138
|
+
if os.path.isfile(path):
|
|
139
|
+
try:
|
|
140
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
141
|
+
for line in f:
|
|
142
|
+
line = line.strip()
|
|
143
|
+
if line:
|
|
144
|
+
self._last_hash = _sha256(line)
|
|
145
|
+
except Exception:
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
def log(self, record: AuditRecord) -> None:
|
|
149
|
+
"""Append one record to the run's audit file. Permissions 0o600."""
|
|
150
|
+
run_id = record.run_id or self.run_id
|
|
151
|
+
if not run_id:
|
|
152
|
+
return
|
|
153
|
+
path = self._path(run_id)
|
|
154
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
155
|
+
self._ensure_chain(path)
|
|
156
|
+
record.prev_record_hash = self._last_hash
|
|
157
|
+
line = record.to_json_line()
|
|
158
|
+
self._last_hash = _sha256(line)
|
|
159
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
160
|
+
f.write(line + "\n")
|
|
161
|
+
try:
|
|
162
|
+
os.chmod(path, 0o600)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
def export(self, run_id: str, format: str = "jsonl") -> str:
|
|
167
|
+
"""Return formatted export for compliance (jsonl, csv, siem)."""
|
|
168
|
+
path = self._path(run_id)
|
|
169
|
+
if not os.path.isfile(path):
|
|
170
|
+
return ""
|
|
171
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
172
|
+
lines = [ln.strip() for ln in f if ln.strip()]
|
|
173
|
+
records = [AuditRecord.from_json_line(ln) for ln in lines]
|
|
174
|
+
if format == "jsonl":
|
|
175
|
+
return "\n".join(r.to_json_line() for r in records)
|
|
176
|
+
if format == "csv":
|
|
177
|
+
headers = [
|
|
178
|
+
"record_id", "timestamp", "run_id", "task_id", "event_type",
|
|
179
|
+
"actor", "resource", "success", "duration_ms", "pii_detected", "pii_redacted",
|
|
180
|
+
]
|
|
181
|
+
rows = [
|
|
182
|
+
",".join(
|
|
183
|
+
str(getattr(r, h, "")).replace(",", ";")
|
|
184
|
+
for h in headers
|
|
185
|
+
)
|
|
186
|
+
for r in records
|
|
187
|
+
]
|
|
188
|
+
return ",".join(headers) + "\n" + "\n".join(rows)
|
|
189
|
+
if format == "siem":
|
|
190
|
+
return json.dumps([r.to_json_line() for r in records])
|
|
191
|
+
return "\n".join(r.to_json_line() for r in records)
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def verify(run_id: str, data_dir: str) -> tuple[bool, str]:
|
|
195
|
+
"""
|
|
196
|
+
Verify chain integrity: each record's prev_record_hash must match previous line hash.
|
|
197
|
+
Return (ok, message).
|
|
198
|
+
"""
|
|
199
|
+
audit_dir = os.path.join(data_dir, "audit")
|
|
200
|
+
path = os.path.join(audit_dir, f"{run_id}.audit.jsonl")
|
|
201
|
+
if not os.path.isfile(path):
|
|
202
|
+
return False, "Audit file not found"
|
|
203
|
+
prev_hash = ""
|
|
204
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
205
|
+
for i, line in enumerate(f, 1):
|
|
206
|
+
line = line.strip()
|
|
207
|
+
if not line:
|
|
208
|
+
continue
|
|
209
|
+
rec = AuditRecord.from_json_line(line)
|
|
210
|
+
expected = _sha256(line)
|
|
211
|
+
if rec.prev_record_hash != prev_hash:
|
|
212
|
+
return False, f"Chain break at record {i}: prev_record_hash mismatch"
|
|
213
|
+
prev_hash = expected
|
|
214
|
+
return True, "Chain intact"
|