Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
e1858a9
Hosting.Channels spec (.NET port of Python hosting channels)
rogerbarreto May 28, 2026
4662915
Hosting.Channels: core abstractions scaffold (draft)
rogerbarreto May 28, 2026
637bd65
Hosting.Channels: FileHostStateStore, OneTimeCodeIdentityLinker, Resp…
rogerbarreto May 28, 2026
e89b5b2
Hosting.Channels.Invocations: JSON invocation channel
rogerbarreto May 28, 2026
6ef5344
Hosting.Channels: WorkflowRunner end-to-end + Invocations workflow re…
rogerbarreto May 28, 2026
41d60bd
Hosting.Channels.Telegram: Telegram Bot channel with webhook + pollin…
rogerbarreto May 28, 2026
f4c7302
Hosting.Channels: sample + 17 unit tests
rogerbarreto May 28, 2026
a870dd5
Hosting.Channels: split samples into one per channel
rogerbarreto May 28, 2026
35a88ae
Reduce hosting channels to ADR-0027 minimal core + Responses channel
rogerbarreto Jun 24, 2026
1b9b3f1
Hosting.Channels: wire channel lifecycle callbacks and stream-transfo…
rogerbarreto Jun 24, 2026
15099d2
Hosting.Channels: in-process integration tests for the host/channel c…
rogerbarreto Jun 24, 2026
91dba34
Hosting.Channels: integration tests for function calling and agent wo…
rogerbarreto Jun 24, 2026
7252e0e
Hosting.Channels: cover both parameterless and parameterized tools in…
rogerbarreto Jun 24, 2026
ce44b0e
Hosting.Channels: drop unused System.Linq.AsyncEnumerable package ref…
rogerbarreto Jun 25, 2026
069679c
Hosting.Channels samples: use OpenAI SDK directly instead of Azure.AI…
rogerbarreto Jun 25, 2026
8a984e0
Hosting.Channels: convert records to plain classes and trim interfaces
rogerbarreto Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
496 changes: 496 additions & 0 deletions docs/specs/003-dotnet-hosting-channels.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion dotnet/agent-framework-dotnet.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Solution>
<Solution>
<Configurations>
<BuildType Name="Debug" />
<BuildType Name="Publish" />
Expand Down Expand Up @@ -377,6 +377,10 @@
<Project Path="samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/HostedAgentSkills.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableAgents/" />
<Folder Name="/Samples/04-hosting/HostingChannels/">
<Project Path="samples/04-hosting/HostingChannels/01_ResponsesAgent/01_ResponsesAgent.csproj" />
<Project Path="samples/04-hosting/HostingChannels/02_ResponsesWorkflow/02_ResponsesWorkflow.csproj" />
</Folder>
<Folder Name="/Samples/04-hosting/DurableAgents/AzureFunctions/">
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/.editorconfig" />
<File Path="samples/04-hosting/DurableAgents/AzureFunctions/README.md" />
Expand Down Expand Up @@ -618,6 +622,8 @@
<Project Path="src/Microsoft.Agents.AI.Hosting.A2A/Microsoft.Agents.AI.Hosting.A2A.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AzureFunctions/Microsoft.Agents.AI.Hosting.AzureFunctions.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.Channels/Microsoft.Agents.AI.Hosting.Channels.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.Channels.Responses/Microsoft.Agents.AI.Hosting.Channels.Responses.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.OpenAI/Microsoft.Agents.AI.Hosting.OpenAI.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting.AspNetCore/Microsoft.Agents.AI.Hosting.AspNetCore.csproj" />
<Project Path="src/Microsoft.Agents.AI.Hosting/Microsoft.Agents.AI.Hosting.csproj" />
Expand Down Expand Up @@ -673,6 +679,8 @@
<Project Path="tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Harness.UnitTests/Microsoft.Agents.AI.Harness.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.A2A.UnitTests/Microsoft.Agents.AI.Hosting.A2A.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.Channels.UnitTests/Microsoft.Agents.AI.Hosting.Channels.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.Channels.IntegrationTests/Microsoft.Agents.AI.Hosting.Channels.IntegrationTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests/Microsoft.Agents.AI.Hosting.AGUI.AspNetCore.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests/Microsoft.Agents.AI.Hosting.AzureFunctions.UnitTests.csproj" />
<Project Path="tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>ResponsesAgentSample</AssemblyName>
<RootNamespace>ResponsesAgentSample</RootNamespace>
<NoWarn>$(NoWarn);MAAI001;OPENAI001;CA1303</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="OpenAI" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hosting.Channels\Microsoft.Agents.AI.Hosting.Channels.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hosting.Channels.Responses\Microsoft.Agents.AI.Hosting.Channels.Responses.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.

// Exposes an AIAgent on the OpenAI Responses-shaped channel.
// POST /responses { "input": "Hi" } -> Responses JSON
// POST /responses { "input": "Hi", "stream": true } -> SSE stream
// Session continuity is keyed by ChannelSession.IsolationKey; identical keys resolve to the same
// cached AgentSession. This sample lets the channel derive the session from previous_response_id.

#pragma warning disable CA1031

using System.ClientModel;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.Channels.Responses;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using OpenAI;
using OpenAI.Chat;

var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")
?? throw new InvalidOperationException("OPENAI_API_KEY is not set.");
var model = Environment.GetEnvironmentVariable("OPENAI_MODEL") ?? "gpt-5.4-mini";

// The hosting channel only needs an OpenAI-compatible chat client, so it depends on the OpenAI SDK
// directly rather than Azure.AI.OpenAI. No Azure-specific features are required here.
AIAgent agent = new OpenAIClient(new ApiKeyCredential(apiKey))
.GetChatClient(model)
.AsAIAgent(name: "Concierge", instructions: "You are a helpful concierge.");

var builder = WebApplication.CreateBuilder(args);

builder.AddAgentFrameworkHost(agent)
.AddResponsesChannel();

var app = builder.Build();
app.MapAgentFrameworkHost();
app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Responses agent

Exposes an `AIAgent` on the OpenAI Responses-shaped channel from
`Microsoft.Agents.AI.Hosting.Channels.Responses`. The smallest demonstration of
`AddAgentFrameworkHost(agent).AddResponsesChannel()` + `MapAgentFrameworkHost()`.

## What it shows

* One `AgentFrameworkHost` owning a single Responses channel
* `POST /responses` returning a Responses JSON object
* `POST /responses` with `"stream": true` returning a Server-Sent-Events stream
* Session continuity keyed by `ChannelSession.IsolationKey` (here derived from `previous_response_id`)

## Requirements

* `OPENAI_API_KEY` set
* `OPENAI_MODEL` optional; defaults to `gpt-5.4-mini`

## Try it

```bash
cd dotnet/samples/04-hosting/HostingChannels/01_ResponsesAgent
dotnet run
```

```bash
curl -X POST http://localhost:5000/responses \
-H "Content-Type: application/json" \
-d '{ "input": "Tell me a short joke." }'

curl -N -X POST http://localhost:5000/responses \
-H "Content-Type: application/json" \
-d '{ "input": "Outline a haiku about spring.", "stream": true }'
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>ResponsesWorkflowSample</AssemblyName>
<RootNamespace>ResponsesWorkflowSample</RootNamespace>
<NoWarn>$(NoWarn);MAAI001;OPENAI001;CA1303</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hosting.Channels\Microsoft.Agents.AI.Hosting.Channels.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Hosting.Channels.Responses\Microsoft.Agents.AI.Hosting.Channels.Responses.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Workflows;

namespace ResponsesWorkflowSample;

/// <summary>Minimal single-executor workflow body: echoes the input string as the workflow output.</summary>
internal sealed class EchoExecutor() : Executor<string, string>("EchoExecutor")
{
public override ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) =>
ValueTask.FromResult($"echo: {message}");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft. All rights reserved.

// Exposes a Workflow on the OpenAI Responses-shaped channel. A run hook turns the parsed Responses
// input (ChatMessage list) into the workflow's typed string input. Workflow checkpoint storage lives on
// the workflow; the host derives a per-isolation-key checkpoint location from StatePaths.

#pragma warning disable CA1031

using Microsoft.Agents.AI.Hosting.Channels;
using Microsoft.Agents.AI.Hosting.Channels.Responses;
using Microsoft.Agents.AI.Workflows;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using ResponsesWorkflowSample;

var builder = WebApplication.CreateBuilder(args);

// Application-defined single-executor workflow that echoes its input.
var echo = new EchoExecutor();
var workflow = new WorkflowBuilder(echo).WithOutputFrom(echo).Build();

builder.AddAgentFrameworkHost(workflow, o => o.StatePaths = new HostStatePathOptions { Root = "./.afhost" })
.AddResponsesChannel(o => o.RunHook = new WorkflowInputRunHook());

var app = builder.Build();
app.MapAgentFrameworkHost();
app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Responses-hosted workflow

Exposes a `Workflow` on the OpenAI Responses-shaped channel. A run hook adapts the channel's parsed
Responses input (a `ChatMessage` list) into the workflow's typed string input before the host invokes the
workflow.

## What it shows

* `AddAgentFrameworkHost(workflow).AddResponsesChannel(o => o.RunHook = ...)`
* The run-hook seam (`IChannelRunHook`) for workflow input preparation
* Host `StatePaths` for per-isolation-key workflow checkpoint location derivation

## Requirements

No model credentials required; the workflow echoes its input.

## Try it

```bash
cd dotnet/samples/04-hosting/HostingChannels/02_ResponsesWorkflow
dotnet run
```

```bash
curl -X POST http://localhost:5000/responses \
-H "Content-Type: application/json" \
-d '{ "input": "ping" }'
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Agents.AI.Hosting.Channels;
using Microsoft.Extensions.AI;

namespace ResponsesWorkflowSample;

