attocode-code-intel 0.1.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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Recursive context retrieval technique.
|
|
2
|
+
|
|
3
|
+
Implements a strategy where the agent progressively deepens context
|
|
4
|
+
by following references found in initial results. Useful for understanding
|
|
5
|
+
complex code flows that span multiple files.
|
|
6
|
+
|
|
7
|
+
Strategy:
|
|
8
|
+
1. Start with a seed query (file path, symbol, etc.)
|
|
9
|
+
2. Retrieve initial context
|
|
10
|
+
3. Extract references/imports from the context
|
|
11
|
+
4. Recursively retrieve context for those references
|
|
12
|
+
5. Stop at a depth/token budget limit
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from collections import deque
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Protocol
|
|
21
|
+
|
|
22
|
+
from attocode_intel._internal.integrations.utilities.token_estimate import estimate_tokens as _estimate_tokens
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ContentProvider(Protocol):
|
|
26
|
+
"""Protocol for retrieving file content."""
|
|
27
|
+
|
|
28
|
+
def get_content(self, file_path: str) -> str | None: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class ContextNode:
|
|
33
|
+
"""A node in the recursive context tree."""
|
|
34
|
+
|
|
35
|
+
file_path: str
|
|
36
|
+
content: str
|
|
37
|
+
depth: int
|
|
38
|
+
references: list[str] = field(default_factory=list)
|
|
39
|
+
estimated_tokens: int = 0
|
|
40
|
+
is_truncated: bool = False
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
if self.estimated_tokens == 0 and self.content:
|
|
44
|
+
self.estimated_tokens = _estimate_tokens(self.content)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True)
|
|
48
|
+
class RecursiveContextResult:
|
|
49
|
+
"""Result of recursive context retrieval."""
|
|
50
|
+
|
|
51
|
+
nodes: list[ContextNode]
|
|
52
|
+
total_tokens: int
|
|
53
|
+
max_depth_reached: int
|
|
54
|
+
files_visited: int
|
|
55
|
+
budget_exhausted: bool = False
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def content(self) -> str:
|
|
59
|
+
"""Combined content from all nodes."""
|
|
60
|
+
parts: list[str] = []
|
|
61
|
+
for node in self.nodes:
|
|
62
|
+
header = f"--- {node.file_path} (depth {node.depth}) ---"
|
|
63
|
+
parts.append(f"{header}\n{node.content}")
|
|
64
|
+
return "\n\n".join(parts)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def extract_python_references(content: str, current_file: str = "") -> list[str]:
|
|
68
|
+
"""Extract file references from Python import statements.
|
|
69
|
+
|
|
70
|
+
Returns a list of potential file paths derived from imports.
|
|
71
|
+
"""
|
|
72
|
+
references: list[str] = []
|
|
73
|
+
|
|
74
|
+
# from X import Y
|
|
75
|
+
for match in re.finditer(r"from\s+([\w.]+)\s+import", content):
|
|
76
|
+
module = match.group(1)
|
|
77
|
+
# Convert module path to file path
|
|
78
|
+
path = module.replace(".", "/") + ".py"
|
|
79
|
+
references.append(path)
|
|
80
|
+
|
|
81
|
+
# import X or import X, Y, Z
|
|
82
|
+
for match in re.finditer(r"^import\s+([\w.,\s]+)", content, re.MULTILINE):
|
|
83
|
+
modules_str = match.group(1)
|
|
84
|
+
for module in modules_str.split(","):
|
|
85
|
+
module = module.strip()
|
|
86
|
+
if module and re.fullmatch(r"[\w.]+", module):
|
|
87
|
+
path = module.replace(".", "/") + ".py"
|
|
88
|
+
references.append(path)
|
|
89
|
+
|
|
90
|
+
return references
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def extract_js_references(content: str, current_file: str = "") -> list[str]:
|
|
94
|
+
"""Extract file references from JS/TS import statements.
|
|
95
|
+
|
|
96
|
+
Returns a list of potential file paths.
|
|
97
|
+
"""
|
|
98
|
+
references: list[str] = []
|
|
99
|
+
|
|
100
|
+
# import ... from 'path'
|
|
101
|
+
for match in re.finditer(r"""from\s+['"]([^'"]+)['"]""", content):
|
|
102
|
+
ref = match.group(1)
|
|
103
|
+
if ref.startswith("."):
|
|
104
|
+
# Relative import
|
|
105
|
+
if not ref.endswith((".js", ".ts", ".tsx", ".jsx")):
|
|
106
|
+
references.append(ref + ".ts")
|
|
107
|
+
references.append(ref + ".js")
|
|
108
|
+
else:
|
|
109
|
+
references.append(ref)
|
|
110
|
+
|
|
111
|
+
# export { X } from './y' (barrel re-exports)
|
|
112
|
+
for match in re.finditer(r"""export\s+\{[^}]*\}\s+from\s+['"]([^'"]+)['"]""", content):
|
|
113
|
+
ref = match.group(1)
|
|
114
|
+
if ref.startswith("."):
|
|
115
|
+
if not ref.endswith((".js", ".ts", ".tsx", ".jsx")):
|
|
116
|
+
references.append(ref + ".ts")
|
|
117
|
+
references.append(ref + ".js")
|
|
118
|
+
else:
|
|
119
|
+
references.append(ref)
|
|
120
|
+
|
|
121
|
+
# require('path')
|
|
122
|
+
for match in re.finditer(r"""require\s*\(\s*['"]([^'"]+)['"]\s*\)""", content):
|
|
123
|
+
ref = match.group(1)
|
|
124
|
+
if ref.startswith("."):
|
|
125
|
+
references.append(ref + ".js")
|
|
126
|
+
|
|
127
|
+
return references
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def extract_references(content: str, file_path: str) -> list[str]:
|
|
131
|
+
"""Extract file references based on file type."""
|
|
132
|
+
if file_path.endswith(".py"):
|
|
133
|
+
return extract_python_references(content, file_path)
|
|
134
|
+
elif file_path.endswith((".js", ".ts", ".tsx", ".jsx")):
|
|
135
|
+
return extract_js_references(content, file_path)
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class RecursiveContextRetriever:
|
|
140
|
+
"""Retrieves context by recursively following references.
|
|
141
|
+
|
|
142
|
+
Starting from a seed file, follows imports and references
|
|
143
|
+
up to a configurable depth and token budget.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
provider: Content provider for reading files.
|
|
147
|
+
max_depth: Maximum recursion depth (default 3).
|
|
148
|
+
token_budget: Maximum total tokens across all files.
|
|
149
|
+
max_files: Maximum number of files to visit.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self,
|
|
154
|
+
provider: ContentProvider,
|
|
155
|
+
*,
|
|
156
|
+
max_depth: int = 3,
|
|
157
|
+
token_budget: int = 50_000,
|
|
158
|
+
max_files: int = 20,
|
|
159
|
+
) -> None:
|
|
160
|
+
self._provider = provider
|
|
161
|
+
self._max_depth = max_depth
|
|
162
|
+
self._token_budget = token_budget
|
|
163
|
+
self._max_files = max_files
|
|
164
|
+
|
|
165
|
+
def retrieve(
|
|
166
|
+
self,
|
|
167
|
+
seed_files: list[str],
|
|
168
|
+
*,
|
|
169
|
+
max_content_per_file: int = 10_000,
|
|
170
|
+
) -> RecursiveContextResult:
|
|
171
|
+
"""Recursively retrieve context starting from seed files.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
seed_files: Initial files to start from.
|
|
175
|
+
max_content_per_file: Maximum characters per file content.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A :class:`RecursiveContextResult` with all gathered context.
|
|
179
|
+
"""
|
|
180
|
+
visited: set[str] = set()
|
|
181
|
+
nodes: list[ContextNode] = []
|
|
182
|
+
total_tokens = 0
|
|
183
|
+
max_depth_reached = 0
|
|
184
|
+
|
|
185
|
+
# BFS queue: (file_path, depth)
|
|
186
|
+
queue: deque[tuple[str, int]] = deque((f, 0) for f in seed_files)
|
|
187
|
+
|
|
188
|
+
while queue:
|
|
189
|
+
file_path, depth = queue.popleft()
|
|
190
|
+
|
|
191
|
+
# Skip if already visited
|
|
192
|
+
if file_path in visited:
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
# Check limits
|
|
196
|
+
if depth > self._max_depth:
|
|
197
|
+
continue
|
|
198
|
+
if len(visited) >= self._max_files:
|
|
199
|
+
break
|
|
200
|
+
if total_tokens >= self._token_budget:
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
visited.add(file_path)
|
|
204
|
+
|
|
205
|
+
content = self._provider.get_content(file_path)
|
|
206
|
+
if content is None:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
is_truncated = False
|
|
210
|
+
if len(content) > max_content_per_file:
|
|
211
|
+
content = content[:max_content_per_file]
|
|
212
|
+
is_truncated = True
|
|
213
|
+
|
|
214
|
+
# Check if adding this would exceed budget
|
|
215
|
+
est_tokens = _estimate_tokens(content)
|
|
216
|
+
if total_tokens + est_tokens > self._token_budget:
|
|
217
|
+
# Try to fit with truncation
|
|
218
|
+
remaining = self._token_budget - total_tokens
|
|
219
|
+
chars = remaining * 4
|
|
220
|
+
if chars < 200:
|
|
221
|
+
break
|
|
222
|
+
content = content[:chars]
|
|
223
|
+
is_truncated = True
|
|
224
|
+
est_tokens = _estimate_tokens(content)
|
|
225
|
+
|
|
226
|
+
# Extract references for next depth
|
|
227
|
+
references = extract_references(content, file_path)
|
|
228
|
+
|
|
229
|
+
node = ContextNode(
|
|
230
|
+
file_path=file_path,
|
|
231
|
+
content=content,
|
|
232
|
+
depth=depth,
|
|
233
|
+
references=references,
|
|
234
|
+
estimated_tokens=est_tokens,
|
|
235
|
+
is_truncated=is_truncated,
|
|
236
|
+
)
|
|
237
|
+
nodes.append(node)
|
|
238
|
+
total_tokens += est_tokens
|
|
239
|
+
max_depth_reached = max(max_depth_reached, depth)
|
|
240
|
+
|
|
241
|
+
# Enqueue references for next depth
|
|
242
|
+
for ref in references:
|
|
243
|
+
if ref not in visited:
|
|
244
|
+
queue.append((ref, depth + 1))
|
|
245
|
+
|
|
246
|
+
return RecursiveContextResult(
|
|
247
|
+
nodes=nodes,
|
|
248
|
+
total_tokens=total_tokens,
|
|
249
|
+
max_depth_reached=max_depth_reached,
|
|
250
|
+
files_visited=len(visited),
|
|
251
|
+
budget_exhausted=total_tokens >= self._token_budget,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
def retrieve_with_priority(
|
|
255
|
+
self,
|
|
256
|
+
seed_files: list[str],
|
|
257
|
+
priority_files: list[str] | None = None,
|
|
258
|
+
*,
|
|
259
|
+
max_content_per_file: int = 10_000,
|
|
260
|
+
) -> RecursiveContextResult:
|
|
261
|
+
"""Retrieve with priority files getting budget preference.
|
|
262
|
+
|
|
263
|
+
Priority files are loaded first and given more token budget.
|
|
264
|
+
Remaining budget is used for recursive exploration.
|
|
265
|
+
"""
|
|
266
|
+
_priority = set(priority_files or [])
|
|
267
|
+
visited: set[str] = set()
|
|
268
|
+
nodes: list[ContextNode] = []
|
|
269
|
+
total_tokens = 0
|
|
270
|
+
max_depth_reached = 0
|
|
271
|
+
|
|
272
|
+
# Phase 1: Load priority files first (use 50% of budget)
|
|
273
|
+
priority_budget = self._token_budget // 2
|
|
274
|
+
for file_path in (priority_files or []):
|
|
275
|
+
if file_path in visited:
|
|
276
|
+
continue
|
|
277
|
+
content = self._provider.get_content(file_path)
|
|
278
|
+
if content is None:
|
|
279
|
+
continue
|
|
280
|
+
visited.add(file_path)
|
|
281
|
+
is_truncated = False
|
|
282
|
+
if len(content) > max_content_per_file:
|
|
283
|
+
content = content[:max_content_per_file]
|
|
284
|
+
is_truncated = True
|
|
285
|
+
est_tokens = _estimate_tokens(content)
|
|
286
|
+
if total_tokens + est_tokens > priority_budget:
|
|
287
|
+
remaining = priority_budget - total_tokens
|
|
288
|
+
chars = remaining * 4
|
|
289
|
+
if chars < 200:
|
|
290
|
+
break
|
|
291
|
+
content = content[:chars]
|
|
292
|
+
is_truncated = True
|
|
293
|
+
est_tokens = _estimate_tokens(content)
|
|
294
|
+
references = extract_references(content, file_path)
|
|
295
|
+
nodes.append(ContextNode(
|
|
296
|
+
file_path=file_path,
|
|
297
|
+
content=content,
|
|
298
|
+
depth=0,
|
|
299
|
+
references=references,
|
|
300
|
+
estimated_tokens=est_tokens,
|
|
301
|
+
is_truncated=is_truncated,
|
|
302
|
+
))
|
|
303
|
+
total_tokens += est_tokens
|
|
304
|
+
|
|
305
|
+
# Phase 2: BFS from seeds with remaining budget
|
|
306
|
+
queue: deque[tuple[str, int]] = deque(
|
|
307
|
+
(f, 0) for f in seed_files if f not in visited
|
|
308
|
+
)
|
|
309
|
+
# Also enqueue references from priority files
|
|
310
|
+
for node in nodes:
|
|
311
|
+
for ref in node.references:
|
|
312
|
+
if ref not in visited:
|
|
313
|
+
queue.append((ref, 1))
|
|
314
|
+
|
|
315
|
+
while queue and total_tokens < self._token_budget and len(visited) < self._max_files:
|
|
316
|
+
file_path, depth = queue.popleft()
|
|
317
|
+
if file_path in visited or depth > self._max_depth:
|
|
318
|
+
continue
|
|
319
|
+
visited.add(file_path)
|
|
320
|
+
content = self._provider.get_content(file_path)
|
|
321
|
+
if content is None:
|
|
322
|
+
continue
|
|
323
|
+
is_truncated = False
|
|
324
|
+
if len(content) > max_content_per_file:
|
|
325
|
+
content = content[:max_content_per_file]
|
|
326
|
+
is_truncated = True
|
|
327
|
+
est_tokens = _estimate_tokens(content)
|
|
328
|
+
if total_tokens + est_tokens > self._token_budget:
|
|
329
|
+
remaining = self._token_budget - total_tokens
|
|
330
|
+
chars = remaining * 4
|
|
331
|
+
if chars < 200:
|
|
332
|
+
break
|
|
333
|
+
content = content[:chars]
|
|
334
|
+
is_truncated = True
|
|
335
|
+
est_tokens = _estimate_tokens(content)
|
|
336
|
+
references = extract_references(content, file_path)
|
|
337
|
+
nodes.append(ContextNode(
|
|
338
|
+
file_path=file_path,
|
|
339
|
+
content=content,
|
|
340
|
+
depth=depth,
|
|
341
|
+
references=references,
|
|
342
|
+
estimated_tokens=est_tokens,
|
|
343
|
+
is_truncated=is_truncated,
|
|
344
|
+
))
|
|
345
|
+
total_tokens += est_tokens
|
|
346
|
+
max_depth_reached = max(max_depth_reached, depth)
|
|
347
|
+
for ref in references:
|
|
348
|
+
if ref not in visited:
|
|
349
|
+
queue.append((ref, depth + 1))
|
|
350
|
+
|
|
351
|
+
return RecursiveContextResult(
|
|
352
|
+
nodes=nodes,
|
|
353
|
+
total_tokens=total_tokens,
|
|
354
|
+
max_depth_reached=max_depth_reached,
|
|
355
|
+
files_visited=len(visited),
|
|
356
|
+
budget_exhausted=total_tokens >= self._token_budget,
|
|
357
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal code intelligence support; not a public API."""
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Agent event types.
|
|
2
|
+
|
|
3
|
+
Comprehensive event system covering lifecycle, execution, multi-agent,
|
|
4
|
+
policy, context, resilience, insight, and subagent categories.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import StrEnum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
# Shared callback type for simple event listeners used across the codebase.
|
|
15
|
+
SimpleEventListener = Callable[[str, dict[str, Any]], None]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SimpleEventEmitter:
|
|
19
|
+
"""Mixin: subscribe/emit ``(event, data)`` events. Listener errors are swallowed."""
|
|
20
|
+
|
|
21
|
+
_listeners: list[SimpleEventListener]
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._listeners = []
|
|
25
|
+
|
|
26
|
+
def on(self, listener: SimpleEventListener) -> Callable[[], None]:
|
|
27
|
+
self._listeners.append(listener)
|
|
28
|
+
|
|
29
|
+
def unsubscribe() -> None:
|
|
30
|
+
if listener in self._listeners:
|
|
31
|
+
self._listeners.remove(listener)
|
|
32
|
+
|
|
33
|
+
return unsubscribe
|
|
34
|
+
|
|
35
|
+
def _emit(self, event: str, data: dict[str, Any]) -> None:
|
|
36
|
+
for listener in self._listeners:
|
|
37
|
+
try:
|
|
38
|
+
listener(event, data)
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EventType(StrEnum):
|
|
44
|
+
"""Types of agent events.
|
|
45
|
+
|
|
46
|
+
Organized into categories:
|
|
47
|
+
- Lifecycle: start, complete, error, shutdown
|
|
48
|
+
- Execution: iteration, tool.*, llm.*
|
|
49
|
+
- Budget: budget.*
|
|
50
|
+
- Context: compaction.*, context.*
|
|
51
|
+
- Multi-agent: subagent.*, swarm.*
|
|
52
|
+
- Policy: policy.*, permission.*
|
|
53
|
+
- Session: session.*
|
|
54
|
+
- Mode: mode.*
|
|
55
|
+
- Plan: plan.*
|
|
56
|
+
- Quality: quality.*, learning.*
|
|
57
|
+
- Resilience: resilience.*, circuit_breaker.*
|
|
58
|
+
- MCP: mcp.*
|
|
59
|
+
- Undo: undo.*
|
|
60
|
+
- Insight: insight.*
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# --- Lifecycle ---
|
|
64
|
+
START = "start"
|
|
65
|
+
COMPLETE = "complete"
|
|
66
|
+
ERROR = "error"
|
|
67
|
+
SHUTDOWN = "shutdown"
|
|
68
|
+
STATUS = "status" # General status update (e.g. "Indexing codebase...")
|
|
69
|
+
|
|
70
|
+
# --- Execution core ---
|
|
71
|
+
ITERATION = "iteration"
|
|
72
|
+
THINKING = "thinking"
|
|
73
|
+
RESPONSE = "response"
|
|
74
|
+
|
|
75
|
+
# --- Tool events ---
|
|
76
|
+
TOOL_START = "tool.start"
|
|
77
|
+
TOOL_COMPLETE = "tool.complete"
|
|
78
|
+
TOOL_ERROR = "tool.error"
|
|
79
|
+
TOOL_APPROVAL_REQUESTED = "tool.approval.requested"
|
|
80
|
+
TOOL_APPROVAL_GRANTED = "tool.approval.granted"
|
|
81
|
+
TOOL_APPROVAL_DENIED = "tool.approval.denied"
|
|
82
|
+
TOOL_BLOCKED = "tool.blocked"
|
|
83
|
+
TOOL_COERCED = "tool.coerced"
|
|
84
|
+
TOOL_CALL_MISMATCH = "tool.call.mismatch"
|
|
85
|
+
SUSPICIOUS_TOOL_MARKUP = "tool.markup.suspicious"
|
|
86
|
+
LOOP_GUARD_ACTIVATED = "tool.loop_guard.activated"
|
|
87
|
+
|
|
88
|
+
# --- LLM events ---
|
|
89
|
+
LLM_START = "llm.start"
|
|
90
|
+
LLM_COMPLETE = "llm.complete"
|
|
91
|
+
LLM_ERROR = "llm.error"
|
|
92
|
+
LLM_RETRY = "llm.retry"
|
|
93
|
+
LLM_STREAM_START = "llm.stream.start"
|
|
94
|
+
LLM_STREAM_CHUNK = "llm.stream.chunk"
|
|
95
|
+
LLM_STREAM_END = "llm.stream.end"
|
|
96
|
+
|
|
97
|
+
# --- Budget events ---
|
|
98
|
+
BUDGET_WARNING = "budget.warning"
|
|
99
|
+
BUDGET_EXHAUSTED = "budget.exhausted"
|
|
100
|
+
BUDGET_CHECK = "budget.check"
|
|
101
|
+
BUDGET_EXTENSION_REQUESTED = "budget.extension.requested"
|
|
102
|
+
BUDGET_EXTENSION_GRANTED = "budget.extension.granted"
|
|
103
|
+
BUDGET_EXTENSION_DENIED = "budget.extension.denied"
|
|
104
|
+
|
|
105
|
+
# --- Context / Compaction events ---
|
|
106
|
+
COMPACTION = "compaction"
|
|
107
|
+
COMPACTION_START = "compaction.start"
|
|
108
|
+
COMPACTION_COMPLETE = "compaction.complete"
|
|
109
|
+
COMPACTION_ERROR = "compaction.error"
|
|
110
|
+
CONTEXT_FRESH_REFRESH = "context.fresh_refresh"
|
|
111
|
+
CONTEXT_OVERFLOW = "context.overflow"
|
|
112
|
+
CONTEXT_BREAKDOWN = "context.breakdown"
|
|
113
|
+
|
|
114
|
+
# --- Session events ---
|
|
115
|
+
SESSION_CREATED = "session.created"
|
|
116
|
+
SESSION_LOADED = "session.loaded"
|
|
117
|
+
SESSION_SAVED = "session.saved"
|
|
118
|
+
SESSION_RESUMED = "session.resumed"
|
|
119
|
+
SESSION_RESUME_REJECTED = "session.resume.rejected"
|
|
120
|
+
SESSION_FORKED = "session.forked"
|
|
121
|
+
SESSION_CHECKPOINT = "session.checkpoint"
|
|
122
|
+
|
|
123
|
+
# --- Mode events ---
|
|
124
|
+
MODE_CHANGED = "mode.changed"
|
|
125
|
+
MODE_WRITE_INTERCEPTED = "mode.write.intercepted"
|
|
126
|
+
|
|
127
|
+
# --- Plan events ---
|
|
128
|
+
PLAN_CREATED = "plan.created"
|
|
129
|
+
PLAN_STEP_START = "plan.step.start"
|
|
130
|
+
PLAN_STEP_COMPLETE = "plan.step.complete"
|
|
131
|
+
PLAN_APPROVED = "plan.approved"
|
|
132
|
+
PLAN_REJECTED = "plan.rejected"
|
|
133
|
+
PLAN_UPDATED = "plan.updated"
|
|
134
|
+
|
|
135
|
+
# --- Subagent events ---
|
|
136
|
+
SUBAGENT_SPAWN = "subagent.spawn"
|
|
137
|
+
SUBAGENT_COMPLETE = "subagent.complete"
|
|
138
|
+
SUBAGENT_ERROR = "subagent.error"
|
|
139
|
+
SUBAGENT_ITERATION = "subagent.iteration"
|
|
140
|
+
SUBAGENT_PHASE = "subagent.phase"
|
|
141
|
+
SUBAGENT_WRAPUP_STARTED = "subagent.wrapup.started"
|
|
142
|
+
SUBAGENT_WRAPUP_COMPLETED = "subagent.wrapup.completed"
|
|
143
|
+
SUBAGENT_TIMEOUT_HARD_KILL = "subagent.timeout.hard_kill"
|
|
144
|
+
|
|
145
|
+
# --- Swarm events ---
|
|
146
|
+
SWARM_START = "swarm.start"
|
|
147
|
+
SWARM_COMPLETE = "swarm.complete"
|
|
148
|
+
SWARM_TASK_QUEUED = "swarm.task.queued"
|
|
149
|
+
SWARM_TASK_START = "swarm.task.start"
|
|
150
|
+
SWARM_TASK_COMPLETE = "swarm.task.complete"
|
|
151
|
+
SWARM_TASK_FAILED = "swarm.task.failed"
|
|
152
|
+
SWARM_WAVE_START = "swarm.wave.start"
|
|
153
|
+
SWARM_WAVE_COMPLETE = "swarm.wave.complete"
|
|
154
|
+
SWARM_WORKER_ASSIGNED = "swarm.worker.assigned"
|
|
155
|
+
SWARM_WORKER_RELEASED = "swarm.worker.released"
|
|
156
|
+
|
|
157
|
+
# --- Policy events ---
|
|
158
|
+
POLICY_EVALUATION = "policy.evaluation"
|
|
159
|
+
POLICY_OVERRIDE = "policy.override"
|
|
160
|
+
PERMISSION_GRANTED = "permission.granted"
|
|
161
|
+
PERMISSION_DENIED = "permission.denied"
|
|
162
|
+
PERMISSION_REMEMBERED = "permission.remembered"
|
|
163
|
+
|
|
164
|
+
# --- Quality events ---
|
|
165
|
+
QUALITY_CHECK = "quality.check"
|
|
166
|
+
QUALITY_SCORE = "quality.score"
|
|
167
|
+
LEARNING_PROPOSED = "learning.proposed"
|
|
168
|
+
LEARNING_APPROVED = "learning.approved"
|
|
169
|
+
LEARNING_REJECTED = "learning.rejected"
|
|
170
|
+
HEALTH_CHECK = "health.check"
|
|
171
|
+
|
|
172
|
+
# --- Resilience events ---
|
|
173
|
+
RESILIENCE_RETRY = "resilience.retry"
|
|
174
|
+
RESILIENCE_FALLBACK = "resilience.fallback"
|
|
175
|
+
CIRCUIT_BREAKER_OPEN = "circuit_breaker.open"
|
|
176
|
+
CIRCUIT_BREAKER_HALF_OPEN = "circuit_breaker.half_open"
|
|
177
|
+
CIRCUIT_BREAKER_CLOSED = "circuit_breaker.closed"
|
|
178
|
+
|
|
179
|
+
# --- MCP events ---
|
|
180
|
+
MCP_CONNECT = "mcp.connect"
|
|
181
|
+
MCP_DISCONNECT = "mcp.disconnect"
|
|
182
|
+
MCP_TOOL_DISCOVERED = "mcp.tool.discovered"
|
|
183
|
+
MCP_TOOL_CALL = "mcp.tool.call"
|
|
184
|
+
MCP_TOOL_ERROR = "mcp.tool.error"
|
|
185
|
+
|
|
186
|
+
# --- Undo events ---
|
|
187
|
+
UNDO_TRACKED = "undo.tracked"
|
|
188
|
+
UNDO_EXECUTED = "undo.executed"
|
|
189
|
+
UNDO_FAILED = "undo.failed"
|
|
190
|
+
|
|
191
|
+
# --- Insight / Loop detection ---
|
|
192
|
+
INSIGHT_DOOM_LOOP = "insight.doom_loop"
|
|
193
|
+
INSIGHT_SATURATION = "insight.saturation"
|
|
194
|
+
INSIGHT_NUDGE = "insight.nudge"
|
|
195
|
+
INSIGHT_PHASE_CHANGE = "insight.phase_change"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class EventCategory(StrEnum):
|
|
199
|
+
"""Categories for event grouping."""
|
|
200
|
+
|
|
201
|
+
LIFECYCLE = "lifecycle"
|
|
202
|
+
EXECUTION = "execution"
|
|
203
|
+
TOOL = "tool"
|
|
204
|
+
LLM = "llm"
|
|
205
|
+
BUDGET = "budget"
|
|
206
|
+
CONTEXT = "context"
|
|
207
|
+
SESSION = "session"
|
|
208
|
+
MODE = "mode"
|
|
209
|
+
PLAN = "plan"
|
|
210
|
+
SUBAGENT = "subagent"
|
|
211
|
+
SWARM = "swarm"
|
|
212
|
+
POLICY = "policy"
|
|
213
|
+
QUALITY = "quality"
|
|
214
|
+
RESILIENCE = "resilience"
|
|
215
|
+
MCP = "mcp"
|
|
216
|
+
UNDO = "undo"
|
|
217
|
+
INSIGHT = "insight"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# Map events to categories
|
|
221
|
+
EVENT_CATEGORIES: dict[EventType, EventCategory] = {}
|
|
222
|
+
for _evt in EventType:
|
|
223
|
+
_name = _evt.value
|
|
224
|
+
if _name in ("start", "complete", "error", "shutdown"):
|
|
225
|
+
EVENT_CATEGORIES[_evt] = EventCategory.LIFECYCLE
|
|
226
|
+
elif _name.startswith("tool."):
|
|
227
|
+
EVENT_CATEGORIES[_evt] = EventCategory.TOOL
|
|
228
|
+
elif _name.startswith("llm."):
|
|
229
|
+
EVENT_CATEGORIES[_evt] = EventCategory.LLM
|
|
230
|
+
elif _name.startswith("budget."):
|
|
231
|
+
EVENT_CATEGORIES[_evt] = EventCategory.BUDGET
|
|
232
|
+
elif _name.startswith(("compaction", "context.")):
|
|
233
|
+
EVENT_CATEGORIES[_evt] = EventCategory.CONTEXT
|
|
234
|
+
elif _name.startswith("session."):
|
|
235
|
+
EVENT_CATEGORIES[_evt] = EventCategory.SESSION
|
|
236
|
+
elif _name.startswith("mode."):
|
|
237
|
+
EVENT_CATEGORIES[_evt] = EventCategory.MODE
|
|
238
|
+
elif _name.startswith("plan."):
|
|
239
|
+
EVENT_CATEGORIES[_evt] = EventCategory.PLAN
|
|
240
|
+
elif _name.startswith("subagent."):
|
|
241
|
+
EVENT_CATEGORIES[_evt] = EventCategory.SUBAGENT
|
|
242
|
+
elif _name.startswith("swarm."):
|
|
243
|
+
EVENT_CATEGORIES[_evt] = EventCategory.SWARM
|
|
244
|
+
elif _name.startswith(("policy.", "permission.")):
|
|
245
|
+
EVENT_CATEGORIES[_evt] = EventCategory.POLICY
|
|
246
|
+
elif _name.startswith(("quality.", "learning.", "health.")):
|
|
247
|
+
EVENT_CATEGORIES[_evt] = EventCategory.QUALITY
|
|
248
|
+
elif _name.startswith(("resilience.", "circuit_breaker.")):
|
|
249
|
+
EVENT_CATEGORIES[_evt] = EventCategory.RESILIENCE
|
|
250
|
+
elif _name.startswith("mcp."):
|
|
251
|
+
EVENT_CATEGORIES[_evt] = EventCategory.MCP
|
|
252
|
+
elif _name.startswith("undo."):
|
|
253
|
+
EVENT_CATEGORIES[_evt] = EventCategory.UNDO
|
|
254
|
+
elif _name.startswith("insight."):
|
|
255
|
+
EVENT_CATEGORIES[_evt] = EventCategory.INSIGHT
|
|
256
|
+
else:
|
|
257
|
+
EVENT_CATEGORIES[_evt] = EventCategory.EXECUTION
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_event_category(event_type: EventType) -> EventCategory:
|
|
261
|
+
"""Get the category for an event type."""
|
|
262
|
+
return EVENT_CATEGORIES.get(event_type, EventCategory.EXECUTION)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass(slots=True)
|
|
266
|
+
class AgentEvent:
|
|
267
|
+
"""An event emitted during agent execution."""
|
|
268
|
+
|
|
269
|
+
type: EventType
|
|
270
|
+
task: str | None = None
|
|
271
|
+
tool: str | None = None
|
|
272
|
+
args: dict[str, Any] | None = None
|
|
273
|
+
result: str | None = None
|
|
274
|
+
error: str | None = None
|
|
275
|
+
tokens: int | None = None
|
|
276
|
+
cost: float | None = None
|
|
277
|
+
iteration: int | None = None
|
|
278
|
+
metadata: dict[str, Any] | None = None
|
|
279
|
+
# Extended fields for rich events
|
|
280
|
+
session_id: str | None = None
|
|
281
|
+
agent_id: str | None = None
|
|
282
|
+
parent_id: str | None = None
|
|
283
|
+
duration_ms: float | None = None
|
|
284
|
+
timestamp: float | None = None
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def category(self) -> EventCategory:
|
|
288
|
+
"""Get the category of this event."""
|
|
289
|
+
return get_event_category(self.type)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Shared severity levels for findings, security patterns, etc."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Severity(StrEnum):
|
|
9
|
+
"""Finding severity level."""
|
|
10
|
+
|
|
11
|
+
CRITICAL = "critical"
|
|
12
|
+
HIGH = "high"
|
|
13
|
+
MEDIUM = "medium"
|
|
14
|
+
LOW = "low"
|
|
15
|
+
INFO = "info"
|