agent-lifecycle-kit 1.50.1__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.
- agent_lifecycle/__init__.py +5 -0
- agent_lifecycle/__main__.py +5 -0
- agent_lifecycle/_version.py +3 -0
- agent_lifecycle/adapter_sessions/__init__.py +41 -0
- agent_lifecycle/adapter_sessions/contracts.py +133 -0
- agent_lifecycle/adapter_sessions/env.py +44 -0
- agent_lifecycle/adapter_sessions/launcher.py +101 -0
- agent_lifecycle/adapter_sessions/process.py +39 -0
- agent_lifecycle/adapter_sessions/redaction.py +30 -0
- agent_lifecycle/adapter_sessions/session_store.py +64 -0
- agent_lifecycle/adapter_sessions/task_intake.py +450 -0
- agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
- agent_lifecycle/audit/__init__.py +67 -0
- agent_lifecycle/audit/bug_forensics.py +94 -0
- agent_lifecycle/audit/implementation.py +516 -0
- agent_lifecycle/audit/ownership.py +119 -0
- agent_lifecycle/audit/proof_integrity.py +814 -0
- agent_lifecycle/audit/review_verdict.py +116 -0
- agent_lifecycle/changesets/__init__.py +5 -0
- agent_lifecycle/changesets/git.py +35 -0
- agent_lifecycle/cli/__init__.py +5 -0
- agent_lifecycle/cli/adapter.py +360 -0
- agent_lifecycle/cli/dispatch.py +1220 -0
- agent_lifecycle/cli/followup.py +89 -0
- agent_lifecycle/cli/main.py +41 -0
- agent_lifecycle/cli/metrics_parser.py +49 -0
- agent_lifecycle/cli/parsers.py +618 -0
- agent_lifecycle/cli/policy.py +101 -0
- agent_lifecycle/cli/progress_hooks.py +117 -0
- agent_lifecycle/cli/worktree.py +74 -0
- agent_lifecycle/compiler/__init__.py +17 -0
- agent_lifecycle/compiler/small_model_packets.py +322 -0
- agent_lifecycle/compiler/task_packets.py +229 -0
- agent_lifecycle/context/__init__.py +20 -0
- agent_lifecycle/context/episode_retrieval.py +30 -0
- agent_lifecycle/context/external_memory.py +71 -0
- agent_lifecycle/context/profiles.py +104 -0
- agent_lifecycle/context/rendering.py +226 -0
- agent_lifecycle/contracts/__init__.py +25 -0
- agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
- agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
- agent_lifecycle/contracts/audit_schemas.py +133 -0
- agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
- agent_lifecycle/contracts/canonical.py +55 -0
- agent_lifecycle/contracts/compatibility.py +242 -0
- agent_lifecycle/contracts/context_model_schemas.py +265 -0
- agent_lifecycle/contracts/core_schemas.py +252 -0
- agent_lifecycle/contracts/cross_check_schemas.py +121 -0
- agent_lifecycle/contracts/errors.py +26 -0
- agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
- agent_lifecycle/contracts/host_capability_schemas.py +64 -0
- agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
- agent_lifecycle/contracts/metric_schemas.py +315 -0
- agent_lifecycle/contracts/paths.py +31 -0
- agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
- agent_lifecycle/contracts/policy_schemas.py +350 -0
- agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
- agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
- agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
- agent_lifecycle/contracts/release_contract_schemas.py +476 -0
- agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
- agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
- agent_lifecycle/contracts/review_quality_schemas.py +109 -0
- agent_lifecycle/contracts/runner_schemas.py +91 -0
- agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
- agent_lifecycle/contracts/sandbox_schemas.py +159 -0
- agent_lifecycle/contracts/schema_builders.py +23 -0
- agent_lifecycle/contracts/schemas.py +93 -0
- agent_lifecycle/contracts/status_goal_schemas.py +469 -0
- agent_lifecycle/contracts/task_template_schemas.py +96 -0
- agent_lifecycle/contracts/usage_export_schemas.py +68 -0
- agent_lifecycle/diagnostics/__init__.py +17 -0
- agent_lifecycle/diagnostics/bundles.py +124 -0
- agent_lifecycle/diagnostics/readiness.py +733 -0
- agent_lifecycle/evidence_index/__init__.py +39 -0
- agent_lifecycle/evidence_index/core.py +272 -0
- agent_lifecycle/evidence_index/episode_index.py +329 -0
- agent_lifecycle/evidence_index/external_context.py +276 -0
- agent_lifecycle/followup/__init__.py +23 -0
- agent_lifecycle/followup/records.py +404 -0
- agent_lifecycle/freeze/__init__.py +5 -0
- agent_lifecycle/freeze/locks.py +23 -0
- agent_lifecycle/goal/__init__.py +15 -0
- agent_lifecycle/goal/records.py +244 -0
- agent_lifecycle/goal/view.py +109 -0
- agent_lifecycle/host_protocol/__init__.py +65 -0
- agent_lifecycle/host_protocol/acp_capability.py +195 -0
- agent_lifecycle/host_protocol/capabilities.py +268 -0
- agent_lifecycle/host_protocol/contracts.py +214 -0
- agent_lifecycle/host_protocol/event_capture.py +390 -0
- agent_lifecycle/host_protocol/events.py +196 -0
- agent_lifecycle/host_protocol/inspection.py +194 -0
- agent_lifecycle/host_protocol/inspection_common.py +310 -0
- agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
- agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
- agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
- agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
- agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
- agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
- agent_lifecycle/host_protocol/receipts.py +58 -0
- agent_lifecycle/host_protocol/scaffold.py +329 -0
- agent_lifecycle/host_protocol/validation.py +229 -0
- agent_lifecycle/imports/__init__.py +76 -0
- agent_lifecycle/imports/agentskills_profile.py +54 -0
- agent_lifecycle/imports/bmad_profile.py +49 -0
- agent_lifecycle/imports/constitution_adr.py +91 -0
- agent_lifecycle/imports/dialect_profiles.py +121 -0
- agent_lifecycle/imports/external_agent.py +28 -0
- agent_lifecycle/imports/external_dialects.py +486 -0
- agent_lifecycle/imports/external_workflow.py +28 -0
- agent_lifecycle/imports/multi_markdown.py +111 -0
- agent_lifecycle/imports/openspec_profile.py +49 -0
- agent_lifecycle/imports/planning.py +536 -0
- agent_lifecycle/imports/spec_kit_profile.py +49 -0
- agent_lifecycle/imports/spec_kitty_profile.py +49 -0
- agent_lifecycle/metrics/__init__.py +60 -0
- agent_lifecycle/metrics/cost_collection.py +315 -0
- agent_lifecycle/metrics/costs.py +203 -0
- agent_lifecycle/metrics/outcome_index.py +330 -0
- agent_lifecycle/metrics/phase_resources.py +208 -0
- agent_lifecycle/metrics/recommendations.py +507 -0
- agent_lifecycle/metrics/regression_signals.py +64 -0
- agent_lifecycle/metrics/usage_export.py +390 -0
- agent_lifecycle/model_routing/__init__.py +19 -0
- agent_lifecycle/model_routing/profiles.py +323 -0
- agent_lifecycle/model_routing/receipts.py +140 -0
- agent_lifecycle/model_routing/resolver.py +566 -0
- agent_lifecycle/neutrality/__init__.py +12 -0
- agent_lifecycle/neutrality/__main__.py +6 -0
- agent_lifecycle/neutrality/authority.py +265 -0
- agent_lifecycle/neutrality/canonical.py +28 -0
- agent_lifecycle/neutrality/cli.py +294 -0
- agent_lifecycle/neutrality/ed25519.py +124 -0
- agent_lifecycle/neutrality/errors.py +89 -0
- agent_lifecycle/neutrality/gate.py +269 -0
- agent_lifecycle/neutrality/paths.py +92 -0
- agent_lifecycle/neutrality/policy.py +76 -0
- agent_lifecycle/neutrality/receipt.py +135 -0
- agent_lifecycle/neutrality/scanner.py +310 -0
- agent_lifecycle/neutrality/test_runner.py +116 -0
- agent_lifecycle/planning/__init__.py +47 -0
- agent_lifecycle/planning/acceptance_markdown.py +159 -0
- agent_lifecycle/planning/completeness.py +362 -0
- agent_lifecycle/planning/continuity.py +255 -0
- agent_lifecycle/planning/templates.py +294 -0
- agent_lifecycle/planning/tiering.py +87 -0
- agent_lifecycle/planning/validation.py +72 -0
- agent_lifecycle/policy/__init__.py +37 -0
- agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
- agent_lifecycle/policy/apply.py +44 -0
- agent_lifecycle/policy/proposals.py +189 -0
- agent_lifecycle/policy/quality_floor.py +144 -0
- agent_lifecycle/policy/runtime_receipts.py +160 -0
- agent_lifecycle/py.typed +1 -0
- agent_lifecycle/quality/__init__.py +89 -0
- agent_lifecycle/quality/bug_forensics.py +727 -0
- agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
- agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
- agent_lifecycle/quality/cross_check.py +437 -0
- agent_lifecycle/quality/failure_classification.py +318 -0
- agent_lifecycle/quality/packs.py +265 -0
- agent_lifecycle/reporting/__init__.py +37 -0
- agent_lifecycle/reporting/change_summary.py +156 -0
- agent_lifecycle/reporting/event_feed.py +101 -0
- agent_lifecycle/reporting/progress_bridge.py +153 -0
- agent_lifecycle/reporting/progress_hooks.py +161 -0
- agent_lifecycle/reporting/progress_terminal.py +95 -0
- agent_lifecycle/reporting/progress_view.py +284 -0
- agent_lifecycle/reporting/status_view.py +125 -0
- agent_lifecycle/reporting/usage_export.py +59 -0
- agent_lifecycle/review/__init__.py +5 -0
- agent_lifecycle/review/validation.py +29 -0
- agent_lifecycle/review_mesh/__init__.py +83 -0
- agent_lifecycle/review_mesh/assignments.py +160 -0
- agent_lifecycle/review_mesh/contracts.py +685 -0
- agent_lifecycle/review_mesh/operator_templates.py +320 -0
- agent_lifecycle/review_mesh/quorum.py +38 -0
- agent_lifecycle/review_mesh/recommendation.py +487 -0
- agent_lifecycle/review_mesh/results.py +98 -0
- agent_lifecycle/review_mesh/synthesis.py +102 -0
- agent_lifecycle/runner/__init__.py +47 -0
- agent_lifecycle/runner/attempt_snapshots.py +244 -0
- agent_lifecycle/runner/core.py +653 -0
- agent_lifecycle/runner/sandbox_receipts.py +628 -0
- agent_lifecycle/specification/__init__.py +23 -0
- agent_lifecycle/specification/completion_check.py +135 -0
- agent_lifecycle/specification/completion_gate.py +501 -0
- agent_lifecycle/specification/completion_signal.py +75 -0
- agent_lifecycle/specification/validation.py +48 -0
- agent_lifecycle/workflow/__init__.py +79 -0
- agent_lifecycle/workflow/artifacts.py +74 -0
- agent_lifecycle/workflow/budget_decisions.py +370 -0
- agent_lifecycle/workflow/budget_policy.py +140 -0
- agent_lifecycle/workflow/budget_receipts.py +70 -0
- agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
- agent_lifecycle/workflow/controller.py +45 -0
- agent_lifecycle/workflow/events.py +50 -0
- agent_lifecycle/workflow/final_proof_integrity.py +74 -0
- agent_lifecycle/workflow/finalization.py +422 -0
- agent_lifecycle/workflow/gates.py +185 -0
- agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
- agent_lifecycle/workflow/leases.py +220 -0
- agent_lifecycle/workflow/lineage.py +108 -0
- agent_lifecycle/workflow/managed_runner.py +167 -0
- agent_lifecycle/workflow/model_usage.py +106 -0
- agent_lifecycle/workflow/next_action.py +86 -0
- agent_lifecycle/workflow/operation_kernel.py +98 -0
- agent_lifecycle/workflow/plan_adoption.py +439 -0
- agent_lifecycle/workflow/query.py +52 -0
- agent_lifecycle/workflow/review_mesh_gate.py +93 -0
- agent_lifecycle/workflow/reviews.py +168 -0
- agent_lifecycle/workflow/run_transitions.py +163 -0
- agent_lifecycle/workflow/sandbox_policy.py +214 -0
- agent_lifecycle/workflow/selectors.py +57 -0
- agent_lifecycle/workflow/state.py +130 -0
- agent_lifecycle/workflow/task_transitions.py +421 -0
- agent_lifecycle/worktree/__init__.py +17 -0
- agent_lifecycle/worktree/isolation.py +407 -0
- agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
- agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Lifecycle resource and process-cost metrics."""
|
|
2
|
+
|
|
3
|
+
from agent_lifecycle.metrics.costs import (
|
|
4
|
+
COST_CATEGORIES,
|
|
5
|
+
DEFAULT_MODE_LIMITS,
|
|
6
|
+
require_lifecycle_cost_pass,
|
|
7
|
+
validate_lifecycle_cost_report,
|
|
8
|
+
)
|
|
9
|
+
from agent_lifecycle.metrics.cost_collection import build_lifecycle_cost_summary, generate_lifecycle_cost_report
|
|
10
|
+
from agent_lifecycle.metrics.phase_resources import (
|
|
11
|
+
build_phase_resource_measurement,
|
|
12
|
+
require_phase_resource_measurement_pass,
|
|
13
|
+
validate_phase_resource_measurement,
|
|
14
|
+
)
|
|
15
|
+
from agent_lifecycle.metrics.outcome_index import (
|
|
16
|
+
build_quality_cost_signal_summary,
|
|
17
|
+
build_quality_cost_signals,
|
|
18
|
+
build_task_outcome_index,
|
|
19
|
+
)
|
|
20
|
+
from agent_lifecycle.metrics.recommendations import (
|
|
21
|
+
build_lifecycle_recommendation_summary,
|
|
22
|
+
recommend_lifecycle_mode,
|
|
23
|
+
recommend_from_quality_cost_signals,
|
|
24
|
+
require_lifecycle_recommendation_pass,
|
|
25
|
+
summarize_lifecycle_overhead,
|
|
26
|
+
validate_lifecycle_baselines,
|
|
27
|
+
)
|
|
28
|
+
from agent_lifecycle.metrics.regression_signals import summarize_regression_signals
|
|
29
|
+
from agent_lifecycle.metrics.usage_export import (
|
|
30
|
+
build_usage_export,
|
|
31
|
+
require_usage_export_pass,
|
|
32
|
+
usage_export_totals,
|
|
33
|
+
validate_usage_export,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"COST_CATEGORIES",
|
|
38
|
+
"DEFAULT_MODE_LIMITS",
|
|
39
|
+
"build_usage_export",
|
|
40
|
+
"build_phase_resource_measurement",
|
|
41
|
+
"build_lifecycle_cost_summary",
|
|
42
|
+
"build_lifecycle_recommendation_summary",
|
|
43
|
+
"build_quality_cost_signal_summary",
|
|
44
|
+
"build_quality_cost_signals",
|
|
45
|
+
"build_task_outcome_index",
|
|
46
|
+
"generate_lifecycle_cost_report",
|
|
47
|
+
"recommend_from_quality_cost_signals",
|
|
48
|
+
"recommend_lifecycle_mode",
|
|
49
|
+
"require_lifecycle_cost_pass",
|
|
50
|
+
"require_lifecycle_recommendation_pass",
|
|
51
|
+
"require_phase_resource_measurement_pass",
|
|
52
|
+
"require_usage_export_pass",
|
|
53
|
+
"summarize_lifecycle_overhead",
|
|
54
|
+
"summarize_regression_signals",
|
|
55
|
+
"usage_export_totals",
|
|
56
|
+
"validate_lifecycle_baselines",
|
|
57
|
+
"validate_lifecycle_cost_report",
|
|
58
|
+
"validate_phase_resource_measurement",
|
|
59
|
+
"validate_usage_export",
|
|
60
|
+
]
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Generate lifecycle cost reports from explicit local artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_bytes, canonical_digest, load_json_object, sha256_hex
|
|
9
|
+
from agent_lifecycle.metrics.costs import (
|
|
10
|
+
COST_CATEGORIES,
|
|
11
|
+
COST_REPORT_SCHEMA,
|
|
12
|
+
COST_SUMMARY_SCHEMA,
|
|
13
|
+
cost_entry_totals,
|
|
14
|
+
cost_ratios,
|
|
15
|
+
summarize_usage_confidence,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate_lifecycle_cost_report(
|
|
20
|
+
*,
|
|
21
|
+
artifact_paths: list[Path],
|
|
22
|
+
mode: str = "standard",
|
|
23
|
+
root: Path | None = None,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Generate a cost report from explicit JSON artifacts without host calls."""
|
|
26
|
+
|
|
27
|
+
if not artifact_paths:
|
|
28
|
+
raise LifecycleError("cost-report-artifacts-required", "at least one artifact path is required")
|
|
29
|
+
project_root = (root or Path.cwd()).resolve()
|
|
30
|
+
source_artifacts: list[dict[str, Any]] = []
|
|
31
|
+
entries: list[dict[str, Any]] = []
|
|
32
|
+
payloads: list[dict[str, Any]] = []
|
|
33
|
+
for index, raw_path in enumerate(artifact_paths):
|
|
34
|
+
payload, source = _load_source_artifact(raw_path, project_root)
|
|
35
|
+
source_artifacts.append(source)
|
|
36
|
+
entries.append(_entry_from_artifact(index, source, payload))
|
|
37
|
+
payloads.append(payload)
|
|
38
|
+
|
|
39
|
+
report = {
|
|
40
|
+
"schemaVersion": COST_REPORT_SCHEMA,
|
|
41
|
+
"mode": mode,
|
|
42
|
+
"generatedBy": "agent-lifecycle metrics cost-report",
|
|
43
|
+
"sourceArtifacts": source_artifacts,
|
|
44
|
+
"lineage": _lineage(source_artifacts, payloads),
|
|
45
|
+
"entries": entries,
|
|
46
|
+
"usageConfidence": summarize_usage_confidence(entries),
|
|
47
|
+
"productionPromotionClaimed": False,
|
|
48
|
+
}
|
|
49
|
+
report["compactSummary"] = build_lifecycle_cost_summary(report)
|
|
50
|
+
return report
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_lifecycle_cost_summary(report: dict[str, Any]) -> dict[str, Any]:
|
|
54
|
+
"""Build a small-context summary from a lifecycle cost report."""
|
|
55
|
+
|
|
56
|
+
entries = report.get("entries") if isinstance(report.get("entries"), list) else []
|
|
57
|
+
totals = cost_entry_totals(entries)
|
|
58
|
+
ratios = cost_ratios(totals)
|
|
59
|
+
usage_confidence = summarize_usage_confidence(entries)
|
|
60
|
+
source_paths = _source_paths(report)
|
|
61
|
+
missing_usage = usage_confidence["missingEntries"]
|
|
62
|
+
next_action = "review missing usage entries" if missing_usage else "continue with the selected lifecycle policy"
|
|
63
|
+
return {
|
|
64
|
+
"schemaVersion": COST_SUMMARY_SCHEMA,
|
|
65
|
+
"mode": report.get("mode"),
|
|
66
|
+
"latestUserIntent": "Keep lifecycle overhead visible without reducing task quality.",
|
|
67
|
+
"activeDecisions": [
|
|
68
|
+
f"pipelineTokenShare={ratios['pipelineTokenShare']}",
|
|
69
|
+
f"pipelineStepShare={ratios['pipelineStepShare']}",
|
|
70
|
+
f"missingUsageEntries={missing_usage}",
|
|
71
|
+
],
|
|
72
|
+
"openBlockers": [],
|
|
73
|
+
"acceptedEvidence": [
|
|
74
|
+
{
|
|
75
|
+
"id": "lifecycle-cost-report",
|
|
76
|
+
"status": "generated",
|
|
77
|
+
"sourceArtifactCount": len(source_paths),
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"changedFiles": source_paths[:20],
|
|
81
|
+
"nextRequiredAction": next_action,
|
|
82
|
+
"doNotDo": [
|
|
83
|
+
"Do not treat missing usage as zero.",
|
|
84
|
+
"Do not reduce required validation solely to improve cost ratios.",
|
|
85
|
+
],
|
|
86
|
+
"categoryTotals": totals,
|
|
87
|
+
"ratios": ratios,
|
|
88
|
+
"usageConfidence": usage_confidence,
|
|
89
|
+
"usefulWorkTokens": totals["implementation"]["tokens"] + totals["productValidation"]["tokens"],
|
|
90
|
+
"processOverheadTokens": totals["pipelineCompliance"]["tokens"] + totals["coordination"]["tokens"],
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _load_source_artifact(raw_path: Path, project_root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
95
|
+
path = raw_path if raw_path.is_absolute() else project_root / raw_path
|
|
96
|
+
display_path = _display_path(path, project_root)
|
|
97
|
+
try:
|
|
98
|
+
data = path.read_bytes()
|
|
99
|
+
except OSError as exc:
|
|
100
|
+
raise LifecycleError(
|
|
101
|
+
"cost-report-artifact-unavailable",
|
|
102
|
+
"cost report artifact cannot be read",
|
|
103
|
+
{"path": display_path, "reason": type(exc).__name__},
|
|
104
|
+
) from exc
|
|
105
|
+
payload = load_json_object(data, label=display_path)
|
|
106
|
+
source = {
|
|
107
|
+
"path": display_path,
|
|
108
|
+
"sha256": sha256_hex(data),
|
|
109
|
+
"bytes": len(data),
|
|
110
|
+
"schemaVersion": payload.get("schemaVersion"),
|
|
111
|
+
"payloadDigest": canonical_digest(payload),
|
|
112
|
+
}
|
|
113
|
+
return payload, source
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _source_paths(report: dict[str, Any]) -> list[str]:
|
|
117
|
+
return [
|
|
118
|
+
item.get("path")
|
|
119
|
+
for item in report.get("sourceArtifacts", [])
|
|
120
|
+
if isinstance(item, dict) and isinstance(item.get("path"), str)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _entry_from_artifact(index: int, source: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
|
|
125
|
+
usage = _usage_from_payload(payload)
|
|
126
|
+
entry = {
|
|
127
|
+
"id": f"artifact-{index + 1}",
|
|
128
|
+
"category": _classify_artifact(str(source.get("path")), payload),
|
|
129
|
+
"tokens": usage["tokens"],
|
|
130
|
+
"steps": usage["steps"],
|
|
131
|
+
"usageConfidence": usage["confidence"],
|
|
132
|
+
"sourcePath": source["path"],
|
|
133
|
+
"sourceSchemaVersion": source.get("schemaVersion"),
|
|
134
|
+
"sourceDigest": source["payloadDigest"],
|
|
135
|
+
}
|
|
136
|
+
for field in ("runId", "packageId", "taskId", "operationId", "planDigest", "sourceRevision"):
|
|
137
|
+
value = payload.get(field)
|
|
138
|
+
if isinstance(value, (str, int)):
|
|
139
|
+
entry[field] = value
|
|
140
|
+
return entry
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _usage_from_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
144
|
+
usage = payload.get("usage")
|
|
145
|
+
if isinstance(usage, dict):
|
|
146
|
+
billable = _int_value(usage.get("billableTokens"))
|
|
147
|
+
steps = _int_value(usage.get("toolCalls")) or 1
|
|
148
|
+
if billable is not None:
|
|
149
|
+
confidence = "ATTESTED" if _attested(payload.get("attestation")) else "ESTIMATED"
|
|
150
|
+
return {"tokens": billable, "steps": max(1, steps), "confidence": confidence}
|
|
151
|
+
return {"tokens": 0, "steps": 1, "confidence": "MISSING"}
|
|
152
|
+
for key in ("usageTotals", "counters"):
|
|
153
|
+
totals = payload.get(key)
|
|
154
|
+
if not isinstance(totals, dict):
|
|
155
|
+
continue
|
|
156
|
+
tokens = _first_int(totals, ("reportedTokens", "billableTokens", "tokens"))
|
|
157
|
+
if tokens is not None and tokens > 0:
|
|
158
|
+
steps = _first_int(totals, ("toolCalls", "validationRuns", "iterations", "steps")) or 1
|
|
159
|
+
return {"tokens": tokens, "steps": max(1, steps), "confidence": "ESTIMATED"}
|
|
160
|
+
if payload.get("schemaVersion") == "agent-lifecycle-model-usage-receipt.v1":
|
|
161
|
+
return {"tokens": 0, "steps": 1, "confidence": "MISSING"}
|
|
162
|
+
if _contains_pair(payload, "usageReceiptRequired", True):
|
|
163
|
+
return {"tokens": 0, "steps": 1, "confidence": "MISSING"}
|
|
164
|
+
return {"tokens": _estimate_tokens(payload), "steps": 1, "confidence": "ESTIMATED"}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _classify_artifact(path: str, payload: dict[str, Any]) -> str:
|
|
168
|
+
schema = str(payload.get("schemaVersion") or "")
|
|
169
|
+
if schema == "agent-lifecycle-model-usage-receipt.v1":
|
|
170
|
+
return _classify_usage_receipt(payload)
|
|
171
|
+
text = _classification_text(path, payload)
|
|
172
|
+
if schema in {"agent-workflow-state.v3", "agent-runner-state.v1"}:
|
|
173
|
+
return "coordination"
|
|
174
|
+
if schema == "agent-task-result.v2":
|
|
175
|
+
return "implementation"
|
|
176
|
+
if any(marker in text for marker in _PRODUCT_VALIDATION_MARKERS):
|
|
177
|
+
return "productValidation"
|
|
178
|
+
if any(marker in text for marker in _PIPELINE_MARKERS):
|
|
179
|
+
return "pipelineCompliance"
|
|
180
|
+
return "implementation"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _classify_usage_receipt(payload: dict[str, Any]) -> str:
|
|
184
|
+
text = _classification_text("", payload)
|
|
185
|
+
if any(marker in text for marker in _PRODUCT_VALIDATION_MARKERS):
|
|
186
|
+
return "productValidation"
|
|
187
|
+
if any(marker in text for marker in _PIPELINE_MARKERS):
|
|
188
|
+
return "pipelineCompliance"
|
|
189
|
+
return "implementation"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _classification_text(path: str, payload: dict[str, Any]) -> str:
|
|
193
|
+
return " ".join(
|
|
194
|
+
str(item)
|
|
195
|
+
for item in (
|
|
196
|
+
path,
|
|
197
|
+
payload.get("schemaVersion"),
|
|
198
|
+
payload.get("operationId"),
|
|
199
|
+
payload.get("modelClass"),
|
|
200
|
+
payload.get("stage"),
|
|
201
|
+
payload.get("role"),
|
|
202
|
+
payload.get("kind"),
|
|
203
|
+
)
|
|
204
|
+
if item is not None
|
|
205
|
+
).lower()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
_PRODUCT_VALIDATION_MARKERS = (
|
|
209
|
+
"task-review",
|
|
210
|
+
"quality",
|
|
211
|
+
"test",
|
|
212
|
+
"security-review",
|
|
213
|
+
"performance-review",
|
|
214
|
+
"specialist-review",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
_PIPELINE_MARKERS = (
|
|
218
|
+
"plan",
|
|
219
|
+
"workflow",
|
|
220
|
+
"runner",
|
|
221
|
+
"gate",
|
|
222
|
+
"neutrality",
|
|
223
|
+
"diagnostic",
|
|
224
|
+
"contract",
|
|
225
|
+
"release",
|
|
226
|
+
"cost",
|
|
227
|
+
"final-proof",
|
|
228
|
+
"handoff",
|
|
229
|
+
"followup",
|
|
230
|
+
"worktree",
|
|
231
|
+
"completion-check",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _lineage(source_artifacts: list[dict[str, Any]], payloads: list[dict[str, Any]]) -> dict[str, list[Any]]:
|
|
236
|
+
values = {
|
|
237
|
+
"runIds": set(),
|
|
238
|
+
"packageIds": set(),
|
|
239
|
+
"taskIds": set(),
|
|
240
|
+
"planDigests": set(),
|
|
241
|
+
"sourceRevisions": set(),
|
|
242
|
+
"evidenceDigests": set(),
|
|
243
|
+
"sourceArtifactDigests": {item["sha256"] for item in source_artifacts},
|
|
244
|
+
}
|
|
245
|
+
for payload in payloads:
|
|
246
|
+
_collect_lineage_values(payload, values)
|
|
247
|
+
return {key: sorted(value) for key, value in values.items()}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _collect_lineage_values(value: Any, found: dict[str, set[Any]]) -> None:
|
|
251
|
+
if isinstance(value, dict):
|
|
252
|
+
for key, item in value.items():
|
|
253
|
+
if key == "runId" and isinstance(item, str):
|
|
254
|
+
found["runIds"].add(item)
|
|
255
|
+
elif key == "packageId" and isinstance(item, str):
|
|
256
|
+
found["packageIds"].add(item)
|
|
257
|
+
elif key == "taskId" and isinstance(item, str):
|
|
258
|
+
found["taskIds"].add(item)
|
|
259
|
+
elif key == "id" and isinstance(item, str) and item.startswith("WS-"):
|
|
260
|
+
found["taskIds"].add(item)
|
|
261
|
+
elif key == "planDigest" and isinstance(item, str):
|
|
262
|
+
found["planDigests"].add(item)
|
|
263
|
+
elif key == "sourceRevision" and isinstance(item, str):
|
|
264
|
+
found["sourceRevisions"].add(item)
|
|
265
|
+
elif _is_evidence_digest(key, item):
|
|
266
|
+
found["evidenceDigests"].add(item)
|
|
267
|
+
_collect_lineage_values(item, found)
|
|
268
|
+
elif isinstance(value, list):
|
|
269
|
+
for item in value:
|
|
270
|
+
_collect_lineage_values(item, found)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _is_evidence_digest(key: str, value: Any) -> bool:
|
|
274
|
+
keys = {"evidenceDigest", "resultDigest", "reviewDigest", "validationDigest", "finalProofDigest", "sha256"}
|
|
275
|
+
return key in keys and isinstance(value, str)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _contains_pair(value: Any, key: str, expected: Any) -> bool:
|
|
279
|
+
if isinstance(value, dict):
|
|
280
|
+
return any(
|
|
281
|
+
(item_key == key and item_value == expected) or _contains_pair(item_value, key, expected)
|
|
282
|
+
for item_key, item_value in value.items()
|
|
283
|
+
)
|
|
284
|
+
if isinstance(value, list):
|
|
285
|
+
return any(_contains_pair(item, key, expected) for item in value)
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _attested(value: Any) -> bool:
|
|
290
|
+
return isinstance(value, dict) and value.get("status") == "ATTESTED"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _first_int(payload: dict[str, Any], keys: tuple[str, ...]) -> int | None:
|
|
294
|
+
for key in keys:
|
|
295
|
+
value = _int_value(payload.get(key))
|
|
296
|
+
if value is not None:
|
|
297
|
+
return value
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _int_value(value: Any) -> int | None:
|
|
302
|
+
if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
|
|
303
|
+
return value
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _estimate_tokens(value: Any) -> int:
|
|
308
|
+
return max(1, (len(canonical_bytes(value)) + 3) // 4)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _display_path(path: Path, root: Path) -> str:
|
|
312
|
+
try:
|
|
313
|
+
return path.resolve().relative_to(root).as_posix()
|
|
314
|
+
except ValueError:
|
|
315
|
+
return path.name
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Validate lifecycle cost reports for resource-disciplined runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from agent_lifecycle.contracts import LifecycleError, canonical_digest
|
|
8
|
+
|
|
9
|
+
COST_REPORT_SCHEMA = "agent-lifecycle-cost-report.v1"
|
|
10
|
+
COST_VALIDATION_SCHEMA = "agent-lifecycle-cost-validation.v1"
|
|
11
|
+
COST_GENERATION_SCHEMA = "agent-lifecycle-cost-generation.v1"
|
|
12
|
+
COST_SUMMARY_SCHEMA = "agent-lifecycle-cost-summary.v1"
|
|
13
|
+
|
|
14
|
+
COST_CATEGORIES = ("implementation", "productValidation", "pipelineCompliance", "coordination")
|
|
15
|
+
USAGE_CONFIDENCE_STATES = ("ATTESTED", "ESTIMATED", "MISSING")
|
|
16
|
+
|
|
17
|
+
DEFAULT_MODE_LIMITS: dict[str, dict[str, float | int]] = {
|
|
18
|
+
"light": {"maxPipelineTokenShare": 0.20, "maxPipelineStepShare": 0.30, "maxPipelineTokens": 1500, "maxPipelineSteps": 3},
|
|
19
|
+
"standard": {"maxPipelineTokenShare": 0.30, "maxPipelineStepShare": 0.40, "maxPipelineTokens": 6000, "maxPipelineSteps": 6},
|
|
20
|
+
"strict": {"maxPipelineTokenShare": 0.45, "maxPipelineStepShare": 0.55, "maxPipelineTokens": 20000, "maxPipelineSteps": 12},
|
|
21
|
+
"release": {"maxPipelineTokenShare": 0.60, "maxPipelineStepShare": 0.70, "maxPipelineTokens": 50000, "maxPipelineSteps": 24},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def validate_lifecycle_cost_report(report: dict[str, Any]) -> dict[str, Any]:
|
|
26
|
+
"""Validate a lifecycle cost report and return a compact receipt."""
|
|
27
|
+
|
|
28
|
+
blockers: list[dict[str, Any]] = []
|
|
29
|
+
if report.get("schemaVersion") != COST_REPORT_SCHEMA:
|
|
30
|
+
blockers.append({"code": "cost-report-schema", "message": "unsupported lifecycle cost report schemaVersion"})
|
|
31
|
+
if report.get("productionPromotionClaimed") is True:
|
|
32
|
+
blockers.append({"code": "cost-report-production-claim", "message": "cost reports must not claim production promotion"})
|
|
33
|
+
mode = report.get("mode")
|
|
34
|
+
if mode not in DEFAULT_MODE_LIMITS:
|
|
35
|
+
blockers.append({"code": "cost-report-mode", "mode": mode})
|
|
36
|
+
mode = None
|
|
37
|
+
entries = report.get("entries")
|
|
38
|
+
if not isinstance(entries, list) or not entries:
|
|
39
|
+
blockers.append({"code": "cost-report-entries", "message": "entries must be a non-empty array"})
|
|
40
|
+
entries = []
|
|
41
|
+
totals = _empty_totals()
|
|
42
|
+
for index, entry in enumerate(entries):
|
|
43
|
+
_add_entry(index, entry, totals, blockers)
|
|
44
|
+
totals["overall"] = {
|
|
45
|
+
"tokens": sum(totals[category]["tokens"] for category in COST_CATEGORIES),
|
|
46
|
+
"steps": sum(totals[category]["steps"] for category in COST_CATEGORIES),
|
|
47
|
+
}
|
|
48
|
+
usage_confidence = summarize_usage_confidence(entries)
|
|
49
|
+
limits = _limits(report.get("limits"), mode)
|
|
50
|
+
ratios = cost_ratios(totals)
|
|
51
|
+
if mode is not None:
|
|
52
|
+
_check_pipeline_limits(totals, ratios, limits, report.get("overLimitReason"), blockers)
|
|
53
|
+
body = {
|
|
54
|
+
"schemaVersion": COST_VALIDATION_SCHEMA,
|
|
55
|
+
"status": "PASS" if not blockers else "FAIL",
|
|
56
|
+
"mode": mode,
|
|
57
|
+
"totals": totals,
|
|
58
|
+
"ratios": ratios,
|
|
59
|
+
"usageConfidence": usage_confidence,
|
|
60
|
+
"limits": limits,
|
|
61
|
+
"blockers": blockers,
|
|
62
|
+
"reportDigest": canonical_digest(report),
|
|
63
|
+
}
|
|
64
|
+
return {**body, "validationDigest": canonical_digest(body)}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def require_lifecycle_cost_pass(validation: dict[str, Any]) -> dict[str, Any]:
|
|
68
|
+
if validation.get("status") == "FAIL":
|
|
69
|
+
raise LifecycleError("lifecycle-cost-validation-failed", "lifecycle cost validation failed", {"validation": validation})
|
|
70
|
+
return validation
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _empty_totals() -> dict[str, dict[str, int]]:
|
|
74
|
+
return {category: {"tokens": 0, "steps": 0} for category in COST_CATEGORIES}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cost_entry_totals(entries: list[Any]) -> dict[str, dict[str, int]]:
|
|
78
|
+
totals = _empty_totals()
|
|
79
|
+
for entry in entries:
|
|
80
|
+
if not isinstance(entry, dict):
|
|
81
|
+
continue
|
|
82
|
+
category = entry.get("category")
|
|
83
|
+
tokens = entry.get("tokens")
|
|
84
|
+
steps = entry.get("steps")
|
|
85
|
+
if category in COST_CATEGORIES and isinstance(tokens, int) and isinstance(steps, int):
|
|
86
|
+
totals[category]["tokens"] += max(0, tokens)
|
|
87
|
+
totals[category]["steps"] += max(0, steps)
|
|
88
|
+
totals["overall"] = {
|
|
89
|
+
"tokens": sum(totals[category]["tokens"] for category in COST_CATEGORIES),
|
|
90
|
+
"steps": sum(totals[category]["steps"] for category in COST_CATEGORIES),
|
|
91
|
+
}
|
|
92
|
+
return totals
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _add_entry(index: int, entry: Any, totals: dict[str, dict[str, int]], blockers: list[dict[str, Any]]) -> None:
|
|
96
|
+
if not isinstance(entry, dict):
|
|
97
|
+
blockers.append({"code": "cost-entry-type", "index": index, "message": "entry must be an object"})
|
|
98
|
+
return
|
|
99
|
+
category = entry.get("category")
|
|
100
|
+
if category not in COST_CATEGORIES:
|
|
101
|
+
blockers.append({"code": "cost-entry-category", "index": index, "category": category})
|
|
102
|
+
return
|
|
103
|
+
tokens = _non_negative_int(entry.get("tokens"), field="tokens", index=index, blockers=blockers)
|
|
104
|
+
steps = _non_negative_int(entry.get("steps"), field="steps", index=index, blockers=blockers)
|
|
105
|
+
confidence = entry.get("usageConfidence")
|
|
106
|
+
if confidence is not None and confidence not in USAGE_CONFIDENCE_STATES:
|
|
107
|
+
blockers.append({"code": "cost-entry-usage-confidence", "index": index, "usageConfidence": confidence})
|
|
108
|
+
if tokens is None or steps is None:
|
|
109
|
+
return
|
|
110
|
+
if tokens == 0 and steps == 0:
|
|
111
|
+
blockers.append({"code": "cost-entry-empty", "index": index})
|
|
112
|
+
return
|
|
113
|
+
totals[category]["tokens"] += tokens
|
|
114
|
+
totals[category]["steps"] += steps
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _non_negative_int(value: Any, *, field: str, index: int, blockers: list[dict[str, Any]]) -> int | None:
|
|
118
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
119
|
+
blockers.append({"code": "cost-entry-numeric", "index": index, "field": field, "value": value})
|
|
120
|
+
return None
|
|
121
|
+
return value
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def summarize_usage_confidence(entries: list[Any]) -> dict[str, int]:
|
|
125
|
+
summary = {
|
|
126
|
+
"attestedEntries": 0,
|
|
127
|
+
"attestedTokens": 0,
|
|
128
|
+
"estimatedEntries": 0,
|
|
129
|
+
"estimatedTokens": 0,
|
|
130
|
+
"missingEntries": 0,
|
|
131
|
+
"missingTokens": 0,
|
|
132
|
+
"unspecifiedEntries": 0,
|
|
133
|
+
"unspecifiedTokens": 0,
|
|
134
|
+
}
|
|
135
|
+
for entry in entries:
|
|
136
|
+
if not isinstance(entry, dict):
|
|
137
|
+
continue
|
|
138
|
+
confidence = entry.get("usageConfidence")
|
|
139
|
+
tokens = entry.get("tokens") if isinstance(entry.get("tokens"), int) else 0
|
|
140
|
+
if confidence == "ATTESTED":
|
|
141
|
+
summary["attestedEntries"] += 1
|
|
142
|
+
summary["attestedTokens"] += max(0, tokens)
|
|
143
|
+
elif confidence == "ESTIMATED":
|
|
144
|
+
summary["estimatedEntries"] += 1
|
|
145
|
+
summary["estimatedTokens"] += max(0, tokens)
|
|
146
|
+
elif confidence == "MISSING":
|
|
147
|
+
summary["missingEntries"] += 1
|
|
148
|
+
summary["missingTokens"] += max(0, tokens)
|
|
149
|
+
else:
|
|
150
|
+
summary["unspecifiedEntries"] += 1
|
|
151
|
+
summary["unspecifiedTokens"] += max(0, tokens)
|
|
152
|
+
return summary
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _limits(raw_limits: Any, mode: str | None) -> dict[str, float | int]:
|
|
156
|
+
if mode is None:
|
|
157
|
+
return {}
|
|
158
|
+
defaults = dict(DEFAULT_MODE_LIMITS[mode])
|
|
159
|
+
if not isinstance(raw_limits, dict):
|
|
160
|
+
return defaults
|
|
161
|
+
override = raw_limits.get(mode)
|
|
162
|
+
if not isinstance(override, dict):
|
|
163
|
+
return defaults
|
|
164
|
+
for key, default in defaults.items():
|
|
165
|
+
value = override.get(key)
|
|
166
|
+
if isinstance(default, float) and isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
167
|
+
defaults[key] = float(value)
|
|
168
|
+
elif isinstance(default, int) and isinstance(value, int) and not isinstance(value, bool):
|
|
169
|
+
defaults[key] = value
|
|
170
|
+
return defaults
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def cost_ratios(totals: dict[str, dict[str, int]]) -> dict[str, float]:
|
|
174
|
+
overall_tokens = totals["overall"]["tokens"]
|
|
175
|
+
overall_steps = totals["overall"]["steps"]
|
|
176
|
+
pipeline_tokens = totals["pipelineCompliance"]["tokens"]
|
|
177
|
+
pipeline_steps = totals["pipelineCompliance"]["steps"]
|
|
178
|
+
return {
|
|
179
|
+
"pipelineTokenShare": round(pipeline_tokens / overall_tokens, 6) if overall_tokens else 0.0,
|
|
180
|
+
"pipelineStepShare": round(pipeline_steps / overall_steps, 6) if overall_steps else 0.0,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _check_pipeline_limits(
|
|
185
|
+
totals: dict[str, dict[str, int]],
|
|
186
|
+
ratios: dict[str, float],
|
|
187
|
+
limits: dict[str, float | int],
|
|
188
|
+
over_limit_reason: Any,
|
|
189
|
+
blockers: list[dict[str, Any]],
|
|
190
|
+
) -> None:
|
|
191
|
+
over = []
|
|
192
|
+
if ratios["pipelineTokenShare"] > float(limits["maxPipelineTokenShare"]):
|
|
193
|
+
over.append("maxPipelineTokenShare")
|
|
194
|
+
if ratios["pipelineStepShare"] > float(limits["maxPipelineStepShare"]):
|
|
195
|
+
over.append("maxPipelineStepShare")
|
|
196
|
+
if totals["pipelineCompliance"]["tokens"] > int(limits["maxPipelineTokens"]):
|
|
197
|
+
over.append("maxPipelineTokens")
|
|
198
|
+
if totals["pipelineCompliance"]["steps"] > int(limits["maxPipelineSteps"]):
|
|
199
|
+
over.append("maxPipelineSteps")
|
|
200
|
+
if not over:
|
|
201
|
+
return
|
|
202
|
+
if not isinstance(over_limit_reason, str) or not over_limit_reason.strip():
|
|
203
|
+
blockers.append({"code": "pipeline-compliance-over-limit", "limits": sorted(set(over))})
|