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,734 @@
|
|
|
1
|
+
"""Shared operation gateway for local and remote MCP/HTTP clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import contextlib
|
|
7
|
+
import inspect
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
11
|
+
from contextvars import copy_context
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
from dataclasses import replace
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from filelock import FileLock
|
|
17
|
+
from mcp import types
|
|
18
|
+
from mcp.server.lowlevel import Server
|
|
19
|
+
|
|
20
|
+
from attocode_intel.catalog import (
|
|
21
|
+
INSTRUCTIONS,
|
|
22
|
+
INSTRUCTIONS_NO_WATCH,
|
|
23
|
+
compact_schema,
|
|
24
|
+
is_write,
|
|
25
|
+
registered_tools,
|
|
26
|
+
tool_catalog,
|
|
27
|
+
)
|
|
28
|
+
from attocode_intel.output import bounded_compact, bounded_text, response_tokens
|
|
29
|
+
from attocode_intel.project_dir import cache_folder
|
|
30
|
+
from attocode_intel.query_ranking import hit_evidence, next_search_top_k
|
|
31
|
+
from attocode_intel.request_context import RequestContext, bind_request, resolve_workspace
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OperationGateway:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
project: str = "",
|
|
38
|
+
profile: str = "full",
|
|
39
|
+
resolver=None,
|
|
40
|
+
*,
|
|
41
|
+
watch=True,
|
|
42
|
+
watch_debounce=500,
|
|
43
|
+
max_workspaces=8,
|
|
44
|
+
):
|
|
45
|
+
self.project = project
|
|
46
|
+
self.profile = profile
|
|
47
|
+
self.resolver = resolver
|
|
48
|
+
self._workers = {}
|
|
49
|
+
self._stores = {}
|
|
50
|
+
self.watch, self.watch_debounce = watch, watch_debounce
|
|
51
|
+
self.instructions = INSTRUCTIONS if watch else INSTRUCTIONS_NO_WATCH
|
|
52
|
+
self._closed = False
|
|
53
|
+
self.max_workspaces = max(1, max_workspaces)
|
|
54
|
+
self._active = {}
|
|
55
|
+
self._last_used = {}
|
|
56
|
+
self._lifecycle = asyncio.Lock()
|
|
57
|
+
self._catalogs = {}
|
|
58
|
+
|
|
59
|
+
def effective_profile(self):
|
|
60
|
+
from attocode_intel.remote import remote_profile
|
|
61
|
+
return remote_profile.get() if self.resolver else self.profile
|
|
62
|
+
|
|
63
|
+
def _catalog(self):
|
|
64
|
+
key = (self.effective_profile(), bool(self.resolver))
|
|
65
|
+
if key not in self._catalogs:
|
|
66
|
+
from jsonschema.validators import validator_for
|
|
67
|
+
catalog = tool_catalog(key[0], remote=key[1])
|
|
68
|
+
self._catalogs[key] = (catalog, {tool.name: validator_for(tool.inputSchema)(tool.inputSchema)
|
|
69
|
+
for tool in catalog})
|
|
70
|
+
return self._catalogs[key]
|
|
71
|
+
|
|
72
|
+
def catalog(self, *, mcp=False):
|
|
73
|
+
catalog = deepcopy(self._catalog()[0])
|
|
74
|
+
if not mcp:
|
|
75
|
+
return catalog
|
|
76
|
+
if not self.resolver and self.effective_profile() == "daily":
|
|
77
|
+
# A local agent does not need these. They stay callable, for harnesses and diagnostics.
|
|
78
|
+
hidden = {"hydration_status", "cross_repo_search"} | ({"notify_file_changed"} if self.watch else set())
|
|
79
|
+
catalog = [tool for tool in catalog if tool.name not in hidden]
|
|
80
|
+
# A model reads this list in every request, so it gets no docstring indent and no pydantic noise.
|
|
81
|
+
for tool in catalog:
|
|
82
|
+
tool.description = inspect.cleandoc(tool.description or "")
|
|
83
|
+
tool.inputSchema = compact_schema(tool.inputSchema)
|
|
84
|
+
properties = tool.inputSchema["properties"]
|
|
85
|
+
if not self.resolver and "workspace" in properties:
|
|
86
|
+
properties["workspace"] = {"type": "string", "description": "Project path (default: this project)"}
|
|
87
|
+
if self.effective_profile() == "daily":
|
|
88
|
+
budget = {"type": "integer", "maximum": 32000, "default": 2000}
|
|
89
|
+
properties["max_tokens"] = {**budget, "minimum": 512} if is_write(tool.name) else budget
|
|
90
|
+
return catalog
|
|
91
|
+
|
|
92
|
+
async def close(self):
|
|
93
|
+
self._closed = True
|
|
94
|
+
async with self._lifecycle:
|
|
95
|
+
for key in list(self._workers):
|
|
96
|
+
await self._drop_workspace(key)
|
|
97
|
+
|
|
98
|
+
async def _drop_workspace(self, key):
|
|
99
|
+
stores, worker = self._stores[key], self._workers[key]
|
|
100
|
+
if watcher := stores.get("watcher"):
|
|
101
|
+
await asyncio.to_thread(watcher.close)
|
|
102
|
+
await asyncio.get_running_loop().run_in_executor(worker, self._close_stores, stores)
|
|
103
|
+
worker.shutdown(wait=True)
|
|
104
|
+
for mapping in (self._workers, self._stores, self._active, self._last_used):
|
|
105
|
+
mapping.pop(key, None)
|
|
106
|
+
|
|
107
|
+
async def _acquire_workspace(self, context):
|
|
108
|
+
key = (context.project_dir, context.source)
|
|
109
|
+
async with self._lifecycle:
|
|
110
|
+
if self._closed:
|
|
111
|
+
raise ValueError("Gateway is closed")
|
|
112
|
+
if key not in self._workers:
|
|
113
|
+
if len(self._workers) >= self.max_workspaces:
|
|
114
|
+
idle = [key for key in self._workers if not self._active[key]]
|
|
115
|
+
if not idle:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
"All workspace slots are busy; retry after an operation completes"
|
|
118
|
+
)
|
|
119
|
+
await self._drop_workspace(min(idle, key=self._last_used.get))
|
|
120
|
+
self._workers[key] = ThreadPoolExecutor(
|
|
121
|
+
max_workers=1, thread_name_prefix="intel-workspace"
|
|
122
|
+
)
|
|
123
|
+
self._stores[key] = {}
|
|
124
|
+
self._active[key] = 0
|
|
125
|
+
self._active[key] += 1
|
|
126
|
+
self._last_used[key] = time.monotonic()
|
|
127
|
+
return replace(context, stores=self._stores[key])
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _close_stores(stores):
|
|
131
|
+
service = stores.get("service")
|
|
132
|
+
if service:
|
|
133
|
+
if service._ast_service:
|
|
134
|
+
service._ast_service.stop_hydration()
|
|
135
|
+
if service._ast_service._store:
|
|
136
|
+
service._ast_service._store.close()
|
|
137
|
+
if service._lsp_manager:
|
|
138
|
+
runner = stores.get("async_runner") or stores.setdefault("async_runner", asyncio.Runner())
|
|
139
|
+
runner.run(service._lsp_manager.stop_all())
|
|
140
|
+
for resource in (service._memory_store, service._semantic_search):
|
|
141
|
+
if resource:
|
|
142
|
+
resource.close()
|
|
143
|
+
for name, resource in stores.items():
|
|
144
|
+
if name not in {"service", "watcher"} and hasattr(resource, "close"):
|
|
145
|
+
resource.close()
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _warm_search(context):
|
|
149
|
+
with bind_request(context):
|
|
150
|
+
context.service._get_semantic_search().warm()
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _refresh_sync(context):
|
|
154
|
+
from attocode_intel.freshness import FreshnessTracker
|
|
155
|
+
|
|
156
|
+
with bind_request(context):
|
|
157
|
+
directory = cache_folder(context.project_dir) / "cache"
|
|
158
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
with FileLock(str(directory / "operations.lock"), timeout=30):
|
|
160
|
+
tracker = context.stores.setdefault(
|
|
161
|
+
"freshness", FreshnessTracker(context.project_dir)
|
|
162
|
+
)
|
|
163
|
+
tracker.refresh(context.service)
|
|
164
|
+
|
|
165
|
+
async def execute_mcp(self, name: str, arguments: dict) -> types.CallToolResult:
|
|
166
|
+
return await self.execute(name, arguments, compact=self.effective_profile() == "daily")
|
|
167
|
+
|
|
168
|
+
async def execute(self, name: str, arguments: dict, *, compact=False) -> types.CallToolResult:
|
|
169
|
+
from attocode_intel.telemetry import emit, enabled
|
|
170
|
+
from attocode_intel.telemetry import timings as active_timings
|
|
171
|
+
start = time.monotonic()
|
|
172
|
+
timings = {}
|
|
173
|
+
token = active_timings.set(timings)
|
|
174
|
+
try:
|
|
175
|
+
result = await self._dispatch(name, arguments, compact, timings)
|
|
176
|
+
if enabled():
|
|
177
|
+
emit({"operation": name, "compact": compact, "timings_ms": timings,
|
|
178
|
+
"total_ms": round((time.monotonic() - start) * 1000, 2),
|
|
179
|
+
"response_bytes": len(result.model_dump_json(exclude_none=True).encode()),
|
|
180
|
+
"response_tokens": response_tokens(result), "error": bool(result.isError)})
|
|
181
|
+
return result
|
|
182
|
+
except Exception:
|
|
183
|
+
emit({"operation": name, "compact": compact, "timings_ms": timings,
|
|
184
|
+
"total_ms": round((time.monotonic() - start) * 1000, 2), "error": True})
|
|
185
|
+
raise
|
|
186
|
+
finally:
|
|
187
|
+
active_timings.reset(token)
|
|
188
|
+
|
|
189
|
+
async def _dispatch(self, name, arguments, compact, timings):
|
|
190
|
+
if self._closed:
|
|
191
|
+
raise ValueError("Gateway is closed")
|
|
192
|
+
args = dict(arguments)
|
|
193
|
+
if compact:
|
|
194
|
+
args.setdefault("max_tokens", 2000)
|
|
195
|
+
workspace = args.pop("workspace", "")
|
|
196
|
+
revision = args.pop("revision", "")
|
|
197
|
+
before = time.monotonic()
|
|
198
|
+
validators = self._catalog()[1]
|
|
199
|
+
if name not in validators:
|
|
200
|
+
raise ValueError(f"Tool {name!r} is not available in profile {self.profile!r}")
|
|
201
|
+
validators[name].validate(arguments)
|
|
202
|
+
if compact and is_write(name) and args["max_tokens"] < 512:
|
|
203
|
+
raise ValueError("Write operations require max_tokens >= 512 so their outcome can be acknowledged")
|
|
204
|
+
timings["validation"] = round((time.monotonic() - before) * 1000, 2)
|
|
205
|
+
if name == "cross_repo_search":
|
|
206
|
+
from attocode_intel.service import SEARCH_WARMUP_WAIT
|
|
207
|
+
|
|
208
|
+
results = []
|
|
209
|
+
warming_workspaces = []
|
|
210
|
+
# Each workspace gets the short wait of the loop below, not the full first-search wait.
|
|
211
|
+
wait = SEARCH_WARMUP_WAIT.set(0.75)
|
|
212
|
+
try:
|
|
213
|
+
for target in args["workspaces"]:
|
|
214
|
+
request = {
|
|
215
|
+
"workspace": target,
|
|
216
|
+
"query": args["query"],
|
|
217
|
+
"top_k": args.get("top_k", 10),
|
|
218
|
+
"mode": "keyword",
|
|
219
|
+
}
|
|
220
|
+
# Tiny/new workspaces commonly finish indexing within this bound.
|
|
221
|
+
# Keep the outer search responsive on large checkouts, where the
|
|
222
|
+
# explicit warming signal is more honest than an empty match set.
|
|
223
|
+
deadline = time.monotonic() + 0.75
|
|
224
|
+
while True:
|
|
225
|
+
response = await self.execute("semantic_search", request)
|
|
226
|
+
index = response.structuredContent["metadata"].get("ranking", {}).get("index", {})
|
|
227
|
+
if index.get("status") != "warming" or time.monotonic() >= deadline:
|
|
228
|
+
break
|
|
229
|
+
await asyncio.sleep(0.05)
|
|
230
|
+
structured = response.structuredContent
|
|
231
|
+
if index.get("status") == "warming":
|
|
232
|
+
warming_workspaces.append(target)
|
|
233
|
+
for rank, match in enumerate(structured["data"]["results"], 1):
|
|
234
|
+
results.append(
|
|
235
|
+
{
|
|
236
|
+
**match,
|
|
237
|
+
"workspace": structured["metadata"]["workspace"],
|
|
238
|
+
"revision": structured["metadata"]["revision"],
|
|
239
|
+
"source": structured["metadata"]["source"],
|
|
240
|
+
"freshness": structured["metadata"]["freshness"],
|
|
241
|
+
"index": structured["metadata"]["coverage"].get("phase", "unknown"),
|
|
242
|
+
"fusion_score": 1 / (60 + rank),
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
finally:
|
|
246
|
+
SEARCH_WARMUP_WAIT.reset(wait)
|
|
247
|
+
results.sort(key=lambda row: row["fusion_score"], reverse=True)
|
|
248
|
+
results = results[: args.get("top_k", 10)]
|
|
249
|
+
budget = args.get("max_tokens", 8000)
|
|
250
|
+
body = ("Search index warming in: " + ", ".join(warming_workspaces)
|
|
251
|
+
+ ". Missing results do not prove absence."
|
|
252
|
+
if not results and warming_workspaces else json.dumps(results))
|
|
253
|
+
text, truncated = bounded_text(body, budget)
|
|
254
|
+
if compact:
|
|
255
|
+
return bounded_compact({"truncated": truncated, "ranking": "reciprocal_rank",
|
|
256
|
+
"workspaces": args["workspaces"],
|
|
257
|
+
"warming_workspaces": warming_workspaces,
|
|
258
|
+
"analysis": {"status": "partial", "absence_proven": False}}, results, budget)
|
|
259
|
+
return types.CallToolResult(
|
|
260
|
+
content=[types.TextContent(type="text", text=text)],
|
|
261
|
+
structuredContent={
|
|
262
|
+
"data": results,
|
|
263
|
+
"metadata": {
|
|
264
|
+
"truncated": truncated,
|
|
265
|
+
"ranking": "reciprocal_rank",
|
|
266
|
+
"workspaces": args["workspaces"],
|
|
267
|
+
"warming_workspaces": warming_workspaces,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
if self.resolver:
|
|
272
|
+
context = await self.resolver(workspace, revision, name)
|
|
273
|
+
if compact and is_write(name):
|
|
274
|
+
self._check_write_budget(context, args["max_tokens"])
|
|
275
|
+
from attocode_intel.knowledge import KNOWLEDGE_TOOLS, execute_knowledge
|
|
276
|
+
|
|
277
|
+
if name in KNOWLEDGE_TOOLS:
|
|
278
|
+
budget = args.pop("max_tokens", 8000)
|
|
279
|
+
data = await execute_knowledge(context, name, args)
|
|
280
|
+
if compact:
|
|
281
|
+
return self._compact(name, {"workspace": context.workspace, "revision": context.revision,
|
|
282
|
+
"source": "remote", "freshness": "committed_snapshot",
|
|
283
|
+
"truncated": False}, data, budget)
|
|
284
|
+
header = f"Source: remote | Workspace: {context.workspace} | Revision: {context.revision}\n"
|
|
285
|
+
text, truncated = bounded_text(header + json.dumps(data), budget)
|
|
286
|
+
return types.CallToolResult(
|
|
287
|
+
content=[types.TextContent(type="text", text=text)],
|
|
288
|
+
structuredContent={
|
|
289
|
+
"result": text,
|
|
290
|
+
"data": data,
|
|
291
|
+
"metadata": {
|
|
292
|
+
"workspace": context.workspace,
|
|
293
|
+
"revision": context.revision,
|
|
294
|
+
"source": "remote",
|
|
295
|
+
"truncated": truncated,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
if revision:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
"Local analysis uses the working tree; revision selection is remote-only."
|
|
303
|
+
)
|
|
304
|
+
path = resolve_workspace(workspace, self.project)
|
|
305
|
+
context = RequestContext(path, path)
|
|
306
|
+
key = (context.project_dir, context.source)
|
|
307
|
+
if self.resolver and name == "bootstrap" and args.get("task_hint"):
|
|
308
|
+
from attocode_intel.knowledge import execute_knowledge
|
|
309
|
+
|
|
310
|
+
args["_knowledge"] = await execute_knowledge(
|
|
311
|
+
context, "recall", {"query": args["task_hint"], "max_results": 5}
|
|
312
|
+
)
|
|
313
|
+
context = await self._acquire_workspace(context)
|
|
314
|
+
if self.watch and context.source == "local" and "watcher" not in context.stores:
|
|
315
|
+
from attocode_intel.freshness import WorkspaceWatcher
|
|
316
|
+
|
|
317
|
+
context.stores["watcher"] = WorkspaceWatcher(
|
|
318
|
+
context.project_dir,
|
|
319
|
+
lambda: (
|
|
320
|
+
None if self._closed else self._workers[key].submit(self._refresh_sync, context)
|
|
321
|
+
),
|
|
322
|
+
self.watch_debounce,
|
|
323
|
+
)
|
|
324
|
+
try:
|
|
325
|
+
timings["enqueued_at"] = time.monotonic()
|
|
326
|
+
return await asyncio.get_running_loop().run_in_executor(
|
|
327
|
+
self._workers[key],
|
|
328
|
+
copy_context().run,
|
|
329
|
+
self._execute_sync,
|
|
330
|
+
context,
|
|
331
|
+
name,
|
|
332
|
+
args,
|
|
333
|
+
compact,
|
|
334
|
+
timings,
|
|
335
|
+
)
|
|
336
|
+
finally:
|
|
337
|
+
if key in self._active:
|
|
338
|
+
self._active[key] -= 1
|
|
339
|
+
if context.source == "local" and not self._closed and key in self._workers:
|
|
340
|
+
# Each call starts the search warm-up after its own work, so that this call
|
|
341
|
+
# is not slower and a later search finds ready indexes.
|
|
342
|
+
with contextlib.suppress(RuntimeError):
|
|
343
|
+
self._workers[key].submit(self._warm_search, context)
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def _check_write_budget(context, budget):
|
|
347
|
+
bounded_compact({"workspace": context.workspace, "source": context.source,
|
|
348
|
+
"revision": context.revision, "freshness": "committed_snapshot" if context.source == "remote" else "working_tree"},
|
|
349
|
+
{"status": "completed"}, budget - 128)
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def _compact(name, metadata, payload, budget):
|
|
353
|
+
try:
|
|
354
|
+
return bounded_compact(metadata, payload, budget)
|
|
355
|
+
except ValueError:
|
|
356
|
+
if not is_write(name):
|
|
357
|
+
raise
|
|
358
|
+
return bounded_compact({**metadata, "truncated": True},
|
|
359
|
+
{"status": "completed", "result_omitted": True}, budget)
|
|
360
|
+
|
|
361
|
+
def _execute_sync(self, context, name, args, compact=False, timings=None):
|
|
362
|
+
timings = timings if timings is not None else {}
|
|
363
|
+
timings["queue"] = round((time.monotonic() - timings.pop("enqueued_at", time.monotonic())) * 1000, 2)
|
|
364
|
+
lock_dir = cache_folder(context.project_dir) / "cache"
|
|
365
|
+
lock_dir.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
start = time.monotonic()
|
|
367
|
+
budget = int(args.get("max_tokens", 8000))
|
|
368
|
+
knowledge = args.pop("_knowledge", None)
|
|
369
|
+
precision_result = None
|
|
370
|
+
ranking = None
|
|
371
|
+
operation_args = dict(args)
|
|
372
|
+
with FileLock(str(lock_dir / "operations.lock"), timeout=30), bind_request(context):
|
|
373
|
+
if compact and is_write(name):
|
|
374
|
+
self._check_write_budget(context, budget)
|
|
375
|
+
timings["lock"] = round((time.monotonic() - start) * 1000, 2)
|
|
376
|
+
if name == "capabilities":
|
|
377
|
+
from importlib.util import find_spec
|
|
378
|
+
|
|
379
|
+
from attocode_intel.analysis_coverage import language_capabilities
|
|
380
|
+
from attocode_intel.remote import remote_profile
|
|
381
|
+
payload = {
|
|
382
|
+
"workspace": context.workspace,
|
|
383
|
+
"source": context.source,
|
|
384
|
+
"revision": context.revision,
|
|
385
|
+
"profile": remote_profile.get() if self.resolver else self.profile,
|
|
386
|
+
"profiles": ["daily", "full"],
|
|
387
|
+
"tools": [t.name for t in self.catalog()],
|
|
388
|
+
"enhancements": {
|
|
389
|
+
"embeddings_package_installed": bool(find_spec("sentence_transformers")),
|
|
390
|
+
"watcher_active": "watcher" in context.stores,
|
|
391
|
+
},
|
|
392
|
+
"guidance": self.instructions,
|
|
393
|
+
"languages": language_capabilities(getattr(context.stores.get("precision"), "verified_languages", ())),
|
|
394
|
+
}
|
|
395
|
+
text = json.dumps(payload, indent=2)
|
|
396
|
+
else:
|
|
397
|
+
from attocode_intel.local_knowledge import LEARNING_TOOLS, execute_local_knowledge
|
|
398
|
+
|
|
399
|
+
tool = registered_tools().get(name)
|
|
400
|
+
# Validate even when called by a REST operation endpoint.
|
|
401
|
+
if tool:
|
|
402
|
+
args = tool.fn_metadata.arg_model.model_validate(args).model_dump()
|
|
403
|
+
else:
|
|
404
|
+
args.pop("max_tokens", None)
|
|
405
|
+
service = context.service
|
|
406
|
+
if context.source == "remote":
|
|
407
|
+
root = Path(context.project_dir).resolve()
|
|
408
|
+
for key, value in args.items():
|
|
409
|
+
if key in {
|
|
410
|
+
"path",
|
|
411
|
+
"file",
|
|
412
|
+
"file_path",
|
|
413
|
+
"files",
|
|
414
|
+
"changed_files",
|
|
415
|
+
"directory",
|
|
416
|
+
"scope",
|
|
417
|
+
}:
|
|
418
|
+
for candidate in value if isinstance(value, list) else [value]:
|
|
419
|
+
if candidate and not (root / candidate).resolve().is_relative_to(
|
|
420
|
+
root
|
|
421
|
+
):
|
|
422
|
+
raise ValueError(
|
|
423
|
+
"Path must remain inside the selected repository"
|
|
424
|
+
)
|
|
425
|
+
if context.source == "local":
|
|
426
|
+
from attocode_intel.freshness import FreshnessTracker
|
|
427
|
+
|
|
428
|
+
tracker = context.stores.setdefault(
|
|
429
|
+
"freshness", FreshnessTracker(context.project_dir)
|
|
430
|
+
)
|
|
431
|
+
before = time.monotonic()
|
|
432
|
+
tracker.refresh(service)
|
|
433
|
+
timings["freshness"] = round((time.monotonic() - before) * 1000, 2)
|
|
434
|
+
if name == "notify_file_changed":
|
|
435
|
+
context.stores.pop("reference_pages", None)
|
|
436
|
+
if name in {"cross_references", "call_graph", "inspect_symbol"} and not args.get("cursor"):
|
|
437
|
+
from attocode_intel.precision import PrecisionSession
|
|
438
|
+
session = context.stores.setdefault("precision", PrecisionSession(service))
|
|
439
|
+
runner = context.stores.get("async_runner") or context.stores.setdefault("async_runner", asyncio.Runner())
|
|
440
|
+
before = time.monotonic()
|
|
441
|
+
precision_result = runner.run(session.enrich(args.get("symbol_name", args.get("symbol", "")),
|
|
442
|
+
args.get("file_path"), args.get("line")), context=copy_context())
|
|
443
|
+
timings["precision"] = round((time.monotonic() - before) * 1000, 2)
|
|
444
|
+
payload = None
|
|
445
|
+
before = time.monotonic()
|
|
446
|
+
if name in LEARNING_TOOLS:
|
|
447
|
+
payload = execute_local_knowledge(context, name, args)
|
|
448
|
+
text = json.dumps(payload)
|
|
449
|
+
elif name == "review_change":
|
|
450
|
+
from attocode_intel.change_review import review
|
|
451
|
+
|
|
452
|
+
payload = review(service, args.get("files"), args.get("mode", "full"))
|
|
453
|
+
text = json.dumps(payload, default=str, indent=2)
|
|
454
|
+
elif name == "semantic_search":
|
|
455
|
+
mgr = service._get_semantic_search()
|
|
456
|
+
candidates = service._search_after_warmup(mgr, lambda: (
|
|
457
|
+
mgr.search_candidates(args["query"], service._retrieval_depth(args["top_k"]), args["file_filter"])
|
|
458
|
+
if args.get("mode") == "keyword"
|
|
459
|
+
else mgr.search(
|
|
460
|
+
args["query"], top_k=service._retrieval_depth(args["top_k"]), file_filter=args["file_filter"]
|
|
461
|
+
)
|
|
462
|
+
))
|
|
463
|
+
results, ranking = service._rank_search_results(
|
|
464
|
+
args["query"], candidates, args["top_k"], args["file_filter"],
|
|
465
|
+
)
|
|
466
|
+
ranking["candidate_pool_count"] = len(candidates)
|
|
467
|
+
if hasattr(mgr, "candidate_diagnostics"):
|
|
468
|
+
ranking["index"] = mgr.candidate_diagnostics()
|
|
469
|
+
result_cap = max(1, budget // 120)
|
|
470
|
+
omitted_results = max(0, len(results) - result_cap)
|
|
471
|
+
results = results[:result_cap]
|
|
472
|
+
ranking["delivered_count"] = len(results)
|
|
473
|
+
if omitted_results:
|
|
474
|
+
ranking["omitted_results_due_to_budget"] = omitted_results
|
|
475
|
+
snippet_cap = max(32, min(320, budget // max(1, len(results))))
|
|
476
|
+
hits = []
|
|
477
|
+
for result in results:
|
|
478
|
+
snippet = (result.text.split("\n", 1)[-1]
|
|
479
|
+
if result.start_line else result.text)
|
|
480
|
+
hits.append({
|
|
481
|
+
"file_path": result.file_path,
|
|
482
|
+
"score": result.score,
|
|
483
|
+
"snippet": snippet[:snippet_cap],
|
|
484
|
+
"snippet_truncated": len(snippet) > snippet_cap,
|
|
485
|
+
"line": result.start_line or None,
|
|
486
|
+
"end_line": result.end_line or None,
|
|
487
|
+
**hit_evidence(args["query"], result),
|
|
488
|
+
})
|
|
489
|
+
payload = {
|
|
490
|
+
"query": args["query"],
|
|
491
|
+
"results": hits,
|
|
492
|
+
"ranking": ranking,
|
|
493
|
+
}
|
|
494
|
+
if ranking.get("index", {}).get("status") == "ready":
|
|
495
|
+
next_k = next_search_top_k(
|
|
496
|
+
len(candidates), args["top_k"], len(results),
|
|
497
|
+
)
|
|
498
|
+
if next_k is not None:
|
|
499
|
+
request = {
|
|
500
|
+
"query": args["query"], "top_k": next_k,
|
|
501
|
+
"file_filter": args["file_filter"],
|
|
502
|
+
"mode": args.get("mode", "auto"),
|
|
503
|
+
"workspace": context.workspace,
|
|
504
|
+
"max_tokens": min(32000, max(budget * 2, next_k * 160 + 512)),
|
|
505
|
+
}
|
|
506
|
+
if args.get("branch"):
|
|
507
|
+
request["branch"] = args["branch"]
|
|
508
|
+
if args.get("revision"):
|
|
509
|
+
request["revision"] = args["revision"]
|
|
510
|
+
payload["follow_up"] = {
|
|
511
|
+
"tool": "semantic_search", "arguments": request,
|
|
512
|
+
"reruns_current_index": True,
|
|
513
|
+
}
|
|
514
|
+
note = (
|
|
515
|
+
"Broad query matches multiple components; narrow with file_filter or a component name.\n"
|
|
516
|
+
if ranking["query"]["ambiguous"] else ""
|
|
517
|
+
)
|
|
518
|
+
if payload.get("follow_up"):
|
|
519
|
+
note += (
|
|
520
|
+
"More retrieved candidates: rerun with "
|
|
521
|
+
f"top_k={payload['follow_up']['arguments']['top_k']} "
|
|
522
|
+
"(current index).\n"
|
|
523
|
+
)
|
|
524
|
+
text = note + mgr.format_results(results, query=args["query"])
|
|
525
|
+
if not results and ranking.get("index", {}).get("status") == "warming":
|
|
526
|
+
text = "Search index warming; retry shortly. Missing results do not prove absence."
|
|
527
|
+
elif name == "inspect_symbol":
|
|
528
|
+
from attocode_intel.symbol_inspection import inspect_symbol_data
|
|
529
|
+
payload = inspect_symbol_data(service, args["symbol_name"], args.get("file_path"), args.get("line"),
|
|
530
|
+
args.get("source_start_line"), args.get("task_hint"))
|
|
531
|
+
text = json.dumps(payload)
|
|
532
|
+
elif name == "suggest_tests":
|
|
533
|
+
from attocode_intel.tools.composite_tools import (
|
|
534
|
+
format_test_suggestions,
|
|
535
|
+
suggest_tests_data,
|
|
536
|
+
)
|
|
537
|
+
payload = suggest_tests_data(args["files"], symbol_name=args.get("symbol_name"), task_hint=args.get("task_hint"))
|
|
538
|
+
text = format_test_suggestions(payload)
|
|
539
|
+
elif name == "cross_references":
|
|
540
|
+
from attocode_intel.reference_pages import continuation
|
|
541
|
+
from attocode_intel.symbol_inspection import scoped_references
|
|
542
|
+
saved = continuation(context, args)
|
|
543
|
+
payload = saved["payload"] if saved else scoped_references(
|
|
544
|
+
service, args["symbol_name"], args.get("file_path"), args.get("line"))
|
|
545
|
+
text = json.dumps(payload)
|
|
546
|
+
elif name in {"symbols", "search_symbols", "dependencies", "call_graph", "file_analysis",
|
|
547
|
+
"hotspots", "dependency_graph", "impact_analysis"}:
|
|
548
|
+
method = getattr(service, name + "_data")
|
|
549
|
+
parameters = inspect.signature(method).parameters
|
|
550
|
+
payload = method(**{key: value for key, value in args.items() if key in parameters})
|
|
551
|
+
text = json.dumps(payload, default=str, indent=2)
|
|
552
|
+
else:
|
|
553
|
+
result = tool.fn(**args)
|
|
554
|
+
if inspect.isawaitable(result):
|
|
555
|
+
runner = context.stores.get("async_runner") or context.stores.setdefault("async_runner", asyncio.Runner())
|
|
556
|
+
result = runner.run(result, context=copy_context())
|
|
557
|
+
text = result if isinstance(result, str) else json.dumps(result, default=str)
|
|
558
|
+
if name in {
|
|
559
|
+
"install_pack",
|
|
560
|
+
"install_community_pack",
|
|
561
|
+
"import_rules",
|
|
562
|
+
"rule_hygiene",
|
|
563
|
+
"evolve_rules",
|
|
564
|
+
}:
|
|
565
|
+
context.stores.pop("rules", None)
|
|
566
|
+
timings["execution"] = round((time.monotonic() - before) * 1000, 2)
|
|
567
|
+
if name == "bootstrap" and args.get("task_hint"):
|
|
568
|
+
if context.source == "local":
|
|
569
|
+
from attocode_intel.local_knowledge import execute_local_knowledge
|
|
570
|
+
|
|
571
|
+
knowledge = execute_local_knowledge(
|
|
572
|
+
context, "recall", {"query": args["task_hint"], "max_results": 5}
|
|
573
|
+
)
|
|
574
|
+
knowledge = [entry for entry in knowledge or [] if not entry.get("stale")]
|
|
575
|
+
if knowledge:
|
|
576
|
+
knowledge_text = bounded_text(json.dumps(knowledge), max(1, budget // 4))[0]
|
|
577
|
+
section = ("## Relevant project knowledge (verify entries marked stale)\n"
|
|
578
|
+
+ knowledge_text)
|
|
579
|
+
if text.startswith("## Relevant Code"):
|
|
580
|
+
boundary = text.find("\n\n## ")
|
|
581
|
+
text = (text[:boundary] + "\n\n" + section + text[boundary:]
|
|
582
|
+
if boundary >= 0 else text + "\n\n" + section)
|
|
583
|
+
else:
|
|
584
|
+
text = section + "\n\n" + text
|
|
585
|
+
service = context.service
|
|
586
|
+
ast_service = service._ast_service
|
|
587
|
+
coverage = ast_service.hydration_snapshot() if ast_service else {"phase": "not_started"}
|
|
588
|
+
phase = coverage.get("phase", "unknown")
|
|
589
|
+
header = f"Source: {context.source} | Workspace: {context.workspace} | Revision: {context.revision} | Index: {phase}\n"
|
|
590
|
+
if phase != "ready" or coverage.get("discovery_truncated"):
|
|
591
|
+
header += "Coverage is incomplete; missing results do not prove absence.\n"
|
|
592
|
+
from attocode_intel.analysis_coverage import analysis_report
|
|
593
|
+
analysis = (analysis_report(service, precision_result) if not compact or name == "capabilities" else
|
|
594
|
+
{"status": "partial" if ast_service else "not_started", "absence_proven": False,
|
|
595
|
+
"precision": precision_result or {"status": "not_requested"}})
|
|
596
|
+
if name == "capabilities":
|
|
597
|
+
payload["analysis"] = analysis
|
|
598
|
+
payload["coverage"] = coverage
|
|
599
|
+
text = json.dumps(payload)
|
|
600
|
+
if name in {"cross_references", "dependencies", "call_graph", "impact_analysis", "suggest_tests"}:
|
|
601
|
+
header += "Analysis: partial; relationships are candidates and missing results do not prove absence.\n"
|
|
602
|
+
body = text
|
|
603
|
+
text, truncated = bounded_text(header + text, budget) if not compact else (text, False)
|
|
604
|
+
truncated = truncated or "[Truncated;" in text
|
|
605
|
+
metadata = {
|
|
606
|
+
"workspace": context.workspace,
|
|
607
|
+
"source": context.source,
|
|
608
|
+
"revision": context.revision,
|
|
609
|
+
"coverage": coverage,
|
|
610
|
+
"analysis": analysis,
|
|
611
|
+
"freshness": "committed_snapshot" if context.source == "remote" else "working_tree",
|
|
612
|
+
"truncated": truncated or bool(ranking and ranking.get("omitted_results_due_to_budget")),
|
|
613
|
+
"duration_ms": round((time.monotonic() - start) * 1000, 2),
|
|
614
|
+
}
|
|
615
|
+
if ranking is not None:
|
|
616
|
+
metadata["ranking"] = ranking
|
|
617
|
+
if name == "inspect_symbol":
|
|
618
|
+
metadata["truncated"] |= bool(payload.get("source", {}).get("truncated")) or any(
|
|
619
|
+
payload.get("total_" + section, 0) > len(payload.get(section, []))
|
|
620
|
+
for section in ("references", "imports", "tests", "definitions"))
|
|
621
|
+
structured = {"result": text, "metadata": metadata}
|
|
622
|
+
before = time.monotonic()
|
|
623
|
+
if name == "cross_references" and (compact or operation_args.get("cursor") or operation_args.get("page_size") is not None):
|
|
624
|
+
from attocode_intel.reference_pages import paginate
|
|
625
|
+
page = paginate(context, operation_args, payload, metadata, budget, compact)
|
|
626
|
+
timings["serialization"] = round((time.monotonic() - before) * 1000, 2)
|
|
627
|
+
if compact:
|
|
628
|
+
return page
|
|
629
|
+
payload, metadata = page
|
|
630
|
+
text, _ = bounded_text(header + json.dumps(payload), budget)
|
|
631
|
+
structured = {"result": text, "metadata": metadata}
|
|
632
|
+
if compact:
|
|
633
|
+
if name == "bootstrap":
|
|
634
|
+
from attocode_intel.output import bounded_bootstrap
|
|
635
|
+
result = bounded_bootstrap(metadata, body, budget)
|
|
636
|
+
else:
|
|
637
|
+
result = self._compact(name, metadata, payload if payload is not None else {"result": body}, budget)
|
|
638
|
+
timings["serialization"] = round((time.monotonic() - before) * 1000, 2)
|
|
639
|
+
return result
|
|
640
|
+
if payload is not None:
|
|
641
|
+
structured["capabilities" if name == "capabilities" else "data"] = payload
|
|
642
|
+
return types.CallToolResult(
|
|
643
|
+
content=[types.TextContent(type="text", text=text)], structuredContent=structured
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def create_mcp_server(gateway: OperationGateway) -> Server:
|
|
648
|
+
from attocode_intel import __version__
|
|
649
|
+
|
|
650
|
+
server = Server("attocode-code-intel", version=__version__, instructions=gateway.instructions)
|
|
651
|
+
|
|
652
|
+
@server.list_tools()
|
|
653
|
+
async def list_tools():
|
|
654
|
+
return gateway.catalog(mcp=True)
|
|
655
|
+
|
|
656
|
+
@server.call_tool()
|
|
657
|
+
async def call_tool(name: str, arguments: dict):
|
|
658
|
+
# Advertised roots are advisory, never a substitute for remote authorization.
|
|
659
|
+
if not gateway.resolver and not gateway.project and not arguments.get("workspace"):
|
|
660
|
+
session = server.request_context.session
|
|
661
|
+
try:
|
|
662
|
+
roots = await session.list_roots()
|
|
663
|
+
from urllib.parse import unquote, urlparse
|
|
664
|
+
|
|
665
|
+
paths = [
|
|
666
|
+
unquote(urlparse(str(root.uri)).path)
|
|
667
|
+
for root in roots.roots
|
|
668
|
+
if str(root.uri).startswith("file://")
|
|
669
|
+
]
|
|
670
|
+
except Exception:
|
|
671
|
+
paths = []
|
|
672
|
+
# A client started in the home folder advertises home as its root.
|
|
673
|
+
# Indexing all of home is not a useful default; --project ~ still selects it.
|
|
674
|
+
home = Path.home().resolve()
|
|
675
|
+
paths = [path for path in paths if Path(path).resolve() != home]
|
|
676
|
+
if len(paths) == 1:
|
|
677
|
+
arguments = {**arguments, "workspace": paths[0]}
|
|
678
|
+
elif len(paths) > 1:
|
|
679
|
+
raise ValueError(f"Multiple workspace roots; supply workspace explicitly: {paths}")
|
|
680
|
+
return await gateway.execute_mcp(name, arguments)
|
|
681
|
+
|
|
682
|
+
@server.list_resources()
|
|
683
|
+
async def list_resources():
|
|
684
|
+
return [
|
|
685
|
+
types.Resource(
|
|
686
|
+
uri="attocode://guidelines", name="Agent guidelines", mimeType="text/plain"
|
|
687
|
+
)
|
|
688
|
+
]
|
|
689
|
+
|
|
690
|
+
@server.list_resource_templates()
|
|
691
|
+
async def list_resource_templates():
|
|
692
|
+
return [
|
|
693
|
+
types.ResourceTemplate(uriTemplate=template, name=name, mimeType="text/plain")
|
|
694
|
+
for template, name in (
|
|
695
|
+
(
|
|
696
|
+
"attocode://project/{path}{?workspace,revision}",
|
|
697
|
+
"File analysis in a selected workspace",
|
|
698
|
+
),
|
|
699
|
+
(
|
|
700
|
+
"attocode://symbols/{name}{?workspace,revision}",
|
|
701
|
+
"Symbol references in a selected workspace",
|
|
702
|
+
),
|
|
703
|
+
("attocode://learnings{?workspace,revision}", "Repository knowledge"),
|
|
704
|
+
)
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
@server.read_resource()
|
|
708
|
+
async def read_resource(uri):
|
|
709
|
+
from urllib.parse import parse_qs, unquote, urlparse
|
|
710
|
+
|
|
711
|
+
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
712
|
+
|
|
713
|
+
if str(uri) == "attocode://guidelines":
|
|
714
|
+
return [ReadResourceContents(gateway.instructions, mime_type="text/plain")]
|
|
715
|
+
parsed = urlparse(str(uri))
|
|
716
|
+
if parsed.scheme != "attocode":
|
|
717
|
+
raise ValueError("Unknown resource scheme")
|
|
718
|
+
args = {
|
|
719
|
+
key: value[-1]
|
|
720
|
+
for key, value in parse_qs(parsed.query).items()
|
|
721
|
+
if key in {"workspace", "revision"}
|
|
722
|
+
}
|
|
723
|
+
if parsed.netloc == "project":
|
|
724
|
+
name, args["path"] = "symbols", unquote(parsed.path.lstrip("/"))
|
|
725
|
+
elif parsed.netloc == "symbols":
|
|
726
|
+
name, args["symbol_name"] = "cross_references", unquote(parsed.path.lstrip("/"))
|
|
727
|
+
elif parsed.netloc == "learnings":
|
|
728
|
+
name = "list_learnings"
|
|
729
|
+
else:
|
|
730
|
+
raise ValueError("Unknown intelligence resource")
|
|
731
|
+
result = await call_tool(name, args)
|
|
732
|
+
return [ReadResourceContents(result.content[0].text, mime_type="text/plain")]
|
|
733
|
+
|
|
734
|
+
return server
|