From ed0b1c79a9eef1ab6406c0538b1fd0d07c604474 Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Thu, 10 Sep 2026 18:15:54 +0300 Subject: [PATCH 1/4] Propose durable agent state schema 2.0 contract Define a draft language-neutral execution/completion contract, synthetic review fixtures, and explicit cross-runtime rollout prerequisites without activating runtime writes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 532fe4f5-939b-4962-989d-a1883dccd283 --- schemas/README.md | 171 ++++++++++++ schemas/durable-agent-entity-state.json | 261 +++++++++++++++++- schemas/fixtures/README.md | 41 +++ ...-durable-agent-state-1.2-python-shape.json | 168 +++++++++++ ...shared-durable-agent-state-2.0-pruned.json | 59 ++++ .../shared-durable-agent-state-2.0.json | 100 +++++++ 6 files changed, 789 insertions(+), 11 deletions(-) create mode 100644 schemas/README.md create mode 100644 schemas/fixtures/README.md create mode 100644 schemas/fixtures/shared-durable-agent-state-1.2-python-shape.json create mode 100644 schemas/fixtures/shared-durable-agent-state-2.0-pruned.json create mode 100644 schemas/fixtures/shared-durable-agent-state-2.0.json diff --git a/schemas/README.md b/schemas/README.md new file mode 100644 index 0000000..e3c084d --- /dev/null +++ b/schemas/README.md @@ -0,0 +1,171 @@ +# Proposed durable agent state 2.0 contract + +**Draft for joint Python, .NET, and Durable Task review; not runtime activation.** +This change contains only a JSON Schema, synthetic fixtures, and documentation. +Neither Python nor .NET is authorized to emit `2.0.0` by this proposal or by a +successful schema validation. Merge of this contract proposal requires agreement +on compatible-reader/consumer semantics and the rollout floor; emitting 2.0 +requires a separately reviewed implementation and an enforced deployment gate. + +The motivation is to separate execution/delivery completion from conversation +history that may be compacted or evicted. Preserving unknown JSON alone cannot +prevent duplicate execution if a reader still uses transcript responses to +decide whether a request completed. [ADR discussion #88](https://github.com/microsoft/agent-framework-durable-extension/pull/88) +provides related compaction/retention context, not a mandate for this exact schema. + +## Version and reader policy + +The [Draft 2020-12 schema](durable-agent-entity-state.json) describes exact review +snapshots `1.0.0`, `1.1.0`, proposed `1.2.0`, and proposed `2.0.0`; it is not a +list of versions supported by today's runtimes. The proposed 1.2 fields are +included for compatibility discussion, not claimed as released Python output. +Legacy transcript entries retain the existing permissive entry shape. The +three mailbox/binding fields are forbidden in 1.x, even if empty. Version 2.0 +requires all three plus `conversationHistory` (which can be empty). + +Major 2 is proposed because the authority for completion and replay changes, +not merely because new optional properties appear. **Is a major version the +right mechanism, or can maintainers enforce an equally safe same-major rollout +gate?** This schema intentionally rejects unlisted versions, including future +2.x versions; accepting them must be a deliberate semantic compatibility +decision, not just a numeric SemVer comparison. + +At upstream main `62afdbac03f0a81b0917abe6328203b706a2f294`, +Python's `DurableAgentState.from_dict` checks for a version's presence but does +not gate its value, and `DurableAgentStateData.from_dict` reads the transcript +without retaining unknown data-level fields. This is a compatibility gap to +resolve jointly, not a criticism or a claim that Python already supports 2.0. +The revised C# preview instead rejects unsupported versions; its acceptance +rules are not a shared runtime guarantee. A new major number alone does not +protect deployments whose existing readers do not enforce it. + +## Proposed wire concepts and semantic invariants + +| Field | Proposed meaning | +| --- | --- | +| `terminalResults[correlationId]` | Immutable terminal response/error envelope, detached from transcript retention. | +| `completionReceipts[correlationId]` | Independent completion evidence, retained after payload expiry and transcript pruning. | +| Receipt `resultState` | `available` with a matching payload, or `unavailable` with a removal timestamp. | +| `historyBinding` | Version 1 binding: `ownerKind` and stable, non-secret logical `providerKey`, fixed for one session generation. | +| `conversationHistory` | Evictable transcript, never the authoritative completion index in 2.0. | + +The schema validates local shape, not all cross-object or temporal invariants. +Any future implementation must additionally enforce: + +- Keys equal the embedded `correlationId`, compared exactly and case-sensitively + without normalization, within one durable entity/session generation. + Request identities must not be reused for different work in that generation. + Every terminal result has exactly one matching receipt. Outcome, completion + instant, and expiry instant (including absence) agree between them. +- Terminal result contents and receipt identity/outcome/completion/expiry are + immutable. Persisting completion must atomically publish the result and + receipt. Availability can transition only from `available` to `unavailable`, + atomically removing the result and recording `resultUnavailableAt`; it never + reopens execution or changes a success into a failure. +- An `available` receipt requires a result; an `unavailable` receipt forbids one. + No receipt means only **no recorded terminal completion**. It means pending + only for a request independently known to have been accepted; unknown request + identities are not automatically pending. This proposal adds no request + admission registry and makes no exactly-once claim for external side effects. +- `resultExpiresAt`, when present, is no earlier than `completedAt`. At or after + that instant a poller must report completed-but-result-unavailable even if a + lazy cleanup has not yet removed a stored `available` payload. It must not + deliver the expired payload, report pending, or rerun the request. + `resultUnavailableAt` is no earlier than completion; for a time-expired + payload it is no earlier than expiry. `unavailable` can also represent an + agreed explicit removal policy; absence of `resultExpiresAt` is not a + guarantee against such removal. +- Receipts survive payload expiry and transcript pruning for the agreed + duplicate-delivery lifetime. No receipt eviction or session-ID reuse policy + is specified here. Whole-entity TTL/deletion and receipt storage growth must + be resolved before deployment, not silently treated as transcript retention. + +The proposed envelope requires `response.messages` for success and failure +(an empty list is allowed). Failure additionally requires `error.code` and +`error.message`; success forbids `error`. Error details and response metadata +are JSON only. A failure's partial messages are diagnostic output, not an +instruction to replay a failed turn. Whether failures should instead use an +error-only union, and how cancellation is represented, remain review questions. + +Owner kinds are `durableState`, `historyProvider`, and `modelService`. Every kind +currently requires a `providerKey`, including a logical local-owner key for +`durableState`. Keys identify configuration, not credentials, endpoints, +runtime type names, or opaque session properties. The binding is not an +authorization grant; trusted host configuration must recognize it. The fixed-owner +C# proposal is a runtime-specific design question, **not an agreed shared +requirement**. Please review whether this field belongs in the common contract, +whether ownership can change, and how a session generation/migration is identified. + +## Existing state, extension data, and trust boundaries + +`session` is opaque JSON continuation/provider state. Preserve it, but do not +infer an owner or instantiate a runtime type from its contents. The optional +terminal `continuationToken` is base64 bytes; cross-language encoding does not +prove cross-provider resumability. Its byte contract needs joint agreement. + +`expirationTimeUtc` documents the existing whole-entity idle TTL field; absent +or null means no stored deadline. Deleting the entity also deletes receipts: +this is distinct from `resultExpiresAt` and ends any in-entity deduplication +evidence. An external tombstone, bounded request lifetime, or session-generation +policy must address late duplicates before such deletion is compatible with 2.0. + +`ingestedPositions` retains the proposed 1.2 highest-contiguous-position +watermark by producer; it is neither an exact receipt set nor a completion +index. `truncation` records evicted message count and first/last eviction +instants (last must be no earlier than first). It is diagnostic evidence, not +model context. Optional `messageId` is message identity, not request identity. + +Unknown properties are allowed and must round-trip **at their original object +locations**, independently of explicit `extensionData` objects, including +nested content and mailbox fields. Known fields must still satisfy their +declared types; do not hide malformed values in extension data. This preservation +rule is a requirement on future compatible readers/writers, not a description +of every current serializer. Metadata cannot override known envelope fields. + +Unknown properties are not unknown discriminators: 2.0 accepts only transcript +`$type` values `request`, `response`, `errorResponse`, and `compaction`. +Compaction has no `correlationId`. Content `$type` values are `data`, `error`, +`functionCall`, `functionResult`, `hostedFile`, `hostedVectorStore`, `usage`, +`text`, `reasoning`, `uri`, and the explicit `unknown` wrapper. Unsupported +versions, owner kinds, binding versions, outcomes, availability values, roles, +or discriminators must be rejected for processing, not silently converted to +success or to an empty unknown-content wrapper. Opaque `unknown.content` itself +can be any JSON value, including null; `$runtimeType` inside it is just data. + +Persisted JSON is untrusted data. Never dynamically load types, follow URIs, +execute tool calls, or log opaque session/error/token contents merely by reading +state. Normal host authorization, redaction, and total storage/depth limits are +still required. Identifier limits count Unicode code points, not UTF-16 units. +Identifiers are nonblank and exclude C0/C1 controls; metadata is not executable. +Usage metadata retains arbitrary JSON even if a runtime cannot represent it as +numeric counts. Integer ranges, timestamp precision, and provider-specific +continuation formats require cross-language agreement; validation alone does +not ensure lossless projection. No provider or retention policy is implemented. + +## Rollout, migration, and maintainer questions + +Before any runtime emits 2.0, agree on and enforce a deployment floor covering +state readers, duplicate lookups, pollers, writers, rollback writers, hosting +consumers, and tools such as the scheduler dashboard. Participants that may +process 2.0 must implement its behavior; others must reject it before processing +or mutation and be isolated from 2.0 routing. Merely preserving fields is not +enough. Rollback to a transcript-only writer must be prevented once 2.0 exists. + +Do not migrate by changing only `schemaVersion` or by adding empty receipt maps +to previously used 1.x state. Pruned transcript cannot prove prior completion +or reconstruct immutable responses. Migration needs authoritative completion +evidence or an explicitly isolated new session generation with agreed duplicate +handling. This PR contains neither a migration nor code to enable revised writes. + +Feedback requested from Ahmed/Python maintainers and .NET/Durable Task maintainers: + +1. Schema shape, correlation scope, major 2 versus an enforceable same-major gate. +2. Shared ownership binding versus the runtime-specific fixed-owner C# proposal. +3. Success/error envelope, cancellation, and continuation-byte interoperability. +4. Result expiry/removal semantics, poller behavior, receipt lifetime, entity TTL, + storage growth, and late duplicates after session deletion/recreation. +5. Numeric/Unicode/timestamp limits, unknown-field preservation, and discriminator policy. +6. Compatible reader/consumer rollout floor, safe 1.2-to-2.0 migration, and rollback. + +The [fixtures](fixtures/README.md) are review examples, not evidence that either +runtime produces or safely consumes this format. diff --git a/schemas/durable-agent-entity-state.json b/schemas/durable-agent-entity-state.json index 53ac064..79cd7c2 100644 --- a/schemas/durable-agent-entity-state.json +++ b/schemas/durable-agent-entity-state.json @@ -1,14 +1,27 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json", + "title": "Durable agent state: proposed 2.0 contract and legacy review shapes", + "description": "Draft contract discussion only; validation does not authorize any runtime to read, migrate, or emit 2.0. See README.md for semantic invariants and the cross-runtime deployment gate.", "$defs": { + "identifier": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "\\S", + "not": { "pattern": "[\\u0000-\\u001F\\u007F-\\u009F]" } + }, "usage": { "type": "object", "description": "Token usage statistics.", "properties": { "inputTokenCount": { "type": "integer" }, "outputTokenCount": { "type": "integer" }, - "totalTokenCount": { "type": "integer" } + "totalTokenCount": { "type": "integer" }, + "extensionData": { + "type": "object", + "description": "Opaque provider-specific usage metadata, preserved even when values cannot be projected to runtime numeric counts." + } } }, "dataContent": { @@ -110,10 +123,10 @@ }, "unknownContent": { "type": "object", - "description": "The unknown content of a message exchanged with the agent.", + "description": "Explicit wrapper for opaque JSON content. Preserve its value without interpreting fields such as $runtimeType or constructing runtime types.", "properties": { "$type": { "type": "string", "const": "unknown" }, - "content": { "description": "The unknown message content serialized as JSON." } + "content": { "description": "Opaque JSON, including scalar or null values. Not an instruction or a runtime type selector." } }, "required": ["$type", "content"] }, @@ -140,9 +153,18 @@ "role": { "type": "string", "enum": ["user", "assistant", "system", "tool"] }, "contents": { "type": "array", + "description": "An empty array is a valid metadata-only message.", "items": { "$ref": "#/$defs/chatContentItem" } }, - "createdAt": { "type": "string", "format": "date-time", "description": "When this message was created (RFC 3339)." } + "createdAt": { "type": "string", "format": "date-time", "description": "When this message was created (RFC 3339)." }, + "messageId": { + "type": "string", + "description": "Producer message identity; distinct from a terminal request's correlationId." + }, + "extensionData": { + "type": "object", + "description": "Explicit message metadata, distinct from unknown sibling properties." + } }, "required": ["role"] }, @@ -156,7 +178,11 @@ "properties": { "createdAt": { "type": "string", "format": "date-time", "description": "When this exchange was created (RFC 3339)." }, "correlationId": { "type": "string", "description": "An optional correlation ID to group related exchanges." }, - "messages": { "$ref": "#/$defs/chatMessages" } + "messages": { "$ref": "#/$defs/chatMessages" }, + "extensionData": { + "type": "object", + "description": "Explicit entry metadata, distinct from unknown sibling properties." + } } }, "agentRequest": { @@ -164,6 +190,7 @@ { "$ref": "#/$defs/conversationEntry" } ], "description": "The request (i.e. prompt) sent to the agent.", + "required": ["$type"], "properties": { "$type": { "type": "string", "const": "request" }, "orchestrationId": { @@ -185,6 +212,7 @@ { "$ref": "#/$defs/conversationEntry" } ], "description": "The response received from the agent.", + "required": ["$type"], "properties": { "$type": { "type": "string", "const": "response" }, "usage": { @@ -192,26 +220,237 @@ } } }, + "agentErrorResponse": { + "allOf": [ + { "$ref": "#/$defs/conversationEntry" } + ], + "description": "Diagnostic record of a failed turn, not model replay context or authoritative completion evidence.", + "required": ["$type"], + "properties": { + "$type": { "type": "string", "const": "errorResponse" }, + "usage": { "$ref": "#/$defs/usage" } + } + }, + "compaction": { + "allOf": [ + { "$ref": "#/$defs/conversationEntry" } + ], + "description": "Compacted model context; it answers no request and has no correlationId.", + "required": ["$type"], + "properties": { + "$type": { "type": "string", "const": "compaction" }, + "correlationId": false + } + }, + "terminalResponse": { + "type": "object", + "description": "JSON-only terminal response projection. No raw runtime object graph is persisted.", + "properties": { + "messages": { "$ref": "#/$defs/chatMessages" }, + "usage": { "$ref": "#/$defs/usage" }, + "createdAt": { "type": "string", "format": "date-time" }, + "responseId": { "$ref": "#/$defs/identifier" }, + "agentId": { "$ref": "#/$defs/identifier" }, + "finishReason": { "$ref": "#/$defs/identifier" }, + "continuationToken": { + "type": "string", + "maxLength": 16384, + "contentEncoding": "base64", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "not": { "pattern": "[^A-Za-z0-9+/=]" }, + "description": "Opaque base64-encoded continuation bytes. Encoding alone does not imply that another runtime or provider can resume them." + }, + "extensionData": { + "type": "object", + "description": "Explicit JSON response metadata; preserve separately from unknown sibling properties.", + "propertyNames": { "$ref": "#/$defs/identifier" } + } + }, + "required": ["messages"], + "additionalProperties": true + }, + "terminalError": { + "type": "object", + "description": "Sanitized JSON terminal failure metadata, not a serialized exception or executable type.", + "properties": { + "code": { "$ref": "#/$defs/identifier" }, + "message": { + "type": "string", + "minLength": 1, + "maxLength": 16384, + "pattern": "\\S" + }, + "details": { "description": "Optional JSON failure details; subject to privacy and storage limits." } + }, + "required": ["code", "message"], + "additionalProperties": true + }, + "terminalResult": { + "type": "object", + "description": "Immutable terminal result detached from conversationHistory. Map-key equality and receipt consistency require semantic validation.", + "properties": { + "correlationId": { "$ref": "#/$defs/identifier" }, + "outcome": { "type": "string", "enum": ["succeeded", "failed"] }, + "completedAt": { "type": "string", "format": "date-time" }, + "resultExpiresAt": { "type": "string", "format": "date-time" }, + "response": { "$ref": "#/$defs/terminalResponse" }, + "error": { "$ref": "#/$defs/terminalError" } + }, + "required": ["correlationId", "outcome", "completedAt", "response"], + "allOf": [ + { + "if": { "properties": { "outcome": { "const": "succeeded" } } }, + "then": { "not": { "required": ["error"] } } + }, + { + "if": { "properties": { "outcome": { "const": "failed" } } }, + "then": { "required": ["error"] } + } + ], + "additionalProperties": true + }, + "completionReceipt": { + "type": "object", + "description": "Completion evidence surviving result expiry. Identity, outcome, completion time, and expiry policy are immutable; availability may transition once from available to unavailable.", + "properties": { + "correlationId": { "$ref": "#/$defs/identifier" }, + "outcome": { "type": "string", "enum": ["succeeded", "failed"] }, + "completedAt": { "type": "string", "format": "date-time" }, + "resultState": { "type": "string", "enum": ["available", "unavailable"] }, + "resultExpiresAt": { "type": "string", "format": "date-time" }, + "resultUnavailableAt": { "type": "string", "format": "date-time" } + }, + "required": ["correlationId", "outcome", "completedAt", "resultState"], + "allOf": [ + { + "if": { "properties": { "resultState": { "const": "available" } } }, + "then": { "not": { "required": ["resultUnavailableAt"] } } + }, + { + "if": { "properties": { "resultState": { "const": "unavailable" } } }, + "then": { "required": ["resultUnavailableAt"] } + } + ], + "additionalProperties": true + }, + "historyBinding": { + "type": "object", + "description": "Proposed fixed owner for one durable session generation. Stable logical provider identity is non-secret and never inferred from runtime type names or opaque session data.", + "properties": { + "version": { "type": "integer", "const": 1 }, + "ownerKind": { + "type": "string", + "enum": ["durableState", "historyProvider", "modelService"] + }, + "providerKey": { "$ref": "#/$defs/identifier" } + }, + "required": ["version", "ownerKind", "providerKey"], + "additionalProperties": true + }, "data": { "type": "object", "description": "The durable agent's state data.", "properties": { "conversationHistory": { "type": "array", - "description": "Ordered list of conversation entries.", + "description": "Ordered, evictable transcript. Legacy entry shape remains permissive; 2.0 requires known entry discriminators.", "items": { "$ref": "#/$defs/conversationEntry" } + }, + "terminalResults": { + "type": "object", + "description": "Immutable results keyed by exact, case-sensitive request correlationId within this entity/session generation.", + "propertyNames": { "$ref": "#/$defs/identifier" }, + "additionalProperties": { "$ref": "#/$defs/terminalResult" } + }, + "completionReceipts": { + "type": "object", + "description": "Terminal evidence keyed by correlationId; independent of transcript and result-payload retention.", + "propertyNames": { "$ref": "#/$defs/identifier" }, + "additionalProperties": { "$ref": "#/$defs/completionReceipt" } + }, + "historyBinding": { "$ref": "#/$defs/historyBinding" }, + "session": { + "type": "object", + "description": "Opaque JSON session continuation and provider state. Preserve without using its fields to select or construct runtime types." + }, + "expirationTimeUtc": { + "type": ["string", "null"], + "format": "date-time", + "description": "Existing whole-entity idle TTL deadline, not result expiry. Absent or null means no stored deadline. Whole-entity deletion removes receipts too; a separate deduplication-lifetime agreement is required." + }, + "ingestedPositions": { + "type": "object", + "description": "Proposed legacy 1.2 scalar watermark: highest contiguous position ingested from each producer. Not request completion evidence.", + "additionalProperties": { "type": "integer", "minimum": 0 } + }, + "truncation": { + "type": "object", + "description": "Diagnostic evidence of transcript removal; not model context or terminal completion evidence.", + "properties": { + "evictedMessageCount": { "type": "integer", "minimum": 1 }, + "firstEvictedAt": { "type": "string", "format": "date-time" }, + "lastEvictedAt": { "type": "string", "format": "date-time" } + }, + "required": ["evictedMessageCount", "firstEvictedAt", "lastEvictedAt"] + }, + "extensionData": { + "type": "object", + "description": "Explicit data metadata; preserve separately from unknown sibling properties." } - } + }, + "additionalProperties": true } }, "type": "object", "properties": { "schemaVersion": { "type": "string", - "description": "Semantic version of this state schema. By convention, this should be the first property.", - "pattern": "^\\d+\\.\\d+\\.\\d+$" + "description": "Exact review snapshots, not a runtime acceptance list. 1.2.0 and 2.0.0 are proposals; future versions require an explicit compatibility decision. By convention this is the first property.", + "enum": ["1.0.0", "1.1.0", "1.2.0", "2.0.0"] }, - "data": { "$ref": "#/$defs/data" } + "data": { "$ref": "#/$defs/data" }, + "extensionData": { + "type": "object", + "description": "Explicit root metadata; preserve separately from unknown sibling properties." + } }, - "required": ["schemaVersion", "data"] + "required": ["schemaVersion", "data"], + "additionalProperties": true, + "allOf": [ + { + "if": { + "properties": { "schemaVersion": { "const": "2.0.0" } } + }, + "then": { + "properties": { + "data": { + "required": ["conversationHistory", "terminalResults", "completionReceipts", "historyBinding"], + "properties": { + "conversationHistory": { + "items": { + "oneOf": [ + { "$ref": "#/$defs/agentRequest" }, + { "$ref": "#/$defs/agentResponse" }, + { "$ref": "#/$defs/agentErrorResponse" }, + { "$ref": "#/$defs/compaction" } + ] + } + } + } + } + } + }, + "else": { + "properties": { + "data": { + "properties": { + "terminalResults": false, + "completionReceipts": false, + "historyBinding": false + } + } + } + } + } + ] } diff --git a/schemas/fixtures/README.md b/schemas/fixtures/README.md new file mode 100644 index 0000000..fb02b00 --- /dev/null +++ b/schemas/fixtures/README.md @@ -0,0 +1,41 @@ +# Durable agent state review fixtures + +All JSON files here are synthetic, language-neutral review data validated with +the parent [Draft 2020-12 schema](../durable-agent-entity-state.json). +They are not captured production state, generated serializer snapshots, or +evidence of Python/.NET 2.0 support. + +| Fixture | Provenance and purpose | +| --- | --- | +| `shared-durable-agent-state-1.2-python-shape.json` | Reproduced from proposal commit `247bbdd60944d5ac93e79079803aa23e992d0369`. Modeled on parallel Python-shaped 1.2 work, with synthetic future fields and content for preservation review. **Not byte-for-byte output from the current Python serializer.** | +| `shared-durable-agent-state-2.0.json` | Reproduced from the same proposal commit. Synthetic available success and expired failure receipt, detached from transcript responses; includes unknown mailbox/binding fields. | +| `shared-durable-agent-state-2.0-pruned.json` | Authored for this contract-only proposal. Empty transcript with available failure, unavailable success, opaque session data, truncation evidence, and whole-entity TTL. Not a migration output. | + +The source proposal builds on `5de13e8d5dd4b4b76e7360e89ceeb3968a103781`; +its runtime DTOs, converters, tests, and test-project fixture links are +deliberately excluded. The corrected `python-shape` filename identifies +provenance, not a promise of current serializer behavior. There is no fixture +generator or test-project integration in this PR. + +For `shared-durable-agent-state-2.0.json`, interpret the example at +`2026-09-10T05:00:05Z`: `corr-2` has an available result and `corr-expired` +proves completed failure without a payload. After `corr-2`'s expiry a compatible +poller must report completed-but-result-unavailable even before cleanup. + +For `shared-durable-agent-state-2.0-pruned.json`, interpret the example at +`2026-09-10T06:00:05Z`: `corr-failed` has an available failure payload and +`corr-pruned` proves completed success without one. Transcript removal did not +erase either completion. `expirationTimeUtc` is a separate whole-entity deadline, +not a proposed resolution of tombstone lifetime after entity deletion. + +For an identity absent from the receipt maps, both examples show only the +absence of recorded completion: a separately accepted request may be pending, +whereas an unrecognized identity remains unknown. No fixture invents an +admission registry, claims exactly-once external side effects, or authorizes +schema-version-only migration. The 1.2 and 2.0 examples are independent +snapshots, not a before/after pair with inferred completion evidence. + +Validation must enable Draft 2020-12 and date-time format checking, then check +the cross-map/time invariants in [the proposal](../README.md). JSON Schema alone +cannot enforce key equality, atomicity, historical immutability, expiry relative +to a clock, or compatibility of a deployed reader. diff --git a/schemas/fixtures/shared-durable-agent-state-1.2-python-shape.json b/schemas/fixtures/shared-durable-agent-state-1.2-python-shape.json new file mode 100644 index 0000000..ada0f64 --- /dev/null +++ b/schemas/fixtures/shared-durable-agent-state-1.2-python-shape.json @@ -0,0 +1,168 @@ +{ + "schemaVersion": "1.2.0", + "data": { + "conversationHistory": [ + { + "$type": "request", + "correlationId": "corr-python", + "createdAt": "2026-07-27T12:34:50+00:00", + "messages": [ + { + "role": "user", + "messageId": "producer-request-id", + "extensionData": { + "origin": "python" + }, + "contents": [ + { + "$type": "text", + "text": "hello" + } + ] + } + ], + "extensionData": { + "requestFuture": true + } + }, + { + "$type": "response", + "correlationId": "corr-python", + "createdAt": "2026-07-27T12:34:51+00:00", + "messages": [ + { + "role": "assistant", + "authorName": "python-agent", + "createdAt": "2026-07-27T12:34:51+00:00", + "messageId": "python-metadata-only", + "extensionData": { + "metadataOrigin": "python" + }, + "contents": [] + }, + { + "role": "assistant", + "contents": [ + { + "$type": "text", + "text": "world" + } + ] + }, + { + "role": "assistant", + "contents": [ + { + "$type": "reasoning", + "text": "private reasoning" + } + ] + }, + { + "role": "assistant", + "messageId": "python-unknown-content", + "contents": [ + { + "$type": "unknown", + "content": { + "$runtimeType": "python-owned-user-field", + "type": "future_python_content", + "payload": "python-value", + "annotations": [ + { + "kind": "citation", + "value": "python-ref" + } + ], + "additional_properties": { + "producer": "python" + }, + "future_payload": { + "nested": [ + 1, + 2, + 3 + ] + } + } + } + ] + } + ], + "usage": { + "inputTokenCount": 4, + "outputTokenCount": 2, + "totalTokenCount": 6, + "extensionData": { + "providerCount": 7, + "futureNumeric": 11, + "futureString": "seven", + "futureObject": { + "count": 8 + }, + "futureArray": [ + 9 + ] + } + } + }, + { + "$type": "errorResponse", + "correlationId": "corr-error", + "createdAt": "2026-07-27T12:34:52+00:00", + "messages": [ + { + "role": "assistant", + "contents": [ + { + "$type": "error", + "message": "failed", + "errorCode": "Example" + } + ] + } + ] + }, + { + "$type": "compaction", + "createdAt": "2026-07-27T12:34:56.123456+00:00", + "messages": [ + { + "role": "assistant", + "contents": [ + { + "$type": "text", + "text": "summary" + } + ] + } + ] + } + ], + "session": { + "type": "session", + "session_id": "@dafx-agent@session", + "state": { + "custom": { + "value": 1 + } + } + }, + "ingestedPositions": { + "input": 0, + "writer": 3 + }, + "extensionData": { + "dataProducer": "python" + }, + "futureDataProperty": { + "preserve": true + } + }, + "extensionData": { + "rootProducer": "interop-fixture" + }, + "futureRootProperty": { + "preserve": true + } +} diff --git a/schemas/fixtures/shared-durable-agent-state-2.0-pruned.json b/schemas/fixtures/shared-durable-agent-state-2.0-pruned.json new file mode 100644 index 0000000..6ac7777 --- /dev/null +++ b/schemas/fixtures/shared-durable-agent-state-2.0-pruned.json @@ -0,0 +1,59 @@ +{ + "schemaVersion": "2.0.0", + "data": { + "conversationHistory": [], + "terminalResults": { + "corr-failed": { + "correlationId": "corr-failed", + "outcome": "failed", + "completedAt": "2026-09-10T06:00:00Z", + "response": { + "messages": [] + }, + "error": { + "code": "example_dependency_unavailable", + "message": "The example dependency could not complete the request.", + "details": { + "synthetic": true + } + } + } + }, + "completionReceipts": { + "corr-failed": { + "correlationId": "corr-failed", + "outcome": "failed", + "completedAt": "2026-09-10T06:00:00Z", + "resultState": "available" + }, + "corr-pruned": { + "correlationId": "corr-pruned", + "outcome": "succeeded", + "completedAt": "2026-09-09T06:00:00Z", + "resultState": "unavailable", + "resultExpiresAt": "2026-09-10T06:00:00Z", + "resultUnavailableAt": "2026-09-10T06:00:01Z" + } + }, + "historyBinding": { + "version": 1, + "ownerKind": "durableState", + "providerKey": "example.local-history.v1" + }, + "session": { + "exampleContinuation": { + "cursor": "synthetic-cursor", + "$runtimeType": "opaque-user-data" + } + }, + "expirationTimeUtc": "2026-10-10T06:00:00Z", + "ingestedPositions": { + "example-producer": 3 + }, + "truncation": { + "evictedMessageCount": 4, + "firstEvictedAt": "2026-09-10T06:00:02Z", + "lastEvictedAt": "2026-09-10T06:00:03Z" + } + } +} diff --git a/schemas/fixtures/shared-durable-agent-state-2.0.json b/schemas/fixtures/shared-durable-agent-state-2.0.json new file mode 100644 index 0000000..3a2702d --- /dev/null +++ b/schemas/fixtures/shared-durable-agent-state-2.0.json @@ -0,0 +1,100 @@ +{ + "schemaVersion": "2.0.0", + "data": { + "conversationHistory": [ + { + "$type": "request", + "correlationId": "corr-2", + "createdAt": "2026-09-10T05:00:00+00:00", + "messages": [ + { + "role": "user", + "messageId": "request-2", + "contents": [ + { + "$type": "text", + "text": "Generate the artifact." + } + ] + } + ] + } + ], + "terminalResults": { + "corr-2": { + "correlationId": "corr-2", + "outcome": "succeeded", + "completedAt": "2026-09-10T05:00:03+00:00", + "resultExpiresAt": "2026-09-11T05:00:03+00:00", + "response": { + "messages": [ + { + "role": "assistant", + "authorName": "fixture-agent", + "messageId": "response-2", + "contents": [ + { + "$type": "text", + "text": "Artifact generated." + }, + { + "$type": "uri", + "uri": "https://example.test/artifacts/2", + "mediaType": "application/json" + } + ] + } + ], + "usage": { + "inputTokenCount": 5, + "outputTokenCount": 3, + "totalTokenCount": 8 + }, + "createdAt": "2026-09-10T05:00:03+00:00", + "responseId": "response-id-2", + "agentId": "agent-id-2", + "finishReason": "stop", + "continuationToken": "AQID", + "extensionData": { + "region": "test", + "attempt": 1 + }, + "futureResponseField": { + "preserve": true + } + }, + "futureResultField": "preserve" + } + }, + "completionReceipts": { + "corr-2": { + "correlationId": "corr-2", + "outcome": "succeeded", + "completedAt": "2026-09-10T05:00:03+00:00", + "resultState": "available", + "resultExpiresAt": "2026-09-11T05:00:03+00:00", + "futureReceiptField": 7 + }, + "corr-expired": { + "correlationId": "corr-expired", + "outcome": "failed", + "completedAt": "2026-09-09T05:00:03+00:00", + "resultState": "unavailable", + "resultExpiresAt": "2026-09-10T05:00:03+00:00", + "resultUnavailableAt": "2026-09-10T05:00:04+00:00" + } + }, + "historyBinding": { + "version": 1, + "ownerKind": "historyProvider", + "providerKey": "contoso.support-history.v1", + "futureBindingField": "preserve" + }, + "futureDataField": { + "preserve": true + } + }, + "futureRootField": { + "preserve": true + } +} From efed11f7786332d8bb0447ddbfc1727ddd7de09b Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Thu, 10 Sep 2026 18:19:28 +0300 Subject: [PATCH 2/4] Record schema contract review approval Record independent gpt-5.6-sol ACCEPT of the unchanged schema-only tree 905127424625d998e3b8d27a9c42f034d78b82ff. Keep the proposal commit unchanged and record both required trailers together in an append-only review record. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 532fe4f5-939b-4962-989d-a1883dccd283 From 664db2d5694700fbba21f0c434f259c02c3f7617 Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Fri, 11 Sep 2026 13:37:12 +0300 Subject: [PATCH 3/4] Address cross-runtime schema contract review feedback Constrain v2 correlations, preserve supported JSON message and structured result shapes, make the provisional binding optional, and clarify legacy ingestion and entity-local commit semantics. Add language-neutral validation cases without runtime activation. Binding representation and expired lookup outcome remain pending agreement. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 532fe4f5-939b-4962-989d-a1883dccd283 --- schemas/README.md | 111 +++++++-- schemas/durable-agent-entity-state.json | 34 ++- schemas/fixtures/README.md | 24 +- ...ared-durable-agent-state-2.0-lossless.json | 77 ++++++ schemas/tests/README.md | 47 ++++ schemas/tests/validation-cases.json | 226 ++++++++++++++++++ 6 files changed, 489 insertions(+), 30 deletions(-) create mode 100644 schemas/fixtures/shared-durable-agent-state-2.0-lossless.json create mode 100644 schemas/tests/README.md create mode 100644 schemas/tests/validation-cases.json diff --git a/schemas/README.md b/schemas/README.md index e3c084d..a6c3b2b 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -1,6 +1,6 @@ # Proposed durable agent state 2.0 contract -**Draft for joint Python, .NET, and Durable Task review; not runtime activation.** +**Proposal for joint Python, .NET, and Durable Task review; not runtime activation.** This change contains only a JSON Schema, synthetic fixtures, and documentation. Neither Python nor .NET is authorized to emit `2.0.0` by this proposal or by a successful schema validation. Merge of this contract proposal requires agreement @@ -21,7 +21,9 @@ list of versions supported by today's runtimes. The proposed 1.2 fields are included for compatibility discussion, not claimed as released Python output. Legacy transcript entries retain the existing permissive entry shape. The three mailbox/binding fields are forbidden in 1.x, even if empty. Version 2.0 -requires all three plus `conversationHistory` (which can be empty). +requires `terminalResults`, `completionReceipts`, and `conversationHistory` +(which can be empty). `historyBinding` is optional and provisional, not a +requirement for session-fixed effective ownership. Major 2 is proposed because the authority for completion and replay changes, not merely because new optional properties appear. **Is a major version the @@ -46,7 +48,7 @@ protect deployments whose existing readers do not enforce it. | `terminalResults[correlationId]` | Immutable terminal response/error envelope, detached from transcript retention. | | `completionReceipts[correlationId]` | Independent completion evidence, retained after payload expiry and transcript pruning. | | Receipt `resultState` | `available` with a matching payload, or `unavailable` with a removal timestamp. | -| `historyBinding` | Version 1 binding: `ownerKind` and stable, non-secret logical `providerKey`, fixed for one session generation. | +| `historyBinding` | Optional, provisional version 1 configuration descriptor; not a session-fixed effective owner. Representation remains under discussion. | | `conversationHistory` | Evictable transcript, never the authoritative completion index in 2.0. | The schema validates local shape, not all cross-object or temporal invariants. @@ -54,12 +56,20 @@ Any future implementation must additionally enforce: - Keys equal the embedded `correlationId`, compared exactly and case-sensitively without normalization, within one durable entity/session generation. + When present on a v2 request, response, or error response, the transcript + `correlationId` uses the same `identifier` constraints as the result maps. + Legacy entry handling is unchanged; compaction still has no correlation. Request identities must not be reused for different work in that generation. Every terminal result has exactly one matching receipt. Outcome, completion instant, and expiry instant (including absence) agree between them. - Terminal result contents and receipt identity/outcome/completion/expiry are - immutable. Persisting completion must atomically publish the result and - receipt. Availability can transition only from `available` to `unavailable`, + immutable. At the entity operation boundary, the result and receipt must + commit atomically with that operation's session/continuation, ingestion, + entity-local transcript, TTL, binding, and other local control-state changes. + They must not commit independently of the corresponding local state. + External provider writes and tool effects are outside this local transaction, + as discussed in the [ADR](https://github.com/microsoft/agent-framework-durable-extension/pull/88). + Availability can transition only from `available` to `unavailable`, atomically removing the result and recording `resultUnavailableAt`; it never reopens execution or changes a success into a failure. - An `available` receipt requires a result; an `unavailable` receipt forbids one. @@ -80,6 +90,13 @@ Any future implementation must additionally enforce: is specified here. Whole-entity TTL/deletion and receipt storage growth must be resolved before deployment, not silently treated as transcript retention. +**Expired lookup outcome is pending agreement.** The proposed caller-visible +behavior is completed-but-result-unavailable plus the retained `succeeded` or +`failed` outcome, with no expired payload. This would require explicit ADR +wording and a Python receipt/lookup update; it is not a claim about current +Python behavior. The schema already preserves outcome after expiry, but this +proposal does not settle the lookup API's shape or enable any runtime behavior. + The proposed envelope requires `response.messages` for success and failure (an empty list is allowed). Failure additionally requires `error.code` and `error.message`; success forbids `error`. Error details and response metadata @@ -87,14 +104,36 @@ are JSON only. A failure's partial messages are diagnostic output, not an instruction to replay a failed turn. Whether failures should instead use an error-only union, and how cancellation is represented, remain review questions. -Owner kinds are `durableState`, `historyProvider`, and `modelService`. Every kind -currently requires a `providerKey`, including a logical local-owner key for -`durableState`. Keys identify configuration, not credentials, endpoints, -runtime type names, or opaque session properties. The binding is not an -authorization grant; trusted host configuration must recognize it. The fixed-owner -C# proposal is a runtime-specific design question, **not an agreed shared -requirement**. Please review whether this field belongs in the common contract, -whether ownership can change, and how a session generation/migration is identified. +`response.value` is an optional, named caller-visible JSON result, independent +of messages and text. An absent field means no structured value; explicit +`null`, `false`, `0`, `""`, `[]`, and `{}` are present values and must survive +round-tripping without truthiness-based omission. No separate presence flag +is needed. Consumers must not infer `value` from text, coerce its type, or +serialize arbitrary model/runtime objects. Unknown nested properties remain +part of the value. The same JSON preservation rule applies if a failed response +carries a diagnostic value; its outcome remains failed. + +### Provisional configuration binding + +Stable provider/configuration identity is distinct from effective per-run +history ownership. The shared contract **does not require session-fixed +effective ownership** and must not prohibit Python's supported per-run +transitions. C# may separately enforce a fixed-owner policy/profile. + +For now, `historyBinding` is optional and keeps the existing review shape +(`version`, `ownerKind`, `providerKey`) without defining an effective-owner +transition protocol. If supplied, `ownerKind` describes the configured facility: +`durableState`, `historyProvider`, or `modelService`; it does not say that +facility owns every run. The non-secret `providerKey` identifies configuration, +not credentials, endpoints, runtime types, or opaque session properties. +Absence does not imply a default owner or authorize inference from session data. +This descriptor is not an authorization grant. + +The exact representation remains pending Ahmed's feedback: an optional shared +`providerBinding`/`configurationBinding`, or an optional runtime extension/profile. +No rename, new profile format, migration rule, or transition restriction is +introduced here. Trusted hosting configuration must still recognize any +descriptor it processes and separately validate supported per-run transitions. ## Existing state, extension data, and trust boundaries @@ -109,12 +148,44 @@ this is distinct from `resultExpiresAt` and ends any in-entity deduplication evidence. An external tombstone, bounded request lifetime, or session-generation policy must address late duplicates before such deletion is compatible with 2.0. -`ingestedPositions` retains the proposed 1.2 highest-contiguous-position -watermark by producer; it is neither an exact receipt set nor a completion -index. `truncation` records evicted message count and first/last eviction +`ingestedPositions` is a legacy highest-seen position by producer, **not proof +of a contiguous delivered prefix**, an exact receipt set, or terminal completion. +Migration must preserve its historical meaning and cannot infer that gaps were +delivered. After delivering `[1, 3]`, selecting `[2, 4]` must deliver both while +remembering that `3` was delivered. A scalar `3` alone cannot establish that. +Exact gap-preserving workflow receipt sets or ranges need separately versioned +bookkeeping with explicit producer/delivery identity, preservation, and migration +semantics, independent of transcript retention. This proposal neither invents +that wire format nor backfills receipts from the scalar. + +`truncation` records evicted message count and first/last eviction instants (last must be no earlier than first). It is diagnostic evidence, not model context. Optional `messageId` is message identity, not request identity. +### Lossless messages and JSON content + +Message roles include `developer`. Function-call `arguments` accepts the +original object or string; preserve the entire string, including whitespace +and incomplete/non-JSON text, without parsing it into an object. URI content +requires a URI but not a media type; absence stays absent rather than being +filled with guessed metadata. These are additive shared message shapes for +review, not evidence that existing 1.x consumers can handle them. + +For supported content not represented by a typed definition, a producer may +use the explicit `{"$type":"unknown","content":...}` wrapper with the complete +original JSON value, including its metadata. An opaque payload with its own +`type` or `$type` remains data and must not select or activate runtime types. +Only a safe, explicit JSON representation qualifies; no arbitrary object +reflection, `repr` fallback, or executable serialization is implied. If a +supported value cannot be preserved safely, report the incompatibility rather +than silently dropping it or claiming a lossless terminal result was persisted. + +Lossless here means JSON value preservation, including array order, exact +string contents, numeric fidelity, and absent versus null fields. It does not +require byte-identical JSON formatting or object-property order. Wrapping is a +producer mapping for supported unmodeled content, not permission for a reader +to hide malformed known wire fields or accept unknown wire discriminators. + Unknown properties are allowed and must round-trip **at their original object locations**, independently of explicit `extensionData` objects, including nested content and mailbox fields. Known fields must still satisfy their @@ -160,12 +231,14 @@ handling. This PR contains neither a migration nor code to enable revised writes Feedback requested from Ahmed/Python maintainers and .NET/Durable Task maintainers: 1. Schema shape, correlation scope, major 2 versus an enforceable same-major gate. -2. Shared ownership binding versus the runtime-specific fixed-owner C# proposal. +2. Optional configuration binding representation versus a runtime extension/profile, + without imposing session-fixed ownership as a shared rule. 3. Success/error envelope, cancellation, and continuation-byte interoperability. -4. Result expiry/removal semantics, poller behavior, receipt lifetime, entity TTL, +4. Proposed exposed outcome after expiry (pending agreement), receipt lifetime, entity TTL, storage growth, and late duplicates after session deletion/recreation. 5. Numeric/Unicode/timestamp limits, unknown-field preservation, and discriminator policy. 6. Compatible reader/consumer rollout floor, safe 1.2-to-2.0 migration, and rollback. The [fixtures](fixtures/README.md) are review examples, not evidence that either -runtime produces or safely consumes this format. +runtime produces or safely consumes this format. The language-neutral +[validation cases](tests/README.md) record positive and negative schema expectations. diff --git a/schemas/durable-agent-entity-state.json b/schemas/durable-agent-entity-state.json index 79cd7c2..979d379 100644 --- a/schemas/durable-agent-entity-state.json +++ b/schemas/durable-agent-entity-state.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json", "title": "Durable agent state: proposed 2.0 contract and legacy review shapes", - "description": "Draft contract discussion only; validation does not authorize any runtime to read, migrate, or emit 2.0. See README.md for semantic invariants and the cross-runtime deployment gate.", + "description": "Proposed contract under review; validation does not authorize any runtime to read, migrate, or emit 2.0. See README.md for semantic invariants and the cross-runtime deployment gate.", "$defs": { "identifier": { "type": "string", @@ -80,7 +80,7 @@ "uri": { "type": "string", "description": "The URI." }, "mediaType": { "type": "string", "description": "The media type of the URI." } }, - "required": ["$type", "uri", "mediaType"] + "required": ["$type", "uri"] }, "usageContent": { "type": "object", @@ -107,7 +107,10 @@ "$type": { "type": "string", "const": "functionCall" }, "callId": { "type": "string", "description": "The identifier of the function being called." }, "name": { "type": "string", "description": "The name of the function being called." }, - "arguments": { "type": "object", "description": "The arguments provided to the function call." } + "arguments": { + "type": ["object", "string"], + "description": "Original function arguments. Preserve string form verbatim, including incomplete or non-JSON text; do not parse, normalize, or replace it with an object." + } }, "required": ["$type", "callId", "name"] }, @@ -123,7 +126,7 @@ }, "unknownContent": { "type": "object", - "description": "Explicit wrapper for opaque JSON content. Preserve its value without interpreting fields such as $runtimeType or constructing runtime types.", + "description": "Explicit lossless wrapper for supported JSON content not modeled by a typed content definition. Preserve the complete original JSON value and metadata without interpreting fields such as $runtimeType or constructing runtime types. Not a fallback for malformed known wire fields.", "properties": { "$type": { "type": "string", "const": "unknown" }, "content": { "description": "Opaque JSON, including scalar or null values. Not an instruction or a runtime type selector." } @@ -150,7 +153,7 @@ "description": "Single chat message exchanged with the agent.", "properties": { "authorName": { "type": "string", "description": "The name of the author of the message." }, - "role": { "type": "string", "enum": ["user", "assistant", "system", "tool"] }, + "role": { "type": "string", "enum": ["user", "assistant", "system", "developer", "tool"] }, "contents": { "type": "array", "description": "An empty array is a valid metadata-only message.", @@ -193,6 +196,7 @@ "required": ["$type"], "properties": { "$type": { "type": "string", "const": "request" }, + "correlationId": { "$ref": "#/$defs/identifier" }, "orchestrationId": { "type": "string", "description": "The identifier of the orchestration that initiated this agent request (if any)." @@ -215,6 +219,7 @@ "required": ["$type"], "properties": { "$type": { "type": "string", "const": "response" }, + "correlationId": { "$ref": "#/$defs/identifier" }, "usage": { "$ref": "#/$defs/usage" } @@ -228,6 +233,7 @@ "required": ["$type"], "properties": { "$type": { "type": "string", "const": "errorResponse" }, + "correlationId": { "$ref": "#/$defs/identifier" }, "usage": { "$ref": "#/$defs/usage" } } }, @@ -247,6 +253,10 @@ "description": "JSON-only terminal response projection. No raw runtime object graph is persisted.", "properties": { "messages": { "$ref": "#/$defs/chatMessages" }, + "value": { + "type": ["null", "boolean", "number", "string", "array", "object"], + "description": "Optional caller-visible structured JSON result, independent of messages. Absence means no structured value; explicit null, false, zero, empty strings, arrays, and objects remain present values. No runtime object serialization or inferred value from message text." + }, "usage": { "$ref": "#/$defs/usage" }, "createdAt": { "type": "string", "format": "date-time" }, "responseId": { "$ref": "#/$defs/identifier" }, @@ -335,14 +345,18 @@ }, "historyBinding": { "type": "object", - "description": "Proposed fixed owner for one durable session generation. Stable logical provider identity is non-secret and never inferred from runtime type names or opaque session data.", + "description": "Provisional optional configuration descriptor, not a session-fixed effective owner. Retained shape pending agreement on a shared configuration binding versus a runtime extension/profile. It does not restrict supported per-run owner transitions or specify their representation.", "properties": { "version": { "type": "integer", "const": 1 }, "ownerKind": { "type": "string", - "enum": ["durableState", "historyProvider", "modelService"] + "enum": ["durableState", "historyProvider", "modelService"], + "description": "Category of the configured history facility, not a requirement that it owns history on every run." }, - "providerKey": { "$ref": "#/$defs/identifier" } + "providerKey": { + "$ref": "#/$defs/identifier", + "description": "Stable, non-secret logical configuration identity; never a runtime type name, credential, or identity inferred from opaque session state." + } }, "required": ["version", "ownerKind", "providerKey"], "additionalProperties": true @@ -380,7 +394,7 @@ }, "ingestedPositions": { "type": "object", - "description": "Proposed legacy 1.2 scalar watermark: highest contiguous position ingested from each producer. Not request completion evidence.", + "description": "Legacy highest-seen position by producer, not proof of a contiguous delivered prefix or request completion. Preserve historical values without inferring missing deliveries during migration. Exact gap-preserving workflow receipts require separately versioned bookkeeping independent of transcript retention.", "additionalProperties": { "type": "integer", "minimum": 0 } }, "truncation": { @@ -424,7 +438,7 @@ "then": { "properties": { "data": { - "required": ["conversationHistory", "terminalResults", "completionReceipts", "historyBinding"], + "required": ["conversationHistory", "terminalResults", "completionReceipts"], "properties": { "conversationHistory": { "items": { diff --git a/schemas/fixtures/README.md b/schemas/fixtures/README.md index fb02b00..7249349 100644 --- a/schemas/fixtures/README.md +++ b/schemas/fixtures/README.md @@ -10,6 +10,7 @@ evidence of Python/.NET 2.0 support. | `shared-durable-agent-state-1.2-python-shape.json` | Reproduced from proposal commit `247bbdd60944d5ac93e79079803aa23e992d0369`. Modeled on parallel Python-shaped 1.2 work, with synthetic future fields and content for preservation review. **Not byte-for-byte output from the current Python serializer.** | | `shared-durable-agent-state-2.0.json` | Reproduced from the same proposal commit. Synthetic available success and expired failure receipt, detached from transcript responses; includes unknown mailbox/binding fields. | | `shared-durable-agent-state-2.0-pruned.json` | Authored for this contract-only proposal. Empty transcript with available failure, unavailable success, opaque session data, truncation evidence, and whole-entity TTL. Not a migration output. | +| `shared-durable-agent-state-2.0-lossless.json` | Authored for review feedback. Synthetic developer-role request, verbatim string-form function arguments, URI without invented media type, complete opaque JSON content, explicit structured `false` value, and no binding. | The source proposal builds on `5de13e8d5dd4b4b76e7360e89ceeb3968a103781`; its runtime DTOs, converters, tests, and test-project fixture links are @@ -17,6 +18,11 @@ deliberately excluded. The corrected `python-shape` filename identifies provenance, not a promise of current serializer behavior. There is no fixture generator or test-project integration in this PR. +The two reproduced source fixtures are unchanged as JSON values. Their bindings +are now interpreted as provisional optional configuration descriptors, not proof +of session-fixed effective ownership. They do not settle the pending choice of +shared binding name/shape versus a runtime extension/profile. + For `shared-durable-agent-state-2.0.json`, interpret the example at `2026-09-10T05:00:05Z`: `corr-2` has an available result and `corr-expired` proves completed failure without a payload. After `corr-2`'s expiry a compatible @@ -28,7 +34,23 @@ For `shared-durable-agent-state-2.0-pruned.json`, interpret the example at erase either completion. `expirationTimeUtc` is a separate whole-entity deadline, not a proposed resolution of tombstone lifetime after entity deletion. -For an identity absent from the receipt maps, both examples show only the +The lossless fixture intentionally contains incomplete function-argument text. +The exact string must survive; parsing it, completing the JSON, or replacing it +with an object would lose information. The URI has no `mediaType` to infer. The +opaque content's nested `$type` and `$runtimeType` are inert JSON, including all +metadata, not type-activation instructions. `response.value: false` is a present +structured result; the earlier fixtures omit `value`. Null, zero, and empty +values have separate [validation cases](../tests/README.md). + +The lossless fixture's scalar `ingestedPositions["legacy-producer"] = 3` records +only highest-seen position. It does not say whether `2` was delivered. None of +these fixtures defines or infers a gap-preserving workflow receipt set. + +The expired receipts retain `outcome`; exposing that outcome with a +completed-but-result-unavailable lookup remains proposed pending agreement and +the corresponding ADR/Python lookup changes. No expired payload is restored. + +For an identity absent from the receipt maps, the examples show only the absence of recorded completion: a separately accepted request may be pending, whereas an unrecognized identity remains unknown. No fixture invents an admission registry, claims exactly-once external side effects, or authorizes diff --git a/schemas/fixtures/shared-durable-agent-state-2.0-lossless.json b/schemas/fixtures/shared-durable-agent-state-2.0-lossless.json new file mode 100644 index 0000000..a0b79df --- /dev/null +++ b/schemas/fixtures/shared-durable-agent-state-2.0-lossless.json @@ -0,0 +1,77 @@ +{ + "schemaVersion": "2.0.0", + "data": { + "conversationHistory": [ + { + "$type": "request", + "correlationId": "corr-lossless", + "messages": [ + { + "role": "developer", + "contents": [ + { + "$type": "text", + "text": "Preserve the original result." + } + ] + } + ] + } + ], + "terminalResults": { + "corr-lossless": { + "correlationId": "corr-lossless", + "outcome": "succeeded", + "completedAt": "2026-09-11T10:00:00Z", + "response": { + "messages": [ + { + "role": "assistant", + "contents": [ + { + "$type": "functionCall", + "callId": "tool-example", + "name": "example_tool", + "arguments": " { \"partial\": " + }, + { + "$type": "uri", + "uri": "https://example.test/media/1" + }, + { + "$type": "unknown", + "content": { + "$type": "example_unmodeled_media", + "$runtimeType": "opaque-data-only", + "bytes": "AQID", + "metadata": { + "presentNull": null, + "empty": [], + "annotations": [ + { + "kind": "synthetic" + } + ] + } + } + } + ] + } + ], + "value": false + } + } + }, + "completionReceipts": { + "corr-lossless": { + "correlationId": "corr-lossless", + "outcome": "succeeded", + "completedAt": "2026-09-11T10:00:00Z", + "resultState": "available" + } + }, + "ingestedPositions": { + "legacy-producer": 3 + } + } +} diff --git a/schemas/tests/README.md b/schemas/tests/README.md new file mode 100644 index 0000000..bd2faf0 --- /dev/null +++ b/schemas/tests/README.md @@ -0,0 +1,47 @@ +# Language-neutral schema validation cases + +`validation-cases.json` records positive and negative review expectations using +the JSON Schema Test Suite's group shape: `description`, `schema`, and `tests`; +each test has `description`, `data`, and `valid`. It is test data, not a durable +state fixture, product implementation, or runtime test-project integration. + +Resolve the canonical schema ID locally to `../durable-agent-entity-state.json`; +do not fetch it from GitHub, which might contain a different revision. Use a +Draft 2020-12 validator. The cases cover v2-only correlation constraints, +unchanged legacy/compaction handling, optional provisional binding, lossless +message shapes, structured-value presence, and historical ingestion scalars. + +For example, from the repository root with an existing Python `jsonschema` +installation, this PowerShell command runs the structural cases without any +network access or dependency installation: + +```powershell +@' +import json +from pathlib import Path +from jsonschema import Draft202012Validator +from referencing import Registry, Resource + +schema = json.loads(Path(r"schemas\durable-agent-entity-state.json").read_text(encoding="utf-8")) +Draft202012Validator.check_schema(schema) +registry = Registry().with_resource(schema["$id"], Resource.from_contents(schema)) +groups = json.loads(Path(r"schemas\tests\validation-cases.json").read_text(encoding="utf-8")) +count = 0 +for group in groups: + validator = Draft202012Validator(group["schema"], registry=registry) + for test in group["tests"]: + actual = validator.is_valid(test["data"]) + if actual != test["valid"]: + raise AssertionError(f'{group["description"]}: {test["description"]}') + count += 1 +print(f"Passed {count} structural validation cases") +'@ | python - +``` + +These cases have no timestamp-format assertions. Validate the separate state +fixtures with date-time format checking as well as the cross-map/time invariants +in the shared proposal. Test data alone cannot prove serializer round-tripping, +atomic entity commits, per-run ownership transitions, or runtime lookup behavior. +Future runtime implementations must additionally verify that original string +arguments, absent media types, opaque JSON metadata, and all present `value` +forms survive persistence without conversion, omission, or invented data. diff --git a/schemas/tests/validation-cases.json b/schemas/tests/validation-cases.json new file mode 100644 index 0000000..3bbe252 --- /dev/null +++ b/schemas/tests/validation-cases.json @@ -0,0 +1,226 @@ +[ + { + "description": "Version-scoped correlation IDs and optional binding", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json" }, + "tests": [ + { + "description": "V2 with empty transcript and no binding", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 request without a correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "request" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 response without a correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 error response without a correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "errorResponse" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 request with a valid correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "request", "correlationId": "Case-sensitive-id" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 response with a valid correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "correlationId": "Case-sensitive-id" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 error response with a valid correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "errorResponse", "correlationId": "Case-sensitive-id" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 blank request correlation is rejected", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "request", "correlationId": "" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "V2 whitespace response correlation is rejected", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "correlationId": " " }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "V2 control-character error correlation is rejected", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "errorResponse", "correlationId": "id\u0000" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "V2 trailing newline correlation is rejected", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "request", "correlationId": "id\n" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "V2 null correlation is not omission", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "correlationId": null }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "Legacy blank correlation remains unchanged", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "$type": "request", "correlationId": "" }] } }, + "valid": true + }, + { + "description": "Legacy whitespace correlation remains unchanged", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "$type": "response", "correlationId": " " }] } }, + "valid": true + }, + { + "description": "Proposed 1.2 control-character correlation remains unchanged", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "$type": "errorResponse", "correlationId": "id\u0000" }] } }, + "valid": true + }, + { + "description": "V2 compaction still has no correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "compaction" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 compaction rejects even a valid correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "compaction", "correlationId": "valid-id" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "Unknown V2 entry discriminator is rejected", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "future" }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "A supplied binding must still be valid", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {}, "historyBinding": {} } }, + "valid": false + }, + { + "description": "Null binding is not omission", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {}, "historyBinding": null } }, + "valid": false + }, + { + "description": "Binding remains forbidden in legacy state", + "data": { "schemaVersion": "1.2.0", "data": { "historyBinding": { "version": 1, "ownerKind": "historyProvider", "providerKey": "example.config" } } }, + "valid": false + } + ] + }, + { + "description": "Lossless shared message content", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json#/$defs/chatMessage" }, + "tests": [ + { "description": "Developer role", "data": { "role": "developer", "contents": [] }, "valid": true }, + { "description": "Unknown role remains invalid", "data": { "role": "future" }, "valid": false }, + { + "description": "Verbatim non-JSON function arguments", + "data": { "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": " { \"partial\": " }] }, + "valid": true + }, + { + "description": "Original empty string arguments", + "data": { "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": "" }] }, + "valid": true + }, + { + "description": "Object arguments remain valid", + "data": { "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": { "presentNull": null } }] }, + "valid": true + }, + { + "description": "Missing arguments remain absent", + "data": { "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f" }] }, + "valid": true + }, + { + "description": "Malformed known argument type remains invalid", + "data": { "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": false }] }, + "valid": false + }, + { + "description": "URI media type may be absent", + "data": { "role": "assistant", "contents": [{ "$type": "uri", "uri": "https://example.test/media" }] }, + "valid": true + }, + { + "description": "URI media type remains valid when supplied", + "data": { "role": "assistant", "contents": [{ "$type": "uri", "uri": "https://example.test/media", "mediaType": "image/png" }] }, + "valid": true + }, + { + "description": "URI media type must not be null", + "data": { "role": "assistant", "contents": [{ "$type": "uri", "uri": "https://example.test/media", "mediaType": null }] }, + "valid": false + }, + { + "description": "URI itself remains required", + "data": { "role": "assistant", "contents": [{ "$type": "uri" }] }, + "valid": false + }, + { + "description": "Complete unmodeled JSON including inert type names", + "data": { "role": "assistant", "contents": [{ "$type": "unknown", "content": { "$type": "future", "$runtimeType": "data-only", "metadata": { "null": null, "false": false, "empty": [] } } }] }, + "valid": true + }, + { + "description": "Explicit opaque null", + "data": { "role": "assistant", "contents": [{ "$type": "unknown", "content": null }] }, + "valid": true + }, + { + "description": "Explicit opaque scalar", + "data": { "role": "assistant", "contents": [{ "$type": "unknown", "content": "verbatim" }] }, + "valid": true + }, + { + "description": "Explicit opaque array", + "data": { "role": "assistant", "contents": [{ "$type": "unknown", "content": [0, false, null] }] }, + "valid": true + }, + { + "description": "Opaque wrapper must include original content", + "data": { "role": "assistant", "contents": [{ "$type": "unknown" }] }, + "valid": false + }, + { + "description": "Unwrapped unknown wire discriminator remains invalid", + "data": { "role": "assistant", "contents": [{ "$type": "future", "content": { "preserve": true } }] }, + "valid": false + }, + { + "description": "Known malformed text is not an opaque fallback", + "data": { "role": "assistant", "contents": [{ "$type": "text", "text": 0, "content": { "preserve": true } }] }, + "valid": false + } + ] + }, + { + "description": "Named terminal structured value", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json#/$defs/terminalResponse" }, + "tests": [ + { "description": "Absent value", "data": { "messages": [] }, "valid": true }, + { "description": "Present null", "data": { "messages": [], "value": null }, "valid": true }, + { "description": "Present false", "data": { "messages": [], "value": false }, "valid": true }, + { "description": "Present zero", "data": { "messages": [], "value": 0 }, "valid": true }, + { "description": "Present empty string", "data": { "messages": [], "value": "" }, "valid": true }, + { "description": "Present empty array", "data": { "messages": [], "value": [] }, "valid": true }, + { "description": "Present empty object", "data": { "messages": [], "value": {} }, "valid": true }, + { "description": "Nested JSON remains intact", "data": { "messages": [], "value": { "items": [null, false, 0, [], {}], "$runtimeType": "inert" } }, "valid": true }, + { "description": "Value does not replace required messages", "data": { "value": false }, "valid": false } + ] + }, + { + "description": "Legacy highest-seen ingestion scalar", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json#/$defs/data/properties/ingestedPositions" }, + "tests": [ + { "description": "Highest seen three does not encode gaps", "data": { "producer": 3 }, "valid": true }, + { "description": "Zero remains valid", "data": { "producer": 0 }, "valid": true }, + { "description": "Negative position remains invalid", "data": { "producer": -1 }, "valid": false }, + { "description": "Receipt sets cannot masquerade as the legacy scalar", "data": { "producer": [1, 3] }, "valid": false } + ] + } +] From eff12f41d3923d0123310a178b9dd6a0df61fb56 Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Sat, 12 Sep 2026 02:29:31 +0300 Subject: [PATCH 4/4] Keep lossless message expansion scoped to schema v2 Restore historical message and content validation, isolate expanded v2 transcript and terminal response definitions, and add full-envelope version boundary cases. Treat historyBinding as an opaque runtime profile and clarify authoritative expired outcomes without inventing legacy outcomes. No runtime activation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 532fe4f5-939b-4962-989d-a1883dccd283 --- schemas/README.md | 97 +++++--- schemas/durable-agent-entity-state.json | 129 +++++++--- schemas/fixtures/README.md | 20 +- schemas/tests/README.md | 35 ++- schemas/tests/validation-cases.json | 12 +- schemas/tests/versioned-envelope-cases.json | 252 ++++++++++++++++++++ 6 files changed, 457 insertions(+), 88 deletions(-) create mode 100644 schemas/tests/versioned-envelope-cases.json diff --git a/schemas/README.md b/schemas/README.md index a6c3b2b..db94d6e 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -22,8 +22,16 @@ included for compatibility discussion, not claimed as released Python output. Legacy transcript entries retain the existing permissive entry shape. The three mailbox/binding fields are forbidden in 1.x, even if empty. Version 2.0 requires `terminalResults`, `completionReceipts`, and `conversationHistory` -(which can be empty). `historyBinding` is optional and provisional, not a -requirement for session-fixed effective ownership. +(which can be empty). `historyBinding` is an optional runtime extension/profile, +not a shared requirement for session-fixed effective ownership. + +Historical `1.0.0`, `1.1.0`, and `1.2.0` message/content validation is unchanged +from the pre-widening proposal at `efed11f7786332d8bb0447ddbfc1727ddd7de09b`: +roles exclude `developer`, function arguments are objects when present, and +URI content requires `mediaType`. The root version selects historical +`conversationEntry` definitions or the v2 definitions. Only v2 transcript entries +and terminal responses use the expanded lossless shapes. This is not a +historical-contract correction, and no persisted legacy bytes are rewritten. Major 2 is proposed because the authority for completion and replay changes, not merely because new optional properties appear. **Is a major version the @@ -48,7 +56,7 @@ protect deployments whose existing readers do not enforce it. | `terminalResults[correlationId]` | Immutable terminal response/error envelope, detached from transcript retention. | | `completionReceipts[correlationId]` | Independent completion evidence, retained after payload expiry and transcript pruning. | | Receipt `resultState` | `available` with a matching payload, or `unavailable` with a removal timestamp. | -| `historyBinding` | Optional, provisional version 1 configuration descriptor; not a session-fixed effective owner. Representation remains under discussion. | +| `historyBinding` | Optional, separately versioned runtime extension/profile; preserved without imposing shared effective-owner semantics. | | `conversationHistory` | Evictable transcript, never the authoritative completion index in 2.0. | The schema validates local shape, not all cross-object or temporal invariants. @@ -90,12 +98,20 @@ Any future implementation must additionally enforce: is specified here. Whole-entity TTL/deletion and receipt storage growth must be resolved before deployment, not silently treated as transcript retention. -**Expired lookup outcome is pending agreement.** The proposed caller-visible -behavior is completed-but-result-unavailable plus the retained `succeeded` or -`failed` outcome, with no expired payload. This would require explicit ADR -wording and a Python receipt/lookup update; it is not a claim about current -Python behavior. The schema already preserves outcome after expiry, but this -proposal does not settle the lookup API's shape or enable any runtime behavior. +Expired lookup reports completed-but-result-unavailable plus the retained +`succeeded` or `failed` outcome, with no expired payload and no reopening of +execution. This agreed contract behavior still requires aligned ADR wording +and a Python receipt/lookup update; it is not a claim about current Python +behavior or authorization to emit 2.0. + +An older receipt without an authoritative outcome must **not** be assigned +`succeeded` or `failed` from absence of an error, a pruned transcript, expiry, +or a default. Preserve its known completion/unavailability facts without +inventing an outcome or rerunning the work. The required v2 receipt `outcome` +may be populated only from authoritative evidence; absent that evidence, the +legacy receipt cannot be promoted to this v2 shape. A separately agreed legacy +lookup/migration representation must retain the distinction. This proposal +does not add a fabricated `unknown` outcome to the v2 enum. The proposed envelope requires `response.messages` for success and failure (an empty list is allowed). Failure additionally requires `error.code` and @@ -113,27 +129,36 @@ serialize arbitrary model/runtime objects. Unknown nested properties remain part of the value. The same JSON preservation rule applies if a failed response carries a diagnostic value; its outcome remains failed. -### Provisional configuration binding +### Optional runtime extension/profile Stable provider/configuration identity is distinct from effective per-run history ownership. The shared contract **does not require session-fixed effective ownership** and must not prohibit Python's supported per-run transitions. C# may separately enforce a fixed-owner policy/profile. -For now, `historyBinding` is optional and keeps the existing review shape -(`version`, `ownerKind`, `providerKey`) without defining an effective-owner -transition protocol. If supplied, `ownerKind` describes the configured facility: -`durableState`, `historyProvider`, or `modelService`; it does not say that -facility owns every run. The non-secret `providerKey` identifies configuration, -not credentials, endpoints, runtime types, or opaque session properties. -Absence does not imply a default owner or authorize inference from session data. -This descriptor is not an authorization grant. - -The exact representation remains pending Ahmed's feedback: an optional shared -`providerBinding`/`configurationBinding`, or an optional runtime extension/profile. -No rename, new profile format, migration rule, or transition restriction is -introduced here. Trusted hosting configuration must still recognize any -descriptor it processes and separately validate supported per-run transitions. +`historyBinding` now denotes an optional, separately versioned runtime +extension/profile rather than a standardized shared binding object. The existing +spelling is retained to avoid moving stored data; there is no new shared +`providerBinding` or `configurationBinding` definition. Compatible writers must +preserve the original JSON value and all nested fields, even when they do not +understand or use that profile. The shared schema intentionally does not validate +its internal shape, version, owner kinds, or provider keys. + +A runtime that relies on the profile for restoration must validate the profile +identity, supported version, required fields, and applicable policy before use, +using trusted configuration rather than dynamically activating types from JSON. +Unsupported or malformed profiles must fail that runtime's restoration path; +they must not be silently ignored when the runtime depends on them. Other +consumers preserve the data without treating shared-schema validation as profile +approval. Even null or a malformed profile can be preserved as opaque JSON; +that does not make it usable by a relying runtime. + +The `version`, `ownerKind`, and non-secret logical `providerKey` in existing +synthetic fixtures illustrate one runtime profile, not a shared mandatory shape. +Profile-specific fixed ownership remains runtime policy. Absence implies neither +a default owner nor permission to infer one from opaque session state. No profile +is an authorization grant. A shared descriptor can be proposed later when there +is a concrete common consumer; no effective-owner transition protocol is imposed. ## Existing state, extension data, and trust boundaries @@ -164,12 +189,14 @@ model context. Optional `messageId` is message identity, not request identity. ### Lossless messages and JSON content -Message roles include `developer`. Function-call `arguments` accepts the +For schema 2.0 only, message roles include `developer`. Function-call `arguments` accepts the original object or string; preserve the entire string, including whitespace and incomplete/non-JSON text, without parsing it into an object. URI content requires a URI but not a media type; absence stays absent rather than being -filled with guessed metadata. These are additive shared message shapes for -review, not evidence that existing 1.x consumers can handle them. +filled with guessed metadata. Versioned message/content definitions keep these +expansions out of historical 1.x validation, including when the same message is +placed in a request, response, error response, or compaction transcript entry. +The terminal-response message path also uses the v2 definitions. For supported content not represented by a typed definition, a producer may use the explicit `{"$type":"unknown","content":...}` wrapper with the complete @@ -179,6 +206,10 @@ Only a safe, explicit JSON representation qualifies; no arbitrary object reflection, `repr` fallback, or executable serialization is implied. If a supported value cannot be preserved safely, report the incompatibility rather than silently dropping it or claiming a lossless terminal result was persisted. +The v2 lossless producer-mapping requirement does not widen the old explicit +`unknown.content` JSON shape: arbitrary JSON inside that wrapper was already +valid historically and remains valid. Historical consumers are not newly +required to understand v2 producer mappings. Lossless here means JSON value preservation, including array order, exact string contents, numeric fidelity, and absent versus null fields. It does not @@ -198,10 +229,12 @@ Unknown properties are not unknown discriminators: 2.0 accepts only transcript Compaction has no `correlationId`. Content `$type` values are `data`, `error`, `functionCall`, `functionResult`, `hostedFile`, `hostedVectorStore`, `usage`, `text`, `reasoning`, `uri`, and the explicit `unknown` wrapper. Unsupported -versions, owner kinds, binding versions, outcomes, availability values, roles, +versions, outcomes, availability values, roles, or discriminators must be rejected for processing, not silently converted to success or to an empty unknown-content wrapper. Opaque `unknown.content` itself can be any JSON value, including null; `$runtimeType` inside it is just data. +Runtime-profile discriminators and versions are different: validate them only +when relying on that profile, and otherwise preserve them without interpretation. Persisted JSON is untrusted data. Never dynamically load types, follow URIs, execute tool calls, or log opaque session/error/token contents merely by reading @@ -231,11 +264,11 @@ handling. This PR contains neither a migration nor code to enable revised writes Feedback requested from Ahmed/Python maintainers and .NET/Durable Task maintainers: 1. Schema shape, correlation scope, major 2 versus an enforceable same-major gate. -2. Optional configuration binding representation versus a runtime extension/profile, - without imposing session-fixed ownership as a shared rule. +2. Runtime-specific profile definitions and restoration validation, without + imposing session-fixed ownership as a shared rule. 3. Success/error envelope, cancellation, and continuation-byte interoperability. -4. Proposed exposed outcome after expiry (pending agreement), receipt lifetime, entity TTL, - storage growth, and late duplicates after session deletion/recreation. +4. Legacy receipt representation when authoritative outcome is absent, receipt + lifetime, entity TTL, storage growth, and late duplicates after session deletion/recreation. 5. Numeric/Unicode/timestamp limits, unknown-field preservation, and discriminator policy. 6. Compatible reader/consumer rollout floor, safe 1.2-to-2.0 migration, and rollback. diff --git a/schemas/durable-agent-entity-state.json b/schemas/durable-agent-entity-state.json index 979d379..7f306aa 100644 --- a/schemas/durable-agent-entity-state.json +++ b/schemas/durable-agent-entity-state.json @@ -80,7 +80,7 @@ "uri": { "type": "string", "description": "The URI." }, "mediaType": { "type": "string", "description": "The media type of the URI." } }, - "required": ["$type", "uri"] + "required": ["$type", "uri", "mediaType"] }, "usageContent": { "type": "object", @@ -107,10 +107,7 @@ "$type": { "type": "string", "const": "functionCall" }, "callId": { "type": "string", "description": "The identifier of the function being called." }, "name": { "type": "string", "description": "The name of the function being called." }, - "arguments": { - "type": ["object", "string"], - "description": "Original function arguments. Preserve string form verbatim, including incomplete or non-JSON text; do not parse, normalize, or replace it with an object." - } + "arguments": { "type": "object", "description": "The arguments provided to the function call." } }, "required": ["$type", "callId", "name"] }, @@ -126,7 +123,7 @@ }, "unknownContent": { "type": "object", - "description": "Explicit lossless wrapper for supported JSON content not modeled by a typed content definition. Preserve the complete original JSON value and metadata without interpreting fields such as $runtimeType or constructing runtime types. Not a fallback for malformed known wire fields.", + "description": "Explicit wrapper for opaque JSON content. Preserve its value without interpreting fields such as $runtimeType or constructing runtime types.", "properties": { "$type": { "type": "string", "const": "unknown" }, "content": { "description": "Opaque JSON, including scalar or null values. Not an instruction or a runtime type selector." } @@ -153,7 +150,7 @@ "description": "Single chat message exchanged with the agent.", "properties": { "authorName": { "type": "string", "description": "The name of the author of the message." }, - "role": { "type": "string", "enum": ["user", "assistant", "system", "developer", "tool"] }, + "role": { "type": "string", "enum": ["user", "assistant", "system", "tool"] }, "contents": { "type": "array", "description": "An empty array is a valid metadata-only message.", @@ -188,9 +185,92 @@ } } }, + "v2FunctionCallContent": { + "type": "object", + "description": "Schema 2.0 function call with lossless original argument form.", + "properties": { + "$type": { "type": "string", "const": "functionCall" }, + "callId": { "type": "string", "description": "The identifier of the function being called." }, + "name": { "type": "string", "description": "The name of the function being called." }, + "arguments": { + "type": ["object", "string"], + "description": "Original function arguments. Preserve string form verbatim, including incomplete or non-JSON text; do not parse, normalize, or replace it with an object." + } + }, + "required": ["$type", "callId", "name"] + }, + "v2UriContent": { + "type": "object", + "description": "Schema 2.0 URI content; absent media type remains absent.", + "properties": { + "$type": { "type": "string", "const": "uri" }, + "uri": { "type": "string", "description": "The URI." }, + "mediaType": { "type": "string", "description": "The media type of the URI, if supplied." } + }, + "required": ["$type", "uri"] + }, + "v2UnknownContent": { + "$ref": "#/$defs/unknownContent", + "description": "Schema 2.0 lossless producer mapping for supported unmodeled JSON content. Preserve the complete original JSON value and metadata without interpreting fields such as $runtimeType or constructing runtime types. Not a fallback for malformed known wire fields. The existing opaque wrapper's accepted JSON shapes are unchanged." + }, + "v2ChatContentItem": { + "oneOf": [ + { "$ref": "#/$defs/dataContent" }, + { "$ref": "#/$defs/errorContent" }, + { "$ref": "#/$defs/v2FunctionCallContent" }, + { "$ref": "#/$defs/functionResultContent" }, + { "$ref": "#/$defs/hostedFileContent" }, + { "$ref": "#/$defs/hostedVectorStoreContent" }, + { "$ref": "#/$defs/usageContent" }, + { "$ref": "#/$defs/textContent" }, + { "$ref": "#/$defs/textReasoningContent" }, + { "$ref": "#/$defs/v2UriContent" }, + { "$ref": "#/$defs/v2UnknownContent" } + ] + }, + "v2ChatMessage": { + "type": "object", + "description": "Schema 2.0 chat message. Historical versions continue to use chatMessage.", + "properties": { + "authorName": { "type": "string", "description": "The name of the author of the message." }, + "role": { "type": "string", "enum": ["user", "assistant", "system", "developer", "tool"] }, + "contents": { + "type": "array", + "description": "An empty array is a valid metadata-only message.", + "items": { "$ref": "#/$defs/v2ChatContentItem" } + }, + "createdAt": { "type": "string", "format": "date-time", "description": "When this message was created (RFC 3339)." }, + "messageId": { + "type": "string", + "description": "Producer message identity; distinct from a terminal request's correlationId." + }, + "extensionData": { + "type": "object", + "description": "Explicit message metadata, distinct from unknown sibling properties." + } + }, + "required": ["role"] + }, + "v2ChatMessages": { + "type": "array", + "description": "Ordered list of schema 2.0 chat messages.", + "items": { "$ref": "#/$defs/v2ChatMessage" } + }, + "v2ConversationEntry": { + "type": "object", + "properties": { + "createdAt": { "type": "string", "format": "date-time", "description": "When this exchange was created (RFC 3339)." }, + "correlationId": { "type": "string", "description": "An optional correlation ID to group related exchanges." }, + "messages": { "$ref": "#/$defs/v2ChatMessages" }, + "extensionData": { + "type": "object", + "description": "Explicit entry metadata, distinct from unknown sibling properties." + } + } + }, "agentRequest": { "allOf": [ - { "$ref": "#/$defs/conversationEntry" } + { "$ref": "#/$defs/v2ConversationEntry" } ], "description": "The request (i.e. prompt) sent to the agent.", "required": ["$type"], @@ -213,7 +293,7 @@ }, "agentResponse": { "allOf": [ - { "$ref": "#/$defs/conversationEntry" } + { "$ref": "#/$defs/v2ConversationEntry" } ], "description": "The response received from the agent.", "required": ["$type"], @@ -227,7 +307,7 @@ }, "agentErrorResponse": { "allOf": [ - { "$ref": "#/$defs/conversationEntry" } + { "$ref": "#/$defs/v2ConversationEntry" } ], "description": "Diagnostic record of a failed turn, not model replay context or authoritative completion evidence.", "required": ["$type"], @@ -239,7 +319,7 @@ }, "compaction": { "allOf": [ - { "$ref": "#/$defs/conversationEntry" } + { "$ref": "#/$defs/v2ConversationEntry" } ], "description": "Compacted model context; it answers no request and has no correlationId.", "required": ["$type"], @@ -252,7 +332,7 @@ "type": "object", "description": "JSON-only terminal response projection. No raw runtime object graph is persisted.", "properties": { - "messages": { "$ref": "#/$defs/chatMessages" }, + "messages": { "$ref": "#/$defs/v2ChatMessages" }, "value": { "type": ["null", "boolean", "number", "string", "array", "object"], "description": "Optional caller-visible structured JSON result, independent of messages. Absence means no structured value; explicit null, false, zero, empty strings, arrays, and objects remain present values. No runtime object serialization or inferred value from message text." @@ -321,7 +401,7 @@ }, "completionReceipt": { "type": "object", - "description": "Completion evidence surviving result expiry. Identity, outcome, completion time, and expiry policy are immutable; availability may transition once from available to unavailable.", + "description": "Completion evidence surviving result expiry. Identity, authoritative outcome, completion time, and expiry policy are immutable; availability may transition once from available to unavailable. Expired lookup retains outcome without a payload. An older receipt lacking authoritative outcome cannot be promoted to this shape by inventing succeeded or failed.", "properties": { "correlationId": { "$ref": "#/$defs/identifier" }, "outcome": { "type": "string", "enum": ["succeeded", "failed"] }, @@ -344,22 +424,7 @@ "additionalProperties": true }, "historyBinding": { - "type": "object", - "description": "Provisional optional configuration descriptor, not a session-fixed effective owner. Retained shape pending agreement on a shared configuration binding versus a runtime extension/profile. It does not restrict supported per-run owner transitions or specify their representation.", - "properties": { - "version": { "type": "integer", "const": 1 }, - "ownerKind": { - "type": "string", - "enum": ["durableState", "historyProvider", "modelService"], - "description": "Category of the configured history facility, not a requirement that it owns history on every run." - }, - "providerKey": { - "$ref": "#/$defs/identifier", - "description": "Stable, non-secret logical configuration identity; never a runtime type name, credential, or identity inferred from opaque session state." - } - }, - "required": ["version", "ownerKind", "providerKey"], - "additionalProperties": true + "description": "Optional separately versioned runtime extension/profile, not a shared binding object or effective-owner policy. The existing field name is retained for preservation. Compatible writers preserve the original JSON value; only a runtime relying on it validates its recognized profile and version before use. Shared shape validation does not imply the value is a usable profile." }, "data": { "type": "object", @@ -367,8 +432,7 @@ "properties": { "conversationHistory": { "type": "array", - "description": "Ordered, evictable transcript. Legacy entry shape remains permissive; 2.0 requires known entry discriminators.", - "items": { "$ref": "#/$defs/conversationEntry" } + "description": "Ordered, evictable transcript. The root schemaVersion selects historical conversationEntry items or the known schema 2.0 entry definitions." }, "terminalResults": { "type": "object", @@ -458,6 +522,9 @@ "properties": { "data": { "properties": { + "conversationHistory": { + "items": { "$ref": "#/$defs/conversationEntry" } + }, "terminalResults": false, "completionReceipts": false, "historyBinding": false diff --git a/schemas/fixtures/README.md b/schemas/fixtures/README.md index 7249349..07300c2 100644 --- a/schemas/fixtures/README.md +++ b/schemas/fixtures/README.md @@ -18,10 +18,13 @@ deliberately excluded. The corrected `python-shape` filename identifies provenance, not a promise of current serializer behavior. There is no fixture generator or test-project integration in this PR. -The two reproduced source fixtures are unchanged as JSON values. Their bindings -are now interpreted as provisional optional configuration descriptors, not proof -of session-fixed effective ownership. They do not settle the pending choice of -shared binding name/shape versus a runtime extension/profile. +The two reproduced source fixtures are unchanged as JSON values. The example +bindings are optional runtime-profile data, not a shared binding shape or proof +of session-fixed effective ownership. Compatible writers preserve them; only a +runtime relying on a profile validates its identity, version, shape, and policy. +The original legacy fixture stays valid under the unchanged historical message +definitions. The expanded lossless fixture is v2-only; its persisted bytes are +unchanged too. For `shared-durable-agent-state-2.0.json`, interpret the example at `2026-09-10T05:00:05Z`: `corr-2` has an available result and `corr-expired` @@ -46,9 +49,12 @@ The lossless fixture's scalar `ingestedPositions["legacy-producer"] = 3` records only highest-seen position. It does not say whether `2` was delivered. None of these fixtures defines or infers a gap-preserving workflow receipt set. -The expired receipts retain `outcome`; exposing that outcome with a -completed-but-result-unavailable lookup remains proposed pending agreement and -the corresponding ADR/Python lookup changes. No expired payload is restored. +The expired receipts retain `outcome`; lookup reports that retained outcome with +completed-but-result-unavailable, without restoring a payload or reopening +execution. This contract still requires the corresponding ADR/Python updates. +No fixture claims that a legacy receipt without authoritative outcome can be +converted by inventing success or failure. Such a receipt cannot satisfy the +v2 required-outcome shape without authoritative evidence. For an identity absent from the receipt maps, the examples show only the absence of recorded completion: a separately accepted request may be pending, diff --git a/schemas/tests/README.md b/schemas/tests/README.md index bd2faf0..70a102a 100644 --- a/schemas/tests/README.md +++ b/schemas/tests/README.md @@ -1,16 +1,26 @@ # Language-neutral schema validation cases -`validation-cases.json` records positive and negative review expectations using +`validation-cases.json` and `versioned-envelope-cases.json` record positive and negative review expectations using the JSON Schema Test Suite's group shape: `description`, `schema`, and `tests`; -each test has `description`, `data`, and `valid`. It is test data, not a durable -state fixture, product implementation, or runtime test-project integration. +each test has `description`, `data`, and `valid`. These are test data, not durable +state fixtures, product implementation, or runtime test-project integration. Resolve the canonical schema ID locally to `../durable-agent-entity-state.json`; do not fetch it from GitHub, which might contain a different revision. Use a Draft 2020-12 validator. The cases cover v2-only correlation constraints, -unchanged legacy/compaction handling, optional provisional binding, lossless +unchanged legacy/compaction handling, opaque runtime profiles, v2 lossless message shapes, structured-value presence, and historical ingestion scalars. +The versioned cases use complete root envelopes for every version rather than +just testing `$defs` fragments. Historical `1.0.0`, `1.1.0`, and `1.2.0` reject +the newly widened developer role, string-form function arguments, and URI content +without media type; v2 accepts them in its transcript and terminal payload paths. +Historical explicit `unknown` JSON was already valid and stays valid in all +versions. The unchanged legacy fixture remains additional compatibility evidence. +Profile cases distinguish opaque shared preservation from validation by a relying +runtime. Outcome cases reject attempts to promote receipts lacking authoritative +outcome into the required-outcome v2 shape; they do not implement migration. + For example, from the repository root with an existing Python `jsonschema` installation, this PowerShell command runs the structural cases without any network access or dependency installation: @@ -25,15 +35,16 @@ from referencing import Registry, Resource schema = json.loads(Path(r"schemas\durable-agent-entity-state.json").read_text(encoding="utf-8")) Draft202012Validator.check_schema(schema) registry = Registry().with_resource(schema["$id"], Resource.from_contents(schema)) -groups = json.loads(Path(r"schemas\tests\validation-cases.json").read_text(encoding="utf-8")) count = 0 -for group in groups: - validator = Draft202012Validator(group["schema"], registry=registry) - for test in group["tests"]: - actual = validator.is_valid(test["data"]) - if actual != test["valid"]: - raise AssertionError(f'{group["description"]}: {test["description"]}') - count += 1 +for path in sorted(Path(r"schemas\tests").glob("*-cases.json")): + groups = json.loads(path.read_text(encoding="utf-8")) + for group in groups: + validator = Draft202012Validator(group["schema"], registry=registry) + for test in group["tests"]: + actual = validator.is_valid(test["data"]) + if actual != test["valid"]: + raise AssertionError(f'{path.name}: {group["description"]}: {test["description"]}') + count += 1 print(f"Passed {count} structural validation cases") '@ | python - ``` diff --git a/schemas/tests/validation-cases.json b/schemas/tests/validation-cases.json index 3bbe252..5eca503 100644 --- a/schemas/tests/validation-cases.json +++ b/schemas/tests/validation-cases.json @@ -94,14 +94,14 @@ "valid": false }, { - "description": "A supplied binding must still be valid", + "description": "Shared validation preserves opaque profile data without approving it for restoration", "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {}, "historyBinding": {} } }, - "valid": false + "valid": true }, { - "description": "Null binding is not omission", + "description": "Opaque null profile is preserved, not interpreted as a usable profile", "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {}, "historyBinding": null } }, - "valid": false + "valid": true }, { "description": "Binding remains forbidden in legacy state", @@ -111,8 +111,8 @@ ] }, { - "description": "Lossless shared message content", - "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json#/$defs/chatMessage" }, + "description": "Schema 2.0 lossless message content", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json#/$defs/v2ChatMessage" }, "tests": [ { "description": "Developer role", "data": { "role": "developer", "contents": [] }, "valid": true }, { "description": "Unknown role remains invalid", "data": { "role": "future" }, "valid": false }, diff --git a/schemas/tests/versioned-envelope-cases.json b/schemas/tests/versioned-envelope-cases.json new file mode 100644 index 0000000..052d31a --- /dev/null +++ b/schemas/tests/versioned-envelope-cases.json @@ -0,0 +1,252 @@ +[ + { + "description": "Historical 1.0.0 full-envelope message boundary", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json" }, + "tests": [ + { + "description": "Original roles, object arguments and URI with media type remain valid", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": {} }, { "$type": "uri", "uri": "https://example.test/media", "mediaType": "image/png" }] }] }] } }, + "valid": true + }, + { + "description": "Developer role is not a historical correction", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "$type": "request", "messages": [{ "role": "developer" }] }] } }, + "valid": false + }, + { + "description": "String arguments are v2-only", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": " { \"partial\": " }] }] }] } }, + "valid": false + }, + { + "description": "Empty string arguments are also v2-only", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": "" }] }] }] } }, + "valid": false + }, + { + "description": "Omitted URI media type is v2-only even on a legacy error entry", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "$type": "errorResponse", "messages": [{ "role": "assistant", "contents": [{ "$type": "uri", "uri": "https://example.test/media" }] }] }] } }, + "valid": false + }, + { + "description": "Historical explicit opaque object, scalar and null remain valid", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "unknown", "content": { "$type": "future", "$runtimeType": "inert", "metadata": [null, false, 0] } }, { "$type": "unknown", "content": "original" }, { "$type": "unknown", "content": null }] }] }] } }, + "valid": true + }, + { + "description": "Unwrapped unknown content discriminator remains invalid", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "future", "content": {} }] }] }] } }, + "valid": false + }, + { + "description": "Malformed known fields remain invalid", + "data": { "schemaVersion": "1.0.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "text", "text": false }] }] }] } }, + "valid": false + } + ] + }, + { + "description": "Historical 1.1.0 full-envelope message boundary", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json" }, + "tests": [ + { + "description": "Original roles, object arguments and URI with media type remain valid", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": {} }, { "$type": "uri", "uri": "https://example.test/media", "mediaType": "image/png" }] }] }] } }, + "valid": true + }, + { + "description": "Developer role is not a historical correction", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "$type": "request", "messages": [{ "role": "developer" }] }] } }, + "valid": false + }, + { + "description": "String arguments are v2-only", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": " { \"partial\": " }] }] }] } }, + "valid": false + }, + { + "description": "Empty string arguments are also v2-only", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": "" }] }] }] } }, + "valid": false + }, + { + "description": "Omitted URI media type is v2-only even on a legacy error entry", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "$type": "errorResponse", "messages": [{ "role": "assistant", "contents": [{ "$type": "uri", "uri": "https://example.test/media" }] }] }] } }, + "valid": false + }, + { + "description": "Historical explicit opaque object, scalar and null remain valid", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "unknown", "content": { "$type": "future", "$runtimeType": "inert", "metadata": [null, false, 0] } }, { "$type": "unknown", "content": "original" }, { "$type": "unknown", "content": null }] }] }] } }, + "valid": true + }, + { + "description": "Unwrapped unknown content discriminator remains invalid", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "future", "content": {} }] }] }] } }, + "valid": false + }, + { + "description": "Malformed known fields remain invalid", + "data": { "schemaVersion": "1.1.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "text", "text": false }] }] }] } }, + "valid": false + } + ] + }, + { + "description": "Historical 1.2.0 full-envelope message boundary", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json" }, + "tests": [ + { + "description": "Original roles, object arguments and URI with media type remain valid", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": {} }, { "$type": "uri", "uri": "https://example.test/media", "mediaType": "image/png" }] }] }] } }, + "valid": true + }, + { + "description": "Developer role is not a historical correction", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "$type": "request", "messages": [{ "role": "developer" }] }] } }, + "valid": false + }, + { + "description": "String arguments are v2-only", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": " { \"partial\": " }] }] }] } }, + "valid": false + }, + { + "description": "Empty string arguments are also v2-only", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": "" }] }] }] } }, + "valid": false + }, + { + "description": "Omitted URI media type is v2-only even on a legacy error entry", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "$type": "errorResponse", "messages": [{ "role": "assistant", "contents": [{ "$type": "uri", "uri": "https://example.test/media" }] }] }] } }, + "valid": false + }, + { + "description": "Historical explicit opaque object, scalar and null remain valid", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "unknown", "content": { "$type": "future", "$runtimeType": "inert", "metadata": [null, false, 0] } }, { "$type": "unknown", "content": "original" }, { "$type": "unknown", "content": null }] }] }] } }, + "valid": true + }, + { + "description": "Unwrapped unknown content discriminator remains invalid", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "future", "content": {} }] }] }] } }, + "valid": false + }, + { + "description": "Malformed known fields remain invalid", + "data": { "schemaVersion": "1.2.0", "data": { "conversationHistory": [{ "messages": [{ "role": "assistant", "contents": [{ "$type": "text", "text": false }] }] }] } }, + "valid": false + } + ] + }, + { + "description": "Schema 2.0 full-envelope lossless message boundary", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json" }, + "tests": [ + { + "description": "Original object arguments and URI media type remain valid", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": {} }, { "$type": "uri", "uri": "https://example.test/media", "mediaType": "image/png" }] }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "Developer role in a v2 request", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "request", "messages": [{ "role": "developer" }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "String arguments in a v2 response", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": " { \"partial\": " }] }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "Empty string arguments in a v2 response", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "c", "name": "f", "arguments": "" }] }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "Omitted URI media type in a v2 error entry", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "errorResponse", "messages": [{ "role": "assistant", "contents": [{ "$type": "uri", "uri": "https://example.test/media" }] }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 compaction uses v2 messages without a correlation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "compaction", "messages": [{ "role": "developer", "contents": [{ "$type": "uri", "uri": "https://example.test/media" }] }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "V2 opaque mapping preserves the original JSON", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "unknown", "content": { "$type": "future", "$runtimeType": "inert", "metadata": [null, false, 0] } }, { "$type": "unknown", "content": "original" }, { "$type": "unknown", "content": null }] }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": true + }, + { + "description": "Unwrapped unknown content discriminator remains invalid in v2", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "future", "content": {} }] }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "Malformed known fields remain invalid in v2", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "response", "messages": [{ "role": "assistant", "contents": [{ "$type": "text", "text": false }] }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "Unknown role remains invalid in v2", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [{ "$type": "request", "messages": [{ "role": "future" }] }], "terminalResults": {}, "completionReceipts": {} } }, + "valid": false + }, + { + "description": "V2 success payload independently uses expanded messages and structured value", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": { "c": { "correlationId": "c", "outcome": "succeeded", "completedAt": "2026-09-11T00:00:00Z", "response": { "messages": [{ "role": "developer", "contents": [{ "$type": "functionCall", "callId": "t", "name": "f", "arguments": " { \"partial\": " }, { "$type": "uri", "uri": "https://example.test/media" }, { "$type": "unknown", "content": { "$runtimeType": "inert", "metadata": null } }] }], "value": false } } }, "completionReceipts": { "c": { "correlationId": "c", "outcome": "succeeded", "completedAt": "2026-09-11T00:00:00Z", "resultState": "available" } } } }, + "valid": true + }, + { + "description": "V2 failure payload independently uses expanded messages", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": { "c": { "correlationId": "c", "outcome": "failed", "completedAt": "2026-09-11T00:00:00Z", "response": { "messages": [{ "role": "developer", "contents": [{ "$type": "uri", "uri": "https://example.test/media" }] }] }, "error": { "code": "example", "message": "Synthetic failure" } } }, "completionReceipts": { "c": { "correlationId": "c", "outcome": "failed", "completedAt": "2026-09-11T00:00:00Z", "resultState": "available" } } } }, + "valid": true + }, + { + "description": "V2 terminal messages still reject malformed known fields", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": { "c": { "correlationId": "c", "outcome": "succeeded", "completedAt": "2026-09-11T00:00:00Z", "response": { "messages": [{ "role": "assistant", "contents": [{ "$type": "functionCall", "callId": "t", "name": "f", "arguments": false }] }] } } }, "completionReceipts": { "c": { "correlationId": "c", "outcome": "succeeded", "completedAt": "2026-09-11T00:00:00Z", "resultState": "available" } } } }, + "valid": false + } + ] + }, + { + "description": "Opaque runtime profiles and authoritative receipt outcomes", + "schema": { "$ref": "https://github.com/microsoft/agent-framework-durable-extension/schemas/durable-agent-entity-state.json" }, + "tests": [ + { + "description": "Nonconsumers preserve a separately versioned profile without interpreting owner fields", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {}, "historyBinding": { "version": 99, "ownerKind": "runtime-specific", "future": [null, false, {}] } } }, + "valid": true + }, + { + "description": "Even malformed profile data passes shared shape validation, not relying-runtime validation", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {}, "historyBinding": "opaque-unusable-profile" } }, + "valid": true + }, + { + "description": "Retained successful outcome survives payload expiry", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": { "c": { "correlationId": "c", "outcome": "succeeded", "completedAt": "2026-09-10T00:00:00Z", "resultState": "unavailable", "resultUnavailableAt": "2026-09-11T00:00:00Z" } } } }, + "valid": true + }, + { + "description": "Retained failed outcome survives payload expiry", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": { "c": { "correlationId": "c", "outcome": "failed", "completedAt": "2026-09-10T00:00:00Z", "resultState": "unavailable", "resultUnavailableAt": "2026-09-11T00:00:00Z" } } } }, + "valid": true + }, + { + "description": "Legacy receipt without outcome cannot be silently promoted to v2", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": { "c": { "correlationId": "c", "completedAt": "2026-09-10T00:00:00Z", "resultState": "unavailable", "resultUnavailableAt": "2026-09-11T00:00:00Z" } } } }, + "valid": false + }, + { + "description": "Null outcome is not authoritative success or failure", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": { "c": { "correlationId": "c", "outcome": null, "completedAt": "2026-09-10T00:00:00Z", "resultState": "unavailable", "resultUnavailableAt": "2026-09-11T00:00:00Z" } } } }, + "valid": false + }, + { + "description": "No synthetic unknown outcome is introduced into the v2 enum", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": { "c": { "correlationId": "c", "outcome": "unknown", "completedAt": "2026-09-10T00:00:00Z", "resultState": "unavailable", "resultUnavailableAt": "2026-09-11T00:00:00Z" } } } }, + "valid": false + } + ] + } +]