Skip to content

Canonicalize classified stream failures - #438

Open
sethjuarez wants to merge 10 commits into
mainfrom
sejuare-microsoft/canonical-stream-failures
Open

Canonicalize classified stream failures#438
sethjuarez wants to merge 10 commits into
mainfrom
sejuare-microsoft/canonical-stream-failures

Conversation

@sethjuarez

Copy link
Copy Markdown
Member

Summary

  • add canonical StreamFailureOutcome, StreamFailure, and discriminator-backed FailureChunk contracts while retaining ErrorChunk
  • regenerate every configured Typra target with the pinned emitter and add shared determinate/indeterminate stream vectors
  • preserve Rust's published handwritten stream API through generated-model conversions and enforce reconciliation/no-completion semantics
  • emit generated failure chunks from TypeScript providers and surface partial content plus requiresReconciliation without committing successful turns

Compatibility

  • ErrorChunk remains in the canonical schema
  • Rust StreamChunk::Error(String) remains constructible and matchable
  • Rust classified failures bridge round-trip through generated model::StreamFailure

Validation

  • schema: npm run build; regeneration is deterministic (git diff --exit-code)
  • Rust: cargo test --workspace passes
  • TypeScript: all workspace builds and tsc --noEmit pass; OpenAI vectors 2/2 and core reconciliation vectors 2/2 pass
  • Python generated failure model: 5/5 pass
  • Go generated failure models: pass
  • C# generated failure models: 6/6 pass
  • OpenAI TypeScript suite: 93 pass; Anthropic: 24 pass; Foundry: 47 pass

Existing unrelated suite failures

Full-suite runs still expose existing failures outside this change: the shared permission-denied journal vector omits tool_result in TypeScript/Python/Go/C#, and older Typra-generated Prompty/property conversion fixtures fail in Python/Go/C#. The classified stream-failure tests and all changed runtime paths pass; this PR does not fold those emitter/harness defects into the schema-contract change.

sethjuarez and others added 3 commits August 4, 2026 00:22
Add canonical determinate and indeterminate stream failure chunks, regenerate every configured model target, and define shared behavior vectors while retaining ErrorChunk compatibility.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve the published handwritten stream API while mapping classified failures to generated models, consuming shared vectors, and enforcing reconciliation without completion commits.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Emit generated failure chunks from provider streams and surface partial content plus reconciliation requirements without committing successful turns.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 4, 2026 08:04

