engineering-platform 2.2.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.
Files changed (130) hide show
  1. engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
  2. engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
  3. engineering_platform/__init__.py +1 -0
  4. engineering_platform/__main__.py +7 -0
  5. engineering_platform/agent_state.py +530 -0
  6. engineering_platform/agent_trust.py +174 -0
  7. engineering_platform/assets/dashboard.css +1317 -0
  8. engineering_platform/assets/dashboard.js +8534 -0
  9. engineering_platform/assets/dashboard_locales.mjs +4049 -0
  10. engineering_platform/assets/dashboard_status_store.mjs +41 -0
  11. engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
  12. engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
  13. engineering_platform/assets/operations-console/icon-dark.png +0 -0
  14. engineering_platform/assets/operations-console/icon-light.png +0 -0
  15. engineering_platform/assets/operations-console/icon-transparent.png +0 -0
  16. engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
  17. engineering_platform/capability_preflight.py +285 -0
  18. engineering_platform/capability_review.py +261 -0
  19. engineering_platform/central_data_transfer.py +195 -0
  20. engineering_platform/central_database.py +245 -0
  21. engineering_platform/central_store_migration.py +1672 -0
  22. engineering_platform/codex_capacity.py +81 -0
  23. engineering_platform/codex_chat.py +226 -0
  24. engineering_platform/codex_observability.py +153 -0
  25. engineering_platform/component_lock.py +40 -0
  26. engineering_platform/component_logging.py +420 -0
  27. engineering_platform/console_presentation.py +14 -0
  28. engineering_platform/console_route_ownership.py +83 -0
  29. engineering_platform/contracts/__init__.py +38 -0
  30. engineering_platform/contracts/ep_consumer.py +391 -0
  31. engineering_platform/contracts/models.py +105 -0
  32. engineering_platform/contracts/projection.py +401 -0
  33. engineering_platform/dashboard_browser_validation.py +206 -0
  34. engineering_platform/dashboard_state.py +630 -0
  35. engineering_platform/dashboard_supervisor.swift +105 -0
  36. engineering_platform/dashboard_translation.py +129 -0
  37. engineering_platform/dependabot_producer.py +349 -0
  38. engineering_platform/drift_diagnostics.py +144 -0
  39. engineering_platform/emergency_recovery.py +268 -0
  40. engineering_platform/engineering_memory.py +139 -0
  41. engineering_platform/ep_consumer_credentials.py +473 -0
  42. engineering_platform/evidence_projection.py +213 -0
  43. engineering_platform/execution_activity.py +218 -0
  44. engineering_platform/execution_context.py +132 -0
  45. engineering_platform/execution_errors.py +42 -0
  46. engineering_platform/execution_evidence.py +24 -0
  47. engineering_platform/execution_executor.py +730 -0
  48. engineering_platform/execution_finalization.py +44 -0
  49. engineering_platform/execution_host.py +3306 -0
  50. engineering_platform/execution_lease.py +365 -0
  51. engineering_platform/execution_lifecycle.py +447 -0
  52. engineering_platform/execution_models.py +43 -0
  53. engineering_platform/execution_readiness.py +166 -0
  54. engineering_platform/execution_reporting.py +1607 -0
  55. engineering_platform/execution_repository.py +253 -0
  56. engineering_platform/execution_timeout_policy.py +56 -0
  57. engineering_platform/execution_timing.py +440 -0
  58. engineering_platform/execution_transaction.py +28 -0
  59. engineering_platform/external_producer_binding.py +235 -0
  60. engineering_platform/file_inbox.py +249 -0
  61. engineering_platform/forensic_attribution.py +338 -0
  62. engineering_platform/forensic_attribution_v2.py +134 -0
  63. engineering_platform/forensic_delta.py +299 -0
  64. engineering_platform/golden_scenario.py +63 -0
  65. engineering_platform/historical_dashboard_configuration.py +171 -0
  66. engineering_platform/host_admin.py +199 -0
  67. engineering_platform/host_preflight.py +231 -0
  68. engineering_platform/installation_relocation.py +122 -0
  69. engineering_platform/investigation_ledger.py +89 -0
  70. engineering_platform/legacy_inbox_migration.py +79 -0
  71. engineering_platform/lifecycle_worker.py +223 -0
  72. engineering_platform/live_status.py +267 -0
  73. engineering_platform/local_api.py +209 -0
  74. engineering_platform/local_api_keychain.py +51 -0
  75. engineering_platform/local_repository_binding.py +138 -0
  76. engineering_platform/managed_autonomy.py +509 -0
  77. engineering_platform/managed_codex_runtime.py +105 -0
  78. engineering_platform/parity_context.py +203 -0
  79. engineering_platform/parity_lifecycle_dispatcher.py +488 -0
  80. engineering_platform/platform_admin.py +13 -0
  81. engineering_platform/platform_api.py +428 -0
  82. engineering_platform/platform_bootstrap.py +385 -0
  83. engineering_platform/platform_components.py +65 -0
  84. engineering_platform/platform_version.py +171 -0
  85. engineering_platform/pr_check_repair.py +276 -0
  86. engineering_platform/pr_evidence_backfill.py +278 -0
  87. engineering_platform/producer.py +209 -0
  88. engineering_platform/project_agent.py +366 -0
  89. engineering_platform/project_agent_service.py +244 -0
  90. engineering_platform/project_topology.py +126 -0
  91. engineering_platform/prompt_history.py +591 -0
  92. engineering_platform/provider_context.py +136 -0
  93. engineering_platform/provider_context_benchmark.py +41 -0
  94. engineering_platform/provider_context_scope.py +90 -0
  95. engineering_platform/provider_interruption.py +168 -0
  96. engineering_platform/provider_process_identity.py +80 -0
  97. engineering_platform/provider_readiness.py +138 -0
  98. engineering_platform/provider_recovery.py +647 -0
  99. engineering_platform/provider_usage.py +497 -0
  100. engineering_platform/providers.py +471 -0
  101. engineering_platform/qualification.py +220 -0
  102. engineering_platform/recommendation_handoff.py +238 -0
  103. engineering_platform/report_analysis.py +193 -0
  104. engineering_platform/repository_attachment.py +171 -0
  105. engineering_platform/repository_handoff.py +95 -0
  106. engineering_platform/resources.py +38 -0
  107. engineering_platform/reviewer_evidence.py +70 -0
  108. engineering_platform/schemas/repository-attachment.schema.json +61 -0
  109. engineering_platform/server.py +3679 -0
  110. engineering_platform/server_console_services.py +2024 -0
  111. engineering_platform/server_relay.py +172 -0
  112. engineering_platform/server_service.py +122 -0
  113. engineering_platform/status_model.py +135 -0
  114. engineering_platform/status_reconciliation.py +34 -0
  115. engineering_platform/storage.py +2440 -0
  116. engineering_platform/submission_cli.py +77 -0
  117. engineering_platform/submission_intake.py +45 -0
  118. engineering_platform/submission_service.py +317 -0
  119. engineering_platform/telemetry.py +951 -0
  120. engineering_platform/templates/workspace-config.json +25 -0
  121. engineering_platform/validation_identity.py +50 -0
  122. engineering_platform/validation_profile.py +211 -0
  123. engineering_platform/workspace_preflight.py +263 -0
  124. engineering_platform/worktree_provenance.py +147 -0
  125. engineering_platform/worktree_tooling.py +18 -0
  126. engineering_platform-2.2.0.dist-info/METADATA +18 -0
  127. engineering_platform-2.2.0.dist-info/RECORD +130 -0
  128. engineering_platform-2.2.0.dist-info/WHEEL +5 -0
  129. engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
  130. engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,497 @@
1
+ """Provider invocation usage and bounded provider-input attribution.
2
+
3
+ This module deliberately stores only counters derived from provider JSONL. It
4
+ never stores prompts, tool arguments, command output, paths, or model replies.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ import json
11
+ from math import ceil
12
+ from pathlib import Path
13
+ import sqlite3
14
+ import re
15
+ from statistics import median
16
+ from typing import Mapping
17
+ from uuid import uuid4
18
+
19
+ from .agent_state import redact_diagnostic
20
+ from .storage import open_storage
21
+
22
+
23
+ RATE_TABLE_VERSION = "2026-08-18"
24
+ EUR_PER_CREDIT = 0.04
25
+ RATE_TABLE = {
26
+ "gpt-5.6-sol": {"uncached_input": 125.0, "cached_input": 12.5, "output": 750.0},
27
+ "gpt-5.6-terra": {"uncached_input": 50.0, "cached_input": 5.0, "output": 300.0},
28
+ "gpt-5.6-luna": {"uncached_input": 5.0, "cached_input": 0.5, "output": 30.0},
29
+ }
30
+ AUTHORITATIVE, DERIVED, UNAVAILABLE = "AUTHORITATIVE", "DERIVED", "UNAVAILABLE"
31
+ _SPEED_STATES = frozenset({"FAST", "NORMAL_DEFAULT", "OTHER", "UNKNOWN"})
32
+ _SAFE_CHURN_TEXT_FIELDS = frozenset({
33
+ "interruption_classification",
34
+ "interruption_reason",
35
+ "usage_state",
36
+ "context_scope_policy",
37
+ "context_scope_initial",
38
+ "context_scope_effective",
39
+ "context_escalation_reasons",
40
+ "context_escalation_boundaries",
41
+ "context_escalation_diagnostic",
42
+ })
43
+ _MODEL_NORMALIZATION = {
44
+ "gpt-5.6-sol": "gpt-5.6-sol",
45
+ "gpt-5.6-terra": "gpt-5.6-terra",
46
+ "gpt-5.6-luna": "gpt-5.6-luna",
47
+ }
48
+
49
+
50
+ def _number(value: object) -> int | None:
51
+ return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
52
+
53
+
54
+ def speed_state(metadata: Mapping[str, object] | None) -> str:
55
+ """Return only a runtime-observed speed state; UI preferences are irrelevant."""
56
+ known_fields = {
57
+ "speed_state",
58
+ "speed_mode",
59
+ "execution_speed",
60
+ "codex_speed_mode",
61
+ "fast_mode",
62
+ "configuration_profile",
63
+ "codex_configuration_profile",
64
+ }
65
+ for key, value in (metadata or {}).items():
66
+ normalized_key = str(key).casefold().replace("-", "_")
67
+ if normalized_key not in known_fields or not isinstance(value, str):
68
+ continue
69
+ normalized_value = value.casefold().strip()
70
+ if normalized_key == "fast_mode":
71
+ if normalized_value in {"true", "fast", "enabled"}:
72
+ return "FAST"
73
+ if normalized_value in {"false", "normal", "disabled"}:
74
+ return "NORMAL_DEFAULT"
75
+ if re.search(r"\bfast(?:\s+mode)?\b", normalized_value):
76
+ return "FAST"
77
+ if re.search(r"\b(?:normal|default)\b", normalized_value):
78
+ return "NORMAL_DEFAULT"
79
+ return "OTHER"
80
+ return "UNKNOWN"
81
+
82
+
83
+ def normalize_codex_model(raw_model: object) -> str | None:
84
+ """Map only explicitly supported Codex runtime model identifiers."""
85
+ if not isinstance(raw_model, str):
86
+ return None
87
+ return _MODEL_NORMALIZATION.get(raw_model.casefold().strip())
88
+
89
+
90
+ def _usage_from_event(event: object) -> dict[str, int]:
91
+ usage: dict[str, int] = {}
92
+ if not isinstance(event, dict):
93
+ return usage
94
+
95
+ def walk(value: object) -> None:
96
+ if isinstance(value, dict):
97
+ for key, item in value.items():
98
+ normalized = key.casefold().replace("-", "_")
99
+ if normalized in {
100
+ "input_tokens",
101
+ "cached_input_tokens",
102
+ "output_tokens",
103
+ "reasoning_tokens",
104
+ "total_tokens",
105
+ }:
106
+ number = _number(item)
107
+ if number is not None:
108
+ usage[normalized] = number
109
+ elif normalized in {"usage", "token_usage"} or isinstance(item, (dict, list)):
110
+ walk(item)
111
+ elif isinstance(value, list):
112
+ for item in value:
113
+ walk(item)
114
+
115
+ walk(event)
116
+ return usage
117
+
118
+
119
+ def usage_snapshots_from_jsonl(*outputs: str) -> tuple[dict[str, int], ...]:
120
+ """Return only actual final-turn counter snapshots, without conversation content.
121
+
122
+ Codex CLI 0.147.0 exposes usage on ``turn.completed``. The counters are
123
+ provider-execution cumulative counters, not an active-context measurement.
124
+ """
125
+ snapshots: list[dict[str, int]] = []
126
+ for output in outputs:
127
+ for line in output.splitlines():
128
+ try:
129
+ event = json.loads(line)
130
+ except json.JSONDecodeError:
131
+ continue
132
+ if isinstance(event, dict) and event.get("type") == "turn.completed":
133
+ found = _usage_from_event(event.get("usage"))
134
+ if found:
135
+ snapshots.append(found)
136
+ return tuple(snapshots)
137
+
138
+
139
+ def usage_from_jsonl(*outputs: str) -> dict[str, int]:
140
+ """Use the last final-turn usage snapshot; never sum repeated snapshots."""
141
+ snapshots = usage_snapshots_from_jsonl(*outputs)
142
+ return dict(snapshots[-1]) if snapshots else {}
143
+
144
+
145
+ def churn_from_jsonl(*outputs: str) -> dict[str, int]:
146
+ """Measure deterministic, content-free churn indicators from JSONL events."""
147
+ result = {
148
+ key: 0
149
+ for key in (
150
+ "file_read_count",
151
+ "distinct_files_read",
152
+ "repeated_file_read_count",
153
+ "glob_search_calls",
154
+ "grep_calls",
155
+ "shell_command_calls",
156
+ "test_commands",
157
+ "tool_output_bytes",
158
+ "maximum_tool_output_bytes",
159
+ "passing_test_output_bytes",
160
+ "failed_test_diagnostic_bytes",
161
+ "git_output_bytes",
162
+ "github_output_bytes",
163
+ "historical_commit_queries",
164
+ "historical_commit_results",
165
+ "historical_pr_queries",
166
+ "historical_pr_results",
167
+ "historical_context_bytes",
168
+ "tool_loop_operations",
169
+ )
170
+ }
171
+ reads: set[str] = set()
172
+ historical_observed = False
173
+ for output in outputs:
174
+ for line in output.splitlines():
175
+ try:
176
+ event = json.loads(line)
177
+ except json.JSONDecodeError:
178
+ continue
179
+ item = event.get("item") if isinstance(event, dict) else None
180
+ if not isinstance(item, dict) or item.get("type") != "command_execution":
181
+ continue
182
+ command = item.get("command")
183
+ if not isinstance(command, str):
184
+ continue
185
+ normalized = command.casefold()
186
+ result["shell_command_calls"] += 1
187
+ # One command-execution event is one observed tool-loop operation.
188
+ # This is a derived churn counter, not token attribution and does
189
+ # not require a storage-schema column.
190
+ result["tool_loop_operations"] += 1
191
+ is_test = bool(re.search(r"\b(?:pytest|unittest|tox|nox|playwright)\b", normalized))
192
+ if is_test:
193
+ result["test_commands"] += 1
194
+ if re.search(r"\b(?:rg|grep)\b", normalized):
195
+ result["grep_calls"] += 1
196
+ if re.search(r"\b(?:find|rg\s+--files|glob)\b", normalized):
197
+ result["glob_search_calls"] += 1
198
+ if re.search(r"\b(?:cat|sed|head|tail|less|awk)\b", normalized):
199
+ # A command may read several paths but retaining them would be sensitive.
200
+ fingerprint = re.sub(r"\s+", " ", command.strip())[:512]
201
+ result["file_read_count"] += 1
202
+ if fingerprint in reads:
203
+ result["repeated_file_read_count"] += 1
204
+ reads.add(fingerprint)
205
+ raw = item.get("aggregated_output", item.get("output", ""))
206
+ size = len(raw.encode("utf-8")) if isinstance(raw, str) else 0
207
+ result["tool_output_bytes"] += size
208
+ result["maximum_tool_output_bytes"] = max(result["maximum_tool_output_bytes"], size)
209
+ if is_test:
210
+ if item.get("exit_code") == 0:
211
+ result["passing_test_output_bytes"] += size
212
+ else:
213
+ result["failed_test_diagnostic_bytes"] += size
214
+ if re.search(r"\b(?:git)\b", normalized):
215
+ result["git_output_bytes"] += size
216
+ if re.search(r"\b(?:gh)\b", normalized):
217
+ result["github_output_bytes"] += size
218
+ if re.search(r"\bgit\s+(?:log|blame)\b", normalized):
219
+ historical_observed = True
220
+ result["historical_commit_queries"] += 1
221
+ result["historical_commit_results"] += len(raw.splitlines()) if isinstance(raw, str) else 0
222
+ result["historical_context_bytes"] += size
223
+ if re.search(r"\bgh\s+(?:pr\s+list|search\s+prs)\b", normalized):
224
+ historical_observed = True
225
+ result["historical_pr_queries"] += 1
226
+ result["historical_pr_results"] += len(raw.splitlines()) if isinstance(raw, str) else 0
227
+ result["historical_context_bytes"] += size
228
+ result["distinct_files_read"] = len(reads)
229
+ if not historical_observed:
230
+ for key in (
231
+ "historical_commit_queries", "historical_commit_results", "historical_pr_queries",
232
+ "historical_pr_results", "historical_context_bytes",
233
+ ):
234
+ result.pop(key)
235
+ return result
236
+
237
+
238
+ def credit_estimate(model: object, usage: Mapping[str, object]) -> dict[str, float | str | None]:
239
+ key = str(model or "").casefold().strip()
240
+ rates = RATE_TABLE.get(key)
241
+ if rates is None:
242
+ return {"rate_table_version": RATE_TABLE_VERSION, "credits": None, "eur": None}
243
+ uncached = _number(usage.get("uncached_input_tokens"))
244
+ cached = _number(usage.get("cached_input_tokens"))
245
+ output = _number(usage.get("output_tokens"))
246
+ if any(value is None for value in (uncached, cached, output)):
247
+ return {"rate_table_version": RATE_TABLE_VERSION, "credits": None, "eur": None}
248
+ credits = (
249
+ int(uncached) * rates["uncached_input"]
250
+ + int(cached) * rates["cached_input"]
251
+ + int(output) * rates["output"]
252
+ ) / 1_000_000
253
+ return {
254
+ "rate_table_version": RATE_TABLE_VERSION,
255
+ "credits": round(credits, 8),
256
+ "eur": round(credits * EUR_PER_CREDIT, 8),
257
+ }
258
+
259
+
260
+ @dataclass(frozen=True)
261
+ class ProviderInvocation:
262
+ run_id: str
263
+ ordinal: int
264
+ provider: str
265
+ model: str | None
266
+ phase: str
267
+ role: str
268
+ started_at: str
269
+ completed_at: str | None
270
+ duration_ms: int | None
271
+ usage: Mapping[str, object]
272
+ model_authority: str = UNAVAILABLE
273
+ raw_provider_model: str | None = None
274
+ runtime_metadata: Mapping[str, object] | None = None
275
+ retry_ordinal: int = 0
276
+ churn: Mapping[str, object] | None = None
277
+ invocation_id: str | None = None
278
+ usage_snapshots: tuple[Mapping[str, object], ...] = ()
279
+
280
+
281
+ def persist_provider_invocation(root: Path, invocation: ProviderInvocation, *, central_database: Path | None = None) -> str:
282
+ """Append one immutable provider invocation; unknowns remain NULL, never zero."""
283
+ usage = dict(invocation.usage)
284
+ input_tokens = _number(usage.get("input_tokens"))
285
+ cached = _number(usage.get("cached_input_tokens"))
286
+ uncached = (
287
+ input_tokens - cached
288
+ if input_tokens is not None and cached is not None and cached <= input_tokens
289
+ else None
290
+ )
291
+ output = _number(usage.get("output_tokens"))
292
+ reasoning = _number(usage.get("reasoning_tokens"))
293
+ total = _number(usage.get("total_tokens"))
294
+ authority = (
295
+ AUTHORITATIVE
296
+ if any(value is not None for value in (input_tokens, cached, output, reasoning, total))
297
+ else UNAVAILABLE
298
+ )
299
+ model_authority = (
300
+ invocation.model_authority
301
+ if invocation.model_authority in {AUTHORITATIVE, DERIVED, UNAVAILABLE}
302
+ else UNAVAILABLE
303
+ )
304
+ model = invocation.model if model_authority != UNAVAILABLE else None
305
+ estimate = credit_estimate(
306
+ model,
307
+ {"uncached_input_tokens": uncached, "cached_input_tokens": cached, "output_tokens": output},
308
+ )
309
+ identifier = (
310
+ invocation.invocation_id or f"{invocation.run_id}-{invocation.ordinal}-{uuid4().hex[:12]}"
311
+ )
312
+ # Invocation churn is normally numeric aggregation. A provider turn that
313
+ # never returns an AgentResult additionally needs one small, deterministic
314
+ # diagnostic to let the watcher recover the same terminal outcome after a
315
+ # host interruption. Keep this allow-list deliberately narrow: arbitrary
316
+ # provider output is never retained here.
317
+ churn: dict[str, int | str] = {}
318
+ for key, value in (invocation.churn or {}).items():
319
+ number = _number(value)
320
+ if number is not None:
321
+ churn[key] = number
322
+ elif key in _SAFE_CHURN_TEXT_FIELDS and isinstance(value, str):
323
+ compact = redact_diagnostic(value, limit=120)
324
+ if compact:
325
+ churn[key] = compact
326
+ snapshots = tuple(
327
+ {
328
+ key: _number(snapshot.get(key))
329
+ for key in ("input_tokens", "cached_input_tokens", "output_tokens", "reasoning_tokens", "total_tokens")
330
+ }
331
+ for snapshot in invocation.usage_snapshots
332
+ if isinstance(snapshot, Mapping)
333
+ )
334
+ if central_database is None:
335
+ connection = open_storage(root)
336
+ else:
337
+ database = central_database.resolve()
338
+ if not database.is_file():
339
+ raise RuntimeError("CENTRAL provider-usage database is unavailable")
340
+ connection = sqlite3.connect(database, isolation_level=None)
341
+ connection.execute("PRAGMA foreign_keys=ON")
342
+ try:
343
+ connection.execute(
344
+ """INSERT OR IGNORE INTO provider_invocations(
345
+ invocation_id,run_id,ordinal,provider,model,model_authority,raw_provider_model,phase,role,started_at,completed_at,duration_ms,
346
+ input_tokens,cached_input_tokens,uncached_input_tokens,output_tokens,reasoning_tokens,total_tokens,
347
+ usage_authority,speed_state,retry_ordinal,estimated_credits,estimated_eur,rate_table_version,churn
348
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
349
+ (
350
+ identifier,
351
+ invocation.run_id,
352
+ invocation.ordinal,
353
+ invocation.provider,
354
+ model,
355
+ model_authority,
356
+ invocation.raw_provider_model,
357
+ invocation.phase,
358
+ invocation.role,
359
+ invocation.started_at,
360
+ invocation.completed_at,
361
+ invocation.duration_ms,
362
+ input_tokens,
363
+ cached,
364
+ uncached,
365
+ output,
366
+ reasoning,
367
+ total,
368
+ authority,
369
+ speed_state(invocation.runtime_metadata),
370
+ invocation.retry_ordinal,
371
+ estimate["credits"],
372
+ estimate["eur"],
373
+ RATE_TABLE_VERSION,
374
+ json.dumps(churn, sort_keys=True, separators=(",", ":")),
375
+ ),
376
+ )
377
+ previous: dict[str, int | None] | None = None
378
+ for ordinal, snapshot in enumerate(snapshots, 1):
379
+ input_tokens = snapshot["input_tokens"]
380
+ cached_input_tokens = snapshot["cached_input_tokens"]
381
+ snapshot["uncached_input_tokens"] = (
382
+ input_tokens - cached_input_tokens
383
+ if input_tokens is not None and cached_input_tokens is not None
384
+ and cached_input_tokens <= input_tokens
385
+ else None
386
+ )
387
+ def delta(key: str) -> int | None:
388
+ if previous is None or snapshot[key] is None or previous[key] is None:
389
+ return None
390
+ return snapshot[key] - previous[key] if snapshot[key] >= previous[key] else None
391
+ connection.execute(
392
+ """INSERT OR IGNORE INTO provider_usage_snapshots(
393
+ invocation_id,ordinal,input_tokens,cached_input_tokens,uncached_input_tokens,
394
+ output_tokens,reasoning_tokens,total_tokens,input_delta,cached_input_delta,
395
+ uncached_input_delta,output_delta
396
+ ) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)""",
397
+ (identifier, ordinal, snapshot["input_tokens"], snapshot["cached_input_tokens"],
398
+ snapshot["uncached_input_tokens"], snapshot["output_tokens"], snapshot["reasoning_tokens"],
399
+ snapshot["total_tokens"], delta("input_tokens"), delta("cached_input_tokens"),
400
+ delta("uncached_input_tokens"), delta("output_tokens")),
401
+ )
402
+ previous = snapshot
403
+ finally:
404
+ connection.close()
405
+ return identifier
406
+
407
+
408
+ def provider_usage_summary(root: Path, run_id: str, *, central_database: Path | None = None) -> dict[str, object]:
409
+ """Derive run-level totals without treating cumulative input as context size."""
410
+ if central_database is None:
411
+ connection = open_storage(root)
412
+ else:
413
+ database = central_database.resolve()
414
+ if not database.is_file():
415
+ raise RuntimeError("CENTRAL provider-usage database is unavailable")
416
+ connection = sqlite3.connect(database, isolation_level=None)
417
+ connection.execute("PRAGMA foreign_keys=ON")
418
+ try:
419
+ rows = connection.execute(
420
+ "SELECT provider,model,model_authority,raw_provider_model,input_tokens,cached_input_tokens,uncached_input_tokens,output_tokens,duration_ms,estimated_credits,estimated_eur,speed_state,usage_authority,churn,role FROM provider_invocations WHERE run_id=? ORDER BY ordinal",
421
+ (run_id,),
422
+ ).fetchall()
423
+ snapshot_rows = connection.execute(
424
+ "SELECT input_delta,cached_input_delta,uncached_input_delta,output_delta FROM provider_usage_snapshots WHERE invocation_id IN (SELECT invocation_id FROM provider_invocations WHERE run_id=?)",
425
+ (run_id,),
426
+ ).fetchall()
427
+ finally:
428
+ connection.close()
429
+ if not rows:
430
+ return {"invocation_detail": UNAVAILABLE}
431
+ inputs = [row[4] for row in rows if isinstance(row[4], int)]
432
+ churn: dict[str, int | str] = {}
433
+ for row in rows:
434
+ try:
435
+ values = json.loads(row[13])
436
+ except (TypeError, json.JSONDecodeError):
437
+ values = {}
438
+ if isinstance(values, dict):
439
+ for key, value in values.items():
440
+ if isinstance(value, int):
441
+ previous = churn.get(key, 0)
442
+ churn[key] = (previous if isinstance(previous, int) else 0) + value
443
+ elif key in _SAFE_CHURN_TEXT_FIELDS and isinstance(value, str):
444
+ # Scope is invocation evidence, not an aggregate. The
445
+ # last invocation is the effective run projection.
446
+ churn[key] = value
447
+
448
+ def total(index: int) -> int | float | None:
449
+ values = [row[index] for row in rows if isinstance(row[index], (int, float))]
450
+ return sum(values) if values else None
451
+
452
+ ordered = sorted(inputs)
453
+ p95 = ordered[min(len(ordered) - 1, ceil(len(ordered) * 0.95) - 1)] if ordered else None
454
+ input_deltas = [row[0] for row in snapshot_rows if isinstance(row[0], int)]
455
+ calls_by_role: dict[str, int] = {}
456
+ uncached_input_by_role: dict[str, int] = {}
457
+ for row in rows:
458
+ role = row[14] if isinstance(row[14], str) and row[14] else "UNSPECIFIED"
459
+ calls_by_role[role] = calls_by_role.get(role, 0) + 1
460
+ if isinstance(row[6], int):
461
+ uncached_input_by_role[role] = uncached_input_by_role.get(role, 0) + row[6]
462
+ observed_history = any(key in churn for key in (
463
+ "historical_commit_queries", "historical_pr_queries", "historical_context_bytes"
464
+ ))
465
+ return {
466
+ "invocation_detail": AUTHORITATIVE,
467
+ "provider_invocation_count": len(rows),
468
+ "provider_invocations_by_role": calls_by_role,
469
+ "uncached_input_by_role": uncached_input_by_role or None,
470
+ "input_tokens": total(4),
471
+ "cached_input_tokens": total(5),
472
+ "uncached_input_tokens": total(6),
473
+ "output_tokens": total(7),
474
+ "total_provider_execution_ms": total(8),
475
+ "max_input_tokens_per_invocation": max(inputs) if inputs else None,
476
+ "median_input_tokens_per_invocation": median(inputs) if inputs else None,
477
+ "p95_input_tokens_per_invocation": p95,
478
+ "estimated_credits": total(9),
479
+ "estimated_eur": total(10),
480
+ "rate_table_version": RATE_TABLE_VERSION,
481
+ "speed_state": next((row[11] for row in rows if row[11] != "UNKNOWN"), "UNKNOWN"),
482
+ "usage_authority": AUTHORITATIVE
483
+ if any(row[12] == AUTHORITATIVE for row in rows)
484
+ else UNAVAILABLE,
485
+ "context_churn": churn,
486
+ "historical_context_metrics_authority": AUTHORITATIVE if observed_history else UNAVAILABLE,
487
+ "historical_commit_queries": churn.get("historical_commit_queries") if observed_history else None,
488
+ "historical_commit_results": churn.get("historical_commit_results") if observed_history else None,
489
+ "historical_pr_queries": churn.get("historical_pr_queries") if observed_history else None,
490
+ "historical_pr_results": churn.get("historical_pr_results") if observed_history else None,
491
+ "historical_context_bytes": churn.get("historical_context_bytes") if observed_history else None,
492
+ "usage_snapshot_count": len(snapshot_rows) or None,
493
+ "intermediate_usage_delta_available": bool(input_deltas),
494
+ "maximum_incremental_input_tokens": max(input_deltas) if input_deltas else None,
495
+ "actual_single_request_context_size": UNAVAILABLE,
496
+ "active_context_size": UNAVAILABLE,
497
+ }