feat: Add typed structured outputs for Node and .NET - #2590
feat: Add typed structured outputs for Node and .NET#2590SteveSandersonMS wants to merge 7 commits into
Conversation
Generate all language RPC wrappers from the local runtime schema, expose per-run output schemas, and correlate schema-bearing waits using originatingMessageId. Include real-provider recording/replay E2Es through the locally built runtime for raw schemas, tools, steering, batches, and overlapping typed sends. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Auto-committed by java-codegen-check workflow.
Report pinned-schema drift without automatically rewriting draft Java output. Keep failure visibility, retain auto-regeneration for ready PRs, and restore the locally generated Java API after the initial workflow regenerated it against the old published runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Regenerate event types for all six SDK languages and document isFinalReply. Add real-provider Node and C# direct-send E2Es that parse the final correlated reply while stop hooks block idle, plus regressions preserving SendAndWait rejection on later errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Regenerate all SDK contracts from the local runtime, remove the provisional final-reply flag, and select correlated responses at idle. Share the real stop-hook correction capture between Node and C# and preserve existing captures while updating direct-send coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Refresh batch contracts, cover late steering with a shared provider capture, and reject malformed typed-wait arguments instead of sending unformatted requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ntracts Regenerate wrappers from the three-way merged release and feature schemas, preserving main's MCP source metadata, client startup fixes, OAuth support, CLI pin, and SourceLink update. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SDK Consistency ReviewThis PR adds provider-native structured output support ( What I checked
AssessmentNo inline comments are needed. The asymmetry is called out by the authors themselves in the PR description ("Provider-native structured output for Node/TypeScript and C#") and is consistent with the repo's pattern of rolling out provider/runtime-dependent features language-by-language before back-filling parity. The generated-code changes for Go/Python/Java/Rust look like plain schema/type regeneration (keeping those SDKs in sync with the runtime contract) with no partial/broken feature exposed — a reasonable, low-risk state for a draft PR. Suggestion for follow-up (not blocking): once this lands, consider tracking Go/Python/Java/Rust structured-output parity as a follow-up issue, since the underlying RPC/type support is now in place for those languages and only the ergonomic wrapper (
|
Summary
Provider-native structured output for Node/TypeScript and C#, paired with
https://github.com/github/copilot-agent-runtime/pull/19652 and related to #1185.
Draft: synchronized with unreleased runtime
b28bd95da9f15fb24a676f828fb9f754b014715d.Updated with SDK
mainatdcfbb938, including its published CLI pin1.0.84-4, startup serialization, OAuth metadata, and SourceLink security fix.
This PR must not land
until the runtime feature ships and the SDK pin can be updated.
Node accepts JSON Schema or Zod on
MessageOptions.responseSchema. Passing aZod schema as the second argument to
sendAndWaitinstead returns an inferred,parsed, validated value. C# accepts
JsonElementonMessageOptions.ResponseSchema, and genericSendAndWaitAsync<TResult>overloadsinfer a schema with the same
Microsoft.Extensions.AI.AIJsonUtilitiesused bycustom tools, then deserialize the selected result.
The existing API shapes already supported the runtime's latest fixes. This
follow-up refreshes generated batch-contract documentation, documents late
steering and reset/restart behavior, corrects send-result ID documentation, and
adds matching Node/C# late-steering E2Es using one shared real-provider snapshot.
It also rejects invalid Node second arguments instead of silently treating a
misplaced raw schema as an unformatted send.
Representative usage
These examples assume a live local
sessionbacked by a model/provider routethat supports native JSON Schema. C# examples use
GitHub.CopilotandSystem.Text.Json; the preview APIs have the repository's experimental annotation.Examples are alternatives, not a single sequence to execute against one session.
1. Prompt to a typed result
Node: infer the return type from a Zod value. A TypeScript type argument alone
cannot supply a runtime schema.
C#: infer from the result type using the normal reflection-enabled defaults.
C# defaults to
AIJsonUtilities.DefaultOptions, just as custom tools do.Deserialization checks JSON/type compatibility and required members, not every
JSON Schema or application constraint. Node additionally calls the supplied
schema's
parsemethod.2. Full message options, time limits, and C# source-generated serialization
Node: keep attachments and other message options while still returning a typed value.
C#: use one serialization contract for both inference and deserialization.
This also works when reflection-based serialization is disabled.
The options object is not modified. Do not also supply
ResponseSchema/responseSchemawhen using the typed overload. Timeout and C# cancellation stopwaiting; they do not abort agent work.
3. Explicit schema, assistant-event result
Use the options property when you want the event envelope and JSON text rather
than automatic typed parsing.
Node
The property can also hold
answerSchemadirectly; that still returns an event,not an inferred
Answerobject.C#
These convenience options request
name: "response"andstrict: true.Raw schema-bearing waits correlate messages, but do not validate or deserialize
the JSON text.
4. Different schemas on overlapping queued sends
Each submitted run owns its schema. Neither result may be replaced by the other
run's answer, although both waits can be delayed until the session is idle.
Node
C#
An independent later send without a schema restores normal output. Immediate
steering is different: it inherits the active run's schema and origin, including
when promoted into a follow-up after the final model request. Send steering
without a schema:
Explicit schemas on immediate delivery are rejected even while idle.
5. Admission-only sends and event-driven applications
send/SendAsyncreturn the submitted user message's ID, not an assistantresponse ID or a completed result. Applications that already own the event loop
can subscribe before sending and retain root assistant messages for correlation.
These are collection fragments, not alternative wait helpers:
Node
C#
After the run has started and the session reaches non-autopilot idle, select the
last root assistant message with this origin and no tool requests. Buffering
before admission completes matters: messages can precede the returned ID.
Handle session errors and aborted idle rather than returning partial output.
Dispose/unsubscribe after completion. For streaming UIs, the ordinary
assistant.message_delta/AssistantMessageDeltaEventevents remain available;do not try to parse each delta as a JSON document.
6. Full response-format metadata and batch RPCs
Use generated RPC methods for the full contract: schema name, description,
strictness, and batches. These return admission information, not typed results.
Node
C#
A batch admitted as new work starts one run. Earlier messages provide
context; the final returned ID is the assistant messages'
originatingMessageId.An empty batch runs over existing history and has no origin. Immediate batches
steer the active run and do not establish a new origin. Put the format beside
the batch, not on individual messages.
Completion and provider semantics
turnIdidentifies a model/tool iteration, not an entire queued run.originatingMessageIdremains stable through tool calls, ordinary steering,late-steering follow-ups, and internal stop-hook corrections.
events, and select the last correlated root assistant message without tool
requests at non-autopilot session idle. There is no final-message flag and
no delayed runtime event publication. Intermediate text need not be valid JSON.
queued work can delay idle. Session errors and aborted idle after the requested
run starts conservatively fail the wait, even if later work caused them.
Unformatted waits retain their existing behavior.
tool clears context, the old run ends; a fresh seed does not inherit its schema
or origin. A typed wait may therefore fail for lack of a structured result.
Schemas are not persisted session defaults; autonomous resume-pending work
after restart does not restore an interrupted send's contract.
the model output. Provider restrictions apply. OpenAI Chat uses
response_format, OpenAI Responses usestext.format, and Anthropic Messagesuses
output_config.format. API-compatible gateways can ignore unsupportedfields. The Claude Chat-completions compatibility route is not the native
Anthropic Messages route. Remote sessions and HydraFusion reject formats.
Generated contracts and release dependency
Regenerated Node, C#, Python, Go, Rust, and Java contracts using the current
generators. The newer released schemas contain MCP source metadata absent from
the runtime feature branch, so generation uses a three-way merge of:
b28bd95da9.7fc0540350.The merged inputs preserve every released contract and apply exactly the runtime
PR's nine RPC-schema changes and one event-schema change. No generated wrapper
was hand-edited and no newer MCP API was dropped. The Python merge conflict was
resolved by regeneration, not by choosing one side.
CONTRIBUTING.mddocumentsthis workflow.
The latest feature delta includes batch-origin documentation; no extra public API
was needed for late steering. Handwritten convenience APIs and feature E2Es remain
limited to Node and C#. The PR also retains the C# generator fix and regressions
for singleton
anyOf/oneOfdefinitions, alongside main's required-null fix.CONTRIBUTING.mdexplains local schema generation andCOPILOT_CLI_PATH.Java's codegen workflow reports drift rather than overwriting generated output
on a draft; ready-for-review PRs retain automatic regeneration. Pinned-schema and
packaged-runtime CI are not expected to be green until the runtime is released
and this draft updates its CLI pin.
Validation
Against local runtime
b28bd95da9:Coverage includes raw schemas, typed inference, tools, immediate steering,
schema clearing, batch RPCs, concurrent schemas, stop-hook corrections, and
late steering after the final model request.
structured_output/typed_wait_returns_late_steering_response.yamlwas recorded once using real
gpt-4.1responses through the existing proxy.Both languages share it, just as both share the stop-hook correction capture.
Their typed waits return corrected 99 rather than initial 42, with the original
origin retained. All 11 structured-output capture files remain unchanged by replay.
No shared test infrastructure or model-response YAML was hand-written.
regressions pass, as do the build, typecheck, and scoped lint.
serialization disabled. Two additional targeted cases exercise default
inference with reflection enabled, including the concurrent-send E2E.
Builds pass for net8.0, net10.0, and netstandard2.0.
generator tests, Python/Go generated-package tests, and Rust compilation.
This follow-up regenerates all languages while preserving main's newer contracts.
C# tests target net8.0 and run on the installed .NET 10 runtime via
DOTNET_ROLL_FORWARD=Major. Main's SourceLink update resolves the previousNU1902 build blocker; no warning exception or security-setting change is needed.
Full SDK suites, Native AOT publishing, and other-language E2Es were not run.