Skip to content

.NET: [BREAKING] Hosting OpenAI Responses protocol helpers and optional execution state#7000

Merged
rogerbarreto merged 29 commits into
microsoft:mainfrom
rogerbarreto:channels-protocol-extensions
Jul 22, 2026
Merged

.NET: [BREAKING] Hosting OpenAI Responses protocol helpers and optional execution state#7000
rogerbarreto merged 29 commits into
microsoft:mainfrom
rogerbarreto:channels-protocol-extensions

Conversation

@rogerbarreto

@rogerbarreto rogerbarreto commented Jul 8, 2026

Copy link
Copy Markdown
Member

Motivation & Context

ADR-0027 refocused the hosting design toward protocol conversion helpers plus optional execution state: Agent Framework owns protocol-native <-> run conversion, while the application owns HTTP routing, authentication, middleware, and storage. The Python slice landed in #6891.

This PR realizes that direction for .NET. A first-principles gap analysis showed .NET already covers most of the capability, and more richly, but bundled behind the route-owning MapOpenAIResponses server. The only genuine gap is the ownership model: an application cannot own its own route and call just the conversion primitives. So this change un-bundles the existing conversion rather than reinventing it. No new package, no OpenAI-SDK-typed reimplementation, and MapOpenAIResponses public behavior is unchanged.

Description & Review Guide

  • What are the major changes?
    • New public facade OpenAIResponses in Microsoft.Agents.AI.Hosting.OpenAI (JsonElement/SSE boundary; wire DTOs stay internal), delegating to the existing internal converters:
      • ToAgentRunRequest(JsonElement) -> messages + AgentRunOptions?
      • WriteResponse(AgentResponse, responseId, sessionId?) -> Responses JSON
      • WriteResponseStreamAsync(IAsyncEnumerable<AgentResponseUpdate>, responseId, ...) -> Responses SSE frames
      • GetSessionId(JsonElement) -> previous_response_id/conversation id (kept separate so the trust boundary stays visible)
      • CreateResponseId() -> resp_*
    • Protocol-neutral execution state in Microsoft.Agents.AI.Hosting:
      • AgentSessionStore.DeleteSessionAsync (added as an abstract method so every store makes a conscious choice: implement deletion or throw NotSupportedException itself; the in-box stores implement it). Hosting is still preview, so this tightens the contract now rather than shipping a silent default.
      • No agent-side holder: applications use AgentSessionStore directly (GetSessionAsync creates on miss, SaveSessionAsync persists post-run including under a newly minted id, DeleteSessionAsync removes). ADR-0032 records why per-run instance and async target setup are left to the DI container in .NET.
      • HostedWorkflowState + HostedWorkflowRunResult (workflow instance or factory + CheckpointManager + an internal sessionId -> CheckpointInfo head cursor; RunOrResumeAsync/RunOrResumeStreamingAsync)
    • Two samples under dotnet/samples/04-hosting/af-hosting/: local_responses (app-owned route, sync + SSE) and local_responses_workflow (checkpoint resume).
    • ADR 0032-dotnet-hosting-protocol-helpers.md and spec 003-dotnet-hosting-protocol-helpers.md.
  • What is the impact of these changes?
    • Breaking (preview package): adds a new abstract member (AgentSessionStore.DeleteSessionAsync) to a public abstract type and renames the store id parameter to sessionStoreId, so external subclasses must implement the new member. The net-new helpers are otherwise additive. MapOpenAIResponses, IResponsesService, ChatCompletions, and Conversations are untouched.
    • Applications can now expose an agent or workflow over OpenAI Responses while owning their own routing, auth, and storage.
  • What do you want reviewers to focus on?
    • The public helper shape and the JsonElement boundary decision (recorded in ADR-0032, alongside why not the OpenAI SDK Responses types).
    • The decision to reuse the richer existing AgentSessionStore instead of cloning Python's in-memory SessionStore, and the abstract (implement-or-throw) DeleteSessionAsync.
    • The HostedWorkflowState head-cursor approach for per-session checkpoint resume.

