codecortex-context-engine 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. codecortex/__init__.py +3 -0
  2. codecortex/architecture/__init__.py +23 -0
  3. codecortex/architecture/drift.py +182 -0
  4. codecortex/architecture/inference.py +160 -0
  5. codecortex/backends/__init__.py +35 -0
  6. codecortex/backends/base.py +38 -0
  7. codecortex/backends/context.py +118 -0
  8. codecortex/backends/contracts.py +65 -0
  9. codecortex/backends/factory.py +60 -0
  10. codecortex/backends/graph.py +107 -0
  11. codecortex/backends/manager.py +303 -0
  12. codecortex/backends/mcp_client.py +196 -0
  13. codecortex/backends/pool.py +128 -0
  14. codecortex/backends/spec.py +83 -0
  15. codecortex/backends/symbols.py +189 -0
  16. codecortex/benchmark.py +211 -0
  17. codecortex/cli.py +409 -0
  18. codecortex/config.py +27 -0
  19. codecortex/context/__init__.py +13 -0
  20. codecortex/context/budget.py +62 -0
  21. codecortex/context/integrated.py +60 -0
  22. codecortex/context/pipeline.py +223 -0
  23. codecortex/core/__init__.py +1 -0
  24. codecortex/core/contracts.py +45 -0
  25. codecortex/core/errors.py +17 -0
  26. codecortex/core/models.py +69 -0
  27. codecortex/dashboard.py +259 -0
  28. codecortex/editing.py +41 -0
  29. codecortex/engines/__init__.py +5 -0
  30. codecortex/engines/builtin/__init__.py +5 -0
  31. codecortex/engines/builtin/factory.py +26 -0
  32. codecortex/engines/builtin/memory.py +35 -0
  33. codecortex/engines/builtin/repository.py +73 -0
  34. codecortex/engines/builtin/symbols.py +89 -0
  35. codecortex/engines/builtin/validation.py +54 -0
  36. codecortex/engines/registry.py +26 -0
  37. codecortex/entrypoint.py +266 -0
  38. codecortex/evaluation/__init__.py +55 -0
  39. codecortex/evaluation/external.py +265 -0
  40. codecortex/evaluation/production.py +670 -0
  41. codecortex/evaluation/regression.py +188 -0
  42. codecortex/gateway.py +38 -0
  43. codecortex/git_intelligence.py +252 -0
  44. codecortex/indexing/__init__.py +6 -0
  45. codecortex/indexing/graph.py +78 -0
  46. codecortex/indexing/impact.py +127 -0
  47. codecortex/indexing/incremental.py +163 -0
  48. codecortex/indexing/incremental_graph.py +189 -0
  49. codecortex/indexing/indexer.py +172 -0
  50. codecortex/indexing/relationships.py +179 -0
  51. codecortex/indexing/resolution.py +88 -0
  52. codecortex/integrations/__init__.py +5 -0
  53. codecortex/integrations/agents.py +235 -0
  54. codecortex/interfaces/__init__.py +1 -0
  55. codecortex/interfaces/mcp_bridge.py +66 -0
  56. codecortex/languages/__init__.py +5 -0
  57. codecortex/languages/native.py +166 -0
  58. codecortex/languages/registry.py +232 -0
  59. codecortex/mcp/__init__.py +5 -0
  60. codecortex/mcp/extended.py +114 -0
  61. codecortex/mcp/server.py +473 -0
  62. codecortex/memory/__init__.py +11 -0
  63. codecortex/memory/json_store.py +52 -0
  64. codecortex/memory/knowledge.py +193 -0
  65. codecortex/memory/team_store.py +193 -0
  66. codecortex/orchestrator.py +153 -0
  67. codecortex/pr_intelligence.py +214 -0
  68. codecortex/retrieval/__init__.py +16 -0
  69. codecortex/retrieval/hybrid.py +67 -0
  70. codecortex/retrieval/index.py +135 -0
  71. codecortex/retrieval/providers.py +67 -0
  72. codecortex/retrieval/repository.py +94 -0
  73. codecortex/router/__init__.py +5 -0
  74. codecortex/router/router.py +79 -0
  75. codecortex/runtime.py +69 -0
  76. codecortex/setup.py +100 -0
  77. codecortex/symbols/__init__.py +5 -0
  78. codecortex/symbols/providers.py +192 -0
  79. codecortex/telemetry/__init__.py +5 -0
  80. codecortex/telemetry/collector.py +43 -0
  81. codecortex/tracing/__init__.py +9 -0
  82. codecortex/tracing/task_trace.py +235 -0
  83. codecortex/workspace/__init__.py +9 -0
  84. codecortex/workspace/federation.py +173 -0
  85. codecortex_context_engine-0.1.0a1.dist-info/METADATA +381 -0
  86. codecortex_context_engine-0.1.0a1.dist-info/RECORD +90 -0
  87. codecortex_context_engine-0.1.0a1.dist-info/WHEEL +4 -0
  88. codecortex_context_engine-0.1.0a1.dist-info/entry_points.txt +3 -0
  89. codecortex_context_engine-0.1.0a1.dist-info/licenses/LICENSE +201 -0
  90. codecortex_context_engine-0.1.0a1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,193 @@
