agentversion 0.2.0__tar.gz → 0.2.2__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.

Potentially problematic release.


This version of agentversion might be problematic. Click here for more details.

Files changed (125) hide show
  1. {agentversion-0.2.0 → agentversion-0.2.2}/CHANGELOG.md +33 -0
  2. {agentversion-0.2.0 → agentversion-0.2.2}/PKG-INFO +1 -1
  3. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/contract.py +20 -1
  4. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/diff.py +3 -3
  5. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/manifest.py +16 -11
  6. {agentversion-0.2.0 → agentversion-0.2.2}/pyproject.toml +1 -1
  7. agentversion-0.2.2/spec/behavioral-policy.md +56 -0
  8. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_contract.py +61 -0
  9. agentversion-0.2.0/spec/behavioral-policy.md +0 -51
  10. {agentversion-0.2.0 → agentversion-0.2.2}/.github/dependabot.yml +0 -0
  11. {agentversion-0.2.0 → agentversion-0.2.2}/.github/workflows/ci.yml +0 -0
  12. {agentversion-0.2.0 → agentversion-0.2.2}/.github/workflows/publish.yml +0 -0
  13. {agentversion-0.2.0 → agentversion-0.2.2}/.gitignore +0 -0
  14. {agentversion-0.2.0 → agentversion-0.2.2}/CONFORMANCE.md +0 -0
  15. {agentversion-0.2.0 → agentversion-0.2.2}/CONTRIBUTING.md +0 -0
  16. {agentversion-0.2.0 → agentversion-0.2.2}/LICENSE +0 -0
  17. {agentversion-0.2.0 → agentversion-0.2.2}/README.md +0 -0
  18. {agentversion-0.2.0 → agentversion-0.2.2}/RELEASING.md +0 -0
  19. {agentversion-0.2.0 → agentversion-0.2.2}/adrs/0000-template.md +0 -0
  20. {agentversion-0.2.0 → agentversion-0.2.2}/adrs/0001-version-spec-core.md +0 -0
  21. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/__init__.py +0 -0
  22. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/_shared.py +0 -0
  23. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/a2a.py +0 -0
  24. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/cli.py +0 -0
  25. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/compatibility.py +0 -0
  26. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/constants.py +0 -0
  27. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/dataset.py +0 -0
  28. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/decision.py +0 -0
  29. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/hasher.py +0 -0
  30. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/ids.py +0 -0
  31. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/py.typed +0 -0
  32. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/refs.py +0 -0
  33. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/replay.py +0 -0
  34. {agentversion-0.2.0 → agentversion-0.2.2}/agentversion/validator.py +0 -0
  35. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/environment-region-change/after.json +0 -0
  36. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/environment-region-change/before.json +0 -0
  37. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/environment-region-change/expected-diff.json +0 -0
  38. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/model-runtime-provider-change/after.json +0 -0
  39. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/model-runtime-provider-change/before.json +0 -0
  40. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/model-runtime-provider-change/expected-diff.json +0 -0
  41. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/output-schema-change/after.json +0 -0
  42. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/output-schema-change/before.json +0 -0
  43. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/output-schema-change/expected-diff.json +0 -0
  44. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/prompt-stack-edit/after.json +0 -0
  45. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/prompt-stack-edit/before.json +0 -0
  46. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/prompt-stack-edit/expected-diff.json +0 -0
  47. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/skill-registry-skill-removed/after.json +0 -0
  48. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/skill-registry-skill-removed/before.json +0 -0
  49. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/skill-registry-skill-removed/expected-diff.json +0 -0
  50. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/subagent-handoff-change/after.json +0 -0
  51. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/subagent-handoff-change/before.json +0 -0
  52. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/subagent-handoff-change/expected-diff.json +0 -0
  53. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/tool-rename/after.json +0 -0
  54. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/tool-rename/before.json +0 -0
  55. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/tool-rename/expected-diff.json +0 -0
  56. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/workflow-graph-change/after.json +0 -0
  57. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/workflow-graph-change/before.json +0 -0
  58. {agentversion-0.2.0 → agentversion-0.2.2}/compatibility-tests/workflow-graph-change/expected-diff.json +0 -0
  59. {agentversion-0.2.0 → agentversion-0.2.2}/examples/.gitkeep +0 -0
  60. {agentversion-0.2.0 → agentversion-0.2.2}/examples/integrations/decimalai_bridge.py +0 -0
  61. {agentversion-0.2.0 → agentversion-0.2.2}/examples/integrations/langgraph_example.py +0 -0
  62. {agentversion-0.2.0 → agentversion-0.2.2}/examples/integrations/otel_mapping.md +0 -0
  63. {agentversion-0.2.0 → agentversion-0.2.2}/examples/manifest/finance-agent-v1.json +0 -0
  64. {agentversion-0.2.0 → agentversion-0.2.2}/examples/manifest/finance-agent-v2.json +0 -0
  65. {agentversion-0.2.0 → agentversion-0.2.2}/examples/scenarios/tool-rename-drift.md +0 -0
  66. {agentversion-0.2.0 → agentversion-0.2.2}/examples/scenarios/walkthrough.py +0 -0
  67. {agentversion-0.2.0 → agentversion-0.2.2}/pyrightconfig.json +0 -0
  68. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/.gitkeep +0 -0
  69. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/agent-manifest.schema.json +0 -0
  70. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/compatibility-batch.schema.json +0 -0
  71. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/compatibility-decision.schema.json +0 -0
  72. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/compatibility-policy.schema.json +0 -0
  73. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/compatibility-report.schema.json +0 -0
  74. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/dataset-snapshot.schema.json +0 -0
  75. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/episode.schema.json +0 -0
  76. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/manifest-diff.schema.json +0 -0
  77. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/replay-job.schema.json +0 -0
  78. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/replay-result.schema.json +0 -0
  79. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/step.schema.json +0 -0
  80. {agentversion-0.2.0 → agentversion-0.2.2}/schemas/task.schema.json +0 -0
  81. {agentversion-0.2.0 → agentversion-0.2.2}/scripts/release.sh +0 -0
  82. {agentversion-0.2.0 → agentversion-0.2.2}/spec/a2a-mapping.md +0 -0
  83. {agentversion-0.2.0 → agentversion-0.2.2}/spec/attestation.md +0 -0
  84. {agentversion-0.2.0 → agentversion-0.2.2}/spec/compatibility-batch.md +0 -0
  85. {agentversion-0.2.0 → agentversion-0.2.2}/spec/compatibility-decision.md +0 -0
  86. {agentversion-0.2.0 → agentversion-0.2.2}/spec/compatibility-policy.md +0 -0
  87. {agentversion-0.2.0 → agentversion-0.2.2}/spec/data-classification.md +0 -0
  88. {agentversion-0.2.0 → agentversion-0.2.2}/spec/dataset.md +0 -0
  89. {agentversion-0.2.0 → agentversion-0.2.2}/spec/diff.md +0 -0
  90. {agentversion-0.2.0 → agentversion-0.2.2}/spec/environment.md +0 -0
  91. {agentversion-0.2.0 → agentversion-0.2.2}/spec/evaluation.md +0 -0
  92. {agentversion-0.2.0 → agentversion-0.2.2}/spec/hashing.md +0 -0
  93. {agentversion-0.2.0 → agentversion-0.2.2}/spec/ids.md +0 -0
  94. {agentversion-0.2.0 → agentversion-0.2.2}/spec/lifecycle.md +0 -0
  95. {agentversion-0.2.0 → agentversion-0.2.2}/spec/manifest.md +0 -0
  96. {agentversion-0.2.0 → agentversion-0.2.2}/spec/otel-mapping.md +0 -0
  97. {agentversion-0.2.0 → agentversion-0.2.2}/spec/reference.md +0 -0
  98. {agentversion-0.2.0 → agentversion-0.2.2}/spec/refs.md +0 -0
  99. {agentversion-0.2.0 → agentversion-0.2.2}/spec/replay-determinism.md +0 -0
  100. {agentversion-0.2.0 → agentversion-0.2.2}/spec/replay.md +0 -0
  101. {agentversion-0.2.0 → agentversion-0.2.2}/spec/versioning-policy.md +0 -0
  102. {agentversion-0.2.0 → agentversion-0.2.2}/tests/fixtures/frozen_hash_vectors.json +0 -0
  103. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_a2a_mapping.py +0 -0
  104. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_audit_v020.py +0 -0
  105. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_audit_v030.py +0 -0
  106. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_behavioral_policy.py +0 -0
  107. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_cli.py +0 -0
  108. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_component_surface_map.py +0 -0
  109. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_conformance.py +0 -0
  110. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_dataset.py +0 -0
  111. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_decision_replay.py +0 -0
  112. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_diff.py +0 -0
  113. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_environment.py +0 -0
  114. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_evaluation.py +0 -0
  115. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_examples_runnable.py +0 -0
  116. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_extension_hatch.py +0 -0
  117. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_frozen_hash_vectors.py +0 -0
  118. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_hasher.py +0 -0
  119. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_ids.py +0 -0
  120. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_lifecycle.py +0 -0
  121. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_manifest.py +0 -0
  122. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_refs.py +0 -0
  123. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_reproducible_replay.py +0 -0
  124. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_trust_observability.py +0 -0
  125. {agentversion-0.2.0 → agentversion-0.2.2}/tests/test_validator.py +0 -0
@@ -7,6 +7,39 @@ 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
+ ## [0.2.2] - 2026-07-15
11
+
12
+ ### Changed
13
+ - **`behavioral_policy` re-scoped as a generic versioned policy-document surface** (docs only).
14
+ The surface previously described itself as bound to skillevaluation's conversation-mode
15
+ `policy_check` state machine; that eval mode was removed (skillevaluation ADR-0007), so the
16
+ docstrings + `spec/behavioral-policy.md` now define it document-agnostically: `policy_hash` is a
17
+ hash of *any* bound policy artifact (refund rules, a safety guardrail set, an escalation SOP…),
18
+ a rule change diffs **breaking**, an unchanged `policy_hash` is non-breaking. **No code or schema
19
+ change**: the `BehavioralPolicy` model still carries `objection_threshold`/`concede_events`/
20
+ `always_forbidden` (now documented as optional, illustrative, back-compat rule fields), the
21
+ `_BEHAVIORAL_POLICY_RULE_FIELDS` diff logic is byte-identical, and every frozen hash-vector +
22
+ the 406-test suite are unchanged. `spec_version` unchanged (1.0.0).
23
+
24
+ ## [0.2.1] - 2026-06-24
25
+
26
+ ### Fixed
27
+ - **`contract_from_components` now emits the `behavioral_policy` and
28
+ `environment` surfaces.** 0.2.0 added the models, surface keys, and diff
29
+ classifiers, but the shared flat-components → contract assembler silently
30
+ dropped both — so a producer (the DecimalAI SDK / the platform) could never
31
+ actually populate them from components. They are now assembled verbatim from
32
+ each component's `schema_json`. Additive-only: a manifest that doesn't declare
33
+ them is unchanged (same `overall_hash`).
34
+ - **`tool_calling_mode` and `runtime_version` are now lifted to `model_runtime`
35
+ top level** (not buried in `model_config_hash`), so a `tool_calling_mode`
36
+ change is classified **breaking** by the diff as intended, and
37
+ `runtime_version` participates in `model_runtime` severity.
38
+
39
+ The DecimalAI SDK exporter (`ManifestSnapshot.to_agentversion`) was edited
40
+ byte-identically and is pinned to this assembly by its conformance test.
41
+ `SPEC_VERSION` is unchanged (1.0.0) — these surfaces were already in the spec.
42
+
10
43
  ## [0.2.0] - 2026-06-24
11
44
 
12
45
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentversion
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -12,7 +12,8 @@ Each component is a plain dict::
12
12
  {
13
13
  "component_type": "tool" | "skill" | "prompt" | "model" | "subagent"
14
14
  | "output_schema" | "workflow" | "guardrail"
15
- | "context_config",
15
+ | "context_config" | "behavioral_policy"
16
+ | "environment",
16
17
  "component_name": str,
17
18
  "component_version": str | None,
18
19
  "content_hash": str | None, # producer-supplied content fingerprint
@@ -146,6 +147,13 @@ def contract_from_components(components: list[dict[str, Any]]) -> dict[str, Any]
146
147
  gen["response_format"] = cfg["response_format"]
147
148
  if gen:
148
149
  model_runtime["generation_config"] = gen
150
+ # tool_calling_mode + runtime_version are TOP-LEVEL model_runtime fields
151
+ # (not under generation_config); the diff classifies a tool_calling_mode
152
+ # change as breaking. Must stay byte-identical with the SDK exporter.
153
+ if isinstance(cfg.get("tool_calling_mode"), str):
154
+ model_runtime["tool_calling_mode"] = cfg["tool_calling_mode"]
155
+ if isinstance(cfg.get("runtime_version"), str):
156
+ model_runtime["runtime_version"] = cfg["runtime_version"]
149
157
  else:
150
158
  model_runtime = {"provider": "unknown", "model": "unknown"}
151
159
 
@@ -252,4 +260,15 @@ def contract_from_components(components: list[dict[str, Any]]) -> dict[str, Any]
252
260
  ctx["retrieval_config_hash"] = ctx_comps[0]["content_hash"]
253
261
  contract["context_config"] = ctx
254
262
 
263
+ # behavioral_policy / environment are single structured surfaces (the surface
264
+ # IS the config), emitted verbatim from schema_json so hash_manifest hashes
265
+ # their contents. Must stay byte-identical with the SDK exporter.
266
+ bp_comps = by_type.get("behavioral_policy", [])
267
+ if bp_comps:
268
+ contract["behavioral_policy"] = dict(bp_comps[0].get("schema_json") or {})
269
+
270
+ env_comps = by_type.get("environment", [])
271
+ if env_comps:
272
+ contract["environment"] = dict(env_comps[0].get("schema_json") or {})
273
+
255
274
  return contract
@@ -741,9 +741,9 @@ _BEHAVIORAL_POLICY_RULE_FIELDS = (
741
741
  def _adapt_behavioral_policy(old: dict[str, Any], new: dict[str, Any]) -> tuple[
742
742
  Literal["breaking", "non_breaking"], Severity, list[str]
743
743
  ]:
744
- """A change to the multi-turn behavioral policy RULES is BREAKING: it invalidates the conversation-
745
- mode eval set and any past traces graded under the old policy. (A bare metadata change with an
746
- unchanged ``policy_hash`` is not a rule change.)
744
+ """A change to the bound behavioral policy RULES is BREAKING: it invalidates any eval set and any
745
+ past traces graded under the old policy. (A bare metadata change with an unchanged ``policy_hash``
746
+ is not a rule change.)
747
747
 
748
748
  Add/remove are asymmetric and handled here so the surface is severitied by its own
749
749
  logic in both directions: *introducing* a policy where there was none is additive
@@ -263,25 +263,30 @@ class Environment(BaseModel):
263
263
 
264
264
 
265
265
  class BehavioralPolicy(BaseModel):
266
- """Multi-turn BEHAVIORAL-POLICY contract surface.
266
+ """Versioned BEHAVIORAL-POLICY contract surface.
267
267
 
268
- The rules a multi-turn agent must hold across turns — e.g. a refund/escalation policy: "deny until
269
- the customer objects N times, then escalate; never admit liability". Bound to skillevaluation's
270
- conversation-mode ``policy_check`` (the deterministic state machine that grades these rules).
268
+ Binds the agent to a named policy artifact — the rules it must hold to, e.g. a refund/escalation
269
+ policy ("deny until the customer objects N times, then escalate; never admit liability"), a safety
270
+ guardrail set, or an escalation SOP. The surface is document-agnostic: ``policy_hash`` is the stable
271
+ identity (a hash of whatever policy artifact you bind), and the value carried is opaque to this
272
+ model.
271
273
 
272
274
  Why it is a first-class surface: a behavioral policy lives *inside* the prompt, so a prompt-stack
273
- hash change is classified ``non_breaking`` — yet flipping the policy (``objection_threshold`` 3 → 1,
274
- or removing an ``always_forbidden`` rule) INVALIDATES the conversation-mode eval set and any past
275
- traces graded under the old policy. Hoisting the policy to its own surface makes that flip visible
276
- and ``breaking`` instead of silently ``keep``. ``policy_hash`` is the stable identity (a hash of the
277
- bound ``policy_check`` spec); the rule fields are carried for human-readable diffs. ``extra='allow'``
278
- so a richer policy spec round-trips without forking this model.
275
+ hash change is classified ``non_breaking`` — yet flipping the policy (a threshold 3 → 1, or removing
276
+ a forbidden rule) INVALIDATES any eval set and any past traces graded under the old policy. Hoisting
277
+ the policy to its own surface makes that flip visible and ``breaking`` instead of silently ``keep``.
278
+
279
+ ``policy_hash`` is the identity; the structured rule fields below are OPTIONAL and carried only for
280
+ human-readable diffs (a policy artifact needn't populate any of them — ``extra='allow'`` lets a
281
+ richer or differently-shaped policy spec round-trip without forking this model). They remain on the
282
+ model for backward compatibility with policies already bound this way.
279
283
  """
280
284
 
281
285
  model_config = ConfigDict(extra="allow")
282
286
 
283
287
  policy_id: str | None = None
284
- policy_hash: str | None = None # stable hash of the bound policy_check spec (the identity)
288
+ policy_hash: str | None = None # stable hash of the bound policy artifact (the identity)
289
+ # Optional structured rule fields — illustrative, kept for back-compat + human-readable diffs.
285
290
  objection_threshold: int | None = None
286
291
  concede_events: list[str] | None = None
287
292
  always_forbidden: list[str] | None = None
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentversion"
7
- version = "0.2.0"
7
+ version = "0.2.2"
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"
@@ -0,0 +1,56 @@
1
+ # `behavioral_policy` surface
2
+
3
+ A contract surface that binds an agent to a **versioned policy document** — the rules it must hold to.
4
+ The surface is document-agnostic: the policy can be a refund/escalation policy (*"deny refunds and offer
5
+ alternatives; only after the customer has objected ≥3 times, escalate to a human; never concede a refund
6
+ yourself, and never admit liability"*), a safety guardrail set, an escalation SOP, or any other bound
7
+ rule artifact.
8
+
9
+ ## Why it is a first-class surface
10
+
11
+ A behavioral policy usually lives *inside the system prompt*. Before this surface existed, flipping the
12
+ policy (tightening a threshold, or dropping a forbidden rule) showed up only as a `prompt_stack`
13
+ **hash change**, which the diff classifies **`non_breaking`** → recommended decision **`keep`**. But a
14
+ policy flip **invalidates** any eval set and any past traces that were graded under the old policy.
15
+ Hoisting the policy to its own surface makes that flip **`breaking`** → `replay`/`drop`, instead of
16
+ silently retaining now-wrong data.
17
+
18
+ The `policy_hash` is the canonical identity — a hash of *whatever policy artifact you bind*. The
19
+ structured rule fields below are optional and carried only for human-readable diffs; the surface does not
20
+ interpret them.
21
+
22
+ ## Shape
23
+
24
+ ```json
25
+ "behavioral_policy": {
26
+ "policy_id": "refund-escalation",
27
+ "policy_hash": "sha256:…", // hash of the bound policy artifact (the identity)
28
+ "objection_threshold": 3, // optional structured rule fields — illustrative,
29
+ "concede_events": ["offered_refund", "escalated"], // carried for readable diffs only
30
+ "always_forbidden": ["admits_liability"]
31
+ }
32
+ ```
33
+
34
+ `policy_id` and `policy_hash` are the only load-bearing keys. The structured rule fields
35
+ (`objection_threshold`, `concede_events`, `always_forbidden`) are **optional**, illustrative, and kept
36
+ for backward compatibility with policies already bound this way — a policy artifact needn't populate any
37
+ of them. The model is `extra="allow"`, so a richer or differently-shaped policy spec round-trips without
38
+ forking it.
39
+
40
+ ## Diff semantics
41
+
42
+ | Transition | Classification | Rationale |
43
+ |---|---|---|
44
+ | rule change (`policy_hash` differs, or any rule field differs) | **breaking** (`major`) | any eval set + past graded traces are now wrong |
45
+ | `policy_hash` unchanged, only metadata (`policy_id`) differs | `non_breaking` (`minor`) | same policy, no rule changed |
46
+ | introduced (absent → present) | `non_breaking` | the old eval set tested no policy; additive |
47
+ | removed (present → absent) | **breaking** (`major`) | past data graded under the policy is now ungoverned |
48
+
49
+ Reason code: `behavioral_policy_changed`. Configurable per-surface via `CompatibilityPolicy.behavioral_policy`
50
+ (defaults: `on_major` → `drop`).
51
+
52
+ ## Hash-safety
53
+
54
+ `behavioral_policy` is **optional and omitted by default**. The `overall_hash` is computed only over the
55
+ surfaces actually present in a contract, so adding this surface to the spec does **not** change the hash
56
+ of any existing (policy-less) manifest. The frozen hash-vectors are unchanged.
@@ -59,6 +59,67 @@ def test_model_provider_inferred_when_absent():
59
59
  assert c["model_runtime"]["provider"] == "anthropic"
60
60
 
61
61
 
62
+ # ── OSS-1/3/7: new 0.2.0 surfaces (must mirror the SDK exporter) ────────────
63
+
64
+ def test_model_runtime_lifts_tool_calling_mode_and_runtime_version():
65
+ """OSS-1: tool_calling_mode + runtime_version are TOP-LEVEL model_runtime
66
+ fields (not under generation_config), so the diff classifies them."""
67
+ c = contract_from_components([
68
+ _comp("model", "default", content_hash="sha256:m",
69
+ schema_json={"provider": "openai", "model": "gpt-4o",
70
+ "tool_calling_mode": "native", "runtime_version": "1.2"}),
71
+ ])
72
+ mr = c["model_runtime"]
73
+ assert mr["tool_calling_mode"] == "native"
74
+ assert mr["runtime_version"] == "1.2"
75
+ assert "tool_calling_mode" not in mr.get("generation_config", {})
76
+
77
+
78
+ def test_behavioral_policy_surface_assembled():
79
+ """OSS-3: behavioral_policy is emitted verbatim from its schema_json."""
80
+ sj = {"policy_id": "refund", "policy_hash": "sha256:p", "objection_threshold": 3,
81
+ "always_forbidden": ["admit_liability"]}
82
+ c = contract_from_components([
83
+ _comp("model", "default", schema_json={"model": "gpt-4o"}),
84
+ _comp("behavioral_policy", "behavioral_policy", content_hash="sha256:p", schema_json=sj),
85
+ ])
86
+ assert c["behavioral_policy"] == sj
87
+
88
+
89
+ def test_environment_surface_assembled():
90
+ """OSS-7: environment is emitted verbatim from its schema_json."""
91
+ sj = {"deployment_id": "d", "region": "us", "runtime_versions": {"python": "3.12"}}
92
+ c = contract_from_components([
93
+ _comp("model", "default", schema_json={"model": "gpt-4o"}),
94
+ _comp("environment", "environment", content_hash="sha256:e", schema_json=sj),
95
+ ])
96
+ assert c["environment"] == sj
97
+
98
+
99
+ def test_new_optional_surfaces_absent_when_undeclared():
100
+ """Additive-only: a model-only contract carries neither new surface, so
101
+ existing manifests keep their exact prior identity hash."""
102
+ c = contract_from_components([_comp("model", "default", schema_json={"model": "gpt-4o"})])
103
+ assert "behavioral_policy" not in c
104
+ assert "environment" not in c
105
+
106
+
107
+ def test_behavioral_policy_participates_in_identity_hash():
108
+ """Adding behavioral_policy, and changing its policy_hash, each change the
109
+ canonical jcs-sha256 identity hash."""
110
+ base = [_comp("model", "default", content_hash="sha256:m",
111
+ schema_json={"provider": "openai", "model": "gpt-4o"})]
112
+ with_p = base + [_comp("behavioral_policy", "behavioral_policy", content_hash="sha256:p",
113
+ schema_json={"policy_id": "p", "policy_hash": "sha256:p"})]
114
+ with_p2 = base + [_comp("behavioral_policy", "behavioral_policy", content_hash="sha256:p2",
115
+ schema_json={"policy_id": "p", "policy_hash": "sha256:p2"})]
116
+ h_base = hash_manifest({"contract": contract_from_components(base)})
117
+ h_p = hash_manifest({"contract": contract_from_components(with_p)})
118
+ h_p2 = hash_manifest({"contract": contract_from_components(with_p2)})
119
+ assert h_base != h_p # the surface participates in identity
120
+ assert h_p != h_p2 # a rule change re-versions
121
+
122
+
62
123
  def test_tool_and_skill_registry_hashes_are_computed():
63
124
  c = contract_from_components([
64
125
  _comp("tool", "t1", version="1", content_hash="sha256:t1",
@@ -1,51 +0,0 @@
1
- # `behavioral_policy` surface
2
-
3
- A contract surface for a multi-turn agent's **behavioral policy** — the rules it must hold *across
4
- turns*, e.g. a refund/escalation policy: *"deny refunds and offer alternatives; only after the customer
5
- has objected ≥3 times, escalate to a human; never concede a refund yourself, and never admit liability."*
6
-
7
- ## Why it is a first-class surface
8
-
9
- A behavioral policy lives *inside the system prompt*. Before this surface existed, flipping the policy
10
- (`objection_threshold` 3 → 1, or dropping an `always_forbidden` rule) showed up only as a `prompt_stack`
11
- **hash change**, which the diff classifies **`non_breaking`** → recommended decision **`keep`**. But a
12
- policy flip **invalidates** the conversation-mode eval set and any past traces that were graded under the
13
- old policy. Hoisting the policy to its own surface makes that flip **`breaking`** → `replay`/`drop`,
14
- instead of silently retaining now-wrong data.
15
-
16
- This binds directly to [skillevaluation](https://github.com/decimal-labs/skillevaluation)'s
17
- conversation-mode `policy_check` — the deterministic state machine that grades these same rules. The
18
- `policy_hash` is the canonical identity (a hash of the bound `policy_check` spec); the rule fields are
19
- carried for human-readable diffs.
20
-
21
- ## Shape
22
-
23
- ```json
24
- "behavioral_policy": {
25
- "policy_id": "refund-escalation",
26
- "policy_hash": "sha256:…", // hash of the bound policy_check spec (the identity)
27
- "objection_threshold": 3,
28
- "concede_events": ["offered_refund", "escalated"],
29
- "always_forbidden": ["admits_liability"]
30
- }
31
- ```
32
-
33
- The model is `extra="allow"`, so a richer policy spec round-trips without forking it.
34
-
35
- ## Diff semantics
36
-
37
- | Transition | Classification | Rationale |
38
- |---|---|---|
39
- | rule change (`policy_hash` differs, or any rule field differs) | **breaking** (`major`) | conversation eval set + past graded traces are now wrong |
40
- | `policy_hash` unchanged, only metadata (`policy_id`) differs | `non_breaking` (`minor`) | same policy, no rule changed |
41
- | introduced (absent → present) | `non_breaking` | the old eval set tested no policy; additive |
42
- | removed (present → absent) | **breaking** (`major`) | past data graded under the policy is now ungoverned |
43
-
44
- Reason code: `behavioral_policy_changed`. Configurable per-surface via `CompatibilityPolicy.behavioral_policy`
45
- (defaults: `on_major` → `drop`).
46
-
47
- ## Hash-safety
48
-
49
- `behavioral_policy` is **optional and omitted by default**. The `overall_hash` is computed only over the
50
- surfaces actually present in a contract, so adding this surface to the spec does **not** change the hash
51
- of any existing (policy-less) manifest. The frozen hash-vectors are unchanged.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes