agentversion 0.2.3__tar.gz → 0.2.4__tar.gz
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.
- {agentversion-0.2.3 → agentversion-0.2.4}/CHANGELOG.md +11 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/PKG-INFO +2 -2
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/cli.py +3 -1
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/diff.py +71 -8
- {agentversion-0.2.3 → agentversion-0.2.4}/pyproject.toml +6 -2
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_diff.py +85 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/.gitignore +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/CONFORMANCE.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/LICENSE +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/README.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/__init__.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/_shared.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/a2a.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/compatibility.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/constants.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/contract.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/dataset.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/decision.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/hasher.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/ids.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/manifest.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/py.typed +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/refs.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/replay.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/validator.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/environment-region-change/after.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/environment-region-change/before.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/environment-region-change/expected-diff.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/model-runtime-provider-change/after.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/model-runtime-provider-change/before.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/model-runtime-provider-change/expected-diff.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/output-schema-change/after.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/output-schema-change/before.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/output-schema-change/expected-diff.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/prompt-stack-edit/after.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/prompt-stack-edit/before.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/prompt-stack-edit/expected-diff.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/skill-registry-skill-removed/after.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/skill-registry-skill-removed/before.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/skill-registry-skill-removed/expected-diff.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/subagent-handoff-change/after.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/subagent-handoff-change/before.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/subagent-handoff-change/expected-diff.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/tool-rename/after.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/tool-rename/before.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/tool-rename/expected-diff.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/workflow-graph-change/after.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/workflow-graph-change/before.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/workflow-graph-change/expected-diff.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/examples/.gitkeep +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/examples/integrations/decimalai_bridge.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/examples/integrations/langgraph_example.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/examples/integrations/otel_mapping.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/examples/manifest/finance-agent-v1.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/examples/manifest/finance-agent-v2.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/examples/scenarios/tool-rename-drift.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/examples/scenarios/walkthrough.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/.gitkeep +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/agent-manifest.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/compatibility-batch.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/compatibility-decision.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/compatibility-policy.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/compatibility-report.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/dataset-snapshot.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/episode.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/manifest-diff.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/replay-job.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/replay-result.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/step.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/schemas/task.schema.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/a2a-mapping.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/attestation.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/behavioral-policy.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/compatibility-batch.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/compatibility-decision.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/compatibility-policy.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/data-classification.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/dataset.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/diff.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/environment.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/evaluation.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/hashing.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/ids.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/lifecycle.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/manifest.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/otel-mapping.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/reference.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/refs.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/replay-determinism.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/replay.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/spec/versioning-policy.md +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/fixtures/frozen_hash_vectors.json +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_a2a_mapping.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_audit_v020.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_audit_v030.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_behavioral_policy.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_cli.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_component_surface_map.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_conformance.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_contract.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_dataset.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_decision_replay.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_environment.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_evaluation.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_examples_runnable.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_extension_hatch.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_frozen_hash_vectors.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_hasher.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_ids.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_lifecycle.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_manifest.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_refs.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_reproducible_replay.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_trust_observability.py +0 -0
- {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_validator.py +0 -0
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
> **Package version ≠ spec version.** This file tracks the **package** version. The on-the-wire `spec_version` is independent and frozen at `1.0.0`; a pre-1.0 package can implement a stable 1.0 spec, which is exactly the situation today.
|
|
9
9
|
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.2.4] - 2026-11-10
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Filling in a `model_runtime` value the old manifest never recorded is no longer a
|
|
16
|
+
change of it.** A blank `provider`, `model` or `tool_calling_mode` becoming known grades
|
|
17
|
+
`minor` and `non_breaking`. It used to read as a provider change: `major`, `breaking`.
|
|
18
|
+
A manifest that follows the spec always carries `provider` and `model`, so this affects
|
|
19
|
+
partial baselines only. Conformance scenarios and hash vectors are unchanged.
|
|
20
|
+
|
|
10
21
|
## [0.2.3] - 2026-08-14
|
|
11
22
|
|
|
12
23
|
Packaging and documentation only. **No code, schema, or `spec_version` change** — the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentversion
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: An open specification for versioning agent runtimes and keeping datasets valid.
|
|
5
5
|
Project-URL: Homepage, https://github.com/decimal-labs/agentversion
|
|
6
6
|
Project-URL: Documentation, https://github.com/decimal-labs/agentversion/tree/main/spec
|
|
@@ -23,7 +23,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
24
|
Requires-Dist: click>=8.0
|
|
25
25
|
Requires-Dist: jcs<1,>=0.2
|
|
26
|
-
Requires-Dist: pydantic>=2.
|
|
26
|
+
Requires-Dist: pydantic>=2.1
|
|
27
27
|
Requires-Dist: rich>=13.0
|
|
28
28
|
Provides-Extra: dev
|
|
29
29
|
Requires-Dist: jsonschema>=4.0; extra == 'dev'
|
|
@@ -141,7 +141,9 @@ def init() -> None:
|
|
|
141
141
|
provider = click.prompt("Model provider (e.g. openai, anthropic, google)", type=str)
|
|
142
142
|
model = click.prompt("Model name (e.g. gpt-4o, claude-opus-4, gemini-2.0-flash)", type=str)
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
# Typed explicitly: since click 8.5 types `click.prompt` as returning str, mypy infers this literal
|
|
145
|
+
# as dict[str, Collection[str]] and rejects the nested lookups below.
|
|
146
|
+
manifest: dict[str, Any] = {
|
|
145
147
|
"spec_version": SPEC_VERSION,
|
|
146
148
|
"kind": "agent_manifest",
|
|
147
149
|
"manifest_id": mint_id("agent_manifest"),
|
|
@@ -70,6 +70,64 @@ def _max_severity(*levels: Severity) -> Severity:
|
|
|
70
70
|
# ── Per-surface severity classifiers ────────────────────
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
_PROMPT_TOKEN_RE = re.compile(r"\w+|[^\w\s]+")
|
|
74
|
+
# Past this many tokens on both sides of one changed stretch, let difflib skip
|
|
75
|
+
# its most frequent tokens again (autojunk): an unbroken run that long is rare
|
|
76
|
+
# in a prompt, and without it a repetitive one costs seconds per comparison.
|
|
77
|
+
_PROMPT_REGION_AUTOJUNK_AT = 4000
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _matched_chars(old_tokens: list[str], new_tokens: list[str]) -> int:
|
|
81
|
+
autojunk = len(old_tokens) + len(new_tokens) > _PROMPT_REGION_AUTOJUNK_AT
|
|
82
|
+
matcher = difflib.SequenceMatcher(None, old_tokens, new_tokens, autojunk=autojunk)
|
|
83
|
+
return sum(
|
|
84
|
+
len(token)
|
|
85
|
+
for block in matcher.get_matching_blocks()
|
|
86
|
+
for token in old_tokens[block.a:block.a + block.size]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _prompt_diff_pct(old_text: str, new_text: str) -> float:
|
|
91
|
+
"""Percent of the prompt's text that changed, 0-100.
|
|
92
|
+
|
|
93
|
+
Measured on words and punctuation, weighted by their characters, so
|
|
94
|
+
re-wrapping or re-indenting a prompt changes nothing and a one-word edit
|
|
95
|
+
counts about that word's share of the text. Lines are aligned first, which
|
|
96
|
+
is cheap (lines are nearly all distinct) and keeps a long prompt's edits
|
|
97
|
+
from being matched against the wrong copy of a repeated phrase; words are
|
|
98
|
+
then compared only inside the lines that changed.
|
|
99
|
+
|
|
100
|
+
It used to be ``difflib.SequenceMatcher(None, old, new).ratio()`` over
|
|
101
|
+
characters. SequenceMatcher's default "autojunk" ignores every element
|
|
102
|
+
that makes up more than 1% of a sequence of 200 or more, which in prose is
|
|
103
|
+
nearly every letter: a one-word edit to a 217-character prompt read 33%
|
|
104
|
+
changed and to a 1,320-character one 99.2%, so every edit to a real
|
|
105
|
+
prompt graded major.
|
|
106
|
+
"""
|
|
107
|
+
old_lines = [_PROMPT_TOKEN_RE.findall(line) for line in old_text.splitlines()]
|
|
108
|
+
new_lines = [_PROMPT_TOKEN_RE.findall(line) for line in new_text.splitlines()]
|
|
109
|
+
old_lines = [tokens for tokens in old_lines if tokens]
|
|
110
|
+
new_lines = [tokens for tokens in new_lines if tokens]
|
|
111
|
+
total = sum(len(t) for line in old_lines for t in line) + sum(
|
|
112
|
+
len(t) for line in new_lines for t in line
|
|
113
|
+
)
|
|
114
|
+
if old_lines == new_lines or not total:
|
|
115
|
+
return 0.0
|
|
116
|
+
old_keys = ["\x00".join(line) for line in old_lines]
|
|
117
|
+
new_keys = ["\x00".join(line) for line in new_lines]
|
|
118
|
+
matched = 0
|
|
119
|
+
lines = difflib.SequenceMatcher(None, old_keys, new_keys, autojunk=False)
|
|
120
|
+
for tag, i1, i2, j1, j2 in lines.get_opcodes():
|
|
121
|
+
if tag == "equal":
|
|
122
|
+
matched += sum(len(t) for line in old_lines[i1:i2] for t in line)
|
|
123
|
+
elif tag == "replace":
|
|
124
|
+
matched += _matched_chars(
|
|
125
|
+
[t for line in old_lines[i1:i2] for t in line],
|
|
126
|
+
[t for line in new_lines[j1:j2] for t in line],
|
|
127
|
+
)
|
|
128
|
+
return round((1 - 2 * matched / total) * 100, 1)
|
|
129
|
+
|
|
130
|
+
|
|
73
131
|
def prompt_severity(old_text: str, new_text: str) -> tuple[Severity, float]:
|
|
74
132
|
"""Classify prompt change severity using text diff ratio.
|
|
75
133
|
|
|
@@ -81,10 +139,7 @@ def prompt_severity(old_text: str, new_text: str) -> tuple[Severity, float]:
|
|
|
81
139
|
changes from hashes/versions alone (no inline text), so they do NOT call
|
|
82
140
|
this — the ratio tiers here are not part of the manifest diff pipeline.
|
|
83
141
|
"""
|
|
84
|
-
|
|
85
|
-
return "minor", 0.0
|
|
86
|
-
ratio = difflib.SequenceMatcher(None, old_text, new_text).ratio()
|
|
87
|
-
diff_pct = round((1 - ratio) * 100, 1)
|
|
142
|
+
diff_pct = _prompt_diff_pct(old_text or "", new_text or "")
|
|
88
143
|
if diff_pct <= 5:
|
|
89
144
|
return "minor", diff_pct
|
|
90
145
|
elif diff_pct <= 30:
|
|
@@ -101,6 +156,8 @@ def model_severity(
|
|
|
101
156
|
- Same model, config tweak (temp/top_p) → minor
|
|
102
157
|
- Version bump within same family → moderate
|
|
103
158
|
- Provider or family change → major
|
|
159
|
+
- A provider, model or tool_calling_mode the old side never recorded being
|
|
160
|
+
filled in → minor. It says what was running; nothing changed.
|
|
104
161
|
"""
|
|
105
162
|
details: list[str] = []
|
|
106
163
|
severity: Severity = "minor"
|
|
@@ -110,10 +167,10 @@ def model_severity(
|
|
|
110
167
|
old_model = str(old.get("model", ""))
|
|
111
168
|
new_model = str(new.get("model", ""))
|
|
112
169
|
|
|
113
|
-
if old_provider != new_provider:
|
|
170
|
+
if old_provider and old_provider != new_provider:
|
|
114
171
|
details.append(f"provider: {old_provider!r} → {new_provider!r}")
|
|
115
172
|
severity = "major"
|
|
116
|
-
elif old_model != new_model:
|
|
173
|
+
elif old_model and old_model != new_model:
|
|
117
174
|
# Check if same family (e.g. gpt-4o vs gpt-4o-mini)
|
|
118
175
|
old_family = _extract_model_family(old_model)
|
|
119
176
|
new_family = _extract_model_family(new_model)
|
|
@@ -123,13 +180,19 @@ def model_severity(
|
|
|
123
180
|
else:
|
|
124
181
|
details.append(f"model version: {old_model!r} → {new_model!r}")
|
|
125
182
|
severity = "moderate"
|
|
183
|
+
else:
|
|
184
|
+
if old_provider != new_provider:
|
|
185
|
+
details.append(f"provider: {old_provider!r} → {new_provider!r}")
|
|
186
|
+
if old_model != new_model:
|
|
187
|
+
details.append(f"model: {old_model!r} → {new_model!r}")
|
|
126
188
|
|
|
127
189
|
# tool_calling_mode lives at the top level of model_runtime.
|
|
128
190
|
if old.get("tool_calling_mode") != new.get("tool_calling_mode"):
|
|
129
191
|
details.append(
|
|
130
192
|
f"tool_calling_mode: {old.get('tool_calling_mode')!r} → {new.get('tool_calling_mode')!r}"
|
|
131
193
|
)
|
|
132
|
-
|
|
194
|
+
if old.get("tool_calling_mode"):
|
|
195
|
+
severity = _max_severity(severity, "moderate")
|
|
133
196
|
|
|
134
197
|
# Generation params are nested under generation_config (not top-level).
|
|
135
198
|
old_gc = old.get("generation_config") or {}
|
|
@@ -676,7 +739,7 @@ def _adapt_model_runtime(old: dict[str, Any], new: dict[str, Any]) -> tuple[
|
|
|
676
739
|
Literal["breaking", "non_breaking"], Severity, list[str]
|
|
677
740
|
]:
|
|
678
741
|
sev, details = model_severity(old, new)
|
|
679
|
-
is_breaking = any(old.get(k) != new.get(k) for k in _BREAKING_MODEL_FIELDS)
|
|
742
|
+
is_breaking = any(old.get(k) and old.get(k) != new.get(k) for k in _BREAKING_MODEL_FIELDS)
|
|
680
743
|
return ("breaking" if is_breaking else "non_breaking"), sev, details
|
|
681
744
|
|
|
682
745
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentversion"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "An open specification for versioning agent runtimes and keeping datasets valid."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -26,7 +26,11 @@ classifiers = [
|
|
|
26
26
|
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
|
-
|
|
29
|
+
# 2.1 is the real floor: this package declares `model_config_hash`, which sits
|
|
30
|
+
# in pydantic's protected `model_` namespace. 2.0 raises NameError on it at
|
|
31
|
+
# class-definition time; 2.1 downgraded that to a warning. Verified 2026-08-17
|
|
32
|
+
# by the floors CI job, which failed on exactly this.
|
|
33
|
+
"pydantic>=2.1",
|
|
30
34
|
"click>=8.0",
|
|
31
35
|
"rich>=13.0",
|
|
32
36
|
# Upper-pinned: jcs does the RFC 8785 canonicalization that every overall_hash
|
|
@@ -227,6 +227,66 @@ class TestPromptSeverity:
|
|
|
227
227
|
assert sev == "major"
|
|
228
228
|
assert pct > 30
|
|
229
229
|
|
|
230
|
+
# difflib's "autojunk" heuristic ignores every character that makes up more
|
|
231
|
+
# than 1% of a text of 200+ characters, which in prose is nearly every
|
|
232
|
+
# letter. A one-word edit to a 217-character prompt then read 33% changed,
|
|
233
|
+
# and to a 1,320-character one 99.2% — every real prompt edit graded major.
|
|
234
|
+
_SUPPORT_PROMPT = (
|
|
235
|
+
"You are the order-support agent for an online store. Look up the order with "
|
|
236
|
+
"lookup_order before you answer anything about it. Be concise and friendly. "
|
|
237
|
+
"Never promise a refund; tell the customer a person will review it."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def test_one_word_edit_to_a_realistic_prompt_is_minor(self):
|
|
241
|
+
from agentversion.diff import prompt_severity
|
|
242
|
+
tweaked = self._SUPPORT_PROMPT.replace("concise and friendly", "concise and warm")
|
|
243
|
+
sev, pct = prompt_severity(self._SUPPORT_PROMPT, tweaked)
|
|
244
|
+
assert sev == "minor", pct
|
|
245
|
+
assert pct < 5
|
|
246
|
+
|
|
247
|
+
def test_one_word_edit_to_a_long_prompt_is_minor(self):
|
|
248
|
+
from agentversion.diff import prompt_severity
|
|
249
|
+
long_prompt = "\n".join(
|
|
250
|
+
f"{i}. {self._SUPPORT_PROMPT}" for i in range(1, 7)
|
|
251
|
+
)
|
|
252
|
+
assert len(long_prompt) > 1200
|
|
253
|
+
sev, pct = prompt_severity(long_prompt, long_prompt.replace("friendly", "warm", 1))
|
|
254
|
+
assert sev == "minor", pct
|
|
255
|
+
assert pct < 1
|
|
256
|
+
|
|
257
|
+
def test_added_paragraph_is_moderate(self):
|
|
258
|
+
from agentversion.diff import prompt_severity
|
|
259
|
+
added = self._SUPPORT_PROMPT + (
|
|
260
|
+
" If the order shipped more than ten days ago, apologise and offer to open a trace."
|
|
261
|
+
)
|
|
262
|
+
sev, pct = prompt_severity(self._SUPPORT_PROMPT, added)
|
|
263
|
+
assert sev == "moderate", pct
|
|
264
|
+
|
|
265
|
+
def test_rewrite_of_a_realistic_prompt_is_major(self):
|
|
266
|
+
from agentversion.diff import prompt_severity
|
|
267
|
+
sev, pct = prompt_severity(
|
|
268
|
+
self._SUPPORT_PROMPT,
|
|
269
|
+
"You handle shipping questions only. Reply in exactly two sentences, quote "
|
|
270
|
+
"the tracking number, and hand anything about money to the billing team.",
|
|
271
|
+
)
|
|
272
|
+
assert sev == "major", pct
|
|
273
|
+
|
|
274
|
+
def test_identical_prompts_are_unchanged(self):
|
|
275
|
+
from agentversion.diff import prompt_severity
|
|
276
|
+
assert prompt_severity(self._SUPPORT_PROMPT, self._SUPPORT_PROMPT) == ("minor", 0.0)
|
|
277
|
+
|
|
278
|
+
def test_large_prompt_is_graded_quickly(self):
|
|
279
|
+
import time
|
|
280
|
+
|
|
281
|
+
from agentversion.diff import prompt_severity
|
|
282
|
+
words = [f"word{i % 997}" for i in range(6000)] # ~50k characters
|
|
283
|
+
old = " ".join(words)
|
|
284
|
+
new = " ".join(words[:3000] + ["inserted"] * 20 + words[3000:])
|
|
285
|
+
start = time.perf_counter()
|
|
286
|
+
sev, pct = prompt_severity(old, new)
|
|
287
|
+
assert sev == "minor", pct
|
|
288
|
+
assert time.perf_counter() - start < 2.0
|
|
289
|
+
|
|
230
290
|
|
|
231
291
|
class TestModelSeverity:
|
|
232
292
|
def test_config_only_is_minor(self):
|
|
@@ -254,6 +314,31 @@ class TestModelSeverity:
|
|
|
254
314
|
)
|
|
255
315
|
assert sev == "major"
|
|
256
316
|
|
|
317
|
+
def test_filling_in_what_was_never_recorded_is_minor(self):
|
|
318
|
+
# A baseline made before the agent first ran has no model on it.
|
|
319
|
+
from agentversion.diff import model_severity
|
|
320
|
+
sev, details = model_severity(
|
|
321
|
+
{}, {"provider": "openai", "model": "gpt-4o", "tool_calling_mode": "auto"},
|
|
322
|
+
)
|
|
323
|
+
assert sev == "minor"
|
|
324
|
+
assert details == [
|
|
325
|
+
"provider: '' → 'openai'",
|
|
326
|
+
"model: '' → 'gpt-4o'",
|
|
327
|
+
"tool_calling_mode: None → 'auto'",
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
def test_losing_a_recorded_provider_is_still_major(self):
|
|
331
|
+
from agentversion.diff import model_severity
|
|
332
|
+
sev, _ = model_severity(
|
|
333
|
+
{"provider": "openai", "model": "gpt-4o"}, {"model": "gpt-4o"},
|
|
334
|
+
)
|
|
335
|
+
assert sev == "major"
|
|
336
|
+
|
|
337
|
+
def test_a_blank_provider_does_not_hide_a_family_change(self):
|
|
338
|
+
from agentversion.diff import model_severity
|
|
339
|
+
sev, _ = model_severity({"model": "gpt-4o"}, {"provider": "google", "model": "gemini-2.0-flash"})
|
|
340
|
+
assert sev == "major"
|
|
341
|
+
|
|
257
342
|
def test_tool_calling_mode_change_is_moderate(self):
|
|
258
343
|
from agentversion.diff import model_severity
|
|
259
344
|
sev, details = model_severity(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/environment-region-change/after.json
RENAMED
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/environment-region-change/before.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/output-schema-change/after.json
RENAMED
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/output-schema-change/before.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/prompt-stack-edit/expected-diff.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/subagent-handoff-change/after.json
RENAMED
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/subagent-handoff-change/before.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/tool-rename/expected-diff.json
RENAMED
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/workflow-graph-change/after.json
RENAMED
|
File without changes
|
{agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/workflow-graph-change/before.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|