| name | foundry-hosted-agents |
| description | Deploy + manage Foundry hosted agents — container deploy via unified azure.yaml is GA; source-code --deploy-mode code is still preview. MAF 1.8.0, azd ext install microsoft.foundry, implicit agent access (no default role grant). Read the body for patterns, identity, runtime, rollout, troubleshooting. USE FOR: deploy foundry agent, hosted agent, container agent, azure.yaml, azd ai agent, microsoft.foundry, MAF, FoundryChatClient, ResponsesHostServer, ACR push, batch eval, agent identity, Foundry User role, entra-agent-id, Responses/Invocations protocols, Activity protocol, blue-green deploy, canary rollout, version rollback, traffic routing, version_selector, agent_endpoint, update_details. DO NOT USE FOR: prompt agents (use foundry-prompt-agents), ACA MCP (use foundry-mcp-aca), GHCP coding agent (use ghcp-hosted-agents), Citadel hub/spoke (use citadel-hub-deploy), pilot pipeline (use threadlight-deploy), continuous eval (use foundry-evals), Routines (use foundry-routines), A2A wiring (use foundry-toolbox).
|
| metadata | {"version":"2.1.0"} |
Microsoft Foundry Hosted Agents — Reference Guide
Production-tested patterns for deploying hosted agents on Microsoft Foundry.
Container-based deployment (Dockerfile + unified azure.yaml + azd) is
GA. Source-code (--deploy-mode code) deployment is a separate, still-preview
surface — see § Preview appendix: source-code deploy
for that path in isolation. Covers the Agent + FoundryChatClient +
ResponsesHostServer (MAF) variant exclusively.
⚠️ MAF 1.8.0 recommended — caller-side FoundryAgent(timeout=...) knob.
Upgrade from 1.6.0 → 1.8.0 to pick up the new HTTP timeout kwarg on the
caller-side agent_framework.foundry.FoundryAgent class (overrides the
OpenAI-SDK 5s connect / 600s total defaults). All MAF 1.6.0 telemetry
bundling (microsoft-opentelemetry + opentelemetry-instrumentation-openai-v2)
is unchanged. The two MAF 1.8.0 [BREAKING] markers (github-copilot
sub-package internal rename + experimental Skill ABC refactor) are
non-impact for hosted-agent code — see § MAF 1.8.0 update.
If you are still on 1.3.x, upgrade directly to 1.8.0 — absorb the 1.4.0
breaking changes below AND the 1.6.0 telemetry improvements AND the
1.8.0 timeout knob in one pass.
When to Use
- Deploying a custom container agent to Foundry (GA path)
- Debugging hosted agent failures (401, 500, import errors)
- Understanding agent identity + when (rarely) explicit RBAC is needed
- Authoring the unified
azure.yaml (azure.ai.project + azure.ai.agent services)
- Blue-green / canary / rollback traffic routing across agent versions
- Migrating from MAF 1.3.x → 1.4.0 (§ below)
- Migrating from the legacy two-file
agent.yaml + agent.manifest.yaml
contract — both are gone; a single azure.yaml is now the source of truth
(§ azure.yaml)
Choosing a model? See references/model-selection.md for the model / region / capacity / data-residency decision before you azd provision.
MAF 1.4.0 breaking changes (May 2026)
If upgrading from 1.3.x today, skip to 1.6.0 directly. The version
pins below show 1.4.0 for historical accuracy; use the 1.6.0 pins from
§ MAF 1.6.0 update instead — they absorb
all 1.4.0 breaking changes AND add gen_ai telemetry.
Azure renamed the Foundry data-plane role from "Azure AI User" to
"Foundry User" and changed the AAD token audience the SDK requests
from https://cognitiveservices.azure.com/.default to
https://ai.azure.com/.default. agent-framework-core 1.4.0
(2026-05-14, alongside agent-framework-foundry-hosting 1.0.0a260514)
is the first SDK that requests the new scope; everything pinned to 1.3.x
or earlier requests the old scope and gets 401 Unauthorized from
the post-rename data plane.
The three changes you have to absorb
| # | Change | Symptom on pinned 1.3.x | Fix |
|---|
| 1 | AAD token scope: cognitiveservices.azure.com → ai.azure.com | Every Responses request → 401 Unauthorized (with valid Foundry User RBAC) | Upgrade to agent-framework-core~=1.4.0 + agent-framework-foundry~=1.4.0 + agent-framework-foundry-hosting==1.0.0a260514 |
| 2 | Role display name "Azure AI User" → "Foundry User" | az role assignment create --role "Azure AI User" fails with RoleDefinitionNotFound | Use --role "Foundry User" OR pin by GUID 53ca6127-db72-4b80-b1b0-d745d6d5456d (unchanged across the rename — the safest call-site form is the GUID) |
| 3 | AzureOpenAIChatClient removed from agent_framework.azure | ImportError: cannot import name 'AzureOpenAIChatClient' after pip install -U | Use OpenAIChatClient(azure_endpoint=..., model=..., credential=...) from agent_framework.openai — see snippet below |
AzureOpenAIChatClient → OpenAIChatClient migration
FoundryChatClient is the right choice for hosted Foundry agents and is
unaffected. The removal hits adjacent code paths that talked directly
to Azure OpenAI (eval judges, batch scoring, sidecar services, agents
that route through APIM):
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import get_bearer_token_provider, DefaultAzureCredential
client = AzureOpenAIChatClient(
endpoint=AZURE_OPENAI_ENDPOINT,
deployment_name=DEPLOYMENT,
ad_token_provider=get_bearer_token_provider(
DefaultAzureCredential(),
"https://cognitiveservices.azure.com/.default",
),
)
from agent_framework.openai import OpenAIChatClient
from azure.identity import DefaultAzureCredential
client = OpenAIChatClient(
azure_endpoint=AZURE_OPENAI_ENDPOINT,
model=DEPLOYMENT,
credential=DefaultAzureCredential(),
)
Drop the explicit get_bearer_token_provider / ad_token_provider —
OpenAIChatClient now derives the scope itself. The unused
get_bearer_token_provider import can be removed.
Image-tag staleness trap (mandatory read for any pilot that pins by digest)
Hosted agent versions reference the orchestrator container by ACR
image reference. There are two common shapes:
<acr>.azurecr.io/tl-maf-orchestrator:latest — re-resolves to the
newest pushed image on every container start. Auto-picks up MAF
rebuilds the next time the agent provisions.
<acr>.azurecr.io/tl-maf-orchestrator@sha256:abc… — pinned to a
specific layer digest. Reproducible, but frozen at the MAF version
that was in the image when the digest was computed.
After a MAF 1.4.0 rebuild, agents using :latest work; agents using
@sha256:… digests from the 1.3.x era keep hitting the old token scope
and fail. Re-import every hosted agent version (or pin by digest to
the freshly-built 1.4.0 image) as part of the upgrade. azd ai agent deploy does this automatically if the YAML references :latest.
Rebuild recipe (copy-paste)
sed -i.bak 's/"agent-framework-core[^"]*"/"agent-framework-core~=1.4.0"/' src/orchestrator/pyproject.toml
sed -i.bak 's/"agent-framework-foundry[^"]*"/"agent-framework-foundry~=1.4.0"/' src/orchestrator/pyproject.toml
sed -i.bak 's/"agent-framework-foundry-hosting[^"]*"/"agent-framework-foundry-hosting==1.0.0a260514"/' src/orchestrator/pyproject.toml
(cd src/orchestrator && uv lock)
ACR=tl<your-acr-suffix>
az acr build --registry "$ACR" \
--image "tl-maf-orchestrator:maf14-$(date +%Y%m%d%H%M)" \
--image "tl-maf-orchestrator:latest" \
--file src/orchestrator/dockerfile src/orchestrator/
azd deploy agents
(cd infra/scripts && uv run deploy_job.py)
Why date-pinned tags matter. :latest is fine for the agent
reference but leaves zero forensic trail in ACR's image history.
The maf14-YYYYMMDDHHMM tag lets you correlate "which agent
version is running which MAF build?" months later when a regression
bisect needs it.
MAF 1.6.0 update (May 2026)
agent-framework-core 1.6.0 and agent-framework-foundry-hosting
1.0.0a260521 ship with instrumentation enabled by default. The
hosting package transitively pulls azure-ai-agentserver-core>=2.0.0b3
which depends on microsoft-opentelemetry>=1.0.0 (resolves to 1.1.0),
bundling all OTel instrumentors:
| Bundled instrumentor | What it captures |
|---|
opentelemetry-instrumentation-openai-v2==2.3b0 | gen_ai spans: model name, token usage, latency for every OpenAI/Responses call |
opentelemetry-instrumentation-openai-agents-v2==0.1.0 | Agent-level invocation spans |
opentelemetry-instrumentation-httpx | HTTP dependency spans |
azure-core-tracing-opentelemetry | Azure SDK call tracing |
What this means for container.py
Remove all manual OTel code. No configure_azure_monitor(), no
OpenAIInstrumentor().instrument(), no custom TracerProvider. The
platform handles everything. Your container code only needs:
cs = os.getenv("APPLICATION_INSIGHTS_CONNECTION_STRING") or \
os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
if cs:
os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"] = cs
try:
await client.configure_azure_monitor(enable_sensitive_data=True)
except Exception:
pass
gen_ai spans appear under cloud_RoleName = "agent_framework"
Important for KQL queries: gen_ai dependency spans use
cloud_RoleName = "agent_framework", NOT agent-{jobId}-maf.
Queries filtering cloud_RoleName startswith "agent-" will miss them.
// gen_ai spans — model, tokens, latency
dependencies
| where timestamp > ago(1h)
| where cloud_RoleName == "agent_framework"
| project timestamp, name, duration,
model=tostring(customDimensions['gen_ai.response.model']),
input_tokens=tostring(customDimensions['gen_ai.usage.input_tokens']),
output_tokens=tostring(customDimensions['gen_ai.usage.output_tokens']),
op=tostring(customDimensions['gen_ai.operation.name'])
| order by timestamp desc
Upgrade recipe (→ 1.7.0)
sed -i.bak 's/"agent-framework-core[^"]*"/"agent-framework-core~=1.7.0"/' pyproject.toml
sed -i.bak 's/"agent-framework-foundry[^"]*"/"agent-framework-foundry~=1.7.0"/' pyproject.toml
sed -i.bak 's/"agent-framework-foundry-hosting[^"]*"/"agent-framework-foundry-hosting==1.0.0a260528"/' pyproject.toml
create_version deduplication trap
Foundry deduplicates create_version when environment variables and
metadata are identical — even if the container image tag/digest
differs. This is SEPARATE from the ACR layer cache trap below. After
a base image rebuild, new code never reaches the container because
Foundry returns the existing version.
Fix: Add a changing environment variable to force a new version:
import time
env_vars["_BUILD_TS"] = str(int(time.time()))
ACR layer cache trap (per-job images built via DockerBuildRequest)
When per-job images are built via the Azure Container Registry
DockerBuildRequest API (e.g., from deploy.py), ACR Tasks uses
layer caching by default. If the per-job Dockerfile's COPY . ./
content hasn't changed (only the base image changed), ACR produces
the identical digest — and Foundry deduplicates the
create_version call, silently returning the existing version.
New base-image code never reaches the container.
Fix (both are required):
no_cache=True on DockerBuildRequest — forces ACR to
rebuild all layers from scratch.
- A used
ARG in the generated Dockerfile — Docker only
invalidates cache when an ARG changes AND is consumed:
ARG BASE_IMAGE=hosted-agent-base-maf:v6
FROM ${BASE_IMAGE}
ARG BUILD_TS=1716400000
RUN echo "build=$BUILD_TS" > /app/.build_ts
# ... rest of Dockerfile
Without the RUN line, Docker ignores the ARG for caching.
Discovered May 2026: base image rebuild with SkillsProvider.from_paths()
fix + observability fix never reached any agent until both guards were
applied.
MAF 1.8.0 update (June 2026)
agent-framework-core and agent-framework-foundry 1.8.x land one
hosted-agent-relevant new knob and two [BREAKING] markers — both
non-impact for this skill.
FoundryAgent(timeout=...) — new HTTP timeout kwarg on the
caller-side agent_framework.foundry.FoundryAgent class. Overrides
the underlying OpenAI-SDK default (connect: 5s, total: 600s). Useful
when orchestrator code calling INTO a hosted agent wants a bounded
ceiling. See § below.
MAF 1.8.0 breaking markers — non-impact analysis
MAF 1.8.0 ships two [BREAKING] markers; neither affects hosted-agent code:
agent-framework-github-copilot sub-package internal rename —
this skill does not pin or import agent-framework-github-copilot. N/A.
- Experimental
Skill abstract-class refactor in agent-framework-core —
this skill uses the high-level SkillsProvider.from_paths(...) facade
(see § Skill Loading below), not the experimental Skill ABC.
Hosted-agent containers continue to work unchanged. Non-impact.
FoundryAgent timeout parameter (MAF 1.8.0)
MAF 1.8.0 adds timeout: float | None = None to
agent_framework.foundry.FoundryAgent.__init__. This is an HTTP-layer
timeout that overrides the OpenAI SDK's default (connect: 5s, total: 600s)
for ALL requests made by this agent instance. Per the upstream docstring:
"HTTP timeout in seconds for requests. When not provided, the OpenAI SDK
default is used (connect: 5s, total: 600s)."
Use it to:
- Lower the ceiling for short-running agents that should fail fast
(e.g.
timeout=30 for tool-routing agents with user-facing chat budgets).
- Raise the ceiling for long-running agents where 600s isn't enough
(e.g.
timeout=1800 for multi-step research loops).
Scope note: this kwarg lives on the CALLER-side FoundryAgent class
(orchestrator code calling a hosted agent). The CONTAINER-side runtime
covered by § Runtime Pattern uses
FoundryChatClient directly and does NOT accept timeout= in 1.8.0 —
apply HTTP-layer transport timeouts on the underlying client instead.
MUST: Copy verbatim from
references/python/foundry_agent_timeout.py.
Do NOT redefine inline — the validator enforces single-source-of-truth.
See also (MAF 1.8.0, experimental):
AgentFileStore — new abstract base class for agent file-management
backends. Re-exported as from agent_framework import AgentFileStore.
Concrete implementations + usage patterns live in foundry-memory and
foundry-toolbox. NOT consumed by the hosted-agents runtime patterns
documented in this skill.
Upgrade recipe (→ 1.8.0)
sed -i.bak 's/"agent-framework-core[^"]*"/"agent-framework-core~=1.8.0"/' pyproject.toml
sed -i.bak 's/"agent-framework-foundry[^"]*"/"agent-framework-foundry~=1.8.0"/' pyproject.toml
sed -i.bak 's/"agent-framework-foundry-hosting[^"]*"/"agent-framework-foundry-hosting==1.0.0a260528"/' pyproject.toml
Note: the hosting package is pinned exact (==1.0.0a260528), not
compatible-release (~=). PEP 440 treats ~=1.0.0aN as
>=1.0.0aN, <1.1 — pip will happily drift to a later alpha
(a260609, a260612, …). Exact pin per AGENTS.md § 9.5 alpha
pre-release discipline.
Container GA status and adjacent preview surfaces
Status. Container-based hosted agent deploy (Dockerfile +
unified azure.yaml + azd) is GA — that is the stable path
documented throughout the rest of this skill. Container deploy was
still labelled preview as recently as the March 2026 announcement
blog; treat "GA" as current as of this skill's last_validated date
in references/upstream-pin.md, not as
a permanently-fixed fact — re-check the sources below if it's been a
while.
A few adjacent surfaces remain preview or partial-GA and are called
out explicitly wherever they appear in this skill, rather than
assumed GA by association:
- Source-code deploy (
--deploy-mode code, ZIP upload, no
Dockerfile) is preview — isolated in its own
§ Preview appendix
at the end of this file. Do not mix its guidance into the container
(GA) path documented above it.
- A2A protocol and Voice Live are each preview, independently
of the container/Responses GA surface — see
foundry-toolbox and
foundry-voice-live respectively for those contracts.
- Hosted tracing (distributed traces / gen_ai spans in App
Insights) is partial GA / preview depending on the exact signal —
see
foundry-observability for the current split.
- Agent guardrails (content-safety toggle), Optimizer,
Routines, and Memory all remain preview surfaces layered on
top of the GA hosted-agent runtime — see their owning skills
(
foundry-routines, foundry-memory) for details; this skill does
not restate their contracts.
Sources:
container deploy,
source-code deploy — preview,
author azure.yaml,
azure.yaml reference,
hosted agent permissions,
install azd Foundry extensions.
Built-in content-safety toggle (preview)
Content safety is a Foundry portal toggle under the Guardrails
blade of the agent resource — not an SDK call, not an azure.yaml
field. Toggle on to enable platform-side prompt/response filtering with
the project's default content-safety policy. Toggle off if you bring
your own filtering layer upstream (e.g., APIM policy fragment, in-agent
middleware). Guardrails remain a preview capability layered on the
GA hosted-agent runtime.
The toggle is per-agent, per-environment. Production environments
should turn it ON by default; dev environments may turn it off to
test prompt patterns that would otherwise be blocked.
Responses, Invocations, Invocations (WebSocket), and Activity
Hosted agent containers can expose one or more protocols side by side.
Per Microsoft Learn: "The Responses, Invocations, and Invocations
(WebSocket) protocols are available in all regions that support Hosted
agents." — none of the three is region-restricted.
| Protocol | Python library | Use for |
|---|
responses | azure-ai-agentserver-responses | Conversational chatbots, streaming, multi-turn with platform-managed history. This skill's primary pattern. |
invocations | azure-ai-agentserver-invocations | Webhook receivers, non-conversational processing, custom async workflows. |
invocations_ws | azure-ai-agentserver-invocations (same package, WebSocket route) | Bidirectional streaming — real-time voice agents, interactive media. Documented here; not exercised by this skill's fixture, which only drives Responses. |
| Activity (auto-bridge) | n/a — platform-managed | Foundry automatically bridges Responses to the Bot Framework Activity protocol for Teams / Microsoft 365 Copilot publishing. No code change on the agent side. Publishing setup itself is out of scope for this skill — see foundry-teams-bot / the Teams publishing backlog for that contract. |
Declare the protocols a container serves in the protocols list of the
azure.ai.agent service in azure.yaml (§ below) — a single container
can serve multiple protocols simultaneously by importing the matching
libraries.
Unified azure.yaml (replaces the old two-file contract)
The old two-file contract (agent.yaml literal values +
agent.manifest.yaml mustache scaffold) is gone. As of the current
Foundry azd extensions, all hosted-agent configuration lives in a
single azure.yaml — see
§ azure.yaml (unified hosted-agent configuration)
below for the canonical shape. If you're migrating an older project,
delete agent.yaml and agent.manifest.yaml and move their content
into the azure.ai.agent service block.
Hosting SDK matrix (Python)
| Framework | Hosting package | Server class |
|---|
| MAF | agent-framework-foundry-hosting | agent_framework_foundry_hosting.ResponsesHostServer |
| BYO Responses | azure-ai-agentserver-responses | azure.ai.agentserver.responses.ResponsesHostServer |
| BYO Invocations (WS) | azure-ai-agentserver-invocations | azure.ai.agentserver.invocations.InvocationsHostServer |
| LangGraph | langchain-azure-ai[hosting] | langchain_azure_ai.agents.hosting.ResponsesHostServer |
Pin to the MAF row unless you have a documented reason to use one of
the BYO packages (custom protocol handler, non-MAF framework lock-in,
LangGraph state-graph requirements).
Runtime Pattern (MAF Variant)
Model selection (verified across recent pilots). Default
to gpt-5.4 for production agents that run instruction chains
with 10+ tool steps (case investigation, multi-source RAG synthesis,
regulatory drafting). Use gpt-5.4-mini only for trivial
1-2-step flows (single-tool lookups, formatters). The mini variant's
tool-call discipline degrades sharply on long chains: in recent
strict-smoke reproducibility on a 7-skill flow went from 1/3 with
gpt-5.4-mini to 3/3 with gpt-5.4 (same MCP server, same prompts).
The mini variant tends to call commit-style tools before evidence is
gathered — partially mitigated by the validate-or-reject pattern in
foundry-mcp-aca, but gpt-5.4 still fixes the root cause.
Cost/latency note. gpt-5.4 GlobalStandard at 50 capacity costs
a few cents per scenario at idle the deployment ticks negligibly,
so the typical pilot cost is dominated by the scenarios you actually
run. Don't downgrade to mini just to save on the deployment standby.
Full model catalog. For the complete May 2026 Foundry model selector
(task/modality tables, region availability tiers, embeddings, rerank,
image/video gen, Document AI, audio), see the
agentic-loop skill §
"Foundry Model Selector". That table is the single source of truth for
model selection across all awesome-gbb skills — we don't duplicate it here.
MUST: Copy verbatim from references/python/main.py. Do NOT redefine inline — the validator enforces single-source-of-truth. That file is the field-validated FoundryChatClient + Agent + ResponsesHostServer shape for a single-purpose hosted agent (one tool, no SkillsProvider, MAF 1.6.0).
Key points (why each line in main.py matters):
FOUNDRY_PROJECT_ENDPOINT is injected by the platform — never declare in azure.yaml
default_options={"store": False} — hosting platform manages conversation history
ResponsesHostServer handles liveness/readiness probes natively
DefaultAzureCredential resolves to the agent's dedicated Entra identity
- Custom tools use
@tool(approval_mode="never_require") with Annotated type hints
Canonical reference files for the rest of this skill (each one is the source of truth for its § below; SKILL.md prose never re-defines these):
⚠️ Deprecation: ChatAgent is gone in MAF 1.6.0
Any older examples showing from agent_framework import ChatAgent followed by ChatAgent(chat_client=...) are stale — no migration alias is exported. The class was renamed to Agent and the kwarg changed from chat_client= to client=. If you're copying code from pre-May-2026 prose or SKILLs, replace ChatAgent(chat_client=client, ...) with Agent(client=client, ...).
Skill Loading — SkillsProvider (recommended) vs. inline _load_skills() (legacy)
Authoritative source. Microsoft Agent Framework — Agent Skills (Python).
SkillsProvider is a first-class MAF Python class shipped in the same
agent_framework package as Agent, MCPStreamableHTTPTool, and @tool
(no extra install). It implements the agentskills.io
4-stage progressive disclosure pattern:
Advertise (~100 tokens/skill) → load_skill → read_skill_resource → run_skill_script.
Wired via context_providers=[skills_provider] on the Agent(...)
constructor — orthogonal to tools=[...].
Cross-link. The exact same SkillsProvider.from_paths(skills_dir)
wiring shape is what threadlight-local-test Pattern 0 — Quickstart
runs locally against a designed PoC — so the prompt context the local
demo agent sees is identical to what ships to Foundry.
⚠️ Separation of concerns (read this first)
When SkillsProvider is wired, _load_instructions() MUST contain only
the baseline orchestration prompt (copilot-instructions.md) plus
SPEC-derived constants (config.json: thresholds, enums, templates).
It MUST NOT also concat SKILL.md content. SkillsProvider advertises
each skill in ~100 tokens, then the agent calls load_skill(name) on
demand. Concatenating both double-counts every skill body, blows
out the static prompt, and silently defeats progressive disclosure.
Recommended — SkillsProvider context provider
⚠️ DO and DO NOT — API change in MAF 1.4.0 / 1.6.0:
- DO use
SkillsProvider.from_paths(skills_dir) (MAF 1.6.0 recommended, works on 1.4.0+)
- DO NOT use the legacy
SkillsProvider(skill_paths=skills_dir) constructor — it was removed in MAF 1.4.0 with no alias
If you see TypeError: SkillsProvider.__init__() got an unexpected keyword argument 'skill_paths' at container startup, replace SkillsProvider(skill_paths=...) with SkillsProvider.from_paths(...) immediately.
Defensive-init helper. The agent stays runnable with context_providers=[]
even if the skills directory is missing or a SKILL.md is malformed —
critical for ops, since a single broken skill file should not crash the
container at startup.
MUST: Copy verbatim from references/python/container.py. Do NOT redefine inline — the validator enforces single-source-of-truth. That file ships the canonical _init_telemetry() (guarded against O-012 AppIn injection failures) + _build_skills_provider() (returns None on missing/corrupt skills dir so the agent stays runnable) + the full FoundryChatClient / Agent / ResponsesHostServer wiring for a multi-SKILL hosted agent.
The pattern in one line: context_providers=[skills_provider] if skills_provider else [] on the Agent(...) ctor — and instructions=_load_instructions() returns base prompt + SPEC config ONLY (never concatenate SKILL.md bodies; that double-counts and defeats progressive disclosure).
Constructor variants.
SkillsProvider.from_paths(skills_dir) — classmethod, the form used in
production hosted agents. Use this.
SkillsProvider(skill_paths=skills_dir) — REMOVED in MAF 1.4.0.
Causes TypeError: SkillsProvider.__init__() got an unexpected keyword argument 'skill_paths' → container crashes before readiness → sticky
session_not_ready on every invocation. Always use from_paths().
The provider searches up to two levels deep, so skills/<name>/SKILL.md
and skills/<group>/<name>/SKILL.md layouts are both auto-discovered.
Per-query cost. SkillsProvider adds +1 load_skill tool call per
skill the agent activates per query (typically 1-3 per query). This is
NOT the 20-34-internal-call overhead seen on CopilotClient runs —
that overhead lives in CopilotClient itself and is unrelated to
SkillsProvider.
Quantified benefits (from production MAF deployments):
- ~75% token saving on the static prompt vs. concatenating all
SKILL.md bodies into instructions.
- Per-skill App Insights telemetry — every
load_skill(name) is a
traceable tool-call span, so routing analysis (which skills get used?
which never load? which load on the wrong intent?) is free.
Legacy alternative — inline _load_skills() concat
The pre-SkillsProvider pattern: read every skills/*/SKILL.md at
startup and append the bodies to the system prompt. Still supported,
still works, occasionally still the right choice.
def _load_skills() -> str:
chunks: list[str] = []
skills_dir = Path(__file__).parent / "skills"
for skill_file in sorted(skills_dir.glob("*/SKILL.md")):
chunks.append(
f"\n\n---\n\n# Skill: {skill_file.parent.name}\n\n"
+ skill_file.read_text(encoding="utf-8")
)
return "".join(chunks)
def _load_instructions_legacy() -> str:
base = (Path(__file__).parent / "copilot-instructions.md").read_text(encoding="utf-8")
return f"{base}\n\n{_load_skills()}"
agent = Agent(
client=client,
instructions=_load_instructions_legacy(),
tools=[my_tool, mcp_tool],
default_options={"store": False},
)
When to choose which
| SkillsProvider (recommended) | _load_skills() concat (legacy) |
|---|
| Per-query overhead | +1 load_skill per skill loaded (1-3/query typical) | None |
| Static prompt size | ~100 tokens/skill advertised | Full SKILL.md bodies always present |
| Total skill content >5 KB | ✅ recommended | ⚠️ blows up context budget every turn |
| Strict latency budget (sub-2s tools) | ⚠️ adds load_skill round-trip | ✅ faster cold path |
| Per-skill usage telemetry | ✅ free (every load_skill is a span) | ❌ invisible |
| New PoCs | ✅ default | only if total skill content is small (<5 KB) |
| Mixing both | ❌ never — double-counts every skill body | n/a |
Centralizing skills via the Foundry Skills REST API
The patterns above all assume SKILL.md files live in the agent's source
tree. If multiple Hosted agents in the same project need to share a
canonical set of SKILL.md files (without each repo carrying its own
copy), publish them to the project-level Foundry Skills store
({project}/skills) and consume them via either:
- Build-time bundle —
azd predeploy hook downloads all published
skills into src/skills/ so the standard SkillsProvider.from_paths(…)
above keeps working unchanged.
- Runtime fetch —
SkillsProvider(source=FoundrySkillsSource(…)) with
a custom SkillsSource that pulls from the REST API on session create.
See foundry-skill-catalog for the REST surface, the silent
JSON-mode-is-write-only trap, and the verified-working FoundrySkillsSource
adapter.
Multi-Agent: Calling Other Foundry Agents as Tools
Use the client-swap pattern to connect to existing prompt/hosted agents in the same project:
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.ai.projects.aio import AIProjectClient
from azure.identity import DefaultAzureCredential
project_client = AIProjectClient(
endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential(),
)
sub_client = FoundryChatClient(project_client=project_client, model="gpt-5.4-mini")
sub_client.client = project_client.get_openai_client(agent_name="my-sub-agent")
sub_agent = Agent(name="my-sub-agent", client=sub_client)
orchestrator = Agent(
client=FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model="gpt-5.4-mini",
credential=DefaultAzureCredential(),
),
instructions="You orchestrate sub-agents.",
tools=[sub_agent.as_tool()],
default_options={"store": False},
)
ResponsesHostServer(orchestrator).run()
Critical rules:
AIProjectClient MUST be from azure.ai.projects.aio (async) — the sync version returns a sync OpenAI client that silently fails inside FoundryChatClient
allow_preview=True is not required for agent_name-based get_openai_client() — verified against azure-ai-projects 2.3.0: the base-URL / query-param resolution the SDK uses for the agent endpoint doesn't gate on allow_preview (the constructor docstring is stale on this point; the actual code path isn't). Only opt into allow_preview=True if you need a genuinely preview surface (e.g. project.beta.* operations).
FoundryChatClient does NOT accept agent_name/agent_version — use the client-swap pattern
⚠️ Historical (MAF 1.1.1) — sub-agent delegation note.
FoundryAgent in MAF 1.1.1 silently hardcoded
extra_body={"agent_reference": ...} internally — the old initial-preview
pattern that silently failed against the refreshed preview ("Function
failed." on tool calls). As of MAF 1.8.0, FoundryAgent has been
rehabilitated: __init__ takes project_endpoint + agent_name +
agent_version directly and extra_body is only available as a user
opt-in via default_options. The 1.1.1-era prohibition no longer
applies; use FoundryAgent freely under MAF ≥ 1.8.0. The client-swap
pattern above remains the recommended path for sub-agent delegation
in this skill's container runtime (it composes cleanly with
FoundryChatClient's async surface); FoundryAgent is most useful
for caller-side orchestration code (see
§ FoundryAgent timeout parameter (MAF 1.8.0)).
Exact version of rehabilitation between 1.1.1 and 1.8.0 not determined.
MCP Tools via FoundryChatClient
🚫 DO NOT and DO — URL and Environment Variable Requirements:
- DO NOT ship
${ENV_VAR} placeholders in url parameters unless they are validated upstream to expand to a non-empty value. Empty expansion produces invalid_payload errors at runtime.
- DO reject any URL that is not
http:// or https:// before calling get_mcp_tool(url=...). Guard with if not url or not url.startswith(("http://", "https://")) to skip gracefully when the MCP server is unreachable or unconfigured.
🟡 Status: bug-009/014 FIXED in agent-framework-core 1.3.0
(released 2026-05-07 via PR #5581,
merged 2026-04-30). The fix prefers plain string entries, then
.text attribute, then entry["text"] for Mapping entries, then
json.dumps(output, default=str) final fallback — sidestepping the
str() fallback that produced [<Content object at 0x...>] Python
reprs from the canonical MCP raw-JSON shape. Companion fix
PR #5687
(also in 1.3.0) stops MCPStreamableHTTPTool from swallowing
asyncio.CancelledError when the MCP server is unreachable.
Both surfaces are now valid in 1.3.0+:
client.get_mcp_tool() — concise, hosted MCP shape, STATIC headers: dict[str, str] only
(no header_provider callback — bearer tokens expire after ~1h, so
this path is not viable for AAD-bearer-authenticated MCP servers
like AI Search KB MCP unless headers can be pinned for the agent's
lifetime; fine for API-key auth or unauthenticated MCP)
MCPStreamableHTTPTool + parse_tool_results — verbose,
client-side, supports header_provider: Callable[[dict], dict[str, str]]
for per-call token refresh — REQUIRED for AAD-bearer auth
Choose MCPStreamableHTTPTool whenever the MCP server uses
short-lived bearer tokens or you need per-request header logic; choose
client.get_mcp_tool() for static-header MCP servers when you want
the hosted MCP execution model.
Pre-1.3.0 versions (1.1.x, 1.2.x) still have the bug. If pinned
to an older release, stay on MCPStreamableHTTPTool + parse_tool_results.
mcp_tool = client.get_mcp_tool(
name="my-mcp",
url="https://my-mcp-server.azurecontainerapps.io/mcp",
headers={"Authorization": f"Bearer {long_lived_or_api_key}"},
approval_mode="never_require",
)
agent = Agent(client=client, tools=[my_tool, mcp_tool], ...)
URL must be a valid URI (starts with http:// or https://). Unresolved
${ENV_VAR} placeholders that expand to empty strings cause invalid_payload
errors at runtime.
MCP Tools — recommended pattern (MCPStreamableHTTPTool + parse_tool_results)
Use MCPStreamableHTTPTool directly with a custom parse_tool_results
callback that extracts the TextContent.text payload from the MCP
CallToolResult and surfaces it to the model as plain JSON. This
sidesteps FoundryChatClient.get_mcp_tool() entirely and avoids the
[<Content object>] repr leak.
Worked example — validated in recent pilots, running on a
10-tool MCP server with gpt-5.4-mini:
import json
from agent_framework import Agent, MCPStreamableHTTPTool, tool
from agent_framework.foundry import FoundryChatClient
from agent_framework_foundry_hosting import ResponsesHostServer
from azure.identity import DefaultAzureCredential
def _mcp_text_extractor(result):
"""Convert an MCP CallToolResult into the plain JSON string the model expects.
Without this callback, agent_framework's default rendering of MCP results
leaks the Python repr of `[<Content object>]` into the model's view, which
gpt-5.4-mini reads as a tool failure. We surface the first TextContent
payload (which the MCP server returns as `json.dumps(...)`) verbatim.
"""
if getattr(result, "isError", False):
return json.dumps({
"error": "mcp_tool_error",
"content": [str(c) for c in (result.content or [])],
})
for c in result.content or []:
text = getattr(c, "text", None)
if isinstance(text, str) and text:
return text
sc = getattr(result, "structuredContent", None)
if sc is not None:
return json.dumps(sc, default=str)
return json.dumps({"_empty": True})
client = FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=DefaultAzureCredential(),
)
mcp_tool = MCPStreamableHTTPTool(
name="<process>_mcp",
url=f"https://{os.environ['MCP_SERVER_FQDN']}/mcp",
approval_mode="never_require",
parse_tool_results=_mcp_text_extractor,
request_timeout=60,
)
agent = Agent(client=client, tools=[my_local_tool, mcp_tool], ...)
Why this works:
MCPStreamableHTTPTool is the lower-level MAF primitive that
client.get_mcp_tool() wraps — bypassing the wrapper avoids the
buggy renderer.
parse_tool_results is invoked by MAF on every tool result before
it's rendered into the model's history; returning a plain string
makes that string what the model sees verbatim.
- The MCP server should return
json.dumps(...) from each tool
handler (FastMCP wraps it as a single TextContent); the extractor
unwraps that back to the JSON string.
When pure-compute logic doesn't need to be on the MCP server,
inline it as @tool. The MCP-vs-@tool rule from recent pilots:
- MCP server for any tool with I/O (DB lookups, file reads,
external API calls).
- Inline
@tool for pure computation (date math, formatting,
validation) — cheaper round-trip, no HTTP hop.
Probe to verify the fix is in place — dump the raw
custom_tool_call_output from a real Foundry trace; the output
field should be a plain JSON string (e.g. {"case_id":"...","status":"..."}),
NOT [<agent_framework._types.Content object at 0x...>]. Your
process repo should ship tests/probe_mcp_output.py for this.
Status note. As of agent-framework-core 1.3.0 (2026-05-07,
PR #5581),
client.get_mcp_tool() is also bug-free — but only supports STATIC
headers (no per-call refresh callback), so it cannot be used for
AAD-bearer-authenticated MCP servers like AI Search KB MCP. The
MCPStreamableHTTPTool + parse_tool_results pattern remains the
canonical choice for any MCP server that needs short-lived tokens
via header_provider. Captured from recent PoC retrospectives.
MCP with per-call AAD bearer (header_provider)
⚠️ Bootstrap caveat (CRITICAL — read first). header_provider=
only covers call_tool() requests, NOT the MCP bootstrap exchange
(initialize + tools/list issued by _ensure_connected). For
AAD-secured MCP endpoints that require auth on bootstrap (Azure AI
Search Knowledge Base MCP, anything behind PMI), header_provider=
fails with 401 BEFORE the first call_tool ever fires — the
_mcp_call_headers ContextVar that header_provider writes to is
only set inside call_tool() (agent_framework/_mcp.py ~line 1589).
On hosted agents the symptom is every Responses request returning
server_error with no useful log signal. Use httpx.AsyncClient(auth=httpx.Auth)
via http_client= instead (companion example below) — see
foundry-iq SKILL § "KB access from a hosted MAF agent — three
routes" → Route B for the canonical pattern.
For MCP servers backed by Azure services that authenticate with
Microsoft Entra ID where bootstrap does NOT require auth (e.g.
some custom MCP servers that auth-gate per-tool but allow open
discovery), use the header_provider callback to mint a fresh
bearer token per tool invocation. Per the SDK's own docstring, the
callback "receives the runtime keyword arguments" — a generic
dict[str, Any] populated by MAF at tool-invoke time — and returns
a dict[str, str] of HTTP headers attached to every outbound request
during that tool call.
import os
from agent_framework import MCPStreamableHTTPTool
from azure.identity import DefaultAzureCredential
_credential: DefaultAzureCredential | None = None
def _get_credential() -> DefaultAzureCredential:
global _credential
if _credential is None:
_credential = DefaultAzureCredential()
return _credential
def _bearer_headers(_kwargs: dict) -> dict[str, str]:
"""MAF header_provider — mints a fresh AAD bearer per MCP call.
Cached by the credential layer; refreshed automatically on expiry.
"""
tok = _get_credential().get_token("https://search.azure.com/.default")
return {"Authorization": f"Bearer {tok.token}"}
kb_mcp = MCPStreamableHTTPTool(
name="my_kb_mcp",
url=f"{os.environ['AI_SEARCH_ENDPOINT']}/knowledgebases/<kb-name>/mcp"
f"?api-version=2025-11-01-preview",
approval_mode="never_require",
parse_tool_results=_mcp_text_extractor,
header_provider=_bearer_headers,
request_timeout=60,
)
Companion: httpx.Auth for AAD-bootstrap-required MCPs (CANONICAL)
When the MCP endpoint authenticates initialize / tools/list (any
Azure AI Search KB MCP, any PMI-protected MCP), supply auth at the
httpx transport layer instead — httpx.Auth.auth_flow() is invoked
on EVERY outbound request including bootstrap, so the agent's MCP
handshake completes successfully:
import os, httpx
from agent_framework import MCPStreamableHTTPTool
from azure.identity import DefaultAzureCredential
_credential = DefaultAzureCredential()
class _AADBearerAuth(httpx.Auth):
"""Mints AAD bearer per request — covers MCP bootstrap, not just call_tool."""
def __init__(self, scope: str) -> None:
self.scope = scope
def auth_flow(self, request: httpx.Request):
token = _credential.get_token(self.scope).token
request.headers["Authorization"] = f"Bearer {token}"
yield request
_http = httpx.AsyncClient(
auth=_AADBearerAuth("https://search.azure.com/.default"),
timeout=120.0,
)
kb_mcp = MCPStreamableHTTPTool(
name="my_kb_mcp",
url=(f"{os.environ['AI_SEARCH_ENDPOINT']}/knowledgebases/<kb-name>/mcp"
f"?api-version=2025-11-01-preview"),
http_client=_http,
parse_tool_results=_mcp_text_extractor,
load_prompts=False,
request_timeout=120,
)
Do NOT combine header_provider= AND http_client= (with auth).
Pick one. httpx.Auth is canonical for AAD-secured Azure MCP
endpoints; header_provider= is appropriate only for tool-call-only
headers (e.g. per-call correlation IDs that don't need to cover
bootstrap).
⚠️ MCP ping trap on Foundry-hosted MCP servers. MAF's
MCPStreamableHTTPTool._ensure_connected() issues an MCP ping
request during agent registration. The Foundry Toolbox MCP
endpoint is documented to return HTTP 500 on ping because the
server doesn't implement the optional MCP ping method (per the
Toolbox docs). The same failure mode has been observed on direct
KB MCP wiring (/knowledgebases/<n>/mcp) but is not yet documented
upstream — treat as suspected, not confirmed, until you've reproduced
it on your tenant. When the trap fires, agent registration fails and
every invoke returns server_error from the responses endpoint with
no useful log signal. Workarounds:
(a) Recommended (MAF 1.6.0+): set _ping_available = False in
__init__. MAF 1.6.0's base _ensure_connected() already
respects a _ping_available flag (set True by default in
MCPStreamableHTTPTool.__init__). Setting it to False makes the
base skip send_ping() without overriding _ensure_connected at
all — cleaner and survives internal attribute renames across MAF
versions (e.g. _client → client in 1.6.0 broke overrides that
referenced self._client):
class _PingSkipMCPTool(MCPStreamableHTTPTool):
"""Skip MCP ping — KB MCP / Toolbox MCP return 500 on ping."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._ping_available = False
(b) Legacy (pre-1.6.0): override _ensure_connected with a
no-op subclass to skip the ping entirely. ⚠️ This pattern broke in
MAF 1.6.0 when self._client was renamed to self.client — if you
reference internal attributes in the override, the agent crashes with
AttributeError on the first request (container starts fine,
readiness passes, but every actual invoke returns empty output: []
/ model: ""). Prefer option (a) on 1.6.0+.
(c) Use the static-headers client.get_mcp_tool() shape if your
MCP doesn't need per-call AAD refresh.
File Generation (@tool pattern)
Agents can generate downloadable files (XLSX, PDF, CSV, HTML) using a custom @tool.
Files are written to $HOME inside the container; the hosting platform exposes them
via the session files API, and the Teams bot delivers them via FileConsentCard
(see foundry-teams-bot skill).
@tool(approval_mode="never_require")
def save_report(
filename: Annotated[str, Field(description="Filename (e.g. report.xlsx, summary.pdf)")],
content: Annotated[str, Field(description="File content: CSV text, JSON for XLSX, HTML for pdf")],
format: Annotated[str, Field(description="Format: csv, xlsx, html, or pdf")] = "csv",
) -> str:
"""Save a report file that the user can download."""
from pathlib import Path
home = Path.home()
filepath = home / filename
if format == "xlsx":
import openpyxl
import json as _json
data = _json.loads(content)
wb = openpyxl.Workbook()
ws = wb.active
if data and isinstance(data, list):
ws.append(list(data[0].keys()))
for row in data:
ws.append(list(row.values()))
wb.save(str(filepath))
elif format == "pdf":
from fpdf import FPDF
import re
cleaned = re.sub(r"<style[^>]*>.*?</style>", "", content, flags=re.DOTALL)
cleaned = re.sub(r'\s+(class|style|id)="[^"]*"', "", cleaned)
cleaned = cleaned.replace("<table", '<table border="1" width="100%"')
cleaned = cleaned.replace("<th", '<th bgcolor="#d0d0d0"')
pdf = FPDF()
pdf.set_auto_page_break(auto=True, margin=15)
pdf.add_page()
pdf.set_font("Helvetica", "", 10)
pdf.write_html(cleaned)
pdf.output(str(filepath))
else:
filepath.write_text(content, encoding="utf-8")
return f"Report saved: {filename} ({filepath.stat().st_size:,} bytes). User can download it."
Required dependencies in pyproject.toml:
dependencies = [
"openpyxl>=3.1.0",
"fpdf2>=2.8.0",
]
Instructions directive — add to copilot-instructions.md:
## Report Generation
You have a `save_report` tool for generating downloadable files:
- **XLSX**: JSON array of objects → `save_report(filename="report.xlsx", content=json_data, format="xlsx")`
- **PDF**: HTML content → `save_report(filename="report.pdf", content=html, format="pdf")`
- **CSV**: Raw CSV text → `save_report(filename="report.csv", content=csv, format="csv")`
- **HTML**: HTML content → `save_report(filename="report.html", content=html, format="html")`
Key points:
fpdf2 is pure Python — no Playwright, wkhtmltopdf, or headless Chrome needed
fpdf2.write_html() renders HTML tables with borders and header highlighting
- Files written to
Path.home() are automatically available via the session files API
- The bot captures
agent_session_id from the response.completed event to locate files
- Wrap each format in try/except to return a clean error if generation fails
Validated — XLSX and PDF file delivery tested end-to-end in recent
file-delivery pilots. FileConsentCard renders clickable OneDrive cards in Teams.
Dependencies (pyproject.toml)
🔴 DO NOT install the agent-framework meta-package with >=. Pin individual sub-packages:
agent-framework-core~=1.11.0
agent-framework-foundry~=1.10.1
agent-framework-foundry-hosting==1.0.0a260709
The meta-package pulls transitive deps that may conflict. Pin what you need.
🚨 READ FIRST. Three pyproject mistakes silently break this stack:
agent-framework>=1.11.0 meta-package — non-deterministic transitive resolution; resolves differently across uv versions. DO pin agent-framework-core and agent-framework-foundry individually.
agent-framework-core[mcp] extra — that extra does NOT exist. MCPStreamableHTTPTool / MCPSseTool / MCPStdioTool are top-level exports of agent_framework; the bare agent-framework-core~=1.11.0 pin already includes them. Writing [mcp] produces a uv warning but does NOT fail resolution, so the pyproject can ship looking "MCP-ready" while operators chase phantom problems.
- Missing
mcp — agent_framework_foundry_hosting._responses imports from mcp import McpError at module-load time, so the container crashes at startup with ModuleNotFoundError: No module named 'mcp' even when no MCP tool is wired. The platform surfaces this as session_not_ready after a ~60 s timeout, so diagnosis cost is high. Pin mcp~=1.28.1 in every hosted-agent pyproject.toml.
MUST: Copy verbatim from references/python/pyproject.toml. Do NOT redefine inline — the validator enforces single-source-of-truth. That file pins agent-framework-core~=1.11.0, agent-framework-foundry~=1.10.1, the alpha hosting at exact ==1.0.0a260709, mcp~=1.28.1 (mandatory — see READ FIRST callout above), python-dotenv~=1.2.2, and azure-identity~=1.25.3. The header comment captures the three pitfalls (meta-package, phantom [mcp] extra, missing mcp) the file prevents.
Do NOT use agent-framework>=1.11.0 as a meta-package. The meta-package's transitive
resolution is non-deterministic across uv versions. Pin agent-framework-core~=1.11.0 and
agent-framework-foundry~=1.10.1 (PEP 440 compatible-release caps) instead, and pin the
alpha hosting package by exact version ==1.0.0a260709 — pre-release cap math doesn't
survive across alpha boundaries, so ~= would silently jump to a later alpha (this is a
recurring policy bug in this skill's own pin-validation script — see
references/upstream-pin.md for the fix). Verified
working on linux/amd64 as the current reference shape.
Simplified deps. The hosting package bundles microsoft-opentelemetry
which transitively pulls ALL OTel instrumentors (openai-v2, agents-v2, httpx, logging,
fastapi, etc.) and the Azure Monitor exporter. Remove any explicit
azure-monitor-opentelemetry, opentelemetry-sdk, opentelemetry-instrumentation-*
lines from your pyproject — they are now transitive and declaring them explicitly causes
version conflicts.
Mandatory adjacent rules (lessons from dependency-resolution retrospectives):
- Drop any explicit
azure-ai-agentserver-responses line — agent-framework-foundry-hosting
pins the right transitive itself; declaring it explicitly causes uv to resolve a stack that
passes install but crashes at first invocation with opaque server_error/model:"".
- Add explicit
mcp~=1.28.1 — mandatory, not conditional. agent_framework_foundry_hosting._responses
imports from mcp import McpError unconditionally (module-level), so the container crashes
at startup with ModuleNotFoundError: No module named 'mcp' even when the agent uses no MCP
tools. The platform surfaces this as session_not_ready after a ~60 s timeout (not as an
import error), so the diagnosis cost is high — pin mcp in every hosted-agent
pyproject.toml. Verified on agent-framework-foundry-hosting==1.0.0a260709.
- Do NOT write
agent-framework-core[mcp]. The [mcp] extra does NOT exist in
agent-framework-core (PEP 503 / setup.cfg of the published wheel has no
[project.optional-dependencies] mcp = […] entry). MCPStreamableHTTPTool /
MCPSseTool / MCPStdioTool are top-level exports of agent_framework — pin
agent-framework-core~=1.11.0 (plain, no extras) and import them with
from agent_framework import MCPStreamableHTTPTool. Writing the non-existent extra
produces a uv warning but does not fail resolution, so a pyproject can ship looking
"MCP-ready" while actually missing nothing (the transports are already there) — but the
warning suggests something's wrong and operators chase the wrong thing.
- Include
[tool.setuptools] packages = [] for clean uv resolution.
prerelease = "if-necessary-or-explicit" is correct — packages with explicit
prerelease markers (e.g. ==1.0.0a260709) resolve to prereleases; everything else
stays GA. Do NOT use "allow" — it can pull an unpinned beta azure-identity.
Dependency Chain (verified on PyPI)
| Package | Version | Type | Pulls in |
|---|
agent-framework-core | 1.11.0 | ✅ Stable | pydantic, opentelemetry-api (instrumentation enabled by default) |
agent-framework-foundry | 1.10.1 | ✅ Stable | core, openai, azure-ai-projects |
agent-framework-foundry-hosting | 1.0.0a260709 | ⚠️ Alpha (pinned exact) | agentserver-core, agentserver-responses (transitive — pinned by the hosting alpha). agentserver-core pulls microsoft-opentelemetry (bundles all OTel instrumentors + exporters) |
mcp | ~=1.28.1 | ✅ Stable | Required by every hosted agent — agent_framework_foundry_hosting._responses imports from mcp import McpError unconditionally, even when no MCP tools are used. Not auto-pulled by core |
azure-identity | ~=1.25.3 | ✅ Stable | |
No override-dependencies needed — the hosting package pins its own transitive deps.
Dockerfile
MUST: Copy verbatim from references/docker/Dockerfile. Do NOT redefine inline — the validator enforces single-source-of-truth. That file is the canonical, field-validated container image for a hosted agent (uv-based dependency sync, port 8088, remote-buildable via azd deploy).
- Port 8088 is the standard Foundry agent port
--platform linux/amd64 only needed for local builds
azd deploy builds remotely in Azure Container Registry by default — no local Docker needed. Use azd ai agent run when you intentionally want to build and run the agent locally.
azure.yaml (unified hosted-agent configuration)
The old two-file contract (agent.yaml + agent.manifest.yaml) is
gone. A single azure.yaml at the project root is now the source of
truth for both the Foundry project/model deployment and the hosted
agent itself — declared as a graph of services, each with a host
field and a uses list of dependencies.
MUST: Copy verbatim from references/yaml/azure.yaml. Do NOT redefine inline — the validator enforces single-source-of-truth. That file is the canonical azure.ai.project + azure.ai.agent service pair for a container-built hosted agent connecting to a pre-provisioned Foundry project.
name: my-agent-project
requiredVersions:
extensions:
azure.ai.agents: '>=1.0.0-beta.4'
services:
ai-project:
host: azure.ai.project
endpoint: ${FOUNDRY_PROJECT_ENDPOINT}
my-agent:
host: azure.ai.agent
project: .
language: docker
uses:
- ai-project
kind: hosted
name: my-agent
protocols:
- protocol: responses
version: 2.0.0
environmentVariables:
- name: AZURE_AI_MODEL_DEPLOYMENT_NAME
value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
container:
resources:
cpu: "1"
memory: 2Gi
infra:
provider: microsoft.foundry
Critical rules
| Rule | Why |
|---|
kind: hosted on the azure.ai.agent service | Marks it as a containerized/source-built hosted agent (as opposed to a Foundry Agent Service prompt agent) |
Protocol version 2.0.0 | Current GA protocol version. "v1" and "1.0.0" are historical preview values from before the two-file contract was retired — see § Migration from Initial Preview |
container.resources nested object | NOT a top-level resources: list |
NO FOUNDRY_PROJECT_ENDPOINT in environmentVariables | Reserved — the platform injects it automatically, along with FOUNDRY_PROJECT_ARM_ID, FOUNDRY_AGENT_NAME, FOUNDRY_AGENT_VERSION, FOUNDRY_AGENT_SESSION_ID, and APPLICATIONINSIGHTS_CONNECTION_STRING. Declaring any of them yourself is redundant at best and risks shadowing the platform value |
uses: [ai-project] on the agent service | Forms the dependency graph azd resolves at provision/deploy time — the agent can't reference the project's model deployment without it |
Connect to an existing project via endpoint: | Set services.ai-project.endpoint from ${FOUNDRY_PROJECT_ENDPOINT} to reuse a pre-provisioned Foundry project instead of having azd provision a new one; omit deployments: you don't want azd to manage |
infra.provider: microsoft.foundry | Selects the Foundry extension's bicep-less provisioning provider for the unified services graph; it does not rename or replace the azure.ai.agents extension |
One schema, no scaffold-time mustache. Unlike the old two-file
contract, there is no separate {{VAR}}-templated companion file.
${VAR_NAME} in azure.yaml resolves client-side from your azd
environment (.azure/<env>/.env) at azd provision/deploy time;
${{ connections.<name>.<path> }} resolves server-side, at sandbox
start, from a Foundry project connection (e.g. secrets) — see the
azure.yaml reference
for the full field list, including azure.ai.connection and
azure.ai.toolbox services this skill doesn't otherwise cover.
Foundry provider is bicep-less. infra.provider: microsoft.foundry selects the Foundry extension's provisioning
provider, which synthesizes infrastructure from the services graph
at azd provision time; there is no local infra/ directory. If you
explicitly eject infrastructure with azd ai agent init --infra or
--infra=terraform, the generated project switches the provider to
bicep or terraform and uses those local files instead.
Guided init vs. direct-copy brownfield deploy
The recommended guided path adopts the manifest and wires the existing
project/registry context into the active azd environment:
PROJECT_ID="/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>"
azd ai agent init -m ./azure.yaml \
--project-id "$PROJECT_ID" \
--deploy-mode container
If you instead copy the canonical files into an existing repository and run
azd deploy directly, azure.yaml alone is insufficient. The
azure.ai.agents service target requires the full project ARM ID, project
endpoint, subscription, and bare ACR login server in the active azd
environment:
azd env set AZURE_SUBSCRIPTION_ID "<sub-id>"
azd env set FOUNDRY_PROJECT_ENDPOINT \
"https://<account>.services.ai.azure.com/api/projects/<project>"
azd env set AZURE_AI_PROJECT_ID \
"/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>"
azd env set AZURE_CONTAINER_REGISTRY_ENDPOINT "<registry>.azurecr.io"
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME "<model-deployment>"
azd deploy my-agent --no-prompt
AZURE_CONTAINER_REGISTRY_ENDPOINT is a bare hostname — no
https:// scheme and no trailing slash. This direct-copy limitation is
the adoption wiring covered by
Azure/azure-dev #8981:
azd ai agent init ... --deploy-mode container wires it automatically;
plain azd deploy cannot infer the omitted brownfield values.
Installing the azd Foundry extensions
azd ext install microsoft.foundry
microsoft.foundry is a beta meta-package — it contributes no
commands of its own. Installing it pulls in every individual Foundry
extension: azure.ai.agents (azd ai agent, the one this skill uses),
azure.ai.connections, azure.ai.inspector, azure.ai.projects,
azure.ai.routines, azure.ai.skills, and azure.ai.toolboxes. It is
not a rename or replacement of azure.ai.agents — the agent
extension still owns the azd ai agent command group, the
azure.ai.agent service host value, and the microsoft.foundry
provisioning identifier azd uses internally when it synthesizes
infrastructure. Installing just azure.ai.agents also pulls in
azure.ai.inspector (a dependency), so it's a valid narrower
alternative to the meta-package:
azd ext install azure.ai.agents
azd ext upgrade microsoft.foundry
Verify what's installed:
azd ext list --output json
Model Version Lookup
| Model | Version |
|---|
gpt-5.6-sol | 2026-07-09 |
gpt-5.6-terra | 2026-07-09 |
gpt-5.6-luna | 2026-07-09 |
gpt-5.4 | 2026-03-05 |
gpt-5.4-mini | 2026-03-17 |
gpt-5.4-nano | 2026-03-17 |
gpt-5.3-codex | 2026-02-24 |
gpt-5.2 | 2025-12-11 |
gpt-5 | 2025-08-07 |
gpt-5-mini | 2025-08-07 |
gpt-4.1 | 2025-04-14 |
gpt-4.1-mini | 2025-04-14 |
GPT-5.6 GA boundary (verified 2026-07-15). Microsoft publishes
gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna as three distinct
model IDs at version 2026-07-09. All three are GA and have a documented
retirement date of 2027-07-09. They share a 1,050,000-token context
envelope (922,000 input + 128,000 output), accept text and image input,
produce text output, and support both the Responses API and Chat
Completions API. gpt-5.6 and gpt-5.6-mini are not documented Azure
model IDs - do not invent either alias. Sources:
model catalog
and retirement schedule.
Verify with: az cognitiveservices account list-models --resource-group <rg> --name <account> -o table
Which model/version/capacity/SKU to put in the block above → references/model-selection.md.
azd env variables for the azure.ai.agents extension
azd ai agent init is bicep-less by default — it doesn't write an
infra/ directory; azd synthesizes infrastructure from the
services graph in azure.yaml at azd provision time. Guided init
writes the brownfield values below. A direct-copy deploy must set them
explicitly before azd deploy:
| Env var | Set via | Format | Why it matters |
|---|
AZURE_SUBSCRIPTION_ID | azd env set or guided init | subscription GUID | Selects the subscription containing the existing project and registry |
FOUNDRY_PROJECT_ENDPOINT | azd env set or guided init | https://<account>.services.ai.azure.com/api/projects/<project> | Deploy-time project lookup for direct-copy brownfield deploy; also injected by Foundry into the running container, but MUST NOT be listed under environmentVariables |
AZURE_AI_PROJECT_ID | azd env set or guided init | /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project> | Full ARM ID required by ensureFoundryProject; an endpoint alone is insufficient |
AZURE_CONTAINER_REGISTRY_ENDPOINT | azd env set or guided init | <registry>.azurecr.io | Bare ACR login server used for container build/push; no scheme or trailing slash |
AZURE_AI_MODEL_DEPLOYMENT_NAME | azd ai agent init records it automatically from the deployment you select; change later with azd env set | deployment name (e.g. gpt-5.4-mini) | Projected through the agent service's environmentVariables entry in azure.yaml, and read by container.py / main.py as os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"] |
azd provision writes these into .azure/<env>/.env. If you eject
Bicep (azd ai agent init --infra), the generated main.bicep exposes
the equivalent outputs and you rarely need to hand-wire them.
Deploy-time azd env vs. runtime container env.
FOUNDRY_PROJECT_ENDPOINT MUST be set in the active azd environment for
direct-copy brownfield deploy, but MUST NOT be projected through the
agent's environmentVariables list. Foundry injects it into the running
container. The same container-only prohibition applies to
FOUNDRY_PROJECT_ARM_ID, FOUNDRY_AGENT_NAME,
FOUNDRY_AGENT_VERSION, FOUNDRY_AGENT_SESSION_ID, and
APPLICATIONINSIGHTS_CONNECTION_STRING.
Identity & RBAC
Agent Identities
Each hosted agent gets a dedicated Microsoft Entra agent identity
(a service principal) at deploy time — this is what your running
container uses to call models and tools. azd, the VS Code Foundry
Toolkit, or the SDK/REST create it automatically; you don't configure
managed identities by hand. View it with azd ai agent show.
⚠️ ServiceIdentity Cosmos limitation. The agent identity's
principal type is not accepted by every data-plane RBAC engine —
Cosmos DB in particular rejects direct role assignments against it
with unsupported type [Unfamiliar]. The same has been seen against
Azure AI Search data-plane in some configurations.
Workaround: route Cosmos / Search access through an MCP server
backed by a separate User-Assigned Managed Identity (UAMI). Grant
the data-plane role to that UAMI, give the agent only the MCP tool;
the agent never touches the data plane directly.
Per-instance identity for Graph API. When a hosted agent needs its own
audit-distinct identity — calling Microsoft Graph (Calendar, Mail, Teams),
performing on-behalf-of (OBO) flows, or operating cross-tenant — the
standard project-scoped agent identity is not enough. The official
entra-agent-id skill documents the fmi_path token-exchange pattern,
app-registration setup, and the ai.azure.com token-scope requirement.
Default case: no agent role assignment needed
Per the Hosted agent permissions reference:
"The agent identity can access model inferencing through the project
endpoint and session storage by default." Implicit access covers:
- Model inferencing through the project endpoint (
FoundryChatClient(project_endpoint=..., credential=DefaultAzureCredential()) — the standard pattern in this skill)
- Session storage read and write
No explicit role assignment or additional configuration is needed
for the standard case documented in § Runtime Pattern.
There is no postdeploy RBAC-grant hook to run, and no canonical
account-scope Foundry User grant for the agent identity in this
skill's default path — both were removed as part of the GA migration.
If you were relying on references/bash/postdeploy-agent.sh or a
manual Foundry User grant to the agent's instance_identity /
blueprint.principal_id from a pre-GA build of this skill, delete
that step; it's not just unnecessary now, it's dead weight.
Deploying user still needs a role to create/update the agent:
| Role | Scope | Why |
|---|
Foundry Project Manager | Foundry project | Data-plane permissions to create/update agents, plus the ability to create role assignments for the agent identity if an advanced scenario needs one |
azd and the VS Code Foundry Toolkit handle the ACR pull role
(Container Registry Repository Reader on the project's managed
identity) automatically as part of azd deploy.
Agent access beyond defaults (advanced scenarios only)
Additional permissions are only needed when an agent uses connected
tools that access external resources, references data outside its
own project, or calls account-level capabilities directly (bypassing
the project endpoint). Grant the least-privilege role for the
specific capability actually called — don't reach for a broad role by
default:
| Scenario | Role | Scope | Notes |
|---|
| Agent calls the account-level OpenAI endpoint directly (bypassing the project endpoint) | Cognitive Services OpenAI User | Foundry account | Covers only OpenAI data actions — narrower than Foundry User |
| Agent calls account-level non-OpenAI capabilities directly (Speech, Content Safety, Vision, Document Intelligence, Language, Translator) | Cognitive Services User | Foundry account | Covers those capabilities without granting OpenAI/agents access |
| Agent needs both of the above with a single grant | Foundry User | Foundry account | Broadest of the three — use only if the agent genuinely needs both OpenAI and non-OpenAI account-level capabilities |
| Custom project-level access beyond the implicit default | Custom role scoped to Microsoft.CognitiveServices/accounts/AIServices/responses/* + agents/*/read + agents/*/write | Foundry project | Lower privilege than the built-in Foundry User role for the project-level inferencing case; requires Microsoft.Authorization/roleAssignments/write at the project scope to create |
Because a role assignment against the agent identity can't be created
until after the agent exists, the user/principal that creates the
agent also needs permission to assign roles. Foundry Project Manager
at the project scope is the recommended assignment for agent creators
in this scenario — it includes both the data-plane permissions and
the ability to assign the Foundry User role.
Deployment Flow
azd up
├── provision → synthesize infra from azure.yaml services (Foundry
│ project + model deployment, ACR, monitoring) — bicep-less by
│ default; use ejected Bicep/Terraform if you materialized infra
├── postprovision hooks (if any)
├── azd ai agent extension →
│ ├── Build container remotely via ACR (default)
│ ├── Push image + create hosted agent version
│ └── Agent gets a dedicated Entra identity with implicit access
│ to model inferencing + session storage — no RBAC grant step
└── deploy other services (bot, MCP servers, etc.)
Invocation
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential
project = AIProjectClient(
endpoint="<project_endpoint>",
credential=DefaultAzureCredential(),
)
oai = project.get_openai_client(agent_name="my-agent")
response = oai.responses.create(input="Hello!", stream=False)
print(response.output_text)
allow_preview=True is not required for this call (see the
Multi-Agent § note above for the SDK-level detail). Or via CLI:
azd ai agent invoke "Hello!". Or via REST:
POST {project_endpoint}/agents/{name}/endpoint/protocols/openai/responses?api-version=v1
Compute Lifecycle
- Automatic — no manual start/stop
- Provisions on first request
- Deprovisions after 15 minutes of inactivity
- No replica management
Container status API. The .../versions/{v}/containers/default
endpoint (and the :start action) is not a supported public status
surface for hosted agents — the platform auto-provisions containers.
If your deploy or eval script polls a container-status endpoint to
check readiness, skip straight to a warmup chat instead. Pattern:
attempt a responses.create("ping") warmup with retry/backoff. Poll
the version's status field (creating → active / failed) via
project.agents.get_version(...) or azd ai agent show if you need
a structured readiness signal.
Region Availability
Not all regions support hosted agents. If you get "The requested experience is not available for this subscription", try a different region.
Snapshot — 2026-07-14. Per the authoritative
Region availability
page, Hosted agents are currently available in 20 regions: East US 2,
North Central US, Sweden Central, Canada Central, Canada East,
Southeast Asia, Poland Central, South Africa North, Korea Central,
South India, Brazil South, West US, West US 3, Norway East, Japan
East, France Central, Germany West Central, Switzerland North, Spain
Central, Australia East. This list is not evergreen — Microsoft
updates it as more regions come online. Re-check the live page before
you commit a region; East US 2 in particular used to fail (a stale,
now-outdated claim from an earlier preview build of this skill) and
is on the current list.
Verify current model + hosted-agent availability together before you
commit a region:
az cognitiveservices account list-models --resource-group <rg> --name <account> -o table
az cognitiveservices model list --location <region> -o table
Region choice interacts with model + capacity availability and data residency — see references/model-selection.md §§ 2–4.
Gateway throttle finding (historical, pre-GA — unverified post-GA)
Status: historical. The finding below was observed during the
May 2026 preview era, before container hosted-agent deploy reached
GA. It has not been re-verified against the current GA gateway.
Treat it as a diagnostic pattern to recognize if you see the same
symptoms — not as production sizing guidance for a GA deployment. If
you reproduce it post-GA, that's worth a fresh, dated finding of its
own; don't assume the ~6-call boundary still applies.
The Foundry hosted-agent gateway had an undocumented preview-time
throttle: ~6 sustained sequential invocations per warm period
before sticky internal_server_error responses with a 5–10 minute
recovery window. Validated in May 2026 against a real pilot agent
across 3 successive eval runs, pre-GA.
Detection signature
Look for ALL of these together:
- Cold-start: first 1–2 warmup pings return
server_error in 5–10s
before the platform brings a replica up
- Warm phase: exactly 5–6 sequential invocations answer in 30–60s
each (real tool work) with normal latency
- Throttle phase: from invocation 7–9 onwards, every call returns
internal_server_error (or server_error / model:"") in 5–10s
consistent latency — too fast for real tool work, too slow for a
4xx routing failure
- Recovery: 5–10 min idle restores normal operation (no manual
restart, no replica scale, no env change required)
What's NOT the cause
Each lever was independently bumped in a controlled test (single pilot
agent, 30-scenario eval) and the boundary did NOT move:
| Lever | Before | After | Effect on 6-call boundary |
|---|
| Container CPU | 0.25 | 1.0 | none |
| Container memory | 0.5Gi | 2Gi | none |
| Replicas | 1 (no scale) | min=1 max=3 | none |
| Model TPM | 50K | 300K | +1 answered scenario only |
| Pacing between invocations | 5s | 30s | none |
| Invocation retries | 1 | 3 | retry burst makes it WORSE |
The boundary is gateway-side, not container-side. Container restart,
replica swap, and azd deploy agent all leave the throttle window
unchanged.
Mitigation (eval / workshop posture)
- Run evals in 5-scenario batches with 5-minute cooldown between