if (data.TryGetValue("kind", out var kindValue) && kindValue is not null)
{
instance.Kind = kindValue?.ToString()!;

@sethjuarez sethjuarez Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is generated by Typra 0.4.2, and the same ?.ToString()! emitter pattern occurs broadly across the generated C# target (359 occurrences in 130 files). Hand-editing this one generated class would be overwritten by deterministic regeneration and would make this schema-only PR non-reproducible. The coordinated Typra 0.4.4 consolidation will update the emitter pin and regenerate the combined schema after this PR lands; CodeQL itself passes in this PR.


if (data.TryGetValue("outcome", out var outcomeValue) && outcomeValue is not null)
{
instance.Outcome = StreamFailureOutcomeParser.Parse(outcomeValue?.ToString()!);

@sethjuarez sethjuarez Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is generated Typra 0.4.2 output, not handwritten runtime code. The same null-conditional emission pattern is emitter-wide (359 occurrences across 130 generated C# files), so a local generated-file edit would be non-deterministic and outside this focused schema contract. The coordinated Typra 0.4.4 follow-up will pin the newer emitter and regenerate all targets after this PR lands; CodeQL passes here.


if (data.TryGetValue("message", out var messageValue) && messageValue is not null)
{
instance.Message = messageValue?.ToString()!;

@sethjuarez sethjuarez Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same emitter-wide Typra 0.4.2 generated-code pattern (359 occurrences across 130 generated C# files). A schema-specific hand edit would be overwritten on regeneration and break reproducibility. We are keeping this PR on the published 0.4.2 baseline and coordinating the 0.4.4 emitter/regeneration follow-up after merge; CodeQL passes on this change.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The TypeScript streaming processors should explicitly cancel/close the underlying async iterator when emitting terminal FailureChunks to avoid leaking streaming resources.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR introduces a canonical, discriminator-backed model for classified terminal streaming failures (StreamFailureOutcome, StreamFailure, FailureChunk) and wires it through the schema, generated runtimes, vectors, and TypeScript streaming processors so partial content can be surfaced while enforcing reconciliation/no-completion semantics.

Changes:

  • Extend the canonical stream-chunk schema with failure discriminator plus StreamFailure{outcome,message} and emit docs/JSON Schemas accordingly.
  • Add shared process vectors for determinate/indeterminate stream failures and update TypeScript/Rust processors + core pipeline to surface requiresReconciliation and partial content without committing successful turns.
  • Regenerate Typra outputs across runtimes (TS/Python/Go/C#/Rust) and add/adjust corresponding generated-model tests.
File summaries
File Description
web/src/content/docs/reference/StreamFailure.md New generated reference doc for StreamFailure.
web/src/content/docs/reference/StreamChunk.md Documents FailureChunk as a StreamChunk subtype.
web/src/content/docs/reference/FailureChunk.md New generated reference doc for FailureChunk.
vscode/prompty/schemas/StreamFailure.yaml New JSON Schema for StreamFailure for the VS Code extension.
vscode/prompty/schemas/StreamChunk.yaml Adds failure to the StreamChunk.kind discriminator schema.
vscode/prompty/schemas/FailureChunk.yaml New JSON Schema for FailureChunk.
spec/vectors/process/stream_failure_vectors.json Adds deterministic/indeterministic stream-failure processing vectors.
schema/tsp-output/.typra-generated/manifest.json Tracks new generated outputs/tests for the added failure contracts.
schema/tsp-output/.typra-generated/export-surfaces.json Exposes FailureChunk/StreamFailure in generated export surfaces.
schema/model/events/stream-chunks.tsp Canonical TypeSpec additions: StreamFailureOutcome, StreamFailure, FailureChunk, discriminator update.
runtime/typescript/packages/openai/tests/stream-failure-vectors.test.ts OpenAI processor conformance against shared stream-failure vectors.
runtime/typescript/packages/openai/src/processor.ts Emits FailureChunk for refusal/transport errors during streaming.
runtime/typescript/packages/core/tests/stream-failures.test.ts Core turn() reconciliation/no-commit semantics validated via vectors.
runtime/typescript/packages/core/tests/model/events/stream-failure.test.ts Generated model serialization/roundtrip tests for StreamFailure.
runtime/typescript/packages/core/tests/model/events/failure-chunk.test.ts Generated model construction/load/save tests for FailureChunk.
runtime/typescript/packages/core/src/model/index.ts Exports StreamFailure and FailureChunk from the model surface.
runtime/typescript/packages/core/src/model/events/stream-failure.ts New generated StreamFailure model/type.
runtime/typescript/packages/core/src/model/events/stream-chunk.ts Adds FailureChunk discriminator branch + generated class.
runtime/typescript/packages/core/src/model/events/index.ts Re-exports StreamFailure/FailureChunk from events index.
runtime/typescript/packages/core/src/index.ts Re-exports StreamFailureError and stream failure model types.
runtime/typescript/packages/core/src/core/pipeline.ts Introduces StreamFailureError, consumes streams, enforces reconciliation/no-success semantics.
runtime/typescript/packages/core/src/core/index.ts Re-exports StreamFailureError from core entry point.
runtime/typescript/packages/anthropic/src/processor.ts Emits FailureChunk on streaming transport errors.
runtime/rust/prompty/tests/stream_chunk_compatibility.rs Validates handwritten Rust stream failure compatibility ↔ generated model bridge.
runtime/rust/prompty/tests/model/events/stream_failure_test.rs Generated Rust model tests for StreamFailure.
runtime/rust/prompty/tests/model/events/mod.rs Registers the new generated Rust stream_failure_test module.
runtime/rust/prompty/src/types.rs Adds StreamFailure ↔ generated model::StreamFailure conversions and derives.
runtime/rust/prompty/src/pipeline/live_turn.rs Adds determinate/indeterminate stream failure live-turn tests driven by vectors.
runtime/rust/prompty/src/model/events/stream_failure.rs New generated Rust StreamFailure + StreamFailureOutcome.
runtime/rust/prompty/src/model/events/stream_chunk.rs Adds FailureChunk discriminator and nested StreamFailure handling.
runtime/rust/prompty/src/model/events/mod.rs Exports the new stream_failure generated module.
runtime/rust/prompty-openai/tests/stream_failure_vectors.rs Rust OpenAI stream processor conformance against shared vectors.
runtime/rust/prompty-openai/src/processor.rs Emits classified StreamChunk::Failure(StreamFailure::Determinate(..)) for refusal cases.
runtime/python/prompty/tests/model/events/test_stream_failure.py Generated Python model tests for StreamFailure.
runtime/python/prompty/prompty/model/events/_StreamFailure.py New generated Python StreamFailure model.
runtime/python/prompty/prompty/model/events/_StreamChunk.py Adds generated FailureChunk discriminator branch/class.
runtime/python/prompty/prompty/model/events/init.py Re-exports StreamFailure/FailureChunk from Python events package.
runtime/python/prompty/prompty/model/init.py Re-exports StreamFailure/FailureChunk from Python model surface.
runtime/go/prompty/model/stream_failure.go New generated Go StreamFailure and StreamFailureOutcome.
runtime/go/prompty/model/stream_failure_test.go Generated Go tests for StreamFailure load/roundtrip helpers.
runtime/go/prompty/model/stream_chunk.go Adds generated Go FailureChunk and discriminator loader branch.
runtime/go/prompty/model/failure_chunk_test.go Placeholder generated Go test file for FailureChunk.
runtime/csharp/Prompty.Core/Model/events/StreamFailureOutcome.cs New generated C# enum for StreamFailureOutcome.
runtime/csharp/Prompty.Core/Model/events/StreamFailure.cs New generated C# model for StreamFailure.
runtime/csharp/Prompty.Core/Model/events/StreamChunk.cs Adds failure discriminator branch to generated C# StreamChunk.
runtime/csharp/Prompty.Core/Model/events/FailureChunk.cs New generated C# model for FailureChunk.
runtime/csharp/Prompty.Core.Tests/Model/events/StreamFailureConversionTests.cs Generated C# conversion/roundtrip tests for StreamFailure.
runtime/csharp/Prompty.Core.Tests/Model/events/FailureChunkConversionTests.cs Placeholder generated C# conversion test file for FailureChunk.
Review details

Files not reviewed (4)

  • runtime/go/prompty/model/failure_chunk_test.go: Generated file
  • runtime/go/prompty/model/stream_chunk.go: Generated file
  • runtime/go/prompty/model/stream_failure.go: Generated file
  • runtime/go/prompty/model/stream_failure_test.go: Generated file

Suppressed comments (1)

runtime/typescript/packages/openai/src/processor.ts:145

  • On refusal, the generator returns early after yielding a FailureChunk but does not cancel the underlying response iterator. Explicitly calling iterator.return() before yielding ensures the upstream stream is closed even if the consumer stops immediately after receiving the failure chunk.
  • Files reviewed: 44/48 changed files
  • Comments generated: 2
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment on lines +101 to +107
let next: IteratorResult<unknown>;
try {
next = await iterator.next();
} catch (error) {
yield failureChunk("indeterminate", errorMessage(error));
return;
}

@sethjuarez sethjuarez Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22351d0. The OpenAI processor now calls iterator.return() before yielding a terminal FailureChunk for both transport failures and refusals; cleanup failures are debug-logged so they cannot mask the canonical stream failure. Added focused tests that assert the upstream iterator is closed.

Comment on lines +90 to +101
let next: IteratorResult<unknown>;
try {
next = await iterator.next();
} catch (error) {
yield new FailureChunk({
failure: new StreamFailure({
outcome: "indeterminate",
message: error instanceof Error ? error.message : String(error),
}),
});
return;
}

@sethjuarez sethjuarez Aug 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 22351d0. The Anthropic processor now calls iterator.return() before yielding the indeterminate terminal FailureChunk; cleanup failures are debug-logged without replacing the primary failure. Added a focused transport-failure test that asserts closure.

sethjuarez and others added 4 commits August 4, 2026 02:04
Cancel upstream async iterators before yielding terminal failure chunks so SSE resources are released without masking the classified failure.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Record permission denials as committed tool results across reference runners without emitting tool execution events.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Remove trailing spaces from the canonical instruction sample so generated JSON round-trip fixtures agree across runtimes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Apply the configured Ruff formatting and assert the nullable strict-output wire shape already produced for optional fields.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 4, 2026 09:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The typra-emitter generated Go and C# FailureChunk conversion test files are empty, leaving the new FailureChunk helpers without direct cross-language conversion/round-trip coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Files not reviewed (5)

  • runtime/go/prompty/model/failure_chunk_test.go: Generated file
  • runtime/go/prompty/model/prompty_test.go: Generated file
  • runtime/go/prompty/model/stream_chunk.go: Generated file
  • runtime/go/prompty/model/stream_failure.go: Generated file
  • runtime/go/prompty/model/stream_failure_test.go: Generated file

Suppressed comments (2)

runtime/csharp/Prompty.Core.Tests/Model/events/FailureChunkConversionTests.cs:11

  • This generated test file defines an empty FailureChunkConversionTests class, so C# has no direct JSON/YAML/round-trip coverage for the new FailureChunk conversion helpers (unlike other generated conversion tests in this directory). Since the header indicates this is typra-emitter output, the fix should be in the emitter’s C# test template (or by adding a minimal conversion test generation for FailureChunk) and then regenerating outputs.
public class FailureChunkConversionTests
{
}

runtime/go/prompty/model/failure_chunk_test.go:4

  • This generated Go test file contains only the header/package declaration and no tests, so there’s no Go-side JSON/YAML/round-trip coverage for the new FailureChunk helpers (contrast with other generated *_test.go files like error_chunk_test.go). Since this is typra-emitter output, update the emitter’s Go test template to generate basic conversion tests for FailureChunk (or omit generating this file) and regenerate.
  • Files reviewed: 56/63 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Add a canonical nested failure sample so Typra emits load/save and JSON/YAML conversion coverage for FailureChunk across configured runtimes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 4, 2026 09:48
@sethjuarez

Copy link
Copy Markdown
Member Author

Addressed the generated conversion-coverage finding in 242e27f3 without changing the emitter or hand-editing generated files. FailureChunk.failure now has a canonical nested @sample, so the pinned Typra 0.4.2 generation emits load/save and JSON/YAML round-trip coverage in Go, C#, TypeScript, and Python. Targeted results: Go package passed, C# 6/6, TypeScript 8/8, Python 5/5; a second full schema build reproduced the exact same diff.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The TypeScript provider processors’ manual iterator.next() loops risk leaving upstream streaming iterators unclosed when consumers cancel early, which can leak streaming resources in real deployments.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Files not reviewed (5)

  • runtime/go/prompty/model/failure_chunk_test.go: Generated file
  • runtime/go/prompty/model/prompty_test.go: Generated file
  • runtime/go/prompty/model/stream_chunk.go: Generated file
  • runtime/go/prompty/model/stream_failure.go: Generated file
  • runtime/go/prompty/model/stream_failure_test.go: Generated file

Suppressed comments (2)

runtime/typescript/packages/openai/src/processor.ts:99

  • streamGenerator switched from for await...of to manual iterator.next() reads; this loses the automatic iterator.return() call that for await...of performs when the consumer stops early (e.g., caller breaks out of the loop or cancels a request). That can leave SSE/ReadableStream-backed providers open longer than necessary unless the generator is explicitly closed.

Consider wrapping the body in try { ... } finally { await closeIterator(iterator); } (and keeping the existing pre-yield closes for refusal/transport errors) so upstream resources are released even on early consumer cancellation.
runtime/typescript/packages/anthropic/src/processor.ts:88

  • streamGenerator uses a manual iterator.next() loop. Unlike for await...of, this won’t automatically call iterator.return() when the consumer stops iteration early, which can keep a streaming connection open unnecessarily.

Recommend adding a try { ... } finally { await closeIterator(iterator); } around the loop (and/or ensuring early cancellation paths always close) so upstream streams are reliably released even when callers abort consumption.

  • Files reviewed: 57/64 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

sethjuarez and others added 2 commits August 4, 2026 03:17
Sample the canonical failure discriminator so generated conversion tests assert it survives JSON and YAML round trips in every configured runtime.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Run upstream iterator cleanup from async-generator finally blocks so consumer cancellation releases SSE resources without double-closing failure paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 4, 2026 10:18
@sethjuarez

Copy link
Copy Markdown
Member Author

Addressed the latest Copilot cancellation finding in ae908bd2. Both OpenAI and Anthropic manual iterator loops now run idempotent upstream cleanup from finally, close before terminal failure chunks, close exactly once when a consumer breaks early, and avoid unnecessary return() after natural exhaustion. Added provider-specific early-cancellation tests; full TypeScript build, type-check, and suites pass (core 1560, OpenAI 96, Foundry 47, Anthropic 26), and deterministic schema regeneration remains clean.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Human review recommended

It introduces cross-runtime schema and streaming-semantic changes that require final human verification beyond spot-checking diffs and unit tests.

Review details

Files not reviewed (5)

  • runtime/go/prompty/model/failure_chunk_test.go: Generated file
  • runtime/go/prompty/model/prompty_test.go: Generated file
  • runtime/go/prompty/model/stream_chunk.go: Generated file
  • runtime/go/prompty/model/stream_failure.go: Generated file
  • runtime/go/prompty/model/stream_failure_test.go: Generated file
  • Files reviewed: 57/64 changed files
  • Comments generated: 1
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment on lines +539 to +541
kind: string = "failure";
failure!: StreamFailure;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FailureChunk.failure intentionally has no schema default: neither a determinate nor indeterminate outcome, nor a fabricated message, is safe to invent. Typra-generated model constructors currently support partial construction across required non-collection fields and omit absent values from save(); this is emitter-wide model behavior, not specific to FailureChunk. Every runtime production construction in this PR supplies a canonical StreamFailure, and the generated/load-save plus shared vectors cover populated instances. Changing constructor validity or required-field enforcement belongs in the coordinated Typra 0.4.4 consolidation, not as a hand edit or schema default in this focused PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants