Skip to content

feat: Add typed structured outputs for Node and .NET - #2590

Draft
SteveSandersonMS wants to merge 4 commits into
mainfrom
sdk/typed-structured-output
Draft

feat: Add typed structured outputs for Node and .NET#2590
SteveSandersonMS wants to merge 4 commits into
mainfrom
sdk/typed-structured-output

Conversation

@SteveSandersonMS

@SteveSandersonMS SteveSandersonMS commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Prototype SDK APIs for provider-native structured output, paired with
https://github.com/github/copilot-agent-runtime/pull/19652 and related to #1185.

Draft: requires the unreleased runtime at 2c7080a298. The SDK's CLI pin is
intentionally unchanged. Do not merge this before the runtime ships and the pin
can be updated.

APIs

Node accepts an explicit JSON Schema or Zod schema on MessageOptions.responseSchema.
That options-based API continues returning an assistant event. A second-argument
Zod schema opts into inferred, parsed, validated results:

const result = await session.sendAndWait(
    "What is 19 + 23?",
    z.object({ answer: z.number().int() })
);
// result.answer is a number

C# accepts an explicit JsonElement on MessageOptions.ResponseSchema, and adds
generic overloads using the same Microsoft.Extensions.AI.AIJsonUtilities schema
inference as custom tools:

var result = await session.SendAndWaitAsync<MathAnswer>("What is 19 + 23?");

public sealed class MathAnswer
{
    public required int Answer { get; set; }
}

Optional JsonSerializerOptions govern both schema inference and deserialization.
Source-generated resolver options support hosts with reflection serialization
disabled. Inferred C# schemas request strict output with all properties required
and additional properties disallowed. Deserialization is not full JSON Schema
validation. Both typed helpers throw for missing or unparseable results.

The high-level schema option uses the name response and strict: true. Generated
session.rpc / session.Rpc APIs expose the full response-format object for
advanced metadata, strictness, and batch sends. Schemas remain opaque at runtime.

Result attribution and completion

turnId identifies a model/tool iteration, not the whole queued run. Telemetry
interaction IDs can be reused. The companion runtime change instead adds
assistant.message.data.originatingMessageId, matching the existing ID returned
by session.send, stable through tools and steering.

Schema-bearing waits subscribe before sending, buffer pre-acknowledgement events,
and select the last matching root assistant message without tool requests. Other
queued runs and subagents cannot replace that result. Intermediate messages need
not themselves be parseable JSON.

