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
codecortex/cli.py ADDED
@@ -0,0 +1,409 @@
1
+ """CodeCortex command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import shlex
7
+ from dataclasses import asdict
8
+ from pathlib import Path
9
+ from typing import Annotated
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from codecortex.architecture import (
16
+ ArchitectureDriftDetector,
17
+ ArchitectureFingerprint,
18
+ ArchitectureInferenceEngine,
19
+ )
20
+ from codecortex.benchmark import BenchmarkSuite, CodeCortexGraphStrategy, FullTextBaseline
21
+ from codecortex.dashboard import run_dashboard
22
+ from codecortex.evaluation import (
23
+ BenchmarkHistory,
24
+ ExternalEvaluationSuite,
25
+ RegressionGate,
26
+ SubprocessEvaluationTarget,
27
+ )
28
+ from codecortex.git_intelligence import GitIntelligence
29
+ from codecortex.indexing.impact import ImpactAnalyzer
30
+ from codecortex.indexing.incremental_graph import IncrementalGraphIndex
31
+ from codecortex.mcp.server import run_stdio
32
+ from codecortex.memory import TeamMemoryStore
33
+ from codecortex.memory.knowledge import ProjectKnowledgeExtractor
34
+ from codecortex.pr_intelligence import PRIntelligence
35
+ from codecortex.retrieval import RepositorySemanticIndex
36
+ from codecortex.runtime import build_runtime
37
+ from codecortex.setup import ProjectSetup
38
+ from codecortex.tracing import TaskTraceRecorder
39
+ from codecortex.workspace import MultiRepositoryWorkspace
40
+
41
+ app = typer.Typer(no_args_is_help=True, add_completion=False)
42
+ console = Console()
43
+
44
+
45
+ def _root(path: Path) -> Path:
46
+ return path.expanduser().resolve()
47
+
48
+
49
+ def _graph(root: Path):
50
+ return IncrementalGraphIndex(root).refresh()[0]
51
+
52
+
53
+ @app.command()
54
+ def init(path: Annotated[Path, typer.Argument(help="Project directory")] = Path(".")) -> None:
55
+ result = ProjectSetup(_root(path)).run()
56
+ console.print("[bold green]CodeCortex ready.[/bold green]")
57
+ console.print_json(
58
+ data={
59
+ "tracked_files": result.index.tracked,
60
+ "symbols": result.symbols,
61
+ "graph_nodes": result.graph_nodes,
62
+ "graph_edges": result.graph_edges,
63
+ "languages": list(result.languages),
64
+ "detected_agents": list(result.detected_agents),
65
+ }
66
+ )
67
+
68
+
69
+ @app.command("index")
70
+ def index_command(path: Annotated[Path, typer.Option("--path", "-p")] = Path(".")) -> None:
71
+ graph, stats = IncrementalGraphIndex(_root(path)).refresh()
72
+ console.print_json(
73
+ data={
74
+ "tracked": stats.index.tracked,
75
+ "added": len(stats.index.added),
76
+ "changed": len(stats.index.changed),
77
+ "removed": len(stats.index.removed),
78
+ "files_reparsed": stats.files_reparsed,
79
+ "full_rebuild": stats.full_rebuild,
80
+ "graph_nodes": len(graph.nodes),
81
+ "graph_edges": len(graph.edges),
82
+ }
83
+ )
84
+
85
+
86
+ @app.command()
87
+ def semantic(
88
+ query: Annotated[str, typer.Argument(help="Semantic repository query")],
89
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
90
+ limit: Annotated[int, typer.Option("--limit", "-n")] = 20,
91
+ ) -> None:
92
+ semantic_index = RepositorySemanticIndex(_root(path))
93
+ semantic_index.refresh()
94
+ console.print_json(
95
+ data={
96
+ "hits": [
97
+ {
98
+ "id": hit.document.id,
99
+ "score": hit.score,
100
+ "vector_score": hit.vector_score,
101
+ "lexical_score": hit.lexical_score,
102
+ "structural_score": hit.structural_score,
103
+ "metadata": hit.document.metadata,
104
+ }
105
+ for hit in semantic_index.search(query, limit)
106
+ ]
107
+ }
108
+ )
109
+
110
+
111
+ @app.command()
112
+ def architecture(path: Annotated[Path, typer.Option("--path", "-p")] = Path(".")) -> None:
113
+ report = ArchitectureInferenceEngine().analyze(_graph(_root(path)))
114
+ console.print_json(data=asdict(report))
115
+
116
+
117
+ @app.command("architecture-baseline")
118
+ def architecture_baseline(
119
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
120
+ ) -> None:
121
+ root = _root(path)
122
+ target = root / ".codecortex" / "architecture" / "baseline.json"
123
+ ArchitectureDriftDetector().fingerprint(_graph(root)).save(target)
124
+ console.print(f"Saved: {target}")
125
+
126
+
127
+ @app.command("architecture-drift")
128
+ def architecture_drift(
129
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
130
+ ) -> None:
131
+ root = _root(path)
132
+ target = root / ".codecortex" / "architecture" / "baseline.json"
133
+ baseline = ArchitectureFingerprint.load(target)
134
+ detector = ArchitectureDriftDetector()
135
+ current = detector.fingerprint(_graph(root))
136
+ if baseline is None:
137
+ current.save(target)
138
+ console.print("Baseline created; no prior baseline existed.")
139
+ return
140
+ report = detector.compare(baseline, current)
141
+ console.print_json(data=asdict(report))
142
+ if report.drifted and report.score >= 0.70:
143
+ raise typer.Exit(code=2)
144
+
145
+
146
+ @app.command("symbol-history")
147
+ def symbol_history(
148
+ target: Annotated[str, typer.Argument(help="Repository-relative path")],
149
+ start: Annotated[int, typer.Argument(help="Start line")],
150
+ end: Annotated[int, typer.Argument(help="End line")],
151
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
152
+ ) -> None:
153
+ report = GitIntelligence(_root(path)).symbol_history(target, start, end)
154
+ console.print_json(data=asdict(report))
155
+
156
+
157
+ @app.command("pr")
158
+ def pr_command(
159
+ base: Annotated[str, typer.Argument(help="Base Git ref")],
160
+ head: Annotated[str, typer.Option("--head")] = "HEAD",
161
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
162
+ ) -> None:
163
+ root = _root(path)
164
+ report = PRIntelligence(root, _graph(root)).analyze(base, head)
165
+ console.print_json(
166
+ data={
167
+ "base_ref": report.base_ref,
168
+ "head_ref": report.head_ref,
169
+ "risk_score": report.risk_score,
170
+ "risk_level": report.risk_level,
171
+ "affected_tests": list(report.affected_tests),
172
+ "files": [asdict(item) for item in report.files],
173
+ "symbols": [
174
+ {
175
+ "node": item.node.model_dump(mode="json"),
176
+ "impact_risk": item.impact_risk,
177
+ "affected_nodes": item.affected_nodes,
178
+ "affected_tests": item.affected_tests,
179
+ }
180
+ for item in report.symbols
181
+ ],
182
+ }
183
+ )
184
+
185
+
186
+ @app.command("team-remember")
187
+ def team_remember(
188
+ key: Annotated[str, typer.Argument()],
189
+ value: Annotated[str, typer.Argument()],
190
+ actor: Annotated[str, typer.Option("--actor")] = "user",
191
+ namespace: Annotated[str, typer.Option("--namespace")] = "project",
192
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
193
+ ) -> None:
194
+ store = TeamMemoryStore(_root(path) / ".codecortex" / "memory" / "team.sqlite3")
195
+ entry = store.put_entry(namespace, key, value, actor=actor)
196
+ console.print_json(data=asdict(entry))
197
+
198
+
199
+ @app.command("team-search")
200
+ def team_search(
201
+ query: Annotated[str, typer.Argument()],
202
+ namespace: Annotated[str, typer.Option("--namespace")] = "project",
203
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
204
+ ) -> None:
205
+ store = TeamMemoryStore(_root(path) / ".codecortex" / "memory" / "team.sqlite3")
206
+ console.print_json(
207
+ data=[asdict(item) for item in store.search_entries(namespace, query, 20)]
208
+ )
209
+
210
+
211
+ @app.command("workspace-add")
212
+ def workspace_add(
213
+ name: Annotated[str, typer.Argument()],
214
+ repository: Annotated[Path, typer.Argument()],
215
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
216
+ ) -> None:
217
+ workspace = MultiRepositoryWorkspace(_root(path) / ".codecortex" / "workspace.json")
218
+ workspace.add_repository(name, repository)
219
+ console.print(f"Added {name}: {_root(repository)}")
220
+
221
+
222
+ @app.command("workspace-search")
223
+ def workspace_search(
224
+ query: Annotated[str, typer.Argument()],
225
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
226
+ ) -> None:
227
+ workspace = MultiRepositoryWorkspace(_root(path) / ".codecortex" / "workspace.json")
228
+ console.print_json(
229
+ data=[
230
+ {
231
+ "repository": hit.repository,
232
+ "score": hit.score,
233
+ "node": hit.node.model_dump(mode="json"),
234
+ }
235
+ for hit in workspace.search(query)
236
+ ]
237
+ )
238
+
239
+
240
+ @app.command("trace-summary")
241
+ def trace_summary(
242
+ trace_id: Annotated[str, typer.Argument()],
243
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
244
+ ) -> None:
245
+ recorder = TaskTraceRecorder(_root(path) / ".codecortex" / "runtime" / "traces.jsonl")
246
+ console.print_json(data=asdict(recorder.summarize(trace_id)))
247
+
248
+
249
+ @app.command()
250
+ def impact(
251
+ query: Annotated[str, typer.Argument(help="Symbol or file")],
252
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
253
+ ) -> None:
254
+ console.print(ImpactAnalyzer(_graph(_root(path))).analyze(query).to_text())
255
+
256
+
257
+ @app.command()
258
+ def history(
259
+ target: Annotated[str, typer.Argument(help="Repository path")],
260
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
261
+ ) -> None:
262
+ console.print_json(data=GitIntelligence(_root(path)).file_history(target))
263
+
264
+
265
+ @app.command()
266
+ def knowledge(path: Annotated[Path, typer.Option("--path", "-p")] = Path(".")) -> None:
267
+ console.print_json(data=ProjectKnowledgeExtractor(_root(path)).extract().facts())
268
+
269
+
270
+ @app.command()
271
+ def mcp(path: Annotated[Path, typer.Option("--path", "-p")] = Path(".")) -> None:
272
+ run_stdio(_root(path))
273
+
274
+
275
+ @app.command()
276
+ def benchmark(
277
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
278
+ cases: Annotated[Path, typer.Option("--cases")] = Path("benchmarks/cases.json"),
279
+ output: Annotated[Path, typer.Option("--output", "-o")] = Path("benchmarks/results.json"),
280
+ ) -> None:
281
+ root = _root(path)
282
+ case_path = cases if cases.is_absolute() else root / cases
283
+ output_path = output if output.is_absolute() else root / output
284
+ suite = BenchmarkSuite.load(
285
+ case_path,
286
+ [FullTextBaseline(root), CodeCortexGraphStrategy(root)],
287
+ )
288
+ report = suite.run()
289
+ report.save(output_path)
290
+ history_store = BenchmarkHistory(root / ".codecortex" / "benchmarks" / "history.json")
291
+ history_store.append(report.summary())
292
+ console.print_json(data=report.summary())
293
+ console.print(f"Saved: {output_path}")
294
+
295
+
296
+ @app.command("benchmark-gate")
297
+ def benchmark_gate(
298
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
299
+ ) -> None:
300
+ snapshots = BenchmarkHistory(
301
+ _root(path) / ".codecortex" / "benchmarks" / "history.json"
302
+ ).load()
303
+ if len(snapshots) < 2:
304
+ console.print("At least two benchmark snapshots are required.")
305
+ return
306
+ report = RegressionGate().evaluate(snapshots[-1], snapshots[-2])
307
+ console.print_json(data=asdict(report))
308
+ if not report.passed:
309
+ raise typer.Exit(code=2)
310
+
311
+
312
+ @app.command("evaluate")
313
+ def evaluate_command(
314
+ suite: Annotated[Path, typer.Argument(help="Evaluation suite JSON")],
315
+ command: Annotated[str, typer.Argument(help="Quoted external target command")],
316
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
317
+ output: Annotated[Path, typer.Option("--output", "-o")] = Path(
318
+ "benchmarks/external/results.json"
319
+ ),
320
+ ) -> None:
321
+ root = _root(path)
322
+ suite_path = suite if suite.is_absolute() else root / suite
323
+ output_path = output if output.is_absolute() else root / output
324
+ target = SubprocessEvaluationTarget("external", tuple(shlex.split(command)), cwd=root)
325
+ report = asyncio.run(ExternalEvaluationSuite.load(suite_path).run(target))
326
+ report.save(output_path)
327
+ console.print_json(data=report.summary())
328
+
329
+
330
+ @app.command()
331
+ def doctor(path: Annotated[Path, typer.Option("--path", "-p")] = Path(".")) -> None:
332
+ runtime = build_runtime(_root(path))
333
+ health = asyncio.run(runtime.gateway.health())
334
+ table = Table(title="CodeCortex Health")
335
+ table.add_column("Capability")
336
+ table.add_column("Status")
337
+ for capability, status in health.items():
338
+ table.add_row(capability, "OK" if status else "Unavailable")
339
+ console.print(table)
340
+
341
+
342
+ @app.command()
343
+ def route(
344
+ query: Annotated[str, typer.Argument(help="Coding request")],
345
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
346
+ ) -> None:
347
+ runtime = build_runtime(_root(path))
348
+ plan = runtime.gateway.route(query, str(runtime.config.project_root))
349
+ console.print_json(data=plan.model_dump(mode="json"))
350
+
351
+
352
+ @app.command()
353
+ def run(
354
+ query: Annotated[str, typer.Argument(help="Coding request")],
355
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
356
+ ) -> None:
357
+ runtime = build_runtime(_root(path))
358
+ result = asyncio.run(runtime.gateway.query(query, str(runtime.config.project_root)))
359
+ console.print(f"[bold]Route:[/bold] {', '.join(item.value for item in result.plan.selected)}")
360
+ console.print(f"[bold]Context:[/bold] {result.context_tokens}/{result.plan.context_budget} tokens")
361
+ if trace_id := result.metadata.get("trace_id"):
362
+ console.print(f"[bold]Trace:[/bold] {trace_id}")
363
+ for engine_result in result.results:
364
+ console.rule(engine_result.capability.value)
365
+ console.print(engine_result.content)
366
+
367
+
368
+ @app.command()
369
+ def remember(
370
+ key: Annotated[str, typer.Argument()],
371
+ value: Annotated[str, typer.Argument()],
372
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
373
+ ) -> None:
374
+ runtime = build_runtime(_root(path))
375
+ asyncio.run(runtime.gateway.remember(key, value))
376
+ console.print("[green]Saved.[/green]")
377
+
378
+
379
+ @app.command()
380
+ def stats(path: Annotated[Path, typer.Option("--path", "-p")] = Path(".")) -> None:
381
+ root = _root(path)
382
+ runtime = build_runtime(root)
383
+ graph, graph_stats = IncrementalGraphIndex(root).refresh()
384
+ git = GitIntelligence(root).analyze(300)
385
+ console.print_json(
386
+ data={
387
+ "files": graph_stats.index.tracked,
388
+ "graph_nodes": len(graph.nodes),
389
+ "graph_edges": len(graph.edges),
390
+ "graph_counts": graph.counts(),
391
+ "git_commits": git.commits,
392
+ "health": asyncio.run(runtime.gateway.health()),
393
+ }
394
+ )
395
+
396
+
397
+ @app.command()
398
+ def dashboard(
399
+ path: Annotated[Path, typer.Option("--path", "-p")] = Path("."),
400
+ host: Annotated[str, typer.Option("--host")] = "127.0.0.1",
401
+ port: Annotated[int, typer.Option("--port")] = 7331,
402
+ ) -> None:
403
+ runtime = build_runtime(_root(path))
404
+ console.print(f"Dashboard: http://{host}:{port}")
405
+ run_dashboard(runtime, host=host, port=port)
406
+
407
+
408
+ if __name__ == "__main__":
409
+ app()
codecortex/config.py ADDED
@@ -0,0 +1,27 @@
1
+ """Configuration for CodeCortex."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class CortexConfig(BaseModel):
11
+ project_root: Path = Field(default_factory=lambda: Path.cwd())
12
+ state_dir_name: str = ".codecortex"
13
+ default_context_budget: int = Field(default=32_000, gt=0)
14
+ hard_context_limit: int = Field(default=128_000, gt=0)
15
+ telemetry_enabled: bool = True
16
+
17
+ @property
18
+ def state_dir(self) -> Path:
19
+ return self.project_root / self.state_dir_name
20
+
21
+ @property
22
+ def memory_dir(self) -> Path:
23
+ return self.state_dir / "memory"
24
+
25
+ def ensure_directories(self) -> None:
26
+ self.state_dir.mkdir(parents=True, exist_ok=True)
27
+ self.memory_dir.mkdir(parents=True, exist_ok=True)
@@ -0,0 +1,13 @@
1
+ """Context processing and token budgeting."""
2
+
3
+ from codecortex.context.budget import BudgetContextProcessor
4
+ from codecortex.context.integrated import IntegratedContextProcessor
5
+ from codecortex.context.pipeline import ContextMetrics, ContextPipeline, ContextResult
6
+
7
+ __all__ = [
8
+ "BudgetContextProcessor",
9
+ "ContextMetrics",
10
+ "ContextPipeline",
11
+ "ContextResult",
12
+ "IntegratedContextProcessor",
13
+ ]
@@ -0,0 +1,62 @@
1
+ """Token-budget aware context processing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from codecortex.core.contracts import ContextProcessor
6
+ from codecortex.core.models import ContextChunk
7
+
8
+
9
+ class BudgetContextProcessor(ContextProcessor):
10
+ """Deduplicate, rank, and fit useful context inside a token budget."""
11
+
12
+ async def fit(self, chunks: list[ContextChunk], budget: int) -> list[ContextChunk]:
13
+ unique = self._deduplicate(chunks)
14
+ ranked = sorted(
15
+ unique,
16
+ key=lambda chunk: (chunk.relevance, -chunk.tokens),
17
+ reverse=True,
18
+ )
19
+
20
+ selected: list[ContextChunk] = []
21
+ remaining = budget
22
+ for chunk in ranked:
23
+ if remaining <= 0:
24
+ break
25
+ if chunk.tokens <= remaining:
26
+ selected.append(chunk)
27
+ remaining -= chunk.tokens
28
+ continue
29
+ if remaining < 32:
30
+ continue
31
+ selected.append(self._truncate(chunk, remaining))
32
+ remaining = 0
33
+ return selected
34
+
35
+ @staticmethod
36
+ def _deduplicate(chunks: list[ContextChunk]) -> list[ContextChunk]:
37
+ by_content: dict[str, ContextChunk] = {}
38
+ for chunk in chunks:
39
+ key = " ".join(chunk.content.split())
40
+ current = by_content.get(key)
41
+ if current is None or chunk.relevance > current.relevance:
42
+ by_content[key] = chunk
43
+ return list(by_content.values())
44
+
45
+ @staticmethod
46
+ def _truncate(chunk: ContextChunk, tokens: int) -> ContextChunk:
47
+ char_limit = max(1, tokens * 4)
48
+ content = chunk.content[:char_limit].rstrip()
49
+ metadata = dict(chunk.metadata)
50
+ metadata.update(
51
+ {
52
+ "truncated": True,
53
+ "original_tokens": chunk.tokens,
54
+ }
55
+ )
56
+ return chunk.model_copy(
57
+ update={
58
+ "content": content,
59
+ "tokens": tokens,
60
+ "metadata": metadata,
61
+ }
62
+ )
@@ -0,0 +1,60 @@
1
+ """Context processor that opportunistically uses the mature compression backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ from codecortex.backends.context import ContextBackendAdapter
8
+ from codecortex.backends.mcp_client import MCPStdioClient
9
+ from codecortex.context.budget import BudgetContextProcessor
10
+ from codecortex.core.contracts import ContextProcessor
11
+ from codecortex.core.models import ContextChunk
12
+
13
+
14
+ class IntegratedContextProcessor(ContextProcessor):
15
+ def __init__(
16
+ self,
17
+ backend: ContextBackendAdapter | None = None,
18
+ *,
19
+ compression_threshold: int = 512,
20
+ ) -> None:
21
+ self.backend = backend
22
+ self.compression_threshold = compression_threshold
23
+ self.budget = BudgetContextProcessor()
24
+
25
+ async def fit(self, chunks: list[ContextChunk], budget: int) -> list[ContextChunk]:
26
+ backend = self.backend
27
+ if backend is None or not await backend.health():
28
+ return await self.budget.fit(chunks, budget)
29
+ candidates = [chunk for chunk in chunks if chunk.tokens >= self.compression_threshold]
30
+ if not candidates:
31
+ return await self.budget.fit(chunks, budget)
32
+ try:
33
+ payloads = await asyncio.to_thread(
34
+ backend.compress_batch,
35
+ [chunk.content for chunk in candidates],
36
+ )
37
+ except Exception:
38
+ return await self.budget.fit(chunks, budget)
39
+ replacements: dict[int, ContextChunk] = {}
40
+ for chunk, payload in zip(candidates, payloads, strict=True):
41
+ content = MCPStdioClient.content_text(payload).strip()
42
+ if not content:
43
+ continue
44
+ compressed_tokens = max(1, len(content) // 4)
45
+ if compressed_tokens >= chunk.tokens:
46
+ continue
47
+ metadata = dict(chunk.metadata)
48
+ metadata.update(
49
+ {
50
+ "compressed": True,
51
+ "original_tokens": chunk.tokens,
52
+ "compression_backend": backend.spec.key,
53
+ "compression_revision": backend.spec.revision,
54
+ }
55
+ )
56
+ replacements[id(chunk)] = chunk.model_copy(
57
+ update={"content": content, "tokens": compressed_tokens, "metadata": metadata}
58
+ )
59
+ normalized = [replacements.get(id(chunk), chunk) for chunk in chunks]
60
+ return await self.budget.fit(normalized, budget)