Related Issue

N/A. This is the .NET realization of the accepted ADR-0027; it adds ADR-0032 and its spec. It supersedes the earlier channel-model draft in #6151 (closed). Opening as a draft for design feedback.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is a breaking change (preview package): a new abstract member is added to AgentSessionStore and the store id parameter is renamed to sessionStoreId. The breaking change label is applied.

Copilot AI review requested due to automatic review settings July 8, 2026 18:06
@giles17 giles17 added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net labels Jul 8, 2026

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.

Pull request overview

This PR implements the ADR-0027 “helper-first” hosting direction for .NET by exposing a public OpenAI Responses protocol conversion facade (OpenAIResponses) and adding optional, protocol-neutral execution state helpers for agent session continuity and workflow checkpoint resume, plus new samples and design docs.

Changes:

  • Adds Microsoft.Agents.AI.Hosting.OpenAI.OpenAIResponses public helper facade and OpenAIResponsesRunRequest for JsonElement-bound request/response conversions (JSON + SSE).
  • Adds protocol-neutral execution state helpers in Microsoft.Agents.AI.Hosting (HostedAgentState, HostedWorkflowState, HostedWorkflowRunResult) and introduces AgentSessionStore.DeleteSessionAsync with in-box store overrides.
  • Adds new hosting samples (agent + workflow) plus unit tests and ADR/spec documentation for the helper-first hosting shape.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/InMemoryAgentSessionStoreTests.cs New tests covering AgentSessionStore.DeleteSessionAsync behavior across in-box stores.
dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostedWorkflowStateTests.cs New tests for HostedWorkflowState argument validation and checkpoint lookup behavior.
dotnet/tests/Microsoft.Agents.AI.Hosting.UnitTests/HostedAgentStateTests.cs New tests for HostedAgentState store delegation and optional per-session locking.
dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesTests.cs New tests for OpenAIResponses request mapping, session id extraction, id creation, and response rendering.
dotnet/src/Microsoft.Agents.AI.Hosting/NoopAgentSessionStore.cs Implements no-op DeleteSessionAsync override.
dotnet/src/Microsoft.Agents.AI.Hosting/Local/InMemoryAgentSessionStore.cs Implements DeleteSessionAsync by removing stored session content.
dotnet/src/Microsoft.Agents.AI.Hosting/IsolationKeyScopedAgentSessionStore.cs Pass-through implementation for DeleteSessionAsync with scoped conversation ids.
dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowState.cs Adds in-memory per-session checkpoint “head cursor” and RunOrResumeAsync.
dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowRunResult.cs Adds run result wrapper carrying session id, emitted events, and head checkpoint.
dotnet/src/Microsoft.Agents.AI.Hosting/HostedAgentState.cs Adds helper that pairs an agent with a session store plus optional per-session locking.
dotnet/src/Microsoft.Agents.AI.Hosting/DelegatingAgentSessionStore.cs Pass-through implementation for new DeleteSessionAsync API.
dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs Adds new virtual DeleteSessionAsync with throwing default.
dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIResponsesRunRequest.cs Adds public run request carrier (messages + optional run options).
dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIResponses.cs Adds public conversion facade for Responses JSON and SSE rendering.
dotnet/samples/04-hosting/HostingResponsesWorkflow/README.md New sample README for app-owned workflow route + checkpoint resume.
dotnet/samples/04-hosting/HostingResponsesWorkflow/Program.cs New minimal API workflow sample using OpenAIResponses + HostedWorkflowState.
dotnet/samples/04-hosting/HostingResponsesWorkflow/HostingResponsesWorkflow.csproj New workflow sample project.
dotnet/samples/04-hosting/HostingResponsesAgent/README.md New sample README for app-owned agent route (sync + SSE).
dotnet/samples/04-hosting/HostingResponsesAgent/Program.cs New minimal API agent sample using OpenAIResponses + HostedAgentState.
dotnet/samples/04-hosting/HostingResponsesAgent/HostingResponsesAgent.csproj New agent sample project.
dotnet/agent-framework-dotnet.slnx Includes the new hosting samples in the solution.
docs/specs/003-dotnet-hosting-protocol-helpers.md Adds accepted spec describing the new helper + state surface and samples.
docs/decisions/0032-dotnet-hosting-protocol-helpers.md Adds accepted ADR capturing the decision and rationale for the new surface.