Direct send callers can now consume a correlated assistant message with
isFinalReply: true without waiting for idle (IsFinalReply in C#). The optional
flag marks only the last terminal reply, not intermediate/tool-call messages.
It is a content-selection signal, not a guarantee that later hooks or cleanup
succeed. Subsequent errors still use normal session events. sendAndWait keeps
its existing completion behavior, including rejection on a later error.

Completion still uses non-autopilot session idle: later queued work can delay
the result. There is no new per-send completion or error event. Session errors
and aborted idle events after the requested run starts conservatively fail the
wait, even if later queued work caused them. Existing unformatted waits retain
their previous behavior.

Default enqueue delivery owns the schema for the run. Ordinary immediate
steering inherits it; an explicit schema on immediate delivery is rejected.

Generated APIs

Regenerated Node, C#, Python, Go, Rust, and Java wrappers from the local runtime's
emitted RPC and session-event schemas, not a published release. Added a small C#
generator fix and two regressions for singleton anyOf / oneOf definitions.

The generated diff also includes genuine contract drift since the SDK's pinned
runtime (factory, model, sandbox, and event additions). It has not been
hand-trimmed or presented as feature-only. Handwritten convenience APIs and E2Es
are limited to Node and C#.

CONTRIBUTING.md documents local schema generation and COPILOT_CLI_PATH wiring.

Java's codegen workflow initially auto-committed the old pinned schema over this
draft, removing the new fields. Regenerated those files from the local runtime
again and changed that workflow to report drift rather than auto-rewrite draft
PRs
. It still fails for stale output; 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

  • Nine SDK E2Es recorded from real gpt-4.1 responses through the shared replay
    proxy and locally built runtime:
    five Node and four C#. These cover raw
    schemas, typed results, generated batch RPCs, tool continuation, schema
    clearing, Node terminal tools and steering, and overlapping typed sends in both
    languages. Concurrent tests hold a tool call until the second send is visibly
    queued; they do not assume RPC admission order.
  • The new direct-send scenario in each language parses the flagged final reply
    after a tool call while an agent-stop hook is deliberately held open. Both
    prove consumption before idle, then release the hook and observe idle. These
    two new captures were recorded from real providers against the rebuilt runtime.
  • All nine replayed without real provider credentials. Each language's final
    captures passed three consecutive strict replay runs with unchanged hashes.
    No model responses or capture YAML were hand-authored.
  • C# default inference was also exercised against a real provider in a
    reflection-enabled host; the resulting capture replays with source-generated
    serialization. Source-generated metadata is covered with reflection disabled.
  • 21 focused Node unit tests (structured output and existing send-and-wait);
    Node build, typecheck, and scoped lint.
  • 28 C# structured-output tests; all C# SDK target frameworks build. Both
    languages have a regression proving a final-reply flag does not hide a later
    session error from sendAndWait. Earlier validation also covered 150 C#
    session-lifetime cases and default inference with reflection enabled.
  • Generator validation: two new C# cases, 41 existing Node cases, four Java
    generator/fetcher cases, 14 Python generated cases, Go generated-package tests,
    and Rust library compilation.

Live tests use an explicit supported OpenAI Chat route through the Copilot API,
not independently authenticated provider accounts. The Claude Chat-completions
compatibility route can ignore native format fields; it is not equivalent to the
runtime's Anthropic Messages adapter.

Full SDK suites and other-language E2Es were not run. The broad Node client suite
was not completed; its packaged-runtime prerequisite is separate from this
local-runtime prototype.

C# validation surfaced an existing Microsoft.Build.Tasks.Git advisory
(NU1902); build commands used -p:WarningsNotAsErrors=NU1902 to retain the warning
without blocking this feature's coverage. No dependency versions were changed.

SteveSandersonMS and others added 2 commits September 9, 2026 15:15
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.
@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Sep 9, 2026
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>
@github-actions

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>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

SDK Consistency Review — PR #2590 "Add typed structured outputs for Node and .NET"

Scope of this PR

This PR adds a high-level typed structured-output API to two SDKs only:

  • Node.js: session.sendAndWait(options, responseSchema, timeout) returning a parsed/validated typed result, plus raw responseSchema/responseFormat support in session.send() (nodejs/src/session.ts).
  • .NET: CopilotSession.SendAndWaitAsync<TResult>(...) with JSON Schema inferred from TResult via Microsoft.Extensions.AI (dotnet/src/Session.StructuredOutput.cs), plus corresponding MessageOptions.ResponseSchema.

Both additions include new unit tests and real-provider-recorded E2E tests (nodejs/test/e2e/structured_output.e2e.test.ts, dotnet/test/E2E/StructuredOutputE2ETests.cs) and new CONTRIBUTING.md/README documentation for the workflow.

Cross-SDK check

All languages' generated RPC/event wrappers were regenerated from the updated runtime schema in this PR, so the low-level responseFormat/JSONSchemaResponseFormat wire types now exist everywhere (e.g. python/copilot/generated/rpc.py: ResponseFormat, go/rpc/zrpc.go, rust/src/generated/rpc.rs, Java's generated JsonSchemaResponseFormat.java). However, only Node and .NET expose this through their hand-written, public session API:

SDK Low-level responseFormat wire type generated? High-level typed/responseSchema send API?
Node.js send/sendAndWait
.NET SendAndWaitAsync<TResult>
Python ✅ (generated/rpc.py) ❌ — session.py: send/send_and_wait have no response_schema/responseFormat parameter at all
Go ✅ (rpc/zrpc.go) ❌ — session.go: Send(ctx, options) has no response-format/schema option
Rust ✅ (generated/rpc.rs) ❌ — session.rs: send/send_and_wait have no response_format/schema parameter
Java ✅ (generated JsonSchemaResponseFormat) ❌ — no responseFormat/ResponseSchema support found in SessionRequestBuilder/session send API

Assessment

This looks like the "Bad: Inconsistent feature" pattern described in the review guidelines: a new, clearly generally-useful capability (typed/structured JSON output for send/sendAndWait) was added to two of six SDKs, while Python, Go, Rust, and Java only picked up the incidental generated-code regeneration and now have no way to request structured output at all (not even the raw untyped responseFormat passthrough).

This may well be intentional — the PR title explicitly scopes itself to "Node and .NET" and CONTRIBUTING.md describes this as testing against an "unreleased runtime API" — suggesting a deliberate phased rollout rather than an oversight. If that's the case, no action is needed now, but it would help to:

  • Track follow-up work (issue or TODO) for adding the equivalent responseSchema/typed send_and_wait support to Python, Go, Rust, and Java once the runtime API stabilizes, so feature parity isn't lost.
  • Ensure the eventual Python (send_schema/response_schema kwarg, snake_case), Go (ResponseSchema field on MessageOptions, PascalCase), Rust (response_schema, snake_case), and Java (responseSchema, camelCase — likely via SessionRequestBuilder) implementations mirror the semantics already established here: mutual exclusivity between explicit/typed schema, mode: "immediate" rejection, and originatingMessageId correlation for concurrent typed sends.

No inline code issues were found in the Node/.NET implementation itself — this comment is purely about cross-SDK feature-parity tracking.

Generated by SDK Consistency Review Agent for #2590 · copilot · sonnet50 · 33.9 AIC · ⌖ 12.4 AIC · ⊞ 8.3K ·

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

Labels

dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant