.NET: Migrate remaining Foundry hosted samples to source deployment (#7668)

* .NET: Migrate 6 hosted-agent samples to source (ZIP) deploy

Extend the source (ZIP) deploy pattern established for Hosted-ChatClientAgent to Hosted-LocalTools, Hosted-Workflow-Simple, Hosted-TextRag, Hosted-Observability, Hosted-Files and Hosted-FoundryAgent. Each gains an azure.yaml (codeConfiguration/remote_build, ASPNETCORE_URLS, model env) and the canonical .agentignore, a self-contained csproj (single target, CPM opt-out, explicit published package versions, AgentFrameworkVersion), a Program.cs that drops the shared contributor scaffolding for DefaultAzureCredential, an updated .env.example and README, and drops the container-mode files (agent.yaml, agent.manifest.yaml, Dockerfile, Dockerfile.contributor). LocalTools, Workflow-Simple, TextRag, Observability and Files were verified deploying live via remote_build; Workflow-Simple returns a workflow runtime error at invoke that is unrelated to the deploy mode.

* .NET: Migrate Hosted-Invocations-EchoAgent and Hosted-LocalCodeAct to source (ZIP) deploy

EchoAgent (Invocations protocol) and LocalCodeAct migrated to the zip/code-deploy pattern (azure.yaml, .agentignore, self-contained csproj, README, container files removed). EchoAgent maps /readiness explicitly because the Invocations SDK does not auto-map it. Both verified live via remote_build on a Foundry project; LocalCodeAct's execute_code ran server-side (compute 21+21 -> 42).

* .NET: Migrate remaining hosted-agent samples to source (ZIP) deploy

Migrate Hosted-McpTools, Hosted-MemoryAgent, Hosted-AgentSkills, Hosted-AzureSearchRag, Hosted-Toolbox, Hosted-Toolbox-AuthPaths and Hosted-ToolboxMcpSkills to the zip/code-deploy pattern (azure.yaml with codeConfiguration + sample-specific env passthrough, canonical .agentignore, self-contained csproj, Program.cs dropping the shared contributor scaffolding for DefaultAzureCredential, updated .env.example and README, container files removed). Also restore the Hosted-Invocations-EchoAgent csproj filename the solution references. McpTools verified live via remote_build against the public Microsoft Learn MCP server; the memory/search/toolbox/skills samples build locally and deploy via remote_build but need their external resources (memory store, search index, toolbox connections, skills) provisioned to exercise end to end.

* .NET: Migrate Hosted-Workflow-Handoff to source (ZIP) deploy

Migrate the triage handoff workflow sample to the zip/code-deploy pattern (azure.yaml with codeConfiguration and Azure OpenAI env passthrough, canonical .agentignore, self-contained csproj using AgentFrameworkVersion for Foundry/Foundry.Hosting/Hosting, Program.cs dropping the shared contributor scaffolding for DefaultAzureCredential, updated .env.example and README, container files removed). Builds via remote_build; live needs an Azure OpenAI resource (AZURE_OPENAI_ENDPOINT/AZURE_OPENAI_DEPLOYMENT).

* .NET: Copy Hosted-AgentSkills skills/ into build output

The startup provisioning helper reads SKILL.md files from AppContext.BaseDirectory/skills, but the project did not copy the skills/ folder to the build/publish output, so at runtime the source directory did not exist and provisioning was silently skipped. Add a Content include (PreserveNewest), matching the resources/ pattern already used by Hosted-Files.

* .NET: Suppress OPENAI001 in Hosted-Workflow-Handoff for standalone ZIP build

The repo-wide Directory.Build.props suppresses OPENAI001, but that file does
not travel in the code/ZIP deploy package. The standalone dotnet publish the
Foundry code deploy runs then fails with error OPENAI001 on the experimental
GetResponsesClient().AsIChatClient() call. Add OPENAI001 to the project NoWarn
so the sample builds in the code-deploy pipeline, matching SimpleAgent.csproj.

* .NET: Document live-verified idiosyncrasies in Foundry hosted sample READMEs

Align every FoundryHostedAgents sample README with the documented azd flow and
add the idiosyncrasies found while live-testing each sample on a Foundry project:

- All samples: 'azd down' reports success but does not delete the hosted agent;
  document the explicit REST DELETE needed to remove it.
- Hosted-Workflow-Handoff: it builds its own AzureOpenAIClient (data-plane), so
  the agent identity needs the 'Cognitive Services OpenAI User' role on the
  Azure OpenAI account. azd only grants 'Foundry User' on the project, so add a
  step to grant the data-plane role and explain the triage-step failure without it.
- Hosted-Toolbox / Toolbox-AuthPaths / ToolboxMcpSkills: the toolbox must already
  exist and the agent identity must be able to read it; toolboxes with OAuth-gated
  tools return an oauth_consent_request and response.incomplete on first invoke.

* .NET: Address Foundry hosted sample review feedback

Make sample configuration reject blank azd substitutions and document every required environment value inside the scaffolded project flow.

Separate the hosted endpoint name from the Foundry managed prompt-agent name, fix standalone MemoryAgent diagnostics, and complete the contributor local package feed for Hosting, LocalCodeAct, and MCP.

Use azd for agent invocation and az rest for authenticated administration without exposing tokens. Add native MCP approval handling to the toolbox consent client and make its local path target the standard responses endpoint.

Validated all changed samples locally, the contributor flow in PowerShell and Bash, and the supported live scenarios on the TAO cace project.

* .NET: Fix advanced hosted sample project access

Document and validate the Foundry User grant required by hosted version identities that access project data plane APIs.

Add the Skills preview feature header and use a writable temporary directory for downloaded skills because source deployments mount the application directory read only.

Update AgentSkills, MemoryAgent, FoundryAgent, and ToolboxMcpSkills deployment guides with the post deploy identity grant. All four scenarios passed live on the TAO cace project.
This commit is contained in:
Roger Barreto
2026-08-19 09:43:14 +00:00
committed by GitHub
parent 9917bddc2b
commit 0f583ec8a3
168 changed files with 4680 additions and 3830 deletions
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,2 +1,3 @@
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
# Foundry project endpoint is not required by this echo sample (no model call).
# Local development only: bind to the readiness port the Foundry runtime probes.
ASPNETCORE_URLS=http://+:8088
@@ -1,17 +0,0 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedInvocationsEchoAgent.dll"]
@@ -1,19 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Abstractions source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-invocations-echo-agent .
# docker run --rm -p 8088:8088 hosted-invocations-echo-agent
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedInvocationsEchoAgent.dll"]
@@ -1,32 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedInvocationsEchoAgent</RootNamespace>
<AssemblyName>HostedInvocationsEchoAgent</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
<UserSecretsId>27e5c7df-546c-477b-ab05-d1e070a1b78a</UserSecretsId>
<AgentFrameworkVersion>1.15.0</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.AgentServer.Invocations" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="OpenTelemetry.Api" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="Microsoft.Agents.AI.Abstractions" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.AgentServer.Invocations" Version="1.0.0-beta.5" />
<PackageReference Include="OpenTelemetry.Api" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Abstractions" Version="1.0.0" />
<PackageReference Include="Azure.AI.AgentServer.Invocations" />
</ItemGroup>
-->
</Project>
</Project>
@@ -19,6 +19,11 @@ builder.Services.AddScoped<InvocationHandler, EchoInvocationHandler>();
var app = builder.Build();
// The Foundry hosted runtime probes GET /readiness before routing invocations to the container.
// The Invocations SDK does not map that route (unlike the Responses SDK), so map it explicitly;
// without it every invoke fails with HTTP 424 session_not_ready.
app.MapGet("/readiness", () => Results.Ok());
// Map the Invocations protocol endpoints:
// POST /invocations — invoke the agent
// GET /invocations/{id} — get result (not used by this sample)
@@ -1,104 +1,121 @@
# Hosted-Invocations-EchoAgent
A minimal echo agent hosted as a Foundry Hosted Agent using the **Invocations protocol**. The agent reads the request body as plain text, passes it through a custom `EchoAIAgent`, and writes the echoed text back in the response. No LLM or Azure credentials are required.
A minimal agent that echoes the user's input back, hosted as a Foundry Hosted Agent over the **Invocations protocol**. No LLM or external service is required, so it is the simplest way to see the hosting pipeline end to end.
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## How it works
- `EchoAIAgent.cs` — a tiny `AIAgent` that returns `Echo: <input>`; no model call.
- `EchoInvocationHandler.cs` — an `InvocationHandler` that reads the request body as plain text, runs the agent, and writes the response back as `text/plain`.
- `Program.cs` — registers the agent and the Invocations SDK (`AddInvocationsServer` / `MapInvocationsServer`), and maps `GET /readiness`.
> **Readiness note:** unlike the Responses SDK, the Invocations SDK does **not** auto-map the
> `GET /readiness` route the Foundry runtime probes before routing calls. `Program.cs` maps it
> explicitly; without it every invoke fails with HTTP 424 `session_not_ready`.
## Files
| File | Purpose |
|------|---------|
| `Program.cs` | Registers the echo agent and the Invocations server, maps `/readiness`. |
| `EchoAIAgent.cs` | The echo agent (no LLM). |
| `EchoInvocationHandler.cs` | Reads the request body, runs the agent, writes `text/plain`. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy) and the `invocations` protocol. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedInvocationsEchoAgent.csproj` | Self-contained project: single target framework and explicit package versions. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An **existing** Foundry project (no model deployment is needed for this echo sample).
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
## Configuration
## Run and test locally
Copy the template:
**Terminal 1 — host the agent:**
```bash
cp .env.example .env
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
This project uses `ProjectReference` to build against the local Agent Framework source.
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent
dotnet run
```
The agent will start on `http://localhost:8088`.
The agent starts on `http://localhost:8088`.
### Test it
**Terminal 2 — invoke it** (the Invocations protocol takes a plain-text body and returns `text/plain`):
PowerShell:
```powershell
(Invoke-WebRequest -Uri http://localhost:8088/invocations -Method POST -Body "Hello!").Content
```
Bash:
```bash
curl -X POST http://localhost:8088/invocations \
-H "Content-Type: text/plain" \
-d "Hello, world!"
curl -X POST http://localhost:8088/invocations -d "Hello!"
```
Expected response:
You get back `Echo: Hello!`.
```
Echo: Hello, world!
```
## Deploy to Foundry (source / ZIP)
## Running with Docker
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output.
```powershell
$work = Join-Path $env:TEMP "hosted-invocations-echo-work"
mkdir $work
cd $work
### 1. Publish for the container runtime (Linux Alpine)
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/azure.yaml"
azd auth login
azd ai agent init -m $sample
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build the Docker image
```bash
docker build -f Dockerfile.contributor -t hosted-invocations-echo-agent .
```
### 3. Run the container
```bash
docker run --rm -p 8088:8088 hosted-invocations-echo-agent
```
### 4. Test it
```bash
curl -X POST http://localhost:8088/invocations \
-H "Content-Type: text/plain" \
-d "Hello from Docker!"
```
## Deploying to Foundry (azd spec)
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
Initialize an `azd` project from this sample's manifest:
```bash
mkdir hosted-invocations-echo-agent && cd hosted-invocations-echo-agent
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/invocations/Hosted-Invocations-EchoAgent/agent.manifest.yaml
```
Then deploy:
```bash
cd hosted-invocations-echo-agent
azd provision
azd deploy
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
Invoke the deployed agent through `azd` using the Invocations protocol:
```bash
azd env set AGENT_NAME hosted-invocations-echo-agent
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
azd ai agent invoke --protocol invocations "Hello!"
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
Clean up with `azd down`, then delete the working directory.
---
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-invocations-echo-agent" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
## NuGet package users
## Deploy your local framework changes (contributors)
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `Hosted-Invocations-EchoAgent.csproj` for the `PackageReference` alternative.
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload. To ship a local framework build instead, run the helper between `azd ai agent init` and
`azd provision`:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-invocations-echo-agent
```
See the
[`Hosted-ChatClientAgent`](../../responses/Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation.
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,27 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-invocations-echo-agent
displayName: "Hosted Invocations Echo Agent"
description: >
A minimal echo agent hosted as a Foundry Hosted Agent using the Invocations
protocol. Reads the request body as plain text, echoes it back in the response.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Invocations Protocol
- Agent Framework
template:
name: hosted-invocations-echo-agent
kind: hosted
protocols:
- protocol: invocations
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -1,9 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-invocations-echo-agent
protocols:
- protocol: invocations
version: 1.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,39 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-invocations-echo-agent
services:
ai-project:
host: azure.ai.project
hosted-invocations-echo-agent:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedInvocationsEchoAgent.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
env:
ASPNETCORE_URLS: http://+:8088
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A minimal agent that echoes the user input, hosted as a Foundry Hosted Agent over the Invocations protocol. No LLM or external service is required.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Agent Framework
name: hosted-invocations-echo-agent
protocols:
- protocol: invocations
version: 1.0.0
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,10 +1,21 @@
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AGENT_NAME=hosted-agent-skills
SKILL_NAMES=support-style,escalation-policy
# Set to true to provision sample skills to Foundry on startup (first-run convenience).
# In production, skills are provisioned externally — leave this unset or false.
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
# Set to true to upload the bundled sample skills on startup; SKILL_NAMES selects which to load.
PROVISION_SAMPLE_SKILLS=true
AZURE_BEARER_TOKEN=DefaultAzureCredential
SKILL_NAMES=<comma-separated-skill-names>
@@ -1,26 +0,0 @@
# Dockerfile for end-users consuming the Agent Framework via NuGet packages.
#
# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container,
# which only succeeds when the project references its dependencies via PackageReference (see the
# commented-out section in HostedAgentSkills.csproj). Contributors building from the
# agent-framework repository source must use Dockerfile.contributor instead because
# ProjectReference dependencies live outside this folder and cannot be restored from inside
# this build context.
#
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
@@ -1,22 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-agent-skills .
# docker run --rm -p 8088:8088 \
# -e AGENT_NAME=hosted-agent-skills \
# -e HOSTED_USER_ISOLATION_KEY=alice \
# --env-file .env hosted-agent-skills
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedAgentSkills.dll"]
@@ -1,40 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedAgentSkills</RootNamespace>
<AssemblyName>HostedAgentSkills</AssemblyName>
<NoWarn>$(NoWarn);MEAI001;OPENAI001;AAIP001</NoWarn>
<UserSecretsId>3ebf91b7-0ecf-4568-8ab9-841ff2132601</UserSecretsId>
<AgentFrameworkVersion>1.15.0-preview.260722.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI\Microsoft.Agents.AI.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
<!-- Bake the sample SKILL.md files into the published output so the startup provisioning
helper can find them under AppContext.BaseDirectory/skills inside the container. -->
<Content Include="skills\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI" Version="1.6.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
</ItemGroup>
-->
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- Include the skills/ directory in the publish output so the sample can provision them -->
<ItemGroup>
<None Include="skills\**\*" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>
</Project>
@@ -20,12 +20,11 @@
#pragma warning disable AAIP001 // ProjectAgentSkills is experimental
using System.ClientModel;
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
@@ -33,10 +32,13 @@ using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
string endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
string skillNames = Environment.GetEnvironmentVariable("SKILL_NAMES")
string deploymentName = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
"gpt-4o")!;
string skillNames = FirstNonBlank(System.Environment.GetEnvironmentVariable("SKILL_NAMES"))
?? throw new InvalidOperationException("SKILL_NAMES is not set. Provide a comma-separated list of skill names (e.g., support-style,escalation-policy).");
string[] requestedSkills = skillNames.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
@@ -60,12 +62,13 @@ foreach (string name in requestedSkills)
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
var credential = new DefaultAzureCredential();
AIProjectClient projectClient = new(new Uri(endpoint), credential);
ProjectAgentSkills skillsClient = projectClient.AgentAdministrationClient.GetAgentSkills();
var adminOptions = new AgentAdministrationClientOptions();
adminOptions.AddPolicy(new FoundryFeaturesPolicy("Skills=V1Preview"), PipelinePosition.PerCall);
var adminClient = new AgentAdministrationClient(new Uri(endpoint), credential, adminOptions);
ProjectAgentSkills skillsClient = adminClient.GetAgentSkills();
// ── Provision skills (sample convenience only — NOT a production pattern) ─────
// In production, skills are provisioned externally (e.g., via CI/CD or a management script).
@@ -73,7 +76,7 @@ ProjectAgentSkills skillsClient = projectClient.AgentAdministrationClient.GetAge
// out of the box without a separate setup step. Set PROVISION_SAMPLE_SKILLS=true to enable.
string sourceSkillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
bool provisionEnabled = string.Equals(
Environment.GetEnvironmentVariable("PROVISION_SAMPLE_SKILLS"), "true", StringComparison.OrdinalIgnoreCase);
System.Environment.GetEnvironmentVariable("PROVISION_SAMPLE_SKILLS"), "true", StringComparison.OrdinalIgnoreCase);
if (provisionEnabled && Directory.Exists(sourceSkillsDir))
{
await EnsureSkillsProvisionedAsync(skillsClient, sourceSkillsDir, requestedSkills);
@@ -83,7 +86,7 @@ if (provisionEnabled && Directory.Exists(sourceSkillsDir))
// Pull the latest copy of each skill from Foundry into a runtime-only folder.
// This directory is recreated on every startup so the agent always picks up
// the latest version of each skill.
string downloadedSkillsDir = Path.Combine(AppContext.BaseDirectory, "downloaded_skills");
string downloadedSkillsDir = Path.Combine(Path.GetTempPath(), "hosted-agent-skills", "downloaded_skills");
await DownloadSkillsAsync(skillsClient, requestedSkills, downloadedSkillsDir);
// ── Wire skills into the agent ───────────────────────────────────────────────
@@ -94,7 +97,7 @@ AgentSkillsProvider skillsProvider = new(downloadedSkillsDir);
AIAgent agent = projectClient.AsAIAgent(new ChatClientAgentOptions
{
Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-agent-skills",
Name = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-agent-skills",
ChatOptions = new ChatOptions
{
ModelId = deploymentName,
@@ -116,15 +119,14 @@ builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
// ── Helpers ──────────────────────────────────────────────────────────────────
static string? FirstNonBlank(params string?[] candidates) =>
Array.Find(candidates, candidate => !string.IsNullOrWhiteSpace(candidate));
// Downloads each named skill from Foundry into a separate subdirectory under the target directory.
// GetSkillContentAsync downloads the skill package and unzips it into the destination directory.
static async Task DownloadSkillsAsync(ProjectAgentSkills skillsClient, string[] skillNames, string targetDir)
@@ -151,6 +153,7 @@ static async Task DownloadSkillsAsync(ProjectAgentSkills skillsClient, string[]
$"Downloaded skill '{name}' did not contain a SKILL.md at the root.");
}
}
}
// Ensures each requested skill is provisioned in Foundry. For each skill name, checks whether
@@ -180,3 +183,24 @@ static async Task EnsureSkillsProvisionedAsync(ProjectAgentSkills skillsClient,
}
}
}
// Skills is a preview data-plane surface and requires an explicit feature opt-in on every call.
internal sealed class FoundryFeaturesPolicy(string feature) : PipelinePolicy
{
private const string FeatureHeader = "Foundry-Features";
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
message.Request.Headers.Add(FeatureHeader, feature);
ProcessNext(message, pipeline, currentIndex);
}
public override ValueTask ProcessAsync(
PipelineMessage message,
IReadOnlyList<PipelinePolicy> pipeline,
int currentIndex)
{
message.Request.Headers.Add(FeatureHeader, feature);
return ProcessNextAsync(message, pipeline, currentIndex);
}
}
@@ -1,135 +1,229 @@
# What this sample demonstrates
# Hosted-AgentSkills
An [Agent Framework](https://github.com/microsoft/agent-framework) agent that loads its behavioral guidelines from [**Foundry Skills**](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/skills) at startup, hosted using the **Responses protocol**. Skills are authored once as `SKILL.md` files, uploaded to your Foundry project through the Skills REST API, and downloaded by the agent on boot so updates ship without code changes.
A hosted agent that uploads and consumes Foundry Skills (SKILL.md) via the Skills REST API. Set PROVISION_SAMPLE_SKILLS=true to have the sample upload its bundled skills on startup, and SKILL_NAMES to select which to load.
## How It Works
### Authoring skills
Each skill is a Markdown file with a YAML front matter block. This sample ships two source skills under [`skills/`](skills/):
| Skill | Purpose |
|---|---|
| [`support-style`](skills/support-style/SKILL.md) | Voice, formatting, and signature rules for Contoso Outdoors support replies. |
| [`escalation-policy`](skills/escalation-policy/SKILL.md) | When and how to escalate a customer ticket. |
Each `SKILL.md` includes a unique `*-CANARY-*` token that the model is asked to echo, so you can prove the skill was loaded from Foundry (not hallucinated) by checking the response.
> The `name` and `description` values in the YAML front matter must be **unquoted** — quoting them causes the Skills REST API to return HTTP 500 on import.
### Uploading skills
The sample includes a convenience provisioning step that checks whether each skill exists in Foundry and uploads it if not, gated behind the `PROVISION_SAMPLE_SKILLS=true` env var. **In production, skill provisioning is an external concern** — it is NOT the hosted agent's responsibility. A real deployment pipeline would provision skills separately (e.g., via a CI/CD step, a CLI script, or a management portal).
The provisioning uses `ProjectAgentSkills.CreateSkillFromPackageAsync(directoryPath)` from the `Azure.AI.Projects.Agents` SDK. The method packages the `SKILL.md` file as a ZIP and uploads it to Foundry.
### Downloading skills at agent startup
[`Program.cs`](Program.cs) reads the comma-separated `SKILL_NAMES` env var and for each skill name downloads the ZIP archive from Foundry via `ProjectAgentSkills.DownloadSkillAsync(name)`, then unpacks it into a **separate runtime directory** at `downloaded_skills/<name>/` (kept distinct from the static `skills/` source folder).
An [`AgentSkillsProvider`](../../../../../src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs) is then built over `downloaded_skills/` and attached to the agent as a context provider. The provider follows the [Agent Skills](https://agentskills.io/) progressive-disclosure pattern:
1. **Advertise** — skill names and descriptions are injected into the system prompt at session start (~100 tokens per skill).
2. **Load** — the model calls the `load_skill` tool when it decides a skill is relevant to the user's turn, and the full `SKILL.md` body is returned.
This means the model only pays the token cost for a skill's full body when it actually needs it, and updating a skill in Foundry + restarting the agent is enough to pick up the change — no code redeploy required.
> **Note:** This sample supports instruction-only and resource-based skills. If your downloaded skills contain scripts, add a script runner when constructing the `AgentSkillsProvider`.
### Agent Hosting
The agent is hosted using the [Agent Framework](https://github.com/microsoft/agent-framework) with the Responses API hosting layer (`AddFoundryResponses` / `MapFoundryResponses`).
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- A Foundry project with a deployed model (e.g., `gpt-4o`)
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
- Permission to assign **Foundry User** on the Foundry project. The agent identity is created by
the first deploy, so this role is granted after `azd deploy`.
### Required RBAC
## Files
Your identity (or the Managed Identity running the container in production) needs **Azure AI User** on the Foundry project scope. This single role covers both authoring skills and downloading them.
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: provisions and loads Foundry Skills, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedAgentSkills.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Running the Agent Host
## Configuration
Set the required environment variables and run the sample with `dotnet run`:
Copy the template and fill in your project endpoint:
```bash
export FOUNDRY_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
export FOUNDRY_MODEL="gpt-4o"
export SKILL_NAMES="support-style,escalation-policy"
export PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry
```
Or in PowerShell:
PowerShell:
```powershell
$env:SKILL_NAMES="support-style,escalation-policy"
$env:PROVISION_SAMPLE_SKILLS="true" # First run only — provisions skills to Foundry
copy .env.example .env
```
You can also place these in a `.env` file next to `Program.cs` — see [`.env.example`](.env.example).
On startup you should see:
```text
Skill 'support-style' already exists in Foundry.
Skill 'escalation-policy' already exists in Foundry.
Downloading skill 'support-style' from Foundry...
Downloading skill 'escalation-policy' from Foundry...
```
The downloaded `SKILL.md` files land under `downloaded_skills/<name>/SKILL.md` next to the published output. This directory is recreated from scratch on every run, so deleting it manually is never necessary.
## Interacting with the agent
> Send a POST request to the server with a JSON body containing an `"input"` field to interact with the agent. For example:
Bash:
```bash
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "Hi, I am Alex. I just want to confirm I can return my tent within 30 days."}'
curl -X POST http://localhost:8088/responses -H "Content-Type: application/json" -d '{"input": "I want a $750 refund on Order #A-1042 right now or I am calling my lawyer."}'
cp .env.example .env
```
| Prompt mentions | Skill that should drive the response |
|---|---|
| Routine return / shipping / care question | Model loads `support-style` (canary `STYLE-CANARY-3318`) — no escalation. |
| Injury, legal threat, press, or refund > $500 | Model loads `escalation-policy` (canary `ESC-CANARY-7742`) **and** `support-style`. |
Because skills are loaded on demand, the canary token in a response also proves the model actually invoked `load_skill` for the matching skill (not just saw its name in the advertised list).
## Deploying the Agent to Foundry
When deploying to Foundry, make sure `SKILL_NAMES` is set in your `azd` environment so it gets injected into the hosted container per [`agent.manifest.yaml`](agent.manifest.yaml):
```bash
azd env set SKILL_NAMES "support-style,escalation-policy"
```env
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
PROVISION_SAMPLE_SKILLS=true
SKILL_NAMES=support-style,escalation-policy
ASPNETCORE_URLS=http://+:8088
AZURE_TOKEN_CREDENTIALS=dev
```
The deployed agent's Managed Identity needs **Azure AI User** on the Foundry project to download skills at startup.
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
> The `skills/` source folder is **not** deployed to Foundry — only the downloaded skills are used at runtime. The provisioning step must have been run against the same Foundry project before the agent can download the skills.
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
### Deploying to Foundry (azd spec)
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character` when a mark is present.
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no
> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS,
> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev`
> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe.
> Only for local runs; the deployed agent uses the platform-injected managed identity.
Initialize an `azd` project from this sample's manifest:
## Run and test locally
```bash
mkdir hosted-agent-skills && cd hosted-agent-skills
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/agent.manifest.yaml
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it,
see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills
az login
dotnet run
```
Then deploy:
The agent starts on `http://localhost:8088`.
**Terminal 2 — chat with it (code-first REPL):**
PowerShell:
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-agent-skills"
dotnet run -- --local
```
Bash:
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-agent-skills"
dotnet run -- --local
```
Try: `Introduce yourself using your configured skills.`
## Deploy to Foundry (source / ZIP)
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
### Step 1: create the working directory and enter it
PowerShell:
```powershell
$work = Join-Path $env:TEMP "hosted-agent-skills-work"
mkdir $work
cd $work
```
### Step 2: scaffold the project
`azd ai agent init` copies the sample into a subfolder named `hosted-agent-skills` (the top-level `name:`
in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts
you to pick the Foundry project; `-d` is the name of an existing model deployment in that project.
PowerShell:
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AgentSkills/azure.yaml"
azd auth login
azd ai agent init -m $sample -d <model-deployment>
```
### Step 3: provision and deploy
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
```
cd hosted-agent-skills
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd env set PROVISION_SAMPLE_SKILLS true
azd env set SKILL_NAMES support-style,escalation-policy
azd provision
azd deploy
# Grant the new hosted-agent identity project data-plane access:
az rest --method get --url "<project-endpoint>/agents/hosted-agent-skills" --url-parameters api-version=v1 --resource https://ai.azure.com --query "versions.latest.instance_identity.principal_id" --output tsv
az role assignment create --assignee-object-id <principal-id-from-previous-command> --assignee-principal-type ServicePrincipal --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope <project-resource-id>
# Wait briefly for the role assignment to propagate, then invoke:
azd ai agent invoke "Introduce yourself using your configured skills."
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
`53ca6127-db72-4b80-b1b0-d745d6d5456d` is the stable role definition ID for **Foundry User**.
Do not substitute `Azure AI Developer`; Microsoft documents that role as insufficient for Foundry
hosted agents. Recreate the role assignment when the agent is deleted and created again, because
the new agent receives a new identity.
The Skills API is a preview surface. `Program.cs` adds the required
`Foundry-Features: Skills=V1Preview` header and downloads skills into the writable temporary
directory. The source-deploy application directory (`/app`) is read-only.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-agent-skills" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-agent-skills
```
Bash:
```bash
azd env set AGENT_NAME hosted-agent-skills
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-agent-skills
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
## Troubleshooting
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
```
azd ai agent invoke --new-conversation "Hello!"
```
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,41 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-agent-skills
displayName: "Hosted Agent Skills"
description: >
An Agent Framework agent that downloads its behavioral guidelines from the Foundry
Skills REST API at startup, demonstrating how to decouple behavioral guidelines
(tone, escalation policy, etc.) from agent code using AgentSkillsProvider.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Agent Framework
- Agent Skills
- Foundry Skills
template:
name: hosted-agent-skills
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- name: FOUNDRY_MODEL
value: "{{FOUNDRY_MODEL}}"
- name: SKILL_NAMES
value: "{{SKILL_NAMES}}"
parameters:
properties:
- name: SKILL_NAMES
secret: false
description: Comma-separated list of Foundry skill names to download at startup (e.g., support-style,escalation-policy)
resources:
- kind: model
id: gpt-4.1-mini
name: FOUNDRY_MODEL
@@ -1,14 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-agent-skills
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
environment_variables:
- name: FOUNDRY_MODEL
value: ${FOUNDRY_MODEL}
- name: SKILL_NAMES
value: ${SKILL_NAMES}
@@ -0,0 +1,42 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-agent-skills
services:
ai-project:
host: azure.ai.project
hosted-agent-skills:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedAgentSkills.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
env:
ASPNETCORE_URLS: http://+:8088
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
SKILL_NAMES: ${SKILL_NAMES}
PROVISION_SAMPLE_SKILLS: ${PROVISION_SAMPLE_SKILLS}
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A hosted agent that uploads and consumes Foundry Skills (SKILL.md) via the Skills REST API.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Agent Framework
name: hosted-agent-skills
protocols:
- protocol: responses
version: 2.0.0
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,8 +1,21 @@
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
FOUNDRY_MODEL=gpt-4o
AZURE_SEARCH_ENDPOINT=<your-azure-search-endpoint>
AZURE_SEARCH_INDEX_NAME=contoso-outdoors
AZURE_BEARER_TOKEN_FOUNDRY=DefaultAzureCredential
AZURE_BEARER_TOKEN_SEARCH=DefaultAzureCredential
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
# Azure AI Search index (must be pre-provisioned; your identity needs read access).
AZURE_SEARCH_ENDPOINT=https://<your-search>.search.windows.net
AZURE_SEARCH_INDEX_NAME=<your-index>
@@ -1,17 +0,0 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedAzureSearchRag.dll"]
@@ -1,23 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-azure-search-rag .
# docker run --rm -p 8088:8088 \
# -e AGENT_NAME=hosted-azure-search-rag \
# -e AZURE_BEARER_TOKEN_FOUNDRY=$AZURE_BEARER_TOKEN_FOUNDRY \
# -e AZURE_BEARER_TOKEN_SEARCH=$AZURE_BEARER_TOKEN_SEARCH \
# --env-file .env hosted-azure-search-rag
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedAzureSearchRag.dll"]
@@ -1,36 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedAzureSearchRag</RootNamespace>
<AssemblyName>HostedAzureSearchRag</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
<UserSecretsId>6f2c3d71-5cbd-48fa-9a66-d807dadfb310</UserSecretsId>
<AgentFrameworkVersion>1.15.0-preview.260722.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Search.Documents" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Azure.Search.Documents" Version="12.0.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.OpenAI" Version="1.6.1" />
</ItemGroup>
-->
</Project>
</Project>
@@ -9,27 +9,27 @@
using Azure;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
using OpenAI.Chat;
// Load .env file if present (for local development)
Env.TraversePath().Load();
string projectEndpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
string projectEndpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
string deploymentName = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
"gpt-4o")!;
string searchEndpoint = Environment.GetEnvironmentVariable("AZURE_SEARCH_ENDPOINT")
string searchEndpoint = FirstNonBlank(System.Environment.GetEnvironmentVariable("AZURE_SEARCH_ENDPOINT"))
?? throw new InvalidOperationException("AZURE_SEARCH_ENDPOINT is not set.");
string searchIndexName = Environment.GetEnvironmentVariable("AZURE_SEARCH_INDEX_NAME")
string searchIndexName = FirstNonBlank(System.Environment.GetEnvironmentVariable("AZURE_SEARCH_INDEX_NAME"))
?? throw new InvalidOperationException("AZURE_SEARCH_INDEX_NAME is not set.");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
@@ -39,9 +39,7 @@ string searchIndexName = Environment.GetEnvironmentVariable("AZURE_SEARCH_INDEX_
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in
// production). The dev credential is scope aware so a single instance serves both Foundry and
// Azure AI Search clients (each Azure SDK client requests a token for its own audience).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
var credential = new DefaultAzureCredential();
// Connect to the pre-provisioned search index. The caller is expected to have created the
// index and populated it with documents matching the schema (id / content / sourceName /
@@ -57,7 +55,7 @@ TextSearchProviderOptions textSearchOptions = new()
AIAgent agent = new AIProjectClient(new Uri(projectEndpoint), credential)
.AsAIAgent(new ChatClientAgentOptions
{
Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-azure-search-rag",
Name = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-azure-search-rag",
ChatOptions = new ChatOptions
{
ModelId = deploymentName,
@@ -74,13 +72,12 @@ builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
static string? FirstNonBlank(params string?[] candidates) =>
Array.Find(candidates, candidate => !string.IsNullOrWhiteSpace(candidate));
// ── Search adapter ───────────────────────────────────────────────────────────
// Wraps a SearchClient as the delegate TextSearchProvider expects. Keyword/full-text only;
// no embeddings. Returns the top results and projects them into TextSearchResult entries
@@ -108,68 +105,3 @@ static Func<string, CancellationToken, Task<IEnumerable<TextSearchProvider.TextS
return results;
};
/// <summary>
/// A scope aware <see cref="TokenCredential"/> for local Docker debugging only.
/// Reads pre-fetched bearer tokens from environment variables, dispensing the right token
/// based on the requested scope:
/// <list type="bullet">
/// <item><description><c>ai.azure.com</c> scopes -> <c>AZURE_BEARER_TOKEN_FOUNDRY</c></description></item>
/// <item><description><c>search.azure.com</c> scopes -> <c>AZURE_BEARER_TOKEN_SEARCH</c></description></item>
/// </list>
/// For any other scope, throws <see cref="CredentialUnavailableException"/> so a chained
/// credential will fall through. This should NOT be used in production: tokens expire (~1 hour)
/// and cannot be refreshed.
///
/// Generate the tokens on your host and pass them to the container:
/// <code>
/// export AZURE_BEARER_TOKEN_FOUNDRY=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// export AZURE_BEARER_TOKEN_SEARCH=$(az account get-access-token --resource https://search.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN_FOUNDRY -e AZURE_BEARER_TOKEN_SEARCH ...
/// </code>
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string FoundryEnvironmentVariable = "AZURE_BEARER_TOKEN_FOUNDRY";
private const string SearchEnvironmentVariable = "AZURE_BEARER_TOKEN_SEARCH";
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> Resolve(requestContext.Scopes);
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> new(Resolve(requestContext.Scopes));
private static AccessToken Resolve(IReadOnlyList<string> scopes)
{
string? envVar = null;
foreach (var scope in scopes)
{
if (scope.Contains("search.azure.com", StringComparison.OrdinalIgnoreCase))
{
envVar = SearchEnvironmentVariable;
break;
}
if (scope.Contains("ai.azure.com", StringComparison.OrdinalIgnoreCase))
{
envVar = FoundryEnvironmentVariable;
break;
}
}
if (envVar is null)
{
throw new CredentialUnavailableException(
$"DevTemporaryTokenCredential cannot serve scopes [{string.Join(", ", scopes)}]; falling through.");
}
var token = Environment.GetEnvironmentVariable(envVar);
if (string.IsNullOrEmpty(token) || string.Equals(token, "DefaultAzureCredential", StringComparison.Ordinal))
{
throw new CredentialUnavailableException(
$"{envVar} environment variable is not set; falling through to next credential.");
}
return new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1));
}
}
@@ -1,221 +1,212 @@
# Hosted-AzureSearchRag
# Hosted-AzureSearchRag
A hosted agent with **Retrieval Augmented Generation (RAG)** capabilities backed by **Azure AI Search**. The agent grounds its answers in product documentation by running a keyword search against an Azure AI Search index before each model invocation, then citing the source in its response.
A hosted RAG agent grounded in an Azure AI Search index. Requires AZURE_SEARCH_ENDPOINT and AZURE_SEARCH_INDEX_NAME pointing at a pre-provisioned search index your identity can read.
This sample is the Azure AI Search counterpart to `Hosted-TextRag`. Where `Hosted-TextRag` uses a mock in-process search function, this sample talks to a real Azure AI Search index that is provisioned out of band (see "Provisioning the search index" below).
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A Foundry project with a deployed model (e.g., `gpt-4o`)
- An Azure AI Search service ([create one](https://learn.microsoft.com/azure/search/search-create-service-portal))
- **A pre-provisioned search index** with the schema and content described in the next section
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
### Required RBAC
## Files
Your identity (or the Managed Identity running the container in production) needs:
- **Azure AI User** on the Foundry project scope
- **Search Index Data Reader** on the Azure AI Search service (the sample only reads from the index)
## Provisioning the search index (one time)
The sample assumes the search index already exists and contains documents the agent can retrieve from. Provision it once via the Azure Portal, the [REST API](https://learn.microsoft.com/azure/search/search-how-to-create-search-index), or the snippet below.
### Index schema
| Field | Type | Attributes |
|---|---|---|
| `id` | `Edm.String` | key, filterable |
| `content` | `Edm.String` | searchable (full-text) |
| `sourceName` | `Edm.String` | retrievable, filterable |
| `sourceLink` | `Edm.String` | retrievable |
### Example: provision and seed via Azure CLI + REST
```bash
SEARCH_ENDPOINT="https://<your-search>.search.windows.net"
INDEX_NAME="contoso-outdoors"
TOKEN=$(az account get-access-token --resource https://search.azure.com --query accessToken -o tsv)
# 1. Create the index.
curl -X PUT "$SEARCH_ENDPOINT/indexes/$INDEX_NAME?api-version=2024-07-01" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "contoso-outdoors",
"fields": [
{ "name": "id", "type": "Edm.String", "key": true, "filterable": true, "searchable": false, "retrievable": true },
{ "name": "content", "type": "Edm.String", "key": false, "filterable": false, "searchable": true, "retrievable": true, "analyzer": "standard.lucene" },
{ "name": "sourceName", "type": "Edm.String", "key": false, "filterable": true, "searchable": false, "retrievable": true },
{ "name": "sourceLink", "type": "Edm.String", "key": false, "filterable": false, "searchable": false, "retrievable": true }
]
}'
# 2. Upload three Contoso Outdoors documents matching the queries below.
curl -X POST "$SEARCH_ENDPOINT/indexes/$INDEX_NAME/docs/index?api-version=2024-07-01" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"value": [
{ "@search.action": "mergeOrUpload", "id": "return-policy", "sourceName": "Contoso Outdoors Return Policy", "sourceLink": "https://contoso.com/policies/returns", "content": "Customers may return any item within 30 days of delivery. Items should be unused and include original packaging. Refunds are issued to the original payment method within 5 business days of inspection. As a thank you, every accepted return ships back with a complimentary Contoso TrailRunner sticker pack, item code TR-CANARY-7821." },
{ "@search.action": "mergeOrUpload", "id": "shipping-guide", "sourceName": "Contoso Outdoors Shipping Guide", "sourceLink": "https://contoso.com/help/shipping", "content": "Standard shipping is free on orders over $50 and typically arrives in 3-5 business days within the continental United States. Expedited options are available at checkout. Use promo code SHIP-CANARY-4493 at checkout for a one-time free overnight upgrade on your first order." },
{ "@search.action": "mergeOrUpload", "id": "tent-care", "sourceName": "TrailRunner Tent Care Instructions", "sourceLink": "https://contoso.com/manuals/trailrunner-tent", "content": "Clean the tent fabric with lukewarm water and a non-detergent soap. Allow it to air dry completely before storage and avoid prolonged UV exposure to extend the lifespan of the waterproof coating. Replacement waterproofing kits are stocked under SKU TENT-CANARY-9067." }
]
}'
```
You can also point the sample at any existing index that exposes the four fields above; the sample reads `content`, `sourceName`, and `sourceLink` as projected by the search results.
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: queries an Azure AI Search index for grounding context, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedAzureSearchRag.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your endpoints:
Copy the template and fill in your project endpoint:
PowerShell:
```powershell
copy .env.example .env
```
Bash:
```bash
cp .env.example .env
```
Edit `.env`:
```env
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
FOUNDRY_MODEL=gpt-4o
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
AZURE_SEARCH_ENDPOINT=https://<your-search>.search.windows.net
AZURE_SEARCH_INDEX_NAME=contoso-outdoors
AZURE_BEARER_TOKEN_FOUNDRY=DefaultAzureCredential
AZURE_BEARER_TOKEN_SEARCH=DefaultAzureCredential
AZURE_SEARCH_INDEX_NAME=<your-index-name>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_TOKEN_CREDENTIALS=dev
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
This project uses `ProjectReference` to build against the local Agent Framework source.
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character` when a mark is present.
```bash
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no
> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS,
> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev`
> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe.
> Only for local runs; the deployed agent uses the platform-injected managed identity.
## Run and test locally
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it,
see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag
AGENT_NAME=hosted-azure-search-rag dotnet run
az login
dotnet run
```
The agent will start on `http://localhost:8088`. The sample assumes the search index has already been provisioned and seeded (see "Provisioning the search index" above).
The agent starts on `http://localhost:8088`.
### Test it
**Terminal 2 — chat with it (code-first REPL):**
Using the Azure Developer CLI:
PowerShell:
```bash
azd ai agent invoke --local "What is your return policy?"
azd ai agent invoke --local "How long does shipping take?"
azd ai agent invoke --local "How do I clean my tent?"
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-azure-search-rag"
dotnet run -- --local
```
Or with curl:
Bash:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "What is your return policy?", "model": "hosted-azure-search-rag"}'
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-azure-search-rag"
dotnet run -- --local
```
## Running with Docker
Try: `What does the indexed documentation say about returns?`
Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output.
## Deploy to Foundry (source / ZIP)
### 1. Publish for the container runtime (Linux Alpine)
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
### Step 1: create the working directory and enter it
PowerShell:
```powershell
$work = Join-Path $env:TEMP "hosted-azure-search-rag-work"
mkdir $work
cd $work
```
### 2. Build the Docker image
### Step 2: scaffold the project
```bash
docker build -f Dockerfile.contributor -t hosted-azure-search-rag .
`azd ai agent init` copies the sample into a subfolder named `hosted-azure-search-rag` (the top-level `name:`
in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts
you to pick the Foundry project; `-d` is the name of an existing model deployment in that project.
PowerShell:
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/azure.yaml"
azd auth login
azd ai agent init -m $sample -d <model-deployment>
```
### 3. Run the container
### Step 3: provision and deploy
Generate two bearer tokens on your host (one per audience) and pass them to the container. A single Azure AD token has only one `aud` claim, so Foundry and Azure AI Search require separate tokens.
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
```bash
# Generate tokens (each expires in ~1 hour)
export AZURE_BEARER_TOKEN_FOUNDRY=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
export AZURE_BEARER_TOKEN_SEARCH=$(az account get-access-token --resource https://search.azure.com --query accessToken -o tsv)
# Run with both tokens
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-azure-search-rag \
-e AZURE_BEARER_TOKEN_FOUNDRY=$AZURE_BEARER_TOKEN_FOUNDRY \
-e AZURE_BEARER_TOKEN_SEARCH=$AZURE_BEARER_TOKEN_SEARCH \
--env-file .env \
hosted-azure-search-rag
```
### 4. Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "What is your return policy?"
```
## How RAG works in this sample
The `TextSearchProvider` runs a keyword search against the configured Azure AI Search index **before each model invocation**. When the index is seeded with the three Contoso Outdoors documents from the provisioning section above:
| User query mentions | Search result injected |
|---|---|
| "return", "refund" | Contoso Outdoors Return Policy (canary token: `TR-CANARY-7821`) |
| "shipping", "promo" | Contoso Outdoors Shipping Guide (canary token: `SHIP-CANARY-4493`) |
| "tent", "fabric" | TrailRunner Tent Care Instructions (canary token: `TENT-CANARY-9067`) |
The model receives the top three search results as additional context and cites the source in its response. Each seeded document includes a unique `*-CANARY-*` token that does not exist in any model training data, so the integration tests can prove an answer was grounded in retrieved content (not fabricated from training) by asking for the canary and asserting it appears in the response.
Replace the seed documents (or point the sample at an existing index with your own content) to ground the agent in your own knowledge base.
## Deploying to Foundry (azd spec)
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
Initialize an `azd` project from this sample's manifest:
```bash
mkdir hosted-azure-search-rag && cd hosted-azure-search-rag
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/agent.manifest.yaml
```
Then deploy:
```bash
cd hosted-azure-search-rag
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd env set AZURE_SEARCH_ENDPOINT https://<your-search>.search.windows.net
azd env set AZURE_SEARCH_INDEX_NAME <your-index-name>
azd provision
azd deploy
azd ai agent invoke "What does the indexed documentation say about returns?"
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-azure-search-rag" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-azure-search-rag
```
Bash:
```bash
azd env set AGENT_NAME hosted-azure-search-rag
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-azure-search-rag
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
---
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedAzureSearchRag.csproj` for the `PackageReference` alternative.
See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
## Troubleshooting
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
so every following invoke fails even though the agent itself is healthy. Start a fresh one:
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
```
azd ai agent invoke --new-conversation "Hello!"
```
Add `--new-session` as well if the failure persists.
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,31 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-azure-search-rag
displayName: "Hosted Azure AI Search RAG Agent"
description: >
A support specialist agent for Contoso Outdoors with RAG capabilities backed by
Azure AI Search. Uses TextSearchProvider with a SearchClient adapter to ground
answers in product documentation indexed in Azure AI Search before each model
invocation.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- RAG
- Azure AI Search
- Agent Framework
template:
name: hosted-azure-search-rag
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -1,9 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-azure-search-rag
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,42 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-azure-search-rag
services:
ai-project:
host: azure.ai.project
hosted-azure-search-rag:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedAzureSearchRag.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
env:
ASPNETCORE_URLS: http://+:8088
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
AZURE_SEARCH_ENDPOINT: ${AZURE_SEARCH_ENDPOINT}
AZURE_SEARCH_INDEX_NAME: ${AZURE_SEARCH_INDEX_NAME}
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A hosted RAG agent grounded in an Azure AI Search index.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Agent Framework
name: hosted-azure-search-rag
protocols:
- protocol: responses
version: 2.0.0
@@ -150,12 +150,7 @@ that too.
> unattended.
`azure.yaml` passes the model deployment to the container by reading it from the `azd` environment.
Confirm it landed there, and set it yourself if it did not:
```
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
```
Step 3 explicitly sets it inside the scaffolded project before provisioning.
PowerShell:
@@ -184,6 +179,8 @@ before the commands below. Everyone else can ignore it.
```
cd hosted-chat-client-agent
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd provision
azd deploy
azd ai agent invoke "Hello!"
@@ -219,6 +216,16 @@ dotnet run -- --remote
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-chat-client-agent" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,6 +0,0 @@
**/bin
**/obj
**/.vs
**/.vscode
.env
*.user
@@ -1,5 +1,17 @@
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
@@ -1,17 +0,0 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedFiles.dll"]
@@ -1,19 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-files .
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-files -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-files
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedFiles.dll"]
@@ -1,19 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedFiles</RootNamespace>
<AssemblyName>HostedFiles</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
<UserSecretsId>204facfe-a494-4273-a330-3ffa649bb789</UserSecretsId>
<AgentFrameworkVersion>1.15.0-preview.260722.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
@@ -24,18 +46,6 @@
</Content>
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
</ItemGroup>
-->
</Project>
</Project>
@@ -9,19 +9,18 @@
//
// * Session files (per-session $HOME volume) — files uploaded at runtime via the alpha
// Azure.AI.Projects.AgentSessionFiles SDK. Live at $HOME inside the per-session
// container, which the platform sets to /home/session by default
// (container-image-spec.md line 127, "If you use the session files API, $HOME is
// also the base path for those operations").
// container, which the platform sets to /home/session by default.
//
// Each source is exposed via a separate tool pair, each rooted at its own directory.
// Tools take a fileName, not a path: Path.GetFileName strips any directory components,
// then a canonicalize + StartsWith(root) check enforces the boundary. The model cannot
// be tricked into reading /etc/passwd or any path outside its tool's root, even via
// indirect prompt injection in an uploaded file.
// then a canonicalize + StartsWith(root) check enforces the boundary.
//
// This sample is deployed to Foundry directly from source (code / ZIP upload), so the
// platform builds and runs your code with no container image.
//
// Required environment variables:
// FOUNDRY_PROJECT_ENDPOINT - Foundry project endpoint
// FOUNDRY_MODEL - Model deployment name (default: gpt-4o)
// FOUNDRY_PROJECT_ENDPOINT - Foundry project endpoint
// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
//
// Optional:
// AGENT_NAME - Agent name (default: hosted-files)
@@ -32,46 +31,46 @@
using System.ComponentModel;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
// Load a local .env file when present (local development only). In Foundry the
// platform injects the required environment variables at runtime.
Env.TraversePath().Load();
// Bypass SampleEnvironment alias (which prompts on missing env vars) for optional values.
string? GetOptionalEnv(string key) => System.Environment.GetEnvironmentVariable(key);
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = GetOptionalEnv("FOUNDRY_MODEL") ?? "gpt-4o";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// Environment variables can arrive set but blank: azd substitutes an empty string when the azd
// environment does not define the variable referenced from azure.yaml. An empty string is not
// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK.
var deploymentName = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
"gpt-4o");
var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-files";
// WARNING: DefaultAzureCredential is convenient for development but requires careful
// consideration in production. Consider a specific credential (for example
// ManagedIdentityCredential) to avoid latency, unintended credential probing, and
// fallback security risks.
var credential = new DefaultAzureCredential();
// ── File roots (canonicalized once) ──────────────────────────────────────────
// Bundled root: where csproj <Content Include="resources\**"> lands at runtime.
// In the container that resolves to /app/resources/.
string bundledRoot = Path.GetFullPath(
GetOptionalEnv("BUNDLED_FILES_DIR")
System.Environment.GetEnvironmentVariable("BUNDLED_FILES_DIR")
?? Path.Combine(AppContext.BaseDirectory, "resources"));
// Session root: the per-session $HOME volume mounted by the Foundry platform.
// Files uploaded via AgentSessionFiles.UploadSessionFileAsync(sessionStoragePath: "foo")
// land at $HOME/foo per container-image-spec.md line 172.
string sessionRoot = Path.GetFullPath(
GetOptionalEnv("HOME")
System.Environment.GetEnvironmentVariable("HOME")
?? "/home/session");
// ── Tools: bundled files (image-baked, /app/resources/) ──────────────────────
@@ -106,7 +105,7 @@ string SafeListNames(string root)
}
return string.Join(
Environment.NewLine,
System.Environment.NewLine,
Directory.EnumerateFiles(root).Select(Path.GetFileName));
}
catch (Exception ex)
@@ -167,7 +166,7 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
both first. Always read the file before answering; do not guess. Quote
numbers and figures verbatim from the file.
""",
name: GetOptionalEnv("AGENT_NAME") ?? "hosted-files",
name: agentName,
description: "Hosted agent that answers questions over bundled (image-baked) and session-uploaded files via two scoped tool pairs.",
tools:
[
@@ -177,51 +176,15 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
AIFunctionFactory.Create(ReadSessionFile),
]);
// Host the agent using the Responses protocol.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
/// <summary>
/// A <see cref="TokenCredential"/> for local Docker debugging only.
/// Reads a pre-fetched bearer token from the <c>AZURE_BEARER_TOKEN</c> environment variable
/// once at startup. This should NOT be used in production.
///
/// Generate a token on your host and pass it to the container:
/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
/// </summary>
internal sealed class DevTemporaryTokenCredential : TokenCredential
{
private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
private readonly string? _token;
public DevTemporaryTokenCredential()
{
this._token = System.Environment.GetEnvironmentVariable(EnvironmentVariable);
}
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> this.GetAccessToken();
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
=> new(this.GetAccessToken());
private AccessToken GetAccessToken()
{
if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
{
throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
}
return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
}
}
// Returns the first candidate that has an actual value, ignoring null and blank entries.
static string FirstNonBlank(params string?[] candidates) =>
Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!;
@@ -1,156 +1,208 @@
# Hosted-Files
# Hosted-Files
A hosted agent that demonstrates **two distinct file knowledge sources** through scoped, security-hardened tools:
A hosted agent that answers questions over two file sources through scoped, path-safe tools: bundled files baked into the upload (read from /app/resources/) and per-session files uploaded at runtime (read from the session HOME volume).
- **Bundled files** (image-baked) — files the author packages with the agent at build time. Live at `/app/resources/` inside the container, copied from this project's [`resources/`](./resources/) folder via the csproj `<Content Include="resources\**\*" CopyToOutputDirectory="PreserveNewest" />` rule.
- **Session files** (per-session `$HOME` volume) — files the user uploads at runtime via the alpha `Azure.AI.Projects.AgentSessionFiles` SDK. Live at `$HOME` inside the per-session container. The Foundry platform sets `HOME=/home/session` by default and roots the session-files API there per [`container-image-spec.md` line 172](https://github.com/microsoft/foundrysdk-specs/blob/main/specs/agents/hosted_agents/container-spec/docs/container-image-spec.md): *"If you use the session files API, `$HOME` is also the base path for those operations; any paths given in those API endpoints will be relative to `$HOME`."*
## Tool surface
Each source is exposed via its own tool pair, rooted at its own directory. The model picks by intent.
| Tool | Source | Root |
|------|--------|------|
| `ListBundledFiles` | Bundled (image-baked) | `/app/resources/` |
| `ReadBundledFile` | Bundled (image-baked) | `/app/resources/` |
| `ListSessionFiles` | Session-uploaded | `$HOME` (`/home/session`) |
| `ReadSessionFile` | Session-uploaded | `$HOME` (`/home/session`) |
## Security model — distinct tools, distinct sandboxes
Each tool takes a `fileName` (no directory components allowed) and enforces three layers of defence inside the implementation:
1. **`Path.GetFileName(input)`** strips any directory parts from the model-supplied name. `"../../etc/passwd"` becomes `"passwd"`.
2. **`Path.GetFullPath(Combine(root, name))`** canonicalises the path.
3. **`fullPath.StartsWith(root + DirectorySeparatorChar)`** rejects anything that resolves outside the tool's root.
Failures return a controlled `"File '<input>' not found in <scope>."` rather than throwing or exposing the canonical path.
This is why the agent has four narrowly-scoped tools instead of a single `ReadFile(path)`:
- **Smaller per-tool attack surface.** Each tool has one purpose, one root, and no path-typed parameter. Even a buggy implementation can only leak its own directory.
- **Cross-boundary access is impossible by schema.** A prompt-injection attempt to make the bundled tool read a session path (or vice versa) does not even compile in the tool schema the model sees.
- **Read-only, non-recursive listing.** No write tools, no glob, no `..`.
## Companion
[`Using-Samples/SessionFilesClient`](../Using-Samples/SessionFilesClient/) — a thin chat REPL (same shape as [`SimpleAgent`](../Using-Samples/SimpleAgent/)) that points at the deployed Hosted-Files endpoint via `FoundryAgent` and lets you ask questions whose answers come from either file source.
## Live proof of the session-files contract
The end-to-end alpha-SDK round trip (client uploads via `AgentSessionFiles.UploadSessionFileAsync` → file arrives at `$HOME/<name>` inside the per-session container → agent's `ReadSessionFile` tool reads it → response quotes the verbatim contents) is exercised live by [`SessionFilesHostedAgentTests.UploadedFile_IsReadByHostedAgentAsync`](../../../../../tests/Foundry.Hosting.IntegrationTests/SessionFilesHostedAgentTests.cs) against the matching `session-files` scenario in the integration test container.
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A Foundry project with a deployed model (e.g., `gpt-4o`)
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
## Files
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: exposes scoped, path-safe tools over bundled and session files, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedFiles.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your project endpoint:
PowerShell:
```powershell
copy .env.example .env
```
Bash:
```bash
cp .env.example .env
```
Edit `.env`:
```env
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_TOKEN_CREDENTIALS=dev
```
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
```bash
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character` when a mark is present.
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no
> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS,
> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev`
> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe.
> Only for local runs; the deployed agent uses the platform-injected managed identity.
## Run and test locally
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it,
see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files
AGENT_NAME=hosted-files dotnet run
az login
dotnet run
```
The agent starts on `http://localhost:8088`.
## Try it from the SessionFilesClient REPL
**Terminal 2 — chat with it (code-first REPL):**
### Bundled files (works against any deployment, including local)
PowerShell:
```bash
cd ../Using-Samples/SessionFilesClient
$env:AGENT_ENDPOINT = "http://localhost:8088"
$env:AGENT_NAME = "hosted-files"
dotnet run
You> What is the total revenue in the contoso file?
Agent> The contoso file reports total revenue of "$1,482.6M".
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-files"
dotnet run -- --local
```
The agent calls `ListBundledFiles`, sees `contoso_q1_2026_report.txt`, calls `ReadBundledFile("contoso_q1_2026_report.txt")` (which resolves under `/app/resources/`), and quotes the figure verbatim.
### Session files (against a deployed agent)
Upload a file to a specific session via `azd ai agent files upload` or via the alpha `AgentSessionFiles` SDK (see the integration test for the SDK call), then ask the agent about it. The agent's `ReadSessionFile` tool reads from `$HOME` and surfaces the content the same way.
## Running with Docker
This project uses `ProjectReference`, so use `Dockerfile.contributor` which takes a pre-published output:
Bash:
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
docker build -f Dockerfile.contributor -t hosted-files .
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-files \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-files
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-files"
dotnet run -- --local
```
The bundled `resources/` folder is part of the published output and ships inside the image.
Try: `List the bundled files and summarize the Contoso Q1 2026 report.`
## Deploying to Foundry (azd spec)
## Deploy to Foundry (source / ZIP)
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
Initialize an `azd` project from this sample's manifest:
### Step 1: create the working directory and enter it
```bash
mkdir hosted-files && cd hosted-files
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml
PowerShell:
```powershell
$work = Join-Path $env:TEMP "hosted-files-work"
mkdir $work
cd $work
```
Then deploy:
### Step 2: scaffold the project
```bash
`azd ai agent init` copies the sample into a subfolder named `hosted-files` (the top-level `name:`
in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts
you to pick the Foundry project; `-d` is the name of an existing model deployment in that project.
PowerShell:
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/azure.yaml"
azd auth login
azd ai agent init -m $sample -d <model-deployment>
```
### Step 3: provision and deploy
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
```
cd hosted-files
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd provision
azd deploy
azd ai agent invoke "List the bundled files and summarize the Contoso Q1 2026 report."
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-files" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-files
```
Bash:
```bash
azd env set AGENT_NAME hosted-files
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-files
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
---
## Troubleshooting
## NuGet package users
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor` and switch the `ProjectReference` entries in `HostedFiles.csproj` to `PackageReference` (commented section in the csproj).
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
## Adding more bundled files
```
azd ai agent invoke --new-conversation "Hello!"
```
Drop additional text files into [`resources/`](./resources/). The csproj `<Content Include="resources\**\*" CopyToOutputDirectory="PreserveNewest" />` rule picks them up on the next `dotnet build` / `docker build`.
## Overrides
| Env var | Purpose | Default |
|---------|---------|---------|
| `BUNDLED_FILES_DIR` | Override the bundled-files root the tools read from. | `<process base dir>/resources` (`/app/resources/` in container) |
| `HOME` | The per-session sandbox volume root the session-files tools read from. Set by the Foundry platform; can be overridden for local testing. | `/home/session` |
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,30 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-files
displayName: "Hosted Files Agent"
description: >
A hosted agent that answers questions over a small set of files bundled
with its container image (under /app/resources/). Two local C# function
tools (ListFiles, ReadFile) surface the bundled file contents to the model.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Bundled Files
- Local Tools
- Agent Framework
template:
name: hosted-files
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -1,9 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-files
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,40 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-files
services:
ai-project:
host: azure.ai.project
hosted-files:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedFiles.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
env:
ASPNETCORE_URLS: http://+:8088
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A hosted agent that answers questions over bundled (image-baked) and session-uploaded files via scoped, path-safe tools.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Agent Framework
name: hosted-files
protocols:
- protocol: responses
version: 2.0.0
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,5 +1,17 @@
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Name of the existing Foundry-managed prompt agent to wrap.
MANAGED_AGENT_NAME=<your-managed-agent-name>
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AGENT_NAME=<your-foundry-agent-name>
AZURE_BEARER_TOKEN=DefaultAzureCredential
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
@@ -1,17 +0,0 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"]
@@ -1,19 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-foundry-agent .
# docker run --rm -p 8088:8088 -e AGENT_NAME=<your-agent> -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-foundry-agent
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedFoundryAgent.dll"]
@@ -1,33 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedFoundryAgent</RootNamespace>
<AssemblyName>HostedFoundryAgent</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
<UserSecretsId>9c3949a1-1d2f-453e-abeb-0643b1b95e88</UserSecretsId>
<AgentFrameworkVersion>1.15.0-preview.260722.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>
-->
</Project>
</Project>
@@ -1,49 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
// Hosted Foundry Agent - wraps an existing Foundry-managed (prompt) agent definition and serves it
// over the Responses protocol as a hosted agent. The managed agent is retrieved by name. It is
// deployed to Foundry directly from source (code / ZIP upload), so the platform builds and runs your
// code with no container image.
using Azure.AI.Projects;
using Azure.AI.Projects.Agents;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Agents.AI.Foundry.Hosting;
// Load .env file if present (for local development)
// Load a local .env file when present (local development only). In Foundry the
// platform injects the required environment variables at runtime.
Env.TraversePath().Load();
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
?? throw new InvalidOperationException("AGENT_NAME is not set.");
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// The existing Foundry-managed prompt agent to wrap. This is intentionally separate from
// FOUNDRY_AGENT_NAME, which the platform reserves for the hosted endpoint being deployed.
var managedAgentName = System.Environment.GetEnvironmentVariable("MANAGED_AGENT_NAME");
if (string.IsNullOrWhiteSpace(managedAgentName))
{
throw new InvalidOperationException("MANAGED_AGENT_NAME is not set.");
}
var aiProjectClient = new AIProjectClient(projectEndpoint, credential);
// WARNING: DefaultAzureCredential is convenient for development but requires careful
// consideration in production. Consider a specific credential (for example
// ManagedIdentityCredential) to avoid latency, unintended credential probing, and
// fallback security risks.
var aiProjectClient = new AIProjectClient(projectEndpoint, new DefaultAzureCredential());
// Retrieve the Foundry-managed agent by name (latest version).
ProjectsAgentRecord agentRecord = await aiProjectClient
.AgentAdministrationClient.GetAgentAsync(agentName);
.AgentAdministrationClient.GetAgentAsync(managedAgentName);
FoundryAgent agent = aiProjectClient.AsAIAgent(agentRecord);
// Host the agent as a Foundry Hosted Agent using the Responses API.
// Host the agent using the Responses protocol.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
@@ -1,161 +1,220 @@
# Hosted-FoundryAgent
# Hosted-FoundryAgent
A hosted agent that delegates to a **Foundry-managed agent definition**. Instead of defining the model, instructions, and tools inline in code, this sample retrieves an existing agent registered in the Foundry platform via `AIProjectClient.AsAIAgent(agentRecord)` and hosts it using the Responses protocol.
A hosted agent that wraps an existing Foundry-managed (prompt) agent definition, retrieves it by name, and serves it over the Responses protocol. Use this when you already have a prompt agent in your project and want to host it with the Agent Framework hosting pipeline.
This is the **Foundry hosting** pattern — the agent's behavior is configured in the platform (via Foundry UI, CLI, or API), and this server simply wraps and serves it.
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A Foundry project with a **registered agent** (created via Foundry UI, CLI, or API)
- An **existing** Foundry project with an **existing Foundry-managed prompt agent**. This sample
retrieves that agent by name; it does not create or configure the prompt agent.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
- Permission to assign **Foundry User** on the Foundry project. The agent identity is created by
the first deploy, so this role is granted after `azd deploy`.
## Files
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: retrieves an existing Foundry-managed agent by name and hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port to the container through env. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedFoundryAgent.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your project endpoint:
PowerShell:
```powershell
copy .env.example .env
```
Bash:
```bash
cp .env.example .env
```
Edit `.env` and set your Foundry project endpoint:
```env
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
MANAGED_AGENT_NAME=<your-managed-agent-name>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_TOKEN_CREDENTIALS=dev
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
You also need to set `AGENT_NAME` — the name of the Foundry-managed agent to host. This is injected automatically by the Foundry platform when deployed. For local development, pass it as an environment variable.
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
## Running directly (contributors)
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character` when a mark is present.
This project uses `ProjectReference` to build against the local Agent Framework source.
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no
> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS,
> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev`
> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe.
> Only for local runs; the deployed agent uses the platform-injected managed identity.
```bash
## Run and test locally
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it,
see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent
AGENT_NAME=<your-agent-name> dotnet run
az login
dotnet run
```
The agent will start on `http://localhost:8088`.
The agent starts on `http://localhost:8088`.
### Test it
**Terminal 2 — chat with it (code-first REPL):**
Using the Azure Developer CLI:
PowerShell:
```bash
azd ai agent invoke --local "Hello!"
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-foundry-agent"
dotnet run -- --local
```
Or with curl (specifying the agent name explicitly):
Bash:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Hello!", "model": "<your-agent-name>"}'
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-foundry-agent"
dotnet run -- --local
```
## Running with Docker
Try: `Hello!`
Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output.
## Deploy to Foundry (source / ZIP)
### 1. Publish for the container runtime (Linux Alpine)
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
### Step 1: create the working directory and enter it
PowerShell:
```powershell
$work = Join-Path $env:TEMP "hosted-foundry-agent-work"
mkdir $work
cd $work
```
### 2. Build the Docker image
### Step 2: scaffold the project
```bash
docker build -f Dockerfile.contributor -t hosted-foundry-agent .
`azd ai agent init` copies the sample into a subfolder named `hosted-foundry-agent` (the top-level
`name:` in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It
prompts you to pick the Foundry project.
PowerShell:
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/azure.yaml"
azd auth login
azd ai agent init -m $sample
```
### 3. Run the container
### Step 3: provision and deploy
Generate a bearer token on your host and pass it to the container:
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
```bash
# Generate token (expires in ~1 hour)
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
# Run with token
docker run --rm -p 8088:8088 \
-e AGENT_NAME=<your-agent-name> \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-foundry-agent
```
> **Note:** `AGENT_NAME` is passed via `-e` to simulate the platform injection. `AZURE_BEARER_TOKEN` provides Azure credentials to the container (tokens expire after ~1 hour). The `.env` file provides the remaining configuration.
### 4. Test it
Using the Azure Developer CLI:
```bash
azd ai agent invoke --local "Hello!"
```
Or with curl (specifying the agent name explicitly):
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Hello!", "model": "<your-agent-name>"}'
```
## Deploying to Foundry (azd spec)
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
Initialize an `azd` project from this sample's manifest:
```bash
mkdir hosted-foundry-agent && cd hosted-foundry-agent
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/agent.manifest.yaml
```
Then deploy:
```bash
cd hosted-foundry-agent
azd env set MANAGED_AGENT_NAME <your-managed-agent-name>
azd provision
azd deploy
# Grant the new hosted-agent identity project data-plane access:
az rest --method get --url "<project-endpoint>/agents/hosted-foundry-agent" --url-parameters api-version=v1 --resource https://ai.azure.com --query "versions.latest.instance_identity.principal_id" --output tsv
az role assignment create --assignee-object-id <principal-id-from-previous-command> --assignee-principal-type ServicePrincipal --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope <project-resource-id>
# Wait briefly for the role assignment to propagate, then invoke:
azd ai agent invoke "Hello!"
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
`53ca6127-db72-4b80-b1b0-d745d6d5456d` is the stable role definition ID for **Foundry User**.
This role lets the hosted-agent identity read and invoke the existing prompt agent. Do not use
`Azure AI Developer`; Microsoft documents that role as insufficient for Foundry hosted agents.
Recreate the assignment when the agent is deleted and created again.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-foundry-agent" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-foundry-agent
```
Bash:
```bash
azd env set AGENT_NAME hosted-foundry-agent
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-foundry-agent
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedFoundryAgent.csproj` for the `PackageReference` alternative.
## How it differs from Hosted-ChatClientAgent
| | Hosted-ChatClientAgent | Hosted-FoundryAgent |
|---|---|---|
| **Agent definition** | Inline in code (`AsAIAgent(model, instructions)`) | Managed in Foundry platform (`AsAIAgent(agentRecord)`) |
| **Model/instructions** | Set in `Program.cs` | Set in Foundry UI/CLI/API |
| **Tools** | Defined in code | Configured in the platform |
| **Use case** | Full control over agent behavior | Platform-managed agent with centralized config |
See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
## Troubleshooting
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
so every following invoke fails even though the agent itself is healthy. Start a fresh one:
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
```
azd ai agent invoke --new-conversation "Hello!"
```
Add `--new-session` as well if the failure persists.
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,28 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-foundry-agent
displayName: "Hosted Foundry Agent"
description: >
A simple general-purpose AI assistant hosted as a Foundry Hosted Agent,
backed by a Foundry-managed agent definition.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
- Agent Framework
template:
name: hosted-foundry-agent
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -1,9 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-foundry-agent
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,40 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-foundry-agent
services:
ai-project:
host: azure.ai.project
hosted-foundry-agent:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedFoundryAgent.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
env:
ASPNETCORE_URLS: http://+:8088
MANAGED_AGENT_NAME: ${MANAGED_AGENT_NAME}
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A hosted agent backed by an existing Foundry-managed (prompt) agent definition, retrieved by name and served over the Responses protocol.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Agent Framework
name: hosted-foundry-agent
protocols:
- protocol: responses
version: 2.0.0
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,6 +1,17 @@
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
LOCAL_CODEACT_PYTHON=python3
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
@@ -1,23 +0,0 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
# Install Python 3 so LocalCodeAct can spawn the embedded runner / validator.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 \
&& rm -rf /var/lib/apt/lists/*
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENV LOCAL_CODEACT_PYTHON=python3
ENTRYPOINT ["dotnet", "HostedLocalCodeAct.dll"]
@@ -1,24 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry and
# Microsoft.Agents.AI.LocalCodeAct sources, which means a standard multi-stage
# Docker build cannot resolve dependencies outside this folder. Instead, pre-publish
# the app targeting the container runtime and copy the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-local-codeact .
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-local-codeact -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-local-codeact
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
# Install Python 3 so LocalCodeAct can spawn the embedded runner / validator.
RUN apk add --no-cache python3
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENV LOCAL_CODEACT_PYTHON=python3
ENTRYPOINT ["dotnet", "HostedLocalCodeAct.dll"]
@@ -1,33 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<NoWarn>$(NoWarn);</NoWarn>
<RootNamespace>HostedLocalCodeAct</RootNamespace>
<AssemblyName>HostedLocalCodeAct</AssemblyName>
<UserSecretsId>dd574e14-75e7-41f0-8ee6-b7c62d6952cb</UserSecretsId>
<AgentFrameworkVersion>1.17.0-preview.260804.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.LocalCodeAct" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.LocalCodeAct\Microsoft.Agents.AI.LocalCodeAct.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.LocalCodeAct" Version="1.6.1-preview.260514.1" />
</ItemGroup>
-->
</Project>
</Project>
@@ -3,36 +3,48 @@
// Hosted Local CodeAct sample. Wires Microsoft.Agents.AI.LocalCodeAct into a
// Foundry hosted agent. The model only sees a single `execute_code` tool;
// `compute` and `fetch_data` are registered as sandbox-only host tools that
// generated Python reaches via `await call_tool(...)`. This mirrors the Python
// `foundry_hosted_agent.py` sample for the local-codeact package.
// generated Python reaches via `await call_tool(...)`. It is deployed to Foundry
// directly from source (code / ZIP upload), so the platform builds and runs your
// code with no container image.
//
// SECURITY: LocalCodeAct executes LLM-generated Python in the agent process.
// Only deploy this sample to an externally sandboxed environment such as a
// Foundry hosted-agent container.
//
// RUNTIME: this sample runs generated Python with a Python interpreter. The
// hosted dotnet_10 source-deployment runtime provides python3. Local runs use
// python.exe on Windows and python3 elsewhere; LOCAL_CODEACT_PYTHON overrides
// that selection when a different executable is required.
using System.ComponentModel;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Agents.AI.LocalCodeAct;
using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
// Load a local .env file when present (local development only). In Foundry the
// platform injects the required environment variables at runtime.
Env.TraversePath().Load();
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
string pythonExecutable = Environment.GetEnvironmentVariable("LOCAL_CODEACT_PYTHON")
?? (OperatingSystem.IsWindows() ? "python.exe" : "python3");
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// Environment variables can arrive set but blank: azd substitutes an empty string when the azd
// environment does not define the variable referenced from azure.yaml. An empty string is not
// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK.
var deploymentName = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
"gpt-4o");
var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-codeact";
var pythonExecutable = FirstNonBlank(
System.Environment.GetEnvironmentVariable("LOCAL_CODEACT_PYTHON"),
OperatingSystem.IsWindows() ? "python.exe" : "python3");
// ── Sandbox-only tools (model never sees these directly) ─────────────────────
@@ -87,10 +99,14 @@ var codeAct = new LocalCodeActProvider(pythonExecutable, codeActOptions);
// ── Build the hosted agent ───────────────────────────────────────────────────
AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
// WARNING: DefaultAzureCredential is convenient for development but requires careful
// consideration in production. Consider a specific credential (for example
// ManagedIdentityCredential) to avoid latency, unintended credential probing, and
// fallback security risks.
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(new ChatClientAgentOptions
{
Name = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-codeact",
Name = agentName,
Description = "Hosted CodeAct agent with sandbox-only compute and fetch_data tools.",
ChatOptions = new ChatOptions
{
@@ -105,15 +121,15 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
AIContextProviders = [codeAct],
});
// Host the agent using the Responses protocol.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
// Returns the first candidate that has an actual value, ignoring null and blank entries.
static string FirstNonBlank(params string?[] candidates) =>
Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!;
@@ -1,173 +1,208 @@
# Hosted-LocalCodeAct
A hosted agent that uses [`Microsoft.Agents.AI.LocalCodeAct`](../../../../../src/Microsoft.Agents.AI.LocalCodeAct/README.md)
to give the model a single `execute_code` tool. Two sandbox-only host tools,
`compute` and `fetch_data`, are registered on `LocalCodeActProvider` and are
reachable from inside generated Python via `await call_tool(...)` — never as
direct LLM tool calls.
A hosted CodeAct agent using the Responses protocol. The model sees a single execute_code tool and reaches sandbox-only compute and fetch_data host tools from generated Python via call_tool(...). SECURITY: LocalCodeAct executes LLM-generated Python in the agent process, so only deploy it to an externally sandboxed environment such as a Foundry hosted-agent container.
This mirrors the Python
[`foundry_hosted_agent.py`](https://github.com/microsoft/agent-framework/blob/main/python/packages/local_codeact/samples/foundry_hosted_agent.py)
sample for the `agent-framework-local-codeact` package.
> **⚠️ Security:** LocalCodeAct executes LLM-generated Python in the agent
> process. The package is not a sandbox — it relies on the Foundry hosted-agent
> container (or another externally sandboxed environment) for process,
> filesystem, and network isolation. Do not run this outside of a sandbox.
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- Python 3 available on `PATH` (used by `LocalCodeActProvider` to execute the
embedded runner and validator). Override with the `LOCAL_CODEACT_PYTHON`
environment variable if you need a specific interpreter path.
- A Foundry project with a deployed model (e.g., `gpt-4o`)
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
## Files
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: registers sandbox-only compute/fetch_data tools with a LocalCodeActProvider, exposes a single execute_code tool, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedLocalCodeAct.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your project endpoint:
PowerShell:
```powershell
copy .env.example .env
```
Bash:
```bash
cp .env.example .env
```
Edit `.env` and set your Foundry project endpoint:
```env
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
LOCAL_CODEACT_PYTHON=python3
AZURE_TOKEN_CREDENTIALS=dev
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
This project uses `ProjectReference` to build against the local Agent Framework
source, including the `Microsoft.Agents.AI.LocalCodeAct` package.
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character` when a mark is present.
```bash
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no
> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS,
> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev`
> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe.
> Only for local runs; the deployed agent uses the platform-injected managed identity.
## Run and test locally
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it,
see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct
AGENT_NAME=hosted-local-codeact dotnet run
az login
dotnet run
```
The agent will start on `http://localhost:8088`.
The agent starts on `http://localhost:8088`.
### Test it
**Terminal 2 — chat with it (code-first REPL):**
Using the Azure Developer CLI:
PowerShell:
```bash
azd ai agent invoke --local "Fetch all users, find the admins, multiply 7 by 6, and print the users, admins, and the multiplication result. Use execute_code with await call_tool(...)."
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-local-codeact"
dotnet run -- --local
```
Or with curl:
Bash:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Fetch all users, find the admins, multiply 7 by 6, and print the users, admins, and the multiplication result. Use execute_code with await call_tool(...).", "model": "hosted-local-codeact"}'
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-local-codeact"
dotnet run -- --local
```
## Running with Docker
Try: `Use compute to add 21 and 21, then tell me the result.`
Since this project uses `ProjectReference`, use `Dockerfile.contributor` which
takes a pre-published output. The image installs Python 3 so the embedded
runner and validator scripts can execute.
## Deploy to Foundry (source / ZIP)
### 1. Publish for the container runtime (Linux Alpine)
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
### Step 1: create the working directory and enter it
PowerShell:
```powershell
$work = Join-Path $env:TEMP "hosted-local-codeact-work"
mkdir $work
cd $work
```
### 2. Build the Docker image
### Step 2: scaffold the project
```bash
docker build -f Dockerfile.contributor -t hosted-local-codeact .
`azd ai agent init` copies the sample into a subfolder named `hosted-local-codeact` (the top-level `name:`
in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts
you to pick the Foundry project; `-d` is the name of an existing model deployment in that project.
PowerShell:
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/azure.yaml"
azd auth login
azd ai agent init -m $sample -d <model-deployment>
```
### 3. Run the container
### Step 3: provision and deploy
Generate a bearer token on your host and pass it to the container:
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
```bash
# Generate token (expires in ~1 hour)
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
# Run with token
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-local-codeact \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-local-codeact
```
### 4. Test it
```bash
azd ai agent invoke --local "Fetch all users and print the admins."
```
## How CodeAct works here
`LocalCodeActProvider` is registered as an `AIContextProvider`. On every run it
injects:
- A single `execute_code` tool that the model can call with a Python snippet.
- CodeAct instructions that teach the model to use `await call_tool(...)` for
the provider-owned host tools, rather than asking for direct tool calls.
The provider-owned host tools in this sample:
| Tool | Description |
|------|-------------|
| `compute(operation, a, b)` | Math operation: `add`, `subtract`, `multiply`, `divide`. |
| `fetch_data(table)` | Returns rows from a simulated `users` or `products` table. |
`execute_code` runs the generated Python in a separate Python process governed
by `ProcessExecutionLimits` (5 second timeout in this sample) and the
default-on AST allow-list validator that rejects disallowed imports, builtins,
and dynamic-eval constructs before execution.
## Deploying to Foundry (azd spec)
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent
spec (`agent.yaml`) for deployment to Foundry.
Initialize an `azd` project from this sample's manifest:
```bash
mkdir hosted-local-codeact && cd hosted-local-codeact
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/agent.manifest.yaml
```
Then deploy:
```bash
cd hosted-local-codeact
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd provision
azd deploy
azd ai agent invoke "Use compute to add 21 and 21, then tell me the result."
```
## NuGet package users
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
If you are consuming the Agent Framework as a NuGet package (not building from
source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See
the commented section in `HostedLocalCodeAct.csproj` for the `PackageReference`
alternative.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-local-codeact" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-local-codeact
```
Bash:
```bash
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-local-codeact
```
See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
## Troubleshooting
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
so every following invoke fails even though the agent itself is healthy. Start a fresh one:
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
```
azd ai agent invoke --new-conversation "Hello!"
```
Add `--new-session` as well if the failure persists.
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,30 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-local-codeact
displayName: "Hosted Local CodeAct Agent"
description: >
A hosted agent that uses the CodeAct pattern via
Microsoft.Agents.AI.LocalCodeAct. The model only sees an `execute_code`
tool and orchestrates `compute` and `fetch_data` sandbox-only host tools
via `await call_tool(...)` from inside generated Python.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Local CodeAct
- Agent Framework
template:
name: hosted-local-codeact
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.5"
memory: 1Gi
parameters:
properties: []
resources: []
@@ -1,9 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-local-codeact
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.5"
memory: 1Gi
@@ -0,0 +1,40 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-local-codeact
services:
ai-project:
host: azure.ai.project
hosted-local-codeact:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedLocalCodeAct.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
env:
ASPNETCORE_URLS: http://+:8088
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A hosted CodeAct agent: the model sees a single execute_code tool and reaches sandbox-only compute/fetch_data host tools from generated Python.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Agent Framework
name: hosted-local-codeact
protocols:
- protocol: responses
version: 2.0.0
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,5 +1,17 @@
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
@@ -1,17 +0,0 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedLocalTools.dll"]
@@ -1,19 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-local-tools .
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-local-tools -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-local-tools
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedLocalTools.dll"]
@@ -1,33 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which does two things this
sample must avoid: it turns on central package management, and it injects analyzer
PackageReference items whose versions it also supplies. Neither exists inside the ZIP, so
without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<!-- Single target: the Foundry dotnet_10 runtime publishes without -f, so the
project must not multi-target. The empty TargetFrameworks clears the value
inherited from the repo's samples Directory.Build.props for in-repo builds. -->
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedLocalTools</RootNamespace>
<AssemblyName>HostedLocalTools</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
<UserSecretsId>682a0481-5544-45e7-ad2b-88e335985c64</UserSecretsId>
<AgentFrameworkVersion>1.15.0-preview.260722.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
</ItemGroup>
-->
</Project>
</Project>
@@ -1,36 +1,36 @@
// Copyright (c) Microsoft. All rights reserved.
// Seattle Hotel Agent - A hosted agent with local C# function tools.
// Demonstrates how to define and wire local tools that the LLM can invoke,
// a key advantage of code-based hosted agents over prompt agents.
// Seattle Hotel Agent - a hosted agent with local C# function tools. Demonstrates how to define
// and wire local tools that the LLM can invoke, a key advantage of code-based hosted agents over
// prompt agents. It is deployed to Foundry directly from source (code / ZIP upload), so the
// platform builds and runs your code with no container image.
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
// Load a local .env file when present (local development only). In Foundry the
// platform injects the required environment variables at runtime.
Env.TraversePath().Load();
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// Environment variables can arrive set but blank: azd substitutes an empty string when the azd
// environment does not define the variable referenced from azure.yaml. An empty string is not
// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK.
var deploymentName = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
"gpt-4o");
var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-tools";
// ── Hotel data ───────────────────────────────────────────────────────────────
@@ -94,7 +94,11 @@ string GetAvailableHotels(
// ── Create and host the agent ────────────────────────────────────────────────
AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
// WARNING: DefaultAzureCredential is convenient for development but requires careful
// consideration in production. Consider a specific credential (for example
// ManagedIdentityCredential) to avoid latency, unintended credential probing, and
// fallback security risks.
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(
model: deploymentName,
instructions: """
@@ -110,23 +114,23 @@ AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
Be conversational and helpful. If users ask about things outside of Seattle hotels,
politely let them know you specialize in Seattle hotel recommendations.
""",
name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-local-tools",
name: agentName,
description: "Seattle hotel search agent with local function tools",
tools: [AIFunctionFactory.Create(GetAvailableHotels)]);
// Host the agent using the Responses protocol.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
// Returns the first candidate that has an actual value, ignoring null and blank entries.
static string FirstNonBlank(params string?[] candidates) =>
Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!;
// ── Types ────────────────────────────────────────────────────────────────────
internal sealed record Hotel(string Name, int PricePerNight, double Rating, string Location);
@@ -1,155 +1,229 @@
# Hosted-LocalTools
# Hosted-LocalTools
A hosted agent with **local C# function tools** for hotel search. Demonstrates how to define and wire local tools that the LLM can invoke a key advantage of code-based hosted agents over prompt agents.
A Seattle hotel search agent hosted as a Foundry Hosted Agent using the Responses protocol. The agent is created inline via `AIProjectClient.AsAIAgent(...)` and given a local C# function tool (`GetAvailableHotels`) that the model can invoke, a key advantage of code-based hosted agents over prompt agents. It is served with `AddFoundryResponses` / `MapFoundryResponses`.
The agent specializes in finding hotels in Seattle, with a `GetAvailableHotels` tool that searches a mock hotel database by dates and budget.
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A Foundry project with a deployed model (e.g., `gpt-4o`)
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
## Files
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: defines the `GetAvailableHotels` tool, builds the agent, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through `env`. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedLocalTools.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your project endpoint:
PowerShell:
```powershell
copy .env.example .env
```
Bash:
```bash
cp .env.example .env
```
Edit `.env` and set your Foundry project endpoint:
```env
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_TOKEN_CREDENTIALS=dev
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
This project uses `ProjectReference` to build against the local Agent Framework source.
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character "»" in variable name` when a mark
> is present. PowerShell's `Set-Content -Encoding UTF8BOM` adds one; use `-Encoding utf8NoBOM`.
```bash
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential` (the pattern the hosted platform
> expects, where a managed identity is injected). On a developer machine with no managed identity,
> `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, `169.254.169.254`) and
> blocks for a long time on the network timeout before every model call, so requests appear to
> hang. Setting `AZURE_TOKEN_CREDENTIALS=dev` restricts `DefaultAzureCredential` to developer
> credentials (Azure CLI, Visual Studio, `azd`) and skips the managed-identity probe. This variable
> is only for local runs; the deployed agent in Foundry uses the platform-injected managed identity.
## Run and test locally
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it
using Agent Framework components, see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools
AGENT_NAME=hosted-local-tools dotnet run
az login
dotnet run
```
The agent will start on `http://localhost:8088`.
The agent starts on `http://localhost:8088`.
### Test it
**Terminal 2 — chat with it (code-first REPL):**
Using the Azure Developer CLI:
PowerShell:
```bash
azd ai agent invoke --local "Find me a hotel in Seattle for Dec 20-25 under $200/night"
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-local-tools"
dotnet run -- --local
```
Or with curl:
Bash:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "Find me a hotel in Seattle for Dec 20-25 under $200/night", "model": "hosted-local-tools"}'
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-local-tools"
dotnet run -- --local
```
## Running with Docker
Try: `Find me hotels in Seattle from 2026-09-01 to 2026-09-03 under $200 per night.`
Since this project uses `ProjectReference`, use `Dockerfile.contributor` which takes a pre-published output.
## Deploy to Foundry (source / ZIP)
### 1. Publish for the container runtime (Linux Alpine)
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
### Step 1: create the working directory and enter it
PowerShell:
```powershell
$work = Join-Path $env:TEMP "hosted-local-tools-work"
mkdir $work
cd $work
```
### 2. Build the Docker image
Bash:
```bash
docker build -f Dockerfile.contributor -t hosted-local-tools .
WORK="${TMPDIR:-/tmp}/hosted-local-tools-work"
mkdir -p "$WORK"
cd "$WORK"
```
### 3. Run the container
### Step 2: scaffold the project
Generate a bearer token on your host and pass it to the container:
`azd ai agent init` copies the sample into a subfolder named after the top-level `name:` in
`azure.yaml`, which is `hosted-local-tools`, and writes the adopted `azure.yaml` and the `azd`
environment there. It prompts you to pick the Foundry project; `-d` is the name of an existing
model deployment in that project.
```bash
# Generate token (expires in ~1 hour)
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
PowerShell:
# Run with token
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-local-tools \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-local-tools
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/azure.yaml"
azd auth login
azd ai agent init -m $sample -d <model-deployment>
```
### 4. Test it
Using the Azure Developer CLI:
Bash:
```bash
azd ai agent invoke --local "What hotels are available in Seattle for next weekend?"
SAMPLE="<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/azure.yaml"
azd auth login
azd ai agent init -m "$SAMPLE" -d <model-deployment>
```
## How local tools work
### Step 3: provision and deploy
The agent has a single tool `GetAvailableHotels` defined as a C# method with `[Description]` attributes. The LLM decides when to call it based on the user's request:
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
| Parameter | Type | Description |
|-----------|------|-------------|
| `checkInDate` | string | Check-in date (YYYY-MM-DD) |
| `checkOutDate` | string | Check-out date (YYYY-MM-DD) |
| `maxPrice` | int | Max price per night in USD (default: 500) |
The tool searches a mock database of 6 Seattle hotels and returns formatted results with name, location, rating, and pricing.
## Deploying to Foundry (azd spec)
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
Initialize an `azd` project from this sample's manifest:
```bash
mkdir hosted-local-tools && cd hosted-local-tools
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/agent.manifest.yaml
```
Then deploy:
```bash
cd hosted-local-tools
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd provision
azd deploy
azd ai agent invoke "Find me hotels in Seattle under $200 a night."
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-local-tools" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-local-tools
```
Bash:
```bash
azd env set AGENT_NAME hosted-local-tools
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-local-tools
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
---
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedLocalTools.csproj` for the `PackageReference` alternative.
Then continue with step 3. See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
## Troubleshooting
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
so every following invoke fails even though the agent itself is healthy. Start a fresh one:
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
```
azd ai agent invoke --new-conversation "Hello!"
```
Add `--new-session` as well if the failure persists.
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,29 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-local-tools
displayName: "Seattle Hotel Agent with Local Tools"
description: >
A travel assistant agent that helps users find hotels in Seattle.
Demonstrates local C# tool execution — a key advantage of code-based
hosted agents over prompt agents.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Local Tools
- Agent Framework
template:
name: hosted-local-tools
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -1,9 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-local-tools
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,46 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-local-tools
services:
ai-project:
host: azure.ai.project
hosted-local-tools:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedLocalTools.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
#
# ${AZURE_AI_MODEL_DEPLOYMENT_NAME} reads the model deployment `azd ai agent init` recorded
# in the active azd environment. Without it the container falls back to the default model
# name hardcoded in Program.cs, which may not exist in the target project.
env:
ASPNETCORE_URLS: http://+:8088
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A Seattle hotel search agent hosted as a Foundry Hosted Agent, demonstrating local C# function tools the model can invoke.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
- Agent Framework
name: hosted-local-tools
protocols:
- protocol: responses
version: 2.0.0
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,5 +1,17 @@
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
@@ -1,17 +0,0 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedMcpTools.dll"]
@@ -1,18 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local source, which means a standard
# multi-stage Docker build cannot resolve dependencies outside this folder.
# Pre-publish the app targeting the container runtime and copy the output:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-mcp-tools .
# docker run --rm -p 8088:8088 -e AGENT_NAME=mcp-tools -e GITHUB_PAT=$GITHUB_PAT -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-mcp-tools
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedMcpTools.dll"]
@@ -1,34 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedMcpTools</RootNamespace>
<AssemblyName>HostedMcpTools</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
<UserSecretsId>2bfa4cec-b6ef-4e75-b2ca-8f7947cb6419</UserSecretsId>
<AgentFrameworkVersion>1.15.0-preview.260722.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
</ItemGroup>
-->
</Project>
</Project>
@@ -6,40 +6,38 @@
// McpClient, discovers tools, and handles tool invocations locally within the agent process.
//
// 2. SERVER-SIDE MCP: The agent declares a HostedMcpServerTool for the same MCP server which
// delegates tool discovery and invocation to the LLM provider (Azure OpenAI Responses API).
// The provider calls the MCP server on behalf of the agent — no local connection needed.
// delegates tool discovery and invocation to the LLM provider (Responses API). The provider
// calls the MCP server on behalf of the agent — no local connection needed.
//
// Both patterns use the Microsoft Learn MCP server to illustrate the architectural difference:
// client-side tools are resolved and invoked by the agent, while server-side tools are resolved
// and invoked by the LLM provider.
// Both patterns use the public Microsoft Learn MCP server. It is deployed to Foundry directly from
// source (code / ZIP upload), so the platform builds and runs your code with no container image.
#pragma warning disable MEAI001 // HostedMcpServerTool is experimental
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
// Load .env file if present (for local development)
// Load a local .env file when present (local development only). In Foundry the
// platform injects the required environment variables at runtime.
Env.TraversePath().Load();
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
var deployment = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL")
?? "gpt-4o";
// WARNING: DefaultAzureCredential is convenient for development but requires careful
// consideration in production. Consider a specific credential (for example
// ManagedIdentityCredential) to avoid latency, unintended credential probing, and
// fallback security risks.
var credential = new DefaultAzureCredential();
// ── Client-side MCP: Microsoft Learn (local resolution) ──────────────────────
// Connect directly to the MCP server. The agent discovers and invokes tools locally.
@@ -70,6 +68,8 @@ Console.WriteLine("Server-side MCP tool: microsoft_docs_search (via HostedMcpSer
// The agent has access to tools from both MCP patterns simultaneously.
List<AITool> allTools = [.. clientTools.Cast<AITool>(), serverTool];
var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-mcp-tools";
AIAgent agent = new AIProjectClient(projectEndpoint, credential)
.AsAIAgent(
model: deployment,
@@ -78,20 +78,15 @@ AIAgent agent = new AIProjectClient(projectEndpoint, credential)
Use the available tools to search and retrieve documentation.
Be concise and provide direct answers with relevant links.
""",
name: "mcp-tools",
name: agentName,
description: "Developer assistant with dual-layer MCP tools (client-side and server-side)",
tools: allTools);
// Host the agent as a Foundry Hosted Agent using the Responses API.
// Host the agent using the Responses protocol.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
@@ -1,130 +1,208 @@
# Hosted-McpTools
# Hosted-McpTools
A hosted agent demonstrating **two layers of MCP (Model Context Protocol) tool integration**:
A hosted agent with dual-layer MCP tools against the public Microsoft Learn MCP server: client-side (McpClient, resolved in-process) and server-side (HostedMcpServerTool, resolved by the LLM provider).
1. **Client-side MCP (Microsoft Learn)** — The agent connects directly to the Microsoft Learn MCP server via `McpClient`, discovers tools, and handles tool invocations locally within the agent process.
2. **Server-side MCP (Microsoft Learn)** — The agent declares a `HostedMcpServerTool` which delegates tool discovery and invocation to the LLM provider (Azure OpenAI Responses API). The provider calls the MCP server on behalf of the agent with no local connection needed.
## How the two MCP patterns differ
| | Client-side MCP | Server-side MCP |
|---|---|---|
| **Connection** | Agent connects to MCP server directly | LLM provider connects to MCP server |
| **Tool invocation** | Handled by the agent process | Handled by the Responses API |
| **Auth** | Agent manages credentials | Provider manages credentials |
| **Use case** | Custom/private MCP servers, fine-grained control | Public MCP servers, simpler setup |
| **Example** | Microsoft Learn (`McpClient` + `HttpClientTransport`) | Microsoft Learn (`HostedMcpServerTool`) |
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A Foundry project with a deployed model (e.g., `gpt-4o`)
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
## Files
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: connects to the Microsoft Learn MCP server client-side and declares a server-side HostedMcpServerTool, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedMcpTools.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your values:
Copy the template and fill in your project endpoint:
PowerShell:
```powershell
copy .env.example .env
```
Bash:
```bash
cp .env.example .env
```
Edit `.env`:
```env
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
FOUNDRY_MODEL=gpt-4o
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
ASPNETCORE_URLS=http://+:8088
AZURE_TOKEN_CREDENTIALS=dev
```
## Running directly (contributors)
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
```bash
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character` when a mark is present.
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no
> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS,
> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev`
> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe.
> Only for local runs; the deployed agent uses the platform-injected managed identity.
## Run and test locally
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it,
see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools
az login
dotnet run
```
### Test it
The agent starts on `http://localhost:8088`.
Using the Azure Developer CLI:
**Terminal 2 — chat with it (code-first REPL):**
```bash
# Uses GitHub MCP (client-side)
azd ai agent invoke --local "Search for the agent-framework repository on GitHub"
PowerShell:
# Uses Microsoft Learn MCP (server-side)
azd ai agent invoke --local "How do I create an Azure storage account using az cli?"
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-mcp-tools"
dotnet run -- --local
```
## Running with Docker
### 1. Publish for the container runtime
Bash:
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-mcp-tools"
dotnet run -- --local
```
### 2. Build and run
Try: `Search Microsoft Learn: what is Azure AI Foundry Agent Service?`
```bash
docker build -f Dockerfile.contributor -t hosted-mcp-tools .
## Deploy to Foundry (source / ZIP)
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
docker run --rm -p 8088:8088 \
-e AGENT_NAME=mcp-tools \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-mcp-tools
### Step 1: create the working directory and enter it
PowerShell:
```powershell
$work = Join-Path $env:TEMP "hosted-mcp-tools-work"
mkdir $work
cd $work
```
## Deploying to Foundry (azd spec)
### Step 2: scaffold the project
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
`azd ai agent init` copies the sample into a subfolder named `hosted-mcp-tools` (the top-level `name:`
in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts
you to pick the Foundry project; `-d` is the name of an existing model deployment in that project.
Initialize an `azd` project from this sample's manifest:
PowerShell:
```bash
mkdir mcp-tools && cd mcp-tools
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/agent.manifest.yaml
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/azure.yaml"
azd auth login
azd ai agent init -m $sample -d <model-deployment>
```
Then deploy:
### Step 3: provision and deploy
```bash
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
```
cd hosted-mcp-tools
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd provision
azd deploy
azd ai agent invoke "Search Microsoft Learn: what is Azure AI Foundry Agent Service?"
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-mcp-tools" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-mcp-tools
```
Bash:
```bash
azd env set AGENT_NAME mcp-tools
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-mcp-tools
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
---
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedMcpTools.csproj` for the `PackageReference` alternative.
## Related samples
- [`Hosted-Toolbox/`](../Hosted-Toolbox/) — connects to a single Foundry Toolbox via the AF Foundry hosting bridge (`AddFoundryToolboxes` + `FoundryAITool.CreateHostedMcpToolbox`).
- [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/) — same hosting bones as `Hosted-Toolbox/`, but the toolbox bundles three MCP tools each authenticated differently (key, Entra agent identity, inline `Authorization`), driven by the shared `Using-Samples/SimpleAgent/` REPL.
See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
## Troubleshooting
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
so every following invoke fails even though the agent itself is healthy. Start a fresh one:
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
```
azd ai agent invoke --new-conversation "Hello!"
```
Add `--new-session` as well if the failure persists.
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,30 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: mcp-tools
displayName: "MCP Tools Agent"
description: >
A developer assistant demonstrating dual-layer MCP integration:
client-side GitHub MCP tools handled by the agent and server-side
Microsoft Learn MCP tools delegated to the LLM provider.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Agent Framework
- MCP
- Model Context Protocol
template:
name: mcp-tools
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -1,9 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: mcp-tools
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,40 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-mcp-tools
services:
ai-project:
host: azure.ai.project
hosted-mcp-tools:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedMcpTools.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
env:
ASPNETCORE_URLS: http://+:8088
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A hosted agent with dual-layer MCP tools (client-side McpClient and server-side HostedMcpServerTool) against the public Microsoft Learn MCP server.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Agent Framework
name: hosted-mcp-tools
protocols:
- protocol: responses
version: 2.0.0
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,8 +1,21 @@
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-ada-002
AZURE_AI_MEMORY_STORE_ID=hosted-memory-sample
AGENT_NAME=hosted-memory-agent
AZURE_BEARER_TOKEN=DefaultAzureCredential
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
# Memory store and embedding deployment (must already exist in your project).
AZURE_AI_MEMORY_STORE_ID=<your-memory-store-id>
AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=<your-embedding-deployment>
@@ -1,26 +0,0 @@
# Dockerfile for end-users consuming the Agent Framework via NuGet packages.
#
# This Dockerfile performs a full `dotnet restore` and `dotnet publish` inside the container,
# which only succeeds when the project references its dependencies via PackageReference (see the
# commented-out section in HostedMemoryAgent.csproj). Contributors building from the
# agent-framework repository source must use Dockerfile.contributor instead because
# ProjectReference dependencies live outside this folder and cannot be restored from inside
# this build context.
#
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedMemoryAgent.dll"]
@@ -1,22 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-memory-agent .
# docker run --rm -p 8088:8088 \
# -e AGENT_NAME=hosted-memory-agent \
# -e HOSTED_USER_ISOLATION_KEY=alice \
# --env-file .env hosted-memory-agent
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedMemoryAgent.dll"]
@@ -1,33 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedMemoryAgent</RootNamespace>
<AssemblyName>HostedMemoryAgent</AssemblyName>
<NoWarn>$(NoWarn);MEAI001;OPENAI001</NoWarn>
<UserSecretsId>7933e8df-ba8d-4e62-b419-6d34d53de308</UserSecretsId>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
<AgentFrameworkVersion>1.15.0-preview.260722.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReferences above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
</ItemGroup>
-->
</Project>
</Project>
@@ -14,10 +14,8 @@
// the FoundryMemoryProviderScope, partitioning memories per user.
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Agents.AI.Foundry.Hosting;
@@ -26,21 +24,28 @@ using Microsoft.Extensions.AI;
// Load .env file if present (for local development).
Env.TraversePath().Load();
var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-memory-agent";
var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
var embeddingDeployment = Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME") ?? "text-embedding-ada-002";
var memoryStoreName = Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID") ?? "hosted-memory-sample";
var agentName = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AGENT_NAME"),
"hosted-memory-agent")!;
var deployment = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
"gpt-4o")!;
var embeddingDeployment = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_EMBEDDING_DEPLOYMENT_NAME"),
"text-embedding-ada-002")!;
var memoryStoreName = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_MEMORY_STORE_ID"),
"hosted-memory-sample")!;
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in foundry).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
var credential = new DefaultAzureCredential();
AIProjectClient projectClient = new(projectEndpoint, credential);
@@ -87,9 +92,8 @@ builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
static string? FirstNonBlank(params string?[] candidates) =>
Array.Find(candidates, candidate => !string.IsNullOrWhiteSpace(candidate));
@@ -1,175 +1,225 @@
# Hosted-MemoryAgent
# Hosted-MemoryAgent
A hosted Foundry agent that uses **FoundryMemoryProvider** to remember user-private details across
requests and across sessions, scoped per end user via the Foundry platform's user identity. The
agent plays a friendly travel assistant: tell it about your trip, ask follow-up questions in a new
session, and it recalls what it learned about you.
A hosted agent with long-term memory backed by a Foundry Memory store and an embedding deployment. Requires AZURE_AI_MEMORY_STORE_ID and AZURE_AI_EMBEDDING_DEPLOYMENT_NAME to be set to resources that exist in your project.
This sample exists to demonstrate two things together:
1. How to host an agent that consumes a `Microsoft.Extensions.AI.AIContextProvider` (specifically
`FoundryMemoryProvider`) under the Foundry Responses hosting layer.
2. How the `HostedSessionContext` flows from the Foundry platform user-identity header
(`x-agent-user-id`) through the `HostedSessionIsolationKeyProvider` into the provider's
`stateInitializer`, so memories are partitioned per user automatically.
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A Foundry project with at least one chat model deployment and one embedding model deployment
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
- Permission to assign **Foundry User** on the Foundry project. The agent identity is created by
the first deploy, so this role is granted after `azd deploy`.
## Files
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: wires a Foundry memory provider (memory store + embedding deployment) into the agent, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedMemoryAgent.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your values:
Copy the template and fill in your project endpoint:
PowerShell:
```powershell
copy .env.example .env
```
Bash:
```bash
cp .env.example .env
```
Required:
```env
FOUNDRY_PROJECT_ENDPOINT=https://<account>.services.ai.azure.com/api/projects/<project>
FOUNDRY_MODEL=gpt-4o
AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=text-embedding-ada-002
AZURE_AI_MEMORY_STORE_ID=hosted-memory-sample
AGENT_NAME=hosted-memory-agent
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
AZURE_AI_MEMORY_STORE_ID=<your-memory-store-name>
AZURE_AI_EMBEDDING_DEPLOYMENT_NAME=<your-embedding-deployment>
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
AZURE_TOKEN_CREDENTIALS=dev
```
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
## How memory scoping works
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
| Layer | Source of the user identity |
|---|---|
| Inbound request | The Foundry platform sets the `x-agent-user-id` header on every request. |
| Hosting layer | `AgentFrameworkResponseHandler` resolves a `HostedSessionIsolationKeyProvider` from DI and calls `GetKeysAsync(context, request, ct)`. The default implementation reads `context.PlatformContext.UserIdKey`. |
| Session | The handler stores the resolved value on the session as a `HostedSessionContext` on the first request, and validates it on every subsequent request that resumes the same conversation (mismatch returns 403). |
| Memory provider | The sample's `stateInitializer` reads `session.GetHostedContext().UserId` and uses it as the `FoundryMemoryProviderScope`. Memories are partitioned per user. |
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character` when a mark is present.
This sample scopes memory per user via `HostedFoundryMemoryProviderScopes.PerUser()`, which requires a
resolved user identity — a request with none throws. So locally you **must** send an `x-agent-user-id`
request header (vary it to simulate distinct users); the default `HostedSessionIsolationKeyProvider`
reads it exactly as it reads the platform-injected value. On the Foundry platform the header is always
present, so no local provider registration is needed.
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no
> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS,
> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev`
> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe.
> Only for local runs; the deployed agent uses the platform-injected managed identity.
## Running directly (contributors)
## Run and test locally
This project uses `ProjectReference` to build against the local Agent Framework source.
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it,
see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
```bash
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent
az login
dotnet run
```
The agent starts on `http://localhost:8088`.
### Test it
**Terminal 2 — chat with it (code-first REPL):**
Per-user memories require an identity. Send an `x-agent-user-id` header to scope the call to a user
(locally you set it yourself; on the platform it is set for you):
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-H "x-agent-user-id: alice" \
-d '{"input": "Hi! My name is Taylor and I am planning a hiking trip to Patagonia in November.", "model": "hosted-memory-agent"}'
```
Wait a few seconds for memory extraction, then ask a follow-up using the response id from the
previous call as `previous_response_id`:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-H "x-agent-user-id: alice" \
-d '{"input": "What do you already know about my upcoming trip?", "previous_response_id": "<id>", "model": "hosted-memory-agent"}'
```
## Running with Docker
Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies
outside this folder. Use `Dockerfile.contributor` which takes a pre-published output.
### 1. Publish for the container runtime (Linux Alpine)
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
```
### 2. Build the Docker image
```bash
docker build -f Dockerfile.contributor -t hosted-memory-agent .
```
### 3. Run the container
```bash
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-memory-agent \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-memory-agent
```
### 4. Smoke test the running container
A scripted smoke test that exercises memory recall and per-user isolation is provided at
`scripts/smoke.ps1`. From the sample folder:
PowerShell:
```powershell
pwsh ./scripts/smoke.ps1
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-memory-agent"
dotnet run -- --local
```
The script publishes the project, builds the image, runs a **single** container, and drives two users
(alice, bob) against it by varying the `x-agent-user-id` request header. It asserts that each user
only sees their own memories, and exits non-zero on failure.
## Deploying to Foundry (azd spec)
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
Initialize an `azd` project from this sample's manifest:
Bash:
```bash
mkdir hosted-memory-agent && cd hosted-memory-agent
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/agent.manifest.yaml
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-memory-agent"
dotnet run -- --local
```
Then deploy:
Try: `Remember that my favorite color is teal.`
```bash
## Deploy to Foundry (source / ZIP)
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
### Step 1: create the working directory and enter it
PowerShell:
```powershell
$work = Join-Path $env:TEMP "hosted-memory-agent-work"
mkdir $work
cd $work
```
### Step 2: scaffold the project
`azd ai agent init` copies the sample into a subfolder named `hosted-memory-agent` (the top-level `name:`
in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts
you to pick the Foundry project; `-d` is the name of an existing model deployment in that project.
PowerShell:
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-MemoryAgent/azure.yaml"
azd auth login
azd ai agent init -m $sample -d <model-deployment>
```
### Step 3: provision and deploy
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
```
cd hosted-memory-agent
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd env set AZURE_AI_MEMORY_STORE_ID <your-memory-store-name>
azd env set AZURE_AI_EMBEDDING_DEPLOYMENT_NAME <your-embedding-deployment>
azd provision
azd deploy
# Grant the new hosted-agent identity project data-plane access:
az rest --method get --url "<project-endpoint>/agents/hosted-memory-agent" --url-parameters api-version=v1 --resource https://ai.azure.com --query "versions.latest.instance_identity.principal_id" --output tsv
az role assignment create --assignee-object-id <principal-id-from-previous-command> --assignee-principal-type ServicePrincipal --role 53ca6127-db72-4b80-b1b0-d745d6d5456d --scope <project-resource-id>
# Wait briefly for the role assignment to propagate, then invoke:
azd ai agent invoke "Remember that my favorite color is teal."
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
`53ca6127-db72-4b80-b1b0-d745d6d5456d` is the stable role definition ID for **Foundry User**.
This role lets the hosted-agent identity create or access the project memory store. Do not use
`Azure AI Developer`; Microsoft documents that role as insufficient for Foundry hosted agents.
Recreate the assignment when the agent is deleted and created again.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-memory-agent" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-memory-agent
```
Bash:
```bash
azd env set AGENT_NAME hosted-memory-agent
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-memory-agent
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
---
## Troubleshooting
## NuGet package users
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
If you are consuming the Agent Framework as a NuGet package (not building from source), use the
standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in
`HostedMemoryAgent.csproj` for the `PackageReference` alternative.
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
## How it differs from sibling samples
```
azd ai agent invoke --new-conversation "Hello!"
```
| | Hosted-ChatClientAgent | Hosted-MemoryAgent |
|---|---|---|
| **Agent definition** | Inline (`AsAIAgent(model, instructions)`) | Inline, plus `AIContextProviders = [memoryProvider]` |
| **State** | None beyond the conversation history | Per-user memories persisted in Foundry Memory |
| **Identity** | Not used | Required: `HostedSessionContext.UserId` flows into the memory scope |
| **Local dev** | Works with no identity header (per-user isolation not triggered) | Requires an `x-agent-user-id` header (memory is per-user); vary it to simulate distinct users |
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
@@ -1,31 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
name: hosted-memory-agent
displayName: "Hosted Memory Agent"
description: >
A travel-assistant hosted agent that uses FoundryMemoryProvider to remember user-private
preferences and details across sessions. Memory is scoped per end user via the Foundry
platform's isolation key headers.
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Responses Protocol
- Streaming
- Agent Framework
- Memory
- Foundry Memory
template:
name: hosted-memory-agent
kind: hosted
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
parameters:
properties: []
resources: []
@@ -1,9 +0,0 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
kind: hosted
name: hosted-memory-agent
protocols:
- protocol: responses
version: 2.0.0
resources:
cpu: "0.25"
memory: 0.5Gi
@@ -0,0 +1,42 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: hosted-memory-agent
services:
ai-project:
host: azure.ai.project
hosted-memory-agent:
project: .
host: azure.ai.agent
language: csharp
uses:
- ai-project
codeConfiguration:
dependencyResolution: remote_build
entryPoint: HostedMemoryAgent.dll
runtime: dotnet_10
# ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
# app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
# readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
# Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
# over this value, so it only matters when the project is pinned to an older package.
env:
ASPNETCORE_URLS: http://+:8088
AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
AZURE_AI_MEMORY_STORE_ID: ${AZURE_AI_MEMORY_STORE_ID}
AZURE_AI_EMBEDDING_DEPLOYMENT_NAME: ${AZURE_AI_EMBEDDING_DEPLOYMENT_NAME}
container:
resources:
cpu: "0.5"
memory: 1Gi
description: |
A hosted agent with long-term memory backed by a Foundry Memory store and an embedding deployment.
kind: hosted
metadata:
tags:
- AI Agent Hosting
- Azure AI AgentServer
- Agent Framework
name: hosted-memory-agent
protocols:
- protocol: responses
version: 2.0.0
@@ -0,0 +1,30 @@
# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
# Note: only the root .agentignore is read; subdirectory files are not supported.
#
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
azure.yaml
.agentignore
# Security / secrets
.env
.env.*
.azure/
.git/
# .NET build output
bin/
obj/
*.user
*.suo
.vs/
# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
# runtime writes its own under the container's home directory, so uploading the local copy
# would ship stale sessions with the agent.
.checkpoints/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
# Framework from the packages shipped in this upload instead of nuget.org.
@@ -1,6 +0,0 @@
.env
bin/
obj/
.vs/
.vscode/
*.user
@@ -1,12 +1,17 @@
# Foundry project endpoint (shape: https://<host>/api/projects/<project>)
FOUNDRY_PROJECT_ENDPOINT=<your-azure-ai-project-endpoint>
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
# Local development only. Bind the app to the port Foundry probes for readiness, which is the
# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
# it themselves, so this only matters while the project is pinned to an older published package.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
AZURE_BEARER_TOKEN=DefaultAzureCredential
# Capture prompt / completion / tool argument content on GenAI spans.
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
# Uncomment and set to send local-run telemetry to Application Insights.
# When the agent runs inside Foundry this value is injected automatically.
#APPLICATIONINSIGHTS_CONNECTION_STRING=<your-app-insights-connection-string>
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
AZURE_TOKEN_CREDENTIALS=dev
@@ -1,17 +0,0 @@
# Use the official .NET 10.0 ASP.NET runtime as a parent image
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
# Final stage
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedObservability.dll"]
@@ -1,19 +0,0 @@
# Dockerfile for contributors building from the agent-framework repository source.
#
# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
# which means a standard multi-stage Docker build cannot resolve dependencies outside
# this folder. Instead, pre-publish the app targeting the container runtime and copy
# the output into the container:
#
# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
# docker build -f Dockerfile.contributor -t hosted-observability .
# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-observability -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-observability
#
# For end-users consuming the NuGet package (not ProjectReference), use the standard
# Dockerfile which performs a full dotnet restore + publish inside the container.
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
WORKDIR /app
COPY out/ .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
ENTRYPOINT ["dotnet", "HostedObservability.dll"]
@@ -1,33 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project>
<!--
Source (ZIP) deploy sample. The code-deploy upload is a flat folder with no repo-level props,
so this project is intentionally self-contained: a single target framework and explicit package
versions. Foundry runs `dotnet restore` + `dotnet publish` on it during provisioning
(dependencyResolution: remote_build in azure.yaml).
ImportDirectoryPackagesProps has to be set before the SDK props are imported, hence the explicit
Sdk imports below instead of the usual Sdk attribute on the Project element. It stops MSBuild
from walking up to the repository's dotnet/Directory.Packages.props, which would turn on central
package management and inject analyzer PackageReference items, neither of which exists inside the
ZIP, so without this the in-repo build would resolve differently from the server-side build.
-->
<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<ImportDirectoryPackagesProps>false</ImportDirectoryPackagesProps>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk.Web" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFrameworks></TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedObservability</RootNamespace>
<AssemblyName>HostedObservability</AssemblyName>
<NoWarn>$(NoWarn);</NoWarn>
<UserSecretsId>f401dcb9-9636-4c3d-ab4a-4fb68773a1a1</UserSecretsId>
<AgentFrameworkVersion>1.15.0-preview.260722.1</AgentFrameworkVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="$(AgentFrameworkVersion)" />
<PackageReference Include="Azure.AI.Projects" Version="2.1.0-beta.4" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" />
</ItemGroup>
<!-- For contributors: uses ProjectReference to build against local source -->
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\..\src\Microsoft.Agents.AI.Foundry.Hosting\Microsoft.Agents.AI.Foundry.Hosting.csproj" />
<ProjectReference Include="..\Hosted_Shared_Contributor_Setup\Hosted_Shared_Contributor_Setup.csproj" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk.Web" />
<!-- For end-users: uncomment the PackageReference below and remove the ProjectReference above
<ItemGroup>
<PackageReference Include="Microsoft.Agents.AI.Foundry" Version="1.6.1-preview.260514.1" />
<PackageReference Include="Microsoft.Agents.AI.Foundry.Hosting" Version="1.6.1-preview.260514.1" />
</ItemGroup>
-->
</Project>
</Project>
@@ -3,33 +3,34 @@
// Hosted Observability Agent - demonstrates that the Foundry hosting pipeline
// emits OpenTelemetry traces, metrics and logs with no extra wiring required.
// Two small tools are included so a request produces a span tree covering
// agent invocation, the chat call, and tool execution.
// agent invocation, the chat call, and tool execution. It is deployed to Foundry
// directly from source (code / ZIP upload), so the platform builds and runs your
// code with no container image.
using System.ComponentModel;
using Azure.AI.Projects;
using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
// Load a local .env file when present (local development only). In Foundry the
// platform injects the required environment variables at runtime.
Env.TraversePath().Load();
string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
var endpoint = System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
string deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
// Use a chained credential: try a temporary dev token first (for local Docker debugging),
// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity in production).
TokenCredential credential = new ChainedTokenCredential(
new DevTemporaryTokenCredential(),
new DefaultAzureCredential());
// Environment variables can arrive set but blank: azd substitutes an empty string when the azd
// environment does not define the variable referenced from azure.yaml. An empty string is not
// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK.
var deploymentName = FirstNonBlank(
System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
"gpt-4o");
var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-observability";
// ── Tools ────────────────────────────────────────────────────────────────────
@@ -51,26 +52,30 @@ string GetWeather(
// and the OTLP exporter is registered by Azure.AI.AgentServer.Core's
// AddAgentHostTelemetry(). No additional observability wiring is required.
AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
// WARNING: DefaultAzureCredential is convenient for development but requires careful
// consideration in production. Consider a specific credential (for example
// ManagedIdentityCredential) to avoid latency, unintended credential probing, and
// fallback security risks.
AIAgent agent = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential())
.AsAIAgent(
model: deploymentName,
instructions: "You are a friendly assistant. Keep your answers brief.",
name: Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-observability",
name: agentName,
description: "A hosted agent that demonstrates Foundry observability.",
tools: [
AIFunctionFactory.Create(GetCurrentLocation),
AIFunctionFactory.Create(GetWeather),
]);
// Host the agent using the Responses protocol.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
app.MapDevTemporaryLocalAgentEndpoint();
app.Run();
// Returns the first candidate that has an actual value, ignoring null and blank entries.
static string FirstNonBlank(params string?[] candidates) =>
Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!;
@@ -1,151 +1,208 @@
# Hosted-Observability
# Hosted-Observability
A hosted [Agent Framework](https://github.com/microsoft/agent-framework) agent that demonstrates how the Foundry hosting pipeline emits OpenTelemetry traces, metrics and logs with no extra wiring.
A hosted agent that demonstrates the Foundry hosting pipeline emits OpenTelemetry traces, metrics and logs with no extra wiring. Two small tools are included so a request produces a span tree covering agent invocation, the chat call, and tool execution.
The agent has two small tools, `GetCurrentLocation` and `GetWeather`, so an end-to-end run produces a span tree covering agent invocation, the underlying chat call, and tool execution.
## How it works
### Instrumentation is on by default
Unlike the Python SDK, the .NET hosting library is instrumented by default. `AddFoundryResponses(agent)` automatically wraps the agent with `OpenTelemetryAgent` (see `Microsoft.Agents.AI.Foundry.Hosting.ServiceCollectionExtensions.ApplyOpenTelemetry`) and the OTLP exporter pipeline is registered by `Azure.AI.AgentServer.Core`'s `AddAgentHostTelemetry()`. There is no `ENABLE_INSTRUMENTATION` flag to set.
### Sensitive content
Prompt, completion and tool argument content are omitted from spans by default. Set the OpenTelemetry standard environment variable to capture them:
```env
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
```
This is the .NET equivalent of the Python sample's `ENABLE_SENSITIVE_DATA`. It is read by `OpenTelemetryAgent.EnableSensitiveData`.
### Where the telemetry goes
Foundry injects `APPLICATIONINSIGHTS_CONNECTION_STRING` when the agent runs in the hosted environment, so traces, metrics and logs flow to Application Insights with no code change. To send telemetry from a local run, set the connection string yourself in `.env`.
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage. Source deploy is the default for .NET.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A Foundry project with a deployed model (e.g., `gpt-4o`)
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
## Files
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: defines two tools, hosts it with the Responses protocol; telemetry is emitted automatically by the hosting pipeline. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through env. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedObservability.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your project endpoint:
PowerShell:
```powershell
copy .env.example .env
```
Bash:
```bash
cp .env.example .env
```
Edit `.env` and set your Foundry project endpoint:
```env
FOUNDRY_PROJECT_ENDPOINT=https://<your-account>.services.ai.azure.com/api/projects/<your-project>
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
FOUNDRY_MODEL=gpt-4o
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
AZURE_TOKEN_CREDENTIALS=dev
```
> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
## Running directly (contributors)
> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
> while this project is pinned to an older published package.
```bash
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character` when a mark is present.
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential`. On a developer machine with no
> managed identity, `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS,
> `169.254.169.254`) and blocks for a long time before every model call. `AZURE_TOKEN_CREDENTIALS=dev`
> restricts it to developer credentials (Azure CLI, Visual Studio, `azd`) and skips that probe.
> Only for local runs; the deployed agent uses the platform-injected managed identity.
## Run and test locally
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it,
see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
**Terminal 1 — host the agent:**
```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability
AGENT_NAME=hosted-observability dotnet run
az login
dotnet run
```
The agent starts on `http://localhost:8088`.
### Test it
**Terminal 2 — chat with it (code-first REPL):**
```bash
azd ai agent invoke --local "What is the current weather where I am?"
PowerShell:
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-observability"
dotnet run -- --local
```
Or with curl:
Bash:
```bash
curl -X POST http://localhost:8088/responses \
-H "Content-Type: application/json" \
-d '{"input": "What is the current weather where I am?", "model": "hosted-observability"}'
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-observability"
dotnet run -- --local
```
## Expected span tree
Try: `What is the weather where I am?`
A single request produces approximately the following spans:
## Deploy to Foundry (source / ZIP)
| Span | Source |
|------|--------|
| `invoke_agent` | Outer span emitted by the Azure AI AgentServer hosting SDK |
| `agent_invoke <name>` | Emitted by `OpenTelemetryAgent` for each agent invocation |
| `chat <model>` | Emitted by the underlying `IChatClient` for each model call |
| `execute_tool <tool>` | Emitted for each invocation of `GetCurrentLocation` / `GetWeather` |
`azd` scaffolds the project into a working folder, so every step below runs from an **empty
directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
See the [OpenTelemetry GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for the attributes captured on each span.
### Step 1: create the working directory and enter it
## Running with Docker
PowerShell:
This project uses `ProjectReference` to the local Agent Framework source, so use `Dockerfile.contributor` with a pre-published output:
```bash
dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
docker build -f Dockerfile.contributor -t hosted-observability .
export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
docker run --rm -p 8088:8088 \
-e AGENT_NAME=hosted-observability \
-e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
--env-file .env \
hosted-observability
```powershell
$work = Join-Path $env:TEMP "hosted-observability-work"
mkdir $work
cd $work
```
## Deploying to Foundry and viewing traces
### Step 2: scaffold the project
Once deployed, telemetry flows to the Application Insights instance attached to your Foundry project. In the Foundry UI, the **Traces** tab next to **Playground** lists conversations and lets you drill into the span tree for any request.
`azd ai agent init` copies the sample into a subfolder named `hosted-observability` (the top-level `name:`
in `azure.yaml`) and writes the adopted `azure.yaml` and the `azd` environment there. It prompts
you to pick the Foundry project; `-d` is the name of an existing model deployment in that project.
## Deploying to Foundry (azd spec)
PowerShell:
This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
```powershell
$sample = "<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/azure.yaml"
Initialize an `azd` project from this sample's manifest:
```bash
mkdir hosted-observability && cd hosted-observability
azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/agent.manifest.yaml
azd auth login
azd ai agent init -m $sample -d <model-deployment>
```
Then deploy:
### Step 3: provision and deploy
```bash
Contributors changing the Agent Framework source: do the extra step in
[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
before the commands below. Everyone else can ignore it.
```
cd hosted-observability
azd env get-values
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME <model-deployment>
azd provision
azd deploy
azd ai agent invoke "What is the weather where I am?"
```
If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
### Step 4: clean up
```
azd down
```
> **`azd down` does not delete the hosted agent.** It reports success but leaves the deployed agent
> in place. Delete it explicitly with a REST call:
>
> ```bash
> az rest --method delete \
> --url "<project-endpoint>/agents/hosted-observability" \
> --url-parameters api-version=v1 force=true \
> --resource https://ai.azure.com
> ```
Then delete the working directory.
## Deploy your local framework changes (contributors)
**Skip this section unless you are changing the Agent Framework itself.** The project restores the
**published** Agent Framework packages, and Foundry restores from nuget.org when it builds the
upload, so editing framework source in this repository changes nothing about the deployed agent.
The helper script packs your local framework source into NuGet packages and puts them **inside the
upload**, together with a `nuget.config` that points the restore at them. Run it in the flow above,
**between step 2 and step 3**:
PowerShell:
```powershell
cd $work
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-observability
```
Bash:
```bash
azd env set AGENT_NAME hosted-observability
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
cd "$WORK"
<repo>/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-observability
```
For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
---
## NuGet package users
If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedObservability.csproj` for the `PackageReference` alternative.
See the
[`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors)
README for the full explanation of what the script changes and why.
## Troubleshooting
**`azd ai agent invoke` fails with `404 not_found: Conversation '<id>' not found`**
`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
so every following invoke fails even though the agent itself is healthy. Start a fresh one:
`azd` reuses the saved session and conversation per agent. Once the agent is redeployed or deleted,
that conversation no longer exists on the server. Start a fresh one:
```
azd ai agent invoke --new-conversation "Hello!"
```
Add `--new-session` as well if the failure persists.
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).

Some files were not shown because too many files have changed in this diff Show More