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.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- 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
|
+
}
|