1
+ """Automatic project knowledge extraction and persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections import Counter
7
+ from dataclasses import asdict, dataclass
8
+ from pathlib import Path
9
+
10
+ from codecortex.git_intelligence import GitIntelligence
11
+ from codecortex.memory.json_store import JsonMemoryStore
12
+
13
+ _LANGUAGE_BY_SUFFIX = {
14
+ ".py": "Python",
15
+ ".js": "JavaScript",
16
+ ".jsx": "JavaScript",
17
+ ".ts": "TypeScript",
18
+ ".tsx": "TypeScript",
19
+ ".go": "Go",
20
+ ".rs": "Rust",
21
+ ".java": "Java",
22
+ ".c": "C",
23
+ ".h": "C/C++",
24
+ ".cc": "C++",
25
+ ".cpp": "C++",
26
+ ".cxx": "C++",
27
+ ".hpp": "C++",
28
+ ".cs": "C#",
29
+ ".php": "PHP",
30
+ ".rb": "Ruby",
31
+ }
32
+
33
+ _PACKAGE_FILES = {
34
+ "pyproject.toml": "Python/pip",
35
+ "requirements.txt": "Python/pip",
36
+ "package.json": "Node.js",
37
+ "pnpm-lock.yaml": "pnpm",
38
+ "yarn.lock": "Yarn",
39
+ "go.mod": "Go modules",
40
+ "Cargo.toml": "Cargo",
41
+ "pom.xml": "Maven",
42
+ "build.gradle": "Gradle",
43
+ "composer.json": "Composer",
44
+ "Gemfile": "Bundler",
45
+ }
46
+
47
+ _TEST_MARKERS = {
48
+ "pytest": ("pytest.ini", "conftest.py", "pytest"),
49
+ "vitest": ("vitest.config", "vitest"),
50
+ "jest": ("jest.config", "jest"),
51
+ "go test": ("_test.go",),
52
+ "cargo test": ("#[test]",),
53
+ "JUnit": ("junit", "@test"),
54
+ "RSpec": ("spec_helper", "rspec"),
55
+ }
56
+
57
+ _ARCHITECTURE_DIRS = {
58
+ "controllers": "controller layer",
59
+ "controller": "controller layer",
60
+ "services": "service layer",
61
+ "service": "service layer",
62
+ "repositories": "repository layer",
63
+ "repository": "repository layer",
64
+ "models": "model layer",
65
+ "domain": "domain layer",
66
+ "api": "API layer",
67
+ "routes": "routing layer",
68
+ "adapters": "adapter layer",
69
+ "ports": "ports layer",
70
+ "core": "core layer",
71
+ "infra": "infrastructure layer",
72
+ "infrastructure": "infrastructure layer",
73
+ }
74
+
75
+ _ENTRY_NAMES = {
76
+ "main.py",
77
+ "app.py",
78
+ "manage.py",
79
+ "index.js",
80
+ "index.ts",
81
+ "server.js",
82
+ "server.ts",
83
+ "main.go",
84
+ "main.rs",
85
+ "Program.cs",
86
+ "Main.java",
87
+ }
88
+
89
+ _EXCLUDED = {".git", ".codecortex", ".venv", "venv", "node_modules", "dist", "build"}
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class ProjectKnowledge:
94
+ languages: tuple[tuple[str, int], ...]
95
+ package_systems: tuple[str, ...]
96
+ entry_points: tuple[str, ...]
97
+ test_frameworks: tuple[str, ...]
98
+ architecture: tuple[str, ...]
99
+ hot_files: tuple[str, ...]
100
+
101
+ def facts(self) -> dict[str, str]:
102
+ return {
103
+ "languages": ", ".join(f"{name} ({count})" for name, count in self.languages),
104
+ "package_systems": ", ".join(self.package_systems) or "unknown",
105
+ "entry_points": ", ".join(self.entry_points) or "not detected",
106
+ "test_frameworks": ", ".join(self.test_frameworks) or "not detected",
107
+ "architecture": ", ".join(self.architecture) or "no strong convention detected",
108
+ "hot_files": ", ".join(self.hot_files) or "not available",
109
+ }
110
+
111
+
112
+ class ProjectKnowledgeExtractor:
113
+ def __init__(self, root: Path) -> None:
114
+ self.root = root.resolve()
115
+
116
+ def _files(self) -> list[Path]:
117
+ result: list[Path] = []
118
+ for path in self.root.rglob("*"):
119
+ if not path.is_file():
120
+ continue
121
+ relative = path.relative_to(self.root)
122
+ if any(part in _EXCLUDED for part in relative.parts):
123
+ continue
124
+ result.append(path)
125
+ return result
126
+
127
+ def extract(self) -> ProjectKnowledge:
128
+ files = self._files()
129
+ languages: Counter[str] = Counter()
130
+ package_systems: set[str] = set()
131
+ entry_points: list[str] = []
132
+ architecture: set[str] = set()
133
+ searchable: list[str] = []
134
+
135
+ for path in files:
136
+ relative = path.relative_to(self.root)
137
+ language = _LANGUAGE_BY_SUFFIX.get(path.suffix.lower())
138
+ if language:
139
+ languages[language] += 1
140
+ package = _PACKAGE_FILES.get(path.name)
141
+ if package:
142
+ package_systems.add(package)
143
+ if path.name in _ENTRY_NAMES:
144
+ entry_points.append(relative.as_posix())
145
+ for part in relative.parts[:-1]:
146
+ marker = _ARCHITECTURE_DIRS.get(part.lower())
147
+ if marker:
148
+ architecture.add(marker)
149
+ if path.stat().st_size <= 512_000:
150
+ try:
151
+ searchable.append(path.read_text(encoding="utf-8").lower())
152
+ except (OSError, UnicodeDecodeError):
153
+ pass
154
+
155
+ joined = "\n".join(searchable)
156
+ test_frameworks = {
157
+ name
158
+ for name, markers in _TEST_MARKERS.items()
159
+ if any(marker.lower() in joined or any(marker.lower() in p.name.lower() for p in files)
160
+ for marker in markers)
161
+ }
162
+ git = GitIntelligence(self.root).analyze(limit=300)
163
+ return ProjectKnowledge(
164
+ languages=tuple(languages.most_common()),
165
+ package_systems=tuple(sorted(package_systems)),
166
+ entry_points=tuple(sorted(set(entry_points))),
167
+ test_frameworks=tuple(sorted(test_frameworks)),
168
+ architecture=tuple(sorted(architecture)),
169
+ hot_files=tuple(item.path for item in git.hot_files[:10]),
170
+ )
171
+
172
+ def save(self, knowledge: ProjectKnowledge | None = None) -> Path:
173
+ knowledge = knowledge or self.extract()
174
+ path = self.root / ".codecortex" / "knowledge" / "project.json"
175
+ path.parent.mkdir(parents=True, exist_ok=True)
176
+ temp = path.with_suffix(".tmp")
177
+ temp.write_text(
178
+ json.dumps(asdict(knowledge), ensure_ascii=False, indent=2),
179
+ encoding="utf-8",
180
+ )
181
+ temp.replace(path)
182
+ return path
183
+
184
+ async def remember(
185
+ self,
186
+ store: JsonMemoryStore,
187
+ namespace: str = "project_knowledge",
188
+ ) -> ProjectKnowledge:
189
+ knowledge = self.extract()
190
+ self.save(knowledge)
191
+ for key, value in knowledge.facts().items():
192
+ await store.put(namespace, key, value)
193
+ return knowledge
@@ -0,0 +1,193 @@
1
+ """Concurrent shared team memory backed by SQLite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+
11
+ from codecortex.core.contracts import MemoryStore
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class TeamMemoryEntry:
16
+ namespace: str
17
+ key: str
18
+ value: str
19
+ revision: int
20
+ actor: str
21
+ source: str
22
+ tags: tuple[str, ...]
23
+ updated_at: str
24
+
25
+
26
+ class RevisionConflict(RuntimeError):
27
+ pass
28
+
29
+
30
+ class TeamMemoryStore(MemoryStore):
31
+ """Shared memory with revisions, audit history, and optimistic concurrency."""
32
+
33
+ def __init__(self, path: Path) -> None:
34
+ self.path = path
35
+ self.path.parent.mkdir(parents=True, exist_ok=True)
36
+ self._initialize()
37
+
38
+ def _connect(self) -> sqlite3.Connection:
39
+ connection = sqlite3.connect(self.path, timeout=5.0)
40
+ connection.row_factory = sqlite3.Row
41
+ connection.execute("PRAGMA journal_mode=WAL")
42
+ connection.execute("PRAGMA synchronous=NORMAL")
43
+ connection.execute("PRAGMA busy_timeout=5000")
44
+ return connection
45
+
46
+ def _initialize(self) -> None:
47
+ with self._connect() as connection:
48
+ connection.executescript(
49
+ """
50
+ CREATE TABLE IF NOT EXISTS memories (
51
+ namespace TEXT NOT NULL,
52
+ key TEXT NOT NULL,
53
+ value TEXT NOT NULL,
54
+ revision INTEGER NOT NULL,
55
+ actor TEXT NOT NULL,
56
+ source TEXT NOT NULL,
57
+ tags TEXT NOT NULL,
58
+ updated_at TEXT NOT NULL,
59
+ PRIMARY KEY(namespace, key)
60
+ );
61
+ CREATE TABLE IF NOT EXISTS memory_history (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ namespace TEXT NOT NULL,
64
+ key TEXT NOT NULL,
65
+ value TEXT NOT NULL,
66
+ revision INTEGER NOT NULL,
67
+ actor TEXT NOT NULL,
68
+ source TEXT NOT NULL,
69
+ tags TEXT NOT NULL,
70
+ updated_at TEXT NOT NULL
71
+ );
72
+ CREATE INDEX IF NOT EXISTS idx_memory_namespace
73
+ ON memories(namespace);
74
+ CREATE INDEX IF NOT EXISTS idx_history_key
75
+ ON memory_history(namespace, key, revision);
76
+ """
77
+ )
78
+
79
+ async def put(self, namespace: str, key: str, value: str) -> None:
80
+ self.put_entry(namespace, key, value)
81
+
82
+ def put_entry(
83
+ self,
84
+ namespace: str,
85
+ key: str,
86
+ value: str,
87
+ *,
88
+ actor: str = "system",
89
+ source: str = "manual",
90
+ tags: tuple[str, ...] = (),
91
+ expected_revision: int | None = None,
92
+ ) -> TeamMemoryEntry:
93
+ if not namespace.strip() or not key.strip():
94
+ raise ValueError("namespace and key are required")
95
+ now = datetime.now(UTC).isoformat()
96
+ encoded_tags = json.dumps(sorted(set(tags)), ensure_ascii=False)
97
+ with self._connect() as connection:
98
+ connection.execute("BEGIN IMMEDIATE")
99
+ row = connection.execute(
100
+ "SELECT revision FROM memories WHERE namespace = ? AND key = ?",
101
+ (namespace, key),
102
+ ).fetchone()
103
+ current_revision = int(row["revision"]) if row else 0
104
+ if expected_revision is not None and expected_revision != current_revision:
105
+ raise RevisionConflict(
106
+ f"expected revision {expected_revision}, found {current_revision}"
107
+ )
108
+ revision = current_revision + 1
109
+ connection.execute(
110
+ """
111
+ INSERT INTO memories(namespace, key, value, revision, actor, source, tags, updated_at)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
113
+ ON CONFLICT(namespace, key) DO UPDATE SET
114
+ value = excluded.value,
115
+ revision = excluded.revision,
116
+ actor = excluded.actor,
117
+ source = excluded.source,
118
+ tags = excluded.tags,
119
+ updated_at = excluded.updated_at
120
+ """,
121
+ (namespace, key, value, revision, actor, source, encoded_tags, now),
122
+ )
123
+ connection.execute(
124
+ """
125
+ INSERT INTO memory_history(namespace, key, value, revision, actor, source, tags, updated_at)
126
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
127
+ """,
128
+ (namespace, key, value, revision, actor, source, encoded_tags, now),
129
+ )
130
+ return TeamMemoryEntry(namespace, key, value, revision, actor, source, tags, now)
131
+
132
+ async def get(self, namespace: str, key: str) -> str | None:
133
+ entry = self.get_entry(namespace, key)
134
+ return entry.value if entry else None
135
+
136
+ def get_entry(self, namespace: str, key: str) -> TeamMemoryEntry | None:
137
+ with self._connect() as connection:
138
+ row = connection.execute(
139
+ "SELECT * FROM memories WHERE namespace = ? AND key = ?",
140
+ (namespace, key),
141
+ ).fetchone()
142
+ return self._row_to_entry(row) if row else None
143
+
144
+ async def search(self, namespace: str, query: str, limit: int = 10) -> list[str]:
145
+ return [entry.value for entry in self.search_entries(namespace, query, limit)]
146
+
147
+ def search_entries(self, namespace: str, query: str, limit: int = 10) -> list[TeamMemoryEntry]:
148
+ terms = [term.lower() for term in query.split() if term.strip()]
149
+ with self._connect() as connection:
150
+ rows = connection.execute(
151
+ "SELECT * FROM memories WHERE namespace = ? ORDER BY updated_at DESC",
152
+ (namespace,),
153
+ ).fetchall()
154
+ scored: list[tuple[int, TeamMemoryEntry]] = []
155
+ for row in rows:
156
+ entry = self._row_to_entry(row)
157
+ haystack = f"{entry.key} {entry.value} {' '.join(entry.tags)}".lower()
158
+ score = sum(3 if term in entry.key.lower() else 1 for term in terms if term in haystack)
159
+ if score or not terms:
160
+ scored.append((score, entry))
161
+ scored.sort(key=lambda item: (-item[0], item[1].updated_at), reverse=False)
162
+ return [entry for _, entry in scored[: max(1, limit)]]
163
+
164
+ def history(self, namespace: str, key: str, limit: int = 50) -> list[TeamMemoryEntry]:
165
+ with self._connect() as connection:
166
+ rows = connection.execute(
167
+ """
168
+ SELECT namespace, key, value, revision, actor, source, tags, updated_at
169
+ FROM memory_history
170
+ WHERE namespace = ? AND key = ?
171
+ ORDER BY revision DESC
172
+ LIMIT ?
173
+ """,
174
+ (namespace, key, max(1, limit)),
175
+ ).fetchall()
176
+ return [self._row_to_entry(row) for row in rows]
177
+
178
+ @staticmethod
179
+ def _row_to_entry(row: sqlite3.Row) -> TeamMemoryEntry:
180
+ try:
181
+ tags = tuple(str(item) for item in json.loads(row["tags"]))
182
+ except (json.JSONDecodeError, TypeError):
183
+ tags = ()
184
+ return TeamMemoryEntry(
185
+ namespace=str(row["namespace"]),
186
+ key=str(row["key"]),
187
+ value=str(row["value"]),
188
+ revision=int(row["revision"]),
189
+ actor=str(row["actor"]),
190
+ source=str(row["source"]),
191
+ tags=tags,
192
+ updated_at=str(row["updated_at"]),
193
+ )
@@ -0,0 +1,153 @@
1
+ """Core request orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from time import perf_counter
7
+
8
+ from codecortex.context import BudgetContextProcessor
9
+ from codecortex.core.models import AgentRequest, Capability, EngineResult, ExecutionResult
10
+ from codecortex.engines import EngineRegistry
11
+ from codecortex.router import AdaptiveRouter
12
+ from codecortex.telemetry import TelemetryCollector
13
+ from codecortex.tracing import TaskTraceRecorder
14
+
15
+
16
+ class Orchestrator:
17
+ """Route requests, execute independent engines concurrently, and fit context."""
18
+
19
+ def __init__(
20
+ self,
21
+ registry: EngineRegistry,
22
+ router: AdaptiveRouter,
23
+ context_processor: BudgetContextProcessor | None = None,
24
+ telemetry: TelemetryCollector | None = None,
25
+ tracer: TaskTraceRecorder | None = None,
26
+ ) -> None:
27
+ self.registry = registry
28
+ self.router = router
29
+ self.context_processor = context_processor or BudgetContextProcessor()
30
+ self.telemetry = telemetry or TelemetryCollector()
31
+ self.tracer = tracer
32
+
33
+ async def execute(self, request: AgentRequest) -> ExecutionResult:
34
+ if self.tracer is None:
35
+ return await self._execute(request, None, None)
36
+ trace_id = str(request.metadata.get("trace_id") or self.tracer.new_trace_id())
37
+ attributes: dict[str, object] = {"query_chars": len(request.query)}
38
+ async with self.tracer.async_span(
39
+ "request.execute",
40
+ trace_id=trace_id,
41
+ attributes=attributes,
42
+ ) as root_span:
43
+ result = await self._execute(request, trace_id, root_span)
44
+ attributes["context_tokens"] = result.context_tokens
45
+ attributes["capabilities"] = [item.value for item in result.plan.selected]
46
+ return result.model_copy(
47
+ update={"metadata": {**result.metadata, "trace_id": trace_id}}
48
+ )
49
+
50
+ async def _run_engine(
51
+ self,
52
+ capability: Capability,
53
+ request: AgentRequest,
54
+ trace_id: str | None,
55
+ parent_span: str | None,
56
+ ) -> EngineResult | None:
57
+ engine = self.registry.get(capability)
58
+ if engine is None or not await engine.health():
59
+ self.telemetry.emit("engine.skipped", capability=capability.value)
60
+ return None
61
+ started = perf_counter()
62
+ try:
63
+ if self.tracer and trace_id:
64
+ attrs: dict[str, object] = {"capability": capability.value}
65
+ async with self.tracer.async_span(
66
+ "engine.execute",
67
+ trace_id=trace_id,
68
+ parent_id=parent_span,
69
+ attributes=attrs,
70
+ ):
71
+ result = await engine.execute(request)
72
+ attrs["chunks"] = len(result.chunks)
73
+ attrs["context_tokens"] = sum(chunk.tokens for chunk in result.chunks)
74
+ else:
75
+ result = await engine.execute(request)
76
+ except Exception as exc:
77
+ self.telemetry.emit(
78
+ "engine.failed",
79
+ capability=capability.value,
80
+ error_type=type(exc).__name__,
81
+ )
82
+ if request.metadata.get("strict_engines"):
83
+ raise
84
+ return None
85
+ self.telemetry.emit(
86
+ "engine.executed",
87
+ capability=capability.value,
88
+ duration_ms=(perf_counter() - started) * 1000,
89
+ )
90
+ return result
91
+
92
+ async def _execute(
93
+ self,
94
+ request: AgentRequest,
95
+ trace_id: str | None,
96
+ parent_span: str | None,
97
+ ) -> ExecutionResult:
98
+ plan = self.router.route(request)
99
+ self.telemetry.emit(
100
+ "route.created",
101
+ kind=plan.request_kind.value,
102
+ capabilities=[capability.value for capability in plan.selected],
103
+ )
104
+ if self.tracer and trace_id:
105
+ self.tracer.record(
106
+ "route.created",
107
+ trace_id=trace_id,
108
+ parent_id=parent_span,
109
+ attributes={
110
+ "kind": plan.request_kind.value,
111
+ "capabilities": [item.value for item in plan.selected],
112
+ },
113
+ )
114
+
115
+ capabilities = [item for item in plan.selected if item != Capability.CONTEXT]
116
+ outcomes = await asyncio.gather(
117
+ *(
118
+ self._run_engine(capability, request, trace_id, parent_span)
119
+ for capability in capabilities
120
+ )
121
+ )
122
+ results = [result for result in outcomes if result is not None]
123
+ all_chunks = [chunk for result in results for chunk in result.chunks]
124
+
125
+ original_tokens = sum(chunk.tokens for chunk in all_chunks)
126
+ fitted = await self.context_processor.fit(all_chunks, plan.context_budget)
127
+ fitted_sources = {(chunk.source, chunk.content) for chunk in fitted}
128
+ normalized_results: list[EngineResult] = []
129
+ for result in results:
130
+ kept = [
131
+ chunk for chunk in result.chunks if (chunk.source, chunk.content) in fitted_sources
132
+ ]
133
+ normalized_results.append(result.model_copy(update={"chunks": kept}))
134
+
135
+ context_tokens = sum(chunk.tokens for chunk in fitted)
136
+ self.telemetry.emit(
137
+ "context.fitted",
138
+ budget=plan.context_budget,
139
+ original=original_tokens,
140
+ used=context_tokens,
141
+ saved=max(0, original_tokens - context_tokens),
142
+ chunks=len(fitted),
143
+ )
144
+ return ExecutionResult(
145
+ request=request,
146
+ plan=plan,
147
+ results=normalized_results,
148
+ context_tokens=context_tokens,
149
+ metadata={
150
+ "original_context_tokens": original_tokens,
151
+ "context_tokens_saved": max(0, original_tokens - context_tokens),
152
+ },
153
+ )