/// <summary>
/// Run hook that adapts the Responses channel's parsed input (a <see cref="ChatMessage"/> list) into the
/// workflow's typed string input before the host invokes the <see cref="Microsoft.Agents.AI.Workflows.Workflow"/>.
/// </summary>
internal sealed class WorkflowInputRunHook : IChannelRunHook
{
public ValueTask<ChannelRequest> OnRequestAsync(ChannelRequest request, ChannelRunHookContext context, CancellationToken cancellationToken)
{
var text = request.Input switch
{
string s => s,
IEnumerable<ChatMessage> messages => string.Join("\n", messages.Select(m => m.Text)),
_ => request.Input.ToString() ?? string.Empty,
};

return new(new ChannelRequest(request) { Input = text });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Agents.AI.Hosting.Channels.Responses;

/// <summary>
/// Extensions on <see cref="IAgentFrameworkHostBuilder"/> for the OpenAI Responses channel.
/// </summary>
public static class AgentFrameworkHostBuilderResponsesExtensions
{
/// <summary>Add the OpenAI Responses-shaped channel.</summary>
public static IAgentFrameworkHostBuilder AddResponsesChannel(
this IAgentFrameworkHostBuilder builder,
Action<ResponsesChannelOptions>? configure = null)
{
Throw.IfNull(builder);
var options = new ResponsesChannelOptions();
configure?.Invoke(options);
return builder.AddChannel(new ResponsesChannel(options));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Text.Json.Serialization;

namespace Microsoft.Agents.AI.Hosting.Channels.Responses;

[JsonSerializable(typeof(ResponsesRequestModel))]
[JsonSerializable(typeof(ResponsesResponseModel))]
[JsonSerializable(typeof(ResponsesStreamResponseEvent))]
[JsonSerializable(typeof(ResponsesStreamTextDeltaEvent))]
[JsonSerializable(typeof(ResponsesErrorModel))]
[JsonSerializable(typeof(System.Text.Json.JsonElement))]
internal sealed partial class ResponsesJsonContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Microsoft.Agents.AI.Hosting.Channels.Responses;

/// <summary>Inbound OpenAI Responses request body (subset).</summary>
internal sealed class ResponsesRequestModel
{
[JsonPropertyName("input")] public JsonElement? Input { get; set; }
[JsonPropertyName("stream")] public bool Stream { get; set; }
[JsonPropertyName("previous_response_id")] public string? PreviousResponseId { get; set; }
[JsonPropertyName("model")] public string? Model { get; set; }
[JsonPropertyName("instructions")] public string? Instructions { get; set; }
[JsonPropertyName("metadata")] public Dictionary<string, object?>? Metadata { get; set; }
}

/// <summary>Outbound Responses object (non-streaming + terminal stream payload).</summary>
internal sealed class ResponsesResponseModel
{
[JsonPropertyName("id")] public string Id { get; set; } = string.Empty;
[JsonPropertyName("object")] public string Object { get; set; } = "response";
[JsonPropertyName("created_at")] public long CreatedAt { get; set; }
[JsonPropertyName("status")] public string Status { get; set; } = "completed";
[JsonPropertyName("model")] public string? Model { get; set; }
[JsonPropertyName("output")] public List<ResponsesOutputMessage> Output { get; set; } = [];
[JsonPropertyName("usage")] public ResponsesUsageModel? Usage { get; set; }
}

internal sealed class ResponsesOutputMessage
{
[JsonPropertyName("type")] public string Type { get; set; } = "message";
[JsonPropertyName("id")] public string Id { get; set; } = string.Empty;
[JsonPropertyName("role")] public string Role { get; set; } = "assistant";
[JsonPropertyName("status")] public string Status { get; set; } = "completed";
[JsonPropertyName("content")] public List<ResponsesOutputText> Content { get; set; } = [];
}

internal sealed class ResponsesOutputText
{
[JsonPropertyName("type")] public string Type { get; set; } = "output_text";
[JsonPropertyName("text")] public string Text { get; set; } = string.Empty;
[JsonPropertyName("annotations")] public List<object> Annotations { get; set; } = [];
}

internal sealed class ResponsesUsageModel
{
[JsonPropertyName("input_tokens")] public int InputTokens { get; set; }
[JsonPropertyName("output_tokens")] public int OutputTokens { get; set; }
[JsonPropertyName("total_tokens")] public int TotalTokens { get; set; }
}

/// <summary>SSE payload for response.created / response.completed.</summary>
internal sealed class ResponsesStreamResponseEvent
{
[JsonPropertyName("type")] public string Type { get; set; } = string.Empty;
[JsonPropertyName("response")] public ResponsesResponseModel Response { get; set; } = new();
}

/// <summary>SSE payload for response.output_text.delta.</summary>
internal sealed class ResponsesStreamTextDeltaEvent
{
[JsonPropertyName("type")] public string Type { get; set; } = "response.output_text.delta";
[JsonPropertyName("item_id")] public string ItemId { get; set; } = string.Empty;
[JsonPropertyName("output_index")] public int OutputIndex { get; set; }
[JsonPropertyName("content_index")] public int ContentIndex { get; set; }
[JsonPropertyName("delta")] public string Delta { get; set; } = string.Empty;
}

/// <summary>Error envelope.</summary>
internal sealed class ResponsesErrorModel
{
[JsonPropertyName("error")] public ResponsesErrorBody Error { get; set; } = new();
}

internal sealed class ResponsesErrorBody
{
[JsonPropertyName("type")] public string Type { get; set; } = "invalid_request_error";
[JsonPropertyName("message")] public string? Message { get; set; }
}
Loading