Comment thread dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIResponses.cs
Comment thread dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIResponses.cs Outdated
Comment thread dotnet/samples/04-hosting/HostingResponsesWorkflow/Program.cs Outdated
Comment thread dotnet/samples/04-hosting/HostingResponsesWorkflow/README.md Outdated
Comment thread docs/specs/003-dotnet-hosting-protocol-helpers.md Outdated

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

Automated Code Review

Reviewers: 5 | Confidence: 88%

✓ Correctness

The framework code (OpenAIResponses facade, HostedAgentState, HostedWorkflowState, DeleteSessionAsync additions) is well-designed and correct. The one correctness issue is in the HostingResponsesAgent sample, which passes responseId as the sessionId parameter to WriteResponse and WriteResponseStreamAsync, whereas the spec's own E2E sample (docs/specs/003-dotnet-hosting-protocol-helpers.md lines 183, 194) correctly passes sessionId. This causes the rendered response's conversation.id to equal the response id rather than the actual conversation/session id, breaking the OpenAI Responses conversation-grouping semantic.

✓ Security Reliability

The PR is well-designed with clear trust boundary documentation and proper input validation. Two issues found: (1) the session locking dictionary in HostedAgentState grows without bound, creating a resource leak for long-lived services; (2) the HostingResponsesAgent sample passes responseId where the spec (in the same PR) passes sessionId as the conversation identifier, causing conversation.id in the response to be per-turn rather than stable.

✓ Test Coverage

The PR adds good unit tests for guard clauses, delegation, and basic conversion paths. However, three significant behaviors lack test coverage: (1) the WriteResponseStreamAsync streaming facade has zero tests — the SSE frame formatting logic is entirely untested; (2) HostedWorkflowState.RunOrResumeAsync only tests argument validation, not the actual run-or-resume logic, event collection, or cursor recording; (3) IsolationKeyScopedAgentSessionStore.DeleteSessionAsync (new code) has no test verifying it applies isolation-key scoping before delegating to the inner store.

✓ Failure Modes

The PR is well-structured from a failure-mode perspective. Disposal patterns for Run are correct (await using), the semaphore releaser is safe against double-release via Interlocked.Exchange, argument validation gates all state operations, and the NotSupportedException default for DeleteSessionAsync is documented, tested, and maintains backward compatibility. No silent failure paths, swallowed exceptions, or partial-write scenarios were found in the new code. The in-memory cursor limitation in HostedWorkflowState is explicitly documented as a v1 trade-off.

✓ Design Approach

I found one production-surface design issue and one sample-level design issue. The new OpenAIResponses helper path no longer enforces the existing OpenAI Responses invariant that conversation and previous_response_id are mutually exclusive, so invalid requests are silently accepted and resolved differently from MapOpenAIResponses. Separately, the new agent sample collapses conversation.id and previous_response_id into a single storage key, which breaks stable conversation.id continuity for callers following the documented protocol shape.


Automated review by rogerbarreto's agents

Comment thread dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/OpenAIResponses.cs Outdated
…workflow resume

Migrate HostingResponsesAgent and HostingResponsesWorkflow samples from
Azure.AI.OpenAI to Azure.AI.Projects (AIProjectClient.AsAIAgent), using the
FOUNDRY_PROJECT_ENDPOINT/FOUNDRY_MODEL convention.

