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.
Files changed (115) hide show
  1. {agentversion-0.2.3 → agentversion-0.2.4}/CHANGELOG.md +11 -0
  2. {agentversion-0.2.3 → agentversion-0.2.4}/PKG-INFO +2 -2
  3. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/cli.py +3 -1
  4. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/diff.py +71 -8
  5. {agentversion-0.2.3 → agentversion-0.2.4}/pyproject.toml +6 -2
  6. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_diff.py +85 -0
  7. {agentversion-0.2.3 → agentversion-0.2.4}/.gitignore +0 -0
  8. {agentversion-0.2.3 → agentversion-0.2.4}/CONFORMANCE.md +0 -0
  9. {agentversion-0.2.3 → agentversion-0.2.4}/LICENSE +0 -0
  10. {agentversion-0.2.3 → agentversion-0.2.4}/README.md +0 -0
  11. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/__init__.py +0 -0
  12. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/_shared.py +0 -0
  13. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/a2a.py +0 -0
  14. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/compatibility.py +0 -0
  15. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/constants.py +0 -0
  16. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/contract.py +0 -0
  17. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/dataset.py +0 -0
  18. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/decision.py +0 -0
  19. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/hasher.py +0 -0
  20. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/ids.py +0 -0
  21. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/manifest.py +0 -0
  22. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/py.typed +0 -0
  23. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/refs.py +0 -0
  24. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/replay.py +0 -0
  25. {agentversion-0.2.3 → agentversion-0.2.4}/agentversion/validator.py +0 -0
  26. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/environment-region-change/after.json +0 -0
  27. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/environment-region-change/before.json +0 -0
  28. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/environment-region-change/expected-diff.json +0 -0
  29. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/model-runtime-provider-change/after.json +0 -0
  30. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/model-runtime-provider-change/before.json +0 -0
  31. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/model-runtime-provider-change/expected-diff.json +0 -0
  32. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/output-schema-change/after.json +0 -0
  33. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/output-schema-change/before.json +0 -0
  34. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/output-schema-change/expected-diff.json +0 -0
  35. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/prompt-stack-edit/after.json +0 -0
  36. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/prompt-stack-edit/before.json +0 -0
  37. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/prompt-stack-edit/expected-diff.json +0 -0
  38. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/skill-registry-skill-removed/after.json +0 -0
  39. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/skill-registry-skill-removed/before.json +0 -0
  40. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/skill-registry-skill-removed/expected-diff.json +0 -0
  41. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/subagent-handoff-change/after.json +0 -0
  42. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/subagent-handoff-change/before.json +0 -0
  43. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/subagent-handoff-change/expected-diff.json +0 -0
  44. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/tool-rename/after.json +0 -0
  45. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/tool-rename/before.json +0 -0
  46. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/tool-rename/expected-diff.json +0 -0
  47. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/workflow-graph-change/after.json +0 -0
  48. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/workflow-graph-change/before.json +0 -0
  49. {agentversion-0.2.3 → agentversion-0.2.4}/compatibility-tests/workflow-graph-change/expected-diff.json +0 -0
  50. {agentversion-0.2.3 → agentversion-0.2.4}/examples/.gitkeep +0 -0
  51. {agentversion-0.2.3 → agentversion-0.2.4}/examples/integrations/decimalai_bridge.py +0 -0
  52. {agentversion-0.2.3 → agentversion-0.2.4}/examples/integrations/langgraph_example.py +0 -0
  53. {agentversion-0.2.3 → agentversion-0.2.4}/examples/integrations/otel_mapping.md +0 -0
  54. {agentversion-0.2.3 → agentversion-0.2.4}/examples/manifest/finance-agent-v1.json +0 -0
  55. {agentversion-0.2.3 → agentversion-0.2.4}/examples/manifest/finance-agent-v2.json +0 -0
  56. {agentversion-0.2.3 → agentversion-0.2.4}/examples/scenarios/tool-rename-drift.md +0 -0
  57. {agentversion-0.2.3 → agentversion-0.2.4}/examples/scenarios/walkthrough.py +0 -0
  58. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/.gitkeep +0 -0
  59. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/agent-manifest.schema.json +0 -0
  60. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/compatibility-batch.schema.json +0 -0
  61. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/compatibility-decision.schema.json +0 -0
  62. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/compatibility-policy.schema.json +0 -0
  63. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/compatibility-report.schema.json +0 -0
  64. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/dataset-snapshot.schema.json +0 -0
  65. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/episode.schema.json +0 -0
  66. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/manifest-diff.schema.json +0 -0
  67. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/replay-job.schema.json +0 -0
  68. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/replay-result.schema.json +0 -0
  69. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/step.schema.json +0 -0
  70. {agentversion-0.2.3 → agentversion-0.2.4}/schemas/task.schema.json +0 -0
  71. {agentversion-0.2.3 → agentversion-0.2.4}/spec/a2a-mapping.md +0 -0
  72. {agentversion-0.2.3 → agentversion-0.2.4}/spec/attestation.md +0 -0
  73. {agentversion-0.2.3 → agentversion-0.2.4}/spec/behavioral-policy.md +0 -0
  74. {agentversion-0.2.3 → agentversion-0.2.4}/spec/compatibility-batch.md +0 -0
  75. {agentversion-0.2.3 → agentversion-0.2.4}/spec/compatibility-decision.md +0 -0
  76. {agentversion-0.2.3 → agentversion-0.2.4}/spec/compatibility-policy.md +0 -0
  77. {agentversion-0.2.3 → agentversion-0.2.4}/spec/data-classification.md +0 -0
  78. {agentversion-0.2.3 → agentversion-0.2.4}/spec/dataset.md +0 -0
  79. {agentversion-0.2.3 → agentversion-0.2.4}/spec/diff.md +0 -0
  80. {agentversion-0.2.3 → agentversion-0.2.4}/spec/environment.md +0 -0
  81. {agentversion-0.2.3 → agentversion-0.2.4}/spec/evaluation.md +0 -0
  82. {agentversion-0.2.3 → agentversion-0.2.4}/spec/hashing.md +0 -0
  83. {agentversion-0.2.3 → agentversion-0.2.4}/spec/ids.md +0 -0
  84. {agentversion-0.2.3 → agentversion-0.2.4}/spec/lifecycle.md +0 -0
  85. {agentversion-0.2.3 → agentversion-0.2.4}/spec/manifest.md +0 -0
  86. {agentversion-0.2.3 → agentversion-0.2.4}/spec/otel-mapping.md +0 -0
  87. {agentversion-0.2.3 → agentversion-0.2.4}/spec/reference.md +0 -0
  88. {agentversion-0.2.3 → agentversion-0.2.4}/spec/refs.md +0 -0
  89. {agentversion-0.2.3 → agentversion-0.2.4}/spec/replay-determinism.md +0 -0
  90. {agentversion-0.2.3 → agentversion-0.2.4}/spec/replay.md +0 -0
  91. {agentversion-0.2.3 → agentversion-0.2.4}/spec/versioning-policy.md +0 -0
  92. {agentversion-0.2.3 → agentversion-0.2.4}/tests/fixtures/frozen_hash_vectors.json +0 -0
  93. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_a2a_mapping.py +0 -0
  94. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_audit_v020.py +0 -0
  95. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_audit_v030.py +0 -0
  96. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_behavioral_policy.py +0 -0
  97. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_cli.py +0 -0
  98. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_component_surface_map.py +0 -0
  99. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_conformance.py +0 -0
  100. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_contract.py +0 -0
  101. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_dataset.py +0 -0
  102. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_decision_replay.py +0 -0
  103. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_environment.py +0 -0
  104. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_evaluation.py +0 -0
  105. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_examples_runnable.py +0 -0
  106. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_extension_hatch.py +0 -0
  107. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_frozen_hash_vectors.py +0 -0
  108. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_hasher.py +0 -0
  109. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_ids.py +0 -0
  110. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_lifecycle.py +0 -0
  111. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_manifest.py +0 -0
  112. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_refs.py +0 -0
  113. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_reproducible_replay.py +0 -0
  114. {agentversion-0.2.3 → agentversion-0.2.4}/tests/test_trust_observability.py +0 -0
  115. {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
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.0
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
- manifest = {
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
- if not old_text and not new_text:
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
- severity = _max_severity(severity, "moderate")
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.3"
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
- "pydantic>=2.0",
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