diff --git a/schemas/README.md b/schemas/README.md new file mode 100644 index 0000000..db94d6e --- /dev/null +++ b/schemas/README.md @@ -0,0 +1,277 @@ +# Proposed durable agent state 2.0 contract + +**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 +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 `terminalResults`, `completionReceipts`, and `conversationHistory` +(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 +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` | 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. +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. 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. + 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. + +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 +`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. + +`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. + +### 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. + +`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 + +`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` 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 + +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. 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 +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. +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 +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 +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, 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 +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. 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. 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. + +The [fixtures](fixtures/README.md) are review examples, not evidence that either +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 53ac064..7f306aa 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": "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", + "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,16 +178,105 @@ "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." + } + } + }, + "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"], "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)." @@ -182,36 +293,245 @@ }, "agentResponse": { "allOf": [ - { "$ref": "#/$defs/conversationEntry" } + { "$ref": "#/$defs/v2ConversationEntry" } ], "description": "The response received from the agent.", + "required": ["$type"], "properties": { "$type": { "type": "string", "const": "response" }, + "correlationId": { "$ref": "#/$defs/identifier" }, "usage": { "$ref": "#/$defs/usage" } } }, + "agentErrorResponse": { + "allOf": [ + { "$ref": "#/$defs/v2ConversationEntry" } + ], + "description": "Diagnostic record of a failed turn, not model replay context or authoritative completion evidence.", + "required": ["$type"], + "properties": { + "$type": { "type": "string", "const": "errorResponse" }, + "correlationId": { "$ref": "#/$defs/identifier" }, + "usage": { "$ref": "#/$defs/usage" } + } + }, + "compaction": { + "allOf": [ + { "$ref": "#/$defs/v2ConversationEntry" } + ], + "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/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." + }, + "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, 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"] }, + "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": { + "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", "description": "The durable agent's state data.", "properties": { "conversationHistory": { "type": "array", - "description": "Ordered list of conversation entries.", - "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", + "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": "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": { + "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"], + "properties": { + "conversationHistory": { + "items": { + "oneOf": [ + { "$ref": "#/$defs/agentRequest" }, + { "$ref": "#/$defs/agentResponse" }, + { "$ref": "#/$defs/agentErrorResponse" }, + { "$ref": "#/$defs/compaction" } + ] + } + } + } + } + } + }, + "else": { + "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 new file mode 100644 index 0000000..07300c2 --- /dev/null +++ b/schemas/fixtures/README.md @@ -0,0 +1,69 @@ +# 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. | +| `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 +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. 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` +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. + +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`; 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, +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-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/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 + } +} diff --git a/schemas/tests/README.md b/schemas/tests/README.md new file mode 100644 index 0000000..70a102a --- /dev/null +++ b/schemas/tests/README.md @@ -0,0 +1,58 @@ +# Language-neutral schema validation cases + +`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`. 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, 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: + +```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)) +count = 0 +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 - +``` + +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..5eca503 --- /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": "Shared validation preserves opaque profile data without approving it for restoration", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {}, "historyBinding": {} } }, + "valid": true + }, + { + "description": "Opaque null profile is preserved, not interpreted as a usable profile", + "data": { "schemaVersion": "2.0.0", "data": { "conversationHistory": [], "terminalResults": {}, "completionReceipts": {}, "historyBinding": null } }, + "valid": true + }, + { + "description": "Binding remains forbidden in legacy state", + "data": { "schemaVersion": "1.2.0", "data": { "historyBinding": { "version": 1, "ownerKind": "historyProvider", "providerKey": "example.config" } } }, + "valid": false + } + ] + }, + { + "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 }, + { + "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 } + ] + } +] 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 + } + ] + } +]