Fix HostedWorkflowState.RunOrResumeAsync: on subsequent turns, restore the
session's latest checkpoint and run the workflow forward with the new turn's
input (mirroring the Python hosting host's restore-then-run semantics) instead
of resuming a halted run with no input, which waited on input indefinitely.
Add round-trip resume tests and update ADR-0032/spec-003 wording.
…ests

On resume, HostedWorkflowState.RunOrResumeAsync drained the workflow with the
blocking WatchStreamAsync overload, so a workflow that halts at an unserviced
RequestInfoEvent (human-in-the-loop / approval) blocked forever — asymmetric
with the first-turn RunAsync path, which returns at the same halt. Break the
drain when a superstep completes with HasPendingRequests, restoring symmetry
with turn 1. Add a HITL approval-gate workflow and a resume-does-not-block test.
Add an optional ILoggerFactory to HostedWorkflowState and log a warning when a
resumed turn produces no events, mirroring the Python host's zero-event restore
warning (a stale checkpoint or an input that does not match the workflow's
expected type leaves session state unprogressed). Add a non-chat string workflow
helper, a capturing logger, and a red/green test.
Add CheckpointManager.GetLatestCheckpointAsync(sessionId) and have
HostedWorkflowState fall back to it when its in-memory head cursor misses, so a
durable CheckpointManager resumes a session across a process restart or a new
holder instead of restarting from the workflow's start executor. Mirrors the
Python host's per-turn get_latest read-through. Add a counting workflow that
proves resume-vs-fresh via accumulated state, plus a red/green test, and update
ADR-0032/spec-003 and the XML remarks.
A single workflow instance backs the holder and workflow instances do not
support concurrent runs (the runner throws "already owned by another runner"),
so concurrent turns could fault or race the head cursor. Serialize all turns
through one SemaphoreSlim (mirroring the Python host's workflow lock) and make
HostedWorkflowState IDisposable to own it. Add a gated workflow and a
deterministic concurrency red/green test.
Add tests for HostedWorkflowState resuming a non-chat-protocol workflow (no
TurnToken) and for a third turn continuing to advance the head checkpoint,
closing the coverage gaps the parity review flagged.
Add HostedWorkflowState.RunOrResumeStreamingAsync, which yields the turn's
WorkflowEvents as they occur (fresh run or checkpoint resume) under the same
serialization lock and records the head checkpoint after the stream drains,
keeping the blocking and streaming workflow paths in lockstep with the Python
host. Honor stream:true in the HostingResponsesWorkflow sample by projecting
AgentResponseUpdateEvent updates over the Responses SSE wire. Add a streaming
resume test and update the README/spec.
…utor

Demonstrate that HostedWorkflowState's generic RunOrResumeAsync<TInput> is the
input-adaptation seam (parity with Python's ResponsesChannel run hook): the app
adapts the Responses input into the workflow start executor's own type at the
call site. Add a typed-brief workflow and a test, and note the seam in spec-003.
The resume drain used a SuperStepCompletedEvent{HasPendingRequests} proxy over
the blocking public WatchStreamAsync. That proxy (a) truncated a resumed turn
when a superstep both emitted a request and queued downstream work, and (b)
could fail to fire at all — re-introducing the indefinite hang — when a resume
input drove no superstep (e.g. a rejected non-chat input).

Make StreamingRun.WatchStreamAsync(bool blockOnPendingRequest, CancellationToken)
public and drain both the blocking and streaming resume paths with
blockOnPendingRequest:false, exactly matching the first-turn RunAsync semantics
(Run.RunToNextHaltAsync). Add guard tests: resume with a rejected input does not
hang, and a resume superstep with a request plus downstream work is not
truncated (verified red against the old proxy).
CheckpointManager.GetLatestCheckpointAsync takes the last entry of a store's
index as the head checkpoint. FileSystemJsonCheckpointStore backed its index
with a HashSet, whose enumeration order is not contractual: after a rollback
frees and reuses a slot, enumeration can diverge from commit order, so the
durable read-through could resume a stale checkpoint. Mirror the HashSet with an
insertion-ordered list and enumerate it from RetrieveIndexAsync so 'latest' is
reliable. Add a CheckpointManager.GetLatestCheckpointAsync contract test over the
file store.

Note: the HashSet disorder is only reachable via the internal rollback path, so
the test locks the ordering contract rather than reproducing the rare disorder.
RunOrResumeStreamingAsync recorded the head checkpoint only after the stream was
fully enumerated. If an SSE consumer disconnected mid-turn after supersteps had
committed, the in-memory cursor kept the previous turn's head; because the next
turn is then a cursor hit, durable read-through could not self-heal, so it
resumed pre-disconnect state. Record the run's last committed checkpoint in a
finally so an abandoned stream still advances the cursor. Add a red/green test.
ExtractUpdates streamed every agent's updates, so the sequential Writer->Reviewer
sample streamed the intermediate draft and the final answer over SSE, differing
from the non-streaming response (final message only). Filter the streamed updates
to the final agent so streaming and non-streaming produce the same response.
Live-verified against Foundry: one output item streamed instead of two.
The concurrency test asserted the second same-session turn did not enter the
workflow, which also passes via the engine's concurrent-run ownership guard
(which faults) rather than the holder lock (which waits). Assert instead that the
second turn is not completed while the first holds the lock: a fault would
complete the task, so a pending task isolates the holder lock from the engine
guard. Verified red with the lock removed.
@rogerbarreto
rogerbarreto force-pushed the channels-protocol-extensions branch from 2fb52b3 to 06ff24b Compare July 9, 2026 16:15
@giles17 giles17 added the workflows Usage: [Issues, PRs], Target: Workflows label Jul 9, 2026
…ssions

HostedWorkflowState backed every session with one shared Workflow instance and
serialized all turns through a lock, so independent sessions could not run
concurrently. Add a workflow-factory constructor and remove the run lock.

- New constructor HostedWorkflowState(Func<CancellationToken, ValueTask<Workflow>>
  workflowFactory, ..., bool cacheWorkflow = false):
  - cacheWorkflow: false (default) builds a fresh instance per run, so independent
    sessions run in parallel. A resume rehydrates a fresh instance from the
    session's checkpoint in the shared store.
  - cacheWorkflow: true builds the workflow once, lazily on first use, and reuses
    it (a deferred, cached target that, like a shared instance, cannot run
    concurrent turns).
- Remove the internal SemaphoreSlim run lock and IDisposable; the instance
  constructor is unchanged in behaviour (one shared instance still cannot run
  concurrent turns). Turns are no longer serialized by the holder; a single
  writer per session is the application's responsibility.
- Switch the local_responses_workflow sample to the factory constructor with an
  explicit cacheWorkflow: false, and document the option.
- Add tests: parallel independent sessions (factory), fresh-instance resume,
  cached factory builds once and reuses, uncached factory builds per run.
- Update ADR-0032, spec-003, and the sample README.
Comment thread docs/decisions/0032-dotnet-hosting-protocol-helpers.md
Comment thread dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs
Comment thread dotnet/src/Microsoft.Agents.AI.Hosting/AgentSessionStore.cs
Comment thread dotnet/src/Microsoft.Agents.AI.Hosting/HostedWorkflowState.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI.Workflows/StreamingRun.cs
@rogerbarreto rogerbarreto added the breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible label Jul 22, 2026
@github-actions github-actions Bot changed the title .NET: Hosting OpenAI Responses protocol helpers and optional execution state .NET: [BREAKING] Hosting OpenAI Responses protocol helpers and optional execution state Jul 22, 2026
@rogerbarreto
rogerbarreto enabled auto-merge July 22, 2026 10:17
@rogerbarreto
rogerbarreto added this pull request to the merge queue Jul 22, 2026
Merged via the queue into microsoft:main with commit 1f1da1b Jul 22, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net workflows Usage: [Issues, PRs], Target: Workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants