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,670 @@
1
+ """Production benchmark harness for real, revision-pinned repositories.
2
+
3
+ The harness intentionally separates measurements from claims. It never fills missing
4
+ metrics with synthetic values: unavailable file-read counts, provider token usage, and
5
+ costs remain ``None`` unless the strategy can observe them directly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+ import re
13
+ import shlex
14
+ import subprocess
15
+ import tempfile
16
+ from collections.abc import Iterable, Mapping, Sequence
17
+ from dataclasses import asdict, dataclass, field
18
+ from pathlib import Path
19
+ from time import perf_counter
20
+ from typing import Any, Literal
21
+
22
+ from codecortex.backends import (
23
+ BackendManager,
24
+ ContextBackendAdapter,
25
+ GraphBackendAdapter,
26
+ SymbolBackendAdapter,
27
+ )
28
+ from codecortex.backends.mcp_client import MCPStdioClient
29
+
30
+ _TEXT_SUFFIXES = {
31
+ ".py",
32
+ ".pyi",
33
+ ".js",
34
+ ".jsx",
35
+ ".ts",
36
+ ".tsx",
37
+ ".go",
38
+ ".rs",
39
+ ".java",
40
+ ".c",
41
+ ".h",
42
+ ".cc",
43
+ ".cpp",
44
+ ".hpp",
45
+ ".cs",
46
+ ".php",
47
+ ".rb",
48
+ ".kt",
49
+ ".kts",
50
+ ".scala",
51
+ ".swift",
52
+ ".vue",
53
+ ".svelte",
54
+ ".md",
55
+ ".toml",
56
+ ".yaml",
57
+ ".yml",
58
+ ".json",
59
+ }
60
+ _EXCLUDED = {".git", ".codecortex", ".venv", "venv", "node_modules", "dist", "build", "target"}
61
+ _PATH_RE = re.compile(
62
+ r"(?:^|[\s\[(`'\"])([A-Za-z0-9_.@+-]+(?:/[A-Za-z0-9_.@+-]+)+\.[A-Za-z0-9]+)"
63
+ )
64
+
65
+ ScenarioName = Literal["vanilla", "graph", "symbols", "context", "full"]
66
+
67
+
68
+ @dataclass(frozen=True, slots=True)
69
+ class RepositorySpec:
70
+ name: str
71
+ url: str
72
+ revision: str
73
+ cases: tuple[BenchmarkCaseSpec, ...]
74
+
75
+ @classmethod
76
+ def from_dict(cls, value: Mapping[str, Any]) -> RepositorySpec:
77
+ return cls(
78
+ name=str(value["name"]),
79
+ url=str(value["url"]),
80
+ revision=str(value["revision"]),
81
+ cases=tuple(BenchmarkCaseSpec.from_dict(item) for item in value.get("cases", [])),
82
+ )
83
+
84
+
85
+ @dataclass(frozen=True, slots=True)
86
+ class BenchmarkCaseSpec:
87
+ id: str
88
+ query: str
89
+ expected_paths: tuple[str, ...] = ()
90
+ expected_symbols: tuple[str, ...] = ()
91
+
92
+ @classmethod
93
+ def from_dict(cls, value: Mapping[str, Any]) -> BenchmarkCaseSpec:
94
+ return cls(
95
+ id=str(value["id"]),
96
+ query=str(value["query"]),
97
+ expected_paths=tuple(str(item) for item in value.get("expected_paths", [])),
98
+ expected_symbols=tuple(str(item) for item in value.get("expected_symbols", [])),
99
+ )
100
+
101
+
102
+ @dataclass(frozen=True, slots=True)
103
+ class ObservedMetrics:
104
+ wall_time_ms: float
105
+ context_chars: int
106
+ estimated_context_tokens: int
107
+ files_read: int | None
108
+ files_surfaced: int
109
+ tool_calls: int
110
+ input_tokens: int | None = None
111
+ output_tokens: int | None = None
112
+ cost_usd: float | None = None
113
+ cost_source: str | None = None
114
+
115
+
116
+ @dataclass(frozen=True, slots=True)
117
+ class ScenarioResult:
118
+ repository: str
119
+ revision: str
120
+ case_id: str
121
+ scenario: ScenarioName
122
+ status: Literal["ok", "skipped", "error"]
123
+ success: bool | None
124
+ path_recall: float | None
125
+ symbol_recall: float | None
126
+ metrics: ObservedMetrics | None
127
+ error: str | None = None
128
+
129
+
130
+ @dataclass(frozen=True, slots=True)
131
+ class SetupMeasurement:
132
+ repository: str
133
+ scenario: ScenarioName
134
+ wall_time_ms: float
135
+ status: Literal["ok", "skipped", "error"]
136
+ detail: str = ""
137
+
138
+
139
+ @dataclass(slots=True)
140
+ class ProductionBenchmarkReport:
141
+ repositories: list[dict[str, str]] = field(default_factory=list)
142
+ setup: list[SetupMeasurement] = field(default_factory=list)
143
+ results: list[ScenarioResult] = field(default_factory=list)
144
+
145
+ def summary(self) -> dict[str, dict[str, float | int | None]]:
146
+ grouped: dict[str, list[ScenarioResult]] = {}
147
+ for result in self.results:
148
+ grouped.setdefault(result.scenario, []).append(result)
149
+ output: dict[str, dict[str, float | int | None]] = {}
150
+ for scenario, rows in grouped.items():
151
+ completed = [row for row in rows if row.status == "ok" and row.metrics is not None]
152
+ scored = [row for row in completed if row.success is not None]
153
+ output[scenario] = {
154
+ "cases": len(rows),
155
+ "completed": len(completed),
156
+ "success_rate": (
157
+ sum(bool(row.success) for row in scored) / len(scored) if scored else None
158
+ ),
159
+ "avg_wall_time_ms": _average(
160
+ row.metrics.wall_time_ms for row in completed if row.metrics
161
+ ),
162
+ "avg_estimated_context_tokens": _average(
163
+ row.metrics.estimated_context_tokens for row in completed if row.metrics
164
+ ),
165
+ "avg_files_read": _average(
166
+ row.metrics.files_read
167
+ for row in completed
168
+ if row.metrics and row.metrics.files_read is not None
169
+ ),
170
+ "avg_files_surfaced": _average(
171
+ row.metrics.files_surfaced for row in completed if row.metrics
172
+ ),
173
+ "avg_tool_calls": _average(
174
+ row.metrics.tool_calls for row in completed if row.metrics
175
+ ),
176
+ "avg_cost_usd": _average(
177
+ row.metrics.cost_usd
178
+ for row in completed
179
+ if row.metrics and row.metrics.cost_usd is not None
180
+ ),
181
+ }
182
+ return output
183
+
184
+ def save(self, output: Path) -> None:
185
+ output.parent.mkdir(parents=True, exist_ok=True)
186
+ payload = {
187
+ "schema_version": 1,
188
+ "measurement_policy": {
189
+ "estimated_context_tokens": "UTF-8 text length divided by four; explicitly estimated",
190
+ "missing_metrics": "null; never synthesized",
191
+ "cost": "reported only when an instrumented agent/provider supplies a measured value",
192
+ },
193
+ "repositories": self.repositories,
194
+ "setup": [asdict(item) for item in self.setup],
195
+ "summary": self.summary(),
196
+ "results": [asdict(item) for item in self.results],
197
+ }
198
+ output.write_text(
199
+ json.dumps(payload, indent=2, ensure_ascii=False) + "\n",
200
+ encoding="utf-8",
201
+ )
202
+
203
+
204
+ @dataclass(frozen=True, slots=True)
205
+ class RetrievalObservation:
206
+ text: str
207
+ files_read: int | None
208
+ tool_calls: int
209
+
210
+
211
+ class RepositoryCheckout:
212
+ """Materialize a public repository at an exact immutable revision."""
213
+
214
+ def __init__(self, cache_root: Path) -> None:
215
+ self.cache_root = cache_root.resolve()
216
+
217
+ def ensure(self, spec: RepositorySpec) -> Path:
218
+ target = self.cache_root / _safe_name(spec.name) / spec.revision[:12]
219
+ marker = target / ".codecortex-benchmark-revision"
220
+ if marker.exists() and marker.read_text(encoding="utf-8").strip() == spec.revision:
221
+ return target
222
+ target.parent.mkdir(parents=True, exist_ok=True)
223
+ if target.exists():
224
+ _remove_tree(target)
225
+ target.mkdir(parents=True)
226
+ _git(target, "init")
227
+ _git(target, "remote", "add", "origin", spec.url)
228
+ _git(target, "fetch", "--depth", "1", "origin", spec.revision, timeout=600)
229
+ _git(target, "checkout", "--detach", "FETCH_HEAD")
230
+ resolved = _git(target, "rev-parse", "HEAD").strip()
231
+ if resolved != spec.revision:
232
+ raise RuntimeError(
233
+ f"revision mismatch for {spec.name}: expected {spec.revision}, got {resolved}"
234
+ )
235
+ marker.write_text(spec.revision + "\n", encoding="utf-8")
236
+ return target
237
+
238
+
239
+ class ProductionBenchmarkRunner:
240
+ """Compare a lexical baseline, individual mature engines, and the integrated stack."""
241
+
242
+ scenarios: tuple[ScenarioName, ...] = ("vanilla", "graph", "symbols", "context", "full")
243
+
244
+ def __init__(
245
+ self,
246
+ specs: Sequence[RepositorySpec],
247
+ *,
248
+ workspace: Path,
249
+ backend_manager: BackendManager | None = None,
250
+ provision_backends: bool = False,
251
+ result_limit: int = 30,
252
+ ) -> None:
253
+ self.specs = tuple(specs)
254
+ self.workspace = workspace.resolve()
255
+ self.checkout = RepositoryCheckout(self.workspace / "repositories")
256
+ self.manager = backend_manager or BackendManager(timeout_seconds=900)
257
+ self.provision_backends = provision_backends
258
+ self.result_limit = result_limit
259
+
260
+ @classmethod
261
+ def load(
262
+ cls,
263
+ spec_path: Path,
264
+ *,
265
+ workspace: Path,
266
+ backend_manager: BackendManager | None = None,
267
+ provision_backends: bool = False,
268
+ ) -> ProductionBenchmarkRunner:
269
+ payload = json.loads(spec_path.read_text(encoding="utf-8"))
270
+ repos = [RepositorySpec.from_dict(item) for item in payload["repositories"]]
271
+ return cls(
272
+ repos,
273
+ workspace=workspace,
274
+ backend_manager=backend_manager,
275
+ provision_backends=provision_backends,
276
+ )
277
+
278
+ def run(
279
+ self,
280
+ scenarios: Sequence[ScenarioName] | None = None,
281
+ ) -> ProductionBenchmarkReport:
282
+ selected = tuple(scenarios or self.scenarios)
283
+ report = ProductionBenchmarkReport()
284
+ for spec in self.specs:
285
+ root = self.checkout.ensure(spec)
286
+ report.repositories.append(
287
+ {"name": spec.name, "url": spec.url, "revision": spec.revision}
288
+ )
289
+ graph = GraphBackendAdapter(root, self.manager)
290
+ symbols = SymbolBackendAdapter(root, self.manager)
291
+ context = ContextBackendAdapter(root, self.manager)
292
+ availability = self._prepare(
293
+ spec,
294
+ root,
295
+ selected,
296
+ graph,
297
+ symbols,
298
+ context,
299
+ report,
300
+ )
301
+ for case in spec.cases:
302
+ baseline: RetrievalObservation | None = None
303
+ for scenario in selected:
304
+ if scenario == "vanilla":
305
+ baseline = self._lexical(root, case)
306
+ report.results.append(
307
+ self._measure(spec, case, scenario, lambda b=baseline: b)
308
+ )
309
+ continue
310
+ if not availability.get(scenario, False):
311
+ report.results.append(
312
+ ScenarioResult(
313
+ repository=spec.name,
314
+ revision=spec.revision,
315
+ case_id=case.id,
316
+ scenario=scenario,
317
+ status="skipped",
318
+ success=None,
319
+ path_recall=None,
320
+ symbol_recall=None,
321
+ metrics=None,
322
+ error="required backend is not provisioned/healthy",
323
+ )
324
+ )
325
+ continue
326
+ if baseline is None and scenario == "context":
327
+ baseline = self._lexical(root, case)
328
+ operation = self._operation(
329
+ scenario,
330
+ case,
331
+ root,
332
+ graph,
333
+ symbols,
334
+ context,
335
+ baseline,
336
+ )
337
+ report.results.append(self._measure(spec, case, scenario, operation))
338
+ return report
339
+
340
+ def _prepare(
341
+ self,
342
+ spec: RepositorySpec,
343
+ root: Path,
344
+ selected: Sequence[ScenarioName],
345
+ graph: GraphBackendAdapter,
346
+ symbols: SymbolBackendAdapter,
347
+ context: ContextBackendAdapter,
348
+ report: ProductionBenchmarkReport,
349
+ ) -> dict[ScenarioName, bool]:
350
+ availability: dict[ScenarioName, bool] = {
351
+ "vanilla": True,
352
+ "graph": False,
353
+ "symbols": False,
354
+ "context": False,
355
+ "full": False,
356
+ }
357
+ adapters = {"graph": graph, "symbols": symbols, "context": context}
358
+ for key, adapter in adapters.items():
359
+ needed = key in selected or "full" in selected
360
+ if not needed:
361
+ continue
362
+ started = perf_counter()
363
+ try:
364
+ if self.provision_backends:
365
+ self.manager.ensure(adapter.spec)
366
+ installed = self.manager.is_installed(adapter.spec)
367
+ if not installed:
368
+ status: Literal["ok", "skipped", "error"] = "skipped"
369
+ detail = "not installed"
370
+ healthy = False
371
+ else:
372
+ healthy = self.manager.probe(adapter.spec, provision=False)
373
+ status = "ok" if healthy else "error"
374
+ detail = "healthy" if healthy else "health probe failed"
375
+ if healthy and key == "graph":
376
+ graph.build()
377
+ elif healthy and key in {"symbols", "context"}:
378
+ adapter.require_tools(adapter.tools(), adapter.required_tools)
379
+ availability[key] = healthy # type: ignore[literal-required]
380
+ except Exception as exc:
381
+ status = "error"
382
+ detail = f"{type(exc).__name__}: {exc}"
383
+ availability[key] = False # type: ignore[literal-required]
384
+ report.setup.append(
385
+ SetupMeasurement(
386
+ repository=spec.name,
387
+ scenario=key, # type: ignore[arg-type]
388
+ wall_time_ms=(perf_counter() - started) * 1000,
389
+ status=status,
390
+ detail=detail,
391
+ )
392
+ )
393
+ availability["full"] = (
394
+ availability["graph"] and availability["symbols"] and availability["context"]
395
+ )
396
+ return availability
397
+
398
+ def _operation(
399
+ self,
400
+ scenario: ScenarioName,
401
+ case: BenchmarkCaseSpec,
402
+ root: Path,
403
+ graph: GraphBackendAdapter,
404
+ symbols: SymbolBackendAdapter,
405
+ context: ContextBackendAdapter,
406
+ baseline: RetrievalObservation | None,
407
+ ):
408
+ if scenario == "graph":
409
+ return lambda: RetrievalObservation(graph.query(case.query), None, 1)
410
+ if scenario == "symbols":
411
+ return lambda: self._symbol_observation(symbols, case)
412
+ if scenario == "context":
413
+ assert baseline is not None
414
+ return lambda: self._context_observation(context, baseline)
415
+ if scenario == "full":
416
+ return lambda: self._full_observation(graph, symbols, context, case)
417
+ raise ValueError(scenario)
418
+
419
+ def _symbol_observation(
420
+ self,
421
+ symbols: SymbolBackendAdapter,
422
+ case: BenchmarkCaseSpec,
423
+ ) -> RetrievalObservation:
424
+ target = case.expected_symbols[0] if case.expected_symbols else case.query
425
+ payload = symbols.call(
426
+ "find_symbol",
427
+ {"name_path_pattern": target, "include_body": True, "depth": 1},
428
+ )
429
+ return RetrievalObservation(
430
+ MCPStdioClient.content_text(payload) or json.dumps(payload),
431
+ None,
432
+ 1,
433
+ )
434
+
435
+ def _context_observation(
436
+ self,
437
+ context: ContextBackendAdapter,
438
+ baseline: RetrievalObservation,
439
+ ) -> RetrievalObservation:
440
+ payload = context.compress(baseline.text)
441
+ text = MCPStdioClient.content_text(payload) or json.dumps(payload)
442
+ return RetrievalObservation(text, baseline.files_read, baseline.tool_calls + 1)
443
+
444
+ def _full_observation(
445
+ self,
446
+ graph: GraphBackendAdapter,
447
+ symbols: SymbolBackendAdapter,
448
+ context: ContextBackendAdapter,
449
+ case: BenchmarkCaseSpec,
450
+ ) -> RetrievalObservation:
451
+ graph_text = graph.query(case.query)
452
+ symbol = self._symbol_observation(symbols, case)
453
+ combined = f"[graph]\n{graph_text}\n\n[symbols]\n{symbol.text}"
454
+ payload = context.compress(combined)
455
+ compressed = MCPStdioClient.content_text(payload) or json.dumps(payload)
456
+ return RetrievalObservation(compressed, None, 3)
457
+
458
+ def _lexical(
459
+ self,
460
+ root: Path,
461
+ case: BenchmarkCaseSpec,
462
+ ) -> RetrievalObservation:
463
+ terms = {
464
+ term.lower()
465
+ for term in re.findall(r"[A-Za-z_][A-Za-z0-9_]{2,}", case.query)
466
+ }
467
+ scored: list[tuple[int, str, str]] = []
468
+ files_read = 0
469
+ for path in root.rglob("*"):
470
+ if not path.is_file() or path.suffix.lower() not in _TEXT_SUFFIXES:
471
+ continue
472
+ relative = path.relative_to(root)
473
+ if any(part in _EXCLUDED for part in relative.parts):
474
+ continue
475
+ try:
476
+ text = path.read_text(encoding="utf-8")
477
+ except (OSError, UnicodeDecodeError):
478
+ continue
479
+ files_read += 1
480
+ lowered = text.lower()
481
+ path_text = relative.as_posix().lower()
482
+ score = sum(
483
+ lowered.count(term) + (5 if term in path_text else 0)
484
+ for term in terms
485
+ )
486
+ if score:
487
+ scored.append((score, relative.as_posix(), text[:8000]))
488
+ scored.sort(key=lambda row: (-row[0], row[1]))
489
+ selected = scored[: self.result_limit]
490
+ text = "\n\n".join(f"[{path}]\n{content}" for _, path, content in selected)
491
+ return RetrievalObservation(text, files_read, 1)
492
+
493
+ def _measure(
494
+ self,
495
+ spec: RepositorySpec,
496
+ case: BenchmarkCaseSpec,
497
+ scenario: ScenarioName,
498
+ operation,
499
+ ) -> ScenarioResult:
500
+ started = perf_counter()
501
+ try:
502
+ observation: RetrievalObservation = operation()
503
+ duration = (perf_counter() - started) * 1000
504
+ path_recall = _evidence_recall(case.expected_paths, observation.text)
505
+ symbol_recall = _evidence_recall(case.expected_symbols, observation.text)
506
+ success = (not case.expected_paths or path_recall > 0) and (
507
+ not case.expected_symbols or symbol_recall > 0
508
+ )
509
+ surfaced = len(_extract_paths(observation.text))
510
+ return ScenarioResult(
511
+ repository=spec.name,
512
+ revision=spec.revision,
513
+ case_id=case.id,
514
+ scenario=scenario,
515
+ status="ok",
516
+ success=success,
517
+ path_recall=path_recall,
518
+ symbol_recall=symbol_recall,
519
+ metrics=ObservedMetrics(
520
+ wall_time_ms=duration,
521
+ context_chars=len(observation.text),
522
+ estimated_context_tokens=max(0, len(observation.text) // 4),
523
+ files_read=observation.files_read,
524
+ files_surfaced=surfaced,
525
+ tool_calls=observation.tool_calls,
526
+ ),
527
+ )
528
+ except Exception as exc:
529
+ return ScenarioResult(
530
+ repository=spec.name,
531
+ revision=spec.revision,
532
+ case_id=case.id,
533
+ scenario=scenario,
534
+ status="error",
535
+ success=None,
536
+ path_recall=None,
537
+ symbol_recall=None,
538
+ metrics=None,
539
+ error=f"{type(exc).__name__}: {exc}",
540
+ )
541
+
542
+
543
+ @dataclass(frozen=True, slots=True)
544
+ class AgentProtocolResult:
545
+ answer: str
546
+ files_read: int | None = None
547
+ tool_calls: int | None = None
548
+ input_tokens: int | None = None
549
+ output_tokens: int | None = None
550
+ cost_usd: float | None = None
551
+ cost_source: str | None = None
552
+
553
+
554
+ class InstrumentedAgentRunner:
555
+ """Run an actual coding agent through a JSON protocol without inventing usage metrics.
556
+
557
+ The configured command receives one JSON request on stdin and must emit one JSON object
558
+ on stdout. Metrics omitted by the agent remain null in the benchmark result.
559
+ """
560
+
561
+ def __init__(self, command: str, *, timeout_seconds: float = 900.0) -> None:
562
+ self.argv = tuple(shlex.split(command))
563
+ if not self.argv:
564
+ raise ValueError("agent command is empty")
565
+ self.timeout_seconds = timeout_seconds
566
+
567
+ def run(
568
+ self,
569
+ *,
570
+ scenario: ScenarioName,
571
+ repository: Path,
572
+ case: BenchmarkCaseSpec,
573
+ environment: Mapping[str, str] | None = None,
574
+ ) -> AgentProtocolResult:
575
+ request = {
576
+ "schema_version": 1,
577
+ "scenario": scenario,
578
+ "repository": str(repository),
579
+ "case": asdict(case),
580
+ }
581
+ process = subprocess.run(
582
+ self.argv,
583
+ cwd=repository,
584
+ env={**os.environ, **dict(environment or {})},
585
+ input=json.dumps(request),
586
+ text=True,
587
+ capture_output=True,
588
+ timeout=self.timeout_seconds,
589
+ check=False,
590
+ )
591
+ if process.returncode != 0:
592
+ raise RuntimeError(
593
+ process.stderr.strip() or f"agent exited with {process.returncode}"
594
+ )
595
+ payload = json.loads(process.stdout)
596
+ if not isinstance(payload, dict) or not isinstance(payload.get("answer"), str):
597
+ raise ValueError("agent must return a JSON object containing string field 'answer'")
598
+ return AgentProtocolResult(
599
+ answer=payload["answer"],
600
+ files_read=_optional_int(payload.get("files_read")),
601
+ tool_calls=_optional_int(payload.get("tool_calls")),
602
+ input_tokens=_optional_int(payload.get("input_tokens")),
603
+ output_tokens=_optional_int(payload.get("output_tokens")),
604
+ cost_usd=_optional_float(payload.get("cost_usd")),
605
+ cost_source=(
606
+ str(payload["cost_source"])
607
+ if payload.get("cost_source") is not None
608
+ else None
609
+ ),
610
+ )
611
+
612
+
613
+ def load_repository_specs(path: Path) -> tuple[RepositorySpec, ...]:
614
+ payload = json.loads(path.read_text(encoding="utf-8"))
615
+ return tuple(RepositorySpec.from_dict(item) for item in payload["repositories"])
616
+
617
+
618
+ def _evidence_recall(expected: Sequence[str], text: str) -> float:
619
+ if not expected:
620
+ return 1.0
621
+ lowered = text.lower()
622
+ return sum(item.lower() in lowered for item in expected) / len(expected)
623
+
624
+
625
+ def _extract_paths(text: str) -> set[str]:
626
+ return {match.group(1) for match in _PATH_RE.finditer(text)}
627
+
628
+
629
+ def _average(values: Iterable[int | float | None]) -> float | None:
630
+ rows = [float(value) for value in values if value is not None]
631
+ return sum(rows) / len(rows) if rows else None
632
+
633
+
634
+ def _optional_int(value: Any) -> int | None:
635
+ return int(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None
636
+
637
+
638
+ def _optional_float(value: Any) -> float | None:
639
+ return (
640
+ float(value)
641
+ if isinstance(value, (int, float)) and not isinstance(value, bool)
642
+ else None
643
+ )
644
+
645
+
646
+ def _safe_name(value: str) -> str:
647
+ return re.sub(r"[^A-Za-z0-9_.-]+", "-", value).strip("-") or "repository"
648
+
649
+
650
+ def _remove_tree(path: Path) -> None:
651
+ import shutil
652
+
653
+ shutil.rmtree(path, ignore_errors=False)
654
+
655
+
656
+ def _git(root: Path, *args: str, timeout: float = 120.0) -> str:
657
+ process = subprocess.run(
658
+ ["git", "-C", str(root), *args],
659
+ text=True,
660
+ capture_output=True,
661
+ timeout=timeout,
662
+ check=False,
663
+ )
664
+ if process.returncode != 0:
665
+ raise RuntimeError(process.stderr.strip() or process.stdout.strip())
666
+ return process.stdout
667
+
668
+
669
+ def temporary_benchmark_workspace() -> tempfile.TemporaryDirectory[str]:
670
+ return tempfile.TemporaryDirectory(prefix="codecortex-benchmark-")